差速驱动底盘与运动控制:让小车听话地走直线、转弯、原地自转
- 用运动学直觉理解差速驱动如何靠左右轮速差实现直行、转弯、原地自转
- 在双路电机驱动之上封装 forward/backward/turnLeft/turnRight/spin/stop 六个动作
- 看懂"同型号电机也跑偏"的根因,并明白为什么这件事会把你逼向编码器闭环
- 完成走正方形回起点的挑战,亲手体会无闭环时的误差累积
你按上一篇把电机驱动板接好了,电机也会转了。于是你写下"两个轮子一起正转",满心期待小车笔直冲出去——结果它走了不到半米就明显往一边偏,再走两米已经歪到桌子边缘。你没接错线,电机也是同一批买的同型号,可它就是不走直线。
这不是你的 bug,这是差速驱动底盘的"出厂特性"。理解它为什么跑偏,恰恰是理解整个底盘运动逻辑的入口。
如果你还没把双路电机转起来,先回 双路电机驱动接线 那篇——驱动板怎么接 TB6612/L298N、IN1/IN2 怎么定方向、PWM 引脚接哪、电机供电怎么单独走,那篇讲透了。本篇不重复电机代码细节,只管一件事:两个轮子怎么配合,才能让整台车走出你想要的轨迹。
差速驱动:两个轮子,一身本事
差速驱动(Differential Drive)的硬件极简:两个独立驱动的轮子,外加一个不出力的万向轮(或牛眼轮)做支撑。左右两个轮子各接一路电机驱动,能独立控制转速和方向。万向轮只负责让车身站稳,自己不参与驱动,往哪偏都行。
就这么三个轮子,靠"左右轮速差"(这就是"差速"二字的来历)能走出全部你需要的动作。运动学直觉其实只有三条,记住就够用:
- 左右同速、同向 → 直行。两轮一样快往前,车就往前;一样快往后,车就倒退。
- 左快右慢(同向) → 往慢的那侧转。左轮跑得快、右轮跑得慢,车头就向右偏,画出一道弧线。速差越大,弧越急。
- 左右反向、等速 → 原地自转。左轮前转、右轮后转,车不前进也不后退,绕着自己车身中心点转圈。这就是差速底盘最爽的能力:零转弯半径,在窄过道里也能掉头。
你不用记任何公式。把两个轮子想象成你划船的两只桨:两桨一起划船直走,左桨用力船右拐,一桨前推一桨后拉船原地打转。差速小车就是一条只有两只桨的船。
观点先放这儿:差速驱动是新手第一台小车的唯一正确选择。 麦克纳姆轮能横移很炫,但四个轮子、四路驱动、运动学解算复杂,调试地狱;阿克曼转向(像真车那样前轮打方向)结构又比差速复杂。差速用最少的零件给你最完整的平面运动能力,先用它把"底盘怎么动"这件事吃透,再去碰花活。
把六个动作封装成函数
光知道"左快右慢往右转"还不够,每次都手写两路 PWM 太啰嗦也容易错。正确做法是封装一层动作 API:上层只管喊 forward()、turnLeft(),底层怎么拨弄两路电机由函数兜住。这样你的主程序读起来就是人话。
下面这段以 ESP32 + TB6612 风格的双路驱动为例。每路电机要两个方向引脚定转向、一个 PWM 引脚定速度。引脚号只是示意,以你自己的接线为准;方向引脚的高低电平对应正转还是反转,也取决于你电机线的接法,跑反了对调一下即可(排查表里会说)。
ESP32 的 PWM 走 LEDC 通道,需要先 setup 再用 ledcWrite 给占空比,细节见 Arduino-ESP32 LEDC 文档。如果你用的是 Arduino UNO,把 ledcWrite 换成 analogWrite 即可,逻辑完全一样。
// ===== 引脚示意(以你的接线为准) =====
// 左电机
const int L_IN1 = 25, L_IN2 = 26, L_PWM = 27;
// 右电机
const int R_IN1 = 32, R_IN2 = 33, R_PWM = 14;
const int CH_L = 0, CH_R = 1; // LEDC 通道
const int PWM_FREQ = 20000; // 20kHz,超声频段,电机不啸叫
const int PWM_RES = 8; // 8位,占空比 0~255
void setupMotors() {
pinMode(L_IN1, OUTPUT); pinMode(L_IN2, OUTPUT);
pinMode(R_IN1, OUTPUT); pinMode(R_IN2, OUTPUT);
ledcSetup(CH_L, PWM_FREQ, PWM_RES); ledcAttachPin(L_PWM, CH_L);
ledcSetup(CH_R, PWM_FREQ, PWM_RES); ledcAttachPin(R_PWM, CH_R);
}
// 单侧电机底层控制:dir=+1 前转 / -1 后转 / 0 停; speed=0~255
void leftMotor(int dir, int speed) {
digitalWrite(L_IN1, dir > 0); digitalWrite(L_IN2, dir < 0);
ledcWrite(CH_L, dir == 0 ? 0 : speed);
}
void rightMotor(int dir, int speed) {
digitalWrite(R_IN1, dir > 0); digitalWrite(R_IN2, dir < 0);
ledcWrite(CH_R, dir == 0 ? 0 : speed);
}
// ===== 六个动作:全用"左右轮 dir+speed"表达 =====
void forward(int spd) { leftMotor(+1, spd); rightMotor(+1, spd); } // 同速同向→直行
void backward(int spd) { leftMotor(-1, spd); rightMotor(-1, spd); } // 同速反向→后退
void turnLeft(int spd) { leftMotor(+1, spd/3); rightMotor(+1, spd); } // 左慢右快→左拐弧线
void turnRight(int spd){ leftMotor(+1, spd); rightMotor(+1, spd/3); } // 左快右慢→右拐弧线
void spin(int dir, int spd) { // dir=+1 顺时针 / -1 逆时针 原地自转
leftMotor(dir, spd); rightMotor(-dir, spd); // 左右反向→零半径旋转
}
void stop() { leftMotor(0, 0); rightMotor(0, 0); }
看 turnLeft/turnRight:我没让一个轮子完全停下,而是给慢侧留了 spd/3,这样转出来的是平滑弧线而不是急拐,整车更稳。想要更急的弯,把慢侧调成 0;想要原地转,那就是 spin() 的活儿。六个动作全部归结为"给左右轮各一个方向和一个速度"——这就是差速底盘的全部运动逻辑,没有第七种动作。
电机的供电一定要和主控(ESP32/Arduino)的供电物理分离:电机用独立电池或电源进驱动板的 VM,主控用自己的 5V/3.3V。但两者的 GND 必须共地(接到一起),否则信号电平没有共同参照,PWM 和方向控制会乱跳甚至烧端口。电机启停瞬间会拉出大电流,绝不能让它从主控的稳压输出取电——这是新手最容易烧板子的一步。
跑偏的根因:为什么你迟早要装编码器
现在回到开头的问题。你调用 forward(200),左右轮拿到的是完全相同的 PWM 占空比 200,按理该一样快。可它就是跑偏,为什么?
因为 PWM 占空比控制的是"加在电机上的平均电压",不是转速。同一个占空比下,两个电机的实际转速会因为这些差异而不同:
- 电机本身的制造公差,同型号也有个体差异,绕组、磁钢都不可能完全一致;
- 两侧的机械阻力不同,齿轮箱松紧、轴承摩擦、轮子安装的同轴度;
- 车身重量分布不均,压在某侧轮子上的负载更大;
- 电池电压在掉,左右驱动通道的压降也未必对称。
结果就是:你下达"一样快"的指令,电机收到的是"一样的电压",但它们跑出来的速度不一样,于是车就偏。你可以手动给慢的一侧多加点 PWM 来补偿(比如 forward 里左轮 200、右轮 195),但这是死参数——电池一掉电、地面一换、负载一变,补偿值就失效,车又开始偏。
这正是差速底盘逼着每个人面对的第一个真相:开环控制(只给指令、不看结果)走不直线。 想真正走直,你得知道每个轮子实际转了多少,再实时调整 PWM 让两边对齐。测量"实际转了多少"的传感器,就是编码器;用测量值反过来纠正输出的控制方法,就是闭环。
这个坑我们不在本篇填,但它是后面的主线——编码器与 PID 闭环 会专门讲怎么把跑偏这件事彻底治好。现在你只要记住:开环差速能让你跑起来、能做 Demo,但凡是要"精确"的场景,编码器闭环迟早要补上。
底盘选型:套件、车架与打滑
硬件这块给你几条不绕弯的建议:
- 直接买两驱小车套件。 某宝上"智能小车底盘 2WD"几十块一套,含 TT 减速电机×2、轮子、万向轮、车架、电池盒,孔位都对好了。新手别自己拼,省下的时间拿去调代码。
- 车架材质:亚克力轻便、金属结实。 亚克力(透明塑料板)便宜、轻、打孔方便,做学习车足够;金属车架更刚、不易变形,适合后面加云台、机械臂这种重负载,但贵且重。学习阶段亚克力够用。
- 轮子打滑是大敌。 塑料光轮在瓷砖、玻璃上抓地差,原地自转时容易打滑空转,导致你以为转了 90 度实际只转了 70 度。换橡胶包胶轮、或在轮子上缠一圈防滑胶带,能立竿见影。打滑还会让后面装的编码器"算的转了多少"和"车实际走了多少"对不上——这是闭环也救不了的物理误差,从轮子上解决最划算。
- 电机选 TT 减速电机起步。 那种黄色塑料齿轮箱的 TT 电机扭矩够、便宜、配套轮子现成,是事实标准的入门款。等你嫌它转速不稳、要做精确里程时,再升级带编码器的减速电机也不迟。
你应该看到什么
接好线、烧完上面的代码,写个简单主程序逐个测试动作,你应该看到:
- 调
forward(200):小车向前走(可能略偏,这正常,先确认方向对);调backward(200)倒退。 - 调
turnLeft(200):小车向左画弧前进;turnRight(200)向右画弧。 - 调
spin(+1, 180):小车原地打转,车身中心基本不挪窝,只是绕着自己转圈。这是验证差速底盘最爽的一刻——它真的能在原地掉头。 - 调
stop():两轮立即停。
如果某个动作方向反了、或者一侧不动,别慌,照下面的排查表逐项对。
故障排查表
| 现象 | 大概率原因 | 怎么修 |
|---|---|---|
| 走不直、明显跑偏 | 两电机同占空比下实际转速不同(正常现象) | 临时给慢侧补点 PWM;根治要上编码器闭环 |
| 某个动作整体转向反了 | 该侧方向引脚 IN1/IN2 接反,或电机两根线接反 | 软件里对调该侧 dir 极性,或硬件对调电机两线 |
| 一个轮子完全不转 | 该路驱动没使能、PWM 引脚没接对、或电机线松 | 查 STBY/使能脚、查 PWM 接线、查电机端子是否拧紧 |
| 原地自转转不动/打滑空转 | 轮子抓地差,或转速太低扭矩不够 | 换橡胶轮/缠防滑胶带;spin 的速度调高一点 |
| 一启动就猛冲、撞东西 | PWM 给太大、急启停没缓冲 | 起步速度调小;用下面的渐变加速变体 |
两个值得加的变体
变体一:加速度渐变,告别急启停。 上面六个动作是"一脚油门到底",电机瞬间从 0 跳到 200,既费电又容易打滑、让车头一冲。给它加个平滑过渡:
// 把当前速度在若干步里渐变到目标速度,避免猛冲
void smoothForward(int target, int step = 10, int gap = 15) {
for (int s = 0; s <= target; s += step) {
forward(s);
delay(gap); // 每步停一小会儿,让速度爬上去
}
forward(target);
}
起步丝滑很多,打滑也少了。停车时同理可以从 target 渐变回 0。
变体二:遥控起步。 把六个动作接到一个数据源上——蓝牙串口收一个字符 'w'/'s'/'a'/'d'/' ' 分别映射 forward/backward/turnLeft/turnRight/stop,你就有了一台手机遥控车。这是验证底盘逻辑最直观的方式,也是后面做自动导航前的"手动挡"练手。
动手挑战:走一个正方形,回到起点
光走直线不过瘾,给你个能暴露问题的挑战:让小车走出一个边长 1 米的正方形,最后回到起点、车头朝向也回到出发时的方向。
思路很直白——重复四次"直行一条边 + 原地右转 90 度":
void drawSquare() {
for (int i = 0; i < 4; i++) {
forward(180); delay(2000); // 走一条边(时间靠试,约 1 米)
stop(); delay(300);
spin(+1, 180); delay(700); // 原地右转约 90 度(时间靠试)
stop(); delay(300);
}
}
烧进去跑一次,你大概率会发现:它走不回起点。 终点和起点差了一截,车头方向也歪了。再多跑几圈,误差越滚越大,轨迹从正方形变成歪扭的螺旋。
这就是这个挑战的全部意义——你亲手摸到了开环控制的误差累积。每条边因为跑偏多走/少走一点,每次转弯因为打滑多转/少转几度,单次误差很小,但四条边四个弯叠下来就明显了,而且没有任何机制把它纠回来。你用 delay 的时间去近似"走 1 米""转 90 度",可时间不等于距离、不等于角度,电池一掉电同样的 delay 走的距离都变了。
记住这个歪掉的正方形。它就是你需要编码器闭环最有力的理由。
小结与下一步
差速驱动底盘的全部运动逻辑,就是一句话:左右两轮各给一个方向和一个速度,靠速差走出直行、弧线和原地自转。 你把这件事封装成六个动作函数,主程序就能用人话指挥小车。但开环的差速注定走不直、回不到起点——根因是 PWM 控的是电压不是转速,而这正是把你引向编码器闭环的钩子。
下一步,在这台能动的底盘上装"眼睛":循迹小车:让它沿着黑线走 会给底盘接上红外传感器,做出第一个能自己判断、自己纠偏的闭环行为——你会发现,循迹其实就是用本篇的六个动作 + 一点点判断逻辑拼出来的。底盘是骨架,从这里开始,小车要长出感官了。