资讯动态

STM32三路超声波自动跟随小车:硬件设计与状态机实现

发布时间:2026/10/14 17:39:49 来源:尧图企业网站定制
简介一套基于STM32的三路超声波自动跟随小车完整工程面向嵌入式入门者、电子竞赛选手及智能车爱好者可用于课程设计或动手实践。项目通过三个超声波传感器采集前方不同方位距离数据由STM32解析并控制电机实现目标跟随涉及GPIO配置、定时器中断、PWM调速和简单控制策略具备典型学习价值。压缩包共309个文件大小约8.45MB主要包含C/H源码、Keil与IAR工程文件.uvproj、.ewp、编译生成的axf/hex文件以及调试辅助脚本目录结构完整可直接在集成环境中打开编译或烧录验证。已有2403人学习使用说明该方案受到一定认可。资料不仅提供可运行代码还保留引脚分配与硬件配置文件方便结合原理图理解接线主函数留有优化空间适合在此基础上尝试PID或模糊逻辑改进是综合练习STM32、传感器应用与嵌入式控制的好素材。1. STM32三路超声波自动跟随小车先弄懂这套资源解决什么问题三路超声波自动跟随小车不是把三个HC-SR04往车头一贴就能跑。只用一个超声波模块目标从侧面一过就丢用三路之后才有“左、中、右”的距离信息小车才能判断人站在哪个方向、该直行、转弯还是停下。这套资源的重点不在那几行C代码而在三路超声波的轮询采集、跟随状态机、PWM差速输出以及跑起来才会遇到的硬核坑。适合刚做完循迹车、想让它追着人跑的开发者也适合课程设计阶段需要交一个完整演示项目的人。2. 硬件选型与测距方案三路HC-SR04怎么排布为什么单路必翻车2.1 从底盘到驱动板先定转向结构自动跟随小车首先需要一个能差速转向的底盘双电机加万向轮最常用。两个后轮独立驱动靠左右轮速度差转弯比舵机前轮转向结构简单也更容易用PWM控制。STM32最小系统板选择上资源包一般以F103C8T6为主Flash和RAM都够用关键是定时器和外部中断资源足够三路超声波和两路电机PWM分配。驱动板优先选TB6612FNG它的逻辑电流小、饱和压降低能用3.3V逻辑电平直接驱动非常适合和STM32配套。L298N不是不能用但它的导通压降大电机的电流会把逻辑电压拖得不稳定超声波模块跟着遭殃。选底盘时还要看电机参数常见小型减速电机额定电压6V空载电流100mA左右堵转电流可能到1.5A。TB6612每路最大输出1.2A刚好卡着用。如果你换了更大扭矩的电机就得上BTN7971或者MOS H桥否则驱动板过热限流小车表现为“跑两步就动不了停一会又活了”。接线之前要先确认三路Echo引脚都支持外部中断。我习惯把左、中、右三路Trig放在PA0、PA2、PA4Echo放在PA1、PA3、PA5这六个引脚分布在EXTI0到EXTI5上每条中断线独立不会互相抢占。PA8和PA9输出TIM1的通道一、通道二分别驱动左、右电机PWM。方向控制引脚用PB3、PB4这样的普通GPIO即可不用复用功能。2.2 三路超声波布局角度比数量更重要HC-SR04测距原理不复杂。给Trig引脚一个10us以上的高电平模块内部发出40kHz超声波脉冲接收探头等待回波回波到达后Echo引脚输出一个高电平高电平持续时间就是声波往返的总时间。距离计算公式是高电平时间us乘以声速340m/s除以2得到单程距离再换算成cm。我一般直接写distance echo_width * 0.017误差在可接受范围内。三个模块怎么摆直接决定跟随效果。前向模块装在车头正中负责判断“跟不跟得上”左、右模块各偏向斜前方25到30度负责“目标往哪跑”。为什么不是正左正右正侧方安装只能测到车身两侧的障碍物目标走到侧前方时探头已经在盲区里左右两路同时报出远距离控制逻辑就会丢掉目标。偏斜安装之后三路测距值才能形成“目标越靠右右路距离越近、左路距离越远”的对比关系转向就有依据了。模块之间的间距也要留意我习惯让相邻探头中心距不小于8cm最好10cm以上。HC-SR04的波束角大约60度装得太近左侧模块发出的声波会直接打到右侧模块的接收探头上造成基线串扰。上螺钉时最好加橡胶垫片减小电机震动传到模块外壳引起的误触发。2.3 电平匹配与供电两个最常见的预埋坑STM32F103的GPIO不是所有引脚都5V容忍HC-SR04在5V供电时Echo输出0V到5V的方波。很多人直接把这根线接到PA1、PA3、PA5上短时间能跑但长期运行或电机干扰大的时候IO口内部保护二极管容易出问题测距值会突然跳成一个大数。我的做法是Echo引脚先串联一个1kΩ电阻然后再用一个1kΩ电阻对地分压把5V降到2.5V左右保证逻辑高电平超过STM32的阈值但又不会超过3.3V。Trig引脚直接由STM32输出3.3VHC-SR04的Trig高电平阈值大概在1.8V能可靠触发不需要额外转换。供电是另一个大坑。单个HC-SR04工作时瞬时电流能到20到30mA三路同时跑接近100mA如果和电机共用一路5V电机一转电压就会剧烈波动。正确做法是电池电压先到电机驱动板驱动板再输出逻辑电源给STM32和超声模块这路5V和电机主电源分开。如果条件有限只能共用就在超声模块电源脚旁边加100uF电解电容和0.1uF瓷片电容放在模块插座附近地线也分开走最后在电池负极单点汇合。这个单点接地原则能解决后面一半的测距乱跳问题。3. 从CubeMX到跟随状态机这套代码到底怎么落地3.1 CubeMX引脚规划与定时器分配拿到资源包后我会先看它的CubeMX工程。常见引脚分配是PA0到PA5分别接三路Trig和EchoPA8/PA9作为TIM1的PWM输出。CubeMX需要配置四类东西GPIO、外部中断、定时器PWM、一个基本定时器计时。Trig引脚配成推挽输出、低速、初始低电平Echo引脚配成下拉输入并打开EXTI。TIM1输出两路PWM给电机频率10kHz避免电机啸叫TIM2做自由运行计数器不分频计数频率1MHz用来记录Echo高电平持续时间。下面这段Echo中断回调是HAL库下的参考实现核心逻辑是上升沿记时间、下降沿算宽度// 外部中断回调三路Echo的上升沿与下降沿 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { // 根据引脚号映射到采样索引0左 1中 2右 uint8_t idx echo_index_from_pin(GPIO_Pin); if (idx 3) return; GPIO_PinState level HAL_GPIO_ReadPin(ECHO_PORT[idx], GPIO_Pin); if (level GPIO_PIN_SET) { // 上升沿记录当前时间戳 echo_start[idx] __HAL_TIM_GET_COUNTER(htim2); } else { // 下降沿计算高电平宽度考虑定时器回绕 uint32_t now __HAL_TIM_GET_COUNTER(htim2); uint32_t width; if (now echo_start[idx]) { width now - echo_start[idx]; } else { // 计数回绕过65535需要补偿 width (65535 - echo_start[idx]) now 1; } echo_width[idx] width; echo_done[idx] 1; } }逻辑说明这段代码在Echo引脚跳变时由EXTI回调触发。上升沿保存计数器值下降沿计算差值差值就是高电平持续us数。TIM2是16位计数器计到65535会回绕到0如果下降沿时刻比上升沿时刻小说明中间发生过一次溢出必须手动补偿。漏掉这个回绕处理测距值会时不时跳出一个接近6米的假大数。参数说明TIM2的计数频率配置为1MHz计数器每1us加1。假设回声宽度是2000对应距离就是2000乘0.017等于34cm。echo_done标志位由主循环在触发前清零否则下一次测距会拿到上一次的旧数据。我见过有人忘了清标志结果三路距离永远是第一次的值小车完全变成瞎子。3.2 三路轮询采样触发顺序与静默时间三路超声波不能同时触发这是多路测距项目里最容易踩的坑。我的做法是把采样写成一个串行函数每次只测一路三路循环一次大约30ms。对跟随小车来说30ms刷新率足够人的移动速度不会快到需要更高帧率。串行轮询还有额外好处干扰源从三路叠加变成单路排查难度直线下降。// 触发第i路超声波并等待回波返回距离cm0xFFFF表示无效 uint16_t read_ultrasonic(uint8_t i) { // i: 0左 1中 2右 uint16_t distance_cm 0xFFFF; // 触发脉宽15us比手册要求的10us多留余量 HAL_GPIO_WritePin(TRIG_PORT[i], TRIG_PIN[i], GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_PORT[i], TRIG_PIN[i], GPIO_PIN_RESET); // 等待回波或超时超时60ms uint32_t start HAL_GetTick(); echo_done[i] 0; while (echo_done[i] 0) { if (HAL_GetTick() - start 60) break; } if (echo_done[i]) { // 回声宽度us乘0.017得到cm distance_cm (uint16_t)(echo_width[i] * 0.017f); if (distance_cm 200) distance_cm 0xFFFF; // 超量程视为无效 } // 静默10ms给声波余振衰减也避免干扰下一路 delay_ms(10); return distance_cm; }逻辑说明函数先拉高Trig保持15us然后等待echo_done标志置位。超时用HAL_GetTick()实现60ms对应回声往返约10m远超跟随场景的实际量程。得到宽度后换算距离超过200cm直接标记无效。函数末尾的delay_ms(10)不是随手加的目的是让当前模块的余振完全衰减否则下一个模块触发时容易被残留声波误判为回波。参数说明delay_us(15)来自HC-SR04手册的“Trig脉宽大于10us”留5us余量静默时间从2ms到15ms都可以太短会串扰太长会降低刷新率。如果三个模块都加了屏蔽罩2ms也能稳定裸奔状态建议至少10ms。读取频率一周大约30Hz做跟随完全够用。3.3 跟随状态机与PWM输出跟随逻辑我建议分成两层上层是状态机下层是电机控制。状态机只做判断不在同一段代码里混合“什么时候转弯”和“转多快”。先看前方距离决定前进、停止还是后退再看左右距离差决定往哪边转。这么拆的好处是后续调速度时不用动判断逻辑。// 主循环中每20ms调用一次 void follow_update(void) { // 获取三路距离0xFFFF视为无效 float front (distance[1] 0xFFFF) ? 0 : (float)distance[1]; float left (distance[0] 0xFFFF) ? 0 : (float)distance[0]; float right (distance[2] 0xFFFF) ? 0 : (float)distance[2]; // 前方量程判断 if (front 30.0f) { follow_state STATE_STOP; // 太近停止防撞 } else if (front 60.0f) { follow_state STATE_FORWARD; // 太远前进追目标 } else { // 中距离根据左右差判断转向 float diff left - right; if (diff 10.0f) { follow_state STATE_RIGHT; // 右边有目标右转 } else if (diff -10.0f) { follow_state STATE_LEFT; } else { follow_state STATE_STOP; // 位置居中保持 } } // 根据状态设置电机速度 switch (follow_state) { case STATE_FORWARD: set_motor(700, 700); break; case STATE_STOP: set_motor(0, 0); break; case STATE_RIGHT: set_motor(600, 300); break; case STATE_LEFT: set_motor(300, 600); break; default: set_motor(0, 0); break; } }逻辑说明前方距离在30cm到60cm之间时小车不贸然前进后退而是先判断左右哪边更近。diff等于left减right右边更近时right小diff就大说明目标在右侧小车右转。这个方向符号是初学者最容易搞错的地方。我第一次调试时就把左右写反小车越追越偏。最后的default分支很关键防止未知状态导致电机乱动。参数说明30cm是防撞阈值60cm是跟随上限。这两个参数必须按实际场景调客厅小就改成25cm和50cm走廊长可以放宽。电机速度700和600只是初始值电池电压不同时相同占空比的实际速度差异很大要现测现调。电机控制函数里还要做软启动和限幅直接给满占空比会让起步电流瞬间拉高单片机容易复位。// 设置左右电机速度范围-1000~1000 void set_motor(int left, int right) { // 限幅防止溢出 if (left 1000) left 1000; if (left -1000) left -1000; if (right 1000) right 1000; if (right -1000) right -1000; // 方向引脚正数前进负数后退 HAL_GPIO_WritePin(DIR_LEFT_PORT, DIR_LEFT_PIN, left 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(DIR_RIGHT_PORT, DIR_RIGHT_PIN, right 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); // PWM占空比取绝对值 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, abs(left)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, abs(right)); }逻辑说明速度用-1000到1000表示正数前进负数后退。方向引脚和占空比分离的好处是转向时不用做正负切换逻辑。PWM比较值直接设为绝对值方向交给GPIO。注意不要在set_motor里做PID运算这个函数只负责最终输出业务逻辑放在上层否则后面加限幅、加滤波时到处都要改。4. 三路超声波自动跟随常见问题排查五个翻车点与处理思路资源包到手后别急着烧录。我见过太多人把程序烧进去小车原地转圈第一反应是改代码实际九成是硬件接线、供电或传感器本身的问题。下面按排查顺序列出这个项目里出现频率最高的五个问题。4.1 现象前方站着人小车却直直撞过去被测到前方有障碍但小车不反应或者反应迟钝。先在串口助手里看三路distance值如果前方有人时三路数值都不变通常是测距函数压根没拿到有效回波。检查Trig引脚是不是真的在输出高电平Echo引脚是不是接在中断线上。有些人把Echo接到了ADC引脚代码里却配置成EXTI中断根本不会触发。排查顺序是先打印、后量电平、最后换模块。打印三路距离全是0就说明没测到回波用万用表量Echo引脚在测距瞬间有没有跳变波形正常但读了0大概率是中断回调里echo_done标志没被正确清除。还有一种很隐蔽的情况Echo和Trig接反了Trig收到3.3V高电平不会触发模块自然没有回波。对调两根线就能解决。4.2 现象小车原地抖动不停目标站定不动小车前后左右小幅来回晃。原因有二一是阈值区间太窄传感器测同一位置会波动2到3cm距离在阈值边缘时一帧判停、下一帧判加速必然抖动二是没有做连续判定单帧结果直接改变状态。解决方法是拉大近远阈值并加入持续帧数计数。例如前方距离小于25cm时才进入停止并且要连续三帧都小于25cm才真正停止大于60cm时连续三帧才前进。对测距值做滑窗平均也能压噪我常用的是取最近五次去掉最大最小再平均数据会平滑很多。代码里就是维护一个长度为5的环形缓冲每次采样后做排序取中位计算量很小F103跑起来没压力。4.3 现象左中右三路测距互相干扰单独测每一路都正常三路一起跑时某一路突然变短或者出现离谱的大数。这是典型的声波串扰左路触发后声波碰到障碍物反射回来右路的接收探头也把这个回波当成自己的或者左路余振还没衰减完中路的声波就来了。还有可能是三路Echo中断同时访问TIM2计数器没有加临界区保护导致宽度算出负值或溢出补偿算错。解决思路是拉开分时静默时间。把上一章read_ultrasonic里的delay_ms从2ms加到10ms或15ms串扰概率会明显下降。更彻底的做法是给每个模块的接收探头加屏蔽罩用热缩管或硬纸板包住探头侧面只留前方开口。如果还串扰就改成“每路连续测三次取中值再切下一路”每次间隔30ms。牺牲一点刷新率换来稳定数据对跟随场景完全值得。4.4 现象电机一转超声波数值乱跳小车静止时测距正常电机一启动尤其是大占空比时测距值像跳楼一样乱变。这是功率地线问题。电机的大电流在地线上产生压降STM32和超声模块的地电位被抬高Echo高电平幅度就不稳定PWM信号还会耦合进Echo信号线干扰中断触发。解决方法是把电机驱动地、单片机地、超声模块地分开走线在电池负极单点汇合。Echo信号线用双绞线或屏蔽线避开PWM输出线。超声模块供电加一个LC滤波一个10uH电感和100uF电容就能让波形平稳很多。电机驱动板上的输出端也加0.1uF瓷片电容吸收换向火花我遇到过电机一反转超声波直接测出65535us宽度的假回波加上这个电容后就消失了。4.5 现象长时间运行后测距值缓慢漂移开机二十分钟后原本50cm的地方测出60cm左中右三路偏差还不一致。HC-SR04这类模块对温度敏感电机发热会让声速变化公式里的340m/s就不再准确另一个原因是STM32最小系统板用了内部HSI时钟误差能到2%左右定时器计数频率不准宽度测量自然偏。解决方法是做温度补偿声速近似等于331.4加0.6乘温度在代码里读一个温度值或者直接按环境温度写死一个修正系数。更接地气的办法是每隔一段时间重新校准把车停在与墙已知距离处测出偏差后修正换算系数。如果漂移只集中在某一路大概率是那个模块本身一致性差对调模块再测一次就能定位。5. 调试与进阶串口波形验证加一点PID味道调试三路超声波我最推荐的工具不是屏幕而是串口绘图。程序里用一行printf把三路距离打出来逗号分隔然后打开串口助手的波形图可以直接看到距离曲线。这一步花五分钟能省下后面两个小时。// 调试用放在主循环里定时打印频率5Hz足够 printf(%d,%d,%d\r\n, distance[0], distance[1], distance[2]);看曲线时抓住两点一是目标匀速移动时曲线是否平滑二是有没有隔帧跳变。如果每两帧跳一次大概率是声波串扰或定时器回绕补偿没做对如果三路同时跳先查电源如果只有一路跳去查那一路的接线和模块。进阶一点的改进是把转向量的计算从状态判断改成比例控制。状态机只负责大方向速度融合则用左右差乘系数类似一个P控制器// 在前方距离有效时用左右差修正两轮速度 float diff distance[0] - distance[2]; int left_speed base_speed (int)(diff * 20); int right_speed base_speed - (int)(diff * 20);参数说明diff乘20是转向灵敏度。车体宽度、电机减速比不同这个系数要重调。我习惯从10开始每两档试一次最后定在20。晚上我拿到资源包时第一件事不是看代码而是先看原作者的CubeMX配置因为这个项目的坑几乎全在引脚规划和供电上。从那以后我每次调试小车都会强制走一遍“先打印原始数据、再看波形、最后动控制参数”的流程这习惯帮我避开了不少玄学问题。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑