基于51单片机的超声波测距仪设计:从HC-SR04时序到报警系统
2026/9/17 9:44:08 网站建设 项目流程

简介:这份压缩包为基于51单片机的超声波测距仪参考源程序,面向单片机初学者与电子竞赛备赛者,用于学习超声波测距、定时器中断和报警功能的综合设计。压缩包内共14个文件,以Keil工程文件(uvproj)、C源程序、hex烧录文件为主,另含备份与编译过程文件,整体仅54KB,轻量且便于快速移植。目前已有2966人浏览学习,适合作为入门阶段的参考代码。程序覆盖了初始化、超声波脉冲发射、回波时间差测量、距离换算、数码管显示和距离阈值报警等完整流程,核心逻辑围绕定时器捕获与GPIO控制展开。代码逻辑简洁,可直接在开发板或Proteus仿真中验证,也可迁移至智能小车避障、倒车雷达等实际测距场景,是理解51单片机与传感器协作的不错范例。

1. 超声波测距仪不只是一块HC-SR04,关键在51单片机怎么卡住那150微秒

拿到这份“超声波测距仪参考源程序”时,大多数人第一反应是找HC-SR04驱动代码。但真正决定测距精度和稳定性的,是51单片机对回波脉冲宽度的测量方式——用定时器轮询还是外部中断,计数溢出了怎么办,晶振频率误差怎么修正。这套基于51单片机超声波测距报警系统的源工程,包含完整的Keil工程文件、可烧录的HEX文件以及调试产生的LST/M51文件,适合正在做课程设计、倒车雷达原型或准备参加电子设计竞赛的人。我会沿着信号时序、定时器配置、距离换算、数码管显示和报警逻辑这条线,把代码里的关键分支拆开讲,并给出可直接照抄的修改参数。超声波测距仪的核心矛盾从来不是发射脉冲,而是如何准确测量往返时间,51单片机资源有限,每一微秒都得花在刀刃上。

2. HC-SR04与51单片机的硬件连接和时序匹配

2.1 超声波模块引脚与单片机的IO分配

HC-SR04是常见的四脚超声波传感器:VCC、Trig(触发)、Echo(回波)、GND。它内部自带超声波发射和接收电路,对外只需要两根信号线。51单片机负责拉高Trig引脚,然后等Echo引脚从低电平变高电平,再记录高电平持续的时间。这个高电平宽度就是超声波往返的耗时。

代码工程里默认用的是P1.0接Trig、P1.1接Echo,实际接线建议参考源程序开头或者硬件原理图里的引脚宏定义。由于HC-SR04功耗较低,VCC可以直接接5V,GND与单片机共地。Echo引脚输出的是5V电平信号,51单片机IO口能直接识别,不需要电平转换。

模块引脚51单片机连接作用电平说明
VCC+5V供电5V直流,电流约15mA
GNDGND共地与单片机电源地相连
TrigP1.0触发测距输入,拉高至少10us
EchoP1.1回波输出输出,高电平宽度代表往返时间

这里有个容易忽略的点:Echo引脚在未接收回波时是低电平,一旦收到反射信号会输出高电平,高电平持续时间和距离成正比。如果探头前方没有障碍物或者障碍物斜度过大,Echo会一直保持低电平,程序需要设置超时判断,否则会卡死在等待循环里。源程序里通常用一个计时变量做超时保护,后面会详细说。

2.2 触发信号的时序要求与代码实现

HC-SR04的触发时序很简单:Trig端口拉高至少10微秒,然后拉低,模块内部会自动发出8个40kHz的超声波脉冲,并等待回波。问题在于51单片机主频通常是12MHz或11.0592MHz,一条NOP或者简单赋值语句的耗时不同,写延时函数时不能凭感觉。

常见做法是用以下代码生成触发脉冲:

void trigger_ultrasonic() { TRIG = 0; // 先拉低确保干净 _nop_(); // 稍作延时,等待电平稳定 TRIG = 1; // 拉高触发 _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); // 5个空操作,12M晶振下约5us _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); TRIG = 0; // 拉低,完成触发 }

这串NOP在12MHz晶振下,每个机器周期恰好1微秒,10个NOP就是10微秒。如果换成11.0592MHz晶振,机器周期约1.085us,10个NOP就是10.85us,仍然满足HC-SR04要求的10us以上。如果主频变化大,比如使用STC的6T模式或者1T模式,机器周期大幅缩短,就需要改用循环延时或用定时器精确延时。源工程默认配置的是传统12T模式,直接使用NOP即可。

触发之后立即开始监听Echo引脚。这里有一个关键选择:轮询等待Echo拉高,还是用外部中断捕捉?51单片机只有两个外部中断INT0和INT1,通常要留给更有价值的信号。测距项目里Echo高电平持续时间最长约20ms(对应4米量程),如果一直轮询等待,单片机做不了其他事。工程里一般用轮询加定时器计数,轮询是在等待Echo从低变高,然后再启动定时器测量高电平宽度。

3. 定时器测量回波宽度——精度从哪里来

3.1 定时器工作模式选择与初值计算

52系列单片机有定时器0和定时器1,每个都是16位计数器。超声波测距需要测量一个持续时间为数百微秒到数十毫秒的高电平,用定时器1工作在模式1(16位不自动重装)比较合适。模式2是8位自动重装,最大只能计数256,量程不够。模式0是13位,没必要用。

定时器初值取决于想测的最大距离。HC-SR04标称量程4米,对应往返时间是4×2/340≈23.5ms。16位定时器在12MHz晶振下,每微秒计数一次,最多计数65535us=65.5ms,足够覆盖4米量程。所以初值可以直接设为0,让定时器从0开始走。

// 定时器1初始化 void timer1_init() { TMOD &= 0x0F; // 只修改T1部分,保留T0设置 TMOD |= 0x10; // T1模式1,16位定时器 ET1 = 0; // 不用定时器1中断,用查询 TR1 = 0; // 先关掉 }

这里特意关闭定时器1中断,原因是Echo高电平期间我们只需要测量宽度,不希望中断把正在测量的状态打断。测距过程是单线程的,轮询方式是等待Echo下降沿时读取TH1和TL1。如果使用中断,则应该考虑在中断里记录累加溢出次数,否则超过65ms会溢出。

3.2 测量Echo高电平宽度的完整状态机

Echo信号在Trig触发后会经历三个阶段:低电平等待发射、高电平等待回波、低电平结束。测量程序要准确捕捉高电平的起点和终点。起点是Echo从0变1的时刻,终点是Echo从1变0的时刻。

unsigned int get_distance_cm() { unsigned int time_us = 0; unsigned int distance = 0; // 发送触发脉冲 trigger_ultrasonic(); // 等待Echo拉高,超时约5ms unsigned int timeout = 0; while (ECHO == 0) { if (++timeout > 5000) return 0; // 超时返回0 delay_1us(); } // 启动定时器1 TH1 = 0; TL1 = 0; TR1 = 1; // 等待Echo拉低,超时约30ms timeout = 0; while (ECHO == 1) { if (++timeout > 30000) break; // 超过量程退出 delay_1us(); } TR1 = 0; time_us = (TH1 << 8) | TL1; // 读取计数值 // 距离 = 时间(us) * 0.017cm/us (往返时间的一半乘以声速) distance = (unsigned int)(time_us * 0.017); return distance; }

这段代码用了两次超时保护。第一次等待Echo拉高,如果5ms内没反应,可能是Trig没发出去或者模块故障。第二次等待Echo拉低,正常情况下Echo高电平时间不会超过30ms,超过则视为超量程,强制退出。超时原因有两种:超声波被大角度反射,或者障碍物超过4米,这两种情况都返回0,由上层程序决定是否报警。

time_us的计算依赖TH1和TL1的拼接。注意定时器在运行期间,如果Echo一直保持高电平且超过65.5ms,TH1和TL1会溢出回0,测量结果错误。这里的30ms超时已经限制住了,所以不会发生溢出。另外,用delay_1us()等待Echo拉高会浪费CPU时间,更高效的做法是直接把Echo接到外部中断引脚,用上升沿中断启动定时器,下降沿中断停止定时器。但考虑到51单片机中断资源紧张,且这个工程里没有其他高优先级任务,轮询方案更简单。如果你用的是STC8或者其它增强型51,有PCA/CCP捕获模块,可以完全硬件测量,误差更小。

3.3 距离换算公式的误差校正

声速在空气中约340m/s,但温度每变化1℃,声速大约增加0.6m/s。一个简单的补偿公式是:声速=331.4+0.6×T(T为摄氏度)。如果要求不高,直接用0.017这个换算系数:距离cm = 时间us × 340m/s ÷ 2 ÷ 10000 = 时间us × 0.017。

0.017的精度在25℃时误差很小,但冬天和夏天差距能达到3%-4%。工程代码里通常固定用常量,建议改成可变的温度补偿参数,从DS18B20读取温度后计算。下面给出一个带温度补偿的距离计算:

float get_distance_temperature_compensated(unsigned int time_us, unsigned char temp_c) { float speed_of_sound = 331.4f + 0.6f * temp_c; // 单位:m/s float distance_m = speed_of_sound * time_us / 1000000.0f / 2.0f; return distance_m * 100.0f; // 转成厘米 }

注意浮点运算在51单片机上比较慢,Keil C51对float支持但会消耗较多代码空间和运行时间。如果不需要温度补偿,建议直接用整数运算:distance_cm = time_us × 17 ÷ 1000,这样只需要乘法和除法,运行速度远快于浮点。源程序如果直接乘0.017,编译器也会自动转换为定点运算,但可读性差。我一般写整数乘除,效果一样。

4. 数码管动态显示与报警阈值——状态机的实际分工

4.1 四位一体数码管的扫描方案

测量得到的距离需要实时显示。最常见的做法是使用两个或四个共阴数码管,通过动态扫描方式点亮。所谓动态扫描,就是每次只点亮一位,轮流点亮每一位,利用人眼视觉暂留形成连续显示。51单片机IO口有限,通常用两个锁存器或三极管位选,源工程里多半直接P0口驱动段码,P2口做位选。

unsigned char code seg_code[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // 共阳段码 0-9 void display_distance(unsigned int cm) { unsigned char thousands = cm / 1000; // 千位,一般不会超过4 unsigned char hundreds = (cm / 100) % 10; unsigned char tens = (cm / 10) % 10; unsigned char ones = cm % 10; // 位选选择第1位 P2 = 0x01; P0 = seg_code[thousands]; delay_scan(); P2 = 0x02; P0 = seg_code[hundreds]; delay_scan(); P2 = 0x04; P0 = seg_code[tens]; delay_scan(); P2 = 0x08; P0 = seg_code[ones]; delay_scan(); }

段码表使用共阳数码管对应的编码,如果你手里的板子是共阴,需要改用另一组段码数组。这里的delay_scan()一般延时1-2ms,四个位轮流一遍大约5ms,刷新率200Hz,足够稳定。需要注意的是,在切换位选的瞬间,P0口会先输出新的段码,而此刻位选可能还没切换,会造成短暂重影。解决方法先关闭所有位选,再送段码,最后打开目标位选。上面的代码如果没有消隐,在高亮度下能看到残影。改进后的写法是每个位都先P2=0x00,再P0=段码,再P2=对应位。

4.2 报警阈值和蜂鸣器动作逻辑

报警系统的核心是设置一个安全距离阈值,当测量距离小于阈值时触发蜂鸣器。阈值可以通过按键调整,固定在程序里也可。源工程中通常定义一个宏或者变量,比如ALARM_DISTANCE=30,单位厘米。当distance不为0且小于等于30时,蜂鸣器响。这里要考虑到超声波模块盲区,通常2cm以内的距离测量不准确,应当忽略或显示特殊值。

#define ALARM_THRESHOLD_CM 30 void check_alarm(unsigned int dist) { if (dist > 0 && dist <= ALARM_THRESHOLD_CM) { BEEP = 0; // 低电平驱动蜂鸣器,根据硬件调整 } else { BEEP = 1; } }

蜂鸣器驱动方式很关键。有源蜂鸣器只需要给高低电平就能发声,无源蜂鸣器需要PWM方波驱动。51单片机IO口驱动能力弱,一般通过PNP三极管或ULN2003驱动。如果直接接IO口,可能推不动。源工程里如果是BEEP=0响,说明使用了低电平有效的有源蜂鸣器。如果需要动态报警节奏,可以在定时器中断里翻转BEEP,例如每100ms翻转一次,形成滴滴声。这需要把测距放在主循环里,定时器0用于产生系统时基,定时器1用于测距,这样就不会互相干扰。

4.3 主循环里的任务调度

把测距、显示、报警放在一个循环里,最简单但要注意时序拖累。每次测距触发后,等待Echo完成大约需要几十微秒到20毫秒,这期间单片机浪费在循环等待上。如果显示扫描也放在主循环,会导致显示亮度不均匀,测距等待时间越长,显示刷新越慢。

更好的做法是把测距放在一个状态机里,或者干脆在等待Echo期间同时进行数码管扫描。因为动态扫描的延迟函数本身就是空耗时间,可以利用这段时间来做测距等待。但工程代码为了可读性,通常顺序执行。对于距离频繁变化的场景,顺序执行没问题。如果追求稳定显示,可以把刷新间隔压缩到1ms,并保持循环频率在50Hz以上。

这里有一个常见的坑:每次测距返回后,需要等待一段时间才能进行下一次触发。HC-SR04要求两次触发间隔至少60ms,否则上一次回波可能还没传播完,测量会不准确。所以主循环里的测距周期不能太短,控制到100ms左右比较合理。可以通过一个软件计数器或者延时函数保证周期。

5. 在Keil中编译源码时的工程文件陷阱与HEX烧录验证

5.1 工程文件差异:uvproj、uvopt、plg和M51分别是什么

压缩包里“基于51单片机超声波测距报警系统.uvproj”是Keil μVision4/5的工程文件,双击即可打开。uvopt文件保存的是编译器选项、调试器设置和窗口布局,不同电脑打开可能因为路径问题报错。plg是编译过程的日志文件,M51是链接后的内存分配文件。一旦你移动了工程目录,Keil找不到原始路径可能会提示“missing uvopt”,这时打开uvproj重新配置一下输出路径即可。

第一次编译时,建议先执行一次“Rebuild All target files”,因为旧的LST文件、OBJ文件可能和当前源码版本不一致。OBJ目录里存放着编译中间产物,比如目标文件的绝对路径记录在lnp文件中。如果你把工程放在中文路径下,个别旧版本Keil可能编译失败,解决办法是把工程目录改成全英文。

5.2 生成HEX文件并烧录到51单片机

源工程已经包含HEX文件,如果想修改源代码后重新生成,需要在Keil里配置Output选项卡。勾选Create HEX File后,编译成功会生成新的HEX。默认生成在工程根目录或OBJ目录里,取决于Output选项卡的Select Folder for Objects。

烧录工具根据你的单片机型号选择:AT89C52用ESP烧录器,STC系列用STC-ISP。注意烧录前要选择正确的单片机型号,并设置波特率。如果使用STC89C52,还需要在STC-ISP里选择“双倍速”或“12T”模式,必须和代码里的循环延时匹配。常见错误是烧录后数码管显示乱码,多半是单片机的时钟模式选错导致延时不对。12MHz晶振对应12T模式,如果误选6T,延时变成原来的一半,触发脉冲宽度不足10us,导致测距无反应。

5.3 实机调试时的波形测量方法

没有示波器很难判断Echo信号是否正常。最简单的办法是写一个测试程序:触发后,如果Echo无响应,点亮一个LED,如果Echo响应则熄灭。这样可以快速判断模块好坏。用万用表测量Echo引脚电压,在触发后如果保持0V,说明模块没有正常工作,可能Trig脉冲宽度不够或者模块损坏。

如果想更精确地确认定时器测量结果,可以把time_us通过串口发送到电脑串口助手。在代码里添加UART初始化函数,然后把time_us拆成高8位低8位发送。串口调试时,将障碍物放在10cm处,观察串口输出应为588us左右。如果偏差超过10us,检查晶振实际频率和定时器配置。我一般会用逻辑分析仪抓Echo引脚,直接读取高电平宽度,和单片机计算结果比对,这样能定位是软件测量问题还是模块信号问题。

最后分享一个调参技巧:报警阈值不要太死板,固定30cm在电路上测试,地面反射可能导致一些误报。建议把阈值定义成变量,通过两个按键调整,存储在EEPROM里(如果单片机支持),这样实机调试时不用反复烧录。整个工程跑顺之后,你会发现51单片机虽然老,但把IO、定时器、中断这些资源安排得明明白白,反而比用STM32更锻炼电路理解力。

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

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

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

立即咨询