ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32实战:HC-SR04超声波测距从原理到排错全攻略

STM32实战:HC-SR04超声波测距从原理到排错全攻略 简介这是一份面向STM32初学者的超声波测距完整工程资源基于HC-SR04传感器实现厘米级距离测量覆盖GPIO控制、定时器计时、中断处理、串口输出等核心知识点适用于智能小车避障、倒车雷达、安全监控等典型场景。压缩包共304个文件大小约7.03MB包含C源码.c/.h、Keil与SI工程配置文件.uvprojx/.siproj等、编译链接产物.axf/.o/.map等、PDF版HC-SR04用户手册、串口调试助手exe以及说明文档目录结构清晰可直接打开工程对照学习或二次开发。目前已有5067人学习下载。资源中既有传感器原理与接线说明也有实际可运行的测距代码和调试工具能帮助读者完整理解超声波测距流程并掌握STM32定时器输入捕获、中断服务程序及串口数据发送等实用技能通过研读源码和官方手册还可延伸学习温度补偿、误差修正及多任务调度等进阶内容为后续嵌入式项目打下扎实基础。1. HC-SR04看似简单坑都藏在原理细节里我先说结论HC-SR04这个模块十个玩STM32的人里至少八个碰过。它价格便宜、接线就四根、网上例程一抓一大把看起来属于“十分钟搞定”的活。但我在实际项目里见过太多人卡在莫名其妙的地方——有的测距恒为0有的数值乱跳有的超过1米就开始胡报。这篇文章就是把HC-SR04的原理、接线、代码、排错一次说透适合刚入手STM32的新手也适合那些“例程在开发板上能跑、换到自己的板子上就出问题”的朋友。HC-SR04本质是一个超声波收发一体的模块。你给它一个至少10us的高电平脉冲模块内部就会发出一串40kHz的超声波脉冲同时把Echo引脚拉高。当模块检测到回波时Echo引脚才被拉低。也就是说Echo引脚高电平持续的时间就是超声波从发射到接收的往返时间。这里面藏着一个新手最容易忽略的点STM32不直接测量距离它只负责测量“一段高电平持续了多久”。真正的距离换算是你的程序在拿到时间之后算出来的。这个“高电平计时”就是整个项目的核心后面所有代码都围绕这一件事展开。1.1 从Echo高电平到距离的换算逻辑声波在空气中的传播速度约为340m/s也就是每微秒走0.034厘米。超声波从模块出发打到障碍物再弹回来走的是“双程路线”所以真实距离是声波路程的一半。换算公式可以写成距离(cm) 时间(us) × 0.034 / 2简化一下就是距离(cm) 时间(us) / 58。这也是HC-SR04例程里那个经典的“除以58”的来历。很多新手看到这个58会一脸懵其实它就是1000000 / 340 / 2约等于58.82再取整得到的经验系数。明白这一层之后你就不会被网上五花八门的代码搞混了——有些写/58有些写*0.017本质都是同一个东西。从Echo高电平时间算起HC-SR04标称测量范围是2cm到400cm。对应的高电平时间范围大概是115us到23.5ms。这个范围决定了你的定时器量程和分频参数怎么定。1.2 声速不是恒定的温度会骗人模块本身不校准声速它只负责发波和收波声速是你程序里写死的。但在实际环境里声速会随温度明显变化。温度每升高1℃声速大约增加0.6m/s。v 331.4 0.6 × T(℃) 单位m/s我在北方冬天室外做过一次测试零下10℃时用25℃环境下校准过的程序去测一个1米的固定距离读出来直接偏了将近3厘米。这个偏差在普通玩一下的项目里可以忽略但如果你的测距模块要用在智能小车避障、水位监测这种对精度有要求的场景最好在代码里加一个温度补偿入口。做法也不复杂接个DS18B20或者直接用板上温湿度传感器测出温度后实时计算声速再算距离float speed_of_sound 331.4f 0.6f * temperature; float distance echo_time_us * speed_of_sound / 20000.0f; // us * cm/us / 2如果你只是室内玩/58够用。但心里要清楚这个系数是个“标准室温近似值”不是真理。2. 接线与定时器选型决定项目稳不稳的前置条件HC-SR04一共四个引脚VCC、GND、Trig、Echo。接线看起来简单但供电和引脚选择这两件事直接影响后面调试的体验。2.1 供电5V还是3.3V这个问题不能省模块标称工作电压是5V。很多新手图省事直接把VCC接到STM32板的3.3V上结果要么测距距离特别短要么数据忽大忽小。原因很简单HC-SR04的发射功率和接收灵敏度都跟供电电压强相关电压不足时回波信号变弱远距离就测不到。正确的接法是把VCC接到5V电源上注意是“能提供足够电流的5V”不是开发板那个已经被稳压器压得只剩4.3V的5V引脚。如果直接用USB口供电我的建议是给VCC和GND之间并联一个100uF的电解电容再并一个104陶瓷电容就近放在模块插座旁边。这个动作能明显减少测量时LED矩阵或其他外设给电源带来的毛刺干扰。Echo引脚输出的是5V高电平。STM32的IO口大部分是FT兼容引脚能容忍5V输入但为了稳妥我还是建议在Echo和MCU引脚之间串一个1k电阻或者用两个电阻分压到3.3V。别嫌麻烦这一颗小电阻能在关键时刻救你一块板子。2.2 引脚和定时器怎么选为什么我推荐TIM2Trig引脚随便找个普通GPIO推挽输出就够但Echo引脚最好接到定时器的输入捕获通道上。这里有一个很多教程没提的关键点STM32F103的TIM2和TIM5是32位计数器而TIM1、TIM3、TIM4是16位计数器。如果用的是16位定时器且计数频率设为1MHz那计数器每65.535ms就溢出一次。Echo高电平时间最大约23.5ms理论上不溢出但如果你在调试时把Trig引脚接错、或者模块在没有任何遮挡时一直收不到回波Echo会一直保持高电平计数器就会溢出。溢出之后你再读CCR值捕获到的就是一组毫无意义的数。用32位的TIM2这个烦恼直接消失。在1MHz计数频率下溢出周期是4294秒你怎么测都测不满。所以我这篇例程里Echo选择接PA0也就是TIM2的CH1输入捕获通道Trig接PA1。接线汇总模块引脚接STM32引脚说明VCC5V并联100uF104电容GNDGND共地必须保证TrigPA1推挽输出默认拉低EchoPA0TIM2_CH1输入捕获串1k电阻进PA02.3 工程初始化标准库写法的关键配置如果你用标准库新建工程时记得开启GPIOA和TIM2的时钟。这里给出完整的初始化流程照着配就行void HC_SR04_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // Trig: PA1 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_ResetBits(GPIOA, GPIO_Pin_1); // Echo: PA0 浮空输入TIM2_CH1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // TIM2时基72MHz经72分频即1MHz计数频率1us计数一次 TIM_TimeBaseStructure.TIM_Period 0xFFFFFFFF; // 32位计数器 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure); // CH1输入捕获先捕获上升沿 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; 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); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }这个配置里有三个细节值得说明一是TIM_Period设为0xFFFFFFFF配合32位计数器从根上消除溢出隐患二是分频72得到1MHz让时间单位直接化简为微秒方便后续计算三是TIM_ICFilter设为0x0F启用输入滤波能滤掉Echo线上的窄脉冲毛刺这一步对抑制误触发有奇效。3. 两种测距代码实现阻塞延时与输入捕获中断代码实现上网上流传着两种主流思路一种是用while死等Echo电平变化我称它为阻塞法另一种是用定时器输入捕获中断不占用CPU我称它为中断法。两种方案我都在项目里实际用过各有适用场景。3.1 阻塞法逻辑直白适合快速验证阻塞法的核心是触发Trig后用两个while循环分别等待Echo的上升沿和下降沿同时用TIM_GetCounter读取计数器值两个值的差就是高电平持续时间。uint32_t HC_SR04_GetEchoTime(void) { uint32_t t1, t2; // 发送10us以上高电平触发脉冲 GPIO_SetBits(GPIOA, GPIO_Pin_1); delay_us(15); GPIO_ResetBits(GPIOA, GPIO_Pin_1); // 等待Echo上升沿加超时保护 uint32_t timeout 100000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0) { if (--timeout 0) return 0; } t1 TIM_GetCounter(TIM2); // 等待Echo下降沿同样加超时 timeout 100000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 1) { if (--timeout 0) return 0; } t2 TIM_GetCounter(TIM2); return t2 - t1; }这个函数里我特意加了超时保护。很多网上代码直接裸写while等一旦模块没接好或Trig没发出去程序就会卡死在循环里表现出来就是整个系统无响应。加超时之后即使模块出问题函数也会立即返回0主程序还能继续走。阻塞法的优点是逻辑简单不需要中断不容易把优先级配置搞乱适合在刚点亮模块做验证时用。缺点是Echo高电平期间CPU一直在死等最坏情况要等20多毫秒。对测距这种低速应用来说完全够用但如果你同时要驱动电机或刷OLED屏卡顿感会比较明显。3.2 中断法上升沿开始、下降沿结束中断法的思路是配置输入捕获通道始终捕获上升沿第一次捕获到上升沿时记下时刻并把捕获极性改为下降沿第二次捕获到下降沿时再记一个时刻两次时刻相减就是高电平时间。因为中断由硬件触发CPU在等待期间可以去干别的事。volatile uint8_t capture_first_edge 1; volatile uint32_t capture_t_start 0; volatile uint32_t capture_t_end 0; volatile uint8_t capture_done 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); if (capture_first_edge) { capture_t_start TIM_GetCapture1(TIM2); TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); capture_first_edge 0; } else { capture_t_end TIM_GetCapture1(TIM2); TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); capture_first_edge 1; capture_done 1; } } }主循环里这样调用void HC_SR04_Trigger(void) { GPIO_SetBits(GPIOA, GPIO_Pin_1); delay_us(15); GPIO_ResetBits(GPIOA, GPIO_Pin_1); } float HC_SR04_GetDistance(void) { HC_SR04_Trigger(); uint32_t timeout 5000; while (!capture_done) { if (--timeout 0) { // 超时重置极性避免下一次捕获状态错乱 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); capture_first_edge 1; return -1; } } capture_done 0; uint32_t echo_time_us capture_t_end - capture_t_start; return echo_time_us / 58.0f; }有一个细节在这里必须提醒超时发生后除了返回错误还把捕获极性强制重置为上升沿。如果不做这一步下一次触发时捕获状态会停留在“等待下降沿”的中间态数据全乱。这个坑我踩过一次排查了整整一晚上最后发现是漏了这条重置语句。3.3 两种方案的对比与选型建议对比维度阻塞法中断法代码复杂度低中CPU占用高死等低超时处理需手动加超时需手动加重置适合场景快速验证、单任务多任务、正式项目我的建议是第一次接触这个模块先用阻塞法跑通亲眼看到串口打印出距离数据建立信心。然后升级到中断法把代码框架改成正式工程的形态。如果你打算做智能小车避障或者把测距功能嵌入到其他多线程任务里直接上中断法别犹豫。4. 数据实测与误差修正别让“/58”这个系数骗了你代码跑通之后最让人头疼的来了数据不准或者偶尔跳一个离谱的值。先说结论这不一定是你的代码问题HC-SR04本身就有物理层面的测量误差你的任务是通过手段把误差压到可接受范围。4.1 实测数据单次测量和多次平均差距有多大我在室内26℃环境下把模块固定好对着一个平面纸箱做了几组实测数据。纸箱实际离模块的距离用卷尺量好每组采集单次值、5次平均值各一个实际距离(cm)单次测量(cm)5次平均(cm)偏差109.810.10.13029.630.20.25049.450.0010098.399.8-0.2200196.5199.5-0.5从数据能看出一个规律单次测量存在约1%到2%的随机抖动而5次平均之后误差能压到0.5%以内。HC-SR04标称精度是±3mm但在实际环境中反射面材质、角度、环境噪声都会额外引入误差不加滤波直接用单次数据的项目几乎都会遇到显示值不停跳几毫米的情况。4.2 简单的滑动平均和中值滤波效果好到超出预期对付随机抖动最有效的不是高大上的卡尔曼滤波而是朴素的中值加平均组合。我对每次测距连续采集7次去掉最大值和最小值剩下5次求平均。原因很好理解HC-SR04的异常值通常是个别极大或极小值中值滤波能直接干掉这类野值再平均则能把微小抖动平滑掉。#define SAMPLE_CNT 7 float HC_SR04_GetStableDistance(void) { uint32_t samples[SAMPLE_CNT]; for (int i 0; i SAMPLE_CNT; i) { samples[i] (uint32_t)(HC_SR04_GetDistance() * 58.0f); // 转回时间再处理 delay_ms(15); } // 冒泡排序取中间区域 for (int i 0; i SAMPLE_CNT - 1; i) { for (int j 0; j SAMPLE_CNT - 1 - i; j) { if (samples[j] samples[j 1]) { uint32_t tmp samples[j]; samples[j] samples[j 1]; samples[j 1] tmp; } } } uint32_t sum 0; for (int i 1; i SAMPLE_CNT - 1; i) { sum samples[i]; } return sum / (SAMPLE_CNT - 2) / 58.0f; }这里我重新把距离乘58转回时间域再处理好处是中值比较发生在“时间”这个原始量上不会在浮点除法过程中引入额外舍入误差。实际用下来同样的测量环境滤波后的数据跳动从±1.5cm降到了±0.3cm以内肉眼可见地稳。4.3 一个更容易被忽略的时序约束模块要求60ms测量周期HC-SR04的数据手册里写了一条硬性要求两次触发之间的间隔不得小于60ms。原因是模块内部处理完一次回波需要时间触发太频繁会让上一次回波的余波干扰下一次测量。我在调试时曾经把主循环里的delay_ms改成10ms试过结果就是数值乱跳而且是那种毫无规律的跳。所以无论你用阻塞法还是中断法测距函数的调用频率一定不要超过20Hz。我在代码里保留了一个delay_ms(100)也就是10Hz的测距频率已经能覆盖绝大多数应用场景了。5. HC-SR04翻车现场从现象反推根因的完整排查思路最后一章分享几个我实际调试中最常遇到的“翻车现象”以及对应的排查顺序。希望能帮你省下几个晚上的调试时间。5.1 现象一距离恒为0怎么触发都没反应排查顺序先看Trig引脚有没有发出脉冲。用示波器量PA1没有示波器就直接用万用表测电压按下触发按钮时引脚应有短暂高电平变化。Trig没问题再看Echo引脚有没有回波高电平如果Echo一直为低说明模块本身可能就没工作检查模块VCC是不是真的有5V、GND是否共地。这里我遇到最多的情况是板子看起来是5V实际上USB口供电不足模块没起来。换个独立供电再试问题立刻消失。软件层面还有一个常见的坑如果你之前用LED点灯程序测试USART还有中断没初始化好串口打印本身就会进入HardFault或者卡死表现也像是“测距为0”。5.2 现象二数据乱跳某个值突然特别大或特别小这个现象十有八九是干扰或时序问题。先按上一章加上中值滤波如果滤波后依然乱跳就要怀疑Echo信号线的噪声了。一个简单验证方法用手捏住Echo线靠近MCU段如果数据跳动明显加剧说明是共模干扰导致的误触发。解决办法一是把Echo线缩短尽量用杜邦线时保持线长在20cm以内二是在TIM2输入捕获配置里把TIM_ICFilter调大这个滤波器能滤掉窄于特定宽度的毛刺信号。实测把滤波系数从0x0F调到0x08抗干扰能力下降明显所以建议直接保持0x0F或者更大。另外一种“乱跳”是软件时序问题两次测距间隔小于60ms。检查主循环里是不是没有加延时或者加了其他耗时操作导致间隔不稳定。5.3 现象三近距离测不准远距离直接无数据如果10cm以内数据不稳定这是HC-SR04的物理盲区导致的属于模块本身特性。超声波发射时探头的余震会让接收端在最初的几百微秒内无法分辨回波所以2cm以内基本不可用5cm以内数据也仅供参考。这个没法靠软件完全解决只能通过改变安装位置来避开。远距离无数据优先怀疑反射面问题。超声波打到柔软、多孔或倾斜角度大的物体上回波衰减很严重。我用一块海绵做过测试距离超过30cm就完全收不到回波了。还有一个常见情况模块两侧的收发探头没有正对反射面稍微偏个几度回波就绕开了接收探头。5.4 现象四Echo引脚一直为高模块像卡住了一样这个现象我遇到过一次原因是Trig引脚被误配置成开漏输出模式而且没有外接上拉电阻导致高电平时引脚实际处于高阻态模块收不到一个干净的触发脉冲状态机卡在了内部处理的某一步。这个坑很隐蔽因为万用表量Trig引脚电压时读数好像是正常的但示波器一看上升沿和下降沿都拖得很长。换成推挽输出模式之后问题立刻消失。如果你也遇到Echo一直为高的现象还有一个常见原因模块的接收部分被强光直射或附近有强超声波干扰源比如空气加湿器的超声振子。这种环境干扰会让模块不断输出有效电平看起来就像卡住了。断电重启模块通常能恢复治本的办法还是让模块远离干扰源。最后再分享一个我在实际项目中养成的小习惯每次测距调用后不要立刻处理数据而是先把原始时间值us打印出来观察一段。肉眼看到的时间序列比距离序列更容易暴露问题——比如周期性波动说明有电机干扰偶发尖峰说明有脉冲噪声。时间序列稳定了距离序列自然就稳了。这个排查习惯帮我解决过不少比HC-SR04复杂得多的传感器调试问题也建议你试试。本文还有配套的精品资源点击获取
返回列表