
简介一份面向嵌入式初学者的Arduino循迹小车源码包整合完整项目代码与配套说明适合正在学习智能小车、传感器检测及电机调速的开发者参考。包内共3个文件包括HTML说明页、inscode代码片段及gitignore版本控制文件压缩包仅6KB轻量而便于快速查阅。源码对循迹原理做了细致拆解传感器将模拟信号转换为数字信号依据左右轮差速实现转向代码覆盖初始化设置、循迹逻辑处理及电机驱动控制并分情况解析了小左转、小右转、大左转、大右转等不同路径状态的判定逻辑。调速部分利用PWM脉冲宽度调制调节电机转速进而控制小车的加减速与运动方向。针对调试中的信号不稳定、电机响应慢、小车偏航等问题也给出了增加滤波算法、调整电机驱动参数、重新校准传感器位置等解决思路。目前已有448人学习下载适合课程设计、竞赛备赛或业余DIY时对照实践。 如果你正在折腾Arduino循迹小车大概率已经过了“硬件一团乱、线插得满桌飞”的阶段真正卡住你的往往是代码逻辑为什么传感器亮灯了小车却像喝醉了一样乱冲为什么明明对着黑线转向却慢半拍这篇我直接把这套循迹小车的源码按模块拆开讲透从传感器采样、阈值校准到电机控制和PID转向每一步都会说明为什么这么写、参数怎么定。适合已经在用或准备买Arduino Uno做智能小车、但又不想只抄别人整段代码的初学者也适合正在准备竞赛、想搞懂原理再改车的朋友。1. 项目整体设计与硬件逻辑1.1 循迹小车到底在“循”什么循迹小车本质上是一个“感知-决策-执行”的闭环系统。感知部分负责告诉单片机“我现在在线的什么位置”决策部分根据位置偏差算出“该往左还是往右、力度多大”执行部分通过电机驱动完成转向和前进。大部分新手项目用的都是“寻黑线”方案白色赛道上一道黑色引导线小车沿黑线跑。为什么选这个方案因为黑白交界处的反光率差异足够大用五路红外对管就能稳定识别不需要上摄像头和图像处理单片机的负担小实时性也高。摄像头方案虽然信息量大但在入门阶段会引入边缘检测、透视矫正等问题调试成本直接翻几倍。感知部分我推荐五路红外对管而不是三路。三路只能做“左偏/正中/右偏”三档判断转向是离散的小车跑快了容易蛇形五路可以算出连续误差值后面配合PID能把转向做得非常平滑。1.2 为什么选 Arduino Uno 红外对管 L298N这套组合几乎是循迹小车入门的标准答案。Uno用的是ATmega328P16MHz主频、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接了PWMIN1~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配置。我自己现在还会在这套框架上继续做灰度精细化识别和速度自适应控制如果你有兴趣可以从这个版本的基础框架继续迭代。祝你的小车早日跑完一圈不飞线。本文还有配套的精品资源点击获取