☰
超声波测距实战指南:从原理到嵌入式竞赛调优
2026/10/15 17:55:12 网站建设 项目流程

不少人在比赛前一周才第一次碰超声波模块。弄一个舵机云台、一块屏幕、一个超声波传感器,在桌面上来回转着测距离,感觉这题稳了。真到了赛场,你会发现它对着一块亚克力板疯狂输出“3.8米”,而障碍物明明就在半米外。超声波传感器在蓝桥杯这类嵌入式竞赛里,从原理、代码到现场调优,到底该怎么准备,有哪些经验能让它在关键时刻不掉链子,这篇文章一次说清楚。内容适合正在备赛嵌入式比赛的同学,也适合那些项目里需要测距但总被噪声折磨的开发者。

1. 超声波模块在竞赛题目里的真实地位

1.1 赛题里它被用来干什么

超声波模块在竞赛题目里很少单独出现,它几乎总是和“运动控制”“状态判断”“人机交互”组合在一起。比较常见的有这么几类用法:控制小车在指定距离停下、根据距离切换屏幕显示内容、用距离变化模拟某种设备的自动开关,或者配合舵机做小范围的扫描测距。

以嵌入式赛题举例,题目给出一块主板、一个屏幕、若干按键和传感器,要求在一定时间内完成一个完整的作品。超声波模块通常作为“环境感知”的输入源,考的不是你会不会算声速,而是你能不能把环境中的真实距离可靠地变成程序里的一个数字。

很多同学备考时只写“读距离并显示”这种最小用例,觉得能出数就算会了。到了赛场上,距离在跳、程序在跑、屏幕在闪,问题就全部暴露出来了。超声波的难点从来不在原理,而在工程化。

1.2 例程能跑通和能拿分是两回事

网上下载的例程通常做了一件很保守的事:主循环里什么都不干,只负责发触发脉冲、等回响、计算距离、串口打印。这个流程在桌面上当然稳定,但竞赛作品的真实情况是:主循环里还要扫按键、刷屏、处理逻辑、驱动电机或舵机。只要一个延时阻塞了,回响信号就丢了。

更隐蔽的问题是模块的一致性。同一批生产的传感器,回响脉冲的上升沿、下降沿表现会有细微差别,更别提不同厂家、不同供电电压下的差异。例程跑通只代表“这一块模块在那一刻能用”,不代表你理解了测距的完整链路。

所以这篇文章我打算从原理开始讲,再把代码方案、调优技巧、现场排查经验串起来。即使你手头已经有能跑的例程,也建议对照着看,把里面隐藏的假设一个个找出来。

2. 测距原理拆解:从一声“嘀”到一个距离数字

2.1 声音的往返时间怎么变成厘米

超声波测距模块的典型工作方式是这样的:你往触发脚(TRIG)送一个不小于10微秒的高电平,模块内部就会发出一串超声波脉冲;声波遇到障碍物反弹回来,回响脚(ECHO)会输出一段高电平,高电平持续的时间就是“声波从出发到返回”的总时长。

距离公式很简单:距离 = 高电平时间 × 声速 ÷ 2。除以2是因为声波走了个来回。

换算成代码里的常用形式:

  • 假设高电平时间是t微秒,声速取340米/秒;
  • 340米/秒 = 0.034厘米/微秒;
  • 距离(厘米)= t × 0.034 ÷ 2 = t × 0.017。

所以如果你量到回响高电平持续了1000微秒,距离就是17厘米。这个换算看起来简单,但很多同学手忙脚乱的时候会把÷2忘了,得到的是双倍距离,我怎么知道的?因为我也干过。

2.2 声速不是常数:温度修正避免“测啥都偏”

声速340米/秒是一个常温近似值,但超声波测距对精度敏感的场景里,这个近似会带来系统性误差。

声速和温度的关系可以近似为:

v = 331.4 + 0.6 × T

其中T是摄氏温度。用这个公式算一下:

  • 0℃时,声速331.4米/秒,按340算,误差约2.6%;
  • 25℃时,声速346.4米/秒,按340算,误差约1.9%;
  • 40℃时,声速355.4米/秒,按340算,误差约4.5%。

4.5%的误差在1米距离上就是4.5厘米,控制设备能不能准确停住,就看这4.5厘米了。

所以我的习惯是,在代码里留一个温度补偿入口。如果板子上有温度传感器,直接读取温度代入公式;如果没有,就在配置文件里放一个常量,比赛现场根据环境温度手动改。虽然看起来不够“自动”,但比写死340强得多。

2.3 盲区从哪来,又该怎么应对

超声波的盲区是很多刚接触它的人完全没概念的东西。所谓盲区,就是障碍物距离太近时,模块测不出正确距离的那一段区间。

原理不复杂:模块发出超声波脉冲后,探头本身有机械余振,也就是压电陶瓷片在停止激励之后还会继续振荡一小段时间。如果回波在这个余振还没衰减完的时候就到了,模块无法区分回波和余振,结果就会失真。常见模块的盲区在2厘米到5厘米不等。

这意味着,如果你把障碍物直接贴在模块探头前面,读数不会聪明地变成0,反而会出现一个偏大的值,或者剧烈跳变。竞赛题目如果要检测“物体是否贴近”,千万别只用超声波,必要时加一个红外对射管或者微动开关做互补判断。

3. 把时间量准:三种代码方案的取舍

3.1 最朴素方案:轮询等待回响脉宽

最直观的做法是GPIO轮询:发送触发脉冲后,死等回响引脚变高,记录时间;再等回响引脚变低,记录时间;两个时间相减就是脉宽。

伪代码大概是这样的:

void send_trig(void) { HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); } uint32_t measure_pulse_width(void) { send_trig(); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_RESET); uint32_t start = get_us_tick(); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET); return get_us_tick() - start; }

这个方案在桌面上完全能跑,但有两个隐患。

第一,如果回响引脚悬空、初始化方向错了、或者模块没供电,第一个while会一直死等,整个程序卡死。竞赛现场一紧张,你连“模块没插稳”这种低级问题都很难一眼看出来。

第二,轮询等待期间,主循环什么都干不了。如果你在等回响的这几十毫秒里还要刷屏、扫按键,程序体验就会变得很“卡”。实际测量一次完整测距周期大约要10到50毫秒,视距离而定,在实时性要求高的题目里这个阻塞不能忽略。

3.2 让时间交给定时器:输入捕获方案

更稳的做法是用定时器的输入捕获功能,让硬件帮你记录回响引脚从低到高、再从高到低的时间点。两个时间点相减就是脉宽,而且整个过程不阻塞主循环。

我以常见的Cortex-M内核芯片为例,思路是这样的:

  • 定时器配置成输入捕获模式,比如用定时器2的通道1;
  • 先捕获上升沿,记下计数值;捕获到上升沿之后,把触发边沿切换成下降沿;
  • 再捕获下降沿,记下计数值;
  • 两者相减,换算成微秒。

需要注意定时器溢出。如果定时器是16位的,计数器会循环回绕,必须正确处理溢出次数,否则捕获值会严重错误。这个坑我专门花了一下午才想明白。

简化写法如下:

// 假设定时器时钟1MHz,计一个数代表1微秒 volatile uint32_t edge_time_1 = 0; volatile uint32_t edge_time_2 = 0; volatile uint8_t edge_count = 0; void timer_capture_isr(void) { uint32_t current = TIM2->CCR1; if (edge_count == 0) { edge_time_1 = current; TIM2->CCER |= TIM_CCER_CC1P; // 翻转成下降沿捕获 edge_count = 1; } else { edge_time_2 = current; TIM2->CCER &= ~TIM_CCER_CC1P; // 翻转成上升沿捕获 edge_count = 0; } } uint32_t get_pulse_width_us(void) { if (edge_time_2 >= edge_time_1) return edge_time_2 - edge_time_1; else return (65535 - edge_time_1) + edge_time_2 + 1; // 处理回绕 }

输入捕获方案的好处是,超声波测距和主逻辑解耦了:你发出触发脉冲之后,该刷屏刷屏,该处理按键处理按键,等捕获中断把数据算好,再去读取结果就行。实测下来,程序响应流畅很多。

3.3 还有一个方案:外部中断加微秒计数

如果你用的平台不支持输入捕获,或者定时器资源不够,还有一种折中方案:外部中断。把回响引脚接到一个支持双边沿触发的中断引脚上,上升沿进入中断时记录当前微秒计数,下降沿进入中断时再记录一次,两次相减。

这个方案看起来和输入捕获差不多,但它依赖你有一个稳定的“微秒计数源”。可以是另一个定时器,也可以是CPU周期计数器。它的缺点是中断响应会有延迟,尤其在系统繁忙、中断嵌套比较深的时候,误差会偏大。输入捕获是硬件电路在电平翻转瞬间锁存的计数值,精度更高。

所以我的建议很直接:如果平台支持输入捕获,优先用输入捕获;如果只是简单验证原理,GPIO轮询也够用;外部中断方案尽量少用,除非你非常确定系统中没有长时间关中断的代码段。

4. 让读数“洗”稳定的四个步骤

我见过太多人写超声波代码,测出了距离就直接拿去控制电机或屏幕显示。读数在真实环境里是不可能一直平滑的,地毯、布料、倾斜的纸箱、桌腿、空调风都会产生各种奇奇怪怪的回波。

4.1 第一步:剔除超范围数据

超声波模块的输出不是“合法值”和“非法值”都标好了标签给你。程序需要自己判断哪些数据是合理的。

首先,模块有最大测量距离,常见型号标称4米到6米,超过之后回波很弱,读数会乱跳。其次,盲区内的数据也不可信。所以我一般会在读距离后先做一次区间过滤:

if ((distance < 2) || (distance > 400)) { return last_valid_distance; }

注意,这里不是返回错误码,而是返回上一次的有效值。这样即使偶尔来一个异常数据,输出也不会断崖式变化。

4.2 第二步:中值滤波对抗单点毛刺

超声波数据最常见的噪声形式是突发性的尖峰:某一次测量突然跳变了几十厘米,下一次又恢复正常。这种问题用均值滤波反而会被拉偏,用中值滤波最合适。

我的做法是连续采集5个数据,排序后取中间值作为本次输出。5个数据的中值滤波对单点毛刺几乎可以完全消除,而且延迟只有几次测量周期,对比赛场景完全够用。

uint16_t median_filter(uint16_t *buf) { // 简单冒泡,buf[0]到buf[4] for (int i = 0; i < 4; i++) { for (int j = 0; j < 4 - i; j++) { if (buf[j] > buf[j + 1]) { uint16_t tmp = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = tmp; } } } return buf[2]; }

如果你觉得排序代码占资源,那就用三取中值,效果也还行,只是抗毛刺能力稍弱。排序5个元素的开销在嵌入式平台上可以忽略不计。

4.3 第三步:滑动平均让输出平稳

中值滤波解决了毛刺,但连续测量之间还是会有小幅抖动。比如静止物体测10次,结果在12.3厘米到13.1厘米之间小幅波动,这是正常的。

这时候加一个滑动平均窗口。我习惯窗口长度设为5到10,窗口越长越平滑,但响应越迟钝。比赛里如果要求快速跟随物体移动,窗口太长会显得“反应慢”;如果只是测个墙壁距离,窗口长一点无所谓。

一个简单的实现:维护一个循环数组,每次把新数据放进去,丢掉最旧的数据,然后求平均。滑动平均的好处是计算量固定,不像累计求和那样要频繁做除法。

4.4 第四步:从距离到控制的平滑输出

前面三步处理完了,输出还只是“显示用的稳定距离”。如果要用距离控制小车、舵机、或者某个执行机构,还需要在控制层面加一步限幅处理:相邻两次输出之间的距离差不能超过一个阈值。

比如本次期望输出20厘米,上次实际输出是15厘米,如果直接跳变到20厘米,执行机构就会猛冲一下。把差值限幅在每周期2厘米,执行器就会平滑地移动过去。这个限幅值取决于你的控制周期和执行机构的响应速度,需要自己调。

竞赛里很多人忽略了这一步,导致小车走到目标位置附近时一抖一抖的,就是因为距离数据虽然滤波了,但控制输出没有做平滑。

5. 赛场翻车点的排查记录

5.1 引脚初始化把回响电平顶高了

我第一块板子接超声波模块时,回响引脚读数一直在两个值之间跳:要么300多厘米,要么个位数。排查了很久,最后发现是引脚内部下拉电阻没关。回响引脚在模块空闲时本来应该保持低电平,但芯片内部的下拉和模块内部的输出级一叠加,电平状态变得不稳定。

解决方法是把回响引脚配置成普通输入,不要使能内部上拉或下拉。触发引脚用推挽输出,速率可以配置成高速,避免信号沿不够陡峭导致触发失败。

这个坑特别隐蔽,因为很多时候模块不接开发板、单独上电测试时一切正常,一接到开发板就出问题。就是引脚内部配置和模块输出级的电气特性冲突了。

5.2 热熔胶堵住探头

听起来像段子,但每年都有人这么干。临时固定模块的时候,热熔胶打得太多,从PCB背面溢到了探头表面,或者直接糊到了探头的开孔上,超声波能量传不出去,回波自然收不回来。

测距表现就是:近的时候还能测到,远了就失效;或者时好时坏,随热熔胶的厚度和位置变化。

我建议固定模块时用螺丝、双面胶、或者只在PCB边缘打胶,探头正面一定保持干净。比赛前一天把所有胶点检查一遍,用手指摸一下探头表面有没有胶痕。

5.3 两个模块同时测距的相互干扰

很多题目会用到两个甚至多个超声波模块,比如左边一个、右边一个。如果你同时触发它们,两个模块发出的声波会相互干扰,结果就是你听到的是自己的回波,它听到的也是你的回波,读数乱作一团。

解决办法是分时触发:第一个模块先触发、读完数据,再触发第二个模块。两次触发之间留出足够的时间,比如至少50毫秒,保证上一次的声波完全衰减掉。

实测下来,50毫秒分时基本能消除串扰。如果模块靠近安装,比如同一个支架上间距不到10厘米,建议把这个间隔再加长到80毫秒以上。代价是整体测距频率变低,但竞赛场景完全能接受。

5.4 环境温度导致距离持续偏低

有一次作品在空调房里调试得好好的,换到没空调的场地,所有距离读数突然偏大了一点,大约多出3%到4%。当时第一反应是模块坏了,实际上就是声速变了。

如果题目对精度有明确要求,建议在代码里加上温度补偿。没有温度传感器的话,也可以准备一个手动的补偿系数。更省事的做法是,用一块已知尺寸的参照物,在赛前校准一个修正系数:实测距离除以真实距离,得到比例因子,后续所有测量值除以这个比例因子。

5.5 主循环里长时间关中断

我见过一个例子:某同学的代码里有大量临界区保护,进入临界区之后要操作完一整个数据结构才退出。超声波测量期间如果发生了关中断,回响引脚的电平变化会丢失或者延迟记录,测出来的脉宽要么偏大要么偏小。

用GPIO轮询方案时,主循环里绝对不能有长时间关中断;用输入捕获方案时,也要检查有没有代码在测量期间关了定时器中断。这个问题的排查方式是:在测距的时候用逻辑分析仪抓回响引脚,波形正常但程序算出来不对,那基本就是中断延迟在捣乱。

6. 模块选型和赛后复用

6.1 常见几类超声波模块怎么选

竞赛里最常见的是HC-SR04那种通用型,便宜、资料多、接口简单。它的缺点也比较明显:供电要求是5V,而大量的嵌入式板子主控工作电压是3.3V,引脚电平匹配需要小心。

US-100这类模块提供了串口和脉宽两种输出方式,直接读到距离数值,省去自己算脉宽的过程,对不想折腾定时器的同学很友好,缺点是价格高一些,而且串口输出本身的延迟偏大。

还有一类是防水型超声模块,探头和主控分离,可以安装在水箱、管道这类环境,适合做液位检测。竞赛里比较少见,但如果你做的作品和水利、环境监测有关,这类模块反而比通用模块更合适。

选型时我会看三个条件:供电电压是否和板子匹配、输出方式是脉宽还是串口、盲区和量程是否符合题目要求。满足这三个条件之后,再考虑价格和购买渠道的可靠程度。

6.2 超声波、红外、激光测距怎么选

很多同学问:既然超声波这么麻烦,为什么不用红外或者激光?

红外测距模块价格低、速度快,但容易受环境光干扰,而且量程一般不超过80厘米;激光测距精度高、量程大,但模块贵,有些高速激光测距模块的驱动和数据处理接口更复杂。

超声波的优势在于:不受环境光线影响,白天黑夜都能用,价格便宜,对白色、浅色障碍物反射效果好。缺点是:对软质材料吸收严重,对倾斜表面的反射不稳定,测距刷新率低。

竞赛里做选择的原则很简单:室内短距离控制优先考虑超声波,因为便宜且大部分题目给的参考例程就是它;如果题目对精度和响应速度要求高,再看红外;如果现场光线条件复杂且预算充足,激光是更省心的选择,前提是你愿意花时间调它的通信协议。

6.3 这套经验如何迁移到后续项目里

超声波测距看起来是个小模块,但里面涉及的知识点可以辐射得很远:定时器输入捕获、中断优先级设计、数据滤波、电气特性匹配、现场调试方法论。这些东西放到任何传感器项目里都一样适用。

我现在做项目时,遇到新的传感器,几乎都会走一遍类似的流程:先啃透数据手册里的时序图,再用逻辑分析仪验证真实波形,然后写一个不阻塞主循环的驱动,最后上一套滤波代码,把数据从“能读”变成“可信”。超声波模块让我练就的正是这套基本功。

比赛成绩只是结果,过程中建立的调试思路,才是之后做项目时真正值钱的部分。

如果让我只留一条经验,我会说:提前把驱动封装好,留出温度补偿接口和滤波开关,赛前把所有电平、固定方式、串扰问题全部测一遍,超声波模块就不会是那个拖后腿的角色。剩下的时间,投入在题目逻辑本身,比反复折腾传感器值钱得多。

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

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

立即咨询