资讯动态

STM32避障小车源码解析:超声波+PWM+状态机实现自动导航

发布时间:2026/10/16 15:52:51 来源:尧图企业网站定制
简介面向嵌入式与智能小车爱好者这是一份基于STM32F103VC的避障智能小车完整工程源码以超声波传感器HC-SR04探测障碍物通过舵机CDS5516控制转向实现自主避障。工程包含初始化、传感器驱动、PWM舵机控制、避障逻辑与中断服务等模块适合正在学习STM32裸机开发、希望从代码层面理解避障算法的人群参考。压缩包共201个文件体积4.73MB。除了33个C源码与34个头文件还包含Keil工程配置、可烧写的HEX文件、编译生成的AXF/OBJ文件及调试辅助文件展开即可直接打开工程编译或烧录验证。目前已有2634人学习下载。这份源码的价值在于提供了从底层驱动到上层策略的完整闭环既有HC-SR04的测距时序处理也有PWM波精确控制舵机角度的方法更包含实际调试时留下的日志与工程备份便于对照排错和二次扩展如加装电机调速、蓝牙遥控等。1. STM32f103 避障智能小车源码一套能直接跑起来的硬件算法落地方案如果你手头有一块 STM32F103C8T6 最小系统板、两个直流减速电机、一个超声波模块和一个 L298N 驱动板那这个项目就是把这些散件变成一个能自己躲开墙、不会撞桌腿的智能小车。它解决的是嵌入式入门最经典的一类需求传感器采集、PWM 调速、状态机决策这三件事怎么在一个单片机上协同工作。源码的价值不在于多复杂而在于它把硬件接线和软件逻辑一一对应起来你照着烧进去就能看到小车动改几个阈值就能看到行为变化。这篇文章基于一个常见做法展开用 HC-SR04 超声波探头加舵机旋转探测左右和前方距离用状态机决定直行、转向还是倒车。适合刚学完单片机、想做一个完整项目的同学也适合比赛前想快速搭一个底盘的队伍。下文从硬件接线讲到底层驱动、决策逻辑和调参踩坑全程可以跟着复现。2. 从硬件选型到接线为什么这套组合是规避坑的最优选2.1 主控、传感、驱动三方选型先看懂各自的工作电压与信号类型做避障小车第一步不是写代码而是把供电和信号电压搞清楚。STM32F103 的工作电压是 3.3V而 HC-SR04 超声波模块的逻辑电平是 5VL298N 的逻辑输入部分也要 5V 供电才能正常工作。很多人第一次烧板子没反应不是代码问题是传感器和单片机的 GND 没共地或者单片机的 PWM 引脚直接接了 5V 设备导致芯片发热甚至烧毁。常见做法是主控用 STM32F103C8T6 最小系统板传感器用 HC-SR04因为 VCC 接 5VTrig 和 Echo 引脚挂一个电阻分压把 Echo 的 5V 回波信号降到 3.3V 以下再进单片机。电机驱动用 L298N它的驱动电源从电池取逻辑电源可以和单片机共用 5V但前提是共地。我一般会先准备一个 7.4V 锂电池组给 L298N 的供电端接电再用 L298N 板载稳压输出 5V 给单片机和舵机供电这样一套电即可点亮所有器件。接线表是这样的单片机 PA0 接超声波 TrigPA1 接超声波 EchoPA2 和 PA3 接舵机信号线PA8 和 PA9 接 L298N 的 IN1-IN4PA10 和 PA11 接 PWM 输出给 L298N 的 ENA 和 ENB。如果板子引脚紧张PWM 也可以用 PB0 和 PB1。这里有个关键点L298N 的 ENA 和 ENB 必须接 PWM不能直接接高电平否则小车只能全速跑避障时无法缓慢转向很容易撞上去。2.2 供电拓扑与共地一套 7.4V 电池怎么让三个模块都稳定工作很多人把 STM32 用 USB 线供电又把电机驱动单独用电池供电结果超声波模块检测距离忽远忽近舵机乱摆头。这是因为两套电源之间没有共同参考点Echo 信号线上出现共模干扰。正确接法是电池正极接 L298N 的 12V 输入口7.4V 也能工作L298N 板载的 5V 输出接到单片机 5V 引脚和超声波 VCC舵机电源也从同一路 5V 取最后把 L298N 的 GND、单片机的 GND、超声波 GND、舵机 GND 全部连在一起。还有一个电压坑L298N 板载稳压输出能力有限舵机堵转时电流能到几百毫安加上超声波正常工作 15mA如果 L298N 同时驱动两个电机电源噪声会很严重。我一般建议电机电源独立信号电源从 L298N 取同时在单片机电源输入附近加一个 100uF 电解电容和 104 瓷片电容滤波。这样做的代价是接线多一根但调试时间能省一大半。2.3 电机与轮径的匹配为什么转向速度和轮距直接决定避障算法复杂度避障算法的核心是转弯时机和转弯幅度这背后是电机转速和轮径的物理参数。比如用常见的 TT 电机减速比 1:48额定电压 6V 时空载转速约 300 转/分配上 65mm 直径的轮子直线速度大约 1m/s。如果超声波检测距离是 30cm那从检测到障碍到车头到达障碍位置只有约 0.3 秒单片机必须在这个时间内完成转向。所以转向速度越快算法越简单但也容易把轮子打滑导致角度不准。另一条路是牺牲一点速度换稳定把 PWM 占空比设成 60% 左右让小车以约 0.5m/s 的速度巡航检测距离阈值设成 40cm这样控制器有约 0.8 秒的反应时间。配合舵机旋转超声波的扫描时间约 0.3 秒决策周期控制在 1 秒左右状态机有充分时间执行完整动作。如果你的车体是四轮底盘但只用两个电机驱动前轮是万向轮那么转弯时万向轮会产生一个额外的滑动摩擦这会让实际转角比理论转角大一些算法里就不要用固定延时转弯而是用舵机回正后的标志位触发直行。3. 拆解避障源码的骨架初始化、测距、决策与驱动四大段3.1 标准外设库初始化先让时钟和引脚各就各位拿到源码后不要急着看避障逻辑先把初始化部分读通。STM32F103 的源码无论用标准外设库还是 HAL 库结构都类似先初始化系统时钟到 72MHz然后使能 GPIOA、GPIOB、TIM2、TIM3 的时钟再配置引脚模式和定时器。下面是一段常见的初始化核心代码用标准外设库风格它配置了超声波触发引脚、回声捕获引脚、两个中断引脚和两路 PWM 输出void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 超声波Trig引脚: PA0, 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 超声波Echo引脚: PA1, 浮空输入 (配合外部下拉电阻) GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 舵机信号: PA2, 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // L298N控制引脚: PA3, PA4, PA5, PA6, 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // PWM输出: PA7(TIM3_CH2)和PB0(TIM3_CH3) GPIO_InitStructure.GPIO_Pin GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOB, GPIO_InitStructure); }这段配置里有两个容易出错的点Echo 引脚用浮空输入还是复用输入标准做法是用浮空输入因为 HC-SR04 的 Echo 引脚在空闲时是低电平触发后拉高单片机只需要读 GPIO 电平不需要外部上拉。另一个点是舵机和 PWM 输出都用复用推挽但舵机用的是定时器输出比较通道L298N 的 IN 引脚用普通推挽即可。配置完成后还要单独初始化一个定时器用来给超声波测距计时通常用 TIM2 或者 TIM4开一个 1us 的计数周期通过读取计数器值来计算高电平持续时间。3.2 超声波测距函数从触发到回波的时序细节和返回值处理HC-SR04 的测距原理是Trig 引脚拉高至少 10us模块内部发出 8 个 40kHz 方波然后 Echo 引脚拉高高电平持续时间就是声波往返时间。距离等于高电平时间乘以声速除以二。源码里最常见的实现是用延时函数触发然后用 while 循环等 Echo 拉高开启定时器再等 Echo 拉低读定时器值。这种实现简单但有个隐患如果前方没有障碍物Echo 会一直保持低电平程序会在 while 里死等整个小车卡死。所以成熟的源码都会加一个超时退出机制。下面这段代码加了 50ms 超时保护同时做了多次采样取平均值这个是避障稳定性的关键uint32_t ultrasonic_measure(void) { uint32_t time_out 0; uint32_t tick_start 0; uint32_t tick_end 0; // 1. 触发拉高10us以上 GPIO_SetBits(GPIOA, GPIO_Pin_0); delay_us(15); GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 2. 等待Echo拉高最多等50ms time_out 50000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0 time_out 0) { delay_us(1); time_out--; } // 3. 若超时则返回0代表检测不到障碍 if (time_out 0) return 0; // 4. 开始计时读取TIM2计数器 TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); // 5. 等待Echo拉低同样加超时 time_out 50000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) ! 0 time_out 0) { delay_us(1); time_out--; } tick_end TIM_GetCounter(TIM2); TIM_Cmd(TIM2, DISABLE); // 6. 超时按最大值处理 if (time_out 0) return 999; // 7. 距离(mm) 时间(us) * 0.17 (声速340m/s时) return tick_end / 58; }这个函数返回的是毫米单位。tick_end 是微秒计数除以 58 是常见的 HC-SR04 距离转换公式因为 1cm 对应约 58us 的回波时间。注意返回值 0 代表没障碍或超时返回值 999 代表回波异常这两种情况在决策逻辑里要分开处理。多次采样取平均数一般放在调用层做比如连续测三次去掉最大和最小值再平均这比简单五连测更抗干扰。3.3 舵机扫描与多方向采样60ms 一拍的轮询逻辑避障不能只测前方否则侧面近处的障碍物会在转弯时刮到。常见方案是舵机带动超声波探头分别朝左、前、右三个方向测量每次测量前先给舵机一个固定角度的 PWM等待舵机稳定后再触发超声波。舵机稳定时间一般要 300ms 到 500ms如果把每个方向都等稳定再测一轮就需要 1.5 秒反应太慢。所以源码里常用「先发角度、不等待、立即测上一次角度」的流水线方式。具体实现第一步把舵机转到左侧 90 度对应 PWM 脉宽约 1500us等待 400ms 后测一次距离第二步转到正前方 90 度再等 400ms 测距第三步转到右侧 90 度同样测距。这个 400ms 足以让舵机转动到位三轮下来总耗时约 1.2 秒。如果你用的舵机是 SG90 模拟舵机脉宽范围是 500us 到 2500us对应 0 到 180 度中间值 1500us 是正前方。左和右各偏移 60 到 75 度视车体宽度而定。void scan_directions(uint32_t *dist_left, uint32_t *dist_front, uint32_t *dist_right) { // 舵机转到左侧 servo_set_angle(LEFT_ANGLE); delay_ms(400); *dist_left ultrasonic_measure(); // 舵机转到正前方 servo_set_angle(FRONT_ANGLE); delay_ms(400); *dist_front ultrasonic_measure(); // 舵机转到右侧 servo_set_angle(RIGHT_ANGLE); delay_ms(400); *dist_right ultrasonic_measure(); }这里的 delay_ms 用 SysTick 实现不要用普通 for 循环空转。因为 400ms 的空转循环会阻止中断响应如果 PWM 是靠定时器输出不受影响但如果你用了外部中断接收编码器信号就会丢脉冲。流水线方式的进阶版是把三向测距拆成三个状态在定时器中断里切换但新手阶段顺序扫描完全够用只要把阈值设得宽裕一些。3.4 状态机避障决策直行、左转、右转、倒车四种状态的转移条件避障逻辑本质上是一个有限状态机。状态有四个前进、左转、右转、倒车。默认状态是前进当正前方距离小于阈值进入避障判断此时比较左右距离哪边远转向哪边如果左右都小于阈值进入倒车退一段后再转向。这里的关键参数是转向持续时间取决于车体转动速度和期望转角。下面给出一个常见的决策函数typedef enum { STATE_FORWARD, STATE_TURN_LEFT, STATE_TURN_RIGHT, STATE_BACKWARD } CarState; CarState current_state STATE_FORWARD; void avoidance_decision(uint32_t front_dist, uint32_t left_dist, uint32_t right_dist) { uint32_t threshold 300; // 避障距离阈值(mm) switch(current_state) { case STATE_FORWARD: if (front_dist threshold) { // 前方障碍比较左右距离 if (left_dist right_dist) { set_motor_speed(LEFT_MOTOR, 0); set_motor_speed(RIGHT_MOTOR, 60); current_state STATE_TURN_LEFT; } else if (right_dist left_dist) { set_motor_speed(LEFT_MOTOR, 60); set_motor_speed(RIGHT_MOTOR, 0); current_state STATE_TURN_RIGHT; } else { // 左右都近倒车后左转 set_motor_speed(LEFT_MOTOR, -40); set_motor_speed(RIGHT_MOTOR, -40); current_state STATE_BACKWARD; } } break; case STATE_TURN_LEFT: // 左转持续400ms后回正 if (turn_timer 400) { set_motor_speed(LEFT_MOTOR, 60); set_motor_speed(RIGHT_MOTOR, 60); current_state STATE_FORWARD; } break; case STATE_TURN_RIGHT: if (turn_timer 400) { set_motor_speed(LEFT_MOTOR, 60); set_motor_speed(RIGHT_MOTOR, 60); current_state STATE_FORWARD; } break; case STATE_BACKWARD: // 倒车600ms后转向 if (turn_timer 600) { // 右转 set_motor_speed(LEFT_MOTOR, 60); set_motor_speed(RIGHT_MOTOR, 0); current_state STATE_TURN_RIGHT; } break; } }这个决策函数里最需要调的是 threshold 和 turn_timer。threshold 设成 300即 30cm适合室内桌面环境如果地面有地毯或者室内通道较窄可以降到 250。turn_timer 不是定时器中断里的全局计数而是一个在主循环里每次递增的变量它必须和扫描周期同步例如扫描一轮 1.2 秒每 50ms 调一次决策那么 400ms 就是 8 次决策周期。用转向标志位而不是绝对时间更稳妥因为每次转向的起始时刻和调用时刻可能相差几十毫秒固定延时会让转向角度误差积累。3.5 电机驱动底层PWM 占空比与正反转逻辑的对应关系L298N 驱动电机是通过 IN1-IN4 控制方向ENA/ENB 控制速度。要前进就需要左轮 IN1H、IN2L右轮 IN3H、IN4L同时 ENA 和 ENB 引脚输出 PWM。由于 STM32 的 PWM 通道有限通常一个定时器输出两路 PWM 分别给左右轮另外四个 IN 引脚用普通 GPIO。PWM 频率设在 1kHz 到 10kHz 之间1kHz 会有轻微电机啸叫但扭矩更足10kHz 静音但电机内阻压降变大。常见做法是 20kHz人耳听不到而且 L298N 开关损耗可以接受。void set_motor_speed(MotorSelect motor, int speed_percent) { // speed_percent: -100到100 if (motor LEFT_MOTOR) { if (speed_percent 0) { GPIO_SetBits(GPIOA, GPIO_Pin_3); // IN1 1 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // IN2 0 } else if (speed_percent 0) { GPIO_ResetBits(GPIOA, GPIO_Pin_3); GPIO_SetBits(GPIOA, GPIO_Pin_4); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_3); GPIO_ResetBits(GPIOA, GPIO_Pin_4); } // 设置占空比: TIM3_CH2 TIM_SetCompare2(TIM3, (uint32_t)(abs(speed_percent) * 100)); } else if (motor RIGHT_MOTOR) { // 同理配置IN3/IN4和TIM3_CH3 // ... } }速度设定值用的是 -100 到 100 的百分比内部映射到 0-1000 的 PWM 比较值。有一个要注意的问题是两个电机的物理特性不完全一样同一占空比下左轮和右轮转速可能有 10% 的差异所以小车会跑偏。要解决跑偏要么在实测后给慢的一侧加一个补偿系数要么用编码器做闭环。入门阶段我建议手动补偿让小车直行观察 2 秒内的偏移方向然后把对应电机的速度值乘 1.1 或 0.9反复调到基本直线为止。4. 主循环怎么把上面这些串起来单线程调度与超时护栏4.1 不用 RTOS 的单线程调度一个 while(1) 撑起全部功能很多入门源码的结构就是一个 while(1) 循环里按顺序执行三件事扫描距离、执行决策、刷新舵机。这样写直观但有个问题超声波测距函数里的延时会让整个循环的周期不稳定舵机转动时的 400ms 等待又让决策变得迟钝。要解决这个问题可以把主循环改成「非阻塞」模式每个功能模块在自己的状态里推进循环每转一圈只处理一个小的步骤。例如扫描三个方向的动作可以拆成 6 个状态SEND_LEFT_ANGLE、WAIT_LEFT_STABLE、MEASURE_LEFT、SEND_FRONT_ANGLE、WAIT_FRONT_STABLE、MEASURE_FRONT最后一个状态送右。每个状态只做一件事然后用一个全局状态变量保存当前进度主循环每次进来就判断当前状态并推进。这样可以保证循环周期稳定在几个微秒级别舵机转向的时间可以并行处理其他计算。缺点是代码比顺序调用复杂一些但带来的响应速度提升很明显。int main(void) { uint32_t dist_left, dist_front, dist_right; SystemInit(); GPIO_Config(); Timer_Config(); servo_set_angle(FRONT_ANGLE); while (1) { // 1. 扫描三向距离 scan_directions(dist_left, dist_front, dist_right); // 2. 执行避障决策 avoidance_decision(dist_front, dist_left, dist_right); // 3. 刷新转向计时 if (turn_timer 9999) { turn_timer; } // 4. 打印调试信息到串口 printf(F:%d L:%d R:%d S:%d\r\n, dist_front, dist_left, dist_right, current_state); } }上面这个主循环是顺序扫描版本每轮耗时约 1.3 秒。如果你觉得决策间隔太长可以把舵机固定不动只测前方这样循环周期能缩到 100ms 左右但小车对侧面障碍毫无感知。折中方案是「前方优先左右辅助」每轮先测前方若前方距离正常直接直行不等待左右扫描若前方障碍近再启动三向扫描。常见做法中这个「按需扫描」能显著提高小车的平均速度。4.2 超时护栏的必要性当超声波「看不到」目标时怎么办实测环境里超声波经常返回异常值。比如在墙角处声波多次反射后 Echo 高电平时间异常长换算出来的距离可能超过 500cm或者在开阔地带前方没障碍物Echo 一直不拉高函数超时返回 0。这两种情况如果直接进入决策逻辑会导致小车突然转弯或者原地乱转。所以源码里要在测距函数返回之后加一个「合法范围过滤」uint32_t get_valid_distance(void) { uint32_t raw ultrasonic_measure(); // 过滤超出感应范围或异常值使用上一次有效值 if (raw 0 || raw 999 || raw 400) { return last_valid_distance; } last_valid_distance raw; return raw; }这里的 last_valid_distance 初始值设为 400cm 单位是 4000但上面函数返回毫米所以这里判断 raw 4000。过滤逻辑的意义在于让决策状态机不会因为单次异常值而抖动。如果你发现小车在空旷地面偶尔自己转一下弯多半是超声波在乱反射调大「连续多少次异常才改变状态」的计数即可比如连续 5 次测距异常才认为前方没有障碍否则维持上一次判断。5. 避障源码的 5 个必调参数与典型翻车现场排查5.1 必调参数一避障距离阈值——设成多少取决于车速和车体长度避障阈值直接决定小车反应时机。设大了小车离障碍老远就停走得扭扭捏捏设小了车头快贴上墙才转舵机扫描还没完成就撞上。一个保守公式阈值(mm) 车速(mm/s) × 决策周期(s) 车体长度(mm) 安全余量(30~50mm)。比如车速 300mm/s决策周期 0.5s车体长 150mm那么阈值 150 150 40 340mm。很多源码默认写 300mm如果你跑得慢可以减到 250mm跑得快必须加。5.2 必调参数二左右转向时间——这个参数决定你能不能走出 U 型障碍转向时间是避障回调后能否成功穿越复杂地形的关键。用上述 1.2 秒扫描周期和 60% 占空比小车差速转向约 90 度需要 350~500ms。你可以在调试时把小车放在开阔地发一个左转指令实测车头转 90 度需要多久然后把这个值写入代码。如果转向时间过长小车会转成掉头过短则无法绕过较宽的障碍物。更精细的做法是转向时让外侧轮全速、内侧轮停转这样转向半径最小但要有足够的电压支撑否则电机带不动。5.3 必调参数三PWM 占空比上限——为什么不能直接 100%很多第一次跑的人把前进速度设成 100% 占空比结果小车猛冲出去超声波还没反应过来就撞墙了。另外 100% 占空比下 L298N 的管子压降很大发热严重电池耗电快。我一般把直线巡航设在 50% 到 65%转向时一侧 20%、一侧 60% 至 70%。如果小车速度太慢走不动优先检查电池电压是不是低于 6V而不是直接提高占空比。5.4 必调参数四舵机角度偏移量——装歪了怎么校准舵机装到车体上时很少是绝对垂直正前方。如果探头偏了 10 度测出的前方距离会偏大或偏小导致避障撞车。校准方法给舵机发 1500us 脉宽在舵机转轴上贴一根细长竹签观察竹签指向是否与车体前进方向平行若不平行在代码里把这个角度对应的脉宽值加或减几十微秒。这个参数每个车都要单独调因为舵机本身有制造公差同一型号两只舵机的 90 度位置可能差 20us。5.5 常见问题排查遇到测距乱跳、舵机抖、无法开机这几类问题先查什么先记录几个高频故障的现象和解决路径。现象一串口打印的距离数值在无遮挡时随机跳。原因大概率是超声波模块的 VCC 供电纹波大或者 Echo 信号线上有干扰。解决把超声波电源从 L298N 的 5V 输出单独接并且 Echo 线用杜邦线屏蔽把旁边接 GND 线夹住再不行就用万用表量 L298N 输出电压是否低于 4.8V。现象二舵机发抖且转不到位。原因可能是 5V 电源被电机拉低舵机供电不足或者 PWM 脉宽范围不对。解决先断开电机只给舵机通电看是否正常如果正常说明是共电源导致欠压需要把舵机电源和电机驱动电源分开或者换一个大功率 5V 稳压模块。现象三烧录后小车无反应。原因可能是 BOOT0 跳线没有接地或者单片机 3.3V 引脚没接 5V 稳压输出而是接了锂电池。常见做法是检查 BOOT0 是否接低电平然后再看 ST-Link 能不能识别芯片。如果 ST-Link 能识别但程序不跑多半是复位电路问题在 NRST 引脚对地加一个 0.1uF 电容即可。现象四超声波测距值偏大或偏小但有规律。偏大通常是 Echo 引脚串联了电阻导致逻辑电平被抬高你需要确认这个引脚的外部下拉电阻是否过高偏小通常是把检测到障碍就当成了 0 距离导致决策逻辑认为左或右是空的实际上被过滤了。解决在代码 logging 出原始 raw 值看看是不是有个固定的偏移量然后修正转换公式。现象五小车转弯时明显一顿一顿。原因是决策函数里调用了 delay_ms(400) 等待舵机稳定整个循环卡在这里任何中断都没法处理。解决把 scan_directions 里的阻塞延时改为「发送角度后立即返回在后续循环间隙续算时间」或者直接用上一节说的非阻塞状态机。6. 升级路线与验证方法从「能跑」到「稳定不撞」的两个阶段6.1 阶段一验证静态标定与单障碍通过测试拿到源码并成功烧录后不要急着放到复杂地形测试。第一步把一个纸箱放在前方 40cm 处串口打印读到的距离值看是否稳定在 38~42cm 之间第二步让小车对准纸箱直行观察它在 20cm 处是否开始转向第三步分别在左侧、右侧各放一个障碍测试转向方向是否正确。三个测试通过说明传感器和基本决策链路是通的。这里有个技巧调整避障阈值时每改一次参数都重新测试一次不要同时改多个参数否则变量无法定位。6.2 阶段二升级加入转向回正后继续探测的机制基础版本在完成一次转向后无条件恢复直行但在 U 型通道或者桌腿密集的环境里小车常常刚转过来还没对准出口又撞上下一根腿。常见升级做法是转向结束后做一次「前方探测」若前方距离仍然小于阈值说明还没绕开障碍应继续转向或者增加一个「沿障碍滑动」的模式。移动模式实现如下以左侧墙壁为例设一个期望距离 20cm实际距离大于 25cm 时稍微右转靠近障碍小于 15cm 时稍微左转远离障碍这样小车沿着墙面滑过去绕行的成功率比单次转向高很多。void wall_follow_mode(uint32_t current_left_dist) { // 期望左墙距离 20cm允许误差 5cm if (current_left_dist 250) { // 距离太远右转靠墙 set_motor_speed(LEFT_MOTOR, 70); set_motor_speed(RIGHT_MOTOR, 50); } else if (current_left_dist 150) { // 距离太近左转离墙 set_motor_speed(LEFT_MOTOR, 50); set_motor_speed(RIGHT_MOTOR, 70); } else { // 在范围内直行 set_motor_speed(LEFT_MOTOR, 60); set_motor_speed(RIGHT_MOTOR, 60); } }这个巡墙函授数看起来简单但有一个关键限制超声波探头是装在舵机上的左转时探头方向偏左测出的距离是斜线距离而不是垂直距离比真实垂直距离大。所以用巡墙模式时如果你测得距离稳定在 20cm实际垂直距离可能只有 14cm。我一般会在代码里加一个三角函数换算真实距离 测量距离 × cos(左偏角度)。如果你把舵机放在车头正中央左偏 60 度测量距离 20cm垂直距离只有 10cm这在狭窄通道里是致命的务必换算。6.3 联调技巧串口数据怎么读才有意义调试时不要盯着 printf 输出的距离值看那个数据刷太快根本反应不过来。正确做法是让源码每 200ms 输出一组格式化数据包含三个距离值和当前状态码然后在电脑串口助手上打开时间戳观察小车撞墙前 1 秒的数据变化。比如看到前方距离从 400 突降到 100说明超声波相对于上一轮扫描的延时太长真正的障碍在两次扫描之间已经进入危险区。这时候就需要缩短扫描周期或者提高决策频率。6.4 最后一步硬件装配与结构化线束习惯当所有逻辑调通后最后决定这台车寿命的是装配方式。我吃过亏的教训是先把舵机线、电机线、超声波线全部用扎带固定再插到单片机上否则小车在转向时线束甩动会导致杜邦线松动面包板接触不良现象表现为「代码跑着跑着突然重启」。焊接版比面包板稳定但如果你坚持用面包板至少给电机驱动板供电的两根线用粗一点的22AWG不要用杜邦线过 2A 电流。装好后把电池固定在小车中部重心位置避免头重脚轻导致转弯时翻车。我自己的习惯是每完成一次避障逻辑调整就用马克笔在车底写下当天的参数组编号地面铺一卷 2 米长的纸板墙记录小车从出发到通过的总时间和碰撞次数。有了记录你才能看清到底是转向时间不够还是避障阈值太激进。希望这篇笔记对你有用祝你的小车早日做到全程不碰墙、稳当地跑到电量耗尽。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑