一、项目概述

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

演示视频:STM32F103+HC-SR04 超声波测距+OLED 显示完整教程|原理+接线+代码逻辑+故障排查https://www.bilibili.com/video/BV1TCge6MEf1/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

二、硬件接线对照表

外设引脚 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 共地
关键接线注意点
  1. ECHO 输出 5V 高电平,PB5 为 5V 容忍引脚可直连;若使用其他 MCU 无 5V 容忍 IO,需 1k+2k 电阻分压降压至 3.3V 左右。
  2. 所有设备必须共地,无共地会出现信号识别异常、测距超时。
  3. PB4 复位默认复用 JTAG NJTRST,代码必须关闭 JTAG 复用,否则无法正常输出 TRIG 信号。

三、HC-SR04 核心工作原理

1. TOF 飞行时间测距法

HC-SR04 内置发射、接收双探头,发送≥10μs 高电平至 TRIG 后,模块自动完成整套动作:

  1. 发射 8 个周期 40kHz 超声波;
  2. ECHO 引脚拉高等待回波;
  3. 收到反射波后拉低 ECHO;
  4. 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 标准时序

  1. STM32 输出 15μs 高电平触发(预留裕量,大于模块要求的 10μs 最小值);
  2. 模块发射 8 个 40kHz 超声波;
  3. ECHO 立刻拉高,等待反射信号;
  4. 收到回波后 ECHO 拉低,高低电平差值即为往返时间 t;
  5. 两次测距间隔建议≥60ms,本程序延时 100ms 防止回波干扰。

五、STM32 底层配置详解

1. GPIO 配置
  • PB4:50MHz 推挽输出,关闭 JTAG 复用,保留 SWD 下载调试功能;
  • PB5:下拉输入,稳定读取 ECHO 电平,无信号时默认低电平。
2. TIM2 定时器微秒计时(核心)

系统时钟 72MHz,APB1 定时器倍频至 72MHz,预分频 71,定时器计数频率 1MHz。 每 1 次计数 = 1μs,无需额外换算,计数值直接等于回波微秒时长。

六、软件完整执行流程

  1. 初始化:GPIO、TIM2、OLED、延时函数;
  2. 单次测距函数逻辑: ① 等待 ECHO 拉低,30ms 超时保护; ② PB4 输出 15μs 触发脉冲; ③ 等待 ECHO 上升沿,30ms 超时; ④ TIM2 清零,持续检测下降沿; ⑤ 捕获下降沿读取总微秒数; ⑥ 公式换算毫米距离,限制 2cm~4m 有效量程;
  3. 测量成功:OLED 打印 XX.X cm;
  4. 超时 / 无回波: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

八、高频故障排查大全

  1. OLED 一直显示 No echo 无回波

    • HC-SR04 未接 5V 供电;
    • 模块、STM32、OLED 未共地;
    • PB4 未关闭 JTAG 复用;
    • TRIG/ECHO 接线颠倒;
    • 目标小于 2cm 或超过 4m;
    • 探头被遮挡。
  2. 近距离物体测不出数值 HC-SR04 存在 2cm 盲区,发射后电路未稳定,近距回波无法识别,测试建议 10cm~1m。

  3. 数值持续跳动不稳定

    • 被测物体过小、表面倾斜;
    • 海绵、布料等吸声材料;
    • 电源纹波大、接地不良;
    • 周边存在其他超声波模块干扰; 优化方案:多次采样取均值滤波。
  4. 测量数值和实际差距大 未做温度补偿,高低温环境声速变化带来系统误差。

九、项目拓展方向

  1. 添加 DS18B20 温度传感器,实时修正声速消除温度误差;
  2. 加入滑动平均滤波,解决数据跳变;
  3. 多 HC-SR04 分时触发,避免相互干扰;
  4. 增加蜂鸣器,距离过近报警提醒。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐