ARTICLE DETAIL

资讯详情

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

STM32+HC-SR04超声波测距实战:从CubeMX配置到代码排坑全记录

STM32+HC-SR04超声波测距实战:从CubeMX配置到代码排坑全记录 简介基于STM32F103C8T6的超声波测距完整工程面向单片机初学者、电子设计开发者及课程设计/毕业设计学生解决测距系统从硬件搭建到软件调试的关键问题。系统硬件集成了78M05稳压电源模块、STM32F103C8T6控制模块、OLED显示模块、蜂鸣器报警模块以及HC-SR04超声波传感器上电后由单片机控制超声波发射与接收处理距离数据并在OLED上以mm和m单位显示低于阈值时触发声光报警软件部分涵盖各模块引脚配置、输入输出方式、数据采集与传输逻辑。压缩包共206个文件、约7.34MB包含Keil MDK工程文件、C语言源文件.c/.h、编译链接生成文件.axf/.hex、分散加载与调试配置等可在Keil中直接打开工程查看、编译和烧录。目前已有594人学习。读者可据此掌握GPIO配置、HC-SR04时序控制、OLED显示驱动及测距数据换算的实现方法并为类似测距或报警项目提供可复用代码与排错思路。 做避障小车、给水箱加低液位报警、搞一套简易倒车雷达这些场景绕来绕去都躲不开一个问题测距。有人选红外有人直接上激光模块但我敢说超声波测距依然是目前门槛最低、性价比最高的方案。HC-SR04 配上 STM32F103C8T6几乎是入门工程师和电子爱好者手边最常见的组合——便宜、资料多、接口简单随便一搜就能找到能亮的 Demo。但如果你真的拿它去干活很快就会遇到读数跳变、程序卡死、距离偏大偏小这些毛病。这篇文章就把我实际调试的完整过程拆开讲清楚从硬件搭线到 CubeMX 配置再到核心代码和排坑记录全部是可复现的实践内容。1. 先从选型说起为什么这个组合经久不衰1.1 测距场景里不同方案的真实差距测距方案看着很多真正到了手头做东西的时候取舍其实很直接。我列个对比表大家感受一下方案典型模块成本量程精度主要限制超声波HC-SR045~10元2cm~4m标称±3mm受温度影响对软质物品不敏感红外红外避障模块3~8元2~30cm低线性差环境光干扰大激光测距VL53L0X30~80元厘米级到2m高I2C配置复杂成本高工业激光TFmini等200元以上0.1~12m高价格贵串口协议也得自己啃如果你做的是智能小车避障、水箱液位检测、倒车提醒这类功能HC-SR04 的几块钱成本和毫米级精度完全够用。它的超声物理特性对光线不敏感在暗处、强光下都能正常工作这是红外方案比不了的。激光模组确实精度高但很多场景压根用不到毫米级精度何必为了那点差异把成本翻十倍。1.2 F103C8T6 这颗“老将”的能力边界STM32F103C8T6 的规格相信大家都很熟悉Cortex-M3 内核主频最高 72MHz64KB Flash20KB SRAMLQFP48 封装。用在这种测距任务上资源绰绰有余——测距本身只涉及几个 GPIO、一个定时器和一个串口连 10% 的 CPU 资源都用不到。我坚持选它的原因其实很朴素工程模板成熟踩坑资料多芯片便宜还好买。哪怕是刚上手的小白用 CubeMX 点几下就能生成工程再配合 ST-Link 下载器调试体验很顺畅。另外这些年 GD32、HK32 这类国产兼容芯片也越来越多老项目替换时基本不用改代码后面我会单独聊这个话题。2. 硬件搭接引脚分配与那几个容易翻车的电平细节2.1 最小系统板的必要外设F103C8T6 最小系统板大概是很多人的第一块开发板核心就六个东西电源、BOOT0、晶振、复位、SWD 下载口和排针。接线前确认一下 BOOT0 跳线帽接在 GND 侧否则芯片上电会进 Bootloader 而不是跑你的程序这个低级错误我见不少人犯过。供电方面建议整个系统用 5V 统一供电板载的 AMS1117-3.3 会把它转成 3.3V 给单片机。要注意的是 ST-Link 的 3.3V 输出能力很弱只够给 MCU 供电额外带超声波模块会直接导致电压跌落、测距数据异常。我后排坑部分会讲一个真实案例。2.2 引脚分配与接线我用的是 PA0 做 TrigPA1 做 Echo。这两个引脚在 F103C8T6 上都是 5V 容忍引脚本身也多用于普通 GPIO不占用 SWD 下载用的 PA13/PA14也不占用串口下载用的 PA9/PA10分配起来很干净。完整的接线表如下模块引脚接单片机的引脚说明VCC5VHC-SR04 推荐 5V 供电GNDGND必须与单片机共地TrigPA0触发输入单片机输出高电平脉冲EchoPA1回波输出返回高电平脉宽接线看似简单但有一个细节直接影响稳定性电源线别用劣质杜邦线供电回路要尽量短。模块瞬间发射超声波时电流峰值不小线材电阻一多VCC 电压就会塌陷回波信号跟着抖动。2.3 Echo 的 5V 电平其实是很多人翻车的地方HC-SR04 的 Echo 引脚在回波时会输出 5V 高电平而 STM32 的 GPIO 工作电压是 3.3V。F103 系列的 PA0、PA1 标称是 FT5V 容忍引脚直接接上去绝大多数情况下不会烧坏但长期可靠性看加一个 1kΩ 串联电阻做限流更稳妥条件允许的话用 2.2kΩ 和 3.3kΩ 电阻分压到 3.3V 最好。另外注意市面上有些改良版的超声波模块Echo 可能是 3.3V 电平还有一些模块 Trig 高电平阈值偏高3.3V 触发不稳定。拿到模块先量一下空闲状态下的引脚电平再决定要不要加三极管或者电平转换电路这个习惯能省去后面大量排查时间。3. CubeMX 配置三处关键设置决定后面少踩坑3.1 时钟树不配置好us 计时全是错的用 CubeMX 生成工程时第一件事是配置 RCC让 HSE 使用外部 8MHz 晶振然后在 Clock Configuration 里把系统时钟倍频到 72MHz。F103C8T6 的时钟树最高就支持到 72MHz超过这个值会跑飞。时钟没配好的直接后果是所有和时间相关的逻辑都是错的延时函数不准、串口波特率乱码、定时器计数偏差。很多人测距数据忽大忽小查半天代码最后发现是主频根本不是 72MHz延时和计时全乱套了。配置完后可以顺手把 MCO 引脚PA8输出到示波器确认频率或者用 HAL_GetTick 和秒表粗略验证心里有底。3.2 GPIO 与 TIM2测距的核心时钟GPIO 的配置不复杂但有两个值得注意的点。PA0 作为 Trig配置成推挽输出速度选 Low 就够因为控制的是脉冲信号而不是高速通信PA1 作为 Echo配置成输入模式注意不要随手开了上下拉HC-SR04 的 Echo 空闲状态是低电平开内部上拉反而可能引入不确定电平。定时器我选了 TIM2三个原因第一它的是 32 位计数器按 1us 计数一次算要 4294 秒才溢出几乎不用管溢出问题第二CubeMX 里 TIM2 可以配置出干净的 1MHz 计数时钟第三代码里用__HAL_TIM_SET_COUNTER清零后直接读TIM2-CNT就行逻辑非常直观。PSC 设为 71计数器时钟就是 72MHz / (71 1) 1MHz再配合 ARR 设为 0xFFFFFFFF就得到了一个可随时清零、不怕溢出的微秒级计时器。3.3 串口、调试口与生成工程选项串口建议直接开 USART1引脚默认 PA9/PA10波特率 1152008 位数据、无校验、1 位停止位。这里有个不被注意的细节SYS 设置里一定要把 Debug 选成 Serial Wire否则工程生成后第一次下载没问题第二次就找不到芯片了还得用串口擦除才能救回来。生成代码时Toolchain 选 MDK-ARM然后勾选Generate peripheral initialization as a pair of .c/.h files per peripheral这样每个外设独立成文件代码结构干净。版本上用较新的 CubeMX 6.x 即可生成的 HAL 库代码和 Keil5 配合起来没什么障碍。4. 测距代码触发、计时、防卡死一个都不能少4.1 先搞清楚模块的工作流程HC-SR04 的逻辑一句话就能说清外部给 Trig 一个大于 10us 的高电平脉冲模块内部会发射 8 个 40kHz 的超声波脉冲并把 Echo 引脚拉高从发射到接收回波的时间就是 Echo 高电平的持续时间。距离等于这个时间乘声速再除以 2因为声波走了个来回。这个流程里最容易忽略的就是时序边界条件。Trig 脉冲不够宽模块直接不响应连续两次触发的间隔太短上次的回波还没结束就又触发读数就会串扰。一般建议测量周期不小于 60ms代码里两次采样之间加一点延时后面滤波部分我会一起处理。4.2 微秒级延时DWT 比 HAL_Delay 好用太多HAL_Delay 是毫秒级延时满足不了 Trig 的 10us 级别需求。CubeMX 生成的工程里虽然可以配合定时器做延时但要为这个额外开一个定时器有点浪费。我习惯直接用 DWT 内核计数器它随内核时钟自增精度高代码极简static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }这个延时函数不受系统滴答中断影响只要主频是 72MHzSystemCoreClock就是 72000000延时精度在微秒级。实测下来比单纯空循环稳定很多。4.3 完整测距函数下面这是我在实际项目里用的版本超时保护是重点没有它Echo 一旦悬空或者模块没供电程序就会死在 while 循环里#define TRIG_GPIO_PORT GPIOA #define TRIG_PIN GPIO_PIN_0 #define ECHO_GPIO_PORT GPIOA #define ECHO_PIN GPIO_PIN_1 float UltraSonic_GetDistance(void) { uint32_t timeout 0; /* 1. 产生 15us 的触发脉冲 */ HAL_GPIO_WritePin(TRIG_GPIO_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_GPIO_PORT, TRIG_PIN, GPIO_PIN_RESET); /* 2. 等待 Echo 上升沿超时保护 */ timeout 0; while (HAL_GPIO_ReadPin(ECHO_GPIO_PORT, ECHO_PIN) GPIO_PIN_RESET) { if (timeout 50000) return -1.0f; } /* 3. 清零计时器等待 Echo 下降沿 */ __HAL_TIM_SET_COUNTER(htim2, 0); timeout 0; while (HAL_GPIO_ReadPin(ECHO_GPIO_PORT, ECHO_PIN) GPIO_PIN_SET) { if (TIM2-CNT 30000) { __HAL_TIM_SET_COUNTER(htim2, 0); return -1.0f; } } uint32_t pulse_width_us TIM2-CNT; /* 4. 最短距离判断防止近距离盲区误报 */ if (pulse_width_us 50) return -1.0f; /* 5. 距离计算pulse_us * 声速(cm/us) / 2 */ return pulse_width_us * 0.017f; }代码里两个超时值各有用意等待上升沿超时是防止 Echo 一直为低导致死循环比如模块没接好等待下降沿超时是防止模块面前有一堵很远还很吸音的墙回波脉冲异常过长。30ms 对应约 5m 距离已经超出 HC-SR04 的 4m 量程所以这里判断逻辑很合理。pulse_width_us * 0.017f这个系数来自 340m/s 声速即 1us 单程距离约 0.017cm。如果要更严谨的温度补偿这个系数要动态调整。4.4 滤波连续多次取中位值加平均HC-SR04 偶尔会蹦出一个野值不是模块坏了而是声波被边缘、斜面反射回来产生了异常长的回波路径。解决办法很简单连续采样多次去掉最大最小值后取平均又叫去极值平均滤波。#define SAMPLE_TIMES 5 float UltraSonic_FilteredDistance(void) { float buf[SAMPLE_TIMES]; for (int i 0; i SAMPLE_TIMES; i) { buf[i] UltraSonic_GetDistance(); HAL_Delay(60); /* 两次测量间隔至少 60ms */ } /* 对 buf 进行冒泡排序去掉最大最小剩余求平均 */ for (int i 0; i SAMPLE_TIMES - 1; i) { for (int j 0; j SAMPLE_TIMES - 1 - i; j) { if (buf[j] buf[j 1]) { float temp buf[j]; buf[j] buf[j 1]; buf[j 1] temp; } } } float sum 0.0f; for (int i 1; i SAMPLE_TIMES - 1; i) sum buf[i]; return sum / (SAMPLE_TIMES - 2); }排序代码虽然笨但胜在直观。如果对实时性要求高可以改成滑动窗口中值滤波或者更简单的限幅滤波——当前值如果比上一次的合法值偏离超过 10cm直接丢弃再用旧值。总之不要让原始数据直接驱动执行机构超声波输出天然带噪声这是模块物理特性决定的。5. 实测数据与踩坑排查记录5.1 用卷尺对比实测结果我把代码烧进去用卷尺做参照连续测了几组数据每组取滤波后的结果卷尺距离实测距离偏差20cm19.8cm-0.2cm50cm50.2cm0.2cm100cm100.7cm0.7cm200cm201.8cm1.8cm近距离偏差在误差范围内远距离偏差变大而且整体偏大。原因有两层一是 0.017 这个固定系数对应的是 20°C 下的声速实际室温稍高一点声速变快算出来的距离就会偏大二是远距离回波信号弱模块内部比较器触发时刻会略有延迟脉宽被拉长。如果对远距离精度有要求建议做温度补偿或者用 100cm 附近的实际读数反推一个校准系数。5.2 坑一Echo 悬空导致程序卡死有次我调试时把 Echo 杜邦线碰掉了然后程序就停在读距离的函数里不动。原因前面说过Echo 浮空后引脚读到的电平不确定等待上升沿的 while 循环一直在空转。这类问题最好的解决方式不是靠代码兜底而是接线检查。但软件超时保护也得有毕竟现场环境复杂不能要求接线永远牢固。我代码里 50000 次循环的超时保护就是在那个场景下加上的。万一模块没接好函数直接返回 -1主逻辑可以根据这个值做错误处理而不是死机。5.3 坑二读数周期性跳变查出来是供电问题另一个典型案例是读数每隔几次就会跳一个明显偏大的值示波器看 Echo 波形偶尔会出现异常宽的脉冲。排查过程很有意思换模块、换引脚、改代码全都没用。最后拿万用表量模块 VCC发现触发瞬间电压从 5.0V 掉到了 4.2V源头是一根又细又长的劣质杜邦线。超声波模块发射瞬间电流脉冲很大供电线路阻抗稍微大一点电压就跌。解决办法很粗暴换粗短的导线另外在模块 VCC 和 GND 之间并一个 100uF 电解电容吸收瞬态电流问题彻底解决。这也说明一个问题很多“模块不行”的结论其实是供电不行的锅。5.4 坑三HAL_UART_Receive_IT 只收一次数据调试时我习惯用串口打印距离值自己写了个串口接收回调结果用HAL_UART_Receive_IT(huart1, rx_data, 1)做指令接收时只收到一次数据就不再触发了。这个坑的根源是 HAL 库的机制IT 接收模式是一次性的接收完成后中断使能被关闭必须在回调函数里重新调用一次HAL_UART_Receive_IT否则 RXNE 中断不会再触发。很多人第一次用都懵以为是串口坏了。正确的写法是在回调里再开一次接收void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 处理 rx_data */ HAL_UART_Receive_IT(huart1, rx_data, 1); /* 重新开启下一次接收 */ } }这个细节虽然不是测距本身的问题但串口是嵌入式调试最重要的窗口踩过的人都知道它有多烦。5.5 关于精度的几句实话HC-SR04 标称精度 ±3mm那是理想条件下的数据。实际使用中目标表面材质影响巨大平整硬墙面数据很稳软海绵、泡沫板基本测不准甚至测不到斜放的目标会让声波反射到别处读数会假性偏大。这些不是代码能解决的是物理限制选型时要心里有数。另外一个容易被忽略的是盲区。模块前方 2cm 以内的目标回波会和发射波混在一起读不出有效距离。做避障小车时如果车身离障碍物已经很近了超声波反而会“失灵”这时候需要红外或者机械防撞做补偿。6. 从测距到产品几个实用扩展方向6.1 给声速加上温度补偿前面提到固定系数 0.017 在温度变化时会有误差严谨一点的工程实现是把声速模型化。空气中声速近似为v 331.4 0.6 * T其中 T 是摄氏温度。用 DS18B20 或者 BME280 测一下环境温度动态更新系数公式变成float v 331.4f 0.6f * temperature; /* m/s */ float dist (pulse_width_us / 1000000.0f) * v / 2.0f * 100.0f; /* cm */按 0°C 到 40°C 变化算声速从 331.4m/s 变到 355.4m/s偏差最大能到 3.5% 左右对 2m 以上的测距就是好几厘米的误差。如果应用场景是户外或者温差大的环境温度补偿不是可选项而是必选项。6.2 安防联动距离阈值触发报警超声波测距最直接的产品化方向就是设一个距离阈值低于阈值触发动作。比如智能家居安防场景把模块贴在门框旁边检测到有人靠近且距离小于 50cm 时蜂鸣器报警或者通过继电器控制摄像头再比如水箱水位检测超声波朝下测液面液位低于设定值就输出提醒信号。这类应用的核心不是测距本身而是阈值判断的可靠性。直接拿单次数据做判断会频繁误报我用的是连续三次超过阈值才触发同时加入回差控制——比如 50cm 触报警但等距离回到 60cm 以下才解除报警防止边界抖动。6.3 工程化建议与国产替代如果这个方案要转产有几个建议供参考。第一代码里的超时保护和看门狗一定要配齐单片机虽然不像 PLC 那么容易死机但外部干扰导致的异常还是可能发生。第二滤波逻辑要放到固定任务周期里执行避免出现测量节奏不稳定带来的额外误差。第三考虑模块替代HC-SR04 同规格的国产模块很多以及带串口输出的 US-100 这类模组可以直接省掉脉宽测量逻辑。顺带说一句F103C8T6 这几年国产替代芯片已经很成熟GD32F103、HK32F103 这类兼容芯片可以直接替换到现有工程里CubeMX 工程绝大多数不用改HAL 库调用也基本一致。唯一要注意的是 Flash 和 SRAM 容量可能略有差异批量选型时以具体型号数据手册为准。对于老工程师来说多一个备选供应源项目风险就低一分。这只是个很基础的测距程序但把它从“能亮”做到“测准”再到“能抗住现场环境”每一步都有值得抠的细节。我的建议是拿手头的板子照着配置先跑通然后故意拔掉 Echo 试试会不会卡死故意用斜板对着模块看看数据怎么变这些实验比单纯抄代码更有价值。本文还有配套的精品资源点击获取
返回列表