简介:面向嵌入式学习者的STM32三路超声波自动跟随小车完整工程,基于STM32与三路超声波传感器实现障碍检测与自动跟随,涵盖控制策略、传感器应用及电机驱动等核心内容,适合作课程设计、毕业设计或智能车入门实践。资源包共309个文件,压缩包8.45MB,以c/h源文件为主,包含大量编译中间文件(o、crf、s等)以及hex、uvproj、ewp等工程配置文件,可对照源码、配置与输出文件快速理解工程结构和调试流程。目前已有2399人学习下载。项目工程不仅给出主函数和硬件配置,还包含Keil/IAR等工程文件、调试脚本与map文件,便于使用者分析编译链接细节和内存分布;同时可结合描述中的PID或模糊控制思路,自行优化跟随策略,是掌握STM32嵌入式开发与超声测距应用的实用参考。 我这次做的小车从立项到跑通,前后折腾了两周半。核心目标很明确:用STM32做主控,靠三路超声波传感器实现对人体的实时测距和方向判断,让小车能自动跟着人走,既不掉队也不怼人。过程中踩了不少坑,也把网上零散的资料整理成了自己的代码框架。这篇就按我的实现思路,把这台三路超声波自动跟随小车的硬件选型、控制逻辑、代码细节和调参经验完整捋一遍。如果你正准备做类似的STM32项目,或者拿它当毕设底子,这篇文章里提到的很多细节可以直接用。
1. 三路超声波方案:它到底比单路、双路强在哪
1.1 自动跟随的需求难点拆解
先说结论:只靠一个超声波模块,做出来的东西充其量叫"避障小车",不叫"跟随小车"。单路超声波放在正前方时,小车能知道前方有没有障碍物、距离多远,但它完全不知道目标在左边还是右边。双路超声波如果左右各放一个,可以判断目标偏哪边,可一旦目标离得很近或者偏离角度超过传感器波束角,小车就会丢失目标,原地愣住。
自动跟随本质上要解决三个同时发生的问题:目标有多远、目标在哪个方向、我该用多大速度往哪个方向跑。三路超声波恰好覆盖了这三个需求——正前方一路负责测纵向距离,左右两路负责测横向偏移。三路数据组合起来,就能判断出目标大致处于小车前方扇形区域的哪个位置,从而决策出前进、后退、左转、右转、停止五个基本动作。
我最终选的是 HC-SR04 超声波模块,三块同型号。之所以不用单总线集成的激光雷达或 TOF 传感器,一是成本问题,三块 HC-SR04 加起来不到三十块钱;二是这个项目本来就是练手性质,HC-SR04 的触发和回波逻辑够简单,配合定时器输入捕获就能做精确测距,非常适合理解测距原理。如果你的预算充裕,可以把传感器换成 US-015 或者 JSN-SR04T,代码逻辑基本不变,只是电气参数和波束角略有差异。
1.2 硬件清单与接线规划
我在硬件选型上没有堆料,全部按"够用且好调"的标准来:
| 器件 | 型号/规格 | 数量 | 作用 |
|---|---|---|---|
| 主控 | STM32F103C8T6 最小系统板 | 1 | 核心控制,定时器捕获、PWM输出 |
| 超声波模块 | HC-SR04(5V供电) | 3 | 前、左、右三向测距 |
| 电机驱动 | L298N 双路H桥 | 1 | 驱动两个直流减速电机 |
| 电机 | 3-6V 直流减速电机 | 2 | 两轮差速驱动 |
| 电池 | 7.4V 18650 电池组两节串联 | 1 | 整车供电 |
| 降压模块 | LM2596S 降压模块 | 1 | 7.4V 降到 5V 给主控和传感器供电 |
| 车架 | 亚克力三层板小车底盘 | 1 | 平台支撑 |
这里 CH1 到 STM32 的接线我直接给出来,免得新手对着数据手册翻半天:
| 传感器信号 | STM32引脚 | 备注 |
|---|---|---|
| 前路 Trig | PA0 | 通用GPIO输出 |
| 前路 Echo | PA1 | 定时器TIM2_CH2输入捕获 |
| 左路 Trig | PA2 | 通用GPIO输出 |
| 左路 Echo | PA3 | 定时器TIM2_CH3输入捕获 |
| 右路 Trig | PA4 | 通用GPIO输出 |
| 右路 Echo | PA5 | 定时器TIM2_CH4输入捕获 |
| 左电机 PWM | PA6 | TIM3_CH1 |
| 右电机 PWM | PA7 | TIM3_CH2 |
| 左电机方向 | PB0/PB1 | L298N IN1/IN2 |
| 右电机方向 | PB10/PB11 | L298N IN3/IN4 |
有一点必须提:HC-SR04 的 Echo 回波引脚输出的是 5V 高电平,而 STM32F103 的 GPIO 是 5V 容忍的,可以直接接,不会烧引脚。但如果是其它型号的单片机,先确认是否 5V 容忍,不是的话必须加电平转换或分压电阻。
2. 传感器布局与跟随决策表:小车是怎么"想"出方向的
2.1 三路探头的物理布局与测量范围
布局方式直接决定决策逻辑能不能成立。我采用的是"正前方 0 度 + 左右各 45 度"的布局:前路传感器平行于车头指向正前方,左右两路分别向外侧偏转 45 度安装。HC-SR04 的波束角大约 15 度,偏转 45 度之后,左右两路的探测区域会在车前方形成交叉覆盖。三路传感器能覆盖车身正前方大约 120 度的扇形区域,目标只要在这个范围内,至少有一路传感器能测到有效距离。
安装高度方面,我踩了个坑:传感器离地太近,会把地面当成障碍物,尤其是走过瓷砖缝隙或者地面有轻微凸起时,回波会直接误判。后来把传感器安装在离地 8 到 10 厘米的高度,倾斜角往下压 5 度左右,让波束平行于人体小腿高度,效果明显改善。
还有一个容易忽略的点:三路传感器的 Trig 和 Echo 信号线要避免和电机电源线走在一起。HC-SR04 的 Echo 回波是微秒级的高电平脉宽,电机启停时的电流尖峰如果串进信号线,测距值会出现偶发抖动。我在布局时把信号线全部走车架上层,电源线走下层的过线槽,两者保持 3 厘米以上间距,实测干扰小了很多。
2.2 五状态跟随决策逻辑
传感器的数据读到之后,不能直接拿去驱动电机,中间必须有一个"状态决策"层。我定义了一个五状态有限状态机:停止、前进、后退、左转、右转。决策输入是三路距离值,输出是电机 PWM 占空比和方向电平。
先设定几个关键阈值:
| 参数 | 数值 | 含义 |
|---|---|---|
| STOP_DIST | 20cm | 小于此距离,认为目标太近或已撞到,停车 |
| FOLLOW_MIN | 30cm | 跟随区间下限,小于此距离开始后退 |
| FOLLOW_MAX | 50cm | 跟随区间上限,大于此距离开始前进 |
| OFFSET_DEAD | 15cm | 左右距离差小于此值,不修正方向,避免频繁摆动 |
跟随的核心逻辑分两层判断。第一层是纵向距离判断:取前路距离值 F,如果 F 小于 20cm,无论左右如何,立即停车;如果 F 在 30cm 到 50cm 之间,纵向不动作,保持当前速度;如果 F 大于 50cm,前进;F 小于 30cm 且大于 20cm,后退。第二层才是方向判断:取左右两路距离 L 和 R,如果 L 比 R 大超过 15cm,说明目标偏右,需要右转;反过来则左转。当左右差在正负 15cm 以内时,认为目标在正前方,不修正方向。
"先判距离、再判方向"这个顺序很重要。如果先判方向,遇到目标很近的情况,小车可能一边停车一边原地转向,姿态非常奇怪。先保证纵向距离安全,再解决横向偏差,控制逻辑会顺很多。
2.3 状态机的代码骨架
状态机本身不复杂,关键是要写得可读、易调参。我的实现里把每个状态对应的动作封装成了独立函数,主循环里只做"状态判定 + 动作执行"两件事:
typedef enum { STATE_STOP, STATE_FORWARD, STATE_BACKWARD, STATE_LEFT, STATE_RIGHT } FollowState; FollowState follow_state_machine(float dist_front, float dist_left, float dist_right) { if (dist_front < STOP_DIST) { return STATE_STOP; } if (dist_front < FOLLOW_MIN) { return STATE_BACKWARD; } if (dist_front > FOLLOW_MAX) { return STATE_FORWARD; } float offset = dist_left - dist_right; if (offset > OFFSET_DEAD) { return STATE_RIGHT; } if (offset < -OFFSET_DEAD) { return STATE_LEFT; } return STATE_STOP; }这个函数返回的是目标状态,真正执行时还要根据目标状态设定电机方向和 PWM。有一点需要额外说明:为什么在 30cm 到 50cm 区间内且左右平衡时选择"停止"而不是"原地等待"?因为在跟随场景下,这个区间是理想跟随时距,此时目标相对于小车基本静止,小车不需要补充位移。如果此时继续前进,会迅速突破 20cm 的安全距离,导致停车-后退-前进的来回震荡。
3. 从定时器捕获到 PWM 输出:代码实现全链路
3.1 超声波测距为什么不建议用 Delay
HC-SR04 的测距原理不难:Trig 引脚给 10 微秒以上的高电平,模块内部自动发出 8 个 40kHz 的超声波脉冲,同时 Echo 引脚拉高;当模块收到回波,Echo 拉低。Echo 高电平的持续时间就是超声波从发射到返回的总时间,距离等于时间乘以声速再除以 2。
网上很多教程直接用一个 GPIO 口测 Echo 高电平时间,方法是while(ECHO_PIN == 1) { time++; }。这种做法在单路测距时勉强能跑,但在三路跟随场景下问题很大:Echo 高电平持续时间最长能到 30ms 级别,如果主循环里死等三路回波,总共可能要阻塞近 100ms,电机控制的实时性完全没法保证。小车会出现在目标已经转向时,控制指令还停留在上一帧的状态。
我改用 STM32 的定时器输入捕获功能来测脉宽。原理是:把定时器配置成捕获模式,Echo 引脚出现上升沿时记录当前计数值,下降沿时再次记录,两个计数值之差除以定时器时钟频率就是高电平持续时长。整个过程不阻塞 CPU,主循环可以同时处理电机控制。
以下是关键配置代码,用的是标准库,TIM2 的四个通道里我用了通道 2、3、4 来接三路 Echo:
void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseInitStructure.TIM_Period = 0xFFFF; TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz,计数周期 1us TIM_TimeBaseInitStructure.TIM_ClockDivision = 0; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM2, &TIM_ICInitStructure); // 通道3、4配置类似,省略 TIM_SelectInputTrigger(TIM2, TIM_TS_TI2FP2); TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset); TIM_Cmd(TIM2, ENABLE); }这里有一个设计细节:我把 TIM2 配置成「上升沿复位从模式」,意思是每当通道 2 出现上升沿时,定时器计数值自动清零。这样当下降沿中断发生时,直接读取TIM_GetCapture2(TIM2)拿到的就是整个高电平的宽度,不需要额外保存上升沿计数值再相减。这个技巧对三路测距尤其有用,因为每路 Echo 都对应一个独立的捕获寄存器,互不干扰。
3.2 超声波轮流触发:三路不打架的关键
三路 HC-SR04 如果同时触发,回波会互相串扰:A 路发出的声波被 B 路接收,或者多路回波叠加导致测距值无规律跳动。解决办法是轮流触发,每一路之间间隔至少 60ms。
这里有个经验值:超声波在空气中的传播速度约 340m/s,实测距离若按 4.5m 最大量程算,单次测距往返时间约 26ms。间隔取 60ms 既保证了上一路回波完全结束,又留出了余量。我的主循环框架大致是这样:
while (1) { // 轮流触发三路超声波 trigger_ultrasonic(TRIG_FRONT); delay_ms(60); dist_front = get_distance(ECHO_FRONT); trigger_ultrasonic(TRIG_LEFT); delay_ms(60); dist_left = get_distance(ECHO_LEFT); trigger_ultrasonic(TRIG_RIGHT); delay_ms(60); dist_right = get_distance(ECHO_RIGHT); // 距离平滑滤波 dist_front = smooth_filter(dist_front, 0.3); dist_left = smooth_filter(dist_left, 0.3); dist_right = smooth_filter(dist_right, 0.3); // 状态判定与电机控制 state = follow_state_machine(dist_front, dist_left, dist_right); motor_control(state); }距离计算公式也一并给出:
float get_distance(uint16_t echo_pin) { uint32_t pulse_width = TIM_GetCapture2(TIM2); // 单位微秒 float distance_cm = pulse_width * 0.0343f / 2.0f; return distance_cm; }0.0343 是声速 343m/s 换算成 cm/us 的值。这个参数在不同温度下略有变化,如果需要高精度可以加温度补偿,但对于跟随小车来说,这个精度完全够用。
3.3 电机的差速控制:左转右转其实是"一边快一边慢"
两轮差速驱动小车的转向原理很简单:想让小车右转,就让左轮转得快、右轮转得慢甚至反转;想让小车左转,则反过来。我用 TIM3 的通道 1 和通道 2 输出两路 PWM,通过改变占空比控制速度。
PWM 频率我选的是 1kHz。这个频率是直流减速电机的典型工作区间,既能保证扭矩输出稳定,又不会产生刺耳的啸叫。1kHz 以上的 PWM 频率会降低电机感性负载的电流纹波,但驱动芯片的开关损耗也会增加,1kHz 在这个项目里是性价比最高的选择。
电机控制函数如下:
void motor_control(FollowState state) { switch (state) { case STATE_STOP: set_motor_left(0, 0); set_motor_right(0, 0); break; case STATE_FORWARD: set_motor_left(SPEED_NORMAL, 1); set_motor_right(SPEED_NORMAL, 1); break; case STATE_BACKWARD: set_motor_left(SPEED_NORMAL, 0); set_motor_right(SPEED_NORMAL, 0); break; case STATE_LEFT: set_motor_left(SPEED_TURN, 0); // 左轮反转 set_motor_right(SPEED_TURN, 1); // 右轮正转 break; case STATE_RIGHT: set_motor_left(SPEED_TURN, 1); set_motor_right(SPEED_TURN, 0); break; } }set_motor_left(speed, direction)的第一个参数是 PWM 占空比比较值,第二个参数控制电机方向。我用的 L298N 模块,IN1/IN2 的高低位组合决定正反转,ENA/ENB 接 STM32 的 PWM 输出控制速度。SPEED_NORMAL我取的 3200(在 0-9999 的 ARR 区间里约占 32%),这个值在实际测试中能让小车以约 0.4m/s 的速度跟随,既不会太慢让目标走远,也不会太快导致反应不及。SPEED_TURN我取 2800,转向稍微降一点速,减少急转时的离心感。
4. 调试过程实录:三路超声波小车最常见的三个坑
4.1 三路传感器互相干扰导致距离跳变
我最初把三路超声波同时触发,结果串口打印出来的距离值非常诡异:正前方明明站着人,显示 80cm,人走开后反而变成 40cm。排查过程是这样的:先用排除法,把左右两路的 Trig 线拔掉,只保留前路,测距恢复正常。再把左路接回去,不触发它,仍然正常。最后确认问题出在"同时触发"上。
原因很直观:三路传感器同时发射 40kHz 超声波,声波在空间里叠加,A 路的回波可能被 B 路的接收头捕捉到。由于三路声波发射时间一致,干扰波和真实回波混在一起,测出来的脉宽毫无规律。
解决方案就是我在 3.2 写的轮流触发。这里额外补充一个细节:左右两路的安装角度是 45 度,声波打到人体后反射路径更复杂,回波信号比前路弱,更容易受干扰。所以我在软件里加了数据有效性判断:距离值小于 2cm 或大于 400cm 时判定为无效数据,丢弃且不更新距离变量,沿用上一次的合法值。这样即使偶尔测到异常值,也不会导致小车突然抽搐。
4.2 电机启动瞬间 STM32 复位
这个坑是最隐蔽的。现象是:小车原地静止时一切正常,一给前进指令电机启动,STM32 就重启,串口打印的初始化信息反复出现。
用万用表量电池电压,空载 7.4V,接上电机后瞬间掉到 6V 以下,但 STM32 的供电是经过 LM2596 降到 5V 的,按理说不会受这么大影响。问题出在共地回了上:L298N 的 GND 虽然和 STM32 的 GND 接了,但电机的大电流脉冲会在这个公共地上产生压降,导致 STM32 的参考地瞬间被抬升,5V 供电实际上变成了 4.2V 甚至更低,主控复位。
解决办法分三步:第一,给 L298N 的电机电源输入端并联一个大电容,我用的是 470uF 电解电容加 0.1uF 陶瓷电容,吸收电机启动时的瞬时大电流;第二,保证 L298N 的逻辑供电(5V)和 STM32 的供电隔离,我用的是双路输出降压模块,一路给主控,一路给电机驱动逻辑电路;第三,PID 调速时避免 PWM 从 0 突然跳到目标值,软件里做了加速斜坡,每 20ms 增加一小步占空比,让电流平缓上升。
4.3 目标跟丢之后的"搜索"策略
小车跟人的过程中,人转弯时很容易突然跑出超声波覆盖范围,三路传感器全测不到有效距离。如果此时不做任何处理,小车会停车等待,体验很差。
我加了一个"丢失计时器"逻辑:当连续 10 个控制周期内三路距离都无效时,认为目标已丢失。此时小车进入搜索模式:先原地右转 90 度,等待 500ms,再左转 180 度扫描一遍。如果在搜索过程中任何一路测到有效距离,立即退出搜索模式,回到正常跟随状态。
这个策略要注意一个问题:单靠超声波搜索目标,本质上是个"盲搜",如果目标站在小车正后方,三路传感器全都测不到,小车永远找不到目标。我做了一个折中:搜索模式持续 5 秒后自动停车,等待人工干预。想彻底解决这个问题,可以考虑加入一个舵机带动超声波模块做 180 度扫描,但这已经超出三路固定传感器的范畴了,可以作为下一个改进方向。
5. 调参经验与下一步能玩出的花活
5.1 实测调参记录:哪些参数最值得反复试
跟随小车的好不好用,一半靠硬件,一半靠参数。我在测试中积累了几个关键调节点:
| 参数 | 初始值 | 最终值 | 调整原因 |
|---|---|---|---|
| FOLLOW_MIN | 25cm | 30cm | 25cm 时人稍向前倾就容易触发后退,太灵敏 |
| FOLLOW_MAX | 60cm | 50cm | 跟随时距超过 50cm 后小车反应有延迟,缩小到 50cm 更跟手 |
| OFFSET_DEAD | 10cm | 15cm | 10cm 时左右轻微晃动就触发转向,左右来回摆动明显 |
| SPEED_NORMAL | 4000 | 3200 | 4000 时起步太冲,人走慢速时小车反复前进后退 |
| 传感器离地 | 6cm | 9cm | 6cm 时地面反光严重,测距跳动频繁 |
OFFSET_DEAD 的调整尤其值得多说两句。这个参数本质上是个滞回比较器,控制左右转向的灵敏度。调小了,小车对目标的横向偏移反应太灵敏,人稍微侧身它就转向,行驶轨迹是"之"字形的;调大了,目标已经偏到很侧边了也不转向,容易跟丢。我的经验是:先定一个值跑直道,观察小车轨迹是不是直线,如果不是,就逐步加大 OFFSET_DEAD,直到直道轨迹平稳。
5.2 功能扩展方向:让跟随小车从"能跑"到"好用"
三路超声波自动跟随小车做到这一步,已经具备了完整的感知、决策、执行闭环。这个平台后续可以玩的方向很多,我列几个我认为性价比最高的:
第一,加入闭环速度控制。当前速度是开环的,电池电量不同、地面摩擦不同都会影响实际速度。用编码器电机加 PID 闭环,可以让小车在坡道和地毯上也保持恒定速度。第二,给小车加一个蜂鸣器和 LED 灯条,用不同声音和光效提示当前状态——跟随中、目标丢失、电量低——实用性一下就上来了。第三,把主控从 STM32F103C8T6 升级到 STM32F407 或加一块 ESP8266 模块,把状态数据无线发到手机 App 上,远程监控小车运行日志,调试效率能高一个档次。
还有一个我自己很感兴趣的进阶方向:用卡尔曼滤波把三路超声波数据和 MPU6050 的角速度数据融合,解决超声波数据毛刺多、刷新率低的问题。这样小车在快速转向时也能保持平滑的姿态估计,跟随体验会接近工业级的 AGV 小车。
这个项目的完整工程文件包括 HARDWARE 驱动层代码、CORE 核心文件、SYSTEM 系统文件、USER 用户代码,以及原理图和 PCB 设计文件,全部整合在一个工程包内。如果你正在准备毕业设计或者想系统练一遍 STM32 开发,直接拿这套工程去改,比从零写要省太多时间。我的建议是:先把超声波测距和电机控制的底层驱动跑通,再一步步套上状态机逻辑,最后再来调参,整个过程会非常顺畅。
本文还有配套的精品资源,点击获取