news 2026/10/23 7:54:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32三路超声波自动跟随小车:硬件设计与状态机实现

简介:一套基于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、PA4,Echo放在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.3V,HC-SR04的Trig高电平阈值大概在1.8V,能可靠触发,不需要额外转换。

供电是另一个大坑。单个HC-SR04工作时瞬时电流能到20到30mA,三路同时跑接近100mA,如果和电机共用一路5V,电机一转电压就会剧烈波动。正确做法是电池电压先到电机驱动板,驱动板再输出逻辑电源给STM32和超声模块,这路5V和电机主电源分开。如果条件有限只能共用,就在超声模块电源脚旁边加100uF电解电容和0.1uF瓷片电容,放在模块插座附近;地线也分开走,最后在电池负极单点汇合。这个单点接地原则,能解决后面一半的测距乱跳问题。

3. 从CubeMX到跟随状态机:这套代码到底怎么落地

3.1 CubeMX引脚规划与定时器分配

拿到资源包后,我会先看它的CubeMX工程。常见引脚分配是PA0到PA5分别接三路Trig和Echo,PA8/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路超声波并等待回波,返回距离cm,0xFFFF表示无效 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配置,因为这个项目的坑几乎全在引脚规划和供电上。从那以后,我每次调试小车都会强制走一遍“先打印原始数据、再看波形、最后动控制参数”的流程,这习惯帮我避开了不少玄学问题。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 4:21:52

磷化工企业供应链降本实战:从磷矿到磷酸铁锂的运营效率密码

最近我把某磷化集团2025年的三季报翻了个底朝天&#xff0c;又对着它的供应链流程做了好几轮复盘。这家公司手里攥着8亿吨磷矿资源储量&#xff08;此处为虚构案例表述&#xff09;&#xff0c;全年净利润预测到了47亿&#xff0c;Q3单季增速直接冲上20%。坦白讲&#xff0c;资…

作者头像 李华
网站建设 2026/10/23 7:54:45

基于MediaPipe与LSTM的手语实时翻译系统SignBridge实战

1. 从"手语翻译"这个需求说起&#xff1a;为什么值得做第一次认真思考手语翻译这件事&#xff0c;是在一个很普通的场景里。一位听障朋友去窗口办事&#xff0c;双方都很客气&#xff0c;但沟通全靠手机打字来回递&#xff0c;一个简单的问题折腾了快二十分钟。当时我…

作者头像 李华
网站建设 2026/10/22 18:44:24

STM32F407移植FX3U PLC固件:烧录、调试与避坑指南

简介&#xff1a;这份资源面向工业自动化与嵌入式开发者&#xff0c;提供将三菱FX3U系列PLC控制逻辑移植到STM32F4平台的完整工程源码&#xff0c;基于标准库开发&#xff0c;涵盖FX3U指令集模拟、I/O信号转换与通信协议实现&#xff0c;适合希望以更低成本实现类FX3U控制方案的…

作者头像 李华
网站建设 2026/10/22 12:35:30

MATLAB手写Softmax实现:数值稳定性的工程实践

1. 项目概述&#xff1a;为什么在MATLAB里亲手写一个Softmax比调用现成函数更有价值 Softmax求解多分类问题的MATLAB实现——这标题乍看像教科书习题&#xff0c;但实际是机器学习工程落地中一道关键分水岭。我带过不少刚从课程设计转到工业项目的开发者&#xff0c;他们能熟练…

作者头像 李华
网站建设 2026/10/22 18:19:38

离线数仓搭建:MySQL远程连接配置与Navicat排障实战

最近在跟一套离线数仓搭建的实战课&#xff0c;前几节刚把 Hadoop 基础服务和 Hive 过了一遍&#xff0c;第 9 节就进入一个看起来很简单、实际却能卡住不少人的环节&#xff1a;安装 MySQL&#xff0c;并用 Navicat 这台远程 GUI 工具连上去管理它。可能有人会觉得&#xff0c…

作者头像 李华