51单片机超声波测距仪源程序详解:HC-SR04定时器测距与实战优化 📅 发布时间:2026/9/17 15:06:36 👁 浏览次数: 简介基于51单片机的超声波测距仪参考源程序是一套面向单片机学习者、电子竞赛选手及嵌入式初学者的完整工程代码用于解决超声波测距、数码管显示和近距离报警等常见实践问题。压缩包共14个文件体积仅54KB以C语言源文件、Keil工程文件uvproj、uvopt及编译生成的hex、obj、m51、lst等为主既可直接在开发环境中打开查看也能烧录到单片机验证效果。已有2966人学习该资源其核心代码包含系统初始化、超声波脉冲发射、定时器时间测量、距离换算与显示、安全距离报警等模块逻辑清晰便于逐段理解。对于正在做课程设计或想掌握HC-SR04类传感器应用的开发者而言这套源码提供了从硬件控制到软件处理的完整参考有助于快速搭建测距功能并拓展报警等附加能力。1. 超声波测距仪参考源程序在 51 单片机上的整体套路把标题拆开看超声波测距仪、参考源程序、51 单片机测距这三样凑在一起其实是很多倒车雷达、智能浇水、避障小车项目的起点。手里拿着一份参考源程序最怕的不是读不懂代码而是不知道这段代码为什么能测出距离、改哪个参数会对精度有什么影响。直接说结论51 单片机测距的参考源程序九成以上都围绕 HC-SR04 这类模块展开核心动作就两个——用 TRIG 引脚发一个至少 10 微秒的高电平脉冲然后用定时器去量 ECHO 引脚维持高电平的时间。时间乘以声速再除以 2就是距离。这套方案说穿了不值钱但落地时常踩坑定时器被串口占着、ECHO 高电平时间超过定时器最大计数、晶振频率导致波特率偏了 3% 串口全是乱码。这篇文章不猜原作者的意图只按工程师手头会做的那套把 TRIG/ECHO 时序、定时器门控位 GATE 测宽、距离换算、串口打印、温度补偿和 Proteus 仿真排错全部串起来让新手照着能把参考源程序改成自己项目能跑的样子让熟手能拿到边界参数和几个平时不太注意的细节。2. HC-SR04 测距时序与 51 单片机定时器的配合方法2.1 TRIG 触发信号的产生10 微秒高电平HC-SR04 的工作时序是主机给 TRIG 引脚一个不小于 10us 的高电平模块内部发出 8 个 40kHz 的超声波脉冲然后 ECHO 引脚被拉高。ECHO 维持高电平的时间就是声波从发射到遇到障碍物返回的总时间。参考源程序里第一步几乎都是把 TRIG 拉高延时再拉低问题就出在延时精度上。用for循环做空转延时在 12MHz 晶振下勉强能用但如果源程序里用了其他晶振频率又不改循环次数实际延时可能只有 7 到 8 微秒模块不会稳定触发。我一般直接用定时器做精确延时或者用两个nop()兜底。实测多次后觉得最稳妥的做法是TRIG 拉高后延时 20us 再拉低多给一倍余量模块不会因为触发脉冲过长而报错。#include reg52.h #include intrins.h sbit TRIG P2^0; sbit ECHO P2^1; void delay_us(unsigned int us) { unsigned int i; for (i 0; i us; i) { _nop_(); _nop_(); // 12MHz 下约延时 2us具体看编译器优化 } } void send_trig_pulse(void) { TRIG 1; delay_us(20); // 比最低要求的 10us 翻一倍 TRIG 0; }_nop_()在 Keil C51 里是一条空指令12MHz 下执行一次约 1us但编译器不开优化时可能被优化掉所以参考源程序里经常看到delay_us里夹两个_nop_()就是为了拿住这个时间。这个函数只负责让模块启动测量真正的高精度计时要靠下一步的定时器触发延时差几微秒对结果影响很小。2.2 ECHO 回响电平与定时器 GATE 位不用轮询的测宽方式大多数参考源程序用while(ECHO 1)来等 ECHO 变低然后读定时器。这个写法有个隐蔽问题单片机在做 while 等待时如果开了中断主循环会被打断计时的起止点就会错位。如果不开中断那整个单片机在测距期间什么都干不了。更专业的参考源程序会用到定时器的 GATE 位也就是门控位。51 单片机的定时器工作在方式 1 时TR0 置 1 且 GATE 置 1此时只有当 INT0P3.2引脚为高电平时定时器才计数INT0 拉低就暂停。把 ECHO 引脚直接接到 INT0 上定时器就能自动量出 ECHO 高电平持续的时间期间 CPU 不用死等甚至可以跳到下一行代码继续做别的事情。这个技巧在很多抄来抄去的参考源程序里会莫名被删掉然后换成一堆 while 循环但实际上 GATE 位是 51 单片机手册里白纸黑字写好的功能。初始化代码这样写void timer0_init_for_echo(void) { TMOD 0xF0; // 只修改低 4 位保留高 4 位给 T1 TMOD | 0x09; // 定时器0 方式1GATE116位定时模式 TH0 0x00; TL0 0x00; TR0 1; // 启动定时器但 GATE1 时由 INT0 引脚控制计数 }TMOD 0x09对应二进制 0000 1001其中第 3 位 GATE 置 1第 1 位和第 0 位是 01 表示方式 1。这里有一个新手常见的误区TR0 设置了 1但 GATE1 且 INT0 为低时定时器实际上不会走。所以必须在发送完 TRIG 脉冲之后ECHO 拉高的瞬间定时器才真正开始计数。测距完成后再把 TR0 清 0 是多余的吗不是读取 TH0 和 TL0 之前建议先关掉 TR0防止在读取高字节和低字节的间隙定时器还在跑。2.3 定时器溢出ECHO 高电平超过 65 毫秒怎么办16 位定时器在 12MHz 晶振下最多计 65535us也就是约 65ms。声速约 340m/s65ms 对应往返距离约 22 米单程约 11 米。HC-SR04 的量程一般标注是 4 米到 5 米正常使用 65ms 是够用的。但参考源程序如果是从网上扒来的往往会忽略一个问题当障碍物超出量程或者根本没障碍物时ECHO 会被一直拉高定时器溢出中断触发后 TH0 和 TL0 归零并继续计数这时主程序读到的数据是错的。参考源程序里要么检测溢出标志位 TF0要么用软件计数器记录溢出次数我用后者因为它更直观unsigned int overflow_count 0; void timer0_isr(void) interrupt 1 { overflow_count; // 每次溢出约 65ms记录次数用于判断超量程 TH0 0x00; // 重装初值和硬件清零不同必须手动装 TL0 0x00; }unsigned long read_echo_time_us(void) { unsigned long time_us; if (overflow_count 2) { // 超过 130ms 还没等到回波 return 0; // 返回 0 表示超量程 } time_us (unsigned long)overflow_count * 65536UL (unsigned int)(TH0 8 | TL0); TR0 0; // 读完立刻关 }overflow_count最大到 2 就视为超量程对应测距范围约 22 米已经在模块能力之外。实际使用时如果量程只需要 2 米这个阈值还能更早截断加快测量周期。注意(unsigned long)overflow_count必须强转否则 16 位乘 16 位在 Keil C51 里会溢出。3. 参考源程序的距离计算、LCD 显示与串口打印的实现3.1 距离换算为什么厘米等于微秒除以 58拿到 ECHO 高电平的微秒数后距离公式是距离厘米 时间微秒 / 58。这个 58 的来历是声速 340m/s往返距离等于声速乘以时间再除以 2。340m/s 换算成厘米每微秒是 0.034cm/us乘 0.5 得到 0.017cm/us取倒数就是约 58.8。参考源程序里直接用 58 是工程简化但严格说是按 340m/s 近似出来的实际声速在 25 摄氏度时是 346.3m/s这时正确换算系数是 57.7。温度一变同样的时间数值算出的距离就会差 2 到 3 厘米这个在第 4 章展开。时间除以 58 在 51 单片机上有几种写法。直接定义成浮点除法最省事但 Keil C51 的软件浮点库要占好几 KB 代码空间而且执行慢。参考源程序如果比较讲究会用整数除法配合余数处理或者干脆用查表。典型写法是unsigned int calc_distance_cm(unsigned long time_us) { unsigned int distance_cm; if (time_us 0) { return 0xFFFF; // 0xFFFF 表示超量程 } distance_cm (unsigned int)(time_us / 58UL); return distance_cm; }time_us / 58有个精度隐患整数除法会把余数直接丢掉测量结果呈阶梯状跳变。要缓解这个可以改成(time_us 29) / 58相当于四舍五入这样在边界处偶尔跳动的情况会好一些。想要更准就把 58 换成 57.7 的整数近似比如time_us * 10 / 577。这是参考源程序里不太会写但实测有效的优化点。3.2 串口通信与 LCD1602 双通道输出做完距离计算参考源程序通常会把结果显示在 LCD1602 或数码管上同时通过串口发出去调试。51 单片机的串口通信在参考源程序里的实现基本是固定的定时器 1 做波特率发生器工作在方式 28 位自动重装SM0 和 SM1 设置为 01 表示串口方式 1。波特率 9600 是默认选择因为 9600 在 11.0592MHz 晶振下能精确生成误差几乎为零。如果用的晶振是 12MHz波特率误差会达到 8.5%串口打印出来的数字就乱成一团。这个坑在基于 51 单片机的倒车雷达报警系统里尤其常见因为这类设计经常 12MHz 晶振配串口结果调试信息没法看。void uart_init(void) { SCON 0x50; // 0101 0000方式1REN1 允许接收 TMOD 0x0F; // 只修改高 4 位 TMOD | 0x20; // 定时器1 方式28位自动重装 TH1 0xFD; // 11.0592MHz 下 9600 波特率对应的重装值 TL1 0xFD; PCON 0x00; // SMOD0 ES 1; // 打开串口中断 EA 1; // 打开总中断 TR1 1; // 启动波特率发生器 } char putchar_char(char c) { SBUF c; while (TI 0); // 等待发送完成 TI 0; return c; }TH1 0xFD只对 11.0592MHz 有效换成 12MHz 要改成 0xF3 附近才有约 2% 的误差。参考源程序里如果直接抄 0xFD 而不看晶振出现乱码先查这一行。串口和 LCD 同时使用时要注意串口发送是阻塞式的while (TI 0)会卡住主循环一段几十微秒的时间如果刚好在超声波测距期间执行ECHO 高电平的时间测量就受到干扰。常用解法是测距完成后把数据缓存起来所有显示串口操作放在测距间隙执行。LCD1602 的显示逻辑相对简单初始化后写命令和写数据各一个函数显示距离时要把整数转换成字符串。距离显示建议带一位小数哪怕整数除法的分辨率不够也要在代码里保留几位小数位的变量类型防止日后升级测量精度时推倒重来。void display_distance(unsigned int dist_cm) { unsigned char buf[16]; if (dist_cm 0xFFFF) { lcd_write_str(0, OUT OF RANGE); } else { sprintf((char*)buf, DIST: %d cm, dist_cm); lcd_write_str(0, buf); } }sprintf在 51 上开%d格式化会引入很大的代码量所以很多参考源程序会手写一个 int 转字符串的函数。运行空间不足时优先替换这个而不是把%d换成%u再骂编译器两者占用的代码空间几乎一样。显示部分的关键是不在主中断里调用 LCD 操作因为 LCD 写入时序要等忙标志会拖住主流程。3.3 测量周期调度多久测一次合适HC-SR04 数据手册建议两次触发之间至少间隔 60ms这是因为模块收到 TRIG 后要等回波完全消失才能开始下一次测量。参考源程序里常见到while(1)里直接循环测距没加间隔结果距离数值会周期性抖动。推荐做法是主循环里维护一个计数变量每 100ms 触发一次测距留给模块充足的处理时间。这个调度逻辑对 51 单片机来说已经足够顺滑因为超声波测距本身不是高刷新率应用屏幕 10Hz 的刷新也够用了。4. 测量精度、温度补偿与阈值报警的实战参数调整4.1 温度对声速的影响程序里的声速不是常数超声波在空气中的传播速度大约每升高 1 摄氏度增加 0.607m/s。20 摄氏度时声速 343.4m/s40 摄氏度时 355.5m/s差了 3.5%。在 3 米距离上这相当于 10 厘米的出入口差异冷库或室外设备的测距误报很多就来自这里。参考源程序里如果写死#define SPEED_OF_SOUND 340那它基本只适合 15 摄氏度左右的实验环境。工程上都用温度传感器做补偿51 单片机配 DS18B20 是常见组合。DS18B20 读出温度后用查找表修正超声波模块的距离值比直接调公式更适合单片机因为浮点运算速度太慢。温度摄氏度声速m/s1 米距离下相对 340m/s 的误差mm10337.57.520343.410.025346.318.530349.126.040355.042.0表格里 25 摄氏度时 1 米距离误差接近 2 厘米如果设计目标是倒车雷达在 30 厘米处报警这个误差就可能导致撞上之前没响。参考源程序里加上温度补偿的常见做法是读温度然后根据温度范围选一个修正系数对距离做乘除调整。代码层面用浮点即可或者用整数查表。unsigned int temp_compensated_distance(unsigned int raw_cm, unsigned int temp_celsius) { static const unsigned int speed_tab[] {337, 340, 343, 346, 349, 352, 355}; unsigned int index temp_celsius / 5; unsigned int speed; if (index sizeof(speed_tab) / sizeof(speed_tab[0])) { index sizeof(speed_tab) / sizeof(speed_tab[0]) - 1; } speed speed_tab[index]; return (unsigned int)((unsigned long)raw_cm * speed / 340); }这个查表函数按 5 摄氏度一档取声速值raw_cm * speed / 340把原始距离按比例放大或缩小。代码里乘除顺序刻意先乘后除避免整型除法把小数截得太狠。查表粒度可以调成 2 摄氏度一档但要手动保证数组够长。需要注意补偿只对声速变化有效湿度、风、温度梯度这些变量这里没考虑进去户外长距离测距时它们是主要误差源。4.2 阈值报警与回差为什么距离在报警值附近会反复响基于 51 单片机的倒车雷达报警系统设计里参考源程序通常会在主循环里加一个 if 判断距离小于等于阈值时让蜂鸣器响。问题出在阈值临界处如果障碍物缓慢靠近又缓慢远离实际距离在 30cm 上下波动蜂鸣器就会吱、停、吱、停这就是没有回差hysteresis导致的。工程做法是设置两个阈值进入报警区域时使用 30cm离开报警区域时必须超过 35cm 才解除报警。这样测距值在 30 到 35cm 之间存在一个死区蜂鸣器不会抖动。#define ALARM_ON_THRESHOLD 30 #define ALARM_OFF_THRESHOLD 35 unsigned char alarm_state 0; void check_alarm(unsigned int dist_cm) { if (alarm_state 0) { if (dist_cm ALARM_ON_THRESHOLD) { alarm_state 1; buzzer_on(); } } else { if (dist_cm ALARM_OFF_THRESHOLD) { alarm_state 0; buzzer_off(); } } }alarm_state是记忆变量保存当前是否处于报警状态。这段代码的要点是而不是因为距离正好等于 35cm 时应该保持报警状态防止边界处误切换。参考源程序里如果把回差写成ALARM_OFF_THRESHOLD ALARM_ON_THRESHOLD那这段代码就退化成了普通单阈值判断失去防抖效果。回差大小按测量噪声幅度设大概取传感器标称误差的 5 到 10 倍比较合理。4.3 晶振选择与定时器重装值一个项目选错全盘乱参考源程序里五花八门的定时器重装值根源都在晶振上。使用 12MHz 时每条机器周期精确 1us定时器算时间最容易。但 12MHz 算不了 9600 以上标准波特率参考源程序在 12MHz 下经常用 4800 波特率做串口。使用 11.0592MHz 时串口波特率可以精确到 9600、19200、115200 这些整数但定时器测时间就得换算一条机器周期约 1.085us每次计数对应的时间不是整数距离计算就有系统性偏差。两种晶振不存在谁更好只看项目里串口和测距哪个优先级更高。晶振频率机器周期9600 波特率 T1 重装值1us 计时误差12MHz1.000us约 0xF3误差较大无11.0592MHz1.085us0xFD误差 0%每次计数多 0.085us表中的计时误差在短距离时微乎其微但 4 米距离时累计误差可能超过 1 厘米。追求极限精度就用 12MHz 加外部高精度测量需要稳定串口打印调试信息就换 11.0592MHz 并把换算系数微调。代码里定时器的初值如果用的是定时器 0 方式 1那么在中断里重装时初值必须写回 TH0 和 TL0这是模板程序里最容易被连带删除的三行。5. Proteus 仿真调试与故障快速定位技巧5.1 Proteus 里没有 HC-SR04 模型时的替代方案Proteus 仿真 51 单片机项目很常见但超声波测距模块未必能在元件库里找到完整型号。常见做法是用信号发生器替代 ECHO 信号TRIG 仍由单片机发出然后让信号发生器输出一个宽度随按键变化的脉冲接到 P3.2也就是 INT0。这样能验证定时器门控、溢出处理和距离换算这三段核心逻辑是否正常。脉冲宽度按实际距离换算比如模拟 1 米距离需要给 5800us 的高电平在信号发生器里把脉冲宽度设成 5.8ms 即可。如果连信号发生器都调不了就写一段测试程序让另一个定时器引脚输出指定宽度脉冲循环喂给 ECHO。这套替代方案的目的是把超声波模块的物理部分剥离掉专注验证代码逻辑。5.2 三个最容易误判的问题第一个是距离永远显示 0参考源程序里超量程返回值设成 0 时障碍物太近或 ECHO 信号没接对都会显示 0。先用万用表量 ECHO 有没有电平变化再用示波器看 TRIG 波形确认 10us 以上的高电平确实发出了。第二个是串口打印乱码大概率是晶振和波特率重装值不匹配优先查 TH1 的值和晶振频率不要先怀疑代码。第三个是 Proteus 仿真里定时器不走十有八九是 GATE 位被当成了普通模式ECHO 没有电平跳变所以定时器永远停着。用示波器在仿真里看 P3.2 引脚波形有 5.8ms 的高电平但定时器没计数就是 TMOD 寄存器被人改了高 4 位导致 T1 方式错乱。参考源程序拿来用之前先编译一遍看有没有 unused warning再把 TMOD 的赋值语句单独摘出来核对能省掉大半天的仿真排错时间。本文还有配套的精品资源点击获取