ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

简介:面向嵌入式与智能小车爱好者,这是一份基于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 超声波模块的逻辑电平是 5V,L298N 的逻辑输入部分也要 5V 供电才能正常工作。很多人第一次烧板子没反应,不是代码问题,是传感器和单片机的 GND 没共地,或者单片机的 PWM 引脚直接接了 5V 设备导致芯片发热甚至烧毁。

常见做法是:主控用 STM32F103C8T6 最小系统板,传感器用 HC-SR04,因为 VCC 接 5V,Trig 和 Echo 引脚挂一个电阻分压把 Echo 的 5V 回波信号降到 3.3V 以下再进单片机。电机驱动用 L298N,它的驱动电源从电池取,逻辑电源可以和单片机共用 5V,但前提是共地。我一般会先准备一个 7.4V 锂电池组给 L298N 的供电端接电,再用 L298N 板载稳压输出 5V 给单片机和舵机供电,这样一套电即可点亮所有器件。

接线表是这样的:单片机 PA0 接超声波 Trig,PA1 接超声波 Echo,PA2 和 PA3 接舵机信号线,PA8 和 PA9 接 L298N 的 IN1-IN4,PA10 和 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 控制速度。要前进,就需要左轮 IN1=H、IN2=L,右轮 IN3=H、IN4=L,同时 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 初始值设为 400(cm 单位是 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 米长的纸板墙,记录小车从出发到通过的总时间和碰撞次数。有了记录,你才能看清到底是转向时间不够还是避障阈值太激进。希望这篇笔记对你有用,祝你的小车早日做到全程不碰墙、稳当地跑到电量耗尽。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进