ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32与HC-SR04超声波测距:从输入捕获到数据滤波的稳定方案

STM32与HC-SR04超声波测距:从输入捕获到数据滤波的稳定方案 简介面向STM32初学者的超声波测距完整项目资源基于STM32F103C8T6最小系统板配合HC-SR04超声波传感器实现距离测量实测结果实时显示在LCD1602液晶屏上并带有强弱分级报警提示适合课程设计、毕业设计、电子竞赛或个人练手。压缩包大小约45.48MB内含完整的C语言工程源码、原理图与PCB设计文件、接线说明以及配套学习资料代码注释完整清晰关键引脚均可按需修改方便直接移植或复现实物。已有146人学习浏览说明该方案具备不错的参考意义。通过本套资料读者能系统掌握定时器输入捕获、超声波时序驱动、LCD1602显示控制以及蜂鸣器报警逻辑等核心知识点结合硬件原理图与PCB文件甚至可以自行打样焊接省去从零设计电路和调试底层的麻烦。此外包内还整理了相关学习资料覆盖从基础概念到实战细节是一份从原理到实现都很完整的学习范本。1. 距离数据跳变才是超声波测距真正要解决的问题拿到一个STM32F103C8T6单片机超声波测距.rar的压缩包解压后通常是一个标准课设工程HC-SR04 模块、STM32F103C8T6 最小系统板、一块 OLED 或 LCD1602代码跑起来能显示厘米数。但这类工程真正做到能稳定测距、能当产品功能用要解决的从来不是能不能测而是测出来之后数据能不能信。回波抖动、温度漂移、环境反射都会让距离值在屏幕上跳来跳去。这篇文章把超声波测距从触发、计时、计算到滤波标定整条链路拆开讲。核心是 STM32F103C8T6 怎么配合 HC-SR04 做高精度计时以及写出来的代码如何应对外界干扰。适合正在做课设、电赛或者准备把超声波模块用进小项目的开发者用 CubeMX 和 Keil5 就能把工程完整落地。2. HC-SR04 测距原理与 STM32F103C8T6 定时器选型2.1 从声波反射到距离换算的完整计算链路HC-SR04 测距原理不复杂单片机向 Trig 引脚拉一个 10us 以上的高电平模块内部发出 8 个 40kHz 脉冲同时 Echo 引脚输出高电平。高电平持续的时间就是超声波从发射到遇到障碍物返回的总飞行时间。距离按L v * t / 2计算其中 v 是声速t 是回波脉冲宽度除以 2 是来回双程距离。声速 v 并不是固定不变的值常温 20°C 下约 343m/s温度变化 1°C 大约会引起 0.6m/s 的声速偏移。这意味着在室外或者温差大的环境同样的回波时间会算出明显不同的距离值。常见做法是在公式里加一个温度补偿系数比如v 331.4 0.607 * TT 为摄氏温度。2.1.1 为什么不能用 delay 轮询计时很多人第一次写 HC-SR04 驱动时习惯用HAL_GPIO_ReadPin去轮询 Echo 引脚的电平变化再用HAL_Delay或者简单循环估算时间。这在逻辑上没有错但精度完全达不到模块标称的 3mm 分辨率。HAL_Delay的精度是毫秒级而超声波测距需要的是微秒级计时而 STM32F103C8T6 主频 72MHz一个定时器计数周期只有 1/72us。STM32F103C8T6 上最合适的是 TIM2、TIM3、TIM4、TIM5 这类通用定时器它们可以配置为输入捕获模式精确记录电平跳变的时刻。先用定时器测量 Echo 高电平的持续时间再换算成距离误差可以控制在几个厘米以内。2.2 定时器输入捕获的工作方式输入捕获的本质是当引脚出现指定边沿时定时器把当前计数器的值拷贝到捕获寄存器中并产生捕获事件。连续配置上升沿和下降沿两次捕获两次计数值之差经过换算就是高电平持续时间。TIM2 在 APB1 总线上时钟频率 72MHz。预分频器PSC设为 71得到 1MHz 的计数频率即每个计数单位 1us。这样回波时间 10ms 对应的计数值是 10000距离大约 1.7m数值范围很适合 16 位计数器直接处理溢出概率很低。在使用 STM32F103C8T6 时需要注意输入捕获通道的引脚不是任意选的。TIM2 的 CH1 对应 PA0CH2 对应 PA1CH3 对应 PA2CH4 对应 PA3。把 HC-SR04 的 Echo 接 PA0用 TIM2_CH1 做输入捕获是硬件上最直接的路径。2.2.1 定时器溢出对计时的干扰超声波测距有一个物理上限问题HC-SR04 最大测量距离约 4m对应回波时间约 23.5ms。如果定时器按 1us 计数16 位计数器最大值是 65535us约 65ms所以在 4m 范围内不需要处理溢出。但如果定制了更长量程的超声波模块比如量程 7m 的回波时间会超过 40ms计数器仍不会满但余量变小。如果配置为 72MHz 不分频计数器 0.91ms 就会溢出一次那必须在溢出中断里做变量累加否则计数值必然出错。建议直接用 1MHz 计数频率配合 16 位定时器省去溢出处理的复杂度。如果用了定时器的自动重装载-中断扩展计数宽度程序逻辑会更复杂但换来的是更大的量程余量适合做 4m 以上量程的场景。2.3 声速补偿和测量范围的边界条件HC-SR04 的手册标称测量范围是 2cm 到 400cm实际上 2cm 的盲区是因为模块发射和接收探头之间存在物理隔离回波太快会导致收发电路串扰。代码里要设置一个最小有效值通常小于 3cm 直接判为无效显示“0”或“—”即可。最大范围的限制因素有两个一是模块发射功率和接收灵敏度二是定时器允许的最大计时长度。STM32F103C8T6 的定时器在 1MHz 计数下支持 65ms 的计时窗口对应约 11m 的量程HC-SR04 本身达不到这个极限所以代码中的超时判断要针对模块特性来设置建议设置为 30ms超过即判为无回波并清空计时值。3. 用 CubeMX 初始化 STM32F103C8T6 定时器与 GPIO3.1 CubeMX 引脚分配和时钟树配置打开 STM32CubeMX选择芯片后先配置时钟树。STM32F103C8T6 的外部高速时钟HSE接 8MHz 晶振系统时钟 SYSCLK 设为 72MHzAPB1 总线时钟分频后为 36MHzTIM2 的时钟源需要翻倍到 72MHzCubeMX 会自动处理这个倍频关系。引脚分配如下PA0 配置为 TIM2_CH1 输入捕获模式连接 HC-SR04 的 Echo 引脚PA1 配置为 GPIO 输出连接 Trig 引脚PC13 连接板载 LED作为测距状态指示。OLED 用 I2C 的话PB8、PB9 配为 I2C1 的 SCL 和 SDA。3.1.1 Timers 参数设置的要点在 Timers - TIM2 中Clock Source 选择 Internal ClockChannel1 选择 Input Capture direct mode。PSC 预分频值设为 71 - 1Counter Period 设为 65535。这里注意 CubeMX 的 PSC 和 ARR 都要填入实际值减一因为寄存器是从 0 开始计数的。输入捕获的极性先在上升沿触发一次捕获到以后在中断服务函数里把捕获极性改成下降沿再次捕获后计算差值。另一种做法是在 CubeMX 里配置两个通道同时捕获上升沿和下降沿但从工程简洁角度单通道改极性更常见。3.2 Keil5 工程中需要的完整初始化代码工程生成后在 main.c 的用户代码区补充一个超声波测距函数。以下代码在测量开始时触发一次 10us 以上的高电平然后等待 Echo 引脚电平变化使用定时器捕获模式记录高电平宽度。uint32_t g_cap_value 0; void Ultrasonic_StartTrigger(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_Delay(1); // 拉高超过10us即可这里放宽到1ms更稳定 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { g_cap_value HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); } } }这段代码的关键在于HAL_TIM_ReadCapturedValue读取的是捕获寄存器 CCR1 的值。如果只是用单次捕获模式读到的是上升沿触发瞬间的计数器值而不是高电平宽度需要在上升沿中断时清零计数器或者在两个边沿各读一次再相减。正确做法是在中断回调中记录上升沿和下降沿两个时刻。常见的代码结构是维护一个状态标志第一次进入中断上升沿时记录t1 当前计数值同时切换捕获极性为下降沿第二次进入中断下降沿时记录t2然后计算duration t2 - t1。如果 t2 小于 t1说明计数器发生了回绕需要加 65535。volatile uint16_t t1 0, t2 0; volatile uint8_t capture_index 0; volatile uint32_t echo_duration_us 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { if (capture_index 0) { t1 HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); capture_index 1; } else if (capture_index 1) { t2 HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (t2 t1) { echo_duration_us (t2 - t1) / 72; } else { echo_duration_us (65536 t2 - t1) / 72; } capture_index 0; __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } } }这里用了一个除数 72对应的是配置时钟时预分频为 71 的情况还是未分频的情况。这个细节容易踩坑。3.2.1 定时器分频系数与回波时间换算的对应关系如果 PSC 设置为 71即计数值 0 到 71 为一个周期等价于 72 分频那么定时器计数频率是 72MHz / 72 1MHz每个计数单位是 1usCCR 的值直接等于微秒数不需要除 72。只有 PSC 0 即 72MHz 计数时每个计数单位是 1/72us才需要用计数值除以 72 得到微秒数。上面的代码写了除以 72实际上是假设 PSC0 的配置。两种写法都可能出现关键在于和 CubeMX 里的配置保持一致。最容易排查的方法是直接用示波器或者逻辑分析仪实测 Echo 高电平时间和串口打印出的微秒数对比误差应该在 1us 级别否则就要检查分频系数。3.3 距离计算函数的参数细节回波时间拿到后按标准公式计算距离float temperature 25.0f; float speed_of_sound 331.4f 0.607f * temperature; float distance_cm speed_of_sound * echo_duration_ms / 2 / 10;这里echo_duration_ms是毫秒。如果echo_duration_us是微秒公式调整为float distance_cm speed_of_sound * (echo_duration_us / 1000.0f) / 2 / 10;把数值带进去验证一下温度 25°C 时声速约 346.5m/s也就是 0.03465cm/us。1ms 的回波时间对应 17.3cm 的距离和 HC-SR04 常见测距数据的经验值非常接近。这段代码建议用常量#define SOUND_SPEED_CM_US 0.0343f简化减少每次计算的浮点开销。temperature变量如果只定义不用编译器会优化掉但代入公式后整个计算就有了物理意义。4. 主循环测距状态机与参数优化4.1 轮询和中断协同的测距流程有了输入捕获做回波计时主循环的逻辑变得很清晰先发一次 Trig 触发信号然后等一次完整的捕获结束读取echo_duration_us算出距离更新 OLED 显示等待一段时间后再触发下一次测量。完整的主循环代码如下while (1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 发送10us以上高电平触发 uint32_t timeout_start HAL_GetTick(); while (capture_index ! 0) { if (HAL_GetTick() - timeout_start 100) { capture_index 0; break; } } float distance 0.0f; if (echo_duration_us 0) { distance SOUND_SPEED_CM_US * (echo_duration_us / 2.0f); } char disp_buf[16]; if (distance 0) { snprintf(disp_buf, sizeof(disp_buf), %.1fcm, distance); } else { snprintf(disp_buf, sizeof(disp_buf), ---); } OLED_ShowString(0, 0, disp_buf); HAL_Delay(100); // 测量周期不低于100ms避免多次触发信号重叠 }超时保护的时间设为 100ms远远大于最大量程对应的 23ms如果捕获异常或者根本没有障碍物能在 100ms 内恢复状态机不会卡死。超声波的发射周期对测量稳定性有很大影响。HC-SR04 模块自己发出的 8 个 40kHz 脉冲在空气中余振会持续一段时间如果测量周期太短上一次的回波会干扰下一次的测量。100ms 的间隔在绝大多数场景是足够的追求更高刷新率可以压到 50ms但再低就容易出跳变。4.2 处理无回波和超时不卡死的方案真实场景中有一个很容易被低估的问题HC-SR04 在无障碍物或者障碍物表面太倾斜时根本不会返回 Echo 高电平。如果程序在等capture_index ! 0时没有超时保护整个系统会卡死在等待循环里。上面代码用HAL_GetTick()做了超时判断100ms 后强制退出等待。这是一种轮询和中断结合的思路逻辑简单且足够可靠。如果想做得更轻量可以在定时器更新中断里做溢出检测连续溢出指定次数后判定超时释放主循环。4.2.1 多级回波问题HC-SR04 的回波不只有直达这一种。声波打到墙角、桌面、导线这些表面反射之后会形成二次回波。如果第一次回波很弱而二次回波较强模块的接收电路可能只检测到二次回波导致距离读数跳到一个明显偏大的值。应对的思路是软件层面做多级判定分别测量三次距离取中位数再输出。稍微复杂一些的做法是连续采样 N 次去掉最大最小值对中间值做平均即 N 点中值平均滤波。实测数据表明这种滤波能有效滤掉偶发的多径干扰尤其适合在实验室桌面环境中。4.3 OLED 显示距离和串口调试输出的联动开发调试阶段一定会用到串口方便看捕获的原始计数值和换算后的距离。用 STM32F103C8T6 的 USART1 以 115200 波特率输出即可接一个 USB 转 TTL 模块在电脑上看日志。串口输出建议区分两层调试模式打印us%lu cm%.1f原始回波时间和距离正常运行模式只打印距离或者不打印避免串口阻塞主循环。USART1 发送是阻塞式的如果每帧都打印在 115200 波特率下 20 字节的数据大约耗时 1.7ms和 100ms 的测量周期相比不算什么但数据量变大后会影响时序所以要控制打印频率。4.4 温度补偿在代码里的实际接入位置前文提到声速受温度影响代码实现时有两种插入方式每次采样后实时计算声速或者每隔一段时间更新一次声速。前者精确但增加浮点计算量后者更符合实际场景因为温度在几秒内变化不超过 1°C。一个务实的方案是在初始化时设定一个默认温度 25°C在系统运行时通过按键或者上位机命令更新温度值。温度每变化 5°C距离 1m 处就会产生约 1cm 的偏差很多人忽略了这个细节等拿到室外对比卷尺测量值时才意识到误差来源。5. 让超声波测距读数更稳的实测标定技巧5.1 滤除随机跳变用的中值算法实现写一个轻量的中值滤波函数采样次数设为 5 次最合适。太少滤波效果差太多会让响应变慢。uint16_t MedianFilter_5(uint16_t data_arr[5]) { uint16_t tmp; for (int i 0; i 4; i) { for (int j i 1; j 5; j) { if (data_arr[j] data_arr[i]) { tmp data_arr[i]; data_arr[i] data_arr[j]; data_arr[j] tmp; } } } return data_arr[2]; }这个冒泡排序每测一轮最多执行 10 次比较在 72MHz 主频下耗时在微秒级不影响测量周期。滤波结果比单次值稳定很多尤其是在传感器对准倾斜表面时明显。串口打印经过滤波的距离值用卷尺实测对比几个点记录误差规律近距离 20cm 内误差通常在 ±1cm中距离 50cm 到 2m 误差在 ±0.5%远距离超过 3m 后误差会迅速变大。根据这个规律可以在代码中加分段补偿系数不过这属于场景定制通用产品一般不轻易加。5.2 模块安装位置对测量精度的直接影响超声波模块的探头方向性是扇形的大约 15° 的波束角。安装时探头平面必须和被测面平行倾斜 10° 以上回波信号强度就会大幅衰减读数开始跳变或测不到。安装在移动小车上时模块不能贴近地面否则地面反射会产生固定的虚假读数。典型表现是显示距离比实际偏大且数值稳定你把模块抬高 5cm 后故障消失。排查跳变问题时有经验的工程师第一反应就是掰模块角度而不是改代码。5.3 用逻辑分析仪验证时序是否正确的快速方法HC-SR04 只有两根控制线逻辑分析仪的测试点就是 Trig 和 Echo。抓一次波形看 Trig 高电平是否超过 10us看 Echo 高电平宽度是否等于障碍物距离对应的回波时间。有一次实测中程序总是显示 0抓波形发现 Trig 的脉冲宽度只有 8us模块没有正常工作把 delay 时间从 10us 放宽到 50us 后问题消失。这个对比经验提醒模块要求的 10us 是保证触发的下限不意味着 10us 一定可靠。每块模块的 LED 驱动电路特性略有差异把触发脉宽放宽到 50us 甚至 1ms 没有任何副作用因为模块内部有自己的脉冲控制逻辑。但触发信号太宽不会影响模块行为因为 Trig 只是门槛信号模块收到后会自己发固定脉宽。5.4 低功耗场景中定时器的启停控制如果项目要用电池供电超声波测距这种每秒测几次的传感器不适合一直开着定时器。常见做法是测距前开定时器测完立刻关。调用__HAL_TIM_ENABLE和__HAL_TIM_DISABLE控制 TIM2 的开关整体功耗可以从几毫安降到微安级别。关定时器时要注意捕获中断的残留状态如果在关闭前刚好来了一个回波中断标志位会悬挂重新开启后立即触发一次假捕获。解决方法是关闭前先清除标志位__HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_CC1 | TIM_FLAG_UPDATE);STM32F103C8T6 进入 STOP 模式后再唤醒定时器配置不会丢失重新打开时钟和定时器即可继续工作。把测距、显示、休眠放在一个周期的状态机里电池供电的续航能明显提升。最后回到开头那个含糊的问题解压 rar 后能用只是一张门票能把数据磨得稳定了这个题目才算真正做完。做嵌入式开发最怕的不是代码复杂而是时序对不上却找不到方向。用逻辑分析仪抓波形看回波用串口把原始计数值打出来对比再决定调代码还是调探头角度这条路比反复改滤波参数高效得多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进