STM32三路超声波自动跟随小车设计与实现
2026/9/8 22:29:45 网站建设 项目流程

简介:面向嵌入式学习者的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 降压模块17.4V 降到 5V 给主控和传感器供电
车架亚克力三层板小车底盘1平台支撑

这里 CH1 到 STM32 的接线我直接给出来,免得新手对着数据手册翻半天:

传感器信号STM32引脚备注
前路 TrigPA0通用GPIO输出
前路 EchoPA1定时器TIM2_CH2输入捕获
左路 TrigPA2通用GPIO输出
左路 EchoPA3定时器TIM2_CH3输入捕获
右路 TrigPA4通用GPIO输出
右路 EchoPA5定时器TIM2_CH4输入捕获
左电机 PWMPA6TIM3_CH1
右电机 PWMPA7TIM3_CH2
左电机方向PB0/PB1L298N IN1/IN2
右电机方向PB10/PB11L298N 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_DIST20cm小于此距离,认为目标太近或已撞到,停车
FOLLOW_MIN30cm跟随区间下限,小于此距离开始后退
FOLLOW_MAX50cm跟随区间上限,大于此距离开始前进
OFFSET_DEAD15cm左右距离差小于此值,不修正方向,避免频繁摆动

跟随的核心逻辑分两层判断。第一层是纵向距离判断:取前路距离值 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_MIN25cm30cm25cm 时人稍向前倾就容易触发后退,太灵敏
FOLLOW_MAX60cm50cm跟随时距超过 50cm 后小车反应有延迟,缩小到 50cm 更跟手
OFFSET_DEAD10cm15cm10cm 时左右轻微晃动就触发转向,左右来回摆动明显
SPEED_NORMAL400032004000 时起步太冲,人走慢速时小车反复前进后退
传感器离地6cm9cm6cm 时地面反光严重,测距跳动频繁

OFFSET_DEAD 的调整尤其值得多说两句。这个参数本质上是个滞回比较器,控制左右转向的灵敏度。调小了,小车对目标的横向偏移反应太灵敏,人稍微侧身它就转向,行驶轨迹是"之"字形的;调大了,目标已经偏到很侧边了也不转向,容易跟丢。我的经验是:先定一个值跑直道,观察小车轨迹是不是直线,如果不是,就逐步加大 OFFSET_DEAD,直到直道轨迹平稳。

5.2 功能扩展方向:让跟随小车从"能跑"到"好用"

三路超声波自动跟随小车做到这一步,已经具备了完整的感知、决策、执行闭环。这个平台后续可以玩的方向很多,我列几个我认为性价比最高的:

第一,加入闭环速度控制。当前速度是开环的,电池电量不同、地面摩擦不同都会影响实际速度。用编码器电机加 PID 闭环,可以让小车在坡道和地毯上也保持恒定速度。第二,给小车加一个蜂鸣器和 LED 灯条,用不同声音和光效提示当前状态——跟随中、目标丢失、电量低——实用性一下就上来了。第三,把主控从 STM32F103C8T6 升级到 STM32F407 或加一块 ESP8266 模块,把状态数据无线发到手机 App 上,远程监控小车运行日志,调试效率能高一个档次。

还有一个我自己很感兴趣的进阶方向:用卡尔曼滤波把三路超声波数据和 MPU6050 的角速度数据融合,解决超声波数据毛刺多、刷新率低的问题。这样小车在快速转向时也能保持平滑的姿态估计,跟随体验会接近工业级的 AGV 小车。

这个项目的完整工程文件包括 HARDWARE 驱动层代码、CORE 核心文件、SYSTEM 系统文件、USER 用户代码,以及原理图和 PCB 设计文件,全部整合在一个工程包内。如果你正在准备毕业设计或者想系统练一遍 STM32 开发,直接拿这套工程去改,比从零写要省太多时间。我的建议是:先把超声波测距和电机控制的底层驱动跑通,再一步步套上状态机逻辑,最后再来调参,整个过程会非常顺畅。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询