简介:这份资源面向Arduino入门学习者与嵌入式开发初学者,围绕循迹小车的完整源码展开,帮助读者理解传感器信号采集、差速转向与电机调速的实现思路。压缩包共3个文件,以inscode工程配置、html页面与gitignore为主,整体约6KB,体积轻量,便于快速导入开发环境查看与运行。目前已有451人学习下载,适合作为循迹小车项目的起步参考。代码部分覆盖初始化设置、循迹逻辑判断与电机驱动控制,并针对小左转、小右转、大左转、大右转等不同检测情况给出对应处理方式;调速环节借助PWM调节电机转速,从而控制小车运动状态。此外还整理了调试中常见的传感器信号不稳、电机响应偏慢、小车偏离路径等问题及解决思路,便于读者对照排查、动手修改,逐步掌握嵌入式开发的基本方法。
1. 从一堆跳线到稳定循迹:Arduino小车代码到底在写什么
很多人第一次拿到 Arduino 循迹小车套件,焊完电机驱动、接好五路红外探头,烧进去一段示例代码,小车要么原地打转,要么冲出赛道。问题往往不在硬件,而在代码里那几个没人讲清楚的环节:传感器阈值怎么定、PID 三个参数怎么调、电机差速怎么映射成 PWM。这篇笔记就把一套能跑稳的循迹代码拆开讲,从最小可跑版本一路改到带 PID 的版本,每一步都给源码和参数说明。适合刚接触嵌入式控制的学生、做课程设计的开发者,以及想把玩具级小车改成能跑复杂赛道的熟手。读完你能自己写出不依赖任何库的循迹逻辑,也知道每个数字改了会发生什么。
2. 循迹小车的信号链:从红外反射到电机 PWM
2.1 五路红外传感器到底输出什么
常见的循迹模块是 TCRT5000 红外对管,每路输出一个数字量或模拟量。数字量模块板载 LM393 比较器,黑线吸光、反射弱,接收管电流小,输出高电平;白底反射强,输出低电平。听起来简单,但实际赛道有反光、有接缝、有环境光干扰,数字量只有 0 和 1,丢线时没有任何中间信息。我一般优先用模拟量输出,接到 Arduino 的 A0~A4,这样能拿到 0~1023 的原始值,后面做加权误差计算才有依据。
模拟量读进来先别急着用。同一块场地,不同位置的白底反射值可能差 100 以上,黑线值也会因为线宽和印刷质量浮动。所以上电后第一步是标定:让小车在赛道白底和黑线上各停一次,记录每路的最大值和最小值,算出中间阈值。这个动作在代码里就是一次calibrate()调用,后面所有判断都基于标定后的归一化值。
// 五路模拟传感器接 A0~A4 const uint8_t SENSOR_PINS[5] = {A0, A1, A2, A3, A4}; int sensorMin[5] = {1023, 1023, 1023, 1023, 1023}; int sensorMax[5] = {0, 0, 0, 0, 0}; int sensorVal[5]; void calibrate() { // 让小车在白底和黑线上各扫一遍,持续约 3 秒 for (int i = 0; i < 200; i++) { for (int j = 0; j < 5; j++) { int v = analogRead(SENSOR_PINS[j]); if (v < sensorMin[j]) sensorMin[j] = v; if (v > sensorMax[j]) sensorMax[j] = v; } delay(15); } }这段代码的逻辑是:在标定阶段不断刷新每路的最小值和最大值,sensorMin记录黑线反射最弱时的读数,sensorMax记录白底反射最强时的读数。参数上,200 次循环乘以 15 毫秒约 3 秒,足够手动把小车从白底推到黑线再推回来。注意sensorMin初值设 1023、sensorMax初值设 0,是为了保证第一次比较一定能更新。标定完不要断电,否则数据丢失,下次上电要重新标。
2.2 把五路读数变成位置误差
拿到五路归一化值后,不能简单用“哪路压线就往哪边转”。五路传感器排成一行,中间那路对应误差 0,左边两路对应负误差,右边两路对应正误差。常见做法是给每路一个权重,比如{-2, -1, 0, 1, 2},然后算加权平均:
float getPositionError() { long sum = 0; int count = 0; for (int i = 0; i < 5; i++) { int v = analogRead(SENSOR_PINS[i]); // 归一化到 0~1000,黑线接近 1000,白底接近 0 int norm = map(v, sensorMin[i], sensorMax[i], 0, 1000); norm = constrain(norm, 0, 1000); sensorVal[i] = norm; sum += (long)norm * (i - 2); // 权重 -2,-1,0,1,2 count += norm; } if (count == 0) return 0; // 全白,丢线 return (float)sum / count; }这里map把原始读数映射到 0~1000,constrain防止标定不准时溢出。权重(i - 2)让中间传感器贡献 0,最左贡献 -2,最右贡献 +2。sum是加权和,count是所有归一化值之和,相除得到误差。如果count为 0,说明五路都看到白底,小车已经丢线,这时候要单独处理,不能直接返回 0 假装在中间。
误差范围大约在 -2000 到 +2000 之间,具体取决于线宽和传感器间距。这个误差就是后面 PID 控制器的输入。
2.3 电机差速与 PWM 映射
Arduino 通过analogWrite给电机驱动模块(常见 L298N 或 TB6612)发 PWM,范围 0~255。两个电机分别控制左右轮,直行时左右 PWM 相同,转弯时一侧加速一侧减速。最简单的映射是:
int baseSpeed = 150; // 基础速度 int leftPWM = baseSpeed - error * Kp; int rightPWM = baseSpeed + error * Kp; leftPWM = constrain(leftPWM, 0, 255); rightPWM = constrain(rightPWM, 0, 255);baseSpeed是直道速度,Kp是比例系数。误差为正说明车偏右,需要左轮加速、右轮减速,所以左轮减、右轮加。constrain防止计算出的 PWM 超出范围导致电机驱动异常。注意电机有死区,PWM 低于某个值(比如 60)时电机不转,所以实际代码里还要加死区补偿,或者把baseSpeed设得足够高。
3. 从最小可跑版本到 PID:代码分三步改
3.1 第一步:纯比例控制的最小闭环
先不碰 PID,用纯比例控制跑通。代码结构是:初始化传感器和电机引脚,循环里读误差、算 PWM、写电机。这个版本能跑直线和缓弯,急弯会冲出去,但它是后面所有优化的基础。
void loop() { float error = getPositionError(); int leftPWM = baseSpeed - error * Kp; int rightPWM = baseSpeed + error * Kp; leftPWM = constrain(leftPWM, 0, 255); rightPWM = constrain(rightPWM, 0, 255); setMotor(leftPWM, rightPWM); delay(5); }Kp先设 0.05 试跑。如果小车在直道上左右摇摆,说明Kp太大;如果过弯时反应迟钝、冲出去,说明Kp太小。这个版本里delay(5)控制循环周期约 200Hz,太快了传感器读数会抖,太慢了控制不及时。我一般从 5ms 开始调。
3.2 第二步:加入积分项消除稳态误差
纯比例控制有个问题:小车在直道上可能一直偏一点点,因为比例输出不足以让电机产生足够的差速。积分项累积误差,慢慢把偏差补上。
float integral = 0; float Ki = 0.001; void loop() { float error = getPositionError(); integral += error; integral = constrain(integral, -5000, 5000); // 抗积分饱和 float output = error * Kp + integral * Ki; int leftPWM = baseSpeed - output; int rightPWM = baseSpeed + output; leftPWM = constrain(leftPWM, 0, 255); rightPWM = constrain(rightPWM, 0, 255); setMotor(leftPWM, rightPWM); delay(5); }Ki从 0.001 开始试。integral要限幅,否则小车长时间丢线时积分会累积到很大,回到线上会猛冲。限幅值 5000 是经验值,具体看误差量级。积分项能让小车在直道上更稳,但Ki太大会导致过弯时积分饱和,出弯后反向过冲。
3.3 第三步:微分项抑制过冲
微分项看误差变化率,误差快速变大时提前减速,防止过冲。但微分对传感器噪声敏感,五路模拟量本身有抖动,所以微分项要配合低通滤波。
float lastError = 0; float Kd = 0.5; void loop() { float error = getPositionError(); float derivative = error - lastError; lastError = error; integral += error; integral = constrain(integral, -5000, 5000); float output = error * Kp + integral * Ki + derivative * Kd; int leftPWM = baseSpeed - output; int rightPWM = baseSpeed + output; leftPWM = constrain(leftPWM, 0, 255); rightPWM = constrain(rightPWM, 0, 255); setMotor(leftPWM, rightPWM); delay(5); }Kd从 0.5 开始试。如果小车在直道上高频抖动,说明Kd太大或传感器噪声太大,可以在getPositionError里对norm做滑动平均。如果过弯时反应还是慢,可以适当加大Kd。三个参数没有标准答案,同一套代码换一块场地就要重调,这是血泪经验。
4. 避坑与排查:循迹代码最常见的五个翻车点
4.1 现象:小车原地打转,完全不跟线
原因通常是电机接线左右反了,或者传感器顺序装反。代码里setMotor(leftPWM, rightPWM)假设左电机接左轮,如果实际接反,误差正反馈变成负反馈,小车就会打转。解决方法是先写一个测试程序,让左轮正转、右轮正转,看小车是否直行;再让左轮正转、右轮反转,看是否原地转。确认接线后再跑循迹逻辑。
4.2 现象:直道上左右摇摆,像喝醉一样
这是Kp太大或循环周期太长的典型表现。比例增益过高时,误差稍微大一点就输出很大的差速,小车冲过中线又反向修正,形成振荡。先把Kp减半,如果摇摆频率降低但幅度还在,再检查delay(5)是否被其他代码拖慢。用micros()测实际循环周期,如果超过 10ms,要把传感器读取和电机写入之间的耗时压缩。
4.3 现象:过弯时冲出去,直道正常
纯比例控制对急弯无能为力,因为误差还没大到产生足够差速时,小车已经冲出赛道。解决办法有两个:一是加微分项,提前感知误差变化;二是降低baseSpeed,给控制留更多时间。如果赛道有直角弯,还要加丢线保护:当count为 0 时,让小车按最后一次误差方向原地转向,直到重新找到线。
4.4 现象:标定后跑一圈正常,第二圈就乱
传感器受环境光影响,尤其是场地灯光变化或电池电压下降时,红外发射管功率降低,反射值整体偏移。解决方法是每次上电都重新标定,或者在代码里加动态阈值:每隔几秒用当前五路读数的最大值和最小值更新sensorMin和sensorMax。但动态阈值在丢线时会失效,所以要加判断,只在count大于某个值时更新。
4.5 现象:电机发出啸叫但不转,或者转速忽快忽慢
PWM 频率和电机驱动模块不匹配。Arduino 默认 PWM 频率约 490Hz,有些电机驱动模块在这个频率下效率低。可以改定时器寄存器把 PWM 频率提到 20kHz 以上,超出人耳听觉范围,电机运行更平滑。但改定时器会影响millis()和delay(),要同步调整。另一个原因是电源功率不足,电机启动瞬间拉低电压导致 Arduino 复位,要在电机电源端并大电容。
5. 进阶技巧:用状态机处理丢线和十字路口
5.1 丢线状态机
基础版本里丢线只是返回 0,实际赛道丢线后要能找回来。我一般用状态机:正常循迹时状态为LINE_FOLLOW,连续多次count为 0 时进入LINE_LOST,记录最后一次误差方向,让小车向该方向原地转,直到重新检测到线再回到LINE_FOLLOW。
enum State { LINE_FOLLOW, LINE_LOST }; State state = LINE_FOLLOW; float lastValidError = 0; int lostCount = 0; void loop() { float error = getPositionError(); int count = getActiveCount(); // 返回归一化值大于阈值的路数 if (count > 0) { lastValidError = error; lostCount = 0; state = LINE_FOLLOW; } else { lostCount++; if (lostCount > 5) state = LINE_LOST; } if (state == LINE_FOLLOW) { // 正常 PID 控制 } else { // 按 lastValidError 方向原地转 int turnPWM = 120; if (lastValidError < 0) setMotor(-turnPWM, turnPWM); else setMotor(turnPWM, -turnPWM); } delay(5); }lostCount > 5表示连续 5 个循环约 25ms 没看到线,才确认丢线,避免传感器抖动误判。lastValidError记录丢线前的误差方向,小车向该方向转,大概率能找回线。这个逻辑在十字路口也适用:如果五路同时看到黑线,count很大但误差接近 0,可以按直行处理,或者加一个路口计数。
5.2 参数整定的实用顺序
调参不要三个一起调。先调Kp,让小车能跟线但允许轻微摇摆;再加Kd,把摇摆压下去;最后加Ki,消除直道上的小偏差。每次只改一个参数,改完跑三圈,记录现象。我习惯用串口把error、leftPWM、rightPWM打到上位机,用曲线看振荡频率和超调量,比肉眼判断准得多。
提示:调参时把
baseSpeed降到 100 左右,低速下振荡和过冲更容易观察,调好后再逐步提速。
5.3 电池电压补偿
电池从 8.4V 降到 7.0V 时,同样 PWM 下电机转速下降约 15%,循迹表现会变差。可以在代码里读电池电压(通过分压电阻接 A5),动态调整baseSpeed:
float readBattery() { int raw = analogRead(A5); return raw * (5.0 / 1023.0) * 3.0; // 分压比 1:3 } void loop() { float vbat = readBattery(); int baseSpeed = map(vbat * 100, 700, 840, 120, 180); baseSpeed = constrain(baseSpeed, 120, 180); // 后续 PID 使用 baseSpeed }map把 7.0V~8.4V 映射到 120~180 的baseSpeed,电压低时自动降速,保证控制余量。分压比 1:3 是常见做法,实际要根据电阻值算。这个补偿能让小车从满电跑到没电,圈速差异控制在 5% 以内。
最后说一个我自己的习惯:每次改完代码,先用串口把五路原始值和归一化值打出来,确认标定正常再跑车。很多看起来像控制算法的问题,其实是传感器没标定好。希望帮到你。
本文还有配套的精品资源,点击获取