一、超声波测距传感器概述
超声波测距传感器是嵌入式项目中最常用的非接触式距离测量器件之一。它以成本低、结构简单、抗环境干扰能力强、在短距离范围内精度适中等特点,被广泛应用于倒车雷达、液位检测、避障小车、停车场车位检测、智能垃圾桶、无人机定高、工业物位测量等场景。对于嵌入式工程师而言,掌握超声波测距传感器的驱动方法,不仅是学习单片机外设编程的经典入门课,也是理解定时器、中断、输入捕获、GPIO 输出状态切换等核心概念的重要实战载体。
本节我们先从物理基础讲起,梳理超声波测距的基本概念、工作原理、常见型号以及选型时需要考虑的关键参数,为后续的硬件连接与代码实现打下基础。
1.1 什么是超声波
超声波是指频率高于 20000 Hz 的机械波。人耳能够听到的声音频率范围大约在 20 Hz 到 20 kHz 之间,超过这一上限的声波即为超声波。与普通声波一样,超声波在空气、水、固体等介质中传播时会发生反射、折射、衰减和衍射。超声波测距正是利用了超声波遇到障碍物会发生反射这一物理特性。
在空气中,超声波传播速度与温度密切相关。常温下的声速约为 340 m/s,更精确的经验公式为:
v ≈ 331.4 + 0.6 × T其中,T 为环境温度,单位为摄氏度。可以看到,当温度每变化 1 摄氏度时,声速大约变化 0.6 m/s。如果环境温度从 0 摄氏度变化到 30 摄氏度,声速会从约 331.4 m/s 变化到约 349.4 m/s,变化幅度接近 5.4%。对于测距精度要求较高的应用,进行温度补偿是很有必要的。
常见的超声波测距传感器工作频率为 40 kHz。选择 40 kHz 的原因在于:该频率远高于人耳可听范围,不会产生可闻噪声;同时 40 kHz 的超声波在空气中衰减适中,能够兼顾测距范围和换能器尺寸;此外,40 kHz 换能器制造工艺成熟、成本低廉,已经形成标准化产业链。
1.2 超声波测距的工作原理
超声波测距最常用的方法是脉冲回波法,也叫渡越时间法。其核心思想可以概括为四个步骤:发射、传播、反射、接收。
- 发射阶段:单片机通过一个 IO 口控制换能器驱动电路,向测距模块的 Trig 引脚发送一个持续 10 微秒以上的高电平触发脉冲。模块内部电路收到触发信号后,会自动产生一组 40 kHz 的方波信号,驱动发射换能器向外发射超声波。
- 传播阶段:超声波以约 340 m/s 的速度在空气中向前传播。如果传播路径上没有障碍物,声波将继续向前传播并逐渐衰减;如果遇到障碍物,声波会发生反射。
- 反射阶段:障碍物表面的材质、形状、角度会影响反射效果。对于平整、坚硬、垂直于声波传播方向的表面,反射效果最好;对于柔软、多孔、倾斜的表面,部分声波会被吸收或散射,回波强度会明显减弱。
- 接收阶段:反射回来的声波被接收换能器捕获,模块内部处理电路将其转换为一个高电平回响信号,并通过 Echo 引脚输出。单片机测量 Echo 引脚高电平的持续时间,即可根据声速和传播时间计算出障碍物距离。
距离计算公式如下:
距离 = 声速 × 时间 ÷ 2除以 2 的原因是超声波实际走过的是往返路程,即从发射换能器到障碍物,再从障碍物反射回接收换能器。假设 Echo 高电平持续时间为 t,声速为 v,则障碍物与传感器的距离 d 可以表示为:
d = v × t ÷ 2如果使用常温声速 340 m/s,且时间 t 以微秒为单位,则距离换算公式可以进一步简化为:
d(cm) = t(us) × 0.017或者写成:
d(cm) = t(us) ÷ 58这两个公式在嵌入式项目中非常常用。理解它们的推导过程,有助于在实际调试中快速判断测量结果是否合理。
1.3 常见型号对比
市面上的超声波测距模块种类较多,但绝大多数都基于 40 kHz 换能器设计,区别主要体现在供电电压、测量范围、输出接口、安装方式和抗干扰能力等方面。下表列出了几种常见的超声波测距模块及其主要参数对比。
| 型号 | 工作电压 | 测量范围 | 输出方式 | 典型应用 |
|---|---|---|---|---|
| HC-SR04 | 5V DC | 2cm–400cm | GPIO 电平 | 教学、避障、倒车雷达 |
| HC-SR05 | 5V DC | 2cm–450cm | GPIO 电平 | 同 HC-SR04,带模式切换 |
| US-015 | 5V DC | 2cm–400cm | GPIO 电平 | 高精度短距离测量 |
| US-100 | 3.3V–5V | 2cm–450cm | GPIO 电平/UART | 带温度补偿的应用 |
| JSN-SR04T | 3.0V–5.5V | 20cm–600cm | GPIO 电平 | 防水、户外、液位检测 |
| AJ-SR04M | 3.0V–5.5V | 20cm–800cm | GPIO 电平 | 远距离检测、工业应用 |
其中 HC-SR04 是最经典、最常见、资料最多的型号,本文的硬件连接、时序分析和代码示例都将以 HC-SR04 为核心展开。对于其他型号,驱动思路基本一致,只需要根据具体数据手册调整供电电压、时序要求和测量范围。
1.4 选型注意事项
在项目中选择超声波测距传感器时,需要重点关注以下几个方面。
- 供电电压:经典 HC-SR04 通常要求 5V 供电,如果使用 3.3V 单片机系统,需要确认模块是否支持 3.3V 工作,或者增加电平转换电路。部分模块如 US-100、JSN-SR04T 支持宽电压输入,便于 3.3V 系统直接使用。
- 测量范围:不同模块的盲区和最大量程不同。HC-SR04 的盲区约为 2cm,即小于 2cm 的距离无法可靠测量。最大量程与换能器功率、接收灵敏度以及环境噪声有关,实际工程中建议留有余量。
- 波束角:超声波换能器的声波并非理想的细窄波束,而是具有一定的发散角。HC-SR04 的典型波束角约为 15 度到 30 度。波束角越大,越容易检测到偏离正前方的物体,但也越容易引入旁瓣干扰。在避障场景中,需要根据实际波束特性合理布置传感器。
- 环境适应性:开放结构模块不适合在潮湿、多尘、腐蚀性环境中长期工作。对于液位检测、户外应用,应选择带防水外壳的型号,如 JSN-SR04T。
- 输出接口:大多数模块采用 Trig 和 Echo 两个数字引脚,便于各种单片机驱动。部分模块还提供 UART、PWM 或模拟电压输出,可以根据系统资源选择。
二、HC-SR04 传感器详解
HC-SR04 是目前最流行的低成本超声波测距模块。它集成度高、接线简单、资料丰富,非常适合作为超声波测距入门的第一选择。深入理解 HC-SR04 的硬件结构和电气特性,能够帮助工程师在实际调试中快速定位问题。
2.1 硬件结构
HC-SR04 模块由以下几个主要部分组成。
- 发射换能器 T:负责将电信号转换为 40 kHz 的超声波信号并向外辐射。模块上通常标注为 T 或 TX。
- 接收换能器 R:负责接收反射回来的超声波信号,并将其转换为微弱电信号。模块上通常标注为 R 或 RX。
- 控制与信号处理电路:模块内部包含一颗专用控制芯片,用于接收 Trig 触发信号、产生 40 kHz 驱动波形、放大并处理回波信号,最终通过 Echo 引脚输出高电平脉冲。
- 晶体振荡器:为控制芯片提供稳定的时钟基准,保证 40 kHz 信号频率的准确性。
从外观上看,HC-SR04 模块有四个引脚,排列为 VCC、Trig、Echo、GND。模块背面通常采用四颗电阻或排针引出接口,安装方式灵活,可以直接焊线,也可以插在标准 2.54mm 排座上。
2.2 引脚定义
HC-SR04 的引脚定义如下表所示。
| 引脚 | 名称 | 方向 | 说明 |
|---|---|---|---|
| 1 | VCC | 电源 | 接 5V 直流电源正极 |
| 2 | Trig | 输入 | 触发信号输入,单片机输出 10us 以上高电平 |
| 3 | Echo | 输出 | 回响信号输出,高电平持续时间与距离成正比 |
| 4 | GND | 电源 | 接电源负极,与单片机共地 |
接线时必须保证模块 GND 与单片机 GND 共地,否则 Trig 和 Echo 信号没有共同的参考电平,无法正常工作。对于使用多路独立电源的系统,这一点尤其重要,往往是被忽略但又极易导致通信异常的细节。
2.3 电气特性
HC-SR04 的典型电气参数如下。
- 工作电压:5V DC
- 静态工作电流:小于 2mA
- 工作电流:约 15mA
- 触发信号:10us 以上的 TTL 高电平
- 回响信号:TTL 高电平,脉宽范围约 115us 到 18ms
- 探测角度:约 15 度到 30 度
- 测量范围:2cm 到 400cm
- 测量精度:约 0.3cm 到 0.5cm
- 工作频率:40 kHz
需要注意,HC-SR04 的 Echo 引脚在无回波时输出低电平,有回波时输出高电平。Echo 引脚输出的高电平电压接近 VCC。当 VCC 为 5V 时,Echo 高电平约为 5V,因此在连接 3.3V 单片机时,需要评估单片机 IO 是否支持 5V 容忍。对于不支持 5V 输入的 3.3V 单片机,应在 Echo 与单片机之间加入分压电阻或电平转换电路。
2.4 工作时序
HC-SR04 的一次完整测距流程可以分为以下四个阶段。
- 触发阶段:单片机向 Trig 引脚输出一个持续至少 10us 的高电平脉冲。模块检测到该脉冲后,内部开始产生 8 个周期的 40 kHz 方波信号。
- 发射阶段:发射换能器将 8 个周期的 40 kHz 方波转换为超声波信号向空气中发射。发射过程持续约 200us。
- 等待阶段:模块发射完成后,自动将 Echo 引脚拉高,并开始等待回波。如果在量程范围内没有检测到回波,Echo 引脚会在超时后自动拉低。HC-SR04 的超时时间大约为 38ms,对应最大量程约 400cm。
- 回波阶段:接收换能器检测到反射回的超声波后,模块内部处理电路立即将 Echo 引脚拉低。Echo 高电平持续时间即对应声波往返时间。
在编写驱动程序时,最常见的问题就是没有正确理解 Echo 高电平的含义。Echo 高电平从发射完成开始,到接收到回波结束,持续时间与距离成正比。程序需要测量的是 Echo 高电平的脉宽,而不是从 Trig 触发到 Echo 拉高的时间间隔。
另外,两次测距之间应留有一定的间隔。由于 HC-SR04 在发射后会等待回波,如果在上一轮测距尚未完成时就再次触发,可能导致 Echo 信号混乱。工程上通常建议测距周期不小于 60ms,即测距频率不超过约 16Hz。对于远距离测量,还需要根据最大量程计算更保守的周期。例如,测量 400cm 时,声波往返时间约为 23.5ms,加上模块内部处理时间,建议测距周期至少为 60ms 到 100ms。
三、测距原理与时序分析
理解了 HC-SR04 的基本结构后,我们需要进一步从信号时序和物理传播的角度,深入分析测距过程中的关键细节。这些知识将直接影响后续代码中定时器参数的计算和滤波算法的设计。
3.1 声波传播与回波特性
超声波在空气中的传播可以近似看作球面波扩散。换能器发射的声波能量会随着传播距离的增加而逐渐衰减。衰减主要有三方面原因:一是几何扩散,声波波前面积随距离平方增长,单位面积上的能量随之下降;二是空气吸收,声波在空气中传播时,部分机械能会转化为热能;三是障碍物吸收和散射,柔软多孔的表面会吸收大量声能,粗糙或不规则表面会将声波散射到其他方向。
因此,回波信号的强弱与障碍物的距离、材质和角度密切相关。近距离、硬质、平整、垂直于声波方向的障碍物回波最强;远距离、软质、倾斜的障碍物回波最弱,甚至无法被检测到。在避障小车等移动应用中,如果障碍物表面为吸声材料或与声波方向夹角过大,可能出现“检测不到障碍物”的情况,这并不意味着传感器损坏,而是回波强度不足。
3.2 Echo 信号的时序细节
从单片机视角看,一次典型测距的 Timing 图如下。
Trig: ___|------|_____________________________________________ >=10us Echo: |______________| |________ 高电平脉宽 t ____________|单片机输出触发脉冲后,Trig 回到低电平。模块在发射完 8 个周期的超声波后,将 Echo 拉高。注意,Echo 并不是在 Trig 上升沿时立即拉高,而是在模块内部完成发射后才拉高。对于 HC-SR04,从 Trig 触发到 Echo 拉高通常有约 200us 到 500us 的延迟,这段时间就是模块内部产生并发射 8 个周期 40 kHz 信号的时间。
当回波到达时,Echo 拉低。因此,Echo 高电平脉宽 t 严格对应“超声波从发射完成到回波到达”的往返传播时间。由于 8 个发射周期约 200us,对应的声波传播距离约为 3.4cm,对于大多数应用场景,这个固定的发射延迟对测量精度影响很小,可以直接忽略。但在近距离高精度测量中,可以通过标定零点进行补偿。
3.3 距离计算公式推导
设 Echo 高电平脉宽为 t,单位为微秒;声速为 v,单位为米每秒;距离为 d,单位为厘米。声波从发射到接收的往返距离为 2d,因此有:
2d = v × t d = v × t / 2当 v = 340 m/s 时,将单位统一为厘米和微秒:
340 m/s = 34000 cm/s = 0.034 cm/us因此:
d = 0.034 × t / 2 = 0.017 × t即距离厘米数约等于 Echo 高电平微秒数乘以 0.017。反过来,如果程序中使用整数运算,可以将公式改写为:
d = t / 58因为 1 / 0.017 ≈ 58.8。在嵌入式程序中,除以 58 比乘以 0.017 更容易用整数运算实现,但会引入约 1.4% 的量化误差。对于精度要求不高的场景,这两种方法均可接受。在后续代码示例中,我们会同时展示浮点运算和整数运算两种写法。
3.4 温度补偿
声速随温度变化,若环境温度偏离 20 摄氏度较多,测距误差会变得明显。假设温度计测得的温度为 T,则声速近似为:
v = 331.4 + 0.6 × T (m/s)换算为厘米每微秒:
v = 0.03314 + 0.00006 × T (cm/us)则距离公式为:
d = (0.03314 + 0.00006 × T) × t / 2在程序中可以这样实现:
float distance_cm = (0.03314f + 0.00006f * temperature) * pulse_us / 2.0f;对于大多数室内教学和实验项目,温度变化不大,可以不进行温补。但在室外车辆倒车雷达、工业储罐液位检测等场景中,温度可能从零下到四十多度变化,建议增加温度传感器并进行软件补偿。部分模块如 US-100 内部集成了温度传感器,可以直接读取温度补偿后的距离值。
四、STM32 驱动实现
STM32 是当前嵌入式开发中使用最广泛的单片机系列之一,其丰富的外设资源为实现高性能超声波测距提供了多种方案。本节以 STM32F103C8T6 为例,使用 HAL 库,逐步讲解 GPIO 轮询、外部中断、定时器输入捕获三种驱动方式,并给出完整可编译的示例代码。
4.1 硬件连接
HC-SR04 与 STM32F103C8T6 的推荐连接如下表所示。
| HC-SR04 引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| VCC | 5V | 连接 5V 电源 |
| GND | GND | 共地 |
| Trig | PA0 | GPIO 输出,产生触发脉冲 |
| Echo | PA1 | GPIO 输入/定时器输入捕获 |
硬件连接时有两个关键点需要注意。
- 电平匹配:STM32F103 系列的 GPIO 引脚多数支持 5V 容忍特性,但在使用前务必查看数据手册确认。如果所用引脚不支持 5V 容忍,需要在 Echo 与单片机之间加入分压电阻,例如通过 1kΩ 和 2kΩ 电阻分压,将 5V 信号降为约 3.3V。
- 电源去耦:HC-SR04 在工作瞬间会产生一定的电流波动,建议在模块 VCC 与 GND 之间并联一个 100uF 电解电容和一个 0.1uF 瓷片电容,以减小电源纹波,提高测量稳定性。
4.2 CubeMX 配置
第一种方案使用 GPIO 轮询方式,配置最简单。在 STM32CubeMX 中按以下步骤配置。
- 选择芯片型号 STM32F103C8T6,配置系统时钟为最高主频 72MHz。
- 将 PA0 配置为 GPIO_Output,标签为 TRIG;将 PA1 配置为 GPIO_Input,标签为 ECHO。
- 如果使用串口调试输出距离,可额外配置 USART1 用于 printf 重定向。
- 使能定时器 TIM2,作为微秒级延时和脉宽测量的时基。TIM2 预分频设为 71,这样定时器计数频率为 1MHz,即每个计数值对应 1us。
- 生成代码,选择 MDK-ARM 或 STM32CubeIDE 工具链。
4.3 GPIO 轮询方式测距
GPIO 轮询方式的核心思想是:拉高 Trig 触发,然后在一个循环中不断检测 Echo 引脚电平,当 Echo 从低变高时启动计数,当 Echo 从高变低时停止计数。计数可以通过定时器或简单的软件循环实现。
软件循环计数的优点是代码简单、不依赖复杂外设,缺点是 CPU 占用高且精度受指令周期影响。定时器计数的优点是精度高、CPU 可稍微解放,但逻辑略复杂。下面先给出基于 TIM2 微秒计时的 GPIO 轮询版本。
#include "main.h" extern TIM_HandleTypeDef htim2; #define TRIG_PORT GPIOA #define TRIG_PIN GPIO_PIN_0 #define ECHO_PORT GPIOA #define ECHO_PIN GPIO_PIN_1 void delay_us(uint32_t us) { __HAL_TIM_SET_COUNTER(&htim2, 0); HAL_TIM_Base_Start(&htim2); while (__HAL_TIM_GET_COUNTER(&htim2) < us) { } HAL_TIM_Base_Stop(&htim2); } float hcsr04_get_distance_cm(void) { uint32_t echo_start_us = 0; uint32_t echo_end_us = 0; uint32_t timeout_us = 30000; HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); delay_us(2); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); __HAL_TIM_SET_COUNTER(&htim2, 0); HAL_TIM_Base_Start(&htim2); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_RESET) { if (__HAL_TIM_GET_COUNTER(&htim2) > timeout_us) { HAL_TIM_Base_Stop(&htim2); return -1.0f; } } echo_start_us = __HAL_TIM_GET_COUNTER(&htim2); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET) { if (__HAL_TIM_GET_COUNTER(&htim2) > timeout_us) { HAL_TIM_Base_Stop(&htim2); return -1.0f; } } echo_end_us = __HAL_TIM_GET_COUNTER(&htim2); HAL_TIM_Base_Stop(&htim2); uint32_t pulse_us = echo_end_us - echo_start_us; if (pulse_us == 0) { return -1.0f; } float distance_cm = pulse_us * 0.017f; return distance_cm; }这段代码的工作流程如下。首先将 Trig 拉低 2us 使模块复位,然后拉高 10us 产生触发脉冲,再拉低。触发完成后,程序循环等待 Echo 引脚变为高电平。如果等待超过 30ms,说明没有收到回波起点,返回 -1 表示测距失败。检测到 Echo 高电平后记录起始时刻,继续循环等待 Echo 变为低电平,同样带有 30ms 超时保护。最后用脉宽乘以 0.017 得到距离。
GPIO 轮询方式的优点是无须配置中断和输入捕获,代码直观易懂。缺点是阻塞时间长,一次测距可能占用几十毫秒的 CPU 时间,不适合多任务实时系统。因此,在需要同时处理多路传感器或执行其他实时任务的场景中,推荐使用中断方式。
4.4 外部中断配合定时器方式
外部中断方式的思想是:将 Echo 引脚连接到具有中断能力的 GPIO,当 Echo 上升沿到来时触发中断并记录时间;当 Echo 下降沿到来时再次触发中断并记录时间,两次时间差即为脉宽。这种方式下,CPU 不必在等待循环中空转,可以执行其他任务。
在 CubeMX 中将 PA1 配置为 GPIO_EXTI1,触发方式选择上升沿和下降沿均触发。同时配置一个 1MHz 的定时器 TIM2 作为自由运行计数器。完整代码如下。
#include "main.h" extern TIM_HandleTypeDef htim2; #define TRIG_PORT GPIOA #define TRIG_PIN GPIO_PIN_0 volatile uint32_t echo_rise_us = 0; volatile uint32_t echo_fall_us = 0; volatile uint8_t echo_valid = 0; void hcsr04_trigger(void) { HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); __HAL_TIM_SET_COUNTER(&htim2, 0); HAL_TIM_Base_Start(&htim2); HAL_Delay(1); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_1) { if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_SET) { echo_rise_us = __HAL_TIM_GET_COUNTER(&htim2); echo_valid = 0; } else { echo_fall_us = __HAL_TIM_GET_COUNTER(&htim2); if (echo_fall_us > echo_rise_us) { echo_valid = 1; } } } } float hcsr04_get_distance_cm(void) { hcsr04_trigger(); HAL_Delay(60); if (echo_valid) { uint32_t pulse_us = echo_fall_us - echo_rise_us; echo_valid = 0; return pulse_us * 0.017f; } return -1.0f; }使用时,在主循环中周期性调用hcsr04_get_distance_cm()。函数触发一次测距后等待 60ms,再检查全局标志echo_valid是否有效。如果有效,返回计算的距离;否则返回 -1。注意,Echo 引脚的中断回调会在中断上下文中执行,因此应尽量保持回调函数短小,不要在其中进行浮点运算、串口打印等耗时操作。
4.5 定时器输入捕获方式
定时器输入捕获方案是最专业、最精确的测量方法。STM32 的高级定时器和通用定时器都支持输入捕获功能,可以自动记录外部信号沿发生的时刻,无需软件干预。以 TIM3 的通道 1 为例,将 Echo 连接至 PA6,配置 TIM3_CH1 为输入捕获,采用上升沿和下降沿双沿捕获。
CubeMX 配置要点如下。
- 将 PA6 配置为 TIM3_CH1。
- 激活 TIM3 的 Input Capture direct mode,选择 Channel1,配置为上升沿触发。
- 设置 TIM3 预分频为 71,使计数频率为 1MHz,即 1us 分辨率。
- 自动重装载值设为 0xFFFF,即 65535us 一个计数周期,足够覆盖 400cm 量程。
- 开启 TIM3 全局中断,用于在捕获事件中处理数据。
完整输入捕获驱动代码如下。
#include "main.h" extern TIM_HandleTypeDef htim3; #define TRIG_PORT GPIOA #define TRIG_PIN GPIO_PIN_0 volatile uint32_t ic_val1 = 0; volatile uint32_t ic_val2 = 0; volatile uint8_t ic_first_edge = 0; volatile uint8_t ic_capture_done = 0; void hcsr04_trigger(void) { HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); ic_first_edge = 0; ic_capture_done = 0; HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1); } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != TIM3) { return; } uint32_t capture_val = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (ic_first_edge == 0) { ic_val1 = capture_val; ic_first_edge = 1; __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else { ic_val2 = capture_val; ic_capture_done = 1; HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1); } } float hcsr04_get_distance_cm(void) { hcsr04_trigger(); HAL_Delay(60); if (ic_capture_done) { uint32_t pulse_us = 0; if (ic_val2 > ic_val1) { pulse_us = ic_val2 - ic_val1; } else { pulse_us = 0xFFFF - ic_val1 + ic_val2 + 1; } return pulse_us * 0.017f; } return -1.0f; }输入捕获方式的关键在于中断回调中的极性切换。当捕获到上升沿后,记录计数值并将捕获极性切换为下降沿;当捕获到下降沿后,停止捕获并置完成标志。主程序读取标志并计算脉宽。输入捕获的优点是测量精度高、CPU 负担小,适合多路传感器和实时系统。缺点是配置相对复杂,且每个 Echo 通道需要占用一个定时器通道。
4.6 多路超声波测量
当项目中需要多个超声波传感器时,可以将多个 Echo 分别连接到不同定时器的输入捕获通道,或者使用一个定时器的多个通道。例如,TIM3 有 4 个通道,TIM2 也有 4 个通道,理论上可以同时测量多路超声波。
对于多路测量,需要注意避免同时触发多个传感器导致声波相互干扰。推荐的调度策略是轮流触发,每次只让一个模块发射,等待该模块测距完成后再触发下一个。假设使用 4 路 HC-SR04,每路测距周期为 60ms,则总采样率约为 4Hz,对于大多数应用是足够的。如果对实时性要求更高,可以让错开安装的模块同时发射,利用方向差异降低串扰,但实现和调试难度会明显增加。
五、Arduino 驱动实现
Arduino 平台凭借简单的编程模型和丰富的社区库,成为快速验证超声波测距方案的热门选择。本节介绍 Arduino 平台的基础驱动、NewPing 库的使用以及多传感器管理方法。
5.1 基础驱动:pulseIn 实现
Arduino 提供了pulseIn()函数,可以方便地测量引脚高电平或低电平的持续时间。利用pulseIn()驱动 HC-SR04 的代码如下。
const int trigPin = 9; const int echoPin = 10; void setup() { Serial.begin(115200); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); } void loop() { digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); unsigned long duration = pulseIn(echoPin, HIGH, 30000); if (duration == 0) { Serial.println("Out of range"); } else { float distance = duration * 0.034f / 2.0f; Serial.print("Distance: "); Serial.print(distance); Serial.println(" cm"); } delay(100); }这段代码中,pulseIn(echoPin, HIGH, 30000)会等待 Echo 引脚变为高电平,然后测量高电平持续时间,最长等待 30000 微秒即 30ms。如果超时返回 0,表示超出量程或者未检测到回波。距离计算使用公式duration * 0.034 / 2,其中 0.034 对应声速 340m/s 换算为 cm/us 的系数。
pulseIn()方式的优点是代码极简,适合快速原型验证。缺点是阻塞式测量,在等待 Echo 期间 Arduino 无法执行其他任务,且测量精度受 Arduino 底层时钟影响,通常在项目中够用。
5.2 NewPing 库的使用
NewPing 是 Arduino 平台非常流行的超声波传感器库,由 Tim Eckel 开发。它优化了测量时序,支持多传感器同时测量,并且提供了中值滤波等实用功能。使用 NewPing 库前,需要先在 Arduino IDE 的库管理器中搜索并安装 NewPing。
基础使用示例:
#include <NewPing.h> #define TRIGGER_PIN 9 #define ECHO_PIN 10 #define MAX_DISTANCE 400 NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); void setup() { Serial.begin(115200); } void loop() { delay(50); unsigned int distance = sonar.ping_cm(); if (distance == 0) { Serial.println("Out of range"); } else { Serial.print("Distance: "); Serial.print(distance); Serial.println(" cm"); } }NewPing 库的核心优势在于:
- 内部使用微秒级定时,精度高于简单的
pulseIn(); - 支持
ping_cm()、ping_in()、ping_median()等多种测量函数; - 多个传感器对象的测量可以通过
ping_timer()实现异步调度,避免阻塞; - 自动处理超时,避免程序因串扰或异常长时间卡死。
对于需要同时驱动 3 个以上传感器的 Arduino 项目,NewPing 是推荐选择。
5.3 多传感器异步测量
NewPing 的ping_timer()方法支持非阻塞测量。其原理是:调用ping_timer()只启动一次测量,不会等待回波;之后通过check_timer()检查测量是否完成,再读取结果。示例代码如下。
#include <NewPing.h> #define SONAR_NUM 3 #define MAX_DISTANCE 400 #define PING_INTERVAL 33 NewPing sonar[SONAR_NUM] = { NewPing(2, 3, MAX_DISTANCE), NewPing(4, 5, MAX_DISTANCE), NewPing(6, 7, MAX_DISTANCE) }; unsigned long pingTimer[SONAR_NUM]; unsigned int distances[SONAR_NUM]; uint8_t currentSensor = 0; void setup() { Serial.begin(115200); for (uint8_t i = 0; i < SONAR_NUM; i++) { pingTimer[i] = millis() + 75 * i; } } void loop() { for (uint8_t i = 0; i < SONAR_NUM; i++) { if (millis() >= pingTimer[i]) { pingTimer[i] += PING_INTERVAL * SONAR_NUM; if (i == 0 && currentSensor == SONAR_NUM - 1) { for (uint8_t j = 0; j < SONAR_NUM; j++) { Serial.print(distances[j]); Serial.print(" "); } Serial.println(); } sonar[currentSensor].timer_stop(); currentSensor = i; distances[currentSensor] = 0; sonar[currentSensor].ping_timer(echoCheck); } } } void echoCheck() { if (sonar[currentSensor].check_timer()) { distances[currentSensor] = sonar[currentSensor].ping_result / US_ROUNDTRIP_CM; } }这段代码实现了一个 3 传感器轮询调度系统。每个传感器分配一个独立的定时触发时刻,间隔错开以避免相互干扰。回调函数echoCheck()在回波完成时被调用,将测量结果换算为厘米并存入数组。主循环不断检查定时器状态,同时保持对其他任务的响应能力。
六、51 单片机驱动实现
51 单片机是国内高校教学和早期工控项目中使用极为广泛的经典架构。尽管其性能和资源远不如 STM32,但对于超声波测距这样的低速应用,51 单片机完全能够胜任。本节以 STC89C52 为例,使用 C51 语言实现超声波测距驱动。
6.1 硬件连接
STC89C52 与 HC-SR04 的连接如下。
| HC-SR04 引脚 | STC89C52 引脚 |
|---|---|
| VCC | 5V |
| GND | GND |
| Trig | P1.0 |
| Echo | P1.1 |
51 单片机工作电压通常为 5V,与 HC-SR04 电平完全兼容,因此不需要电平转换。如果使用 3.3V 的低功耗 51 型号,则需要评估 Echo 引脚的 5V 输入是否安全。
6.2 定时器与计数器配置
STC89C52 具有两个 16 位定时器/计数器 T0 和 T1。我们可以使用 T0 的定时模式来测量 Echo 高电平脉宽。51 单片机传统工作频率为 12MHz,一个机器周期为 1us,因此 T0 在不分频时每个计数周期为 1us,直接对应微秒计数,非常方便。
测量思路与 STM32 类似:触发 Trig 后,等待 Echo 变高,启动 T0 计数;等待 Echo 变低,停止 T0 计数,读出计数值即为高电平微秒数。
6.3 完整驱动代码
#include <reg52.h> #include <intrins.h> sbit TRIG = P1^0; sbit ECHO = P1^1; unsigned int distance_cm = 0; void delay_us(unsigned int us) { while (us--) { nop(); } } void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; TH0 = 0; TL0 = 0; TR0 = 0; } void hcsr04_trigger(void) { TRIG = 0; delay_us(2); TRIG = 1; delay_us(12); TRIG = 0; } unsigned int hcsr04_measure(void) { unsigned int time_us = 0; unsigned int timeout = 0; hcsr04_trigger(); timeout = 0; while (ECHO == 0) { timeout++; if (timeout > 60000) { return 0; } } TH0 = 0; TL0 = 0; TR0 = 1; while (ECHO == 1) { timeout++; if (timeout > 60000) { TR0 = 0; return 0; } } TR0 = 0; time_us = (TH0 << 8) | TL0; if (time_us == 0) { return 0; } distance_cm = time_us / 58; return distance_cm; } void main(void) { timer0_init(); while (1) { hcsr04_measure(); delay_ms(60); } }51 单片机代码中有一个细节需要注意。标准 51 单片机在 12MHz 晶振下,12 个振荡周期为一个机器周期,因此 T0 每 1us 计数一次。但如果使用 11.0592MHz 晶振,一个机器周期约为 1.085us,此时所得计数值需要乘以 1.085 进行修正。很多初学者在更换晶振后发现测距值偏小,正是没有意识到机器周期变化。
此外,51 单片机的while循环等待方式同样是阻塞式的。对于需要同时驱动数码管显示、按键扫描等任务的系统,可以考虑将测距放入中断中,或使用软件状态机实现非阻塞调度。
七、滤波与数据处理
直接读取单次测距值往往会受到环境噪声、电路抖动和回波不稳定等因素影响,导致数据波动。为了获得平滑、可靠的测量结果,通常需要对原始数据进行滤波处理。本节介绍中值滤波、滑动平均滤波、卡尔曼滤波以及野值剔除等常用方法,并分析各自适用场景。
7.1 中值滤波
中值滤波的原理是对连续采样的 N 个数据进行排序,取中间值作为本次输出。中值滤波对脉冲型干扰具有非常好的抑制效果,尤其适合消除偶发的异常尖峰。N 通常取奇数,如 5、7、9。
中值滤波的 C 语言实现如下。
#define MEDIAN_N 5 float median_filter(float *buf, int n) { float temp[MEDIAN_N]; for (int i = 0; i < n; i++) { temp[i] = buf[i]; } for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { if (temp[i] > temp[j]) { float t = temp[i]; temp[i] = temp[j]; temp[j] = t; } } } return temp[n / 2]; }中值滤波的缺点是排序运算在数据量较大或采样频率较高时会占用较多 CPU 时间,且 N 越大,引入的延迟越大。对于超声波测距,一般取 N 为 5 到 9 即可。
7.2 滑动平均滤波
滑动平均滤波也叫递推平均滤波,它维护一个长度为 N 的环形缓冲区,每次加入新数据并移除最旧数据,然后计算缓冲区所有数据的平均值。滑动平均滤波对周期性干扰和随机噪声有较好的平滑作用,输出曲线更连续。
#define AVG_N 8 float avg_buf[AVG_N]; int avg_index = 0; int avg_count = 0; float avg_sum = 0.0f; float moving_average_filter(float new_value) { avg_sum -= avg_buf[avg_index]; avg_buf[avg_index] = new_value; avg_sum += new_value; avg_index = (avg_index + 1) % AVG_N; if (avg_count < AVG_N) { avg_count++; } return avg_sum / (float)avg_count; }滑动平均的平滑效果与 N 大小成反比,N 越大越平滑,但响应速度越慢。在避障小车等需要快速响应的场景中,N 不宜过大,一般取 4 到 8。在液位检测等变化缓慢的场景中,可以取 10 到 20。
7.3 卡尔曼滤波
卡尔曼滤波是一种基于状态空间模型的最优估计方法,能够根据系统模型和观测噪声动态调整滤波增益,在平滑噪声的同时保持较快的响应速度。对于超声波测距,可以建立一维匀速或近似静止的位置模型。
一维卡尔曼滤波的简化实现如下。
typedef struct { float x; float p; float q; float r; float k; } kalman1d_t; void kalman1d_init(kalman1d_t *kf, float q, float r, float init_value) { kf->x = init_value; kf->p = 1.0f; kf->q = q; kf->r = r; kf->k = 0.0f; } float kalman1d_update(kalman1d_t *kf, float measurement) { kf->p = kf->p + kf->q; kf->k = kf->p / (kf->p + kf->r); kf->x = kf->x + kf->k * (measurement - kf->x); kf->p = (1.0f - kf->k) * kf->p; return kf->x; }使用卡尔曼滤波时需要调节过程噪声方差 q 和观测噪声方差 r。q 越大,滤波器越信任测量值,响应越快但平滑效果变差;r 越大,滤波器越信任模型预测,输出越平滑但滞后越明显。实际调参时可以先固定 r 为传感器实测方差,再逐步调整 q 直至达到满意的响应与平滑平衡。
7.4 野值剔除策略
在超声波测距中,偶尔会出现明显偏离真实值的测量结果,例如回波来自地板、墙壁或其他传感器的串扰。这些点称为野值。简单的处理策略如下。
- 范围限制:根据传感器量程和应用场景,设定有效距离范围,如 2cm 到 400cm。超出范围的测量值直接丢弃。
- 突变检测:将当前测量值与上一有效值比较,如果变化超过预设阈值(如 20cm),则认为当前值为野值,保留上一有效值。
- 聚类投票:连续多次测量后,检查是否存在一组接近的值,以多数一致的值作为可信结果。
实际工程中,通常将范围限制与中值滤波、滑动平均组合使用,先剔除明显野值,再进行平滑处理,可获得稳定的测量输出。
八、多传感器应用
许多实际项目需要同时使用多个超声波传感器。例如,小车避障通常在前、左、右三个方向各安装一个;倒车雷达则可能使用四个传感器覆盖车尾不同区域。多传感器应用带来了新的工程挑战,主要是触发调度和串扰抑制。
8.1 多路复用与调度策略
最简单的多路方案是独立测量,每个传感器使用各自的 Trig 和 Echo 引脚,软件依次触发并等待。这种方式逻辑清晰,但总采样率会随着传感器数量增加而下降。对于 4 路传感器,每路 60ms,完整扫描一轮需要 240ms,对应约 4Hz 的刷新率。
为了减少等待时间,可以采用“发射后立即处理其他任务”的异步方式,或者利用不同传感器的空间隔离进行错峰发射。例如,在环形布置的系统中,相隔最远的两个传感器同时发射,可以降低串扰概率,同时将总扫描时间缩短一半。
8.2 串扰的成因与抑制
串扰是指一个传感器的回波被另一个传感器的接收端误检。由于超声波换能器的波束角和接收灵敏度有限,串扰通常发生在传感器距离较近、发射方向平行或对射的情况下。抑制串扰的常见方法包括:
- 时分隔离:确保任意时刻只有一个传感器处于发射和接收状态,这是最有效的方法。
- 频率区分:使用不同中心频率的模块,如 40kHz 和 58kHz,但成本和选择范围受限。
- 物理隔离:在传感器之间增加吸音材料或隔板,并调整安装角度,减少声波重叠。
- 软件过滤:根据回波到达时间判断是否可能来自其他传感器,结合距离突变检测剔除可疑数据。
8.3 阻塞与非阻塞架构
在多任务系统中,阻塞式测距会严重影响其他功能的实时性。建议采用定时器中断配合状态机的非阻塞架构。测距流程可以划分为四个状态:空闲、触发、等待回波、完成。定时器每 1ms 或每 10ms 中断一次,在中断服务函数或低优先级任务中推进状态机,同时主循环继续执行显示、通信等任务。
以 Arduino 的 NewPing 库为例,其ping_timer()就是典型的非阻塞实现。在 STM32 上,可以使用输入捕获中断配合 FreeRTOS 任务和消息队列实现更优雅的多路异步测量。
九、常见问题与故障排查
超声波测距传感器看似简单,但在实际调试中仍会遇到各种问题。本节梳理常见的故障现象、可能的原因和排查方法,帮助读者快速定位并解决问题。
9.1 测距值明显偏差或跳变
测距值偏大或偏小的常见原因如下。
- 声速取值不当:使用了错误单位或没有考虑温度影响。
- 定时器计数频率错误:如 STM32 预分频配置错误,导致 1 个计数值不是 1us。
- Echo 测量方法错误:误将 Trig 到 Echo 的时间差当作脉宽。
- 供电不稳定:电源纹波过大,导致模块内部时序抖动。
排查时建议先用示波器观察 Trig 和 Echo 波形,确认触发脉冲宽度和 Echo 脉宽是否与预期相符。通过已知距离(如 30cm)进行标定,反推实际声速系数,可以快速判断是硬件问题还是软件问题。
9.2 近距离无法测量
所有脉冲回波式超声波测距模块都存在盲区。HC-SR04 的盲区约 2cm,JSN-SR04T 的盲区可达 20cm。当障碍物距离小于盲区时,回波会在模块尚未切换到接收状态时返回,导致无法检测。解决方法是根据应用选择盲区更小的模块,或将传感器后移。对于必须测量极近距离的场景,建议改用红外、激光或电容式接近传感器。
9.3 回波丢失或信号不稳定
回波丢失通常与以下因素有关。
- 障碍物材质:软质、多孔、倾斜的表面回波弱,可尝试在障碍物表面贴反射板。
- 距离过远:接近模块最大量程时信噪比降低,应适当留有余量。
- 环境噪声:其他超声波源、机械振动、电磁干扰都可能影响接收,应进行屏蔽和时序避让。
- 电源干扰:电机启动瞬间的大电流可能导致模块复位,应做好电源隔离和滤波。
9.4 Echo 引脚电平异常
如果 Echo 引脚始终为高电平,可能原因包括:模块损坏、Echo 引脚与 VCC 短路、接收换能器故障等。如果 Echo 始终为低电平,可能原因包括:Trig 未正确触发、模块未供电、共地不良等。检查时先用万用表确认电源电压,再用示波器观察触发后 Echo 是否有电平变化。
在连接 3.3V 单片机时,还需要特别注意 Echo 的 5V 高电平是否对 GPIO 造成损伤。简单可靠的做法是使用一颗 NPN 三极管或者专用电平转换芯片进行双向电平匹配。
十、实际应用案例
理解了原理和驱动方法之后,我们通过几个典型应用案例,把理论转化为完整的工程实践。
10.1 倒车雷达
倒车雷达是超声波测距最经典的应用之一。系统通常在车尾等距安装四个传感器,实时检测车辆与后方障碍物的距离,并根据距离分级发出蜂鸣提示。系统设计要点包括:
- 传感器安装在保险杠上,采用防水型号并做好密封;
- 四路传感器轮流触发,避免串扰;
- 根据距离分段,如大于 150cm 不报警、100cm 到 150cm 慢响、50cm 到 100cm 快响、小于 50cm 连续长鸣;
- 加入温度补偿和滤波,减少误报。
倒车雷达的核心控制逻辑可以用状态机实现,将四路传感器的测量结果取最小值作为当前障碍物距离,再驱动蜂鸣器提示。
10.2 液位检测
使用防水超声波模块如 JSN-SR04T,可以非接触式检测储罐、水井、污水池等容器的液位。传感器安装在容器顶部,向下发射超声波,声波经液面反射后返回。液位越高,超声波往返时间越短,计算出的液面距离越小。系统设计要点包括:
- 选用防水、防腐、耐温的传感器;
- 容器顶部安装面要平整,发射方向尽量垂直于液面;
- 对液面波动进行滤波,可适当降低测量频率并增加滤波深度;
- 注意盲区,传感器到最高液面的距离应大于盲区。
10.3 避障小车
避障小车通常在前方、左侧和右侧安装三个超声波传感器。小车行驶过程中不断刷新三路测量值,当某一方向的距离小于安全阈值时,执行对应的转向或停车动作。设计要点包括:
- 根据车体宽度和车速设定安全阈值,建议阈值不小于 20cm;
- 传感器安装高度要避开地面回波,通常安装高度在 15cm 到 30cm,并略微向下倾斜;
- 三路传感器必须轮流触发,防止近距离串扰;
- 对测量值进行滤波,避免单次误测导致小车误动作;
- 可将超声波与红外、激光雷达等传感器融合,提升复杂环境下的避障可靠性。
10.4 停车场车位检测
在停车场车位检测中,超声波传感器常安装在地面或车位上方,用于判断车位是否被占用。安装在地面时,发射方向朝上,有车时会检测到车底反射,距离值大幅减小;安装在车位上方时,发射方向朝下,有车时检测到车顶反射。设计要点包括:
- 地面安装必须使用抗压、防水结构,并防止泥沙堵塞换能器;
- 根据车型和安装高度确定有车无车的距离阈值;
- 采用低功耗设计和定时唤醒,延长电池寿命;
- 通过无线模块将车位状态上传至管理平台。
十一、总结与进阶方向
本文从超声波测距的物理原理出发,深入分析了 HC-SR04 模块的硬件结构、电气特性和工作时序,随后详细讲解了 STM32、Arduino、51 单片机三大平台的驱动实现方法,包括 GPIO 轮询、外部中断、定时器输入捕获等方案。在此基础上,我们讨论了滤波处理、多传感器调度、常见故障排查等内容,并通过倒车雷达、液位检测、避障小车和车位检测四个案例展示了超声波测距的工程应用。
回顾全文,超声波测距驱动的核心要点可以归纳为以下几条:
- 理解时序:Echo 高电平脉宽代表声波往返时间,这是所有驱动代码的基础。
- 选对定时方案:根据系统实时性要求,合理选择轮询、中断或输入捕获方式。
- 重视电源和接线:共地不良、电源纹波大、电平不匹配是初学者最常遇到的硬件故障。
- 善用滤波:中值滤波、滑动平均和野值剔除能够显著提升测量稳定性。
- 控制触发节奏:多传感器场景下,时分隔离是避免串扰的最有效手段。