经典三问
Q: 位置环是什么?
A: 位置环用累计编码器脉冲表示当前位置,并根据目标位置误差产生运动输出。
Q: 为什么位置控制需要停止条件?
A: 到达目标后若仍持续输出,电机就会在误差和噪声作用下抖动或越过机械限位。
Q: 位置环常见使用场景?
A: 舵机替代机构、云台转角、轮子定量移动和机械结构的回零定位。
速度环稳定后,给它一个新问题:左轮向前走 300 个编码器脉冲,右轮也走 300 个,然后停在那儿。
这句话和"让它转得快一点"完全不是一回事。速度环每 20ms 看一小段脉冲,位置环要把这些小段一路记下来。轮子已经走过的距离不能丢,下一次控制要知道自己离终点还剩多少。
位置从哪里来
速度环读到的是一段时间里的增量:
text
第 1 个 20ms:+12
第 2 个 20ms:+18
第 3 个 20ms:+17
当前位置: 47
代码只多了一个累加器:
c
int32_t left_position = 0;
left_delta = EncoderDual_ReadLeftDelta();
left_position += (int32_t)left_delta;
右轮独立保存一份 right_position。两轮实际转速不一样时,位置差会逐渐显出来;共用一个"平均位置"会把偏差藏起来,最后车身斜着停。
上电时位置从 0 开始,代表"现在就是起点"。这不是绝对坐标。断电再上电,编码器不知道轮子之前停在哪,除非另有零点开关、机械标记或掉电保存。第一次实验把车轮摆在容易辨认的起点,复位后再给目标。
目标、误差和 PWM
位置环的误差变成:
c
error = target_position - current_position;
目标是 300,当前位置是 120,误差为 180,输出朝前;当前位置越过 300,误差变负,输出应该朝后收回来。位置 PID 给出的 PWM 仍然要经过方向和限幅,电机驱动接口不需要知道这是速度环还是位置环。
工程使用位置式 PID。它每轮根据当前误差、累积误差和误差变化,重新算出一份完整 PWM:
c
left_output = PID_Update(&left_pid,
POSITION_PID_TARGET_PULSE,
left_position);
真正跑在控制循环里的不是这一行,而是一层薄包装 PositionPID_Update():它先判误差带,没进带才调上面这个 PID_Update()。
这里先把 Ki 设成 0。位置误差在目标附近会自然变小,积分很容易把电机推过终点;低速定位若出现持续偏差,再单独试很小的 Ki。位置环并不要求三个参数同时存在。
先让位置环保持安静
代码中的默认配置:
c
#define POSITION_PID_TARGET_PULSE 300L
#define POSITION_PID_ENABLE_MOTOR 0U
#define POSITION_PID_TOLERANCE 2L
它会初始化 OLED、两个编码器和两个 PID,但每个 20ms 调用 Motor_Stop()。OLED 还是速度环那篇的四行,只是内容换成了位置:左目标、左实际位置、右目标、右实际位置。
先手转轮子,观察左右实际位置是否按约定方向变化。手转一圈后数值不必刚好等于商品页标称值,减速比、倍频和磁极对都会影响读数;连续转动时,正负号和计数是否连续才是第一件要确认的事。
位置模式也继承速度模式的安全前提:电机正转时编码器必须给出正位置。方向宏在 encoder.c 与 motor.c 中保留,先用架空测试校准,再进入定位实验。
架空只是检查,落地才是调参
把车架空,开启 POSITION_PID_ENABLE_MOTOR,目标先改成很小的 30L。轮子没有压地,电机很容易动起来,能快速看出三个问题:
- 目标为正时轮子是否向前;
- 实际位置是否朝目标增加;
- 误差变小时 PWM 是否收回。
架空时看到它准确停在 30 附近,不代表放到地上就能停住。轮胎落地后有静摩擦,电机可能在 5% PWM 时仍然不动;位置环继续看到误差,但 Ki 等于 0 时输出停在 Kp × 误差 上不动,永远越不过起转门槛,轮子一直不转。负载、地面材质和左右轮胎压力都会改变这个门槛。
我会把调试分成两段:
- 架空只确认方向、计数、目标变化和停机路径,不把架空的 Kp 当最终参数。
- 落地后从很小目标开始,记录"能起转的最小输出"和到达目标后的停稳情况,再调 Kp、Kd。
这两段数据故意分开保存。把桌上调好的参数原样搬到地上,常见结果是电机根本不动,或者突然冲过目标。那是摩擦和负载换了,控制对象也换了。
到达目标后为什么要有误差带
有完整 Kp、Ki 的时候,编码器计数不会永远刚好等于 300。电机停下时可能停在 298,下一次控制又想补一点,轮子再滚到 302。若代码只写"等于目标才停",它可能永远等不到。
工程用了一个很小的误差带:
c
if ((error <= POSITION_PID_TOLERANCE) &&
(error >= -POSITION_PID_TOLERANCE))
{
PID_Reset(pid);
return 0.0f;
}
return 0.0f 是把这一侧输出置零,PID_Reset() 是把 PID 的状态清干净------清的就是结构体里那几个状态字段:
c
void PID_Reset(PID *pid)
{
pid->integral = 0.0f;
pid->prev_error = 0.0f;
pid->prev_prev_error = 0.0f;
pid->output = 0.0f;
pid->out_p = 0.0f;
pid->out_i = 0.0f;
pid->out_d = 0.0f;
}
进入 ±2 个脉冲就把该侧输出置零并清掉 PID 状态。误差带不是越大越好;它决定了定位精度。车轮直径、编码器分辨率和机械间隙都比"2"这个数字更值得先确认。需要更精确时缩小它,发现电机在目标附近来回点动时再把它放宽一点,并记录实际位置误差。
四个现场实验
只改目标距离
先从默认停机配置开始,确认 OLED 四行数值正常。只把 POSITION_PID_TARGET_PULSE 从 300L 改成 30L,保持电机关闭,手转轮子观察目标行和实际行之间的距离关系;恢复 300L。
这一步没有运动,却能确认目标单位确实是编码器脉冲,而不是厘米、PWM 百分比或某个隐藏的角度值。
让一个方向符号反过来
车架空、位置环电机保持关闭,打开 ENCODER_LEFT_REVERSED。只用手转左轮,观察左实际位置行是否整体变号------原来往正方向涨,现在往负方向走,右侧那行不受影响。恢复宏并重新手转确认。
再开启电机前,两个轮的正方向都必须通过这项检查。位置 PID 接到反号反馈后,误差会越走越大,输出会一直保持在错误方向;这时看起来像"位置环爆炸",根因仍然是反馈方向。
Kp 太大,目标附近来回找
落地低速运行,先用一组小 Kp、Kd 为 0 的参数,看实际位置行能否停在目标附近。只把 Kp 提高一档,重复同一个目标。若实际位置快速越过目标又倒退,OLED 上会看到实际位置行在目标附近来回跳:刚显示越过 300,下一次刷新又退回 296,电机声音也跟着前进、倒退地换向。恢复上一次稳定 Kp。
位置环的 Kp 太大,车轮会为了几脉冲误差反复换向。架空时这种现象更容易被放大,落地后的摩擦又可能把它暂时"粘住",两种场景的数字不应混进同一张参数记录。
目标到了,电机还拖着走
把 POSITION_PID_TARGET_PULSE 保持不变,只把 POSITION_PID_TOLERANCE 改成 0L,重新运行一次,记录目标附近的实际位置和 PWM 声音。不同硬件可能表现为来回点动、慢慢爬过目标或停在某个无法精确相等的位置。恢复为 2L,重复运行。
这个实验只改变停止判定,不改 PID 参数。它展示了误差带和"等于目标"之间的区别:编码器读数是离散的,机械系统又有摩擦和间隙,控制器需要一个可以停下来的范围。
最后橘猫说
现在的位置环直接把 PWM 当作位置控制输出,结构简单,适合第一次把"累计脉冲"跑通。负载变化大、低速爬行明显时,外层位置环会不断修改 PWM,却不知道电机当前速度是否平顺。
更完整的结构会把位置环输出变成速度目标,再由内层速度环负责让轮子跟住这个速度。位置环管"还差多远",速度环管"现在走多快",两层各自面对一个更小的问题。循迹误差接进来时,也会遇到同一个选择:直接写死左右差速,还是让误差经过一个有反馈的控制环。
先把当前位置、目标位置、误差带和落地摩擦记录清楚。控制算法的每一层,最后都要回到轮子真实走了多少。