
简介一份面向单片机入门者、智能车竞赛爱好者和电子课程设计学生的四路循迹小车完整工程源码包主要解决小车沿黑色引导线自动行驶时的传感器检测、方向判断与电机调速问题。压缩包共14个文件包含C语言主程序、汇编启动文件、Keil工程文件、可直接烧录的Hex文件以及obj、lst、m51等编译过程文件整体仅23KB结构精简清晰便于对照学习和二次修改。内容覆盖红外或光敏传感器阵列采集、信号滤波与差分处理、PID纠偏控制、PWM调速等关键环节并把硬件接线方式、软件流程和调参思路串成一个完整的实战案例。已有788人学习适合刚接触单片机控制、准备智能车比赛或完成课程设计的读者参考可直接打开工程查看代码、理解逻辑并烧录到小车验证效果。 大家好我最近把工作室里的老巡线小车重新翻出来给它写了一套新的“四路循迹程序”。这个题目听起来简单但真正把四路传感器调稳、让小车在复杂的黑白赛道上不抖、不冲、不丢线里面还是有不少门道的。这篇文章我就把自己从硬件接线、软件逻辑到实测调参的完整过程整理出来希望能给正在做智能车、电子设计竞赛或者单纯想玩循迹小车的朋友一些参考。先说清楚这套程序能做什么它适配市面上最常见的四路红外循迹传感器模块TTL电平输出配合 STM32 或 Arduino 这类主流单片机能够实现小车沿黑色引导线稳定行驶支持直线、弯道、十字路口和断线区域的识别。适合拿来跑竞赛赛道、课程设计或者作为入门自动控制的小项目来学习。很多人在网上找“四路循迹程序”的时候拿到的代码往往只能让车“勉强能动”直道走得还行一到急弯就摆来摆去过十字路口时彻底懵掉。我这次没有采用网上那种单纯“查表给PWM”的做法而是在算法上改用了带偏差归一化的比例转向控制并且把传感器的四种状态重新定义成“位置-状态分级”模式。整套程序写下来不算长但逻辑上更接近工业上常用的运动控制思路稳定性比裸查表高很多。下面我把整套方案拆开从硬件接线、逻辑设计、实车调试到问题排查一步一步讲清楚。1. 整体设计思路从“查表”到“位置连续化”1.1 为什么不能直接照搬网上的查表写法网上一搜“四路循迹程序”出来最多的是下面这种写法if((s10)(s21)(s31)(s40)) { // 直走 } else if((s10)(s21)(s30)(s40)) { // 右转 } ...这种开关式逻辑最大的问题在于它把转向当成了“离散档位”。小车一旦偏离中心传感器状态只会告诉你“偏了”但到底偏了多少、该用多大力度修正代码完全不知道。结果是小车要么修正量太小而冲出赛道要么修正过大而左右来回蛇形。尤其是四路传感器在黑白边界处会有频繁的0和1跳变单纯的 if-else 链条会变得非常脆稍微有点光线干扰就跑飞。1.2 我的方案核心位置加权与偏差归一化我这套程序的核心只有一句话把所有传感器的状态转换成小车当前位置相对引导线的“水平偏差”再把这个偏差映射到转向PWM差值上。具体做法是给四个传感器分别设定权重比如从右到左依次是-3, -1, 1, 3方向定义可以根据左右电机接线调整。当某个传感器压到黑线时它的状态值为1我们累加它的权重最后把累加值除以“当前亮起传感器数量之和”得到归一化位置。举个例子如果最右边两个传感器同时压到线则当前位置值 (-3 -1) / 2 -2表示小车偏右较多。如果只有中间第二个传感器压到线则当前位置 1 / 1 1说明只是轻微偏左。这样一个连续的数值天然就带着“方向和大小”信息转向力度就可以随之线性调节而不是跳档。1.3 四个传感器的典型状态与含义为了后面读代码方便我把四个传感器从右到左称为S1, S2, S3, S4输出低电平表示检测到黑线模块上的指示灯会亮。实际编码时我把它们的有效状态统一转成了1检测到线和0未检测到线再进行加权计算。状态组合 (S1 S2 S3 S4)含义判断推荐动作0 0 0 0全白小车完全丢线或位于起跑区大空白处保持上次转向低速搜索0 0 0 1最左压线左偏严重大幅右转0 0 1 0左侧第二路压线左偏较小中量右转0 0 1 1左侧两路压线线在车左中部较大右转0 1 1 0中间两路压线居中良好直行1 1 0 0右侧两路压线线在车右中部较大左转1 0 0 0最右压线右偏严重大幅左转1 1 1 1四路全压线常见于十字或粗线区域直行通过注意状态表只是初判实际程序里我没有完全依赖某一个单独状态而是把这些组合汇总成加权位置值这样代码更简洁也不容易漏状态。2. 硬件准备与接线细节2.1 传感器选型与安装间距四路循迹传感器模块现在很多我用的是一块很常见的TCRT5000 四路红外反射模块工作电压3.3V-5V数字量输出板上自带比较器能直接输出TTL电平。模块上有四个红外对管每一个都对应一个灵敏度可调电位器。安装间距这一点很多人会忽略传感器之间的中心距最好接近“黑线宽度”的二分之一到三分之二。如果间距太宽小车在直道上会经常出现只有某一路悬空的情况如果间距太窄四路很容易同时压线又区分不出轻微偏移。我用的赛道黑线宽度约2厘米传感器间距设成约1.2厘米效果最好。2.2 单片机接线表下面是我在 STM32F103C8T6 上的接线方式如果你用 Arduino直接把引脚宏定义改成对应数字口即可。传感器模块单片机引脚说明VCC5V 或 3.3V供电和单片机共地GNDGND必须共地OUT1PA0对应 S1最右OUT2PA1对应 S2右中OUT3PA2对应 S3左中OUT4PA3对应 S4最左电机驱动 IN1PB0左电机正转电机驱动 IN2PB1左电机反转电机驱动 IN3PB2右电机正转电机驱动 IN4PB3右电机反转PWM 左PB6左电机调速PWM 右PB7右电机调速这里有个关键提醒传感器模块的 VCC 尽量不要直接接单片机板载的 3.3V 引脚因为四个红外对管同时点亮时电流不低可能会导致电压跌落。我实测直接从 USB 转串口的 5V 供电给传感器再和单片机共地工作非常稳。2.3 电机驱动的PWM频率选择很多初学者把PWM频率随便设成几kHz结果电机尖叫、扭矩下降。对于常用的 TB6612 或者 L298N 驱动板我建议PWM频率设置在 10kHz 到 20kHz 之间。STM32 的定时器可以方便地设置到 10kHz 以上Arduino 默认的 490Hz 其实也能跑只是会听到明显的电流声。我的程序中直接把定时器2的PWM频率配置成 10kHz占空比用__HAL_TIM_SET_COMPARE动态更新。3. 核心源码结构与逻辑解析3.1 程序整体框架我用的是“主循环轮询定时中断控制”的结构。传感器状态读取放在主循环中进行但电机PWM更新放在定时中断里这样可以保证即使某次读取被其他任务耽误电机的控制周期依然是稳定的。尤其是使用 STM32 的 HAL 库时HAL_Delay 会阻塞主循环所以不能让电机控制在延时里面进行。主循环 1. 读取四路传感器并转成 0/1 状态 2. 计算加权位置 currentPosition 3. 根据 currentPosition 计算转向差量 4. 更新左右电机目标速度 5. 延时 10ms防止读取过于频繁 定时中断1ms 或 5ms 根据目标速度动态更新PWM占空比3.2 核心运动控制代码STM32 版本下面的代码是完整的核心逻辑去掉了无关工程文件保留了最常见、最实用的部分。整个程序我按模块化方式写方便你复制到自己的工程里调整。// 定义传感器引脚 #define S1 HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) // 最右 #define S2 HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) // 右中 #define S3 HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) // 左中 #define S4 HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) // 最左 // 传感器状态数组全局变量方便调试 uint8_t sensor[4] {0, 0, 0, 0}; // 读取并转换成有效状态输出低电平表示检测到黑线 void Read_Sensor(void) { sensor[0] (S1 GPIO_PIN_RESET) ? 1 : 0; sensor[1] (S2 GPIO_PIN_RESET) ? 1 : 0; sensor[2] (S3 GPIO_PIN_RESET) ? 1 : 0; sensor[3] (S4 GPIO_PIN_RESET) ? 1 : 0; } // 计算归一化位置范围约 -3 到 3 // 负值表示线在车体右侧正值表示线在车体左侧前提是安装方向正确 int Calculate_Position(void) { int weight[4] {-3, -1, 1, 3}; int sum 0; int count 0; for (int i 0; i 4; i) { if (sensor[i]) { sum weight[i]; count; } } if (count 0) { // 全白丢线状态返回一个特殊标记 return 0; } return sum / count; } // 核心转向控制 void Track_Control(void) { int pos Calculate_Position(); int baseSpeed 60; // 基础PWM根据电机实际调整 int turnStrength 35; // 转向最大附加量根据赛道急弯程度调整 int leftSpeed baseSpeed; int rightSpeed baseSpeed; if (pos 0) { // 如果之前是直行状态丢线时继续直行如果之前有大偏移这里可以加入记忆 // 我习惯保持上次的方向代码上先简单处理成直行 leftSpeed baseSpeed; rightSpeed baseSpeed; } else { // 线性映射位置从 -3..3 映射到 -turnStrength..turnStrength int diff (pos * turnStrength) / 3; leftSpeed baseSpeed - diff; rightSpeed baseSpeed diff; } // 限制PWM范围 if (leftSpeed 100) leftSpeed 100; if (leftSpeed 0) leftSpeed 0; if (rightSpeed 100) rightSpeed 100; if (rightSpeed 0) rightSpeed 0; // 应用PWM __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, leftSpeed); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_2, rightSpeed); // 方向引脚控制小车主板只向前不反向 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET); }代码不复杂真正需要动脑子的地方是pos的正负号和左右速度映射的关系。如果装上去发现小车应该左转却右转不要急着改算法先交换左右电机的PWM输出通道或者把权重数组调成{3, 1, -1, -3}二选一即可。我每次重装小车都要踩这个坑。3.3 转向映射的数学含义有人可能会问为什么要用diff (pos * turnStrength) / 3而不是直接查表因为这样处理以后转向量和偏差量成线性关系小车在轻微偏线时只做温和修正在严重偏移时才大幅转向不会出现“小偏差遇大转向”产生抖动。这里其实是一个最简单的比例控制器P控制虽然没有积分和微分但在循迹这种低速场景下已经足够稳定。如果你想让响应更平滑可以在连续几个周期对pos做一次均值滤波比如filteredPos (filteredPos * 0.7) (pos * 0.3)实测能明显减少传感器在黑白边界抖动带来的影响。4. 四个关键调参与实测效果4.1 基础速度和转向强度的调节顺序我调试的时候遵循一个固定的先后顺序不然会越调越乱先把baseSpeed降到 30 左右turnStrength调到 20保证小车能慢速跑完完整赛道。观察直道是否稳定如果出现轻微蛇形优先降低turnStrength而不是降低速度。观察急弯是否冲出赛道如果冲出去优先加大turnStrength也可以把baseSpeed适当降低。基础稳定后再逐步提高baseSpeed每提高10就重新测试一遍所有弯道。最后微调传感器模块上的灵敏度电位器确保四路在黑白线上状态分明、没有半灰不灰的中间态。我的最终参数是baseSpeed 60turnStrength 35。这套参数在普通PVC赛道上能以约0.8米/秒的速度稳定跑完过直角弯时最大横向偏移不超过5厘米。如果你用的是比较宽的赛道线turnStrength可能可以降到25如果是细线可能要升到45。4.2 传感器灵敏度调节技巧每个红外模块上的电位器都对应一路传感器调整时要拿一张白纸和黑胶带把传感器分别放在全白、全黑区域上用万用表或者看模块LED指示灯确认输出电平翻转。一个很有效的做法是把传感器放在黑白边界处来回移动观察LED是否在同一个物理位置附近快速翻转。如果翻转区间太大说明灵敏度太高如果检测不到黑线说明灵敏度太低。我印象最深的一次问题是四个传感器中有两个在强光下全白区域误触发最后发现不是灵敏度问题而是传感器装得太靠近地面红外光被地面漫反射干扰。后来把传感器抬高到离地约1厘米误触发立刻消失。4.3 丢线处理的改进方向前面代码里把全白状态简单按直行处理这在大多数非极限场景下够了但如果赛道上有大段断线或者直角折返简单直行很容易跑飞。一个成本很低但效果明显的改进是加一个“方向记忆”记录上一次非零位置lastPos当检测到全白时不直接归零而是用lastPos继续控制并同时降低速度。比如int lastPos 0; int Get_ControlPos(void) { int pos Calculate_Position(); if (pos 0) { // 完全丢线使用记忆方向并减速 return lastPos; } else { lastPos pos; return pos; } }对于一段不超过20厘米的断线这个简单策略实测可行。但是要注意如果小车是全白起步、或者刚上电时没有记忆值需要把lastPos初始化成0避免一开机就猛打方向。5. 常见问题排查速查表调试四路循迹过程中最容易出问题的地方往往不是算法而是硬件和接线。下面把我在实际调试中遇到过的典型问题整理成一张表方便你对着排查。现象可能原因排查方法解决办法小车完全不动电源供电不足电机驱动板没使能测量电机驱动供电电压检查PWM引脚更换稳压模块单独给驱动板供电小车原地打转左右电机PWM通道接反或方向引脚逻辑不对用固定速度测试左右电机是否能独立正转交换PWM通道或修正方向引脚直道左右蛇形转向强度过大传感器抖动降低turnStrength增加均值滤波将turnStrength从40降到30急弯冲线转向强度不足或基础速度太快单独测试急弯区域观察转向是否到位加大turnStrength或降低baseSpeed全白时不动作传感器检测不到黑线或全白处理逻辑简单检查传感器离地高度和灵敏度调整电位器抬高/降低安装高度只有某一路失灵该路电位器偏移或红外对管污损把该路单独放在黑白区域测试清洁对管重新校准电位器光线强时误动作环境光对红外造成干扰查看模块是否有遮光罩给传感器加装遮光片或降低灵敏度速度一快就失控主循环周期太长控制响应跟不上用示波器或逻辑分析仪查看控制周期把延时降到5ms或把控制放到定时中断这张表里的每一项我几乎都踩过。尤其是“原地打转”那个问题经常不是程序问题而是电机驱动板的方向引脚没有正确设置。很多四路循迹代码默认小车只有前进和左右差速不带后退所以方向引脚直接拉高和拉低就好如果四根方向引脚全接错小车就会在原地画圈。6. 更近一步如何把循迹程序扩展到其他场景四路循迹程序写完之后如果你想让它具备更强的竞赛能力或实际应用价值有几个低成本扩展方向。第一个方向是“速度自适应”。目前代码里的基础速度是固定值但我们可以根据当前位置pos的大小来降低速度。例如当abs(pos) 2时说明进入急弯自动把基础速度降到原来的60%出弯后再恢复。这种逻辑只需要在Track_Control()里加一个if判断实测可以大幅提升过弯稳定性。第二个方向是增加“路口识别”。当四路全为1时可以判定为十字路口此时不改变方向继续直行。如果想实现“遇路口左转或右转”需要在代码里检测四路全黑持续的时间一旦持续超过一定周期例如50ms就启动一个带有时间或编码器计数的转弯任务。这个扩展通常需要追加一个状态机。第三个方向是移植到 ROS 或更高级的控制板。四路循迹本质上是一个简单的传感器反馈控制器逻辑上和机器人中的“偏差跟踪”同源。你可以把Calculate_Position()函数抽象成一个类直接复用到装有线阵传感器或摄像头循迹的机器人上只需要把位置信号的来源从GPIO换成串口或I2C即可。就我个人而言四路循迹程序最好的学习价值不在于那几行PWM控制代码而在于它让人直观理解了“传感器数据 - 连续偏差 - 执行器控制”这条自动控制最核心的链路。想清楚这一步后面学 PID、学路径规划都会顺很多。希望这篇文章能帮你把小车调稳也欢迎你在实际调试中遇到奇葩问题后回来对照这张排查表。本文还有配套的精品资源点击获取