资讯动态

基于STM32的超声波测距OLED显示与蜂鸣器报警系统实现

发布时间:2026/8/31 11:47:04 来源:尧图企业网站定制
简介本资源是一套基于STM32F10x系列的超声波测距嵌入式实践项目面向嵌入式初学者、单片机课程设计学生及电子竞赛备赛者解决距离感知、多外设协同驱动与人机交互反馈等典型工程问题。压缩包共103个文件含42个头文件.h定义硬件寄存器与模块接口、40个C源文件.c实现超声波时序控制、OLED I²C显示驱动、蜂鸣器报警逻辑及系统初始化流程另有启动文件.s、Keil工程配置.uvprojx/.uvoptx、调试配置.dbgconf及烧录脚本.bat整体体积仅418KB结构紧凑、可直接编译运行。已有5908人学习下载资源代码规范、模块划分清晰涵盖HC-SR04传感器精确测距算法、OLED动态刷新机制、低功耗蜂鸣器触发策略并附带完整Keil工程环境配置便于快速上手、调试验证与二次开发。 把超声波测距、OLED显示、蜂鸣器报警这三样东西拼在一起几乎是每个玩STM32的人都会做一遍的项目。但越是这种看起来条条大路通罗马的入门组合越容易在细节上翻车明明照着教程接好了线Echo引脚就是不回高电平OLED花屏、闪屏、亮度不均匀蜂鸣器阈值设简单了人一走过就疯叫不止。这篇内容我会完整记录一遍基于STM32的超声波测距OLED显示蜂鸣器短距报警系统的实现过程把硬件选型、时序测量、数据滤波、OLED驱动、报警策略和实测数据全部铺开讲适合正打算做课程设计、电子竞赛入门或者单纯想在桌面放一个声光测距仪的朋友参考。先给结论这个项目用到的核心器件是STM32F103C8T6最小系统板、HC-SR04超声波模块、0.96寸SSD1306 OLED屏、有源蜂鸣器软件部分用标准库或HAL库都可以测量原理用定时器输入捕获做回响时间再配合温度补偿和滑动排序滤波最后按距离区间分三级报警。项目真正难的地方不在让每个模块跑起来而是模块之间的电压匹配、时序稳定性和报警策略的防抖设计。1. 项目拆解一个连板就能跑的测距报警器真正的坑在哪儿1.1 这套系统由哪几部分组成从功能上看这套系统就是三个模块围绕一个主控工作STM32负责发超声波触发信号、测量回响时间、换算距离、驱动OLED屏幕、控制蜂鸣器。传感器用HC-SR04这是市面上最普及也最便宜的超声波测距模块官方标称测量范围2cm到400cm实测在20cm到200cm区间表现比较稳定显示端用0.96寸I2C接口的SSD1306 OLED因为I2C只占两根引脚配线特别方便报警端用有源蜂鸣器给高电平就响省去PWM产生频率的麻烦。我把这套系统的目标定得很具体实时测量正前方障碍物距离显示在OLED上当距离进入不同档位时蜂鸣器给出不同节奏的报警提示。这样做的好处是每一个功能模块都有明确的验收标准排查问题的时候可以按传感器→计算→显示→报警这条链路逐段定位。1.2 硬件清单与接线表我用的是最常见的STM32F103C8T6蓝色Pill开发板俗称蓝丸。这个芯片虽小但72MHz主频、20KB RAM、64KB Flash跑这种组合项目绰绰有余。以下是完整接线方式模块引脚接到STM32的引脚HC-SR04VCC5V模块工作电源需要5VHC-SR04GNDGNDHC-SR04TrigPA0HC-SR04EchoPA1必须加电阻分压见下文OLEDVCC3.3VOLEDGNDGNDOLEDSCLPB6I2C1_SCLOLEDSDAPB7I2C1_SDA有源蜂鸣器模块I/OPA2三极管驱动有源蜂鸣器模块VCC5V有源蜂鸣器模块GNDGND如果你用的是蜂鸣器模块自带三极管驱动而不是裸蜂鸣器可以直接把I/O接到PA2如果用的是裸蜂鸣器需要自己加NPN三极管驱动这个我会在报警章节详细讲。1.3 第一个被忽略的坑Echo引脚的电压不匹配大多数人做这个项目第一次失败都栽在HC-SR04的Echo引脚上。模块用5V供电意味着Echo回响引脚输出高电平时是5V而STM32的GPIO耐压上限是3.6V直接把5V灌进PA1轻则读到异常电平重则烧引脚。解决办法是电阻分压在Echo和PA1之间串一个1kΩ电阻PA1对地接一个2kΩ电阻5V经分压后约3.3V正好落在STM32的逻辑高电平范围内。很多教程里没提这步直接用杜邦线连上去居然也能跑那是因为有的模块Echo实际输出不到5V或者板子内部有保护但这是运气不是设计方法。提示Echo引脚一定加电阻分压不要直接接STM32。这个坑属于典型的偶尔能跑但随时会坏等烧了IO口再找原因就晚了。2. HC-SR04测距不是拉高Trig数Echo就完事2.1 触发与回响时序HC-SR04的测量协议很粗暴主控给Trig引脚一个至少10微秒的高电平脉冲模块内部自动发出8个40kHz的超声波脉冲同时把Echo引脚拉高超声波遇到障碍物反射回来模块收到回波后把Echo拉低。所以Echo高电平的持续时间就是超声波往返的总时间。距离计算公式距离(cm) Echo高电平时间(us) / 58这个58是从声速推导出来的。常温下声速约340m/s也就是0.034cm/us。往返时间是两倍距离所以距离(cm) 时间(us) × 0.034 / 2 时间(us) / 58.8工程上取58偏差不大。但是要注意这个0.034cm/us是基于20°C空气声速的如果环境温度偏差大还需要做补偿我会在2.4节展开。2.2 阻塞延时 vs 定时器输入捕获测量Echo高电平时间有两条路线一是阻塞式脉冲计数代码简单但不精准二是定时器输入捕获推荐用这种方式。阻塞式写法就是拉高Trig后延时10us然后死循环读Echo引脚电平用DWT-CYCCNT或者SysTick计时等Echo变低再读时间。这种写法最大的问题在于STM32主频72MHz一条循环指令几个周期配合空循环延时可以做但精度受中断和编译器优化影响测出来数据会抖动。我推荐用定时器输入捕获。核心思路是配置一个定时器比如TIM2的通道1为输入捕获模式让Echo引脚连接到PA0或PA1对应的通道捕获上升沿和下降沿的计数值两个计数值之差换算成时间。以STM32F103标准库为例大致配置如下void TIM2_CH1_Capture_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/72 1MHz每个计数是1us TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); }把预分频器设为71定时器时钟就是1MHz计数器每加1等于过1微秒。捕获到上升沿时记录第一次的值然后把捕获极性切换为下降沿捕获到下降沿时再读取计数值两者的差值就是Echo高电平持续了多少微秒。注意如果Echo高电平时间超过了65535us约65ms计数器会溢出对应距离超过11米一般不存在这个问题但还是要处理溢出标志位避免拿一个错误值计算。我在实际代码里会在中断里判断TIM_GetFlagStatus(TIM2, TIM_FLAG_CC1OF)也就是捕获溢出标志一旦溢出就丢弃这次测量结果。2.3 数据可靠性处理排序滤波与滑动平均超声波测得的原始数据抖动非常大同一个障碍物放在50cm处连续测10次可能得到48cm到53cm的波动。原因包括超声波的锥形波束被物体边缘散射、模块自身噪声、周围环境的漫反射、温度变化。所以必须做软件滤波。我最常用的组合是中值滤波滑动平均。操作步骤连续采集5次数据对这5个数排序取中间那个作为有效值把有效值放入一个长度为5的队列对队列里的5个值取平均作为最终显示和判断报警的距离。中值滤波的好处是能剔除突变的毛刺比如偶尔一次测到0cm或者300cm的异常值排序后根本不参与平均。滑动平均则是让显示距离变化平滑不会一格一格跳。代码实现也不难uint16_t get_filtered_distance(void) { uint32_t raw[5]; uint32_t sorted[5]; static uint16_t queue[5]; static uint8_t index 0; uint32_t sum 0; uint8_t i, j; for (i 0; i 5; i) { raw[i] HCSR04_GetDistance(); delay_ms(20); // 两次触发间隔至少20ms防止余振干扰 } // 冒泡排序5个元素足够 for (i 0; i 5; i) sorted[i] raw[i]; for (i 0; i 4; i) { for (j i 1; j 5; j) { if (sorted[j] sorted[i]) { uint32_t temp sorted[i]; sorted[i] sorted[j]; sorted[j] temp; } } } queue[index] sorted[2]; // 中位数 index (index 1) % 5; for (i 0; i 5; i) sum queue[i]; return (uint16_t)(sum / 5); }每次触发间隔至少20ms是因为40kHz超声波的余振需要时间衰减连续快速触发会互相干扰数据反而更差。2.4 温度补偿的计算方法声速并不是恒定的340m/s它在空气中的速度随温度变化近似公式c 331.4 0.6 * T其中T是摄氏温度。比如夏天35°C时声速约为352.4m/s冬天5°C时声速约为334.4m/s相差约5%。如果项目在室温恒定的环境里跑直接除以58问题不大如果是室外或者温差大的地方建议加一个DS18B20温度传感器测到温度后动态计算声速。具体做法是温度传感器的读数T换算成声速c 331.4 0.6 * T然后用这个公式计算距离距离(cm) 时间(us) * c / 1000000 / 2 * 100 时间(us) * c / 20000举个例子如果Echo高电平时间是3000us室内25°C下声速346.4m/s距离 3000 × 346.4 / 20000 ≈ 51.96cm。如果直接用除以58算出来是51.72cm差了大约0.24cm。这个偏差在近距离看起来无所谓但到了1米以上就逼近0.5cm对于要求严格的场景不可忽略。如果不想加温度传感器也可以保持除以58的简化算法但建议实测后记下环境温度做一个固定偏移量修正。我在项目里用了DS18B20把温度显示在OLED的第二行这样测距数据自带环境参考后续调参也有据可依。3. OLED显示与距离刷新从能亮到好用3.1 SSD1306驱动的初始化要点0.96寸OLED屏的驱动芯片99%是SSD1306虽然也有少数SH1106但初始化序列接近。I2C接口的地址默认是0x3C少数屏是0x3D如果屏幕不亮第一件事去确认地址。SSD1306初始化序列是固定的不需要理解每一行但有几个关键点必须关注关闭电荷泵0x8D 0x10的时候屏幕是不亮的必须打开电荷泵0x8D 0x14否则屏幕只是有数据但没背光显示起始行、段重映射、COM扫描方向这几个寄存器决定了画面是不是镜像倒转的如果显示出来上下颠倒就去改0xA0/A1或者0xC0/C8对比度设置寄存器0x81后面的值OLED的亮度跟它直接相关我一般设为0x7F太亮反而糊。我提供一个简化的初始化序列基于常见的四线I2C模式void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 页寻址模式 OLED_WriteCmd(0xB0); // 页地址起始 OLED_WriteCmd(0xC8); // COM反向扫描 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0x7F); // 对比度 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 多路复用率 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 打开电荷泵 OLED_WriteCmd(0xAF); // 开启显示 }如果用的是标准库I2C通信可以用硬件I2C也可以用GPIO模拟I2C。我个人的建议是直接用软件模拟I2C。原因是STM32F1系列硬件I2C众所周知有些刁钻容易卡在总线忙状态对新手排查不友好。软件模拟I2C只要把SCL、SDA两个GPIO配置成开漏输出配合上拉电阻代码稳定性和可移植性都很好。当然如果你已经熟练掌握了硬件I2C的坑用硬件I2C可以节省CPU时间但在这种低频显示场景里软件模拟完全够用。3.2 局部刷新与整屏刷新怎么权衡SSD1306的显存是128×64位按8页划分每页8行。常见的驱动写法有两个流派一种是你每次写入一个字节到显存对应位置然后主动把GRAM刷新到屏幕另一种是建立一个和显存同样大小的缓存数组所有画点、画字符的操作都在缓存里完成最后统一把缓存推送到SSD1306。对于这个项目我建议用缓存方案。原因是刷新频率和显示内容不是简单的全刷和不刷二选一如果只更新数字部分要精确计算要刷新的页和列范围代码复杂度高如果每次都整屏刷刷新一屏要发128×81024字节的数据I2C在400kHz速度下大约要20多毫秒而超声波测量周期本身就有几十毫秒两者叠加会明显卡顿。折中方案是小范围局部刷新把距离数值和报警状态固定在屏幕的特定区域每次只刷新那几行字节。比如距离显示在屏幕左上角那么对应页是第1页和第2页每页128字节总共256字节刷新时间只有整屏的1/4肉眼几乎无感。我在代码里直接把数字转换成6×8或16×24的字体位图填到缓存数组再把对应的页范围推送到OLED。3.3 界面布局让报警状态一眼可见OLED显示不需要花哨信息清晰最重要。我的布局是这样的第一行显示Distance:和小数点后一位数值单位cm第二行显示温度值比如Temp: 25.3C第三行显示当前报警等级用NORMAL、CAUTION、WARNING、或者用图形化的三角号、图标。第三行报警状态我建议用色块反转的方式突出。比如SSD1306支持反白显示把报警状态的字符所在区域填充为白色字符本身用黑色视觉冲击力比普通字符强很多适合扫一眼就判断当前状态。可以把报警等级做一个简单的函数映射void display_alarm_status(uint8_t level) { switch (level) { case 0: OLED_ShowString(0, 3, NORMAL , 0); break; case 1: OLED_ShowString(0, 3, CAUTION , 1); break; case 2: OLED_ShowString(0, 3, WARNING , 1); break; } }最后一个参数是反白标志实现方式就是画一个反色的矩形框再往里面写字符。OLED屏幕在这种项目里最大的优势是自发光、对比度高哪怕视角很偏也能看清状态比LCD1602舒服太多。4. 蜂鸣器分级报警阈值越简单越容易啸叫4.1 有源还是无源蜂鸣器分有源和无源两类这里的源指的是有没有内置振荡电路。有源蜂鸣器只要给直流电平就会发声频率固定无源蜂鸣器必须给方波或PWM信号频率可以自己控制。测距报警场景里我选用有源蜂鸣器原因很简单代码少不会因为频率控制不好发出怪声报警节奏可以靠延时脉冲来实现。但有源蜂鸣器也有缺点声音比较尖锐刺耳而且只有一个音调。如果你想让报警声音更丰富比如距离越近声音越急促可以用无源蜂鸣器配合PWM把报警频率和BEEP节奏都程序化。这个项目的基础版我用有源扩展部分我会讲如何切换无源蜂鸣器。4.2 三极管驱动电路STM32 GPIO的灌电流和拉电流一般只有几毫安到20毫安直接驱动蜂鸣器模块可以但驱动裸蜂鸣器就不够。正确的接法是用NPN三极管比如S8050做开关PB引脚 → 1kΩ电阻 → 三极管基极 三极管发射极 → GND 蜂鸣器正极 → 5V 蜂鸣器负极 → 三极管集电极 集电极和发射极之间加一个1N4148二极管方向是从地到集电极防止蜂鸣器关断瞬间的反向电动势打坏三极管代码就不需要管三极管只要把GPIO拉高基极有电流三极管导通蜂鸣器通电发声GPIO拉低三极管截止蜂鸣器停止。在PA2上配置推挽输出即可。需要注意的是三极管的基极电阻不能太大会导致驱动电流不足也不能太小一般1kΩ到4.7kΩ都行我习惯用1kΩ实测S8050放大倍数足够GPIO的电流输出能力也不用担心。4.3 分级报警与消抖逻辑报警策略是整个项目里最能体现细节的部分。如果只设一个简单阈值比如距离小于30cm就响会带来两个问题一是距离在阈值附近来回抖动时蜂鸣器会反复通断听起来像接触不良二是如果障碍物突然进入探测范围又快速离开会触发一个极短的报警音起不到警示作用。我的做法是分级加消抖距离区间报警等级蜂鸣器行为距离 50cm正常不响20cm 距离 ≤ 50cm注意每500ms短响一次距离 ≤ 20cm警告连续长鸣代码里实现连续3次测量结果都处于同一个等级才切换报警状态避免单次抖动导致误报void update_alarm(uint16_t distance) { static uint8_t confirm_cnt 0; static uint8_t current_level 0; uint8_t level; if (distance 50) level 0; else if (distance 20) level 1; else level 2; if (level current_level) { confirm_cnt 0; } else { confirm_cnt; if (confirm_cnt 3) { current_level level; confirm_cnt 0; } } if (current_level 1) { BEEP_ON; delay_ms(100); BEEP_OFF; delay_ms(400); } else if (current_level 2) { BEEP_ON; } else { BEEP_OFF; } }这个逻辑里最容易被忽略的是连续确认的必要性。超声波数据本身有波动即使做了滤波50cm阈值临界点的测量也可能在49cm和52cm之间跳动。没有消抖蜂鸣器会像老式电话忙音一样响个不停。有了3次确认机制短时间波动直接忽略对于这个级别的应用来说已经足够可靠。另外一个细节蜂鸣器声音不要跟OLED刷新叠加到同一时刻否则OLED的I2C通信时序会被延长虽然不会崩溃但可能出现屏幕闪烁。我通常让OLED刷新在蜂鸣器停止的间隙进行比如蜂鸣器响100ms后、延迟的400ms间隔里刷新OLED这样互不干扰。5. 实测数据、问题排查与后续扩展5.1 真实环境下的一组测距数据我在室内常温约24°C环境下用一块平整纸板作为障碍物从10cm到200cm做了多次测量取滤波后的稳定读数实际距离(cm)OLED显示(cm)误差(cm)备注1010.10.1接近盲区需要纸板正对探头2020.30.3稳定3029.8-0.2稳定5049.6-0.4有轻微抖动8079.2-0.8抖动在±1cm内10098.9-1.1反射面积越大越准150148.3-1.7开始受环境反射影响200197.5-2.5建议作为可信上限从数据能看出来近处误差小越远误差越大原因主要有两个一是远距离回波信号弱Echo电平的下降沿时间有微秒级的误差微秒级误差换算成厘米就是零点几厘米二是波束发散远处反射路径不是严格的直线往返斜向路径会被当成更长的距离。5.2 高频踩坑排查表做这个项目过程中我遇到过的问题基本都能归进下面这张表现象可能原因排查方法OLED完全不亮I2C地址错误、SCL/SDA接反、没开电荷泵扫描I2C地址对调SCL/SDA确认初始化序列里0x8D/0x14OLED花屏或显示乱码电源干扰、I2C速率过快、初始化时序不完整检查电源纹波将I2C时钟降到100kHz重新核对初始化序列超声波一直测到0cmEcho没接对、分压电阻太大导致高电平阈值不够用万用表量Echo高电平电压把分压电阻换成1k2k的组合超声波偶发测出300cm周围没有障碍物、Echo引脚浮空确认测量目标在探头正前方给Echo加一个下拉电阻蜂鸣器不响三极管接反、基极电阻太大、GPIO没配置成推挽输出先直接给GPIO高电平测试检查三极管引脚排列下载程序时找不到芯片BOOT0没接地、串口驱动没装、没有按复位按住复位键再点下载出现下载进度时松开有一条排查经验很值得分享不要同时调试三个模块。很多朋友上来就把所有模块全部接好一次下载完代码结果屏幕亮但距离恒定在0瞬间不知道去查哪个模块。我建议的节奏是先单独点亮OLED再单独测超声波用串口打印距离再单独让蜂鸣器响最后把三者合并。每个阶段都能确认一个模块正常最后联调时问题范围一下子缩小到模块之间相互作用的层次。5.3 可以往哪个方向改造这个项目的完成度已经适合作为入门或课设作品但要想继续深化有几个方向我觉得性价比很高把测量数据显示成波形在OLED上用实时曲线画出距离变化能看到障碍物移动的轨迹。OLED刷新率是个瓶颈但配合局部刷新可以做到每秒5-10帧的波形滚动增加语音播报用SYN6288或者JQ8900语音模块距离低于阈值时播放请注意前方障碍物比蜂鸣器更直观换成单向测距传感器如果觉得HC-SR04的精度不够可以换成VL53L0X激光测距传感器I2C接口直接替代精度能到毫米级代价是成本翻好几倍增加舵机云台把HC-SR04放在舵机上做扫描配合OLED画一个简单的扇形图就变成了一个迷你雷达。这个扩展对逻辑能力的锻炼很值得也是很多人做完这个项目后自然而然想走到下一步。老实说这个项目的代码量并不大去掉OLED驱动库核心逻辑不到200行。它真正的价值在于让你把时序捕获、滤波算法、外设驱动、交互策略这几个嵌入式开发里最关键的环节串起来。当我后来做更复杂的控制系统时这套认知框架——先分层验证再合并联调最后针对边界条件加保护——一直沿用到现在。如果我在初学阶段只停留点亮LED的难度后面也不会有能力去碰运动控制和实时系统。希望你做完这个项目之后不只是拿到一块能响的板子而是真正理解每一个模块背后你为什么要这么设计。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价