STM32F103+HC-SR04 超声波测距+OLED 显示完整教程|原理+接线+代码逻辑+故障排查
一、项目概述
本项目基于 STM32F103C8T6 主控,搭配 HC-SR04 超声波测距模块、I2C OLED 屏幕实现实时距离测量。 整体流程:STM32 PB4 输出 15μs 触发信号启动测距→HC-SR04 发射 40kHz 超声波→物体反射回波→ECHO 输出对应往返时间的高电平→TIM2 定时器精准测量脉宽→声速公式换算距离→OLED 实时展示厘米数值。 适合单片机入门、传感器实训课堂教学,完整覆盖硬件接线、时序原理、定时器配置、软件逻辑、常见踩坑问题。

二、硬件接线对照表
| 外设引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| HC-SR04 TRIG | PB4 | 输出触发脉冲,启动测距 |
| HC-SR04 ECHO | PB5 | 输入回波脉冲,脉宽代表超声波往返时间 |
| HC-SR04 VCC | 5V | 模块供电,不可用 3.3V |
| HC-SR04 GND | GND | 与 STM32、OLED 共地(必做) |
| OLED SCL | PB8 | 软件 I2C 时钟 |
| OLED SDA | PB9 | 软件 I2C 数据 |
| OLED VCC | 3.3V | OLED 供电 |
| OLED GND | GND | 共地 |
关键接线注意点
- ECHO 输出 5V 高电平,PB5 为 5V 容忍引脚可直连;若使用其他 MCU 无 5V 容忍 IO,需 1k+2k 电阻分压降压至 3.3V 左右。
- 所有设备必须共地,无共地会出现信号识别异常、测距超时。
- PB4 复位默认复用 JTAG NJTRST,代码必须关闭 JTAG 复用,否则无法正常输出 TRIG 信号。
三、HC-SR04 核心工作原理
1. TOF 飞行时间测距法
HC-SR04 内置发射、接收双探头,发送≥10μs 高电平至 TRIG 后,模块自动完成整套动作:
- 发射 8 个周期 40kHz 超声波;
- ECHO 引脚拉高等待回波;
- 收到反射波后拉低 ECHO;
- ECHO 高电平时长 = 超声波往返总时间。
测距核心公式推导: 总路程 = 2× 距离 = 声速 × 往返时间 d = v × t / 2
室温声速 340m/s 简化换算: 距离 (cm) = 回波时间 (μs) / 58 完整毫米整数计算公式(单片机无浮点): measured_mm = (echo_time_us * 10 + 29) / 58 +29 作用:整数除法四舍五入,避免精度丢失。
2. 声速温度误差说明
标准声速公式:v=331.4+0.6T(T 为摄氏度) 本工程未做温度补偿,环境温差大时会出现测量偏差;进阶项目可外接温感动态修正声速。
3. 模块标称参数
- 供电:5V
- 工作频率:40kHz
- 量程:2cm~4m
- 盲区:2cm 以内无法测量
- 测量角度:15°
- 分辨率:约 3mm
四、TRIG&ECHO 标准时序
- STM32 输出 15μs 高电平触发(预留裕量,大于模块要求的 10μs 最小值);
- 模块发射 8 个 40kHz 超声波;
- ECHO 立刻拉高,等待反射信号;
- 收到回波后 ECHO 拉低,高低电平差值即为往返时间 t;
- 两次测距间隔建议≥60ms,本程序延时 100ms 防止回波干扰。
五、STM32 底层配置详解
1. GPIO 配置
- PB4:50MHz 推挽输出,关闭 JTAG 复用,保留 SWD 下载调试功能;
- PB5:下拉输入,稳定读取 ECHO 电平,无信号时默认低电平。
2. TIM2 定时器微秒计时(核心)
系统时钟 72MHz,APB1 定时器倍频至 72MHz,预分频 71,定时器计数频率 1MHz。 每 1 次计数 = 1μs,无需额外换算,计数值直接等于回波微秒时长。
六、软件完整执行流程
- 初始化:GPIO、TIM2、OLED、延时函数;
- 单次测距函数逻辑: ① 等待 ECHO 拉低,30ms 超时保护; ② PB4 输出 15μs 触发脉冲; ③ 等待 ECHO 上升沿,30ms 超时; ④ TIM2 清零,持续检测下降沿; ⑤ 捕获下降沿读取总微秒数; ⑥ 公式换算毫米距离,限制 2cm~4m 有效量程;
- 测量成功:OLED 打印 XX.X cm;
- 超时 / 无回波:OLED 提示 No echo,检查接线。
超时保护作用
三处 30ms 超时判断,防止无目标、接线故障时程序卡死 while 循环,屏幕持续刷新报错信息。
七、核心函数调用示例
u16 distance_mm;
HC_SR04_Init();
// 循环测距
while(1)
{
if(HC_SR04_ReadDistance(&distance_mm) != 0)
{
// 测量成功,distance_mm单位毫米
OLED_ShowDistance(distance_mm);
}
else
{
// 超时/接线错误/超量程
OLED_ShowError();
}
delay_ms(100);
}
main.c
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "HC_SR04.h"
#define HC_SR04_REFRESH_MS 100
static void OLED_ShowDistance(u16 distance_mm);
static void OLED_ShowError(void);
int main(void)
{
u16 distance_mm;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
HC_SR04_Init();
OLED_Clear();
OLED_ShowString(36, 0, (u8 *)"HC-SR04", 12);
OLED_ShowString(10, 18, (u8 *)"TRIG:PB4 ECHO:PB5", 12);
OLED_ShowString(20, 40, (u8 *)"Initializing", 12);
OLED_Refresh();
delay_ms(200);
while(1)
{
if(HC_SR04_ReadDistance(&distance_mm) != 0U)
{
OLED_ShowDistance(distance_mm);
}
else
{
OLED_ShowError();
}
delay_ms(HC_SR04_REFRESH_MS);
}
}
static void OLED_ShowDistance(u16 distance_mm)
{
u16 distance_cm;
u8 x;
OLED_Clear();
OLED_ShowString(36, 0, (u8 *)"HC-SR04", 12);
OLED_ShowString(10, 16, (u8 *)"TRIG:PB4 ECHO:PB5", 12);
OLED_ShowString(10, 32, (u8 *)"Distance:", 12);
distance_cm = distance_mm / 10U;
x = 24;
if(distance_cm >= 100U)
{
OLED_ShowChar(x, 46, (u8)(distance_cm / 100U + '0'), 16);
}
else
{
OLED_ShowChar(x, 46, ' ', 16);
}
x += 8;
if(distance_cm >= 10U)
{
OLED_ShowChar(x, 46, (u8)((distance_cm / 10U) % 10U + '0'), 16);
}
else
{
OLED_ShowChar(x, 46, ' ', 16);
}
x += 8;
OLED_ShowChar(x, 46, (u8)(distance_cm % 10U + '0'), 16);
OLED_ShowChar((u8)(x + 8U), 46, '.', 16);
OLED_ShowChar((u8)(x + 16U), 46,
(u8)(distance_mm % 10U + '0'), 16);
OLED_ShowString((u8)(x + 24U), 46, (u8 *)" cm", 16);
OLED_Refresh();
}
static void OLED_ShowError(void)
{
OLED_Clear();
OLED_ShowString(36, 0, (u8 *)"HC-SR04", 12);
OLED_ShowString(10, 20, (u8 *)"No echo / timeout", 12);
OLED_ShowString(13, 40, (u8 *)"Check PB4 / PB5", 12);
OLED_Refresh();
}
HC_SR04.c
#include "HC_SR04.h"
#include "delay.h"
#include "stm32f10x_tim.h"
#define HC_SR04_TRIG_PIN GPIO_Pin_4
#define HC_SR04_ECHO_PIN GPIO_Pin_5
#define HC_SR04_TIMER_FREQUENCY_HZ 1000000UL
#define HC_SR04_TIMEOUT_US 30000U
#define HC_SR04_MAX_ECHO_US 24000U
/*
* 等待 ECHO 引脚变为指定电平。
* 参数 level:需要等待的目标电平。
* 参数 timeout_us:最长等待时间,单位为微秒。
* 返回值:1 表示检测到目标电平;0 表示等待超时。
*
* 每次等待前将 TIM2 计数器清零。这样既可以判断超时,也能防止
* 传感器未连接或无回波时程序永久阻塞在循环中。
*/
static u8 HC_SR04_WaitEchoLevel(BitAction level, u16 timeout_us)
{
TIM_SetCounter(TIM2, 0);
while(GPIO_ReadInputDataBit(GPIOB, HC_SR04_ECHO_PIN) != level)
{
if(TIM_GetCounter(TIM2) >= timeout_us)
{
return 0;
}
}
return 1;
}
void HC_SR04_Init(void)
{
GPIO_InitTypeDef gpio;
TIM_TimeBaseInitTypeDef timer;
RCC_ClocksTypeDef clocks;
u32 timer_clock;
/*
* STM32F103 上电复位后,PB4 默认是 JTAG 的 NJTRST 引脚。
* 这里关闭 JTAG 功能以释放 PB4,同时保留 PA13、PA14 上的 SWD,
* 因此仍然可以使用 ST-Link 下载程序和在线调试。
*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO |
RCC_APB2Periph_GPIOB, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
/* 将 PB4 配置为推挽输出,用于产生 TRIG 触发脉冲。 */
gpio.GPIO_Pin = HC_SR04_TRIG_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &gpio);
GPIO_ResetBits(GPIOB, HC_SR04_TRIG_PIN);
/* 将 PB5 配置为下拉输入,用于读取 ECHO 回波脉冲。 */
gpio.GPIO_Pin = HC_SR04_ECHO_PIN;
gpio.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOB, &gpio);
/*
* 将 TIM2 配置为 1 MHz 向上计数,因此计数器每增加 1 就表示经过
* 1 微秒。当 APB1 总线存在分频时,APB1 定时器的输入时钟为
* PCLK1 的两倍,所以需要根据当前时钟树计算 TIM2 的实际输入时钟。
*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_GetClocksFreq(&clocks);
timer_clock = clocks.PCLK1_Frequency;
if((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1)
{
timer_clock *= 2U;
}
TIM_TimeBaseStructInit(&timer);
timer.TIM_Prescaler = (u16)(timer_clock /
HC_SR04_TIMER_FREQUENCY_HZ - 1U);
timer.TIM_Period = 0xFFFFU;
timer.TIM_CounterMode = TIM_CounterMode_Up;
timer.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &timer);
TIM_SetCounter(TIM2, 0);
TIM_Cmd(TIM2, ENABLE);
}
u8 HC_SR04_ReadDistance(u16 *distance_mm)
{
u16 echo_time_us;
u32 measured_mm;
/* 防止调用者传入空指针。 */
if(distance_mm == 0)
{
return 0;
}
/* 测量开始前确认 ECHO 为低电平,避免上一次残留脉冲影响本次测量。 */
if(HC_SR04_WaitEchoLevel(Bit_RESET, HC_SR04_TIMEOUT_US) == 0U)
{
return 0;
}
/* TRIG 输出不少于 10 微秒的高电平,通知 HC-SR04 发射超声波。 */
GPIO_SetBits(GPIOB, HC_SR04_TRIG_PIN);
delay_us(15);
GPIO_ResetBits(GPIOB, HC_SR04_TRIG_PIN);
/* 等待 ECHO 上升沿;检测到高电平表示模块已经开始输出回波脉冲。 */
if(HC_SR04_WaitEchoLevel(Bit_SET, HC_SR04_TIMEOUT_US) == 0U)
{
return 0;
}
/* 从 ECHO 上升沿开始计时,下降沿到来时得到高电平脉宽。 */
TIM_SetCounter(TIM2, 0);
while(GPIO_ReadInputDataBit(GPIOB, HC_SR04_ECHO_PIN) == Bit_SET)
{
if(TIM_GetCounter(TIM2) >= HC_SR04_TIMEOUT_US)
{
return 0;
}
}
echo_time_us = TIM_GetCounter(TIM2);
/* 脉宽为零或超过模块正常测距范围时,本次测量判定为无效。 */
if(echo_time_us == 0U || echo_time_us > HC_SR04_MAX_ECHO_US)
{
return 0;
}
/*
* HC-SR04 常用换算公式:
* 距离(厘米)= ECHO 高电平时间(微秒)/ 58
*
* 这里直接换算为毫米,并通过加上 29 后再除以 58 实现四舍五入:
* 距离(毫米)= (脉宽 × 10 + 29) / 58
*/
measured_mm = ((u32)echo_time_us * 10U + 29U) / 58U;
/* HC-SR04 的标称最大测量距离为 4 米。 */
if(measured_mm > 4000U)
{
return 0;
}
*distance_mm = (u16)measured_mm;
return 1;
}
HC_SR04.h
#ifndef __HC_SR04_H
#define __HC_SR04_H
#include "sys.h"
/*
* 文件名称:HC_SR04.h
* 功能说明:声明 HC-SR04 超声波测距模块的初始化与距离读取接口。
*
* 硬件连接:
* HC-SR04 TRIG 引脚 -> STM32 PB4
* HC-SR04 ECHO 引脚 -> STM32 PB5
*
* 距离读取结果以毫米为单位,主程序可直接将其转换为带一位小数的
* 厘米数进行显示,整个计算过程不需要使用浮点运算。
*/
/* 初始化 PB4、PB5 和用于微秒计时的 TIM2。 */
void HC_SR04_Init(void);
/*
* 启动一次超声波测距并读取距离。
* 参数 distance_mm:用于保存测量距离,单位为毫米。
* 返回值:1 表示测量成功;0 表示参数无效、等待回波超时或距离超量程。
*/
u8 HC_SR04_ReadDistance(u16 *distance_mm);
#endif
八、高频故障排查大全
-
OLED 一直显示 No echo 无回波
- HC-SR04 未接 5V 供电;
- 模块、STM32、OLED 未共地;
- PB4 未关闭 JTAG 复用;
- TRIG/ECHO 接线颠倒;
- 目标小于 2cm 或超过 4m;
- 探头被遮挡。
-
近距离物体测不出数值 HC-SR04 存在 2cm 盲区,发射后电路未稳定,近距回波无法识别,测试建议 10cm~1m。
-
数值持续跳动不稳定
- 被测物体过小、表面倾斜;
- 海绵、布料等吸声材料;
- 电源纹波大、接地不良;
- 周边存在其他超声波模块干扰; 优化方案:多次采样取均值滤波。
-
测量数值和实际差距大 未做温度补偿,高低温环境声速变化带来系统误差。
九、项目拓展方向
- 添加 DS18B20 温度传感器,实时修正声速消除温度误差;
- 加入滑动平均滤波,解决数据跳变;
- 多 HC-SR04 分时触发,避免相互干扰;
- 增加蜂鸣器,距离过近报警提醒。
更多推荐




所有评论(0)