简介:一份面向嵌入式初学者的Arduino循迹小车源码包,整合完整项目代码与配套说明,适合正在学习智能小车、传感器检测及电机调速的开发者参考。包内共3个文件,包括HTML说明页、inscode代码片段及gitignore版本控制文件,压缩包仅6KB,轻量而便于快速查阅。源码对循迹原理做了细致拆解:传感器将模拟信号转换为数字信号,依据左右轮差速实现转向;代码覆盖初始化设置、循迹逻辑处理及电机驱动控制,并分情况解析了小左转、小右转、大左转、大右转等不同路径状态的判定逻辑。调速部分利用PWM脉冲宽度调制调节电机转速,进而控制小车的加减速与运动方向。针对调试中的信号不稳定、电机响应慢、小车偏航等问题,也给出了增加滤波算法、调整电机驱动参数、重新校准传感器位置等解决思路。目前已有448人学习下载,适合课程设计、竞赛备赛或业余DIY时对照实践。 如果你正在折腾Arduino循迹小车,大概率已经过了“硬件一团乱、线插得满桌飞”的阶段,真正卡住你的往往是代码逻辑:为什么传感器亮灯了,小车却像喝醉了一样乱冲?为什么明明对着黑线,转向却慢半拍?这篇我直接把这套循迹小车的源码按模块拆开讲透,从传感器采样、阈值校准,到电机控制和PID转向,每一步都会说明为什么这么写、参数怎么定。适合已经在用或准备买Arduino Uno做智能小车、但又不想只抄别人整段代码的初学者,也适合正在准备竞赛、想搞懂原理再改车的朋友。
1. 项目整体设计与硬件逻辑
1.1 循迹小车到底在“循”什么
循迹小车本质上是一个“感知-决策-执行”的闭环系统。感知部分负责告诉单片机“我现在在线的什么位置”,决策部分根据位置偏差算出“该往左还是往右、力度多大”,执行部分通过电机驱动完成转向和前进。
大部分新手项目用的都是“寻黑线”方案:白色赛道上一道黑色引导线,小车沿黑线跑。为什么选这个方案?因为黑白交界处的反光率差异足够大,用五路红外对管就能稳定识别,不需要上摄像头和图像处理,单片机的负担小,实时性也高。摄像头方案虽然信息量大,但在入门阶段会引入边缘检测、透视矫正等问题,调试成本直接翻几倍。
感知部分我推荐五路红外对管而不是三路。三路只能做“左偏/正中/右偏”三档判断,转向是离散的,小车跑快了容易蛇形;五路可以算出连续误差值,后面配合PID能把转向做得非常平滑。
1.2 为什么选 Arduino Uno + 红外对管 + L298N
这套组合几乎是循迹小车入门的标准答案。Uno用的是ATmega328P,16MHz主频、32KB Flash,跑几路传感器的逻辑绰绰有余,而且Uno的库和教程最多,遇到问题搜一下基本都有答案。
红外对管模块通常长这样:一个发射管一个接收管,发射管持续发红外光,接收管根据反射量输出不同电平。在黑线上方,红外被黑色吸收,接收管收到的反射小,输出高电平(数字读出来是1);在白色地面,红外被反射,输出低电平。不同模块可能逻辑相反,这点一定要先确认。
L298N电机驱动里的关键点在于它内部是H桥结构,通过IN1/IN2两路电平组合控制电机正反转,通过ENA使能端的PWM占空比控制速度。我见过很多新手把PWM接到IN引脚上,结果电机要么不转要么全速转,就是因为没搞懂H桥的逻辑。
提示:模块供电不要和Arduino的5V混着用,L298N的电机电源单独接7.2V以上电池组,逻辑电源再接Uno,否则电机一启动,单片机就容易复位。
2. 代码主流程与工程框架
2.1 引脚定义与初始化
我们先把引脚规划写清楚。这里给出一份顺手就能用的定义:
// 五路红外传感器 const int sensorPins[5] = {A0, A1, A2, A3, A4}; // L298N驱动 const int ENA = 9; // 左电机PWM const int ENB = 10; // 右电机PWM const int IN1 = 6; const int IN2 = 7; const int IN3 = 8; const int IN4 = 4;写初始化代码时,我发现很多初学者会漏掉一个关键点:analogRead不需要pinMode,但数字输出引脚必须有pinMode(pin, OUTPUT)。而且PWM引脚必须选支持PWM输出的脚位,Uno上9、10是高频率PWM脚,适合电机控制,3、5、6也可以,但我习惯把PWM和方向引脚分开管理,这样查线方便。
void setup() { Serial.begin(9600); for (int i = 0; i < 5; i++) { pinMode(sensorPins[i], INPUT); } pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); }2.2 主循环是怎么跑起来的
主循环不需要写得花里胡哨,核心就三步:读传感器、算偏差、控电机。我的框架是这样:
void loop() { int error = readLineError(); // 1. 读五路传感器,算位置偏差 int correction = computePD(error); // 2. 用PD算法算出修正量 driveMotor(correction); // 3. 根据修正量控制左右电机 }很多人会在这个循环里加delay(50)想让它跑慢一点,我劝你趁早去掉。delay会让整个程序停下来,传感器不采样、电机不响应,车跑快了根本来不及纠正,结果就是冲出赛道。想让电机速度慢一点,应该减小PWM占空比,而不是用延时拖慢循环。
另外要强调一点:analogRead每次转换大约要100微秒,五路读下来也就0.5毫秒,再加上PD计算和analogWrite,整个循环周期1毫秒左右,完全够用。不要在这个基础上反复加延时或过多的Serial.print,调试时打印没问题,跑车时最好关掉,否则串口输出会占用相当一部分CPU时间,影响响应速度。
3. 循迹核心:传感器采样与阈值校准
3.1 为什么必须做阈值校准,而不是写死判断
新手最常见的写法是if (analogRead(A0) < 300),这个300是怎么来的?大概率是拿传感器对着地面看串口数据估出来的。问题在于:环境光变化、地面颜色深浅、传感器安装高度、电池电压下降,都会让阈值失效。上午调好的车,下午换个教室就可能乱跑。
所以规范的做法是每次上电先自动校准。校准的原理很简单:采样100次,记录每路传感器的最小值和最大值,然后取中值作为阈值。在校准过程中,你需要手动把小车放在赛道上左右摇晃,让所有传感器都扫过黑线和白色区域。这段校准代码我贴在这里:
int threshold[5]; void calibrateThreshold() { int minVal[5] = {1023, 1023, 1023, 1023, 1023}; int maxVal[5] = {0, 0, 0, 0, 0}; Serial.println("Calibrating... Move the car over the line!"); unsigned long startTime = millis(); while (millis() - startTime < 3000) { for (int i = 0; i < 5; i++) { int val = analogRead(sensorPins[i]); if (val < minVal[i]) minVal[i] = val; if (val > maxVal[i]) maxVal[i] = val; } delay(10); } for (int i = 0; i < 5; i++) { threshold[i] = (minVal[i] + maxVal[i]) / 2; Serial.print("Sensor "); Serial.print(i); Serial.print(" threshold: "); Serial.println(threshold[i]); } }校准时你把小车拿在手里,在三秒内让它五路轮着压过黑线就行,重点是让每一路都见过黑白两种状态。如果某一路的max和min差值很小,说明它可能没扫到线,或者是安装位置有问题。
3.2 传感器状态怎么组合成位置误差
阈值有了,接下来把模拟值转成数字状态,再算误差。这里我给每路一个权重:左二为-2,左一为-1,中间为0,右一为1,右二为2。哪一路压到黑线,就把对应的权重相加,得到误差值。
int readLineError() { int error = 0; int weight[5] = {-2, -1, 0, 1, 2}; for (int i = 0; i < 5; i++) { int val = analogRead(sensorPins[i]); if (val < threshold[i]) { // 假设低于阈值表示压到黑线 error += weight[i]; } } return error; }这个误差值范围在-6到+6之间。实际上五路全压黑线的情况很少见,常见组合就是中间三路各有一位为0,其他为1。误差值为正说明车偏左了(因为右侧传感器压线),负值说明偏右。
提到一点:有些模块上电时电平逻辑和这个相反,黑线时输出低电平、白线输出高电平,所以if (val < threshold[i])这个判断需要根据你的模块调整。判断逻辑反了问题也不大,就是车会往反方向跑,还挺容易发现的。
4. 电机控制与差速转向实现
4.1 PWM调速与L298N接线要点
电机这部分我单独拿出来讲,是因为90%的“车不动”问题都出在这里。L298N的ENA/ENB接了PWM,IN1~IN4接方向控制。同一个电机的两路IN电平必须相反,电机才转得起来;如果两路都是HIGH或都是LOW,电机是刹车的。
我的电机控制函数长这样:
void driveMotor(int leftSpeed, int rightSpeed) { leftSpeed = constrain(leftSpeed, -255, 255); rightSpeed = constrain(rightSpeed, -255, 255); digitalWrite(IN1, leftSpeed >= 0 ? HIGH : LOW); digitalWrite(IN2, leftSpeed >= 0 ? LOW : HIGH); digitalWrite(IN3, rightSpeed >= 0 ? HIGH : LOW); digitalWrite(IN4, rightSpeed >= 0 ? LOW : HIGH); analogWrite(ENA, abs(leftSpeed)); analogWrite(ENB, abs(rightSpeed)); }速度值用正负号表示方向,正数前进、负数后退,constrain先把值限制在有效范围内,再用abs取绝对值给PWM。这样不管误差是多少,电机都不会收到越界值。
4.2 转向策略:从“开关控制”到“比例输出”
入门代码里最常见的是这种写法:左边压线就往右猛打,右边压线就往左猛打。这能跑,但看起来很僵硬,赛道稍微弯急一点,车就左右甩。我的做法是把转向力度和误差值挂钩,误差越大,速度差越大。
int baseSpeed = 140; void simpleFollow() { int error = readLineError(); int leftSpeed = baseSpeed; int rightSpeed = baseSpeed; if (error < 0) { // 车偏右,需要左转:左轮减速,右轮加速 leftSpeed = baseSpeed + error * 10; // error是负数,所以左轮会减速 rightSpeed = baseSpeed - error * 10; } else if (error > 0) { // 车偏左,需要右转 leftSpeed = baseSpeed - error * 10; rightSpeed = baseSpeed + error * 10; } driveMotor(leftSpeed, rightSpeed); }这段代码里error * 10就是比例系数,你可以手动调。这个方案的好处是在误差小的时候转向平缓,误差大时转向激进,比单纯的开关控制自然得多。但它还是有个问题:车在直道上如果持续有个微小偏差,它只会一直轻微转向,积累下来还是会偏。这时候就该上PID了。
5. PID循迹算法与调参实战
5.1 误差从哪来:五路传感器的位置权重
PID里面的“P”指的就是比例(Proportional),它做的事情和上面error * 10是一样的。但PID完整版不只是看当前偏差,还要看偏差的变化量和历史积累,我用一个生活化的例子解释:
你用手去够一个来回晃动的吊灯,光看当前位置去够,总是不够稳,因为你没考虑它正在往哪个方向飞;如果加上对“它正在朝你飞过来的速度”的判断,你就能在它到之前提前伸手,这就是微分项的直觉。至于积分项,相当于你在反复调整中发现“每次都差一点点”,于是越补越大,最终把误差吃掉。
对循迹小车来说,传感器读到的误差就是“位置偏差”,PD通常就够用了。五路传感器权重我已经给了,需要说明的是:误差值不一定要是整数,你可以用模拟值做加权平均,得到更精细的误差,但入门阶段用整数权重足够。
5.2 P、I、D三个环分别在干嘛
- P项:根据当前误差直接输出修正,误差大就猛纠,误差小就轻纠。
- I项:把一段时间内的误差累积起来再修正,能消除稳态误差。但循迹小车是动态系统,积分容易积过头,跑快了会明显冲过头,所以我一般建议把积分项设小一点甚至去掉。
- D项:根据误差变化率提前做抑制,误差在快速增大时,它会产生反向力把它压住。这是防止小车左右蛇形的关键。
我实际用的PD控制代码贴出来:
float Kp = 22.0; float Kd = 90.0; int baseSpeed = 150; int lastError = 0; int computePD(int error) { int derivative = error - lastError; lastError = error; return (int)(Kp * error + Kd * derivative); } void pidFollow() { int error = readLineError(); int correction = computePD(error); int leftSpeed = baseSpeed - correction; int rightSpeed = baseSpeed + correction; driveMotor(leftSpeed, rightSpeed); }注意correction的符号:假设误差为正说明车偏左了,往右转需要右轮加速、左轮减速。上面代码里baseSpeed - correction会让左轮减速,baseSpeed + correction会让右轮加速,方向正好正确。
5.3 PD调参步骤与踩坑记录
调参是个体力活,但讲究顺序。我一般这样调:
- 先把Kd设为0,只留Kp,从10开始慢慢加,调到车能勉强过弯、但会轻微左右晃为止。
- 然后把Kd加上去,从50开始,一次加20左右。D项会明显让车“稳住”,你调到它能平稳过弯、不再蛇形,就说明差不多。
- 如果Kp太大,车会冲过头;Kd太大,转向会变得非常迟钝,甚至原地抖。
调参时记得把电池充满再调,因为电池电压直接影响电机转速,同一个参数在满电和低电下表现完全不同,很多“昨天还好好的今天跑偏”的灵异事件都跟电压有关。
我踩过的最大一个坑是lastError没在刚开始时初始化,导致一上电第一个D值异常大,车猛地一甩。后来我在启动时先把误差读一遍填到lastError里,问题就没了。
6. 调试实录与常见问题排查
6.1 常见问题速查表
这部分直接整理成表格,方便你遇到问题快速对号入座。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 上电后完全不动 | 电机供电没接、L298N逻辑电源没接 | 先用万用表量电机电源电压;确认L298N的5V使能跳线帽是否插好 |
| 小车只会全速前进、不会转向 | IN引脚接错或PWM没接 | 手动给各IN引脚赋电平,确认每个电机方向对不对 |
| 跑起来左右剧烈甩头 | Kp偏大、Kd偏小 | 降低Kp、加大Kd,先把D项加到50%再细调 |
| 直道跑不远就偏出去 | 阈值没校准或者安装高度不对 | 重新执行校准,确认传感器离地高度在1~2cm |
| 过弯时冲出去 | baseSpeed太高、PWM输出不够 | 先把baseSpeed降到120再调;弯道可以额外加转向限速逻辑 |
| 电机转动但车不走 | 轮胎打滑或电压不足 | 给驱动轮增大摩擦表面,换大扭矩电机或提高供电电压 |
6.2 串口打印与Wokwi仿真辅助调参
调试时别全靠眼睛看,串口才是你的“仪表盘”。我习惯在loop里打印误差值error和修正量correction,用串口曲线窗口观察波形。如果误差波形是一个稳定的小范围波动,说明控制很健康;如果波形上下剧烈震荡,就是参数不合适。
如果你手上暂时没有硬件,或者不想一遍遍蹲在地板上放车,我推荐用Wokwi这个在线仿真平台。它在浏览器里能模拟Arduino Uno、传感器、电机驱动,还能串口看输出,特别适合验证代码逻辑、跑通PID框架。等仿真逻辑没问题了,再上真车调参,效率高很多。
6.3 个人踩坑心得与扩展方向
最后分享几个实操里容易忽略的细节。传感器支架一定要固定牢,我用热熔胶固定后跑着跑着还是会松,后来换成3D打印的支架加螺丝锁紧,效果明显稳定。另外传感器的间隔也影响循迹精度:间隔太小,过弯时容易丢线;间隔太大,直道上对误差的分辨率不够。我试下来五路传感器中心间距在1.5cm到2cm之间比较舒服。
这个框架的扩展性也很好,你可以把PID改成串级控制,或者加一个蓝牙模块在手机上看实时参数。如果换用ESP32、STM32这类平台,代码逻辑完全通用,只需要改底层引脚和PWM配置。我自己现在还会在这套框架上继续做灰度精细化识别和速度自适应控制,如果你有兴趣,可以从这个版本的基础框架继续迭代。祝你的小车早日跑完一圈不飞线。
本文还有配套的精品资源,点击获取