简介:HC-SR04超声波测距模块资料包面向电子爱好者、物联网开发者与嵌入式初学者,系统梳理了模块工作原理、TRIG/ECHO引脚接口、触发时序、测距公式及使用注意事项,可支撑避障、安防、智能家居等真实距离检测项目的快速落地。压缩包共61个文件、约1.8MB,内容以51和PIC单片机C语言源码、可烧录HEX文件、Keil工程文件、PDF参考手册与JPG接线图为主,其中还包含多个.bak备份与编译中间文件,便于对比调试;细分出LCD1602、LCD12864、数码管和串口等多种显示方案的子压缩包。目前已有1120人下载学习,适合初学者对照代码与接线图理解从发射到回波测距的完整流程。借助整理好的源码和电路说明,读者可直接移植到Arduino、树莓派等平台,同时多个显示方案便于按项目需求选型比较,为后续扩展多模块3D测距或融合红外、激光传感器打下基础。
1. 超声波测距模块 HC-SR04:最便宜的距离传感器,为什么还要认认真真做资料整理
HC-SR04 超声波测距模块大概是创客和工程师手里最没有神秘感的传感器之一:四个引脚、一块双色 PCB、两个圆柱形探头,淘宝几块钱就能买到。可就是这么个看起来“傻瓜式”的模块,真正把它用明白、用在量产产品里不掉链子的人,反而不多。很多人照着网上的例程接上线、敲完代码,发现距离数值乱跳、测不远、甚至上电就烧引脚,最后只能归咎于“模块质量差”。
我在几个项目里把它当过倒车雷达的探测头、水箱液位计的传感单元,也拿它做过机器人避障的原型验证。几次折腾下来最大的感受是:HC-SR04 的资料并不缺,缺的是把时序图、电平逻辑、声速补偿、盲区限制这些散落的信息串成一条可落地的主线。这篇笔记就把我验证过的接线方式、驱动代码、参数选择和踩坑记录完整拆开讲,从“能跑通”讲到“测得准”。如果你正打算用它做原型验证或者小批量产品,这篇文章应该能帮你少烧几个引脚、少调几天数据。需要说明的是,下文所有案例均来自我自己的模拟项目X与某跨平台系统的开发过程,仅供技术路径参考。
2. 从时序图读懂 HC-SR04:原理不长,但电平细节全是坑
2.1 回波时间到距离:一个简单的公式,两个容易被忽略的隐含条件
HC-SR04 的测距原理不复杂。模块上的发射探头发出 8 个 40kHz 的超声脉冲,脉冲遇到障碍物反射回来,接收探头捕获回波。单片机只需要记录从发射到收到回波的这段时间 T,距离就等于声速 c 乘以时间 T 再除以 2,因为超声波走了个来回。公式写出来是:
距离 = (声速 × 回波高电平持续时间) / 2这个公式看着简单,但落地时有两个隐含条件每个新手都会忽略。第一,声速不是固定的 340m/s,它随温度变化明显,零下 10 度和 40 度环境下测同一个障碍物,误差能到 3% 左右,换算成 2 米的测量距离就是 6 厘米,这个量级在倒车雷达场景里是无法接受的。第二,这里的 T 在 HC-SR04 的硬件设计里直接对应 Echo 引脚上高电平的持续时间,不是你自己用定时器掐表得到的理论值,所以能不能精确测量这个高电平宽度,直接决定了整个系统的精度上限。
2.2 触发与回波:HC-SR04 的电气参数和时序,照着这个表核对不会错
HC-SR04 对外暴露四个引脚,VCC、Trig、Echo、GND。VCC 典型工作电压 5V,Trig 是输入引脚,用来触发一次测距,Echo 是输出引脚,回波信号从这里读出来。下表是我在实际项目中整理过的关键电气参数,照着核对能避免不少低级问题:
| 参数 | 数值范围 | 说明 |
|---|---|---|
| 工作电压 | 5V DC | 低于 4.5V 时测距精度明显下降 |
| 静态电流 | 小于 2mA | 待机时电流很小 |
| Trig 触发信号 | 10us 以上的高电平 | 低于 10us 可能不触发 |
| Echo 输出电平 | 与 VCC 同电位(5V) | 接 3.3V 单片机必须做电平转换 |
| 测量范围 | 2cm - 400cm | 标称值,实际稳定段在 2cm - 300cm |
| 测量角度 | 小于 15 度 | 超过后回波衰减严重 |
| 精度 | 约 3mm | 理想平坦表面的重复精度 |
时序关系是:单片机把 Trig 拉高至少 10us,然后拉低;模块自动发送 8 个 40kHz 脉冲;Echo 引脚在发射后立即拉高,直到收到回波才拉低。所以 Echo 高电平的宽度就代表了超声波的往返时间。这里有个坑是好多新手容易搞反的:以为 Echo 高电平是从模块收到回波才开始,其实它是从发射那刻就开始拉高了。
2.3 为什么有些项目用不了 HC-SR04:测量盲区、锥形波束和表面反射
了解完原理之后要做的第一件事不是接线,而是评估这个模块适不适合你的场景。HC-SR04 有严格的最小测量距离,标称 2cm,实际测试中小于 2cm 时 Echo 输出会变得极不稳定,甚至一直保持高电平。这是因为发射探头和接收探头之间有一定距离,而且发射脉冲本身会有余振,回波信号会被淹没在发射余波里。
另外超声波波束是一个圆锥形,不是一条直线。模块正对障碍物时回波最强,偏 10 度回波幅度就掉一截,偏 15 度以上经常直接测不到。所以如果你要做的是斜着安装的避障方案,必须预留角度余量。软性材料也要特别注意,海绵、布料、厚绒布这类表面对 40kHz 超声波的反射极差,我做过一次对比测试,模块正对一块 2cm 厚的海绵,1 米距离上直接无回波输出,这点在选型时最容易栽跟头。
3. 裸机 GPIO 驱动 HC-SR04:用最少的代码跑通最小系统
3.1 先搭硬件:5V 供电、电平转换和去耦电容一个都不能少
我习惯先把硬件环境弄干净再写代码,因为 HC-SR04 对供电质量比较敏感。模块的 VCC 和 GND 最好直接从电源端引出,不要跟舵机、电机共用一个电源轨。舵机启动瞬间电流能到 1A 以上,会把电源电压拉低几百毫伏,HC-SR04 在这种供电下测距数据会跳得乱七八糟,看起来就像模块坏了。如果必须共用电源,至少要在模块的 VCC 和 GND 之间加一个 100uF 的电解电容做去耦,靠近模块引脚放置。
Echo 引脚输出的是 5V 高电平,如果你用的是 3.3V 供电的 MCU(比如某常见型号的 MCU 开发板),直接接 GPIO 会有烧毁风险。常见的做法是两个电阻分压,把 5V 分到 3.3V 以下。我一般用 1k 和 2k 电阻串联,Echo 先过 1k 电阻,然后在 2k 电阻两端取信号给 MCU,分压后大概是 3.3V,逻辑电平完全兼容。千万别偷懒直接连,我见过一个项目为了省两个电阻把 MCU 的 GPIO 烧了,后续排查花了大半天。
3.2 最小驱动代码:定时器捕获 Echo 高电平宽度
下面这段代码是驱动 HC-SR04 最基础的形式,假设你用的是标准库函数操作 GPIO。核心逻辑是先给 Trig 一个 10us 以上的高电平脉冲,然后等待 Echo 引脚拉高,拉高后开始计时,等到 Echo 拉低时停止计时。计时方式我用的是查询引脚电平配合微秒延时函数,虽然不如中断精确,但胜在简单直观,适合第一版验证:
// 定义引脚,以某常见 MCU 为例 #define TRIG_PIN GPIO_PIN_8 #define ECHO_PIN GPIO_PIN_9 void HC_SR04_Init(void) { // 配置 Trig 为推挽输出,Echo 为上拉输入 GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pin = TRIG_PIN; HAL_GPIO_Init(GPIOB, &gpio); gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; gpio.Pin = ECHO_PIN; HAL_GPIO_Init(GPIOB, &gpio); } uint32_t HC_SR04_Measure(void) { uint32_t startTime = 0; uint32_t endTime = 0; uint32_t timeout = 0; // 触发一次测距:Trig 拉高至少 10us HAL_GPIO_WritePin(GPIOB, TRIG_PIN, GPIO_PIN_SET); delay_us(20); HAL_GPIO_WritePin(GPIOB, TRIG_PIN, GPIO_PIN_RESET); // 等待 Echo 拉高,代表发射开始 while (HAL_GPIO_ReadPin(GPIOB, ECHO_PIN) == GPIO_PIN_RESET) { if (++timeout > 5000) return 0; // 超时保护,避免死等 } // 记录 Echo 高电平开始的时刻 startTime = GetTimerUs(); // 等待 Echo 拉低,代表回波接收完成 while (HAL_GPIO_ReadPin(GPIOB, ECHO_PIN) == GPIO_PIN_SET) { if (++timeout > 50000) return 0; // 超时说明没有回波 } endTime = GetTimerUs(); // 高电平持续时间就是往返时间 return endTime - startTime; }这段代码的核心逻辑是两次电平轮询,第一次等待 Echo 上升沿,第二次等待 Echo 下降沿,两次之间的时间差就是回波时间。有几个参数需要说明:Trig 高电平我给了 20us,比规格书要求的 10us 多了一倍,留出余量,实测更稳定;timeout 的上限值是根据最长测量距离算出来的,HC-SR04 标称 4 米,往返最远 8 米,对应时间大约是 23.5ms,所以等待回波超时设置成 50ms 足够,超过这个时间还等不到下降沿就判定为无回波。
3.3 把时间换算成距离:声速补偿从第一版就加上
拿到了高电平持续时间,下一步就是换算距离。如果直接除以 58 得到厘米数,那是网络上的简化写法,在常温下误差不大,但温度一变就露馅。我习惯把声速作为变量放进代码里,留出温度补偿接口:
float HC_SR04_GetDistanceCm(uint32_t echoTimeUs, float temperatureC) { // 声速公式:331.5 + 0.6 * 温度(单位 m/s) float speedOfSound = 331.5f + 0.6f * temperatureC; // 时间单位 us 转 s,距离 = 速度 * 时间 / 2 float distanceM = speedOfSound * echoTimeUs / 1000000.0f / 2.0f; return distanceM * 100.0f; }这个函数里声速公式是行业里常用的经验近似式,在 -20 到 50 度范围内精度足够。换算过程先转成米,再转成厘米,避免中间单位搞混。如果你不想引入温度传感器,可以把这个函数的温度参数固定成 25 度,误差在室内场景一般能接受。
4. 用输入捕获中断替代轮询:精度提升一个量级的关键
4.1 轮询的局限:中断引脚被占用时,测距就卡死
GPIO 轮询的方式在原型验证阶段够用,但有个致命缺陷是阻塞。等待 Echo 高电平结束的整个过程,MCU 都被这个 while 循环占住了,干不了别的事。而且如果你同时驱动一个 OLED 屏幕、处理几个按键,轮询的时序会被其他中断打乱,测出来的时间长度忽长忽短。更麻烦的是,有些 MCU 的 HAL 库函数内部有自己的超时机制,嵌套调用容易出问题,我调试某模拟项目X时就遇到过 HAL_Delay 和轮询循环互相干扰的情况。
这时候就该用定时器输入捕获了。原理是把 Echo 引脚接到定时器的捕获通道上,配置成上升沿捕获和下降沿捕获两次触发,定时器自动记录两次触发时的计数值,差值就是高电平持续时间。整个过程由硬件完成,不占用 CPU 时间,而且定时器的时钟频率通常在 1MHz 以上,分辨率比软件轮询高一个数量级。
4.2 输入捕获配置步骤:以某常见 MCU 定时器为例
以某常见 MCU 的通用定时器为例,Echo 引脚复用为定时器输入捕获通道,配置分成四步:开启定时器时钟和 GPIO 时钟、配置 GPIO 复用功能、配置定时器时基、配置捕获通道。下面是初始化代码:
// 假设使用 TIM2 的通道 1,对应引脚 PA0 void HC_SR04_Capture_Init(void) { // 1. 使能时钟 __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 2. 配置 PA0 为定时器输入功能 GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Alternate = GPIO_AF1_TIM2; HAL_GPIO_Init(GPIOA, &gpio); // 3. 配置定时器时基,计数频率 1MHz TIM_HandleTypeDef htim2 = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 84 - 1; // 84MHz 时钟分频到 1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFFFFFF; // 32 位计数器,不会溢出 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(&htim2); // 4. 配置输入捕获通道,上升沿触发 TIM_IC_InitTypeDef ic = {0}; ic.Channel = TIM_CHANNEL_1; ic.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; ic.ICSelection = TIM_ICSELECTION_DIRECTTI; ic.ICPrescaler = TIM_ICPSC_DIV1; ic.ICFilter = 0x0F; // 滤波,过滤毛刺 HAL_TIM_IC_ConfigChannel(&htim2, &ic); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); }这段初始化里最关键的两个参数是 Prescaler 和 Period。时钟频率 84MHz 分频 84 倍得到 1MHz 计数频率,每个计数周期是 1us,非常方便换算。Period 设置成 0xFFFFFFFF 是因为 TIM2 是 32 位定时器,不用担心溢出翻转。ICFilter 加了一级滤波,能滤掉 Echo 信号上的毛刺,避免误触发。
4.3 上升沿下降沿两次捕获:中断回调里完成时间差计算
捕获配置好之后,还需要在中断回调函数里区分是上升沿还是下降沿。第一次捕获上升沿记录起始时刻,第二次捕获下降沿记录结束时刻,两次相减就得到高电平宽度。代码实现如下:
volatile uint32_t capture_rise = 0; // 上升沿计数值 volatile uint32_t capture_fall = 0; // 下降沿计数值 volatile uint8_t capture_done = 0; // 完成标志 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { // 读取当前捕获值,并切换捕获极性 if (capture_rise == 0) { capture_rise = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 切换为下降沿捕获 TIM_OC2_PolarityConfig(TIM2, TIM_ICPOLARITY_FALLING); } else { capture_fall = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); capture_done = 1; // 切回上升沿,准备下一次 TIM_OC2_PolarityConfig(TIM2, TIM_ICPOLARITY_RISING); } } } } uint32_t HC_SR04_GetEchoTimeUs(void) { // 触发测距后等待 capture_done 置位 while (capture_done == 0) { // 这里可以处理其他任务 } uint32_t diff = capture_fall - capture_rise; capture_rise = 0; capture_fall = 0; capture_done = 0; return diff; // 单位微秒,因为计数频率 1MHz }这段代码的关键技巧是每次捕获后切换极性:上升沿记录起始值,然后马上配置成下降沿,让下一次捕获落在下降沿上,记录结束值。因为计数频率是 1MHz,计数器每 1us 加 1,所以两个计数值直接相减得到的就是高电平持续时间的微秒数,不需要额外换算。
参数上有个容易踩的坑:上升沿和下降沿之间的时间最长约 23.5ms,对应计数器数值约 23500,这个值远小于 32 位计数器的最大值,所以不需要担心计数器翻转到 0 的问题。但如果你在 8 位或 16 位定时器上做,就一定要处理溢出,否则距离突变到几百米就是计数器翻转导致的。
4.4 多次测量取中值:数据抖动不是玄学,是反射叠加
用输入捕获之后,单次测量的精度已经不错了,但实际场景中测出来的数据还是会跳动。这不是硬件玄学,而是超声波在真实环境里会发生多次反射:主波碰到障碍物返回,一部分声波被地面或墙壁反射后再回来,Echo 引脚可能捕获到的是二次反射回波。应对办法是连续测量多次,取中值或者平均值。我一般连续测 5 次,去掉最大值和最小值再取平均,这样能过滤掉大部分突变数据。
测量间隔也要注意。HC-SR04 单次测量周期大约需要 30ms 到 50ms,如果两次触发间隔太短,上一次的余振还没消散,下一次的测距结果会受到干扰。规格书上没有明确写这个最小间隔,我的经验是触发间隔不要小于 60ms,对应一秒钟最多测 16 次左右,对于绝大多数避障和测距场景足够用了。
5. HC-SR04 避坑指南:五个让我翻过车的高频问题
5.1 现象:上电后 Echo 引脚一直为高电平,程序卡死在等待下降沿
这是我第一次用 HC-SR04 时遇到的老大难问题。现象是模块上电后 Echo 引脚直接用万用表量到 3.3V 左右,触发也没有反应。排查了半小时,最后发现是模块的 VCC 接到了 3.3V 上。虽然 HC-SR04 手册标称工作电压 5V,但实际在 3.3V 下也能工作,只是性能下降,很多模块在 3.3V 下能测到近距离目标但测不远,而且 Echo 高电平幅度只有 3V 左右,某些 MCU 识别不了导致程序一直死等。后面我统一用 5V 供电,这个问题再没出现过。
5.2 现象:近距离测量数据反复横跳,2cm 以内数值乱变
把模块贴在墙面上,距离明明只有 2cm,测出来的数值却忽大忽小,甚至隔几秒出现一个一百多厘米的随机数。原因是 HC-SR04 的发射探头有机械余振,发射完 8 个脉冲后探头不会立刻停止振动,这种余振会被接收探头误当成回波。余振的持续时间大概对应 2cm 到 3cm 的距离,这个范围内检测到的回波并不可靠。解决办法是做好量程管理,小于 3cm 的数据直接丢弃或者标记为盲区。
5.3 现象:接 3.3V 单片机后,某个 GPIO 直接烧毁,芯片发热
冒烟现场大多数是因为 Echo 引脚直接连了 3.3V 单片机的 GPIO。Echo 输出高电平是 5V,3.3V 的引脚虽然标称耐压 5V,但长时间被 5V 电平灌入,内部保护二极管会导通,电流从 GPIO 引脚流进 VCC,时间一长引脚就废了。解决方法是加电阻分压,或者用逻辑电平转换芯片。我建议直接在 Echo 和 MCU 引脚之间串一个 1k 电阻,即使电压偏高也会被电阻限流,大部分场景下不会烧引脚。
5.4 现象:同一根总线上挂两个 HC-SR04,两个模块的数据都乱了
多模块组网时,如果两个模块同时被触发,超声波信号会在空气中互相干扰,A 模块发出的超声波可能被 B 模块的接收探头收到,测出来的距离完全不可信。解决方法是错开触发时间,保证同一时刻只有一个模块在测距。我一般把每个模块的触发时刻错开 100ms 以上,或者在软件里做成轮询式:先触发模块 1,等它测完,再触发模块 2。
5.5 现象:温度变化后系统测距误差越来越大,夏天和冬天差出 5cm
这是个容易被当成“设备老化”的问题,实际是声速随温度变化导致的系统性误差。0 度时声速约 331.5m/s,30 度时约 349.5m/s,变化了 5.4%,对应 2 米距离误差约 10cm。如果你的应用场景温度变化超过 10 度,一定要做温度补偿。低成本的做法是加一个温度传感器,比如用某款常见的数字温度传感器,读到的温度代入前面代码里的声速公式,成本增加不到两块钱,精度提升非常明显。
6. 进阶调试技巧:验证数据可信度的一套组合拳
6.1 用已知长度的障碍物标定测距系统
拿到一套新的 HC-SR04 模块后,先别急着焊进电路板,我习惯做一次简单的标定。找一面平整的墙,把模块用支架固定好,分别放在 20cm、50cm、100cm 的距离上,各测 20 次记录数据。如果 20 次测量值的极差超过 1cm,说明模块自身或者供电有问题,需要先解决硬件问题再做后续开发。这个操作看起来简单,但能帮你把“模块本身的问题”和“代码逻辑的问题”区分开,避免后面排查时两头怀疑。
6.2 示波器验证时序:Trig 和 Echo 的波形能说明一切
如果你手头有示波器,调试 HC-SR04 的效率会翻倍。把探头夹在 Trig 引脚上,观察触发脉冲的宽度和幅度是否符合 10us 以上的要求;再把探头移到 Echo 引脚,正常情况下应该看到一个干净的高电平脉冲,宽度对应距离。如果 Echo 信号上有明显的毛刺或者幅度衰减,说明模块周围有干扰源或者供电不足。Echo 高电平的上升沿和下降沿如果不陡峭,多半是上拉电阻阻值太大或者信号线上长了走线。
6.3 数据滤波的最终选择:滑动窗口中值滤波的工程实践
滤波算法我最终固定下来用的是滑动窗口中值滤波。每组测距数据连续采 11 次,排序后取第 6 个值作为输出。中值滤波对脉冲噪声的抑制效果比平均值好很多,平均滤波会把一次偶然的二次反射误差平均进来,导致整个输出值偏离真实距离,而中值滤波能直接丢掉那个离群值。代价是响应速度变慢,11 次测量需要大约 700ms,对应输出频率只有 1.4Hz 左右。如果对实时性有要求,可以减少窗口大小,但不要低于 5 次,否则滤波效果不明显。
6.4 集成测试时别忘了安装角度
最后提醒一个在集成阶段才会暴露的问题:模块的安装角度直接影响测量结果。如果探测面没有垂直于被测物体,超声波斜射过去回波路径变长,测出来的距离会比实际距离偏大。而且在斜射角度超过 15 度后,回波衰减严重,经常出现漏测。我在某跨平台系统的设备外壳设计里吃过这个亏,安装支架没做角度调整,量产了 50 台后现场反馈有一半设备测距异常,后来重新设计了支架才解决。所以原理验证阶段就要把安装角度问题考虑进去,用可调节的安装支架固定模块,别等结构件开模了再后悔。
HC-SR04 本身不贵,资料也满天飞,但真正把它用好需要把这些细节串起来。我自己的习惯是每拿到一个新批次模块,先跑一轮上面说的标定测试,记录当前环境的温度补偿参数,再决定用轮询还是输入捕获。测距这件事没有银弹,有的只是对时序的敬畏和对环境变量的控制。希望这篇笔记能帮你把 HC-SR04 用得明明白白,祝你在自己的项目里一次点亮、数据稳定。
本文还有配套的精品资源,点击获取