轮式编码器这东西,看着不起眼,但在移动机器人项目里,里程估计十有八九都要靠它起步。我做过几个AGV和巡检机器人项目,从最初拿编码器数脉冲做速度环,到后来用它做定位先验,踩过的坑比想象中多得多。这篇就围绕“基于轮式编码器的里程估计”展开,聊聊它到底怎么算、参数怎么定、左右轮速度怎么合成航向和位移,以及我实际调试中遇到的那些容易让人抓狂的问题。如果你刚接触里程估计,或者正被“机器人走着走着就偏了”困扰,这篇应该能帮你把思路理顺。
1. 里程估计到底解决什么问题:从速度闭环到定位先验
1.1 轮式编码器是怎么数出“转了几圈”的
编码器本质上是一个转动传感器,装在电机输出轴、减速器输出端或轮毂上,把旋转角度变成脉冲信号。常见的有光电编码器和霍尔编码器两种。光电编码器内部有个码盘,码盘上均匀分布透光孔,LED照过去,接收端在码盘转动时输出高低电平,一个透光孔对应一个脉冲。霍尔编码器则是靠磁铁和霍尔元件感应磁场变化输出脉冲。两者原理不同,但到了代码层面,我们关心的只有一个东西:单位时间内收到多少个脉冲。
编码器又分增量式和绝对值式。做里程估计常用增量式,因为它便宜、安装简单、输出是正交信号AB相,既能数脉冲,还能通过相位差判断方向。绝对值式自带零点,断电后位置不丢,多用于关节模组和高精度设备,但在轮式里程计里相对少见,因为轮子转多少圈本来就是相对量,增量式完全够用。
把这个原理理解透了,你就明白里程估计的起点是什么:编码器告诉我们轮子转过的角度,结合轮径就能估算出车身走过的路程。这也是为什么轮式编码器被称作“穷人的定位传感器”,它不需要外部环境信息,只要轮子转,它就有输出。
1.2 里程估计能干什么,不能干什么
在很多项目里,编码器里程计不是用来做“终局定位”的,而是承担三类任务。
第一类任务是速度闭环。控制层需要实时知道轮子转多快,编码器脉冲换算成轮速,喂给PID做反馈。这个场景下,里程估计关注的是短周期内的速度准确性,误差大一点问题不大,因为反馈控制会把误差收敛掉。
第二类任务是短时定位。在激光雷达、GPS这些外部参考暂时失效的时候,用里程计递推车身当前在哪儿,撑住几秒到几十秒。比如AGV在货架区穿行,激光被遮挡的瞬间,靠里程计提供连续轨迹,不让导航彻底中断。
第三类任务是给SLAM提供先验。激光匹配、视觉特征匹配都需要一个初始猜测值。里程计给出的“我大概在这个位置附近”能大大缩小匹配搜索范围,减少误匹配概率。很多SLAM框架里,里程计不是用来跟激光抢定位精度的,而是给优化提供一个可靠的局部约束。
但里程计也有硬伤:它是开环递推,没有绝对校正,误差只增不减。走得越远,位置方差越大。这就好比你闭着眼睛走路,脚每迈一步都有一点偏差,几百米下来,你觉得往东,实际上已经偏到东南方向了。所以我们后面会反复强调:标定、局部可靠、融合校正。这三个动作缺一个,里程计都会飘。
2. 从脉冲到米:核心换算公式与参数选型
2.1 三个必须搞清楚的参数:分辨率、减速比、轮径
做里程估计,第一个要面对的问题是:收到N个脉冲,车子到底走了多远?公式不复杂:
总行程 = (脉冲数 / 每圈脉冲数 / 减速比) × π × 轮径
但这里的三个参数每个都有坑。
第一个是编码器分辨率,常见标注有PPR、CPR、线数。PPR是每转输出的脉冲数,光电编码器经常写多少线,比如500线。要区分清楚:有的编码器在AB相上每个周期输出1个脉冲,有的会输出4倍频,如果频率测量用1倍频还是4倍频,算出来的结果差4倍。我建议在代码里统一按“实际计到的脉冲数”来标定,别拿理论PPR直接计算,因为倍频方式和计数方式一变,数值就变。
第二个是减速比。编码器如果装在电机轴上,电机转30圈输出轴才转1圈,那减速比就是30:1。这时轮轴每转一圈,编码器实际输出PPR×30个脉冲。不少项目直接在代码里写“减速比=30”,但没注意到编码器输出轴和轮轴的关系,导致里程整体放大或缩小。
第三个是轮径,这里有个关键概念:不能用几何直径,要用有效滚动半径。轮胎压在地上会变形,实际上等于用一个比自由直径略小的“有效直径”滚动。橡胶轮、充气胎尤其明显。所以,理论算出来的轮径只能当初始值,后续必须用实测标定。
举个例子。假设编码器PPR=500,减速比30:1,轮子标称直径120mm。轮轴转一圈,编码器输出15000个脉冲。车轮周长是π×0.12≈0.377米。那么每个脉冲对应的位移是0.377/15000≈2.5×10⁻⁵米,大约0.025毫米。这个数值很小,说明编码器分辨率足够细腻。但如果你把减速比漏掉了,每个脉冲对应位移就变成0.754毫米,里程会直接放大30倍,这种错误在实际项目里真的有人犯过。
2.2 用固定采样周期转成速度,注意低速量化误差
里程估计通常按固定周期执行,比如10ms、20ms、50ms。每次读一次编码器累计值,跟上次做差,得到这个周期内的脉冲增量。然后换算成位移增量,最后累加到总里程里。速度就是位移增量除以采样周期。
这里有个很常见的坑:低速时的量化误差。假设采样周期20ms,机器人以0.1m/s缓慢行进,20ms内实际位移只有2mm。如果编码器每个脉冲对应0.025mm,那理论上应该有80个脉冲。这个量看起来不少,但如果机器人更慢,比如0.01m/s,20ms内位移0.2mm,只有8个脉冲,慢速运动时每次可能只跳到个位数甚至0个脉冲。于是速度信号呈现“一步有一步没有”的阶梯状,PID看到这种反馈,轻则颤动,重则系统振荡。
解决思路有三种。一是提高编码器分辨率或者采用时间戳法,记录相邻脉冲的时间间隔,用间隔的倒数算瞬时速度,低速下更平滑。二是适当增大采样窗口,比如把20ms的测速窗口改成50ms,减少单周期的量化跳跃。三是加入滤波,但注意滤波会引入相位滞后,不能过度滤波。
我个人的经验是:低速场景优先采用时间戳法或M法/T法结合的方式。M法适合中高速,单位时间数脉冲;T法适合低速,数两个脉冲之间的时间。两者切换逻辑不复杂,但效果改善非常明显。
2.3 速度信号要不要滤波?怎么滤最划算
滤波这块很容易走向两个极端:要么完全不滤,速度曲线毛刺多;要么滤得太狠,系统响应变肉。做速度环时,我推荐先用滑动平均或一阶低通,把高频毛刺拉平。
滑动平均的窗口别开太大,5到10个样本就够。一阶低通可以写成:
filtered_v = alpha × raw_v + (1 - alpha) × filtered_v
alpha一般取0.2到0.5,具体调。调alpha的原则是:观察机器人低速启动一瞬间,速度是从零缓慢爬升,还是出现反向抖动。如果抖动,说明alpha偏大,毛刺偏高。
滤波会带来延迟。在里程计做定位更新时,我不建议对位移增量做重滤波,因为位移是积分量,本身已经平滑了。需要滤波的是速度信号,因为控制环对噪声敏感。弄明白“哪里该滤、哪里别多此一举”,往往比学会滤波器本身更重要。
3. 左右轮合成:差速模型与航向推算
3.1 线速度与角速度的计算公式
两轮差速机器人是最常见的底盘形式,左右轮各自独立驱动。给定左右轮速度vl和vr,车身中心的线速度和角速度用两个公式算:
v = (vl + vr) / 2 ω = (vr - vl) / b
其中b是左右轮之间的轮距,也叫基线宽度。理解这个公式,可以想象你走路:两脚速度一样时走直线,左脚快右脚慢时向右转,速度差越大,转弯半径越小。角速度公式里的轮距b出现在分母上,意味着b估算不准,航向角误差会被直接放大。
实际代码里,更常用的是先算左右轮各自的位移增量Δsr和Δsl,再合成:
Δs = (Δsr + Δsl) / 2 Δθ = (Δsr - Δsl) / b
这里的Δθ单位是弧度。一旦算错单位,把角度当弧度用,航向会偏差得非常离谱。我见过有人把弧度结果直接拿来当度数累加,转一圈下来航向变成360度乘以57.3倍,直接爆表。
3.2 弧长法更新坐标,比直线近似更稳
拿到Δs和Δθ,怎么更新x、y坐标?最直观的是直线近似:
x += Δs × cos(θ) y += Δs × sin(θ) θ += Δθ
这在采样周期很短、转向很慢时是够用的。但采样周期稍长或者转向较猛,直线近似会低估转弯路径的弧线长度。更稳妥的办法是用弧长法:
θ_mid = θ + Δθ / 2 x += Δs × cos(θ_mid) y += Δs × sin(θ_mid) θ += Δθ
也就是说,位移增量按这一小段弧线上的中间航向角来投影。这个做法的物理含义是:在Δt内,机器人不是走一条直线,而是走一条微弧,用弧中点方向计算投影更贴近真实轨迹。
下面给一段简单的Python示例,方便直接理解整个更新流程:
import math class Odometry: def __init__(self, baseline): self.x = 0.0 self.y = 0.0 self.theta = 0.0 self.baseline = baseline # 轮距,单位米 def update(self, d_sr, d_sl): d_s = (d_sr + d_sl) / 2.0 # 位移增量 d_theta = (d_sr - d_sl) / self.baseline # 航向角增量 theta_mid = self.theta + d_theta / 2.0 self.x += d_s * math.cos(theta_mid) self.y += d_s * math.sin(theta_mid) self.theta += d_theta每周期调用一次update,传入左右轮各自的位移增量。这种写法有两个好处:一是没有多余的变量累积,二是弧中点投影已经在公式里,不会因为采样周期拉长而出现明显的轨迹畸变。
3.3 轮距不准时航向误差会被悄悄放大
轮距b在公式里是分母。假设实际轮距是500mm,你代码里写成480mm,那么所有角速度计算都会放大4%左右。这个误差,最明显的后果是:原地旋转时,机器人实际转了100度,里程计认为转到96度左右,看起来差距不大,但累积一圈后,方向误差会明显反映在终点位置上。
更麻烦的是,轮距误差和轮径误差产生的现象很像,都表现为“转完之后位置偏了”。区别在于,轮距误差主要影响航向,轮径误差除了影响航向,还会影响直行距离。排查时要区分:跑直线不直,可能是左右轮径不一致;原地旋转后航向偏移,优先检查轮距;跑正方形最后无法回原点,就要结合起来看。
所以,我强烈建议在新底盘接入里程计之前,先把轮径和轮距标定做掉。这一步偷懒,后面所有调试都会被里程误差干扰,你会分不清是控制器问题、SLAM问题还是里程计问题。
4. 标定轮径和基线宽度:入手里程估计的第一道关
4.1 直线往返标定法:校准轮子的有效直径
标定轮径的土办法也很有效:找一块平整地面,用卷尺量出10米的直线距离,让机器人以较低速度、开环方式沿直线跑过去。注意一定要开环,不要用里程反馈闭环,因为闭环会修正误差,导致测不出真实的轮径偏差。
记录机器人从起点到终点累计的里程计位移,用实际距离除以里程计读数,得到一个修正系数。然后把理论轮径乘上这个系数,得到新的有效轮径。举个例子,理论轮径120mm,在10米直线行驶后,里程计显示9.35米,修正系数就是10/9.35≈1.0695,实际有效轮径约为128.3mm。这个值比卡尺量的几何直径更接近真实滚动半径。
往返各跑几次,取平均,能排除地面坡度、起步打滑等随机误差。标定速度不要高,建议0.2到0.5m/s,因为低速下轮子和地面的滑移最小,测出来的滚动半径更接近真实。
4.2 原地旋转标定法:反推真实基线宽度
轮距标定可以用原地旋转法。让机器人原地旋转N圈(比如5圈),同时用高精度陀螺仪或外部量角器测出实际旋转角度。里程计内部会按照当前代码里的轮距b计算累计航向角。如果实际旋转角度和里程计计算角度不一致,就能反推出真实轮距:
真实b = 理论b × 里程计计算角度 / 实际旋转角度
比如代码设b=500mm,原地旋转5圈后里程计显示1800度,但陀螺仪实测为1820度,里程计比实际转少了1.1%,说明真实轮距比500mm大,修正为500×1820/1800≈505.6mm。
这里有个细节:旋转速度不能太快,否则打滑会影响旋转圈数;旋转时地面要平整,不能有坡度和斜纹。如果没有陀螺仪,也可以让机器人贴着一个圆柱或墙角旋转,用外部标记物做角度参考,但精度会差一些,适合没传感器的情况。
4.3 跑一圈正方形校验:把误差现象和原因对上
标定完之后,强烈建议做一个综合校验:控制机器人沿正方形跑一圈,边长3到5米,记录回到起点后的位置偏差。这个实验能一次性暴露多个误差源。
如果终点横向偏移明显,比如向右偏了20cm,大概率是左右轮径不一致导致直线不直;如果终点朝向偏了但位置偏移不明显,比如回到起点附近但车头角度不对,说明轮距标定还有误差;如果整个正方形轨迹呈顺时针旋转扩散状,说明左右轮径误差和轮距误差叠加了。根据现象再稍微微调一两次参数,里程计就能达到“短距离内很好用”的状态。
这个校验过程,我建议保留数据。把每次标定的参数、跑正方形后的偏差都记录下来,后面换轮胎、换底盘后,直接对比基线数据,比重新从头摸索快很多。我自己就有一个专门的本子记录这些参数,每次改完都会注释在代码文件头部,方便回滚对比。
5. 常见问题与排坑实录
5.1 编码器丢脉冲:接线、中断与软件滤波
丢脉冲是里程估计里最隐蔽的问题,不会直接报错,只是里程悄悄变短。比如轮子转了一圈,按分辨率应该输出500个脉冲,但实际只计到480个,长期累积下来,里程明显偏小且无规律。
这类问题通常有三个来源。一是接线接触不良,特别是电机运行振动后,编码器线束插头松动,产生瞬间丢脉冲。二是电气干扰,电机PWM驱动线上的大电流变化会干扰编码器信号线,脉冲毛刺可能导致计数器多计或者少计。三是中断处理太慢,如果单片机主循环里有耗时操作,外部中断的脉冲计数服务被抢占,就会漏计。
排查建议:先把编码器线换成屏蔽双绞线,屏蔽层单端接地,并且让编码器线和电机动力线分开走线槽,不要平行捆在一起。然后在电机驱动输出端加续流吸收,或者给编码器供电加一级LC滤波。代码层面,优先用硬件定时器捕获外部中断,中断服务函数里只做计数和清零标志,不要在中断里做浮点运算或里程更新。
5.2 打滑和空转:里程虚增的几个典型现场
打滑是轮式里程计最无解的问题,因为它相当于给编码器一个虚假的转角输入,轮子转了但车身没动,里程计却以为走了很远。打滑常发生在急加速、急刹车、粗糙度差异大的地板、上坡启动等场景。
针对打滑,软件层面能做的事有限。一种思路是检测“轮速异常”:比如一个轮子瞬间速度远大于另一个轮子,或者当前电流很大但轮速很小,就认为打滑,暂时冻结里程更新,等运动恢复后再继续。另一种思路是限制加速度,让底盘柔和启停,从物理上减少打滑机会。
悬空空转也是类似问题。有些机器人会被人工抬起来转运,此时轮子还在转,里程计已经在“虚拟跑图”。如果传感器里有IMU,可以通过加速度计检测到机身姿态变化或振动特征,识别“被抬起”的状态,主动把里程计清零或者标记为无效。这个功能在AGV项目里很实用,不然一台被搬走的Robot,定位会瞬间漂到十万八千里外。
5.3 轮径随负载变化:不要指望一次标定吃一辈子
橡胶轮和充气轮其实是被压扁的,负载越大变形越厉害,有效滚动半径越小。同一个人之后,空载和满载状态下跑同样的路程,里程计读数能差1%到3%。如果你在AGV上装了较重的料架,里程计的定位误差会在几个小时后变成明显的累积误差。
在实际工程中,很难对每种负载都重新标定。比较可行的做法是:把里程计当作短时相对约束,定期用激光雷达、二维码或地面磁条等绝对参考来校正位置。也就是说,里程计的定位输出只用于前后几秒内的推算,一旦绝对参考更新,就重置里程计的累积误差。这种做法既能发挥里程计平滑连续的优势,又不会让它把系统误差带飞。
另外,定期检查轮胎胎压。市面上很多机器人用低配橡胶轮,胎压变化对有效半径的影响肉眼可见。气胎胎压不足时,轮径变小,里程计系统性偏大,体现在直行距离“虚胖”上。如果你发现同一路线一段时间后里程偏差明显变大,先去看看胎压,再考虑别的。
5.4 与IMU融合时的协方差设置思路
里程计和IMU是经典互补组合:里程计短时位移稳定,但航向角受轮距误差影响;IMU的加速度计噪声大,但陀螺仪短时航向变化很准。融合时要注意,里程计的航向在长时间后会漂移,陀螺仪也漂移,两者不能互相彻底修正,只能做短期内的互相约束。
一个我踩过的坑是:把里程计的协方差设得太小,导致EKF过度信任里程计,一旦轮子打滑,融合结果直接被带偏。后来我把里程计的噪声方差设置为随着运行距离增长而增大,也就是信任度随时间降低;IMU的陀螺仪航向在短时间内的权重调高,这样才能在打滑瞬间让融合输出更加平滑。
里程估计在工程里的定位,应该是“好用的局部约束”,而不是“全局万能定位”。想明白这一点,融合的参数调节才不会走弯路。
最后分享一点个人体会:里程估计的实现并不难,难的是理解它的边界。编码器给出的永远是“轮子转了多少圈”,而不是“车身实际走了多远”。如果能接受这个前提,老老实实做标定、做校验、合理设置融合权重,轮式编码器绝对是最可靠又低调的定位基石。如果非要拿它当万能定位源,再好的公式和代码也救不回来。