简介:面向STC单片机入门者的寻迹避障小车简单避障程序,是一份围绕STC15系列MCU的嵌入式实践代码包,适合正在学习8051内核、传感器检测与小车运动控制的开发者参考。程序涵盖初始化配置、IR红外传感器读取、避障策略判断以及PWM电机调速等核心环节,并保留了循迹扩展的基础结构,整体逻辑清晰、可直接在Keil中打开编译。资源包共33个文件,压缩后仅60KB。文件类型较为典型:包含C语言源文件与头文件用于功能实现,Keil工程文件(uvproj/uvopt)便于直接加载,Hex文件可烧录至单片机,还附带了lst/obj等编译中间文件,方便查看生成过程。小巧的体积与明确的目录结构,降低了下载和部署成本。目前已有1232人学习该资源,适合初次接触STC避障小车项目的读者快速上手。通过梳理源码中的PWM控制、传感器信号处理与避障逻辑,能够直观理解嵌入式小车的完整工作流程,同时也可借鉴作者在硬件配置和调试方面的工程习惯,为后续功能扩展打下基础。
1. 简单避障程序,为什么反而是最容易翻车的部分
做寻迹避障小车的人,十有八九先栽在程序上,而不是硬件。寻迹的逻辑很直白,底下的红外对管看到黑线就跟着走,避障却要在“冲过去”和“躲开”之间做取舍——距离阈值设大了,小车在空旷场地里频繁转向,走得歪歪扭扭;设小了,等超声波读回距离再处理,车已经撞上去了。这篇笔记就把“简单避障程序”这件事讲透:你先看明白超声波和红外循迹各管什么,再照着第 3 章的代码一步步接起来跑通,最后用第 4 章的避坑清单去排查那些让小车原地转圈或一头撞墙的常见问题。适合刚把车架装好、还在调第一个控制程序的开发者,也适合想把自己那套“能跑但不稳”的代码理顺的人。
2. 避障硬件选型与接线:用最便宜的方案,搭一套能稳定跑的底盘
2.1 为什么是 Arduino + HC-SR04 + L298N,而不是更“高级”的方案
网上做寻迹避障小车,方案五花八门,但多数人最后走的是这条线:Arduino 做决策,HC-SR04 超声波测距,TCRT5000 红外对管循迹,L298N 驱动直流减速电机。这个组合没有一个是性能标杆,但它有一个别的方案比不了的优势:每一环的坑都被前人踩得差不多了,资料好找,示波器都不用,串口监视器就能把问题看得七七八八。
如果你用的是树莓派配摄像头做视觉避障,精度上限确实高,但那是另一个量级的问题——你要处理帧率、光线、模型推理延迟,一个环节没弄好整台车就变成“睁眼瞎”。而超声波避障本质上是一个“阈值决策”问题:前方 20 厘米内有东西,就停下来换个方向。运算量小,实时性够,最重要的是一套代码逻辑写出来,换到第二台车上只需要调几个参数。
我一般建议选 Arduino Uno 或 Nano,不要一上来就上 STM32。原因很简单:避障程序的调试重心在“决策逻辑”而不是“计算性能”,Uno 的 16MHz 主频处理一个 HC-SR04 的脉冲回波绰绰有余,串口打印调试信息也顺手。等你把逻辑跑顺了,再移植到 STM32 也就是改改引脚定义的事。L298N 这个驱动板虽然效率不高、发热明显,但胜在皮实,初学者把电源接反了它也就是冒个烟,换一个不心疼。
2.2 接线表与引脚定义:照着插,别让电源和信号线互相干扰
先给出一份我实际用过的接线表。注意 L298N 的供电部分,这是新手最容易接错的地方。
| 模块 | 引脚 | Arduino 引脚 | 说明 |
|---|---|---|---|
| L298N 电机驱动 | IN1 / IN2 | D5 / D6 | 左侧电机正反转控制 |
| L298N 电机驱动 | IN3 / IN4 | D9 / D10 | 右侧电机正反转控制 |
| L298N 电机驱动 | ENA / ENB | D3 / D11(PWM) | 调速,不接时默认全速 |
| HC-SR04 超声波 | Trig | D12 | 发送 10us 触发脉冲 |
| HC-SR04 超声波 | Echo | D13 | 接收回波,输入引脚 |
| TCRT5000 循迹模块 x2 | DO(数字输出) | D7 / D8 | 左 / 右传感器 |
| L298N 电源输入 | VIN / GND | 外部电池 7.4V~12V | 给电机供电 |
| Arduino | 5V / GND | L298N 的 5V 输出 | 给传感器和 Uno 供电 |
接线有个关键原则:电机电源和逻辑电源要分开走。L298N 的板载稳压可以输出 5V,但这路 5V 是给 Arduino 和传感器用的,不是给电机用的。电机的瞬间启动电流能到 1A 以上,如果 Arduino 也从这个口取电,电压跌落会让单片机直接复位,表现就是小车一启动就重启,或者超声波读数突然变成 0。我用的是两节 18650 串联供电,正极进 L298N 的 VIN,L298N 的 5V 输出再供给 Arduino 的 5V 引脚。如果你手头是单块 9V 电池,续航和电流都撑不住,建议换电池方案。
2.3 写一个最小接线测试程序,先确认每个模块都活着
不要一上来就写完整的避障加寻迹程序。先把三个模块分开测,任何一个没接对,后面排查的成本会翻倍。第一个要测的是超声波。
// 超声波最小测试:读距离并通过串口打印 #define TRIG 12 #define ECHO 13 void setup() { Serial.begin(9600); pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); } void loop() { digitalWrite(TRIG, LOW); delayMicroseconds(5); digitalWrite(TRIG, HIGH); delayMicroseconds(10); // 至少 10us 的高电平触发脉冲 digitalWrite(TRIG, LOW); long duration = pulseIn(ECHO, HIGH); // 等待回波,单位是微秒 float distance = duration * 0.034 / 2; // 声速 340m/s,往返除 2 Serial.print("Distance: "); Serial.print(distance); Serial.println(" cm"); delay(200); }这段代码的逻辑是:给 Trig 引脚一个 10 微秒以上的高电平,超声波模块内部会自动发出 8 个 40kHz 脉冲,然后 Echo 引脚会保持高电平,高电平的持续时间就是声波往返的时间。pulseIn函数就是等这个高电平回来。duration * 0.034 / 2是标准的距离换算公式,0.034 是声速的单位换算结果,除以 2 是因为声波走了个来回。
用这把“尺子”量一下:把手放在探头正前方 10 厘米处,串口监视器应该稳定输出 9~11 之间的数字。如果读数一直显示 0,先检查 Echo 引脚是不是接到了 Arduino 的 13 号脚,再确认模块的 VCC 接的是 5V 而不是 3.3V——HC-SR04 在 3.3V 下经常不工作,这是最容易踩的坑之一。
循迹模块的测试更简单,上电后把模块贴近白纸,用黑色胶带贴一条黑线,数字输出引脚在没压到黑线时是 HIGH,压到黑线时变成 LOW。串口打印两个引脚的电平状态,手推小车经过黑线,确认电平翻转就说明接线和模块都没问题。L298N 的测试是给 IN1 高电平、IN2 低电平,看左轮转不转,反一反看反转是否正常。这三个测试都过了,再进第 3 章的正式程序。
3. 简单避障程序的逻辑与代码实现:从阈值判断到寻迹避障切换
3.1 决策逻辑先想清楚:避障的“简单”是指决策简单,不是代码简单
写程序前先把逻辑画清楚。一台寻迹避障小车在路上跑,面临两种状态:看到黑线就循迹走,遇到障碍就停下来绕。问题的核心是这两种状态怎么切换、切换后怎么恢复。
我见过很多开发者写的程序,把避障和循迹写成两个独立的 if,结果小车到了障碍物前面,一边执行避障转向,一边循迹模块还在检测黑线,两边争抢控制权,车身抖得像抽风。正确的做法是先定优先级:避障优先于寻迹。超声波距离小于阈值,说明前面有东西,这时候根本不看循迹传感器的状态,直接进入避障流程;距离安全,再循迹。
避障流程本身也分两个层次。最简单的做法是“遇到障碍就原地转一个固定角度的弯”,但这会导致小车在复杂环境里陷入死循环——左边转 90 度有墙,转回右边又有墙,来回摆头出不去。稍微好一点的做法是先停下来,控制舵机把超声波探头转到左边测一下、右边测一下,哪边距离远就往哪边转。这个“先探测再决策”的思路只增加了一小段代码,但避障成功率提升了一个级别。
3.2 完整可运行的避障主程序:贴代码并逐段讲清参数含义
下面这套代码是完整的“循迹 + 超声波避障”程序,主循环里先查距离再查轨迹,控制逻辑写在自定义函数里,方便你单独调参。
#include <Servo.h> // 引脚定义 #define TRIG 12 #define ECHO 13 #define LEFT_TRACK 7 #define RIGHT_TRACK 8 // L298N 控制引脚 #define IN1 5 #define IN2 6 #define IN3 9 #define IN4 10 #define ENA 3 #define ENB 11 // 参数区 #define SAFE_DIST 20 // 避障触发距离,单位 cm #define TURN_SPEED 180 // 转向时 PWM 占空比,0~255 #define FORWARD_SPEED 200 // 直行 PWM 占空比 Servo headServo; // 控制超声波探头的舵机 void setup() { Serial.begin(9600); pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); pinMode(LEFT_TRACK, INPUT); pinMode(RIGHT_TRACK, INPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT); headServo.attach(9); // 舵机信号线接 D9 headServo.write(90); // 探头朝前 delay(200); } float readDistance() { digitalWrite(TRIG, LOW); delayMicroseconds(5); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); long duration = pulseIn(ECHO, HIGH, 30000); // 超时 30ms,防卡死 if (duration == 0) return 999; // 超时视为无障碍 return duration * 0.034 / 2; } void setMotor(int leftSpeed, int rightSpeed) { // 正数前进,负数后退,0 停止 if (leftSpeed >= 0) { digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); } else { digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); } if (rightSpeed >= 0) { digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW); } else { digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH); } analogWrite(ENA, abs(leftSpeed)); analogWrite(ENB, abs(rightSpeed)); } void followLine() { int left = digitalRead(LEFT_TRACK); int right = digitalRead(RIGHT_TRACK); if (left == LOW && right == HIGH) { // 左轮压线,说明车往右偏了 setMotor(TURN_SPEED, -TURN_SPEED); // 向左转修正 } else if (left == HIGH && right == LOW) { setMotor(-TURN_SPEED, TURN_SPEED); // 向右转修正 } else { setMotor(FORWARD_SPEED, FORWARD_SPEED); // 直行 } } void avoidObstacle() { // 停下来,先朝左看再朝右看 setMotor(0, 0); headServo.write(30); delay(300); float leftDist = readDistance(); headServo.write(150); delay(300); float rightDist = readDistance(); headServo.write(90); delay(200); if (leftDist > rightDist) { // 左边空间大,向左转 90 度 setMotor(-TURN_SPEED, TURN_SPEED); delay(400); } else { setMotor(TURN_SPEED, -TURN_SPEED); delay(400); } setMotor(0, 0); } void loop() { float dist = readDistance(); Serial.print("Dist: "); Serial.println(dist); if (dist < SAFE_DIST) { avoidObstacle(); } else { followLine(); } delay(50); }先说readDistance里的超时参数30000,这个很重要。pulseIn默认会一直等回波,如果超声波前方是吸音海绵或者角度太偏,Echo 可能永远不来,程序会卡死在那里,小车就像丢了魂一样停在原地。加超时之后,30 毫秒没等到就返回 999,视为前方无障碍,程序能继续往下走。
avoidObstacle里的转向延时400是拍脑袋估的吗?不是,它是根据车轮直径和底盘轮距算出来的:轮距约 15 厘米,要原地转 90 度,轮子外缘需要走大约 23 厘米的弧长,全速下每秒约 60 厘米,所以 400 毫秒左右转 90 度。换底盘后这个值必须重测,方法很简单:写个小程序让车原地转一圈,用手机秒表看它多久转完 360 度,再除以 4 就是 90 度的延时。
SAFE_DIST设为 20 厘米,是考虑了车身的刹车滑行距离。小车全速前进时,从检测到障碍到电机真正停止,车轮还会往前拱一小段。如果你用的是全向轮或者抓地力差的硬塑料轮,滑行距离更长,20 厘米可能不够,调到 25~30 更稳。反过来,场地狭窄的话设太大会让车频繁转向,几乎走不动道。
3.3 参数怎么调:用一套“观察-改值-复测”的顺序,别乱试
调参最忌讳的是同时改两三个参数,出了问题根本不知道是谁引起的。我的习惯是按这个顺序来:
先调转向延时。把小车的轮子架空,不要放地上,把avoidObstacle里的延时从 200 毫秒开始试,每次加 50,看轮子转过多少度,直到接近 90 度。这个步骤排除地面摩擦的干扰,先把底盘自身的转向特性摸清楚。
再调FORWARD_SPEED。全速 255 看上去气势足,但超声波在运动状态下测距会有拖影,而且急停急转特别伤电池。一般 180~200 是折中值,既跑得起来,又给控制留有余量。调完速度,原来算好的转向延时可能又不对了,因为速度变了,同样的延时时走过的路程不一样了。所以调完速度后要回头复测一次转向。
最后调SAFE_DIST。把车放在一条直道上,前方放一个纸箱,从 30 厘米开始往 15 厘米递减,看车表现:距离太大,车离箱子老远就开始转向,走路歪歪扭扭;距离太小,车撞到箱子才反应过来。找到一个“车停下来时,车头离纸箱还有 3~5 厘米”的阈值,就刚刚好。
参数调好后,把程序里的参数区单独划出来,养成注释的习惯。我见过太多人把delay(400)写成delay(500)之后,过两天忘了自己为什么改,又把SAFE_DIST从 20 调成 10,结果小车全方位撞墙。参数区加一行注释记录“这个值是在什么场景下标定的”,比写十行代码注释更有价值。
4. 避障程序调试避坑:五个让小车“原地转圈或撞墙”的细节
4.1 超声波读数跳变:Echo 引脚悬空和供电不稳的双重问题
现象是串口监视器里距离值在正常读数和 0 之间来回跳,或者小车明明前方空旷,却突然触发避障转向。
排查下来,一半原因是 Echo 引脚接触不良。HC-SR04 的排针和杜邦线之间虚接,回波信号时有时无,pulseIn读到 0,而代码里把超时返回 999 当成无障碍,虚接时却可能读到一段病态的短脉冲,距离显示几厘米,直接触发避障。解决方法是把排针重新插紧,或者直接上锡焊。
另一半原因是供电。超声波模块的 VCC 如果和电机共用一个电源,电机启动瞬间电压跌落,模块的震荡电路频率漂移,回波时间变长,读数偏大。这个我一般建议给模块的 VCC 和 GND 之间并一个 100uF 电解电容,如果手头有 0.1uF 的瓷片电容也并上,滤高频和低频一起处理。
4.2 转向延时不准:换电池后小车“性格”突变
同一个程序,上午跑得好好的,下午再跑就开始撞墙或者原地转圈,很多人第一反应是代码出了问题。其实是电池电量变化导致的。
新电池电压高,电机转速快,固定延时 400 毫秒能转 90 度;用了几轮电,电压掉下来,转速变慢,同样的延时只能转 70 度。转向不足的结果是,小车第一次避障没躲开,第二次检测到障碍又转向,来回折腾就变成了原地抽风的“转圈车”。
这个问题没有完美的代码解法,只能从硬件上堵。我试过的办法里,用一个稳压模块给 L298N 供电是最省心的,常见做法是接一个 DC-DC 降压模块,把 7.4V 电池电压稳到 7V 左右给电机驱动板。电压稳了,转速就稳,转向延时才有意义。如果不想加硬件,那就把转向做成闭环:在程序里保留一个“本次转向后超声波距离是否变远”的判断,变远了说明转对了,没变远就再转一次,用逻辑补偿机械的偏差。
4.3 循迹检测与避障抢控制权:抖动和画龙的根本原因
前面提到过优先级问题,但实际跑起来还有一种情况:小车在黑线附近,避障刚结束,循迹模块检测到车身歪了,立刻执行转向修正,而这个修正动作让车身又靠近了障碍物,超声波又触发避障。两个逻辑来回切换,车身抖动,速度稍快就开始画龙。
我用的解法是在循迹分支里加一个“避障锁存时间”,代码层面就是加一个计数器:
// 避障结束后,给循迹一个稳定窗口,避免刚转向就又被避障打断 int stabilityCounter = 0; const int STABILITY_WINDOW = 10; // 约 0.5 秒 // 在主循环中: if (dist < SAFE_DIST) { avoidObstacle(); stabilityCounter = STABILITY_WINDOW; } else { if (stabilityCounter > 0) { stabilityCounter--; setMotor(FORWARD_SPEED, FORWARD_SPEED); // 锁存期内只管直行 } else { followLine(); } }这段逻辑的意思是,避障完成后让小车直行一小段时间,等车身姿态稳定了,循迹修正再介入。这个“锁存窗口”的值要根据实际车速调,太快了窗口内就撞上下一个障碍,太慢了循迹轨道又跑偏。10 个周期在 50 毫秒主循环下就是 0.5 秒,是个比较安全的起点。
4.4 强光下循迹模块全线输出 LOW:把高低电平读反了还不自知
TCRT5000 这类红外反射模块在阳光直射下会“假触发”,传感器的红外发射管被强烈的环境红外干扰,数字输出持续为 LOW,看起来就像永远压在黑线上。这时小车的循迹逻辑会认为左右都压线,按代码里的 else 分支直行,结果就是小车笔直冲出去,连黑线都不认了。
排查方法是用串口打印左右传感器的电平值,在强光下来回推车,看电平时序是否和黑线位置对应。如果环境光确实太强,最彻底的做法是改用带遮挡罩的循迹模块,或者把循迹传感器抬高一点,让它的发射角度更聚焦于地面。还有个小技巧,黑线用哑光黑色胶带,不要用光泽面,光泽面在强光下反射特性不稳定,读数更飘。
4.5 超声波对“软”障碍失效:泡沫箱和布料让小车直接撞上去
超声波的原理是接收回波,但软质材料吸收声波,回波弱到 HC-SR04 的接收端检测不到。前方放一块厚泡沫板,超声波显示 999(无障碍),小车直挺挺撞上去。
这算是超声波避障的物理天花板,程序层面能做的有限。我的做法是把SAFE_DIST稍微调大一些,给软障碍留出缓冲,同时在避障逻辑里加一个保险:如果前方持续测不到回波但红外循迹检测到车身严重偏离轨迹,强制停车。这个逻辑不完美,但能减少一部分软障碍事故。真要彻底解决,得换红外测距模块 TOF 或者激光雷达,那已经超出“简单避障程序”的范畴了。
5. 把避障做成一个状态机:三个状态串起整套行为,告别“if 套 if”
到了这一步,你的小车已经能完成基本的寻迹和避障了,但代码里全是独立的判断分支,加一个新功能就要到处改。我建议用一个极简状态机把整套行为串起来,状态切换条件清晰,以后加“路口转弯”或“回到起点”都是加一个状态的事。
三个核心状态是:FOLLOW(循迹直行)、AVOID(避障转向)、RECOVER(避障后恢复)。切换条件分别是:距离安全时进入FOLLOW;距离小于阈值进入AVOID;避障转向完成后短暂进入RECOVER,让车身姿态稳定后再回FOLLOW。
enum RobotState { FOLLOW, AVOID, RECOVER }; RobotState state = FOLLOW; unsigned long stateStartTime = 0; void updateState(float dist) { switch (state) { case FOLLOW: if (dist < SAFE_DIST) { state = AVOID; stateStartTime = millis(); } break; case AVOID: // 转向动作完成后切到 RECOVER,等车身稳定 if (millis() - stateStartTime > 600) { state = RECOVER; stateStartTime = millis(); } break; case RECOVER: // 恢复窗口结束,回到循迹 if (millis() - stateStartTime > 1200) { state = FOLLOW; } break; } }这个状态机的核心价值在于,每个状态只做自己的事,状态切换完全由时间和距离条件驱动,不会再出现上一节的“抢控制权”问题。millis()做超时判断有个注意点:它会在大约 50 天后溢出归零,对小车这种短时间运行不影响,但你如果做长时间无人值守,要处理溢出的边界情况。
做这个状态机时,我踩过最大的坑是在AVOID状态里没有把“转向完成”这个动作量化,只是简单地延时 600 毫秒,结果换了个地面材质(比如地毯上摩擦变大),转向不足,小车在AVOID和RECOVER之间来回切换,看起来像在原地打摆子。后来在AVOID结束前读一次超声波距离,如果发现距离还是小于阈值,就再执行一次转向,用“结果校验”代替“时间猜测”,小车才真正稳定下来。这个教训让我明白一件事:简单避障程序,代码可以简单,但决策逻辑一定不能简化成“猜时间”。现在的习惯是,每个状态的切换条件都写清楚是“基于时间”还是“基于测量”,调试的时候能少掉一半头发。希望帮到你。
本文还有配套的精品资源,点击获取