前言
在之前的文章中,我使用HAL库实现了HC-SR04的驱动。但仍有不少开发者在使用 STM32标准外设库,因此本文将完全基于标准库,手把手带你从寄存器层面的初始化到完整测距功能实现,代码可直接整合到你的工程中编译运行。
本文基于 STM32F103C8T6 最小系统,使用 TIM3通道1 输入捕获测量ECHO高电平时间,测距范围 2cm ~ 400cm。
一、HC-SR04模块原理回顾

1.1 引脚与电气参数
| 引脚 | 功能 |
|---|---|
| VCC | 5V供电 |
| GND | 地 |
| TRIG | 触发信号输入(≥10μs高电平) |
| ECHO | 回响信号输出(高电平时间 = 超声波往返时间) |
工作频率40kHz,测距角度约15°,精度3mm。
1.2 工作时序
- 单片机拉高 TRIG ≥10μs 后拉低。
- 模块自动发射8个40kHz超声波,同时 ECHO 引脚输出高电平。
- 超声波遇到障碍物返回,模块接收后拉低 ECHO。
- ECHO高电平持续时间 = 声波往返时间 t。
1.3 距离计算公式
常用工程公式(结果为厘米):
距离(cm) = 高电平时间(μs) / 58
推导:声速340m/s = 0.034 cm/μs,往返距离 = 0.034×t,单程距离 = (0.034/2)×t = t/58.82 ≈ t/58。
二、标准库下的测量方案设计
仍采用 定时器输入捕获 方式。利用 TIM3_CH1 捕获 PA6 上的ECHO信号:
- 上升沿 :记录当前计数器值
capture_val1,同时切换为下降沿捕获。 - 下降沿 :记录计数器值
capture_val2,计算高电平时间 = capture_val2 - capture_val1(考虑溢出)。
使用定时器更新中断作为超时处理:如果只捕获到上升沿后长时间未捕获到下降沿(例如超出测距范围无回波),更新中断会重置状态并标记无效测量。
定时器关键配置
- TIM3 挂载在 APB1,最大频率72MHz。
- 预分频 PSC = 71 → 计数频率 = 72MHz/(71+1) = 1MHz,即每个计数1μs。
- 自动重装载 ARR = 65535(满量程)。
这样捕获值差直接就是微秒数,无需换算。
三、硬件接线
| HC-SR04 | STM32F103C8T6 |
|---|---|
| VCC | 5V |
| GND | GND |
| TRIG | PB0 |
| ECHO | PA6 (TIM3_CH1) |
注意:ECHO输出5V电平,虽然F103的IO口可容忍5V,但建议串接一个1kΩ电阻限流保护。
四、标准库工程配置与代码编写
如果你从零搭建工程,需要:
- 添加标准库文件(3.5版本)到工程。
- 在
stm32f10x_conf.h中开启所需外设头文件(GPIO、TIM、RCC、NVIC、USART等)。 - 配置系统时钟为72MHz(在
system_stm32f10x.c中默认即可,或直接调用SystemInit())。
以下给出完整的模块化代码。
4.1 hc_sr04.h
c
#ifndef __HC_SR04_H
#define __HC_SR04_H
#include "stm32f10x.h"
/* 引脚定义 */
#define TRIG_GPIO_PORT GPIOB
#define TRIG_GPIO_PIN GPIO_Pin_0
#define TRIG_RCC RCC_APB2Periph_GPIOB
#define ECHO_GPIO_PORT GPIOA
#define ECHO_GPIO_PIN GPIO_Pin_6
#define ECHO_RCC RCC_APB2Periph_GPIOA
/* 超声波模块初始化 */
void HC_SR04_Init(void);
/* 触发一次测距 */
void HC_SR04_Trigger(void);
/* 获取距离(厘米),成功返回正值,失败返回-1 */
float HC_SR04_GetDistance(void);
/* 检查一次测量是否完成(非阻塞查询用) */
uint8_t HC_SR04_IsDone(void);
/* 输入捕获中断服务函数中调用 */
void HC_SR04_IRQHandler(void);
/* 定时器溢出中断服务函数中调用 */
void HC_SR04_OverflowHandler(void);
#endif
4.2 hc_sr04.c
c
#include "hc_sr04.h"
#include <stdio.h> // 若使用printf
/* 静态全局变量 */
static __IO uint8_t capture_state = 0; // 0: 空闲,1: 已捕获上升沿,等待下降沿
static __IO uint16_t capture_rise = 0; // 上升沿计数值
static __IO uint16_t capture_fall = 0; // 下降沿计数值
static __IO uint8_t measure_complete = 0; // 测量完成标志
static float distance = -1.0f; // 当前距离值
/**
* @brief 微秒级延时(72MHz)
* @param us: 延时的微秒数
*/
static void delay_us(__IO uint32_t us)
{
uint32_t i;
for (; us > 0; us--)
for (i = 0; i < 12; i++); // 72MHz下实测约1μs
}
/**
* @brief 初始化TRIG引脚和ECHO捕获定时器
*/
void HC_SR04_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* ---- 1. 使能时钟 ---- */
RCC_APB2PeriphClockCmd(TRIG_RCC | ECHO_RCC, ENABLE); // GPIO
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3挂载APB1
/* ---- 2. 配置TRIG引脚(PB0)为推挽输出 ---- */
GPIO_InitStructure.GPIO_Pin = TRIG_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TRIG_GPIO_PORT, &GPIO_InitStructure);
GPIO_ResetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN); // 默认低电平
/* ---- 3. 配置ECHO引脚(PA6)为浮空输入 ---- */
GPIO_InitStructure.GPIO_Pin = ECHO_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(ECHO_GPIO_PORT, &GPIO_InitStructure);
/* ---- 4. 定时器基本配置:1MHz计数频率,自动重载65535 ---- */
TIM_TimeBaseStructure.TIM_Period = 65535; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC,72M/(71+1)=1MHz
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
/* ---- 5. 输入捕获通道1配置:上升沿捕获 ---- */
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 = 0x0; // 不滤波(可根据实际情况加滤波值)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
/* ---- 6. 使能输入捕获中断和更新中断 ---- */
TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_Update, ENABLE);
/* ---- 7. 配置NVIC ---- */
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 使能定时器 */
TIM_Cmd(TIM3, ENABLE);
}
/**
* @brief 触发测距:TRIG输出10~20μs高电平
*/
void HC_SR04_Trigger(void)
{
GPIO_SetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN);
delay_us(15); // 15μs
GPIO_ResetBits(TRIG_GPIO_PORT, TRIG_GPIO_PIN);
/* 清除上次状态,准备本次测量 */
capture_state = 0;
measure_complete = 0;
distance = -1.0f;
/* 启动定时器输入捕获(实际定时器一直运行,只需清除中断标志并开启捕获) */
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1 | TIM_IT_Update);
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
/* 设置首次捕获为上升沿(可能上一次是下降沿) */
TIM_ICInitTypeDef TIM_ICInitStructure;
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 = 0x0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
}
/**
* @brief 获取距离值
* @return 距离(cm),无效返回-1
*/
float HC_SR04_GetDistance(void)
{
return distance;
}
/**
* @brief 检查测量是否完成
* @return 1: 完成,0: 等待中
*/
uint8_t HC_SR04_IsDone(void)
{
return measure_complete;
}
/**
* @brief 输入捕获中断处理(在TIM3_IRQHandler中调用)
*/
void HC_SR04_IRQHandler(void)
{
/* 通道1捕获事件 */
if (TIM_GetITStatus(TIM3, TIM_IT_CC1) == SET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
if (capture_state == 0)
{
/* 上升沿捕获:记录当前计数器值,切换为下降沿捕获 */
capture_rise = TIM_GetCapture1(TIM3);
capture_state = 1;
/* 重新配置通道为下降沿捕获 */
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
}
else if (capture_state == 1)
{
/* 下降沿捕获:记录,关闭捕获中断,计算距离 */
capture_fall = TIM_GetCapture1(TIM3);
uint32_t high_time;
if (capture_fall > capture_rise)
{
high_time = capture_fall - capture_rise;
}
else
{
high_time = (0xFFFF - capture_rise) + capture_fall; // 发生一次溢出
}
/* 距离 = 时间(μs) / 58 */
distance = (float)high_time / 58.0f;
/* 范围检查 */
if (distance < 2.0f || distance > 400.0f)
{
distance = -1.0f;
}
measure_complete = 1;
capture_state = 0;
/* 关闭捕获中断,结束本次测量 */
TIM_ITConfig(TIM3, TIM_IT_CC1, DISABLE);
}
}
}
/**
* @brief 定时器溢出中断处理(超时用)
*/
void HC_SR04_OverflowHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
/* 如果正在等待下降沿,但溢出说明超时无回波 */
if (capture_state == 1)
{
capture_state = 0;
measure_complete = 1;
distance = -1.0f;
TIM_ITConfig(TIM3, TIM_IT_CC1, DISABLE); // 关闭捕获中断
}
}
}
4.3 main.c
c
#include "stm32f10x.h"
#include "hc_sr04.h"
#include "usart.h" // 假设已封装了串口初始化函数
/* TIM3中断服务函数 */
void TIM3_IRQHandler(void)
{
HC_SR04_IRQHandler(); // 处理输入捕获
HC_SR04_OverflowHandler(); // 处理溢出超时
}
int main(void)
{
/* 配置系统时钟 */
SystemInit();
/* 初始化超声波模块 */
HC_SR04_Init();
/* 初始化串口用于打印(根据实际情况) */
USART1_Init();
printf("=== HC-SR04 Standard Library Demo ===\r\n");
while (1)
{
/* 触发测距 */
HC_SR04_Trigger();
/* 等待测距完成(此处简单延时100ms即可,也可用标志位查询) */
delay_ms(100);
float dist = HC_SR04_GetDistance();
if (dist > 0)
{
printf("Distance: %.1f cm\r\n", dist);
}
else
{
printf("Out of range!\r\n");
}
}
}
如果你的工程中没有
delay_ms,可自行使用SysTick实现一个毫秒延时。
五、代码详解与调试建议
-
定时器配置
TIM_Prescaler = 71得到1MHz计数频率。- 捕获值直接以微秒为单位,代入
t/58即得厘米距离。 - 测量上限:65535μs ≈ 65ms,对应距离约1129cm,远超模块量程,故无需担心正常情况下的溢出。
-
输入捕获中断处理
- 上升沿到来时,
TIM_GetCapture1获取当前计数值,并立即将捕获极性改为下降沿。 - 下降沿到来时,再次获取计数值,计算差值得到脉宽。
- 判断是否发生了一次定时器溢出(
capture_fall < capture_rise),若溢出则加上65536补偿。
- 上升沿到来时,
-
超时机制
- 定时器更新中断:如果已捕获到上升沿,但一直没有下降沿,随着定时器计数到65535溢出,会进入更新中断。
- 在中断中判断
capture_state == 1,说明超时无回波,将本次测量标记为无效。
-
测量周期
- 主循环中
delay_ms(100)保证相邻测量间隔至少100ms,远大于模块要求的60ms。
- 主循环中
-
移植到其他定时器
- 只需修改
hc_sr04.c中的 TIM3 相关宏,比如改用 TIM2_CH2(PA1),注意对应的APB和外设时钟。
- 只需修改
六、常见问题排查
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 距离一直为 -1 | 模块5V供电不稳或未连接 | 检查电源,VCC必须5V |
| 串口打印频繁"Out of range" | 被测物体表面不平或超出角度 | 使用大面积平整物体测试 |
| 距离值严重跳变 | ECHO引脚被干扰 | 可在PA6加一个小电容或使能捕获滤波 |
| 捕获中断不触发 | TIM3时钟未使能、NVIC未配置 | 检查 RCC_APB1PeriphClockCmd 和 NVIC_Init |
七、总结
本文提供了基于STM32标准外设库驱动HC-SR04的完整工程代码,采用定时器输入捕获方式,代码结构清晰,可直接移植使用。理解其原理后,你可轻松替换成其他定时器或通道,甚至同时驱动多个超声波模块。
核心要点再强调一次:
- t/58 公式,前提是定时器计数频率为1MHz。
- 输入捕获中断中切换上升/下降沿配置,避免使用轮询。
- 超时处理可防止程序卡死。
希望这篇文章能帮助还在使用标准库的开发者们快速上手超声波测距。如有问题,欢迎在评论区交流。