做移动机器人的人,大概率都绕不开两轮差速底盘。不管是入门用的平衡小车、实验室里的导航开发平台,还是工厂里的AGV(自动导引车),很多项目的底层都长这样:左右两个独立驱动的轮子,加一个万向轮或从动轮撑着。结构确实简单,但你真要把这辆车控得稳、走得准、能跑闭环,光靠“给俩轮子发PWM”可远远不够。这篇文章我就从运动学建模、控制器设计到STM32工程落地,把两轮差速机器人这套东西完整串一遍。
文章内容适合正在做毕设、参加智能车比赛、或者刚接触移动机器人准备自己攒一台底盘的朋友。我会尽量把“为什么这么做”讲透,而不只是给一堆公式和代码。数学上不会搞得太吓人,工程实现上的坑会多聊一些——这些东西都是我实际调试中踩过的,教科书里通常不写。
1. 运动学建模:先把“车怎么走”讲清楚
1.1 从几何约束说起,为什么要建模型
两轮差速底盘看起来简单,但它有个非常核心的几何约束:车体只能沿着当前朝向方向运动,不能像全向轮那样横着飘。也就是说,你控制的是车体在二维平面上的平移速度(前后方向)和旋转角速度,这两个量直接决定车的运动轨迹。
建模的目的,就是要把左右轮子的转速,映射成车体的速度,再把车体的速度映射回轮子的转速。前者叫正运动学,后者叫逆运动学。正运动学用来做状态估计——比如通过编码器读数算出车到底走了多远、转了多少度;逆运动学用来做运动控制——比如你告诉控制器“我要以0.5 m/s前进、同时以0.2 rad/s转弯”,控制器就知道该给两个轮子多少转速。
这里涉及一个非常重要的概念:瞬时旋转中心(ICR)。差速车转弯时,车体并不是绕某个固定的圆心转,而是绕一个随速度变化而移动的点转。理解这一点,你就明白为什么“给两个轮子不同的速度”会产生圆弧轨迹——因为两轮线速度不同,车体相当于在绕着两轮连线延长线上的某个点画弧。
1.2 正运动学模型推导:从轮速到车体速度
我们先把问题简化,假设车轮不打滑,车体是刚体。设轮距(两驱动轮中心之间的距离)为W,轮子半径为R,左轮线速度为v_L,右轮线速度为v_R。车体质心(其实就是两轮连线中点)的前向速度为v,旋转角速度为ω。
推导思路很直接:车体瞬时速度是左右轮速度的平均值,瞬时旋转角速度则是左右轮速度差除以轮距。用公式写出来就是:
- v = (v_L + v_R) / 2
- ω = (v_R - v_L) / W
这里有个细节需要特别提醒:如果坐标系规定车头方向为x轴正方向,逆时针旋转为正,那么右轮速度大于左轮时车体向左转(逆时针),ω为正。不同的教材可能坐标定义不同,符号会差一个负号,工程上以自己代码里的约定为准,但一定要统一。
有了线速度v_L和v_R,轮子转速n_L、n_R就和它直接关联:v_L = 2πR·n_L/60,注意单位换算,转速n常用的单位是转/分钟,但PID控制器里一般要统一成 rad/s 或者 m/s。
正运动学到这一步就完成了。它的作用体现在两个地方:一是你在做仿真时,给左右轮速,就能推算出车体的轨迹;二是在实际系统中,利用编码器测出左右轮实际转速,估算出车体的实际速度和角速度,再做闭环控制。
1.3 逆运动学模型:把目标速度拆给两个轮子
逆运动学是控制里更常用的方向。给定目标速度v_target和角速度ω_target,反推左右轮速:
- v_R = v_target + (ω_target × W) / 2
- v_L = v_target - (ω_target × W) / 2
注意如果目标角速度为正(逆时针),那么右轮应该比左轮快。这个公式非常直观:让车转得更急,就加大两轮速度差;让车直线前进,就把ω设成0。
从这两个公式出发,还能导出两个竞速场景下经常用到的概念:旋转半径和曲率。如果v_L = -v_R,车体原地旋转,旋转半径为0;如果v_L = v_R,车体直线运动,旋转半径无穷大。中间状态对应不同半径的圆弧。
运动学模型是整个控制系统的“翻译官”:把用户级的目标速度翻译成电机级的目标转速,再把电机级的实际转速翻译成用户级的状态反馈。它的准确性直接决定控制精度。
1.4 关于运动学模型的两个工程提醒
轮距W不是随便测一下两轮中心距离就完事的。
如果你用的是轮毂电机或者带减速箱的电机,轮子本身有一定宽度,实际的等效轮距可能会比几何中心距略大或略小,尤其是转弯半径较小时,误差会更明显。最可靠的做法是“用实验结果反推”:让车原地旋转一定圈数,对比编码器累计角度和实际角度,反向标定W的等效值。
车轮半径R同样存在“有效半径”问题。
胎压不同、地面软硬不同,实际滚动半径和几何半径可能差2%~5%。这在小半径转弯时可能不会暴露,但跑长距离直线导航时,里程计累计误差会让你怀疑人生。所以做导航类项目,里程计标定是必须做的第一步。
2. 控制策略设计:让模型真正动起来
2.1 从运动学到控制的闭环框架
有了运动学模型,下一步是控制。差速底盘的控制一般分两层:底层轮速环和上层运动环。底层轮速环的任务是“让左右轮精确达到目标转速”,属于电机级的高速控制,通常在单片机的定时中断里跑,控制周期1~10ms;上层运动环的任务是“让车体达到目标速度和角速度”,属于车体级的低速控制,控制周期可以放宽到10~50ms。
上层运动环的输出经过逆运动学分解,变成左右轮的目标转速,发给底层轮速环;底层轮速环通过PID调节电机PWM,使实际转速逼近目标值。编码器测得轮速后,再通过正运动学反馈给上层,形成完整的闭环。
这种分层架构的好处是职责清晰。如果直接在运动环里调PWM,电机响应差异、负载变化都会严重影响运动控制效果;而有了底层速度环之后,上层面对的是一个“近似线性”的执行器,控制难度大大降低。
2.2 轮速环PID:位置式还是增量式
STM32工程里最经典的底层轮速环就是PID。先分清楚两种形式的适用场景:
- 位置式PID:输出是PWM的绝对占空比。它需要累积误差项,容易积分饱和,但实现简单,适合不常改变目标值的场合。
- 增量式PID:输出是PWM占空比的增量。它天然自带积分截断效应,对执行器饱和有抑制作用,而且不会出现大的阶跃冲击,适合电机这种需要频繁调速的负载。
我实际做底盘控制时,轮速环更推荐用增量式。原因很实际:电机启停和负载突变时,位置式PID的积分项容易“灌满”,导致响应迟钝甚至超调;增量式每次只改变一个小值,动态响应更平滑,安全性也更好。
增量式PID的离散公式是:
Δu(k) = Kp·[e(k) - e(k-1)] + Ki·e(k) + Kd·[e(k) - 2e(k-1) + e(k-2)]
然后 u(k) = u(k-1) + Δu(k)。注意这里的e(k)是目标转速与实际转速的差值。
2.3 级联PID:什么时候需要上层速度环之外再套位置环
如果你只是做底盘运动控制,轮速环就够了。但如果你做的是“走直线走准”“原地转固定角度”这类任务,单靠轮速环还不够——你还需要一个位置环或角度环。
位置环的输入是期望位置(或期望角度),输出是目标速度,作为轮速环的给定值。这就形成了所谓的级联PID:外环(位置/角度环)→ 内环(速度环)→ PWM输出。级联的好处是每个环只处理自己带宽范围内的干扰,内环先把电机非线性吃掉,外环就能用比较温和的参数。
我之前做过一个自动归位的小车,要求它每次前进2米误差不超过2厘米。只用速度环的话,因为轮速存在稳态误差和启动延迟,停下来位置飘到5厘米以上;加上位置环之后,靠近目标时外环会降低速度命令,小车会“缓缓蹭过去”,最终精度能稳在1厘米左右。
级联PID整定有个基本顺序:先整内环再整外环。内环调到“响应快、无超调”之后,外环再逐步加P,发现震荡就退回去,加D抑制。切忌同时调两层参数,那只会让你分不清问题出在哪个环。
2.4 运动学控制的边界:什么情况会被模型“坑”
运动学模型成立的前提是“纯滚动无滑动”,但现实中这个前提经常被打破。地面太滑,轮子打滑,编码器读数会“骗人”;地面有凸起,轮子瞬间悬空,转速突变;负载变化大,电机响应变慢,PID参数不再适用。
这些问题不是靠调参能彻底解决的。工程上的常见做法是加入自适应控制或者至少做一个“异常检测”:比如当目标速度和实际速度误差持续超过阈值时,不再盲目加大PID输出,而是降低速度设定值或者报警。另外就是做加速度限制,不要让速度阶跃变化,用梯形加减速或S曲线加减速平滑速度命令,这样既能减少打滑,又能保护电机和机械结构。
3. 从模型到硬件:STM32工程落地全流程
3.1 硬件选型:电机、编码器、驱动板怎么配
以最常见的STM32F103C8T6(就是俗称的“蓝丸”)为例,做一台两轮差速小车,核心件就是直流减速电机加编码器。编码器推荐用霍尔式AB相增量编码器,常见的是每转几十线的霍尔编码器,经过减速箱输出轴后,实际分辨率能达到千级别的脉冲数,够用且抗干扰能力比光栅式好,价格也低。
电机驱动这边用TB6612或者DRV8833这类芯片就很稳,它们内部集成了H桥,逻辑输入兼容3.3V,直连STM32不需要电平转换。注意电机驱动芯片逻辑电源和电机电源要分开走线,共地但不要共用一根粗线,否则电机启动瞬间的压降会把单片机拉复位——这个问题我在调试无数次后才意识到是电源布线的锅。
轮速编码器的接线要特别注意。AB相脉冲输出是开漏结构的话,必须外部上拉;如果用推挽输出,可以直接接STM32的输入捕获引脚。读取频率上,霍尔编码器的脉冲频率一般不超过几千Hz,STM32的定时器捕获完全能应对。
3.2 编码器读取与速度计算:核心代码结构
编码器数据的正确读取是速度闭环的基础。在STM32上,推荐用定时器的编码器模式:把AB两相接到同一个定时器的两个通道上,配置为Encoder Mode,硬件自动完成鉴相和倍频,CPU只需要读取计数寄存器的累加值。比如TIM3的CH1和CH2分别接左轮编码器的A、B相,硬件会每产生一个有效沿就自动加减计数。
速度计算的做法是在固定周期(比如1ms或5ms)内读取计数差值,乘以一个换算系数得到线速度。我习惯用1ms定时中断里的高频任务来做编码器采样和PID运算,这样速度环的控制周期是1ms,响应足够快。
核心换算逻辑大致是:
转速(n) = 采样周期内的脉冲差值 / (编码器线数 × 减速比 × 采样秒数)
再把轮子线速度算出来:v = 2πR×n/60。这里每个量都要带单位算清楚,最笨但最可靠的方法是全部换成国际单位:米、秒、弧度。单位写进代码注释里,能省不少后面的排查时间。
3.3 PID控制器实现与参数整定记录
底层速度环的PID实现,我建议用结构体封装:
typedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float feedback; // 实际反馈值 float error; float last_error; float prev_error; float integral; float derivative; float output; float output_limit; // 输出限幅 } PID_t;每次进入1ms定时中断,执行流程是:读编码器差值→计算实际轮速→计算偏差→运行增量式PID→更新PWM占空比。两个轮子各跑一套独立的PID,不要共用变量。
参数整定我分享一下个人的“土办法”:先只加P,从小到大,直到轮子出现轻微震荡(听到电机“呜呜”声或者速度波形等幅震荡),记录此时的Kp,然后把Kp设为这个值的一半左右,再加I慢慢消除稳态误差,最后加一点D来抑制超调。实测下来这个方法比教科书里的“临界比例度法”更习惯,因为直流电机的非线性太强,理论计算出来的参数往往偏差不小。
底层的目标转速不要直接丢给PID,最好经过一个斜坡发生器做加减速限制。比如目标速度从0到1 m/s,设定加速度为1 m/s²,那么要1秒才能加到目标值。这样不仅启动平稳,而且对编码器噪声的放大也会小很多。
3.4 频率、中断优先级与实时性的取舍
STM32工程做底盘控制,实时性是最容易被低估的问题。我的建议是:编码器采样与PID运算放在1ms定时中断里,串口调试输出放在主循环里用非阻塞方式做,无线通信模块(比如ESP8266或者nRF24L01)的数据接收用串口中断+环形缓冲区。
有一个很实际的注意点:多个中断同时发生时,优先级一定要设计好。编码器采样/PID中断优先级要高于串口中断,否则某个时刻串口数据一多,PID周期被拉长,速度环带宽下降,小车会“一顿一顿”。我遇到过类似现象,排查了一天才意识到是USART1的接收中断抢占了TIM中断的CPU时间。
另外,PWM的更新频率也要匹配。电机驱动的PWM频率建议设置在10kHz~20kHz,这个区间既可以避开人耳可听噪声,又不会让MOS管开关损耗过大。PID输出直接把占空比写入CCR寄存器即可,不需要做额外的滤波。
4. 常见问题与排查技巧实录
4.1 车轮不转或只往一个方向转
这是新手最容易遇到的情况。先量电机驱动板的逻辑电源和电机电源是否正常,再检查PWM引脚有没有波形输出,最后看使能引脚(如STBY)是否拉高。TB6612这类芯片如果STBY不拉高,输出端永远是高阻态,电机自然不转。
如果两个轮子转向相反,先不要急着改程序——先判断是机械装配问题还是接线问题。最简单的办法是手动转动车轮,观察编码器计数方向是否一致。如果硬件上两轮编码器方向反了,软件里加个负号就能解决,但这属于“掩盖问题”的做法,时间久了容易坑自己。建议统一在机械装配时就保证左右轮编码器方向对称。
4.2 小车走不直:轮子一点点偏移就歪
直线走歪的核心原因有两个:一是左右轮速度闭环性能不一致(一个响应快一个响应慢),二是轮距或轮径标定不准。
先检查速度环。把目标速度给到左右轮相同值,用串口把实时轮速打印出来,对比两轮的速度曲线。如果稳态误差虽然为零但动态响应差异很大(一个超调多一个超调少),说明两套PID参数不一致,分别整定。另一种情况是电机本身空载转速不同,即使PID参数一样,输出相同的PWM占空比,两轮转速也会不同——这正是速度闭环存在的意义,它会自动修正这种差异。
如果速度曲线一致但直线还是歪,那大概率是左右轮直径不一致或者轮距标定有偏差。解决方案是做一个简单的比例补偿:让车跑一段已知距离和已知偏航角,算出修正系数,把这个系数乘到某一侧的轮速命令上。这叫“软标定”,工程上非常实用。
4.3 小车原地转圈时位置漂移
原地旋转要求左右轮速度大小相等、方向相反。如果旋转时车体中心位置发生明显偏移,说明左右轮速度幅值不一致(一个快一个慢)。这时可以用轮速环的实时数据排查:让车原地转几圈,打印两个轮的累加里程,如果差值越来越大,锁定是速度幅值不对称问题。
另外还有一个容易忽略的因素:地面摩擦不均。如果地面有油污或者轮胎磨损程度不同,即使左右轮电机转速一致,实际线速度也可能不同。这时候不是控制器能解决的,需要清理地面或更换轮胎。
4.4 悬空测试正常、落地就跑偏,该怎么定位
这种情况十有八九是编码器采集被噪声干扰了,或者是电机负载大导致电流突变,影响了电源稳定性。排查方法是:接上串口,把编码器原始计数差值打出来,观察数值跳动是否正常。正常的计数差值应该在某个范围内平滑变化,如果频繁出现大跳变或毛刺,优先怀疑编码器线缆的屏蔽和上拉电阻。
电源方面,建议给电机和单片机系统分别供电,实在不行至少要在电机电源端加大容量电解电容(几百微法到一千微法),并在电机引脚上并接陶瓷电容和磁珠,基本能消除大部分传导干扰。我之前有台小车总是悬空正常、落地就抽风,最后发现是电机电刷火花干扰了编码器信号,换用带屏蔽的编码器线后问题直接消失。
5. 一条完整调试路径:从零到闭环走通
5.1 分五步走,每一步验证完了再进行下一步
我整理一条自己惯用的调试路径,照着做可以少走弯路:
- 开环测试:不用PID,直接给固定占空比PWM,确认左右轮转动方向和预期一致,确认编码器计数正常。
- 速度环开环标定:手动给不同占空比,测量对应转速,做一个粗略的占空比-转速映射表,看一下电机的线性区在哪里。
- 速度环闭环:接上PID,让给定目标速度后轮速快速且稳定地跟随,观察阶跃响应的超调和稳态误差。
- 运动环开环测试:用串口发目标线速度和角速度,车体匀速直线前进、原地旋转,观察实际轨迹是否接近预期。
- 运动环闭环:接上位置环或者角度环,跑“前进2米停止”“转90度”这类任务,验证级联控制效果。
每一步验证时都要保留数据。比如步骤2建立一个标定表,步骤3记录不同Kp下的阶跃响应数据,后面就算出问题也能回溯。很多人调试卡住是因为跳步太多,出了问题根本不知道是哪个环节的锅。
5.2 调试过程中的数据记录与波形观察技巧
底盘调试最怕“凭感觉”。有条件的话,用串口把实际轮速、目标轮速、PID输出这些变量以CSV格式发出来,然后用Python简单画一下曲线,问题往往一眼就能看出来。
如果没有上位机条件,可以用一个更土的办法:把PID输出发送到串口监视器,观察数值变化的模式。比如输出在最高值和最低值之间持续震荡,说明Kp过大或采样周期过长;输出缓慢增大到饱和但误差还是很大,说明电机堵转或者目标速度超过了物理极限。
我个人习惯在代码里加一个调试开关,可以通过串口命令实时切换“输出原始编码器值”还是“输出目标/反馈/PID输出”,这样不用反复修改代码编译烧录,调试体验会好很多。
5.3 从底盘控制到后续扩展:里程计与导航
底盘控制只是移动机器人的第一步。当你把两轮差速底盘的运动学和控制打磨稳定之后,前面还有很多扩展空间:
- 里程计推算(Odometry):基于编码器数据和运动学模型,实时推算车体在世界坐标系中的位姿(x, y, θ)。这是SLAM和导航定位的前提。
- 路径跟踪:给定一条直线或圆弧路径,基于当前位姿偏差计算目标速度和角速度,这就是纯跟踪(Pure Pursuit)或Stanley控制要做的事。
- 与上位机通信:让底盘接收更高级别控制器的速度指令,同时回报当前实际速度与里程计数据。
以我自己做过的项目为例,底层底盘控制跑稳后,上层用ROS做SLAM和自主导航,整条链路就通了。底层的响应质量和里程计精度直接决定了上层导航的稳定性,一台“脚底下虚”的底盘,再强的导航算法也跑不出效果。
6. 部署时的实战经验与心得
6.1 轮胎打滑:所有控制算法的天敌
两轮差速车的控制上限,很多时候不是控制器决定的,而是物理条件决定的。轮子和地面之间的摩擦力是唯一的驱动力来源,一旦超出摩擦极限,再精确的PID也无能为力。
工程上的缓解手段有几个:一是控制加速度,不要让速度阶跃突变;二是选择合适的轮胎材质,橡胶轮比硬塑料轮的抓地力强很多;三是有条件的场合可以增加悬挂机构,避免过弯时内侧轮因载荷转移而打滑。做比赛和做产品侧重点不同,但“降低打滑概率”这个目标是共通的。
悬空测试和落地测试的差异也来源于此。悬空时轮子没有负载,电机的动态特性完全体现不出来;落地后地面摩擦、转动惯量都变了,PID参数必须重调。所以别偷懒,底盘调试一定要在地上跑。
6.2 标定是工程里最不值钱但最见效的工作
我见过很多团队花大量时间调PID,却不愿意花10分钟标定轮距和轮径。实际上,参数标定的收益往往是立竿见影的。
轮距标定方法很简单:让小车原地旋转N圈,读取编码器估算出旋转角度,与实际角度对比,修正轮距参数。一个晚上就能把旋转精度从“差10度”提高到“误差小于1度”。轮径标定类似:让小车直线跑一段已知距离,根据实际距离反推有效轮径。
这种标定不用做得很频繁,但如果换了轮子、调了胎压、或者换了地面摩擦环境,最好重新标一遍。我自己习惯了在代码里把轮距、轮径这两个参数设为编译期宏定义,每次标定完直接修改宏重新编译,简单粗暴。
6.3 控制周期与滤波器设计:一个常被忽视的维度
底层轮速环的控制周期,我推荐先设1ms。如果你的代码在主循环里跑PID,周期抖动会很严重,导致速度环带宽不稳定。用定时器中断固定1ms调用一次,基本能满足大多数直流减速电机的需求。如果你用的是无刷电机或者对响应要求特别高,可以考虑把周期缩到500us,甚至用FOC方案,那就是另一个层次的问题了。
编码器计数差分得到的速度信号有一个固有的问题:高频率下噪声被放大。PID里的微分项本身也放大噪声,两者叠加会导致PWM输出“哗啦哗啦”乱抖,电机发热严重。解决办法是加一阶低通滤波:speed_filtered = α×speed_raw + (1-α)×speed_filtered_last,α取0.2~0.4比较合适。滤波会增加相位延迟,所以α不要太小,否则速度环响应会变得迟钝。
6.4 另一条可行路径:用现成轮毂电机方案
如果你不想从电机、驱动、编码器这种分立器件开始做,也可以考虑带轮毂电机的现成差速底盘方案。这类底盘一般自带闭环控制,通过串口或CAN总线直接收速度命令,内部已经完成了速度环和部分运动学处理。优点是开发速度快,缺点是价格高、灵活性差,而且出了问题很难排查到电机底层。
对于以SLAM导航、视觉感知为主项目的团队,我其实更推荐这种方案,把精力集中在更上层的问题上。但如果你想深入理解移动机器人底层的运动学和控制系统,自己从零搭一台差速底盘绝对是不可替代的学习路径。
写在最后
两轮差速移动机器人的运动分析、建模和控制,是一个从理论到实践跨度很大的系统工程。运动学模型给了你理解底盘运动的数学框架,PID控制解决了执行层面的跟踪问题,而STM32工程落地则把所有逻辑变成真正能动起来的机器。
我个人最大的体会是:别被公式和术语吓住,两轮差速底盘的核心逻辑非常简单——左右轮速度相加是前进,相减是转向。把这句话吃透了,再去看运动学推导和控制架构,很多东西自然就通了。但也别轻视工程细节:标定、滤波、中断优先级、电源布线,这些“脏活累活”做得越扎实,后面的调试就越顺利。
这篇文章是按我自己的项目经验来写的,很多参数和结论都是基于常见的直流减速电机和STM32平台。如果你用的是无刷电机、麦克纳姆轮或者其他平台,部分内容需要做相应调整,但整体思路是共通的。希望这篇内容能帮你少踩几个坑,尽快地把底盘调稳、跑直。