Arduino寻迹避障小车:简单避障程序从接线到状态机全解析 简介面向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 / IN2D5 / D6左侧电机正反转控制L298N 电机驱动IN3 / IN4D9 / D10右侧电机正反转控制L298N 电机驱动ENA / ENBD3 / D11PWM调速不接时默认全速HC-SR04 超声波TrigD12发送 10us 触发脉冲HC-SR04 超声波EchoD13接收回波输入引脚TCRT5000 循迹模块 x2DO数字输出D7 / D8左 / 右传感器L298N 电源输入VIN / GND外部电池 7.4V~12V给电机供电Arduino5V / GNDL298N 的 5V 输出给传感器和 Uno 供电接线有个关键原则电机电源和逻辑电源要分开走。L298N 的板载稳压可以输出 5V但这路 5V 是给 Arduino 和传感器用的不是给电机用的。电机的瞬间启动电流能到 1A 以上如果 Arduino 也从这个口取电电压跌落会让单片机直接复位表现就是小车一启动就重启或者超声波读数突然变成 0。我用的是两节 18650 串联供电正极进 L298N 的 VINL298N 的 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结束前读一次超声波距离如果发现距离还是小于阈值就再执行一次转向用“结果校验”代替“时间猜测”小车才真正稳定下来。这个教训让我明白一件事简单避障程序代码可以简单但决策逻辑一定不能简化成“猜时间”。现在的习惯是每个状态的切换条件都写清楚是“基于时间”还是“基于测量”调试的时候能少掉一半头发。希望帮到你。本文还有配套的精品资源点击获取