简介这是基于STM32单片机同时驱动4个HC-SR04超声波测距传感器、并在OLED屏幕上实时显示距离数据的完整源代码工程适合嵌入式入门及进阶开发者学习多传感器管理与显示交互。工程共235个文件包含C源代码、头文件、编译中间产物、工程配置与辅助脚本等压缩包约12.63MB可在Keil等IDE中直接打开参考。项目重点展示四个HC-SR04的独立触发与回波捕获、GPIO控制、定时器输入捕获、OLED的I2C/SPI驱动、中断处理及多路数据融合处理涵盖从底层驱动到应用层滤波与实时展示的完整链路并涉及模块化编程与实时系统资源分配思想。目前已有1649人学习对希望从零实现多路超声波测距系统、提升STM32外设综合运用能力的开发者有直接参考价值代码结构清晰便于二次开发与移植。 最近在整理一个手头的小项目用STM32单片机驱动4个HC-SR04超声波测距传感器再把4个方向的测量距离实时显示在一块0.96寸OLED屏幕上。这个组合在智能小车避障、多方位距离监测、小型雷达模拟这类场景里非常常见也是很多用单片机做课设、比赛或入门装机的经典起点。这套东西单看每一个模块都不复杂但一旦从“1个传感器串口打印”升级到“4个传感器OLED可视化”里面就藏着不少坑引脚怎么分配、如何避免4路超声波互相串扰、怎么在轻量级单片机上同时搞定多路测距和屏幕刷新、代码怎么组织才不混乱。这篇博文我会把我实际做通的这套源代码思路、接线方案、核心代码片段和调试时踩过的问题完整拆出来适合正在做STM32综合小项目、或者想从1路超声波升级成多路监测的朋友直接参考。1. 方案选型为什么是STM324路HC-SR04OLED1.1 这套组合解决什么问题先聊清楚需求。很多项目到后期会遇到一个瓶颈一个传感器只能测一个方向要么装在车头做避障要么只做单点入侵检测信息量太单一。如果把传感器扩展到4路就能同时监测前后左右四个方位实时感知周围的障碍物分布这才像一个“周边感知系统”该有的样子。选择STM32F103C8T6作为主控理由很实际它内部有足够多的定时器和GPIO引脚主频72MHz处理4路超声波的时间测量绰绰有余而且CubeMXHAL库的开发方式非常成熟网上资料铺天盖地遇到问题基本都能搜到答案。对于实验室、比赛、毕设这类场景选择F103几乎是性价比最高的方案。OLED屏幕选0.96寸I2C接口版本主要有两个原因。第一I2C只需要两根线SCL/SDA不额外占用太多GPIO毕竟4路超声波已经吃掉了8个引脚第二OLED的对比度高、可视角度大在强光下也比普通LCD屏幕清楚显示数字和状态信息非常合适。这4路传感器的数据和OLED显示配合起来就是一个“多方向距离监测站”如果扩展一下中断逻辑还能做成自动报警或避障的小系统。1.2 4路传感器带来的设计约束真正开始布线之前必须想清楚三个约束条件。第一个约束是引脚资源。每路HC-SR04需要1个Trig触发引脚和1个Echo回声引脚4路总共8个GPIO。为了保证信号稳定这8个引脚尽量集中在同一组GPIO上代码写起来也方便我直接选用了PA0到PA7这8个引脚。OLED则固定挂在I2C1的PB6/PB7上。第二个约束是声波串扰。4个HC-SR04如果同时触发发出的超声波会在空气中互相交叉回波信号也会混叠测出来的距离会莫名其妙地跳动。解决思路很简单把4路传感器的触发时间彻底错开一次只测一个方向测完一个再测下一个不让任何两个传感器在同一时刻工作。第三个约束是时间测量精度。HC-SR04输出的是一段高电平脉冲脉冲宽度就是超声波从发射到接收经过的总时间这个时间通常是几百微秒到几十毫秒。要准确测量这个宽度系统的计时精度至少要达到微秒级。如果用HAL库里1ms精度的HAL_GetTick()来做误差会非常大后面我会专门讲用什么方式实现微秒级计时。2. 硬件接线把HC-SR04和OLED接到STM32上2.1 物料清单做这套项目需要准备的东西不算多都是常规器件物料型号/规格数量备注主控板STM32F103C8T6核心板1块“蓝丸”最小系统板超声波模块HC-SR044个需要注意供电电压5VOLED屏幕0.96寸 SSD1306 I2C接口1块也可以选SPI版本接线图不同下载调试器ST-Link V21个支持SWD下载杜邦线母对母若干建议买不同颜色方便区分面包板或洞洞板常规尺寸1块方便固定和改线这里有个容易忽视的点HC-SR04官方标称工作电压是5V而STM32的GPIO工作电压是3.3V两者直接连接在思路可行但需要谨慎。Trig引脚由STM32的3.3V高电平驱动这个电平高于HC-SR04的TTL高电平阈值约为2V所以能正常触发。Echo引脚反馈回来的是5V高电平这就要求STM32侧的引脚必须是5V容忍FT引脚。PA0到PA7这8个引脚在F103C8T6数据手册里都属于FT引脚可以直接接受5V电平。如果你换了其他型号单片机接线前务必确认引脚是不是5V容忍不是的话就用电阻分压把Echo信号降到3.3V最简单的方法是串一个1k电阻再并联一个2k电阻到地分压到约3.3V。2.2 完整接线表我实际使用的接线关系如下模块引脚接到STM32超声波1 TrigPA0方向前超声波1 EchoPA1方向前超声波2 TrigPA2方向左超声波2 EchoPA3方向左超声波3 TrigPA4方向右超声波3 EchoPA5方向右超声波4 TrigPA6方向后超声波4 EchoPA7方向后OLED SCLPB6I2C1_SCLOLED SDAPB7I2C1_SDAOLED/传感器 VCC5V共用系统5V电源OLED/传感器 GNDGND必须共地整套系统里有一个“共地”原则所有模块的GND必须和STM32的GND连接在一起否则信号电平就没有统一参考测距和显示都会出现诡异问题。电源方面建议用一个稳压模块统一给STM32和传感器供电如果传感器数量多避免直接从单片机3.3V引脚取电HC-SR04在工作时电流峰值可以达到15mA左右4个同时工作再加上屏幕3.3V稳压芯片容易发烫。3. 原理与代码设计从回波时间到屏幕数字3.1 HC-SR04的测距公式是怎么来的HC-SR04的工作流程非常有规律给Trig引脚一个不低于10微秒的高电平模块内部就会发出8个40kHz的超声波脉冲同时把Echo引脚拉高然后模块等待超声波碰到障碍物反弹回来一旦收到回波Echo引脚被拉低。Echo引脚高电平持续的时间正好就是声波从发射到返回的总时间。距离计算公式是距离cm 高电平时间us× 0.017这个系数很多人不清楚怎么来的。声波在空气中的传播速度约为340m/s也就是34cm/ms换算到微秒就是0.034cm/us。但超声波走的是往返两个方向实际距离是声波总路程的一半所以要除以2于是0.034除以2等于0.017。也就是说每1微秒的回波时间对应0.017厘米的往返半距离。这个公式的前提是声速固定为340m/s但实际声速会随温度变化0℃时约331m/s25℃时约346m/s。如果环境温度变化大测量误差会变得明显更精确的做法是加入温度补偿公式变成声速V 331 0.6 × TT为环境温度单位℃然后根据实测的高电平时间t用V × t / 2计算距离。做室内常温测量的话直接用340m/s问题不大你要是做户外项目建议加上温度传感器。HC-SR04的标称测量范围是2cm到400cm实际使用中稳定区大概在3cm到350cm。距离太近会进入盲区导致读数乱跳距离太远回波衰减严重容易超时测不到。代码里一定要做好超时处理一般Echo高电平超过25ms还没结束就认为本次测量无效。3.2 OLED显示的驱动思路SSD1306这款OLED驱动芯片几乎占据了0.96寸屏幕的大半市场它的内部是一块128×64像素的点阵用I2C接口只需要两根线就能完成数据通信。默认I2C从机地址是0x3C7位地址模式也有少数屏幕是0x3D如果屏幕点亮后无反应第一件事就要检查地址对不对。驱动OLED的核心操作很简单往控制寄存器写命令往显存写数据。基于HAL库的移植方式非常成熟可以直接引入现成的ssd1306驱动文件初始化流程大约包括屏幕开关、显示时钟分频、复用比率、列地址起始、对比度设置、扫描方向、电荷泵开启等十几个指令。我建议把常见的字符显示函数显示字符串、显示数字封装好后续在屏幕上显示测距结果时就不需要关心底层细节了只需要调用 ShowString(x, y, CH1) 这样的接口即可。4路传感器显示时注意屏幕分区128×64的屏幕适合分成4行每行8像素高或者用8×16字号分成4块每个角落显示一个通道。3.3 软件架构按模块拆分这种小系统的代码量并不大但如果不做模块化功能一多就会乱成一锅粥。我的习惯是拆成三个文件层次ultrasonic.c/h负责HC-SR04的引脚映射、触发、回波时间读取、距离计算对外提供单个传感器测距函数和多路扫描函数。oled.c/h负责屏幕初始化、清屏、字符显示、数字显示对外提供距离刷新函数。main.c只做两件事初始化外设然后在while(1)里循环执行多路扫描和显示刷新。这样做的好处很直接如果你想换成SPI接口的OLED只需要改oled.c/h测距代码完全不用碰如果想把4路超声波扩展成6路也只需要在ultrasonic.c/h里加引脚映射主逻辑保持不动。模块之间的耦合越小后期调试越省心。4. 关键代码实现轮询加微秒级计时方案4.1 用DWT实现微秒级计时测量Echo高电平时间是这个项目的核心我用的是Cortex-M3内核自带的DWT周期计数器而不是额外占用一个定时器。DWTData Watchpoint and Trace单元里有一个32位的CYCCNT计数器每个CPU时钟周期自动加1主频72MHz时相当于计时精度到1/72微秒。初始化代码很简短#include core_cm3.h void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; }初始化之后读取DWT-CYCCNT的值就能获得当前时刻的累计周期数。两个时刻的周期数之差除以72就是经过的微秒数。顺带还能写一个微秒级延时函数用来给Trig引脚提供10us的高电平脉冲void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * 72; while ((DWT-CYCCNT - start) ticks); }这段代码放在延时要求不高但对精度有要求的场景非常好用不用配置定时器也不用写中断服务函数几行代码就能实现。4.2 单路超声波测距函数整体流程分四步第一步给Trig发10us高电平第二步等待Echo变高第三步记录Echo变高后的周期计数第四步等待Echo变低并计算经过的微秒数最后换算成距离。完整代码如下float Ultrasonic_Read(uint8_t ch) { GPIO_TypeDef *trig_port, *echo_port; uint16_t trig_pin, echo_pin; uint32_t timeout; uint32_t start_cycle, end_cycle; switch (ch) { case 0: trig_port GPIOA; trig_pin GPIO_PIN_0; echo_port GPIOA; echo_pin GPIO_PIN_1; break; case 1: trig_port GPIOA; trig_pin GPIO_PIN_2; echo_port GPIOA; echo_pin GPIO_PIN_3; break; case 2: trig_port GPIOA; trig_pin GPIO_PIN_4; echo_port GPIOA; echo_pin GPIO_PIN_5; break; case 3: trig_port GPIOA; trig_pin GPIO_PIN_6; echo_port GPIOA; echo_pin GPIO_PIN_7; break; default: return -1.0f; } HAL_GPIO_WritePin(trig_port, trig_pin, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(trig_port, trig_pin, GPIO_PIN_RESET); timeout HAL_GetTick(); while (HAL_GPIO_ReadPin(echo_port, echo_pin) GPIO_PIN_RESET) { if (HAL_GetTick() - timeout 20) return -1.0f; } start_cycle DWT-CYCCNT; while (HAL_GPIO_ReadPin(echo_port, echo_pin) GPIO_PIN_SET) { if ((DWT-CYCCNT - start_cycle) / 72 25000) return -1.0f; } end_cycle DWT-CYCCNT; return (float)(end_cycle - start_cycle) / 72.0f * 0.017f; }等待Echo变低的循环里也要加超时判断防止传感器损坏或者前方完全无回波时程序死循环。超时阈值我设为25ms如果Echo高电平超过25ms说明测量超出量程或者模块故障直接返回-1上位机判断到负数就知道本次测量无效。4.3 4路分时扫描与简单滤波多路扫描最忌讳同时触发我的做法是在主循环里逐个测量一个测完后再测下一个测完所有4路之后统一更新显示float dist[4]; while (1) { for (uint8_t i 0; i 4; i) { dist[i] Ultrasonic_Read(i); HAL_Delay(30); } OLED_ShowAllDistance(dist); HAL_Delay(50); }每路之间加30ms延时一方面是为了保证HC-SR04模块完成一次完整测量另一方面是让声波在空气中衰减掉减少对下一路测量的干扰。整个扫描一轮大约耗时150ms左右刷新频率约6Hz对于距离显示和小车避障来说完全够用。如果你发现测量值显示出来有轻微抖动可以加一个简单的三点中值滤波。思路是连续测量3次取中间值作为最终结果。中值滤波能有效去掉突发的异常尖峰比如超声波偶尔收到一个来自侧面的杂波。代价是响应速度会慢一些每路的测量时间会从几十毫秒变成上百毫秒。对于多数场景这个取舍是值得的。4.4 OLED刷新显示OLED显示部分不打算把完整驱动贴出来那些是现成库的内容只说关键的应用层代码。屏幕上按4行布局每一行显示一路传感器的编号和距离void OLED_ShowAllDistance(float *dist) { char buf[16]; OLED_Clear(); for (uint8_t i 0; i 4; i) { if (dist[i] 0) { sprintf(buf, CH%d: --cm, i 1); } else { sprintf(buf, CH%d: %3dcm, i 1, (int)dist[i]); } OLED_ShowString(0, i * 16, buf); } }这里有一个改善显示质量的小技巧不要每刷新一次就调用OLED_Clear()整屏清空因为清屏过程中屏幕会明显闪一下白底蓝底尤其明显。更好的做法是只更新数值变化的区域比如固定显示“CH1:”这几个字符只刷新后面的距离数字区域。如果屏幕是黑底白字整屏清空的闪烁感还能接受但在实际项目里能局部刷新就局部刷新这样看起来专业很多。5. 编译下载与调试绕开环境里的暗坑5.1 工程初始化与CubeMX配置建议直接用STM32CubeMX生成工程骨架配置项非常清晰芯片选STM32F103C8TxRCC选择外部晶振PA0到PA7全部设为GPIO输出或输入模式PB6/PB7设为I2C1调试接口选Serial Wire。具体到引脚模式Trig引脚需要连续输出高电平设置为GPIO_PIN_OUTPUT即可Echo引脚用于读取外部信号设置为GPIO_PIN_INPUT即可。需要注意如果是从别的工程模板复制过来的初始化代码一定要核对GPIO模式Echo如果被配置成输出模式读取到的数据永远是固定的表现为距离一直为0或一直为最大值。时钟树上把主频配到72MHzI2C1初始化时选标准模式100kHz不要用400kHz快速模式。快速模式在OLED上通常也能跑但线长、上拉电阻不合适时容易偶发性花屏追求稳定就100kHz起步。5.2 最常见的“no STM32 target found”怎么处理开发过程中被问得最多的一个问题就是Keil报错no stm32 target found! if your product embedsdebug authentication...。这个报错字面意思是下载器没有找到目标芯片通常不是代码问题而是硬件连接或调试配置问题。排查顺序我整理成一个表排查项具体操作SWD线序ST-Link的SWDIO接PA13SWCLK接PA14GND必须连接芯片供电量一下STM32的3.3V引脚确认核心板稳压输出正常调试器模式Keil里选ST-Link DebuggerSettings里选SW模式而不是JTAGBOOT0状态BOOT0一般拉低到GND如果不小心悬空或拉高可能进不了主程序下载时复位Keil Debug Settings里的Connect选项选择under Reset驱动安装确认ST-Link驱动已装好设备管理器里能看到ST-Link设备如果以上都检查了仍然连不上把ST-Link从电脑拔掉重插同时给目标板彻底断电再上电。很多“连不上”的情况其实就是下载器固件卡死重新枚举USB设备就好。另外确认你的核心板没有被之前烧录的程序占用了SWD引脚有些初始化代码会把PA13/PA14重映射成普通GPIO这会导致第二次无法下载解决办法是BOOT0拉高进入ISP模式后再擦除或者用ST-Link Utility连上后执行擦除。5.3 外部晶振和起振电容的关系这个项目和晶振有关的坑比较隐蔽。STM32F103C8T6最小系统板的板载晶振通常是8MHz晶振旁边两个负载电容的值不是随便选的它直接影响晶振能不能正常起振。负载电容的计算公式是CL (C1 × C2) / (C1 C2) Cstray一般C1和C2取相同值所以可以简化为C 2 × (CL - Cstray)其中CL是晶振手册里给出的负载电容参数常见值在12pF到22pF之间Cstray是PCB走线和引脚带来的杂散电容估算3到5pF。如果晶振规格CL20pF杂散电容约5pF则C2×(20-5)30pF取常见的33pF。如果CL12pF则C约15pF取15pF或者18pF。如果焊上去的电容偏差太大可能出现“程序在低温环境起不来”“偶尔跑飞”“串口乱码”这类奇怪现象。项目里如果遇到了这类不稳定问题别怀疑代码先去查最小系统电路。6. 常见问题与排查技巧实录6.1 测量值一直为0这是超声波项目里最容易出现的问题。先分清是“程序读到的Echo时间始终为0”还是“Echo引脚根本不变高”。用示波器或者干脆用万用表量Echo引脚在触发后的电平变化就能快速定位。如果Echo始终是低电平大概率是Trig没有正常触发最常见的原因是GPIO配置成了复用模式或者触发的10us脉冲没发出去如果Echo一直是高电平大部分是Echo引脚被配置成了输出模式或者模块损坏。另外前面提到的电平匹配问题也要排查echo的5V信号如果接到非FT引脚上可能直接把引脚内部钳位电路拉死最终表现为一直读到低电平。6.2 测量值抖动剧烈排除声波串扰之外最常见原因是供电不足。4路HC-SR04同时工作的时候电流脉冲会在供电线上产生压降导致模块内部参考电压抖动测距结果随之跳变。解决办法是在每个传感器的VCC和GND之间就近并联一个100uF电解电容或者10uF陶瓷电容成本几毛钱效果立竿见影。第二个常见原因是传感器正对的环境里有角度倾斜的反射面比如墙面和超声波模块不是垂直关系回波会反射到别处去读到的距离忽远忽近。这个时候不能怪代码要调整传感器安装角度让探头的方向尽量和被测面垂直。6.3 4路超声波互相干扰如果你发现“前面有障碍物但显示的是后面那一路的读数”这就基本可以断定声波串扰了。一个传感器的超声波发射出去被另一个传感器的探头接收就会把别人的回波当成自己的。解决分两个层面硬件上让4个传感器之间保持至少10cm的间距如果空间不允许就在传感器之间加隔音挡板软件上严格执行分时触发相邻两次触发之间留出足够的静默时间。另外避免选择同一触发频率的传感器同时工作不过HC-SR04的40kHz频率是固定的所以分时是唯一可靠手段。6.4 OLED花屏或者白屏花屏的排查方向很固定I2C地址对不对、接线牢不牢、电源稳不稳。先确认屏幕模块的参数地址是0x3C还是0x3D然后把I2C速度降到100kHz最后把连接线尽可能缩短杜邦线太长容易引入干扰。白屏还有一个常见原因是SSD1306的电荷泵没有开启初始化代码里命令0x8D、参数0x14这个组合必须执行很多精简驱动的初始化列表里漏了这一步就会导致屏幕完全没有显示。6.5 测距精度提升的三个细节如果你希望测量精度更高一些除了前面说的温度补偿外三个点值得做。一是去掉每次测量的固定系统误差用尺子量一段已知距离对比模块读数和真实值的偏差在代码里减掉这个偏差就能从模块本身消除大部分误差。二是对4路传感器的测量结果分别标定因为每个传感器的声学特性略微不同统一用一个补偿值精度提升不明显逐路标定才是正道。三是减小OLED刷新和测距之间的相互影响在测距过程中不要频繁操作I2C总线我把扫描和显示放在同一个主循环里扫描完成后再统一刷新屏幕避免I2C通信产生的延时干扰计时。这套4路测距加OLED显示的系统做完后我最大的体会是“代码不算难难的是让4个传感器在一个系统里和平共处”。分时触发这个思路不仅在HC-SR04上有用换成红外测距、激光测距模块同样成立——多路传感系统永远要把通道隔离放在第一位。最后再分享一个小技巧如果后续你打算把这个系统接到电机驱动上做自动避障Echo引脚可以改用外部中断加定时器输入捕获的方式实现非阻塞测量CPU不用死等回波测距的同时还能处理电机闭环控制、执行其他任务。我最初用轮询方式是为了让代码结构足够直观真正工程化的时候建议把测距部分改造为中断驱动那样整个系统的实时性会再上一个台阶。本文还有配套的精品资源点击获取
