ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STC15单片机超声波测距实战:从定时器门控到OLED显示

STC15单片机超声波测距实战:从定时器门控到OLED显示 简介面向嵌入式开发者的STC15超声波测距OLED显示工程包完整演示如何利用IAP15系列8051内核MCU控制HC-SR04类传感器通过I2C/SPI驱动OLED实时显示四路测距结果适用于智能机器人避障、环境监测等场景读者需具备一定单片机与C语言基础既适合入门学习也适合进阶实践。压缩包共12个文件体积仅70KB包含C源码与头文件、Keil工程配置uvproj/uvopt、编译生成的hex固件、m51映射文件、原理图及预览图等类型源码与工程分离且目录结构清晰便于直接打开编译和对照硬件设计。目前已有327人学习下载。资源内含完整的超声波测距算法实现、OLED显示驱动、四路并行测量逻辑与硬件原理图可帮助理解定时器捕获回波、距离计算距离声速×时间/2、显示刷新机制以及低成本硬件电路搭建思路同时提供IAP在线编程相关工程配置是一份紧凑且可直接复用的嵌入式实战参考。1. 为什么超声波测距项目要选 STC15 而不是老款 51很多超声波测距教程用的是 STC89C51 加 HC-SR04 加 1602 液晶代码能跑但你把模块换成 OLED、把供电改成 3.3V、再把测距结果接到上位机时老平台就吃力了。STC15 单片机是 1T 增强型 8051内部时钟、ADC、比较器都集成在片内做超声波测距时用定时器门控方式测量 Echo 脉宽比我以前用 STC89 靠循环等待精确得多。这篇按“STC15 时钟模型 → 测距时序 → 原理图设计 → Keil C51 工程 → 滤波与联调 → 示波器排错”的顺序把参数怎么定、接线怎么画、代码怎么写完整梳理一遍。适合手里有 STC15W 系列开发板或者正准备自己画板子做小型测距仪表的工程师。2. 超声波测距的时间测量模型从 HC-SR04 时序到定时器2.1 为什么 Echo 脉宽直接决定测距精度HC-SR04 的完整时序是MCU 给 Trig 引脚送一个不低于 10us 的高电平脉冲模块内部发 8 个 40kHz 方波检测到回波后在 Echo 引脚输出一段高电平。这段高电平的宽度就是从发射到接收的往返时间。按声速 340m/s 算往返 2cm 大约需要 118us往返 400cm 大约需要 23.5ms所以 Echo 脉宽的测量精度直接决定显示值的跳动幅度。常见做法是写一个while (ECHO 1)循环等 Echo 变低再用一个变量自增估算时间。这在 12T 单片机上加优化等级还能凑合换到 STC15 之后循环执行周期变了同样代码测出来的数值可能整体偏大或偏小。STC15 的优势是 1T 模式下机器周期等于时钟周期用 12MHz 内部时钟时定时器每计数一次就是 1us配合 GATE 门控功能可以让 Timer0 自动跟随 Echo 高电平启停不需要 CPU 用循环去卡时间。代码里就体现为这一小段初始化void Timer0_Gate_Init(void) { TMOD 0xF0; // 保留定时器1的模式配置 TMOD | 0x09; // 定时器0: 16位定时器, GATE1, C/T0 TH0 0; TL0 0; TR0 1; // 使能定时器0, 但计数由INT0引脚高电平控制 }这里的关键是TMOD | 0x09低四位二进制是1001含义是把定时器0配置为方式116位计数器并且打开 GATE 门控。TR0 置 1 后定时器不会立刻计数只有 INT0 引脚为高电平才开始INT0 拉低就自动停止。这样 Echo 接到 STC15 的 P3.2 引脚上硬件替我们完成了“只在 Echo 高电平期间计数”的逻辑。2.2 用 GATE 门控测量 Echo 脉宽的最小代码初始化之后每次测量先发触发脉冲然后等待 Echo 变高变高后定时器自动计数Echo 变低时定时器自动停止此时读取 TH0 和 TL0 合并成计数值即可。完整测距函数可以写成这样uint16_t GetEchoUs(void) { uint16_t cnt 0; uint16_t guard 0; TH0 0; // 清零计数值 TL0 0; TF0 0; TRIG 1; // 触发HC-SR04 PulseTriggerDelay(); // 保持高电平至少10us TRIG 0; while (ECHO 0) // 等待Echo变高 { if (guard 60000) { return 0; // 没有回波, 按0处理 } } // 此时Timer0已经在硬件控制下开始计数 while (ECHO 1) // 等待Echo变低 { if (TF0) // 16位计数溢出 { TF0 0; return 0xFFFF; // 表示超量程 } } cnt (uint16_t)((TH0 8) | TL0); // 读出计数值, 12MHz下单位是us return cnt; }读计数值的时机放在 Echo 变低之后因为 GATE 模式下引脚一拉低计数器就停止了TH0/TL0 里保留的是完整脉宽。后面那个while (ECHO 0)是防死循环保护万一模块没接好或者没有障碍物程序不至于卡死在里面。超量程返回0xFFFF在主程序里处理成999显示比较直观。STC15 的 Timer0 是 16 位最大计数 65535usHC-SR04 最大量程约 25ms理论上是够用的。但如果你把模块换成量程更大的传感器或者用更低的主频就必须检查溢出标记否则计数值回绕之后会显示出一个很小的距离值。2.3 与 PCA 捕获方式的取舍STC15 的部分型号带 CCP/PCA 模块也可以用来捕获 Echo 的上升沿和下降沿读两次捕获寄存器求差值。这种方法不占 CPU代码逻辑也更接近“事件记录”但有两个前提第一捕获引脚一般是固定的比如 P1.1、P3.4 之类原理图必须提前规划第二PCA 的寄存器操作比 Timer 复杂配置错了波形根本抓不到。我一般对于“STC15 HC-SR04 OLED”这个组合默认选 GATE 门控方案原因是 STC15 全系列都有 INT0Echo 接 P3.2 是通用做法随便哪个型号都能跑。PCA 捕获更适合你准备做更复杂的多路测距或者对 CPU 占用极其敏感的场景。原理图设计时只要记住把 Echo 接到 P3.2后面的软件方案就固定下来了。3. 原理图要点最小系统、OLED 接线和电平匹配3.1 STC15 最小系统可以省掉晶振和复位电路STC15 系列内部有高精度 IRC 时钟烧录程序时通过 STC-ISP 软件选择频率即可常见有 11.0592MHz 和 12MHz。测距项目建议选 12MHz这样 Timer0 每计数一次正好是 1us距离计算不需要额外换算比例。原理图里不需要画外部晶振也不需要传统 51 的 31 引脚上拉电路这让板子面积小很多。最小系统实际上只需要四部分VCC 和 GND、电源去耦电容、串口下载电路、IO 引出排针。电源处习惯放一个 100nF 陶瓷电容贴近 MCU 的 VCC 引脚再并一个 10uF 电解电容稳住低频波动。下载电路用 CH340 的 TXD 接 STC15 的 RXDP3.0CH340 的 RXD 接 STC15 的 TXDP3.1注意 STC15 下载需要冷启动也就是先点下载再给板子上电。下面这个表是测距项目里比较稳定的接线关系画原理图时可以照着拉线外设引脚STC15 引脚接线说明HC-SR04 VCCVCC5V与 MCU 电源同一路就近加 100nFHC-SR04 GNDGND必须与 MCU 共地否则波形毛刺明显HC-SR04 TrigP1.0普通推挽输出触发脉冲HC-SR04 EchoP3.2 / INT0输入引脚直接连 Timer0 门控OLED VCC3.3V 或 5V按模块实际版本确定OLED SCLP2.0模拟 I2C 时钟OLED SDAP2.1模拟 I2C 数据3.2 OLED 的 I2C 地址和电压匹配问题市面上常见的 0.96 寸 OLED 模块有两种接口7 针 SPI 和 4 针 I2C。做测距显示我会优先选 I2C 版本少占 2 个 IO接线四根线就够代码里用模拟 I2C 可以接任意引脚。SSD1306 驱动的 I2C 地址一般是 0x3C左移一位后写入地址是 0x78。部分模块背面有电阻可以改成 0x3D对应写入地址 0x7A。电压匹配是这最容易踩的坑。STC15 如果工作在 5V而 OLED 模块是 3.3V 版本OLED 的 SCL 和 SDA 引脚未必能承受 5V。稳妥做法是 OLED VCC 接 3.3VSCL 和 SDA 串一个 1k 电阻到 STC15 的 IO或者把 STC15 的电源直接降到 3.3V。不要为了省电阻把 5V 直接接到 3.3V OLED 上I2C 总线上两个器件互相灌电流轻则显示花屏重则烧掉 OLED 驱动芯片。STC15 的 IO 模式寄存器对这块也有影响。默认准双向口自带弱上拉模拟 I2C 能工作但信号边沿偏慢。更稳的做法是把 SCL 和 SDA 设成推挽输出代码里在初始化时配置 P2M0 和 P2M1P2M1 0x00; P2M0 0x03; // P2.0/P2.1 设为推挽输出如果是开漏模式就必须外部加上拉电阻否则 SCL 低电平释放后总线没有能力拉回高电平时序直接出错。3.3 原理图布局里 Echo 线和电源地的处理HC-SR04 和 OLED 都是低速外设原理图布线上不需要像高速数字电路那样讲究阻抗但有一个问题经常被人忽略Echo 信号线离电源走线太近模块发射时瞬间电流变化会耦合进 Echo 线导致测量值偶尔跳变。建议 Echo 和 Trig 走线不要平行超过 2cm中间留出地线间隔如果结构上必须长距离走线在 Echo 引脚对地并联一个 100pF 电容可以滤掉一部分高频干扰。另一个是共地问题。HC-SR04 的地和 STC15 的地不要在空中飞线而是回到原理图上同一个 GND 网络OLED 的地也走同一网络。很多人测距值跳变最后查出来是模块地和主板地之间存在电位差Echo 的参考电平不对。原理图上把三个器件的 GND 画到一起这个问题就提前解决了。4. Keil C51 工程实现时钟配置、延时函数和 OLED 驱动4.1 STC15 的头文件选择和时钟初始化Keil C51 默认不带 STC15 的头文件需要先安装 STC 官方器件库或者在工程里手动添加STC15W4K.H这类头文件。STC-ISP 软件里也有一键复制头文件的功能。没有头文件时TMOD、CLK_DIV这些寄存器都是未定义的编译直接报错。所以建工程第一步不是写主函数而是确认 Keil 能识别你手上的具体型号。时钟初始化只需要设置分频系数。STC15 内部 IRC 的频率在烧录时已经固定代码里通过CLK_DIV做分频0x00表示不分频也就是跑在用 STC-ISP 选定的频率上。这里要注意的是 STC15 是 1T 单片机所有基于传统 51 的延时习惯都要重新检查。初始化函数一般这样写void SystemClockInit(void) { CLK_DIV 0x00; // 不分频, 以烧录时选择的IRC频率运行 P0M0 0x00; P0M1 0x00; // P0 准双向 P1M0 0x01; P1M1 0x00; // P1.0 推挽, 用于Trig P2M0 0x03; P2M1 0x00; // P2.0/P2.1 推挽, 用于OLED I2C P3M0 0x00; P3M1 0x00; // P3.2 保持输入状态 }CLK_DIV的寄存器地址在 STC15 系列里是统一的但头文件版本不同可能写法有差异以你手上的型号头文件为准。IO 配置这里需要理解PxM0和PxM1的组合规则00 准双向01 推挽10 高阻输入11 开漏。Echo 引脚保持默认准双向即可因为它是输入方向不需要特殊配置。4.2 触发脉冲的延时不能依赖手写循环HC-SR04 要求 Trig 高电平至少 10us。很多从 STC89 迁移过来的代码直接把原来能用的延时函数抄过来结果 STC15 执行速度快了大约 12 倍同一个函数实际延时只有原来的八分之一左右Trig 脉宽不够模块可能不响应。反过来如果你用 STC-ISP 里“软件延时计算器”生成的函数注意选择时钟频率要和烧录频率一致12MHz 就选 12MHz别选 24MHz 生成又烧到 12MHz。触发脉冲不需要高压精度所以这里不纠结具体延时时间只要保证大于 10us 即可void PulseTriggerDelay(void) { volatile uint16_t i 0; while (i 80); }volatile是为了防止 Keil 优化掉整个空循环。在 12MHz 的 STC15 上这个循环大概能撑住几十微秒余量足够。真正影响测距精度的不是 Trig 脉宽而是 Echo 高电平期间 Timer0 的计数过程所以触发延时用这种粗粒度写法完全没问题。4.3 模拟 I2C 驱动 SSD1306SSD1306 的 I2C 通信协议本身不复杂起始信号、从机地址、控制字节、数据字节。控制字节0x00表示后续是命令0x40表示后续是显示数据。模拟 I2C 的核心是四个函数起始、停止、发送字节、发送命令。下面给出最小可用实现#define OLED_ADDR 0x78 // 0x3C左移1位 void I2C_Start(void) { OLED_SDA 1; OLED_SCL 1; I2CDelay(); OLED_SDA 0; I2CDelay(); OLED_SCL 0; } void I2C_Stop(void) { OLED_SDA 0; OLED_SCL 1; I2CDelay(); OLED_SDA 1; I2CDelay(); } void I2C_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { OLED_SCL 0; if (dat 0x80) OLED_SDA 1; else OLED_SDA 0; I2CDelay(); OLED_SCL 1; I2CDelay(); dat 1; } OLED_SCL 0; OLED_SDA 1; // 释放SDA, 不读ACK }这里的I2CDelay()可以是几个空指令也可以是一个带volatile变量的短循环。STC15 跑 12MHz 时如果完全不加延时I2C 时钟频率可能超过 SSD1306 的 400kHz 上限导致偶发花屏。我一般会把I2CDelay()做成一个至少占用 5 条指令的短函数宁可慢一点也要保证时序合格。初始化函数将配置命令分成几个事务发送比一次性发一串更保险void OLED_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_SendByte(OLED_ADDR); I2C_SendByte(0x00); I2C_SendByte(cmd); I2C_Stop(); } void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xA8); // 设置多路复用比 OLED_WriteCmd(0x3F); // 64行 OLED_WriteCmd(0xD3); // 显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 起始行0 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // COM扫描方向 OLED_WriteCmd(0xDA); // COM引脚配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); // 对比度 OLED_WriteCmd(0xCF); OLED_WriteCmd(0xAF); // 开启显示 }每个命令单独走一次 Start 和 Stop虽然让 I2C 总线多了几个往返但排错方便实际显示一个数字也就几十条命令性能完全够。初始化之后你要显示字符还需要一个OLED_ShowString函数内部按 SSD1306 的页地址模式先把光标定位到目标页和列然后连续发送0x40控制字节和数据。字模数组用取模软件生成时要选“阴码、列行式、逆向”否则屏幕上的字会左右颠倒。5. 测距显示联调温度修正、数据滤波和刷新节流5.1 声速不是固定 340m/s超声波在空气中的速度受温度影响很明显0℃ 时约 331.5m/s20℃ 时约 343.5m/s40℃ 时约 355.5m/s。如果你在恒温室内测一个固定距离340m/s 的误差不会超过 2%但设备如果要在户外或者车间里使用冬天和夏天的测距结果会相差几个厘米。修正公式是 v 331.4 0.6 * TT 是摄氏温度。比如室温 25℃ 时声速是 346.4m/s用distance_cm us * 346.4 / 20000换算注意单位是“往返时间除 2 再乘声速”所以分母是 20000。代码里可以把声速定义成宏#define SOUND_SPEED 346 // 25℃环境下约346m/s如果不想引入温度传感器就把这个宏按你所在环境的平均温度改一次。对于 STC15 这种低算力单片机建议直接用整数除法dist_cm (uint16_t)(raw_us * 17UL / 1000);这个公式等价于把声速近似成 340m/sus 乘 17 再除以 1000计算过程不需要浮点Keil C51 编译后代码量很小。需要温度补偿时把系数 17 改成SOUND_SPEED / 20并四舍五入。5.2 滑动平均滤波让显示值不跳变HC-SR04 的原始测量值受反射面角度、障碍物边缘和电源波动影响直接显示时小数点后一位会不停跳动。常见做法是做滑动平均用最近 4 次结果求平均。这里给出一个环形缓冲区实现不用动态内存适合单片机uint16_t FilterDistance(uint16_t new_val) { static uint16_t buf[4]; static uint8_t idx 0; static uint8_t cnt 0; uint32_t sum 0; uint8_t i; buf[idx] new_val; idx (idx 1) 0x03; if (cnt 4) cnt; for (i 0; i cnt; i) sum buf[i]; return (uint16_t)(sum / cnt); }注意sum用 32 位因为 4 个 0 到 999 的数相加最大 399616 位足够但如果你把原始 us 值也放进来最大25000*4100000就超出 16 位范围了。这个函数在每次测量到新值后调用返回的是平滑后的距离。滑动平均的缺点是目标真正移动时显示值会“拖尾”。如果你做的是小车避障需要快速响应改成加权平均更合适新值占 3/4旧值占 1/4这样既滤掉毛刺又不至于反应太迟钝。5.3 主循环节奏和 OLED 刷新节流HC-SR04 模块本身有测量周期限制连续触发间隔最好大于 60ms否则模块还在处理上一次回波就收到新的触发输出会不稳定。所以主循环里用软件延时或者定时器计数做 100ms 左右的间隔而不是发完一轮立刻再发。OLED 刷新也要节流。SSD1306 的 I2C 接口传输几十字节数据需要毫秒级时间如果每轮测距都刷新整个屏幕主循环会被拖慢测量周期变得不规律。一般做法是每 5 轮测距刷新一次数字区域标题字符串只在初始化时写一次。只看数字区的更新函数不开全局刷新屏幕不会闪CPU 也有时间处理其他任务。主循环骨架可以这样搭void main(void) { uint16_t raw_us; uint16_t dist_cm; volatile uint16_t loop_cnt 0; SystemClockInit(); Timer0_Gate_Init(); OLED_Init(); OLED_ShowString(0, 0, Dist:); OLED_ShowString(0, 2, cm); while (1) { raw_us GetEchoUs(); if (raw_us 0xFFFF) dist_cm 999; // 超量程或超时 else dist_cm FilterDistance((uint16_t)(raw_us * 17UL / 1000)); if (loop_cnt 5) { loop_cnt 0; OLED_ShowNum(0, 1, dist_cm, 3); } DeviceDelay100ms(); } }这里DeviceDelay100ms()用定时器1来计时更准确但手写一个带volatile的多层循环也能用。注意不要在GetEchoUs()里引入长延时否则测距周期会被拉长。OLED 显示函数放在循环计数后面保证即使显示有波动测量始终按固定节奏执行。6. 用示波器和串口做最后的时序校验6.1 示波器看 Echo 波形是最快的定位手段当测距值异常时先不急着改代码把示波器探头地夹到 HC-SR04 的 GND探头点住 Echo 引脚触发方式设为上升沿时间轴打到 5ms/div。正常情况下手在模块前晃动会看到一段宽度随之变化的高电平脉冲没有波形时查 Trig 有没有正常输出有波形但脉宽完全不变化时查模块供电和 Trig 触发电平。这个检查比任何代码调试都直接。HC-SR04 的 Echo 输出高电平幅值通常接近模块供电电压。如果模块用 5V 供电Echo 高电平约 5V接入 STC15 时因为 STC15 也是 5V不存在电平兼容问题。如果单片机是 3.3V 版本Echo 的 5V 高电平需要分压后再进引脚。6.2 串口打印原始计数值辅助分析OLED 显示的是滤波后的结果不方便判断问题出在传感器还是算法。建议在调试阶段把GetEchoUs()返回的原始 us 值通过串口发出来接一个 USB 转 TTL 模块到电脑波特率设 9600。下面的发送函数可以直接用void UART1_SendByte(uint8_t dat) { SBUF dat; while (!TI); TI 0; }调用时把计数值的高字节和低字节分别发送电脑端用十六进制显示。如果数值随距离线性变化说明 Timer 门控路径没问题如果数值固定跳变问题很可能在触发脉冲或者环境噪声。串口打印本身不影响 OLED 刷新调试完删掉发送代码即可。6.3 OLED 不亮时的排查顺序OLED 不出字的概率比测距不准高得多我遇到的案例里八成是硬件问题而不是驱动代码问题。按这个顺序查先确认 VCC 和 GND 没接反再确认 SCL 和 SDA 没有对调然后用万用表量 OLED VCC 引脚电压是否与模块型号一致。排到软件层时重点检查地址是0x78还是0x7A可以在初始化里分别试一次。最后才是加长I2CDelay()把 I2C 时钟降下来排除速率过高导致的时序问题。一个实用的验证技巧是只用一条OLED_WriteCmd(0xAF)打开显示屏幕如果变亮说明命令通路正常再逐步加初始化命令。这样能把“屏幕不亮”的问题精确缩小到某一行配置上比反复烧录完整工程快很多。调试完建议把对比度命令0x81和参数0xCF保留这两个值在多数 OLED 模块上显示效果最清楚不会过暗也不会拖影。本文还有配套的精品资源点击获取
返回列表