STC15单片机四路超声波测距与OLED显示实现
2026/9/16 14:20:24 网站建设 项目流程

简介:基于STC15单片机(8051内核)的超声波测距OLED显示设计资料,包含四路超声波测距方案与完整硬件原理图,面向单片机初学者、嵌入式开发人员以及测距类课程设计,具有较强的参考与移植价值。压缩包共12个文件,覆盖C语言源文件、头文件、Keil工程文件、原理图文件与hex可烧录文件,整体大小仅70KB,结构清晰,便于直接导入Keil编译与验证。已有327人学习/下载。资源提供了从STC15初始化、超声波发射与回波捕获、基于声速与时间差的距离计算,到OLED显示驱动的完整例程;配合原理图可系统理解I/O控制、定时器计时、I2C/SPI通信、中断处理等知识点。四路设计支持同时测量多个方向,适合扩展至机器人避障、区域监测等场景,同时利用STC15的IAP在线编程特性,可大幅简化固件调试流程,对掌握嵌入式软硬件协同开发很有帮助。

1. 超声波测距从原理图到固件,难的不是探头而是定时器

超声波测距从原理图到能跑通的固件,门槛不在 HC-SR04 的那根 Trig 引脚上,而在 STC15 的定时器怎么不失真地记录回波脉宽。这个项目把 STC15 单片机、四路 HC-SR04 和一个 I2C 接口的 OLED 屏组合在一起,用 8051 内核里最朴素的定时器资源解决测距计时,最后在屏幕上实时刷新厘米级距离。它与 STM32 加 HAL 库那类玩法完全不同:没有现成的固件库可调,中断和定时器全部靠寄存器配置,反而更容易把测距时序抠清楚。如果你之前在 STM32 上点亮过 OLED,再回头用 STC15 会明显感觉到寄存器操作的直接感。这个设计适合正在做智能小车避障、低成本多方向探测的嵌入式开发者,也适合课程设计里需要把单片机、传感器和显示技术串成完整闭环的人。

2. STC15与HC-SR04的时序配合:触发信号与回波捕获的硬件基础

2.1 HC-SR04的引脚特性与STC15的IO差异

HC-SR04 一共四个引脚:VCC、Trig、Echo、GND。单片机给 Trig 一个大于 10us 的高电平,模块内部会发出 8 个 40kHz 的超声波脉冲,然后 Echo 引脚输出一段高电平,这段高电平的宽度正好等于声波从发射到反射回来的总时间。所以整个测距系统要解决的核心问题只有一个:精确测量 Echo 高电平持续的时长。

STC15 的 IO 默认工作在准双向口模式,读外部引脚电平之前要先向端口锁存器写 1,否则可能读到内部拉低后的错误电平。这一点在接 Echo 引脚时尤其容易踩坑:如果只是把sbit ECHO = P2^1;定义好就直接读,而没有先ECHO = 1;,回波信号来了也可能一直读到 0,程序会死死卡在等待回波的 while 循环里。原工程的原理图里 Echo 引脚直接连到 STC15 的普通 IO,软件上只要在初始化时统一把 Echo 对应的引脚置高即可。

2.2 原理图里容易被忽略的三个电路细节

第一个是电源滤波。HC-SR04 发射瞬间电流变化很快,如果模块和单片机共用一个稳压输出,超声波脉冲会引起 VCC 上几十毫伏的毛刺,严重时会让 STC15 误复位。原理图上最好在模块供电端并联 100uF 电解电容和 0.1uF 瓷片电容,两者各司其职,电解电容扛低频跌落,瓷片电容吸收高频干扰。

第二个是 Echo 引脚的保护。项目使用的 HC-SR04 模块 Echo 输出是 5V 逻辑电平,而 STC15 系列既有 5V 型号也有 3.3V 型号。如果单片机供电是 5V,Echo 可以直连;如果是 3.3V 版本,推荐加一级 1k 电阻串联或者用两个电阻分压到 3.3V。原理图上虽然没有标注这个细节,但按我调类似模块的经验,串一个 1k 电阻对 IO 的静电保护和误接反压都有实际意义。

第三个是 OLED 屏 I2C 总线的上拉电阻。SSD1306 控制器的 OLED 小屏,SCL 和 SDA 两根线通常需要外部上拉到 VCC,阻值 4.7k 比较常见。有些现成模块已经焊好上拉电阻,直接从 STC15 的 IO 接过去就能用;有些模块没有,这时如果总线时序不对,屏幕初始化之后没有任何反应,先在原理图上找一找有没有这两颗电阻。

2.3 四路传感器的IO分配参考

四路测距意味着四个 HC-SR04 要同时挂在 STC15 上。Trig 引脚是输出,Echo 引脚是输入,分配时优先保证 Echo 占用的引脚支持外部中断,为后面做并行捕获留余地。下面这张表是我参考 STC15 系列常见封装整理的一种分配方式,对应原理图里的 P2 口:

通道Trig引脚Echo引脚外部中断说明
CH1P2.0P3.2INT0优先作为中断通道
CH2P2.2P3.3INT1优先作为中断通道
CH3P2.4P2.5轮询读取
CH4P2.6P2.7轮询读取

如果封装上没有这么多 P2 引脚,也可以把全部 Trig 并联到同一个引脚,用同一根线同时触发四个模块,然后分别读取四路 Echo,这是后面章节要讲的并发思路。原理图里没有固定这个分配,实际布线时只要保证 Echo 不走太长飞线,避免和 Trig 平行走线产生串扰就行。

2.4 定时器与GPIO的初始化代码

STC15 的定时器初始化参数直接影响后续测距的量程和分辨率。这里给出基于 Keil5 工程风格的初始化代码,定时器 0 工作在 16 位模式,选择 12T 分频而不是 1T 分频,原因在下一章详细说明:

#include "STC15F2K60S2.h" #include <intrins.h> sbit TRIG1 = P2^0; sbit ECHO1 = P3^2; void GPIO_Init(void) { TRIG1 = 0; // Trig默认输出低电平 ECHO1 = 1; // 准双向口读引脚前先置1 } void Timer0_Init(void) { AUXR &= ~0x80; // 清T0x12,定时器0工作在12T模式 TMOD &= 0xF0; // 只清T0的模式位,不影响T1 TMOD |= 0x01; // T0为16位定时器,不自动重装 TH0 = 0; TL0 = 0; ET0 = 0; // 测距主流程用查询方式,不用中断 TR0 = 0; // 先不启动,等待触发后手动打开 }

代码里AUXR &= ~0x80这一行是关键,很多从 STM32 转过来的开发者习惯性认为时钟越快越好,结果把 T0x12 置 1 选成 1T 模式。12T 模式下定时器计数频率是系统时钟的十二分之一,在 11.0592MHz 晶振下大约是 921.6kHz,每秒计 92 万个脉冲,每个计数对应约 1.085us;1T 模式下计数频率直接等于系统时钟,11.0592MHz 时 16 位计数器在约 5.9ms 内就会溢出,这个时间只够大概 1 米范围内的测距。四路 HC-SR04 的标称量程是 2cm 到 400cm,4 米对应约 23ms 的往返时间,1T 模式根本撑不住。

3. 定时器计时与距离换算:16位计数器在1T/12T模式下的量程边界

3.1 Echo脉宽与定时器计数的对照关系

声速在 20℃ 空气里约 343m/s,即每微秒走 0.343 毫米。超声波测距测的是往返时间,所以距离等于时间乘声速再除以 2。HC-SR04 的 Echo 高电平宽度与距离严格成正比,下表列出了几个典型距离下的时间参数,按 12T 模式、11.0592MHz 晶振、每计数约 1.085us 计算:

目标距离单程时间Echo高电平宽度12T模式计数值16位余量
2cm58.3us116.6us10765429
10cm291.5us583us53764999
100cm2.915ms5.83ms537260164
400cm11.66ms23.32ms2149344043

从表中可以看到,12T 模式下即使测到 4 米,计数也只用到 21493,距离 16 位定时器的 65536 上限还有充足余量。而如果误选 1T 模式,11.0592MHz 下最大计时仅 5.9ms,算下来极限距离刚过 1 米,超出量程后定时器已经翻转,算出的距离会变得毫无规律。这就是上一章代码里坚持用 12T 模式的根本原因。

3.2 带超时保护的测距函数

触发 HC-SR04 的流程并不复杂:Trig 拉高 10us 以上再拉低,然后等 Echo 变高开始计时,等 Echo 变低停止计时,读取定时器值换算距离。真正考验代码鲁棒性的地方在等待 Echo 下降沿这一步,如果前方没有障碍物或者目标超出量程,Echo 可能一直保持高电平,程序就会死循环。下面的函数加入定时器溢出检测,用 TF0 标志判断是否超时:

unsigned int GetDistance1(void) { unsigned int time_count; TRIG1 = 1; _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); TRIG1 = 0; // 保持约10us高电平,触发模块 TH0 = 0; TL0 = 0; TF0 = 0; TR0 = 1; // 启动定时器0 while (ECHO1 == 0) // 等待回波上升沿 { if (TF0) // 等待超时,说明没有回波 { TR0 = 0; return 999; // 返回超量程标识 } } TH0 = 0; TL0 = 0; TF0 = 0; // 上升沿到达,重新清零计时 while (ECHO1 == 1) // 等待回波结束 { if (TF0) { TR0 = 0; return 999; } } TR0 = 0; time_count = ((unsigned int)TH0 << 8) | TL0; return time_count / 54; // 11.0592MHz + 12T,约54个计数等于1cm }

这里time_count / 54是简化换算系数,推导过程是:每个定时器计数约 1.085us,声速 34300cm/s,往返旅程除 2,得到每厘米需要约 54 个计数。如果板子换成 12MHz 晶振,12T 模式下每计数 1us,每厘米对应约 58 个计数,系数就要改成 58。Keil5 工程里用(unsigned int)TH0 << 8而不是直接TH0 << 8,是为了避免 8 位变量左移时被编译器截断成意外结果。

3.3 等待上升沿阶段的清零时机问题

上面代码里有两个清零动作,第一个是在触发后的定时器清零,目的是给整个回波等待过程设一个时间上限,防止 Trig 拉了之后根本没有回波;第二个是在捕获到上升沿之后再清零一次,目的是把等待上升沿消耗的时间剔除出去。如果没有第二个清零,Echo 在 2 米处才拉高,此前空的等待时间会被一起算进回波脉宽,最终显示的距离会比实际距离偏大。

常见的一个错误写法是只在触发前清零一次,然后先等上升沿再等下降沿,直接读定时器。这种写法在近距离测量时误差不大,因为上升沿在几百微秒内就会到来,但距离拉到 3 米开外后,等待上升沿的时间本身可能接近 20ms,和真正的回波宽度在同一数量级,测出的距离几乎翻倍。把清零动作放到上升沿捕获之后,是保证测量值只反映 Echo 高电平宽度的关键。

3.4 定时器溢出返回值的工程约定

返回 999 表示超量程,这是 HC-SR04 类程序里比较通行的约定。主流程拿到 999 后,OLED 上可以显示三条横线或者 OFFLINE 之类的提示,不要让 999 直接参与后续取平均值或者避障判断。需要注意 HC-SR04 的最小盲区约 2cm,在这个距离内模块会把发射探头的余振误判成回波,测量值经常固定在某个偏大的数字上毫无波动。发现 OLED 上近距离读数不变化时,优先考虑是不是进入了盲区,而不是怀疑代码逻辑。

4. 四路并行测距与OLED驱动:IO分配和显存刷新的协同设计

4.1 串行轮询四个通道的代价

四个 HC-SR04 如果按最简单的串行方式逐通道测距,主流程依次调用GetDistance1GetDistance2直到第四路,每路最坏情况要等满 4 米对应的 23ms,四路加起来接近 100ms,OLED 显示刷新率只能做到 10Hz 左右。这个刷新率对静态测距或障碍物提示没问题,但如果用在移动小车上,10Hz 意味着小车开 0.5m/s 时每帧之间有 5cm 的盲区,避障响应明显不够跟手。

串行轮询的好处是代码结构最简单,四路的软件逻辑完全一致,不需要处理中断嵌套和共享定时器的竞争问题。原工程的olcd.uvproj里如果没有编译并烧录过,建议先把串行版本跑通,OLED 能看到四路距离都已经正常刷新,再考虑并发优化。并发方案是在串行版本基础上改出来的,而不是另起炉灶。

4.2 同时触发与中断捕获的并发思路

并发方案的核心是让四路模块同一时刻开始测距,声波飞行时间只消耗一次,而不是四次。做法是把四个 Trig 引脚全部并联到 STC15 的同一个 IO,触发后四个模块同时发出超声波脉冲,然后四路 Echo 各自返回高电平,宽度分别代表四个方向的障碍物距离。读取阶段用 STC15 的 INT0 到 INT3 四个外部中断,分别接四路 Echo 的下降沿,中断服务里读出当前定时器值存到对应全局变量。

void INT0_Isr(void) interrupt 0 { TR0 = 0; // 停止计时,保存当前计数 dist_ch1 = ((unsigned int)TH0 << 8) | TL0; } void INT1_Isr(void) interrupt 2 { TR0 = 0; dist_ch2 = ((unsigned int)TH0 << 8) | TL0; }

注意这段代码的局限:第一个下降沿到来后 TR0 被关闭,后续几路中断读到的计数值相同。要在中断里保留各自的时间戳,正确做法是保持 TR0 一直运行,中断里只做保存计数值的操作,等到最晚的一路回波结束后再由主循环关闭定时器。同时触发还有一个理论问题:四路 Echo 上升沿到达时刻不同,而我们要测的是每路高电平宽度,不是绝对到达时刻,所以严格做法是每个通道单独捕获上升沿和下降沿两次沿。STC15 一个外部中断只能配置成上升沿或者下降沿触发,工程上通常保留轮询方式等待上升沿再开启对应中断捕获下降沿,或直接用轮询读完四路,并发方案在中断资源不足时并不比串行快多少。

4.3 串行轮询四路的主循环结构

考虑到多数使用场景测量的是静态或慢速目标,工程里最稳妥的还是串行轮询,配合 OLED 刷新。主循环只要做到三件事:依次测四路距离、把结果写入 OLED、控制两次测量间的间隔防止屏幕上数字跳得太快:

void main(void) { unsigned int d1, d2, d3, d4; GPIO_Init(); Timer0_Init(); OLED_Init(); while (1) { d1 = GetDistance1(); d2 = GetDistance2(); d3 = GetDistance3(); d4 = GetDistance4(); OLED_ShowString(0, 0, "CH1:"); OLED_ShowNum(5, 0, d1, 3); OLED_ShowString(0, 2, "CH2:"); OLED_ShowNum(5, 2, d2, 3); OLED_ShowString(0, 4, "CH3:"); OLED_ShowNum(5, 4, d3, 3); OLED_ShowString(0, 6, "CH4:"); OLED_ShowNum(5, 6, d4, 3); delay_ms(50); // 限制显示刷新频率,避免数字闪烁 } }

OLED_ShowString的第一个参数是页地址,0 到 7 对应 64 行 OLED 的每 8 行一页;第二个参数是列地址,按字符宽度递增。OLED_ShowNum的第三个参数 3 表示显示三位数字,配合 HC-SR04 的 2cm 到 400cm 量程刚刚好,超过 999 时显示逻辑要单独处理。

4.4 OLED初始化与I2C写入的要点

以 SSD1306 控制器的 0.96 寸 OLED 为例,I2C 写地址通常是 0x78,部分模组是 0x7A,初始化不成功时先换这个地址试。STC15 没有硬件 I2C 外设时用 IO 口模拟时序,SCL 和 SDA 两根线在空闲状态下都是高电平,SCL 高电平期间 SDA 跳变是起始或停止条件。初始化序列只需要保证关闭显示、打开电荷泵和设置显示时钟这几步,缺了电荷泵屏幕大概率一直黑屏:

void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 打开内部电荷泵 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 显示时钟分频 OLED_WriteCmd(0xAF); // 打开显示 }

OLED_WriteCmd内部把控制字节 0x00 放在命令前面发给从机,数据写入时换成 0x40。SSD1306 的显示数据是按页缓存到芯片内部 RAM 的,写显示字符串时先计算字符在字库数组里的偏移位置,再把字节一列一列搬进对应页。I2C 刷整屏 128 乘 64 像素需要传 1024 字节显示数据,用 100kHz 速率一次全刷大约是 10ms 级别的耗时,对四路测距主循环来说这个开销可以接受,但不建议把整屏刷新放在高频中断里,会让测距的计时被拖乱。

5. STC-ISP烧录与现场调参:声速温度补偿和回波脉宽验证

5.1 下载前检查型号与冷启动

STC15 不同于 STM32 的 SWD 烧录方式,它依靠串口配合 STC-ISP 上位机下载。打开 Keil5 工程编译生成olcd.hex后,在 STC-ISP 软件里选择对应单片机型号,原工程原理图标注的是 IAP15 系列,实际烧录时根据原理图上丝印选择,例如 STC15F2K60S2 或 IAP15W4K58S4。连接好 USB 转 TTL 的 TX 和 RX,记得交叉连接,然后点击下载按钮再给板子重新上电,整个过程就是常说的冷启动。

5.2 声速温度补偿的定点计算

20℃ 时的声速约 343m/s 只代表室温。0℃ 时约 331m/s,40℃ 时约 355m/s,跨度接近 7%。对 100cm 的目标,环境温度从 0℃ 到 40℃ 会产生约 7cm 的系统性误差。补偿公式为声速 v = 331.4 + 0.6T,T 是摄氏温度。STC15 没有硬件浮点单元,尽量用整数运算:

unsigned int GetDistanceComp(int temperature) { unsigned int time_count; unsigned long dist_x100; unsigned int vv; time_count = GetRawCount(); if (time_count == 0) return 0; if (time_count >= 65000) return 999; vv = 33140 + 60 * (unsigned int)temperature; // 单位:cm/s dist_x100 = (unsigned long)time_count * vv / 20000; return (unsigned int)((dist_x100 + 50) / 100); // 四舍五入到cm }

time_count单位是微秒级的计数值,vv是声速的厘米每秒表示。分母 20000 的来源是时间转换到秒再乘 100 到厘米的换算,dist_x100保留两位小数,最后一步把带小数的值四舍五入回整数厘米。温度值可以由 DS18B20 这类传感器读取后传入,没有温度传感器时直接把参数固定为 25 就行,室内环境误差不大。

5.3 用示波器验证回波脉宽

拿到一块新板子,OLED 显示的数字对不上尺子量出的距离时,先用示波器夹在 HC-SR04 的 Echo 引脚上看真实脉宽。比如实测脉宽 583us,理论距离等于 0.000583 秒乘 343m/s 除 2,约等于 10cm,如果 OLED 同时显示 10 左右,说明定时器分频和换算系数正确。这个方法能快速区分问题出在硬件时序还是软件换算,比反复改代码盲调快得多。逻辑分析仪采样率不需要太高,1MHz 以上抓 Echo 信号就够看脉宽了。

5.4 免理论公式的现场标定技巧

最后分享一个我调 HC-SR04 惯用的标定方法,完全绕开声速公式。固定一个障碍物在已知距离处,比如 10cm,读取此时定时器原始计数值记为 cal_cnt,之后测量时直接按time_count * 10 / cal_cnt换算距离。这样温度、晶振频率误差、模块个体差异都被一次性吸收进 cal_cnt 里,室内环境下精度往往比用理论声速系数算出来的还稳。换一个 HC-SR04 模块或者改过晶振后重做一次标定即可。

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

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

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

立即咨询