简介:智能小车是嵌入式系统与机器人控制的经典入门项目,其运动控制核心在于差速驱动模型:通过左右驱动轮的转速差实现前进、转弯与原地旋转,而万向轮仅作支撑。Arduino UNO作为主控平台,负责读取循迹传感器、超声波或蓝牙指令,并输出PWM信号驱动电机,从而完成预期的轨迹运动。理解差速运动学的基本公式、电机驱动板的选型与引脚映射,是让小车稳定行走的前提。在此之上,循迹避障、蓝牙遥控等工程实践进一步将底层控制原语与状态机结合,形成可复用的软件架构。本文从三轮底盘的运动学原理出发,解析基于Arduino UNO的例程设计思路,并给出硬件接线、参数标定与调试排查方法,帮助初学者快速掌握从电机驱动到智能控制的完整链路。
1. Arduino UNO 3轮智能小车例程:先把差速运动学想清楚
毕设文件包里放着一个基于arduino UNO 3轮智能小车例程.zip,多数人第一件事是解压烧录,结果电机一边转一边不转。三轮小车的本质是差速运动学:两个驱动轮承担牵引,一只万向轮只做支撑,前进、后退、转弯全部靠左右轮转速差实现。Arduino UNO 在这里是主控,负责读取循迹传感器、超声波或手机蓝牙下发的指令,再用两路 PWM 控制电机驱动板。这套例程要解决的是从“电机能转”到“小车按期望轨迹走”之间的工程问题:引脚映射、驱动使能、差速换算和阈值标定。适合刚接触 Arduino 的课设生,也适合答辩前要把每个参数讲出依据的人。
2. 三轮小车的底盘选型与 Arduino UNO 接线
解压 zip 之后不要急着打开 .ino。先把硬件清单一字排开:底盘、两个直流减速电机、一块电机驱动板、Arduino UNO、5V 供电和电池。例程包的文件一般按电机、循迹、避障、蓝牙拆成独立目录或单独 .ino,接线表在注释里写得最全,先按 2.3 的表接完板子,再看代码才有意义。
2.1 为什么课设三轮比四轮更好写
四轮车如果四个轮子都做驱动,转向要么引入阿克曼梯形,要么靠一侧打滑实现滑移转向,两种方案对底盘精度和程序复杂度要求都高。三轮差速底盘只有两个驱动轮加一只万向轮,转向中心落在两驱动轮连线中点附近,运动模型就一组公式:线速度 v=(vL+vR)/2,角速度 ω=(vR−vL)/W,转动半径 R=W(vL+vR)/(2(vR−vL)),其中 W 是左右轮轮距。例程里所有转向函数,最后都是在往这组公式里填数;答辩时老师追问的往往也是这个,而不是“用了什么高级算法”。
万向轮要选带滚珠的转向轮,不要用固定硬脚。三轮车重心落在两驱动轮轴线附近,急加速时前端万向轮阻力一大,车头就打横,循迹表现为过弯甩尾。常见配法是 6.5cm 尼龙万向轮加 TT 电机,或者 GA12-N20 减速电机加定制支架,后者扭矩大、虚位小,适合跑直线。
2.2 电机驱动板对照:L298N、TB6612 与 DRV8833
驱动板是接线最容易出错的一环。三块常见板子的对比:
| 驱动板 | 每路持续电流 | 逻辑电压 | 供电范围 | 特点 | 课设适用度 |
|---|---|---|---|---|---|
| L298N | 2A 标称,1A 以下稳妥 | 5V | 6~12V | 便宜、压降约 2V、重、发热大 | 一般 |
| TB6612FNG | 1.2A,峰值 3A | 3.3/5V | 2.7~13.5V | MOSFET 桥、压降小、带 STBY 使能 | 高 |
| DRV8833 | 1.5A | 2.7~5.5V | 2.7~10.8V | 体积小、逻辑电压低 | 中 |
Arduino UNO 的 5V 引脚来自 USB 或 Vin 上的板载稳压器,容量只够传感器和逻辑电路用;电机堵转电流动辄 1A 以上,绝不能从 5V 引脚取电。驱动板单独接电池,然后与 Arduino 共地。课设我一般选 TB6612:逻辑接口兼容 5V,压降低,两节 18650 串联的 7.4V 输入到电机端还能剩 7V 左右;同样电压下 L298N 因为功率管压降大,转速明显偏低,例程里标定好的 base 值要整体往上调。
2.3 以 TB6612 为例的引脚映射与接线表
这套例程我习惯把两路 PWM 放在 UNO 的 D5、D6,方向信号放 D7~D10,STBY 放 D4,为循迹、超声波、蓝牙把 D2、D3、D11、D12、A0、A1 都留出来。
| Arduino 引脚 | TB6612 / 外设 | 说明 |
|---|---|---|
| D5 | PWMA | 左电机调速(PWM,约 976Hz) |
| D6 | PWMB | 右电机调速(PWM,约 976Hz) |
| D7 / D8 | AIN1 / AIN2 | 左电机方向 |
| D9 / D10 | BIN1 / BIN2 | 右电机方向 |
| D4 | STBY | 使能,必须为 HIGH |
| 5V / GND | VM / GND | 驱动板逻辑供电与共地 |
选 D5、D6 不是顺手写的。UNO 的 PWM 引脚分布在三个定时器上:D5、D6 属于 timer0,默认约 976Hz;D9、D10 属于 timer1,约 490Hz。两个电机用同一频率,起步和微调的声感一致,直行更均匀;如果一个在 timer0 一个在 timer1,会听出明显的“双频电机啸叫”。
const int AIN1 = 7; // 左电机方向信号 A const int AIN2 = 8; // 左电机方向信号 B const int PWMA = 5; // 左电机调速,timer0 通道 const int BIN1 = 9; // 右电机方向信号 A const int BIN2 = 10; // 右电机方向信号 B const int PWMB = 6; // 右电机调速,timer0 通道 const int STBY = 4; // TB6612 使能引脚 void setup() { pinMode(AIN1, OUTPUT); // 方向信号按数字输出配置 pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(STBY, OUTPUT); digitalWrite(STBY, HIGH); // 使能必须优先于任何 PWM 输出 analogWrite(PWMA, 0); // PWM 归零,防上电窜动 analogWrite(PWMB, 0); } void loop() { digitalWrite(AIN1, HIGH); // 左轮正转 digitalWrite(AIN2, LOW); analogWrite(PWMA, 150); // 占空比 150/255,约 59% digitalWrite(BIN1, HIGH); // 右轮正转,同速即直行 digitalWrite(BIN2, LOW); analogWrite(PWMB, 150); delay(2000); digitalWrite(AIN1, LOW); // 左轮反转 digitalWrite(AIN2, HIGH); digitalWrite(BIN1, HIGH); // 右轮仍正转,形成原地旋转 digitalWrite(BIN2, LOW); delay(2000); }这段程序只验证一件事:电机接线和驱动板工作正常。先直行 2 秒再原地差速转 2 秒;如果有一个轮子不转,交换该电机在驱动板上的 OUT 接线重新测,能区分是板子通道坏还是电机坏。analogWrite第二参数是占空比不是电压,150 表示 150/255。TB6612 的 AIN1/AIN2 同时为低输出刹车,同时为高是急停,理解这点能避开“电机嗡嗡响但不转”的排查方向。
注意:STBY 同时控制左右两路,忘记拉高是“烧录成功但轮子毫无反应”的头号原因。示例把它接在 D4,也可以直接接 5V,但保留引脚能在软件里做总开关。之后若用 TCCR 寄存器重映射 PWM 频率,不要动 timer0(D5/D6),否则 millis() 和 delay() 的时间基准会被一起改掉。
3. 用最小例程把 Arduino UNO 的电机控制封装成一个原语
上一章的 setup 只做硬件验证。真正进入例程结构,第一步是写一个统一的电机输出函数,后面循迹、避障、蓝牙全部复用这一层,业务代码里不要再出现裸的 digitalWrite 和 analogWrite。
3.1 run(left, right):把方向与速度合并
void motorWrite(int in1, int in2, int pwmPin, int speed) { speed = constrain(speed, -255, 255); // 越界值截断到合法区间 if (speed >= 0) { // 正数正转 digitalWrite(in1, HIGH); digitalWrite(in2, LOW); } else { // 负数反转,取绝对值输出 digitalWrite(in1, LOW); digitalWrite(in2, HIGH); speed = -speed; } analogWrite(pwmPin, speed); } void run(int left, int right) { motorWrite(AIN1, AIN2, PWMA, left); // 左电机 motorWrite(BIN1, BIN2, PWMB, right); // 右电机 } void setup() { pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(STBY, OUTPUT); digitalWrite(STBY, HIGH); // 使能一次到位 Serial.begin(9600); // 调试串口 } void loop() { run(150, 150); // 直行 800ms delay(800); run(80, 150); // 左转:左慢右快 delay(800); run(-150, 150); // 原地旋转:左右反号 delay(800); run(0, 0); // 刹车 Serial.println("cycle done"); // 标记一次周期完成 delay(1000); }constrain不是可选项。遥控或导航计算一旦出现越界速度,未限幅直接输出会让电机瞬间满转,小车窜出去撞墙。run(-150,150)表示左右轮反方向等速,车围绕两轮连线中点原地旋转,这是差速底盘转弯半径最小的动作。串口打印放在 loop 末尾,串口监视器里看不到 “cycle done” 时,说明代码根本没跑到那个位置,先查前一行。
3.2 速度怎么给:base、turn 与最小转弯半径
例程里最常见的写法是定义一个基准速度 base 和一个转向增量 turn,业务层做组合:
int base = 130; // 直行基准,占空比约 51% int turn = 70; // 转向增量,决定转弯半径 // 左转:左轮减速、右轮加速 run(base - turn, base + turn); // 右转:右轮加速、左轮减速 run(base + turn, base - turn);参数有三个约束:base+turn 不能超过 255,否则高速轮丢速度;base−turn 不能低于 30 左右,否则低占空比下启动转矩不足,小车直线变画龙;turn 是最直接的转弯半径旋钮。把左转参数代入 2.1 的公式:vL=base−turn、vR=base+turn,vR−vL=2·turn,得到 R=W·base/(2·turn)。轮距 W 取 14cm,base=130、turn=70 时 R≈13cm;turn 降到 40,R≈23cm。循迹赛道弯道变缓,优先降 turn 而不是降 base,速度损失更小。
还要留意占空比与转速不是线性关系。TT 电机在中低占空比存在死区,0~40 之间可能不转,40~120 转速爬得快,120 以上才进入接近线性的工作段。所以 base 的经验区间是 120~160,低于这个区间先考虑垫高工作点,而不是急着重写 PID。
3.3 烧录验证:IDE 设置与最省事的检查顺序
Arduino IDE 里开发板选 “Arduino Uno”(ATmega328P),端口选 CH340 或 USB-ACM 对应的串口号;Windows 设备管理器看不到端口一般是 CH340 驱动没装。用 VSCode 加 Arduino 扩展编译也没问题,但板型和端口选择逻辑与 IDE 一样,报错第一反应还是查这两项。烧录时若提示 avrdude stk500_recv 通信失败,把电机和驱动板的信号线拔掉再烧,UNO 在串口握手阶段被外部灌电流时会失败。
注意:验证顺序固定为 直行 → 原地转 → 挂传感器。直行跑偏先查左右轮胎压、万向轮阻力和两个电机是否同减速比;原地转画圈半径大则查左右轮 PWM 是否同频。例程在这个阶段就能演示“模块化封装”,答辩时把 run 的源码和这轮测试数据一起放出来,比只贴 main 里的 if/else 有说服力。
4. 循迹、避障与蓝牙遥控:智能小车例程三件套的参数调优
毕设最常见的要求就是把循迹、避障、遥控三件事凑齐。这三部分共用一套引脚分配,先看下面的接线总表,再逐个调参。
| 外设 | 引脚 | 说明 |
|---|---|---|
| 左循迹 TCRT5000 | D11 | D0 输出,黑线上为 1(以模块实测为准) |
| 右循迹 TCRT5000 | D12 | D0 输出 |
| 超声波 HC-SR04 | A0 / A1 | TRIG / ECHO |
| 蓝牙 HC-05 | D3 / D2 | RX / TX,软件串口 |
| 舵机(可选) | D13 | 超声波云台 |
4.1 TCRT5000 循迹例程:先把阈值读进串口再写逻辑
循迹模块的核心是 TCRT5000 反射式光电传感器加 LM393 比较器,输出一路数字电平。白纸反光强、黑线吸光弱,比较器翻转点靠板上电位器调。第一次接线不要凭感觉写逻辑,先把两个头的原始电平打出来看:
const int TRACK_L = 11; // 左循迹模块数字输出 const int TRACK_R = 12; // 右循迹模块数字输出 void setup() { Serial.begin(9600); pinMode(TRACK_L, INPUT); pinMode(TRACK_R, INPUT); } void loop() { Serial.print("L="); Serial.print(digitalRead(TRACK_L)); // 打印原始电平 Serial.print(" R="); Serial.println(digitalRead(TRACK_R)); delay(100); }模块悬在纸面上方 0.5~1.5cm,黑线上读到 1、白纸上读到 0(部分模块极性相反,取反即可)。电位器调到白纸和黑线都能稳定输出、不在临界点抖动的程度。循迹控制的四分支状态机是例程的标准答案:
void trackRun() { int l = digitalRead(TRACK_L); // 1 表示压到黑线 int r = digitalRead(TRACK_R); if (!l && !r) run(130, 130); // 都在白纸上:直线段直行 else if (l && !r) run(85, 150); // 左轮压线:车头偏右,左轮降速 else if (!l && r) run(150, 85); // 右轮压线:车头偏左,右轮降速 else run(130, 130); // 双线(十字/断点),保持直行冲过 }两路循迹只能纠偏不能预测,turn 要调得保守:缓弯道 turn 40~60,连续 S 弯 turn 给到 80 并同步降 base 到 110。过直角弯两路会同时出线,else 分支保持直行 100~150ms 让它冲过弯心;时间太长会冲出赛道。
4.2 HC-SR04 避障例程:pulseIn 超时与三段距离策略
避障逻辑比循迹更固定。Trig 接 A0、Echo 接 A1,测距函数:
const int TRIG = A0; // 触发引脚 const int ECHO = A1; // 回波引脚 float measureDistance() { digitalWrite(TRIG, LOW); delayMicroseconds(2); // 拉低稳定电平 digitalWrite(TRIG, HIGH); delayMicroseconds(10); // 至少 10us 触发脉冲 digitalWrite(TRIG, LOW); long us = pulseIn(ECHO, HIGH, 30000); // 30ms 超时 if (us == 0) return 999; // 超时按无障碍处理 return us * 0.034f / 2.0f; // 距离 = 声速 × 时间 / 2 }pulseIn 第三个参数是超时值,单位微秒。不加超时,Echo 悬空会让主循环卡死在等待回波里,小车直接停摆。30ms 对应约 5m 量程,避障用不完,但留足余量能避免近距离强反射丢波。0.034 是声速折半再换算到厘米的系数;气温低声速略降,测距差 1~2cm 属正常。
避障例程常见做法是三段式:大于 25cm 直行,15~25cm 减速转向,小于 15cm 后退转向。
void avoidRun() { float d = measureDistance(); if (d > 25) run(150, 150); // 安全区,直行 else if (d > 15) run(60, 150); // 警戒区,右轮加速左转绕过 else run(-100, 150); // 危险区,后退并左转 delay(50); // 每个周期留 50ms,防转向过冲 }超声波有约 2cm 盲区和 30° 波束角,单探头对斜前方的障碍容易漏检。例程常见的升级是给探头加舵机云台,左右各扫 90° 取最小距离:用servo.write()控制角度,按角度记忆距离数组。这是从例程自然地长出“创新点”的位置;但舵机瞬时电流 500mA 以上,不能从 UNO 5V 引脚取电,单独一路 5V 共地。
4.3 蓝牙遥控例程:HC-05 波特率与串口指令解析
蓝牙走软件串口,把硬件串口留给 USB 调试是最省事的做法。HC-05 的 RX 逻辑是 3.3V,UNO 的 TX 是 5V,中间串一个 1kΩ 电阻再进模块 RX,防止烧引脚。
#include <SoftwareSerial.h> SoftwareSerial bt(2, 3); // RX=D2, TX=D3,对应 HC-05 的 TX/RX void setup() { Serial.begin(9600); // USB 调试串口 bt.begin(9600); // 蓝牙串口,波特率与模块保持一致 pinMode(2, INPUT); // 软件串口 RX 脚 pinMode(3, OUTPUT); } void handleCmd() { if (bt.available() <= 0) return; // 无数据直接返回 char c = bt.read(); // 读取一字节指令 while (bt.available()) bt.read(); // 丢弃本帧剩余字节,防粘包 switch (c) { case 'F': run(160, 160); break; // 前进 case 'B': run(-160, -160); break; // 后退 case 'L': run(90, 160); break; // 左转 case 'R': run(160, 90); break; // 右转 case 'S': run(0, 0); break; // 停止 default: break; } } void loop() { handleCmd(); delay(20); // 20ms 轮询,约 50Hz 指令更新率 }HC-05 出厂常在 38400,HC-06 出厂常见 9600。模块上电前按住板载按键再上电进入 AT 模式,发AT+UART=9600,0,0把波特率固定到 9600 后再接线跑例程。Serial.read()读到的是 ASCII 码,switch 里必须写'F'而不是F,这是编译期最容易栽的坑。模式切换用一个全局变量 mMode:'1' 切循迹、'2' 切避障、'3' 切遥控,主循环按 mMode 调用 trackRun、avoidRun 或 handleCmd,三个状态机之间不互相抢电机,答辩时也讲得清。这套“底层驱动原语 + 三个模式状态机”的结构,以后换 STM32 或 ESP32 也只需要重写电机驱动层,业务逻辑原样平移。
5. 仿真验证与供电排错:让例程直接变成毕设作品
5.1 用 Wokwi 做第一轮逻辑验证
没有硬件或者硬件在别人手里时,Wokwi 这类浏览器仿真平台可以先跑逻辑。新建 Arduino Uno 项目,把代码原样贴进去,电机选直流电机组件,循迹信号用按键模拟电平翻转,超声波用虚拟距离组件改变回波,蓝牙直接靠仿真串口发字符。仿真验证的不是电压和转速,而是状态机分支:按键模拟压线时两个电机的 PWM 值对不对,串口发 'L' 时左轮是不是真的减速。
仿真通过后再烧真实板子,调试时间能砍掉大半。Wokwi 的串口监视器同样能看到 “cycle done” 这类标记。要注意仿真电机模型是理想线性的,真实电机有死区和负载特性,仿真里标定的 base 值上板后必须重新调一轮;仿真最大的价值是帮你确认引脚冲突和逻辑分支,而不是替代实物标定。
5.2 供电拓扑与两道检查清单
供电是例程上板后问题最多的环节。推荐拓扑:两节 18650 串联 7.4V 进驱动板 VM,驱动板 GND 与 UNO GND 共地,UNO 用 USB 或 Vin 单独供电;舵机云台另接一路 5V,不要并接在 UNO 5V 引脚。电池端并联 470μF 电解电容,吸收电机换向的反电动势尖峰,能明显减少转弯瞬间复位。
检查顺序固定为先硬件后软件:万用表量驱动板 VM 与 GND 确认电压范围,再量 STBY 引脚电平,不到 3.3V 以上就先查使能。软件侧建议把调试输出统一成定长帧,例如Serial.println("L=130 R=70 M=TRACK"),串口监视器里一眼看到当前模式、左右轮目标速度和状态机是否跳对了,比零散 print 好排查得多。最后记得 loop() 里不要放长 delay 阻塞,循迹与避障共用控制周期时,一次超过 100ms 的阻塞就会让小车丢响应。
本文还有配套的精品资源,点击获取