ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32编码器PID距离闭环:2.5米定点停车调参实战

STM32编码器PID距离闭环:2.5米定点停车调参实战

简介:围绕STM32实现电机按指定距离精准停靠这一典型闭环控制需求,这份工程资料以PID算法与编码器反馈为主线,面向已掌握HAL库基础、希望打通「位置采集—误差计算—PWM输出」完整链路的嵌入式学习者与电控方向学生。包内共236个文件,以121个.h头文件与107个.c源文件为主体,涵盖STM32F4系列HAL库外设驱动、定时器编码器接口与PID控制模块的完整实现,另附工程配置文件、启动汇编文件、可烧录的hex固件及一键清理批处理脚本,压缩包约1.65MB,按库文件与用户代码分层组织,便于对照移植。已有1023人学习。读者可从中获得比例—积分—微分三环节的参数整定思路、编码器脉冲计数换算实际位移的方法、采样周期与控制量映射到PWM占空比的落地写法,以及超调抑制、积分饱和处理与过载保护等调试经验。

1. 走2.5米总差十几厘米:STM32距离闭环到底该控什么

小车要求从A点走2.5米停在标线内,开环PWM调好的占空比,换块电池就差出十几厘米;这是STM32控制行驶指定距离时最常见的翻车点。单靠延时和固定PWM,地面摩擦、电压跌落、轮胎打滑都会把误差放大。把编码器脉冲当里程尺,再用STM32 PID做位置闭环,才能把“走多远”变成可重复的事。这套方案适合差速小车、移动平台、课程设计里需要定点停车的场景,也适合已经会点灯但还没把闭环串起来的开发者。先明确一个边界:不是所有电机都值得上位置环,带编码器的减速电机是前提,没有测距反馈就只能做时间开环。

2. 把米换算成脉冲:STM32编码器测距的四个标定参数

距离控制的第一步不是调PID,而是把“米”变成MCU能读的脉冲。换算错一个轮径参数,后面PID再准也白搭:目标2.5米,实际走2.3米,积分项拼命补,最后要么冲过头,要么在终点附近来回蹭。常见做法是编码器A/B相接定时器编码器模式,读CNT增量,再按编码器线数、倍频、减速比、轮径换算出每米脉冲数。这一章把测距链路拆开,参数怎么填、标定怎么做、代码怎么累加,逐项落到可复现的步骤上。

2.1 编码器模式与定时器配置:别把正交解码写成普通计数

STM32的定时器编码器模式,核心是把TI1和TI2两路正交信号接到定时器输入,硬件根据相位关系自动加/减CNT,不需要在外部中断里软件判断方向。常见配置是预分频0、ARR设0xFFFF、编码器模式TI1 and TI2,这样CNT在0到65535之间循环,方向由硬件管。有人图省事用普通外部中断数A相脉冲,低速抖动时丢脉冲,正反转切换时多计数,最后里程像玄学一样飘。血泪经验是:只要电机带正交编码器,优先用定时器编码器模式,软件只读差值。

接线层面,A/B相最好加上拉电阻,线缆用屏蔽双绞,电机动力线和编码器线分开走。编码器电源和MCU共地,但电机地线上的大电流容易串噪声,编码器地单点接到控制板地。定时器输入滤波可以开一点,比如ICFilter设6到10,能压住毛刺又不至于把低速脉冲滤掉。ARR不要随意改小,16位定时器转一圈溢出一次,读差值用有符号int16_t处理;如果里程累计用int32_t,跑几十米也不会溢出。配置完先不转电机,手转轮子看CNT是加还是减,确认方向再进闭环。

2.2 轮径、减速比、脉冲数:把米换算成脉冲的公式与标定

每米脉冲数的理论公式是:编码器线数 × 倍频 × 减速比 ÷(π × 轮径)。以常见参数为例,编码器线数11、四倍频、减速比30、轮径65mm,算下来约6465脉冲/米。这个数决定位置环的误差单位:目标2.5米就是16162个脉冲,到位窗口±8mm就是±52个脉冲。轮径要用负载后的实际直径量,胎压不足或橡胶压缩会让有效轮径变小,理论值偏大。左右轮分别标定,不要图省事取平均,差速小车左右轮每米脉冲数差2%就能让直线跑偏。

参数符号示例值说明
编码器线数PPR11电机尾端编码器每转脉冲
倍频MULTI4定时器编码器模式四倍频
减速比GEAR30减速箱输入转数/输出转数
轮径D0.065 m负载后实测直径
每米脉冲PPM6465公式计算后实测修正

标定别只信公式。找一块平整地面,画3米线,手推车走完,读总脉冲,反算每米脉冲;重复三次取平均。再跑2.5米目标,看编码器里程和卷尺差多少,差2%以内算合格。地面滑、轮胎磨损、轮径变化都会让标定值漂,换场地或换轮胎后重新标一次,比调半天PID管用。

2.3 定时采样与速度估计:M/T法在低速段的取舍

位置环可以直接用累计脉冲,速度环才需要算速度。常见采样周期是10ms或20ms,用定时器中断置标志,主循环里读编码器差值。速度等于Δ脉冲除以Δt再除以每米脉冲数。低速时20ms内可能只读到几个脉冲,量化误差大,速度曲线像台阶;可以把窗口拉到50ms,或者用M/T法:低速测周期,高速测频率。简单工程里用20ms采样加一阶低通滤波就够,滤波系数α取0.3左右,太小响应慢,太大噪声进PID。

位置量不要做低通滤波。位置滤波会引入滞后,停车判定时实际已经到位,滤波值还没到,车继续走。速度滤波可以,位置直接用累计脉冲。采样周期必须和PID周期一致,不要在main里用HAL_Delay凑周期,delay会被串口打印、OLED刷新打断,周期一抖,微分项直接放大成噪声。用定时器中断给标志,主循环判断标志再计算,周期稳定在±1ms以内。

2.4 代码:编码器读取与里程累加

#include "stm32f1xx_hal.h" // 按实际芯片系列替换头文件 #define ENCODER_PPR 11.0f #define ENCODER_MULTI 4.0f #define GEAR_RATIO 30.0f #define WHEEL_DIAMETER_M 0.065f #define PI_F 3.1415926f static int16_t last_cnt = 0; static int32_t total_pulse = 0; static float pulses_per_meter = 0.0f; void Encoder_Init(void) { // 定时器编码器模式初始化略:TI1/TI2均触发计数,ARR=0xFFFF last_cnt = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); pulses_per_meter = ENCODER_PPR * ENCODER_MULTI * GEAR_RATIO / (PI_F * WHEEL_DIAMETER_M); } void Encoder_Update(void) { int16_t now = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); int16_t delta = (int16_t)(now - last_cnt); // 有符号溢出处理 last_cnt = now; total_pulse += delta; } float Get_Distance_M(void) { return (float)total_pulse / pulses_per_meter; } void Reset_Distance(void) { total_pulse = 0; last_cnt = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); }

这段代码的关键在int16_t delta = (int16_t)(now - last_cnt)。16位CNT从65535回到0时,直接相减会得到巨大正数,转成int16_t后自动变成-1,方向正确。total_pulse用int32_t,累计几十米不溢出。pulses_per_meter按公式算完不要死认,后面用卷尺标定后直接改这个值。左右轮各建一套结构体,别共用全局变量。Reset_Distance在每次起步前调用,把当前CNT存进last_cnt,避免起步第一拍把旧差值算进去。

3. STM32 PID 选型:位置式、增量式在距离环里的边界与整定顺序

距离环本质是位置PID:输入是剩余脉冲,输出是PWM或目标速度。单环位置PID结构简单,但起步冲击大、低速容易爬行;双环串起来稳,参数多一倍。选型不看哪个高级,看电机惯量、地面一致性和停车精度要求。位置式PID输出直接对应PWM,积分项物理意义清楚;增量式PID输出是PWM增量,适合执行器带积分特性的场景,但停车瞬间切零不如位置式直接。这一章把单双环选择、位置式代码、整定顺序和抗饱和处理讲清楚。

3.1 位置环和速度环串起来:双环还是单环

单环位置PID:误差乘Kp直接给PWM。走固定距离、地面不变、电机小惯量时够用。问题是起步时误差大,输出直接饱和,轮子猛窜;接近目标时误差小,PWM掉到死区以下,车不动,积分慢慢涨,最后突然冲一下。双环是位置PID输出目标速度,速度PID跟踪目标速度输出PWM。位置环只管“还剩多少”,速度环管“当前走多快”,减速段能按速度曲线收住,停车更平顺。

方案适合场景优点代价
单环位置PID小车型、短距离、地面固定参数少、代码短起步冲击、低速爬行
双环串级负载变化、要求平顺停车速度可控、抗扰好多一套速度环参数
位置环+速度前馈固定轨迹、重复定位响应快、误差小需要标定前馈系数

我一般会先做单环把方向、标定、停车逻辑跑通,再加速度环。直接上双环,两个环耦合,调参时不知道是谁在振。速度环的采样周期和位置环一致,20ms;速度反馈用滤波后的值,目标速度来自位置环输出限幅。位置环输出限幅设成最大线速度对应的脉冲/秒,别让位置环输出一个电机根本追不上的速度。

3.2 位置式PID代码与积分限幅

typedef struct { float kp; float ki; float kd; float integ; float integ_max; float out_max; float deadzone; float last_err; } PosPID; float PosPID_Calc(PosPID *p, float target, float actual, float dt) { float err = target - actual; p->integ += err * dt; if (p->integ > p->integ_max) p->integ = p->integ_max; if (p->integ < -p->integ_max) p->integ = -p->integ_max; float out = p->kp * err + p->ki * p->integ + p->kd * (err - p->last_err) / dt; p->last_err = err; // 死区补偿:小于启动PWM时补到能转 if (out > 0.0f && out < p->deadzone) out = p->deadzone; if (out < 0.0f && out > -p->deadzone) out = -p->deadzone; if (out > p->out_max) out = p->out_max; if (out < -p->out_max) out = -p->out_max; return out; }

误差单位是脉冲,输出单位是PWM计数值。integ_max按“最大积分贡献不超过输出限幅的一半”估,比如Ki=0.01、输出限幅1000,积分限幅给50000。deadzone实测:给不同PWM看轮子什么时候开始转,取刚好能转的值加10%。kd对编码器噪声敏感,距离环里先设0,有超调再加一点点。dt用实际定时周期,0.02秒,不要用循环里算出来的抖动值。

3.3 参数整定顺序:先P后I再D,但距离控制先看死区

整定前先测死区。把车架空,PWM从0慢慢加,记录轮子开始转的值;再放地上,记录能克服静摩擦起步的值。两个值取大的那个作为死区补偿。死区没补,Kp调大了车一窜一停,调小了根本不动,容易误判成PID参数问题。

整定顺序:Kp从0开始加,先让车能走到目标附近,允许有静差;然后加Ki消静差,积分限幅先设小,观察接近目标时有没有过冲;最后加Kd压超调,但距离环的微分对脉冲噪声很敏感,Kd一般给Kp的1/10到1/20,甚至不给。采样周期20ms时,Kp的量级可以按“误差1000脉冲时输出200到400”反推,即Kp=0.2到0.4。Ki按“10秒内消掉大部分静差”估,0.005到0.02起步。每改一次参数跑三次,看最大偏差和重复性,不要拿单次结果定参数。

阶段操作观察常见问题
死区PWM缓加,记录启动值轮子开始转的PWM死区随电量变
Kp从0加到能到终点是否爬行、是否冲过头Kp大起步猛
Ki小积分限幅加起静差是否消掉积分饱和过冲
Kd少量加或保持0超调是否减小噪声放大抖动

3.4 抗积分饱和与刹车段处理

位置式PID最容易翻车的地方是接近目标时积分还在涨。误差从1000脉冲降到100脉冲,积分项已经把输出顶到饱和,车到点了还在给PWM,直接冲过去。处理办法有两种:积分分离,误差大于阈值时不清积分但停止累加;或者误差小于阈值才允许积分。阈值按每米脉冲数的1%到2%取,2.5米目标、6465脉冲/米,阈值给150到300脉冲。刹车段另外处理:剩余距离小于刹车距离时,位置环输出切零或反向制动,不再让积分继续累。

刹车段不要直接PWM=0等滑行。H桥可以把下桥臂短接做能耗制动,或者给反向小PWM,但反向PWM容易过流,先确认驱动允许。更稳妥的做法是减速曲线提前收速度,让车到点时速度已经很低,再切零。积分项在刹车段强制清零,下次起步重新累,避免残留积分把车往后拽。

4. 停车不靠猜:减速曲线、到位窗口与制动补偿

距离到了但车还在滑,是机械惯性,不是PID没调好。编码器显示2.50米时,车可能已经滑到2.53米;如果按误差=0才切PWM,每次都过。停车判定要同时看剩余距离和当前速度,还要给制动留提前量。这一章讲梯形速度曲线的最小实现、到位窗口怎么定、主循环代码怎么串,以及打滑后怎么补偿。

4.1 减速段规划:梯形速度曲线的最小实现

梯形曲线分加速、匀速、减速三段。给定最大速度Vmax和加速度a,剩余距离s_rem对应的允许速度是sqrt(2×a×s_rem)。当这个值小于Vmax时进入减速段,目标速度按这个公式收;大于Vmax时保持Vmax。代码里不用真画梯形,每次循环算允许速度,限幅后给速度环。参数示例:Vmax=0.45m/s,a=0.8m/s²,减速距离Vmax²/(2a)=0.126米。2.5米目标,走到2.37米左右开始收速度,到点前速度已经降到很低。

减速段加速度要比加速段小,机械冲击小,编码器打滑也少。地面滑时把a降到0.5m/s²,减速距离拉长到0.2米,提前收速度。Vmax不要设到电机极限,留20%余量给PID调节。速度曲线只给目标,实际跟踪靠速度环,位置环仍然管剩余距离,两者不要互相打架。

4.2 到位窗口与刹车距离:阈值怎么定

到位窗口是“算到位”的误差带,不是机械公差。窗口太小,车在终点附近来回找,电机反复正反转;窗口太大,停得不准。按重复定位要求取,常见±5mm到±10mm,对应±32到±65脉冲。窗口内还要看速度,速度大于0.03m/s不算到位,继续刹车。只判位置不判速度,车滑过窗口又回来,状态机抖。

刹车距离实测:让车以Vmax跑,到某点切PWM=0,量滑行距离。这个距离包括采样延迟、执行器延迟和机械惯性。提前量=滑行距离+速度×采样周期。比如滑行0.04米,速度0.45m/s,20ms采样,提前量约0.049米。目标2.5米,实际在2.45米就开始切零。提前量随速度变,低速时小,高速时大,不要用固定值。

4.3 代码:距离闭环主循环与停车逻辑

#define TARGET_M 2.50f #define WINDOW_M 0.008f #define VMAX_MPS 0.45f #define ACC_MPS2 0.80f typedef enum { RUN, BRAKE, DONE } RunState; static RunState state = RUN; void Distance_Loop(void) { if (state == DONE) return; float s = Get_Distance_M(); float rem = TARGET_M - s; float v = Get_Speed_Mps(); if (state == RUN) { // 减速段允许速度 float v_allow = sqrtf(2.0f * ACC_MPS2 * fabsf(rem)); if (v_allow > VMAX_MPS) v_allow = VMAX_MPS; // 位置环输出目标速度,误差用剩余脉冲 float v_cmd = PosPID_Calc(&pos_pid, rem * pulses_per_meter, 0.0f, 0.02f); if (v_cmd > v_allow) v_cmd = v_allow; if (v_cmd < -v_allow) v_cmd = -v_allow; // 速度环跟踪目标速度 float pwm = PosPID_Calc(&spd_pid, v_cmd, v, 0.02f); Motor_SetPWM(pwm); // 进入刹车段:剩余距离小于提前量 if (fabsf(rem) < 0.05f) { state = BRAKE; } } else if (state == BRAKE) { Motor_SetPWM(0); // 或短接制动,按驱动能力选 if (fabsf(rem) < WINDOW_M && fabsf(v) < 0.03f) { state = DONE; Motor_SetPWM(0); pos_pid.integ = 0.0f; // 清积分,避免下次起步残留 spd_pid.integ = 0.0f; } } }

这段主循环把位置环和速度环串起来,位置误差乘pulses_per_meter换成脉冲,输出目标速度;速度环输出PWM。刹车段用固定提前量0.05米,实际按实测滑行距离改。state进DONE后不再计算,下次起步前要重置状态和里程。Motor_SetPWM里做死区补偿和限幅,不要在PID里直接写寄存器。刹车段切零还是短接制动,看H桥是否支持,短接制动电流大,先小占空比试。

4.4 打滑与地面补偿:为什么换场地就偏

编码器测的是轮子转了多少,不是车走了多少。驱动轮打滑时,编码器里程大于实际距离,车没到目标就判到位。地面从水泥地换到瓷砖,摩擦系数掉一半,同样的加速度起步就打滑。补偿办法:降低加速度,让轮子慢慢建立抓地;在驱动轮上加配重;标定打滑系数,实际距离除以编码器距离,得到0.95就按0.95修正目标脉冲。打滑系数不是常数,急加速和急减速时不同,只能作为粗补,不能当精确修正。

更稳的做法是限制加速度和最高速度,让轮子始终在附着极限内。2.5米短距离,Vmax降到0.35m/s,打滑明显减少,停车重复性反而好。轮胎表面清洁、地面无沙粒,比调PID有效。换场地后先跑三次看偏差方向,偏大是打滑,偏小是轮径或标定问题,别一股脑加Ki。

5. 避坑与排查:行驶距离控制里最容易翻车的五处

距离环调不通,多数不是PID公式写错,而是测距、执行器、供电、采样这些外围环节埋了雷。下面五条是血泪经验里出现频率最高的,每条按“现象→原因→解决”拆开,遇到问题先对照,别急着改Kp。

5.1 现象:每次停车位置随机差几厘米 → 原因:编码器计数溢出或方向抖动

现象是重复性差,有时2.48米,有时2.55米,误差方向随机。原因常见两个:定时器CNT16位溢出时差值没按有符号处理,过零点多算65535;或者A/B相受电机噪声干扰,方向位抖动,CNT在临界点来回加减。解决:读差值统一用int16_t,累计用int32_t;编码器线加屏蔽和上拉,定时器输入滤波开到6到10;电机PWM频率避开编码器采样频段,动力线和信号线分开走。还查一下编码器电源是否和电机共地,共地噪声大时加磁珠或单独LDO。

5.2 现象:PID输出饱和但电机不转 → 原因:死区未补偿或驱动使能没开

现象是串口看PID输出已经到限幅,轮子一动不动,手推一下才转。原因是PWM低于电机启动死区,或者驱动使能脚没拉高、刹车脚没释放。解决:架空测死区,记录能转的最小PWM,在输出后加死区补偿;检查驱动EN、DIR、BRAKE引脚电平,用万用表量电机端有没有电压;电源电流限流是否触发。死区随电池电压变,电量低时死区变大,补偿值要留余量。

5.3 现象:电池电压下降后距离变短 → 原因:开环PWM没做电压前馈

现象是满电走2.50米,跑十分钟后只走2.42米。PID输出是PWM占空比,电池电压从12.6V掉到11.0V,同样占空比电机扭矩和转速都降,积分项补得慢。解决:加电池电压采样,做前馈补偿PWM_out = PID_out * Vnom / Vbat,限幅后输出;或者速度环把实际速度跟紧,让位置环自动补。前馈不能代替闭环,但能减轻积分负担。电压采样加RC滤波,别把PWM噪声采进去。

5.4 现象:低速接近目标时来回蹭 → 原因:采样周期抖动或速度滤波太重

现象是车到终点前一顿一顿,正反转切换。原因是速度采样周期被串口打印拖到30ms以上,微分项放大;或者速度低通α太小,速度反馈滞后,PID以为还没到,继续给PWM。解决:PID计算放定时器中断置标志的主循环,串口打印降频到100ms一次;速度滤波α给0.3到0.5,别低于0.2;接近目标时积分分离,误差小于200脉冲停止积分累加。还查一下到位窗口是不是太小,±3mm窗口对普通小车太苛刻。

5.5 现象:换场地后打滑距离偏大 → 原因:轮胎附着不够或加速度太大

现象是编码器显示2.50米,卷尺量2.56米,车滑过头。原因是驱动轮打滑,编码器多算,或者减速段切零太晚。解决:降低加速度到0.5m/s²,Vmax降到0.35m/s;驱动轮加配重;轮胎擦干净;刹车提前量按实测滑行距离加0.01到0.02米。打滑严重时标定打滑系数,实际距离/编码器距离,按系数修正目标脉冲。不要用打滑系数硬补急加速工况,那是不归路。

6. 用串口波形验收:一次距离闭环的复现与调参记录

调距离环不能只看最后停在哪,要看过程。串口每20ms打一行目标距离、实际距离、剩余脉冲、速度、PWM,用波形工具看曲线,超调、积分饱和、刹车点一目了然。我一般会先做静态标定,再跑阶跃,最后跑重复定位。静态标定看每米脉冲准不准;阶跃看起步有没有猛窜;重复定位跑十次,记录最大偏差和标准差。验收表比感觉可靠。

验证项方法合格线看什么
静态标定手推3米读脉冲误差<1%左右轮分别标
阶跃响应0到2.5米超调<3cm起步PWM是否饱和
重复定位连续10次最大偏差<1cm偏差方向是否随机
电压跌落满电到低电距离变化<5mm前馈是否生效
打滑场景湿滑地面不冲过2.55米减速段是否提前
// 20ms定时打印,波特率115200,CSV格式方便波形工具解析 printf("%.3f,%.3f,%.3f,%.3f,%d\r\n", HAL_GetTick() / 1000.0f, TARGET_M, Get_Distance_M(), Get_Speed_Mps(), (int)Motor_GetPWM());

打印里时间用秒,目标距离固定,实际距离和速度保留三位小数,PWM取整。串口打印本身会占时间,放在低优先级任务或主循环里判断标志,别在定时器中断里printf。波形上看三个点:起步段PWM是否瞬间顶到限幅,如果是,Kp大了或加速度给猛了;减速段速度是否按sqrt曲线收,如果提前掉到零,刹车点太早;到位段实际距离是否在窗口内稳住,如果来回穿,窗口太小或速度环振荡。调参记录写清楚每次改了什么、跑了多少次、最大偏差多少,下次复现不用从头猜。

从那以后我每次调距离环,都先拿卷尺量三米,再跑十次取最大偏差,不拿单次结果当结论;标定表、PID参数和串口打印格式都固定下来,换车只改轮径和减速比。这套距离闭环的工程模板和调参记录我整理在资源包里,按第2章的公式改参数、按第4章的状态机搭主循环就能复现;需要的话去对应下载页取。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进