单片机超声波测距实战:从HC-SR04到STM32代码与误差校正
2026/9/19 21:20:59 网站建设 项目流程

简介:这是一份基于STC89C52单片机的超声波测距系统设计资料,适合电子信息、自动化等专业学生用于课程设计或毕业设计参考。内容围绕超声波传感器的工作原理与特性、发射接收电路、液晶显示模块、报警模块以及温度补偿机制展开,完整呈现了从方案论证、硬件电路设计到软件算法与误差修正的设计思路。压缩包内仅含1个docx文档,大小约389KB,属于可直接打开阅读的设计报告格式,便于按章节查阅系统总体方案、单片机控制流程、测距计算过程以及报警范围设定等内容。目前已有一百人学习下载。对于正在做单片机测距类课题、需要论文框架或实现细节的读者,这份资料可作为低成本高精度测距系统设计与撰写的参考,能够帮助快速理解超声波测距原理,并借鉴其硬件电路和软件设计方法。

1. 基于单片机的超声波测距:从 HC-SR04 到实物量产的完整链路

如果只把 HC-SR04 模块的四根线接到单片机最小系统上,串口助手能蹦出距离值,这个“单片机超声波测距”的课程设计其实只完成了一半。定时器分频选错了,回波脉宽会量不准;Trig 脉宽卡在临界值,模块会偶尔不响应;Echo 悬空时,程序干脆死在等待语句里——这些全是实物调试一定会遇到的边界问题。下面按“基于单片机的超声波测距设计与实现”这条路,先讲物理原理和器件选型,再给 C51 与 STM32 两套能直接烧录的代码,最后用温度补偿和实物标定把误差压到毫米级,适合正在做课程设计、备赛蓝桥杯单片机,或者想把手头开发板变成测距工具的读者。

2. 超声波测距原理与器件选型:从声速公式到定时器量程

超声波测距的逻辑非常朴素:发射端发出 40kHz 的脉冲串,声波遇到目标反射回来,接收端记录从发射到收到回波的时间,距离就是声速乘时间的一半。单片机在这条链路里的任务只有两个:触发发射、测量回波高电平的宽度。难点不在于触发,而在于把时间量准。

2.1 回波时间与声速公式:÷2 是最容易漏掉的一步

先写结论:在 20℃ 空气中,声速约为 343m/s,合 0.343mm/us。如果回波脉宽是 t,单位是 us,那么目标距离的计算方式如下:

// 单程距离 = 声速 × 时间 / 2 float dist_mm = 0.343f * echo_ticks / 2.0f;

echo_ticks是定时器测量到的 Echo 高电平持续时间,单位是 us。之所以要除以 2,是因为声波走了去程加回程两倍距离,HC-SR04 的 Echo 脚恰好把这两段合并成一个高电平。C51 里做整数运算时,常见写法是时间乘 17 再除 100,得到的单位是毫米,本质就是 0.343 / 2 约等于 0.1715 的整数近似。这个写法没问题,但要注意 16 位乘法溢出,尤其是回波时间超过 38000us 时。

依赖固定声速 343m/s 有一个隐含前提:空气成分基本不变、温度 20℃、湿度适中。实际环境里温度每升 1℃,声速约增加 0.6m/s,这会在 2m 距离上带来约 3.5mm 的偏差。量程很短时无所谓,量程超过 1m 后,就应该把温度补偿作为正式功能纳入设计,而不是可选优化。具体公式和标定流程放在第 5 章。

2.2 HC-SR04 与单片机的选型边界:从课程设计到工程现场

课程设计和竞赛中最常见的组合是 51 单片机加 HC-SR04,STC 系和 STC32 这类增强型 51 内核单片机也能直接驱动 HC-SR04。HC-SR04 的核心参数是量程 2cm 到 400cm、工作电压 5V、Trig 和 Echo 为 TTL 电平。JSN-SR04T 是防水探头版本,量程更长,适合液位检测;US-016 输出模拟电压,适合不需要回波时序的场景。下表是几种常见模块的对比。

模块量程供电接口方式典型场景
HC-SR042cm~400cm5VTrig/Echo,电平宽度课程设计、避障小车
US-0162cm~300cm5V/3.3V模拟电压/串口对时序敏感度低的应用
JSN-SR04T20cm~600cm5VTrig/Echo,探头与电路分体水箱液位、户外探测

选型时我一般先确认工作电压。STM32F103 的 IO 是 3.3V 逻辑,虽然能驱动 HC-SR04 的 Trig,但 Echo 返回的高电平是 5V,直接接在 3.3V 单片机上存在烧引脚风险,必须加电平处理。51 单片机在这一点上有天然优势,5V IO 直接对接,这也是大量超声波课设选 51 的原因之一。超声波雷达在倒车系统中用的也是同一套回波测距逻辑,HC-SR04 把收发端做成一体模块,单片机要做的动作被简化到只剩触发和计时。

2.3 定时器计数频率决定毫米级精度:量化误差不是噪声

测距的核心是把 Echo 高电平宽度量准。常见做法是把定时器配置成固定频率的计数器,例如 12MHz 晶振的 51 单片机在 12 分频下,计数器每 1us 加 1;主频 72MHz 的 STM32 把定时器预分频设为 71,同样得到 1us 计数周期。不同配置对应的分辨率如下表。

单片机/晶振定时器分频计数周期距离分辨率单次最大量程(16位)
51 @ 12MHz12 分频(默认)1us约 0.1715mm约 11.2m
51 @ 11.0592MHz12 分频(默认)约 1.085us约 0.186mm约 10.4m
STM32 @ 72MHz72 分频1us约 0.1715mm约 11.2m
STM32 @ 72MHz36 分频0.5us约 0.086mm约 5.6m

这里有两个容易被忽略的结论。第一,1us 计数量化带来的误差是 0.17mm 级别,远小于 HC-SR04 本身的测量抖动,所以从 12MHz 51 换成 72MHz STM32,并不会让距离数据变干净。第二,16 位定时器的最大计数是 65535,在 1us 周期下对应约 65ms,按 343m/s 算能覆盖到约 11m,比 HC-SR04 的 4m 标称量程大;但如果把分频压到 0.5us,可测距离会缩到约 5.6m,这是实际使用中需要权衡的。

代码实现上要注意:不要在 while 循环里裸等while (Echo == 1);,而要在循环里检查定时器溢出标志。不然目标超出量程时 Echo 一直为高,程序会死锁,具体写法在第 4 章。

3. 硬件电路设计:最小系统、电平匹配与干扰处理

从模块插上下载线能用,到画一块能复现的板子,中间隔着三个问题:电平能不能安全对接、引脚分配会不会影响下载调试、供电纹波会不会让距离值周期性漂移。硬件设计的目标是把这三个问题在画板阶段解决掉,而不是等实物出了问题再飞线。

3.1 3.3V 单片机与 5V Echo 的电平转换:分压是最稳的入门方案

HC-SR04 手册没有统一标准,但市面上大部分版本的 Echo 输出为 5V TTL。51 单片机 IO 能容忍 5V,直连即可;STM32 这类 3.3V 器件直连,长时间处于高电平就可能超过引脚绝对最大额定值。最稳妥且最简单的做法是电阻分压。

以 PA0 接 Echo 为例:Echo 输出串一个 1kΩ 电阻到 PA0,再从 PA0 对地接一个 2kΩ 电阻,得到的分压约为 5 × 2 / (1 + 2) ≈ 3.33V,正好落在 3.3V 逻辑高电平范围内。电阻选 1k/2k 而不是更大的阻值,是为了保证分压节点在高阻输入下仍有足够驱动能力,不至于因为引脚偏置电流把电压拉低。不要只串一个限流电阻而不接地,那样得到的不是固定电平,效果取决于单片机引脚内部结构。

// 分压网络选型示意:Echo(5V) --[1kΩ]-- PA0 --[2kΩ]-- GND // 分压约 3.33V,可直接进入 3.3V 单片机 IO

如果追求更低的输出阻抗,也可以用 NPN 三极管做电平转换,Echo 接基极、集电极上拉到 3.3V。但测距这种低速信号,分压电阻方案已经足够,没必要引入额外器件。

3.2 引脚分配:Trig 与 Echo 放同一组端口,避开串口下载脚

硬件设计的第二个坑是引脚分配。51 单片机的 P3.0/RXD 和 P3.1/TXD 同时承担串口下载功能,如果把 Echo 接到 P3.0,外部干扰可能拉高引脚,导致下载失败或者程序启动异常。常见做法是把 Trig 和 Echo 放到 P2 口或者 P1 口的相邻引脚,便于 PCB 布线,也方便程序里用同一个端口的位操作。

信号51 常用引脚STM32 常用引脚作用
TrigP2.0PA1输入,单片机给至少 10us 高电平脉冲
EchoP2.1PA0(需分压)输出,高电平宽度等于声波往返时间
VCC5V5V 供电模块供电
GNDGNDGND必须与单片机共地

在线调试还有一个细节:如果调试器用的是 SWD,而 SWDIO/SWCLK 恰好被传感器占用,也会导致无法连接。尽量把 Trig/Echo 安排在定时器通道和 SWD 都不冲突的引脚。下面把端口和位号抽成宏,换平台时只改这一处:

#define TRIG_PORT P2 #define TRIG_PIN 0 #define ECHO_PORT P2 #define ECHO_PIN 1

把端口、位号抽成独立宏,代码其他地方不再出现硬编码引脚号,这在从 C51 迁移到 STM32 时能少改很多处。要注意 Trig 和 Echo 不要共用一个位操作,Trig 始终是输出,Echo 是输入,测距中 Echo 会被模块从低拉高再拉低,共用一个变量会互相覆盖。

3.3 电源去耦与共地:供电纹波会让距离值周期性漂移

超声波模块发射瞬间电流会突然拉高,如果电源线又细又长,模块供电电压会瞬间跌落,导致 Echo 波形宽度抖动。典型现象是距离值每隔几百毫秒整体跳变一两个厘米,且与测量目标无关。解决手段:在模块 VCC 与 GND 之间并一个 100uF 电解电容和一个 100nF 陶瓷电容,电解电容负责吸收瞬态电流,陶瓷电容负责滤高频;模块地线尽量单独走线回到电源地,不要和单片机大电流回路共用一段细走线。

更隐蔽的问题是 Echo 信号线上的干扰。HC-SR04 的 Echo 输出阻抗不低,靠近电机或继电器会耦合噪声。如果实测发现 Echo 上升沿抖动,可以在 Echo 引脚对地并联一个 100pF 电容做简单滤波,但电容值不要选太大,否则会对回波边沿产生积分延迟,影响精度。这个 100pF 只能针对高频毛刺,不能解决共地不良,根因还是要处理地线。

4. 软件实现:C51 与 STM32 双平台的测距时序与滤波

硬件把信号链路打通后,软件要回答的问题只有一个:怎么把一个变化的高电平宽度,变成稳定可信的距离。这一章给出 C51 和 STM32 两套实现,第一套用定时器轮询 Echo,第二套用输入捕获中断,分别对应课程设计和稍高一点的工程要求。

4.1 HC-SR04 的触发时序:手册上的 10us 不是等宽脉冲

手册对 Trig 的典型描述是给一个大于 10us 的高电平,但实际触发逻辑是检测上升沿,高电平持续时间不是精确定时关键。常见错误是延时函数恰好写成 10us,而函数调用和入栈出栈开销就可能让实际脉宽不足。我一般直接用 15us 到 20us,留出余量,模块会自行完成 8 个 40kHz 脉冲的发射。

// 触发一次超声波发射,Trig 脉冲宽度约 15us Trig = 0; delay_us(5); Trig = 1; delay_us(15); // 略大于 10us 即可,不要纠结精确值 Trig = 0;

delay_us在 12MHz 51 下用空循环实现,偏差不会影响触发结果。关键是Trig = 0之后要等待一小段时间再拉高,确保模块内部状态机复位,避免连续触发时模块还没准备好。

提示:在 Proteus 仿真里,HC-SR04 不会自己产生 Echo 回波,需要手动加一个脉冲信号模拟。仿真只能验证触发时序和测距计算,不能验证真实回波质量,实物调试前别只依赖仿真结果。

4.2 C51 完整测距代码:定时器 0 计时 Echo 脉宽,串口输出

#include <reg52.h> #include <stdio.h> sbit Trig = P2^0; sbit Echo = P2^1; void delay_us(unsigned int n) { while (--n); } void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); } char putchar(char c) { SBUF = c; while (TI == 0); TI = 0; return c; } unsigned int measure_once(void) { unsigned int cnt = 0; Trig = 0; delay_us(5); Trig = 1; delay_us(15); Trig = 0; // 等待 Echo 拉高,带超时保护 while (Echo == 0) { if (++cnt > 60000) return 0; // 约 60ms 无回波,按超时处理 } TH0 = 0; TL0 = 0; TR0 = 1; // 测量高电平宽度,溢出即时退出,避免死锁 while (Echo == 1) { if (TF0 == 1) { TR0 = 0; TF0 = 0; return 0xFFFF; // 超量程 } } TR0 = 0; return (TH0 << 8) | TL0; } void main(void) { unsigned int ticks; unsigned long dist_mm; TMOD = 0x21; // T0 模式1(16位),T1 模式2(波特率) SCON = 0x50; // 串口模式1 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; TR1 = 1; TI = 1; while (1) { ticks = measure_once(); if (ticks == 0) printf("no echo\r\n"); else if (ticks == 0xFFFF) printf("out of range\r\n"); else { // 17/100 等价于 0.343/2 换算到毫米 dist_mm = (unsigned long)ticks * 17UL / 100UL; printf("dist=%lumm\r\n", dist_mm); } delay_ms(200); } }

代码里有三个值得说明的地方。第一,TMOD = 0x21一次性配置好 T0 为 16 位手动重装模式、T1 为 8 位自动重装模式,避免分散初始化时覆盖对方。第二,等待 Echo 拉高用cnt做超时保护,目标是太近或者没有目标时程序不会卡死。第三,(unsigned long)ticks * 17UL / 100UL必须先乘后除,ticks 最大 65535,乘以 17 后约 111 万,超过 16 位范围,所以必须强转成unsigned long。这个类型转换是初学者最容易漏的地方,漏掉后除法结果会完全不对。

串口部分用putchar重定向,Keil C51 工程里不需要额外库配置,printf就能直接往串口发数据。波特率 9600,定时器 1 初值 0xFD 对应 11.0592MHz 晶振,换晶振时要同步改。

4.3 STM32 输入捕获与溢出处理:1us 计数,溢出即退出

STM32 上更推荐用定时器输入捕获,测距过程不占用 CPU 轮询。核心思路是配置 TIM2 通道 1,捕获 Echo 的上升沿和下降沿,用两次捕获值之差得到高电平宽度。

void TIM2_IC_Init(void) { GPIO_InitTypeDef gpio; TIM_ICInitTypeDef ic; TIM_TimeBaseInitTypeDef tb; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; // PA0 接 Echo gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); tb.TIM_Prescaler = 72 - 1; // 72MHz 分频到 1MHz tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_Period = 0xFFFF; tb.TIM_ClockDivision = 0; TIM_TimeBaseInit(TIM2, &tb); ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; // 先捕获上升沿 ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = 0; ic.TIM_ICFilter = 0x0F; TIM_ICInit(TIM2, &ic); // 每次上升沿清零计数器,只保留单次高电平宽度 TIM_SelectInputTrigger(TIM2, TIM_TS_TI1FP1); TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset); TIM_SelectMasterSlaveMode(TIM2, TIM_MasterSlaveMode_Enable); TIM_Cmd(TIM2, ENABLE); TIM_ClearFlag(TIM2, TIM_FLAG_CC1); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); }

这里的TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset)是关键:每次上升沿到来计数器自动清零,于是捕获到的 CCR1 值本身就是 Echo 高电平的持续时间,单位是 us。中断里只需把 CCR1 读出来转成距离。

量程处理要对应第 2 章的分辨率表格。65535us 在 1us 计数下对应约 11m,HC-SR04 标称 4m,所以中断里判断CCR1 > 12000(约 4m)直接丢弃,避免模块在弱回波时输出异常长的高电平。

4.4 中值滤波与滑动平均:把厘米级跳动降下来

实测中 HC-SR04 的表现不是噪声大,而是偶尔冒出明显异常值,例如 50cm 目标连续测几次,偶尔会跳成 55cm。这种情况用滑动平均效果不好,因为均值会被野值拉偏;中值滤波更合适。常见做法是每轮取 5 次测量,排序后取中间值。

#define FILTER_N 5 unsigned int median_filter(unsigned int *buf) { unsigned char i, j; unsigned int tmp; for (i = 0; i < FILTER_N - 1; i++) for (j = i + 1; j < FILTER_N; j++) if (buf[i] > buf[j]) { tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } return buf[FILTER_N / 2]; }

要点是滤波在原始ticks上进行,而不是在毫米换算之后,因为换算后的毫米值已经是整数,会丢掉亚毫米信息。排序 5 个数在 12MHz 51 上开销也很小,但要注意每次测距之间保留几十毫秒间隔,不能连续触发 HC-SR04,否则模块尚未结束上一次回波处理,测量值会整体偏移。

5. 温度补偿与实物标定:把误差压到毫米级

5.1 温度每升 1℃,声速增加 0.6m/s:少算的距离有多明显

第 2 章提过固定声速 343m/s 的误差来源。标准计算公式是 v = 331.4 + 0.6T,T 是摄氏温度。20℃ 时声速约 343.4m/s,和代码里的 343 几乎一致;但环境是 5℃ 的户外时,声速变成 334.4m/s,同样回波时间下用 343 计算,距离会少算约 2.6%。在 2m 距离上就是 52mm 的偏差,这已经远超 HC-SR04 本身毫米级的标称分辨率。所以只要设备可能在非空调房工作,温度补偿就是必要功能。

5.2 单点标定法:用卷尺反推补偿系数

温度补偿公式处理的是物理规律带来的偏差,但模块自身还有零偏和安装偏差,比如传感器没有完全垂直于目标平面。实际工程里更可靠的做法是把两者合并,做一次单点标定。

标定方法:目标放在已知距离处,例如卷尺量出 1000mm,连续测 10 次取平均值,用实际距离除以测量距离得到修正系数,然后写进代码。下面是一组标定示例。

卷尺距离(mm)测量均值(mm)修正前误差(mm)修正后误差(mm)
500493-70
1000998-2+1
20002004+40

标定的测量距离要覆盖实际使用范围,不能只在 500mm 处标定然后用于 2m 场景,单点标定只能修正乘性误差和固定偏移叠加的结果。代码里加一个比例因子即可:

float calibration_k = 1.0f; // 单点标定系数,默认不修正 float speed = 331.4f + 0.6f * temp_c; // temp_c 来自 DS18B20 或 BME280 float dist_mm_raw = speed * echo_ticks / 2000.0f; float dist_mm = dist_mm_raw * calibration_k;

calibration_k就是 1000 / 998 这类比值,第一次运行时打印出来,之后作为常量写死。如果装了温度传感器,temp_c直接参与计算;没装就把temp_c固定为当前室温,标定系数仍然能吸收一部分固定偏差。把这一行替换进第 4 章的距离计算部分,比在 printf 里修修改改干净得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询