四路循迹小车程序实战:从传感器排布到差速PD控制
发布时间:2026/10/14 19:01:24 锦皓数字建站

简介面向单片机与智能小车开发者的四路循迹程序工程包基于AVR或STM32平台实现黑线自动循迹。资源围绕四路红外/光敏传感器阵列展开覆盖信号采集、位置偏差计算、PID纠偏控制及PWM调速转向等关键环节适用于课程设计、电子竞赛或嵌入式入门学习。压缩包共14个文件约23KB包含Keil工程.uvproj/.uvopt、C源码、A51启动代码以及编译生成的OBJ、LST、HEX、M51、LNP中间文件与备份配置既有可读源码也有可直接烧录的HEX固件便于对照检查链接和存储分配问题。目前已有791人学习下载。从源码编辑、编译验证到烧录运行读者可对照工程文件理解四路循迹从硬件检测到PWM电机控制的完整闭环也能直接复用实验工程实现快速复现与二次改进。1. 四路循迹程序小车不走直线的根源不在电机而在感知做小车的人通常先怀疑电机但一台差速小车在白色地面上追黑色胶带时跑不跑得直多半不是电机PID没调好而是前面的四路传感器没有把“线在哪里”讲清楚。四路循迹程序要做的就是把这四个红外反射信号合并成一个位置偏差再映射到两轮差速线偏左左轮慢右轮快线偏右反过来。代码虽短却决定了小车是稳定还是神经质。智能车竞赛、课程设计、闲时组一台玩具底盘都会先撞上这个模块。下面从硬件排布、状态映射、PD控制到排查清单按实际调车顺序展开。全程用常见单片机和普通电机驱动即可复现不依赖某个厂商的专用库。读完你能直接抄一套最小程序也知道每个参数动了之后车会有什么反应。2. 四路传感器的选型与排布为什么四路比三路更容易调2.1 红外反射式传感器反射率差异如何变成电平四路循迹常被简称为“循迹探头”的红外反射式传感器模块每个探头内有一只红外发射管和一只接收管。发射管持续发出红外光光线打到地面上后发生漫反射部分光回到接收管。白色表面反射率高接收管的光电流大黑色表面反射率低光电流小。模块内部再用比较器把电压差换算成数字电平或者直接把接收管的原始电压引出来作为模拟量。数字量模块上的蓝色电位器调的是比较器的翻转电压也就是“多黑算黑、多白算白”的阈值。大多数模块还带一个指示灯翻转时亮起。调车时先看灯再读程序能快速判断阈值和安装高度是否合适。电位器拧得太偏一侧会出现黑色压线却读成白色的情况程序里再怎么调PID都没用。这个环节有个容易被忽略的概念叫回差滞回。比较器通常设计成少量回差防止信号在阈值附近抖来抖去。如果模块没有回差或回差过小输出在黑白边界会不停闪动程序读到的是乱跳的电平。判断方法很简单用手慢慢横移一张黑纸条看指示灯从灭到亮和从亮到灭的位置是否在同一处。两处明显不同说明有回差几乎重合且快速闪烁说明回差不足这类模块在高速过弯时最容易输出抖动数据。对四路循迹来说传感器读数的物理意义就是“这一路下方是黑还是白”。至于输出高电平表示黑还是白不同模块定义不同软件里可以在读取后取反一次。所以不要一上来就改硬件接线先用串口打印确认极性再决定代码里要不要加一个取反。2.2 传感器间距与安装高度影响偏差分辨率的三个尺寸确定传感器间距前先量赛道线宽。常见的黑色电工胶带宽度为15mm、18mm、20mm。四路传感器一字排开时相邻两路间距d和线宽w的关系决定了车能感知的最小横向偏移。若d远大于w线经常落在两路之间的盲区出现“两路都没亮但车已经偏了”的状态若d远小于w一路就能覆盖整条线四路状态退化成“左中右”三档失去四路的优势。我的参考值是d取0.8w到1.2w。以18mm线宽为例四路从最左到最右跨距约54mm到72mm能覆盖常见的差速小车底盘。如果底盘很窄可以考虑前两路后两路的对角排布但映射关系会复杂这里不展开。高度方面传感器底面离地8mm到20mm都能工作推荐先调到15mm再依据实测调整。高度升高单路检测区域变大黑白边界更平滑但更容易被环境光淹没高度降低边界清晰但对颠簸更敏感。实际测试时把模块从最低慢慢抬起来记录黑白色差开始明显变小的高度把安装高度定在差信号还足够大的范围内。还有一个容易被新手漏掉的尺寸传感器组到前轮的纵向距离。循迹要求在车体转向之前提前看到线所以传感器组应尽量装在车头最前方。前置距离太短入弯响应慢会在弯道外侧丢线太长车头在急弯扫出的圆弧会超出赛道范围表现为转弯半径过大。我一般保证传感器组比驱动轮轴线至少靠前20mm具体值按底盘长度和弯道半径调整。2.3 数字量还是模拟量两种方案的取舍对比数字量与模拟量的选择决定了程序的数据形态。数字量方案中四路数据合起来最多16种组合软件只认“压线/没压线”逻辑简单抗干扰能力强比较器自带阈值处理。模拟量方案读取的是满量程之间的连续电压软件要自己算阈值、自己处理环境光漂移但换来的是连续位置信息。维度数字量四路模拟量四路引脚需求4个GPIO4个ADC通道调阈值电位器手动调代码动态标定丢线判断状态组合直接可判需设黑白阈值弯道顺滑度阶梯式转向连续比例转向环境光敏感度低高新手友好度高低从实际调车经验看第一版几乎都是数字量四路起步。它让你先把“状态→偏差→差速”这条链路打通跑出稳定一圈之后再谈顺滑。如果一开始就上四路模拟量标定、滤波、环境光补偿三座山叠在一起出了问题很难分清是算法还是硬件。我习惯的做法是数字量四路作为主状态外加一路模拟量做精细插值这样既不怕干扰又能在直道修正时消除阶梯感。四路比三路强的地方就在这里三路在十字路口和断线边缘经常全部丢线四路多一个自由度能多识别出几种“半丢线”状态回旋救车的成功率明显更高。3. 四路循迹程序的主循环从传感器读取到差速输出的可抄代码3.1 主循环结构采样、状态映射、转向输出三步四路循迹程序没有复杂的并发结构核心就是一个紧循环。每个循环周期内完成三件事读四路电平得出偏差err根据err计算PWM。三次读取最好连续执行不要在中间穿插耗时操作。比如常见开发框架里的analogRead很快但println会阻塞如果每个周期都打印控制频率会被拖到几十赫兹以下车会一卡一卡。我一般把循环目标定在5到10毫秒以内也就是100Hz以上的控制频率。为了消除机械抖动和传感器闪变可以每路连续读两次两次结果不同就再读一次但最多读三次避免在丢线状态下因为去抖浪费太多时间。去抖代码放在read_sensors里void read_sensors() { s1 read_pin_stable(2); s2 read_pin_stable(3); s3 read_pin_stable(4); s4 read_pin_stable(5); } uint8_t read_pin_stable(uint8_t pin) { uint8_t v1 digitalRead(pin); uint8_t v2 digitalRead(pin); if (v1 v2) return v1; return digitalRead(pin); }这段代码的逻辑是读两次一致就采用不一致说明引脚在跳变再读第三次取多数结果。三次读取总共消耗十几微秒几乎不占时间预算。这种轻量去抖比delay(10)可靠得多delay会把整个控制周期拉长让转向响应变肉。主循环里另一个原则是控制周期要相对固定。框架自带的loop天然是无固定周期的循环严格做法是用定时器或微秒计时保证每次控制间隔一致。对入门场景只要循环内没有阻塞调用周期抖动在2到3毫秒内是可接受的。如果发现车在某个速度段开始共振优先检查循环里有没有隐藏的串口打印和阻塞库函数而不是先调PID。3.2 四路信号转偏差值状态表与加权法四路数字量拼成一个4位二进制数从0b0000到0b1111共16种。理论上有用组合只有几种线居中、线偏左、线偏右、全压线、全丢线。手动调车时我习惯把16种状态全部打印出来因为有些组合是传感器视野重叠或线宽导致的“两路同时亮”不能只靠if分支处理必须先观察再写映射。常见状态表如下状态 S1 S2 S3 S4含义err0110中间两路压线车在线正上方00100第二路压线线偏左一点-11100第一、二路压线线偏左较多-21000第一路压线线在最左-30010第三路压线线偏右一点10011第三、四路压线线偏右较多20001第四路压线线在最右31111十字路口或全部压线00000全丢线走回旋策略其他半丢线边界保持last_err这个表假设模块输出1表示压到黑线。如果模块输出反逻辑程序读完后统一取反比如s1 !digitalRead(S1)。设定err符号时要让“线偏右”对应正偏差这样后面的差速公式才能让左轮加速、右轮减速车头右转去追线。方向定义反了车就会反向跑这是四路循迹最常见的“上电就画圈”原因。把状态表落成代码时不要一次性写16个if先处理最有把握的8个分支边界状态统一走else保持last_err。这样逻辑清晰也方便以后加入模拟量插值。3.3 电机差速输出查表 PD 和简易 PID 的参数偏差err计算出来后两轮PWM的基本公式是int16_t speed_diff kp * err kd * (err - last_err); left_duty base speed_diff; right_duty base - speed_diff;直觉解释当err为正线偏右speed_diff为正左轮加速、右轮减速车头向右修正。微分项err - last_err表示偏差正在变大的速率在偏差快速增长时提前加大转向量抵消转向滞后。这一项是弯道不甩尾的关键。参数起点建议值如下参数起点范围调整方向base30~60 百分比提速时每次加5kp0.5~1.2车身抖动时减小kd0.1~0.5过弯反应慢时加大diff限幅20~40急弯不够用时放宽I项不推荐加。循迹系统的误差来自赛道几何不是持续的单向扰动积分会让车辆在弯道里越转越深出弯后还继续偏。如果发现直道有固定方向的静差应该先查轮径差和电机一致性而不是靠I项去抹平。电机驱动方向的控制需要在PWM之外设置方向引脚。驱动板一般有两个方向输入引脚控制H桥正反转PWM引脚控制占空比。在测试阶段先用固定PWM和方向引脚组合验证正反转再接入PD公式否则差速的正负号会被接线逻辑吃掉。3.4 一条可直接抄的四路循迹示例代码把上面三个函数合并加上引脚初始化和电机驱动封装就是一份最小可跑程序。下面代码针对数字量四路模块、两路PWM电机驱动和常见开发框架写// 四路循迹最小程序数字量四路 差速PD #define S1 2 #define S2 3 #define S3 4 #define S4 5 #define AIN1 7 // 左电机方向引脚1 #define AIN2 8 #define PWMA 9 #define BIN1 11 // 右电机方向引脚1 #define BIN2 12 #define PWMB 10 uint8_t s1, s2, s3, s4; int16_t err 0; int16_t last_err 0; void setup() { pinMode(S1, INPUT); pinMode(S2, INPUT); pinMode(S3, INPUT); pinMode(S4, INPUT); pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(PWMA, OUTPUT); pinMode(PWMB, OUTPUT); } void read_sensors() { s1 digitalRead(S1); s2 digitalRead(S2); s3 digitalRead(S3); s4 digitalRead(S4); } void compute_error() { uint8_t state (s1 3) | (s2 2) | (s3 1) | s4; if (state 0b0110) err 0; // 居中 else if (state 0b0010) err 1; // 线偏右一格 else if (state 0b0011) err 2; // 线偏右两格 else if (state 0b0001) err 3; // 线在最右 else if (state 0b0100) err -1; // 线偏左一格 else if (state 0b1100) err -2; // 线偏左两格 else if (state 0b1000) err -3; // 线在最左 else if (state 0b1111) err 0; // 十字路口按直行 else err last_err; // 丢线或异常组合 last_err err; } void set_motor_duty() { int16_t base 60; // 基础占空比 0-100 float kp 0.8; float kd 0.4; int16_t speed_diff kp * err kd * (err - last_err); speed_diff constrain(speed_diff, -40, 40); int16_t left_duty constrain(base speed_diff, -100, 100); int16_t right_duty constrain(base - speed_diff, -100, 100); motor_write(left_duty, right_duty); } void motor_write(int16_t left, int16_t right) { digitalWrite(AIN1, left 0); digitalWrite(AIN2, left 0); analogWrite(PWMA, abs(left) * 2.55); digitalWrite(BIN1, right 0); digitalWrite(BIN2, right 0); analogWrite(PWMB, abs(right) * 2.55); } void loop() { read_sensors(); compute_error(); set_motor_duty(); }代码里motor_write做了三件事方向引脚根据占空比正负切换PWM引脚输出0到255百分比乘以2.55转成PWM寄存器值。如果你的开发框架里analogWrite范围是0到1023把2.55改成10.23。接线验证时先断开电机用手缓慢把线从传感器左边移到右边同时观察串口打印的err不要一上电就放车否则方向和极性都未验证车一通电就会乱冲。4. 阈值标定与赛道适配让同一套程序适应三种地面4.1 开机自动标定滑过黑白边界采集最大最小值数字量模块的阈值在硬件电位器上但换了地面、换了灯光原本的翻转点就可能失效表现为指示灯常亮或常灭。软件能做的补救是如果模块支持模拟量输出就不要用固定阈值而是在上电后自动标定一次。做法是让传感器依次划过白色区域和黑色区域每个通道连续采样几十次记下最大值和最小值阈值取两者中间值。int16_t calMin[4], calMax[4]; void calibrate_once() { for (int i 0; i 4; i) { calMin[i] 4095; calMax[i] 0; } // 假设四路接 ADC 通道 A0..A3ADC 为 12 位 int samples 0; while (samples 500) { for (int i 0; i 4; i) { int16_t v analogRead(A0 i); calMin[i] min(calMin[i], v); calMax[i] max(calMax[i], v); } samples; delay(2); } } uint8_t read_line_bit(uint8_t idx) { int16_t v analogRead(A0 idx); int16_t th (calMin[idx] calMax[idx]) / 2; return (v th) ? 1 : 0; // 黑线反射率低电压低 }标定期间需要小车以缓慢速度滑过一段同时包含黑色和白色的路径或者直接由人推着车走一遍。如果上电时车不动采样到的只有一种颜色阈值会落在错误位置。很多成品方案会让小车在开机后原地摆动一下制造扫描过程。注意黑色和白色的电压方向取决于模块电路有的模块是黑线输出低电平有的相反。代码里return那一行的比较符号要按实际串口打印确认后设置。设反了的典型症状是四路灯全反车一上电就朝随机方向冲。500次采样上限的意义是防止人忘记推车导致死等。正常情况下2毫秒一次500次大约1秒刚好够推车滑过一小段黑白边界。如果你的赛道起跑区只有白色可以延长上限到2000或者增加一个“看到过黑色才结束”的条件。更稳妥的做法是同时判断seen_black和seen_white两个标志两者都为真才结束标定否则继续采这样标定结果一定覆盖两个极值。4.2 丢线策略全丢时是回旋还是直行四路循迹最容易被忽视的环节是丢线处理。四路全部为0表示车下没有线可能是在急弯处冲出赛道也可能是线太窄脱离了四路覆盖范围。这时如果err保持上次值小车会继续沿上次方向冲大概率飞出去如果err直接置0小车会直行也可能错过弯道。合理做法是记住丢线前err的符号让小车朝着丢线前相反的方向回旋寻找线。uint32_t lost_time 0; void handle_lost(uint8_t state) { if (state ! 0) { lost_time millis(); return; } if (millis() - lost_time 300) { // 丢了超过 300ms转入回旋模式 err (last_err 0) ? -3 : 3; } }这里的关键是回旋方向取丢线前err的反方向如果刚才线偏右、车正在向左修那丢线后车应该右转去找线。回旋幅度取偏差最大值3并且用持续时间和次数做上限防止在错误路线上原地打转。如果回旋模式持续超过800毫秒还没找到线就强制恢复直行等待人工干预或进入停车逻辑。还有一类特殊情况直角弯和锐角弯中传感器组会在某一瞬间完全离开线但车头其实还在赛道内。这种丢线时间很短通常小于100毫秒不需要触发回旋。所以我代码里的300毫秒延迟很关键短丢线直接保持last_err长丢线才回旋。丢线阈值设得太短车在过直角弯时会突然大转向反而更容易飞线。4.3 漂移阈值和转向死区参数该怎么设数字量状态下err是离散整数没有严格意义上的死区。但在模拟量方案里err是带小数的连续值传感器抖动会让err在0附近反复跳变变成高频来回修正。解决方法是设死区当err的绝对值小于某个阈值时强制按0处理。if (fabs(err) 0.5) err 0;除了死区还有两个参数直接影响稳定性。一个是转向输出下限当err不为零但计算出的speed_diff很小可能不足以克服轮胎静摩擦车会表现为“知道偏了但不动”。我通常在speed_diff绝对值小于8时直接提到8让修正量有最小力度。另一个是加减速平滑base速度不要瞬时切换用一个斜坡函数每10毫秒升降1到2个单位避免电池电压波动和机械冲击引起车身晃动。参数推荐范围作用死区0.2~1.0消除模拟量抖动最小修正PWM5~10克服静摩擦斜坡步长1~2/10ms平滑加减速回旋延迟200~400ms区分短丢线和真丢线这三个参数共同决定一辆车是“指针式”还是“平滑式”。调车时先用大死区小修正量跑稳定再逐步缩小死区提高响应。很多调车翻车不是因为PID公式不对而是死区、限幅、斜坡这几个外围参数和速度不匹配。记住一个顺序先标定地面再调死区再调kp和kd最后才提速。跳过任何一步后面都会回来返工。5. 四路循迹排查清单丢线、震荡、原地打转的定位方法5.1 现象直道轻微跑偏走成S形小车在直道上不是一次修正到位而是左右来回摆像画蛇形。原因常常不是控制参数而是左右两侧机械不对称两个电机空载转速不同、车轮直径差一点、底盘轴距歪、传感器组中心没对准两轮中点。程序上表现出来的就是同一个base速度下车体自然偏向一侧P控制器不断纠偏纠偏和自然偏航互相叠加形成振荡。解决方法是先做机械和基线标定再动PID。把车架空两个轮子悬空给定左右相等的占空比观察是否走得直。然后在同一段直道上多次正反放车记录偏航方向。如果恒定偏向左边就在程序里给左右轮加一个固定偏置比如左轮62、右轮58。这个偏置量通常是2到5个百分比单位加完再观察。如果架空时两轮速度就不一致优先查电机驱动接线和电源不是查程序。5.2 现象过急弯直接冲出赛道车在直道正常一到急弯就冲出线。常见原因有三个层面速度太快弯道里横向偏差大PD输出的修正量被限幅卡住微分项不足误差快速变化时没有提前减速丢线状态处理得太激进入弯瞬间最外侧传感器还没来得及压线车已经把线丢了然后进入回旋模式反而转向过度。解决顺序是先把base降到40%以下确认弯道能过再逐步提速。提速后如果弯道开始冲出把speed_diff的限幅放宽同时把kd往上调0.1到0.2。如果弯道出线但回旋模式不及时介入检查丢线方向的逻辑是否与实际情况相反。我的排查习惯是在弯道前放一个明显标记通过串口记录err序列正常入弯应该是err从0平滑变到3再回0如果err直接跳到3并长时间保持说明传感器组“看”弯道太晚硬件上应该往车头前方前移。5.3 现象一过交叉线就左右摆头十字路口四路全部压线程序当作直行通过。问题出在传感器组跨越交叉区域时会依次经历“左边两路亮、四路全亮、右边两路亮”的连续状态。如果不加处理车会以为线在左右横跳P项跟着来回打方向表现为点头式摆头。解决方法是给十字状态加保持时间。检测到0b1111时记录进入时间在150到250毫秒内强制err为0不响应左右两侧的状态跳变。uint32_t cross_time 0; void handle_cross(uint8_t state) { if (state 0b1111) { cross_time millis(); err 0; } if (millis() - cross_time 200) { err 0; } }保持时间长短取决于车速和传感器前置距离。速度越快保持时间越短否则出十字后真弯道会被忽略。如果车速不固定不要把保持时间设成常数而要根据当前速度计算保持距离除以当前速度。另一个细节全压线状态也可能是“整个传感器组压在一段宽的色块上”不一定是十字所以保持时间不宜超过250毫秒避免压住连续色块时完全失去转向能力。5.4 现象上午能跑下午失灵同一赛道时好时坏这种环境相关的不稳定首先要怀疑供电和干扰其次才是传感器。电池电压下降会让电机驱动板把母线电压拉低红外发射管的亮度随之波动模拟量输出的反射电压整体漂移。如果程序用固定阈值老化、阳光角度变化、地面反光都会让阈值失效。我在一个模拟项目上遇到过同样症状室内正常窗边频繁丢线最后查到是阳光直射白色地面让黑白两态的电压差从1.2V压到了0.3V。解决方法是三件套给传感器加遮光罩至少挡住从侧面来的光改用开机自动标定每次上电重新采黑白极值给单片机、传感器和电机驱动分路供电或者加稳定的低压差稳压器避免电机启停把传感器参考电压拉偏。硬件电位器标定的模块把翻转阈值设在黑白电压差的中间偏白一侧也就是宁可误判黑为白也不要误判白为黑因为丢白线比丢黑线更容易纠正。还有一个实操技巧检查四个模块的电位器是否一致个别模块的翻转点差得远是最隐蔽的玄学问题肉眼很难看出来串口打印四路原始值对比一下立刻见效。5.5 现象上电后一个电机不转或反转这类问题不是循迹算法问题而是程序之外的接线问题但它会让所有循迹代码看起来像废的。常见症状是小车向前冲时明显兜圈PWM越大越往一侧转电机尖叫但不动。原因通常是驱动板方向引脚和PWM引脚的逻辑组合不对或者使能引脚没拉高也有可能是左右电机驱动板的地线没共地。解决方法是做最小单轮测试只给左轮输出一个固定的正向占空比确认左轮正转再给左轮输出负值确认反转右轮重复一遍。测试代码和循迹代码分离避免一次排查多个变量。确认方向之后再把motor_write函数里的方向引脚逻辑和测试结果对正。这样处理后循迹程序里的正负号才有意义否则PID符号也要跟着反调一天都可能找不到规律。6. 进阶技巧把四路循迹从「能走」调到「走得稳」6.1 加权平均把离散状态变成连续偏差数字量状态下err只有几个整数档转向呈阶梯式。想更顺滑就把每路传感器的模拟值归一化到0到1再做位置加权err (w1x1 w2x2 w3x3 w4x4) / (x1x2x3x4) - center。权重w按传感器位置取-1.5、-0.5、0.5、1.5。这条公式在传感器部分压线时能算出小数偏差转向力度随位置连续变化直道修正几乎没有顿挫。实现时要注意分母为0的情况四路全为0时直接进入丢线逻辑不要做除法。6.2 状态机与计时处理环岛和断线救回四路循迹做到后期单纯比例控制不够用。我把运行逻辑拆成状态机直线寻线、十字保持、甩尾回旋、停车等待四个状态配合进入状态的计时器。遇到环岛时最外侧传感器长时间保持压线状态需要单独检测“外侧连续亮超过N毫秒”来触发环岛模式而不是走默认丢线逻辑。这个思路不复杂但能把最后一圈的稳定性拉开明显差距。6.3 验证习惯与收尾我调循迹有一套固定流程先换三种不同地面比如瓷砖、哑光地面、反光地面各跑一次每次上电强制重新标定再用手机慢动作视频回放看转向介入的时机和车身姿态。凡是找不到原因的抖动先回退到全丢线回旋模式再用二分法调kp和kd。这些习惯能把“玄学”变成可复现的数据少走很多弯路。四路循迹程序的价值不在于代码有多复杂而在于它让一辆小车真正具备“感知自己偏了”的能力这也是后续更复杂的视觉循迹、路径规划共同的地基也希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。