资讯动态

别再傻傻用while等回波了!STM32驱动HC-SR04,用外部中断+定时器才稳(附标准库/HAL库代码)

发布时间:2026/8/14 11:24:28 来源:尧图企业网站定制
STM32驱动HC-SR04超声波模块中断触发与定时器捕获的工程实践在嵌入式开发中超声波测距模块HC-SR04因其成本低廉、使用简单而广受欢迎。但许多开发者在使用过程中常会遇到一个棘手问题——当ECHO引脚因接线松动或环境干扰无法正常返回信号时采用while循环轮询的代码会陷入死循环导致整个系统卡死。本文将分享一种更可靠的解决方案通过STM32的外部中断和定时器实现稳定测距。1. 传统轮询方案的缺陷与改进思路大多数入门教程中HC-SR04的驱动代码通常是这样实现的// 发送触发信号 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(20); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 等待回波信号 while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) GPIO_PIN_RESET); start_time get_current_time(); while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) GPIO_PIN_SET); end_time get_current_time(); // 计算距离 distance (end_time - start_time) * 0.034 / 2;这种实现存在三个明显问题系统卡死风险当ECHO引脚接触不良或超声波模块未正常工作时程序会永久阻塞在while循环CPU资源浪费轮询方式持续占用CPU资源无法执行其他任务测量精度受限轮询方式的时间分辨率受限于循环执行时间更优的方案是利用STM32的外部中断检测ECHO引脚边沿变化配合定时器精确测量高电平持续时间。这种中断驱动的方式具有以下优势系统不会因信号异常而卡死CPU仅在信号变化时被唤醒节省资源定时器硬件计时可达微秒级精度2. 硬件设计与初始化配置2.1 硬件连接HC-SR04模块与STM32的典型连接方式如下HC-SR04引脚STM32连接说明VCC5V建议使用LDO稳压供电GNDGND共地连接TRIGPA0任意GPIO输出ECHOPA1需支持外部中断的GPIO注意ECHO引脚输出的是5V电平而STM32的GPIO通常耐受3.3V。建议使用电阻分压或电平转换电路保护IO口。2.2 初始化代码实现标准库版本void Ultrasonic_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; EXTI_InitTypeDef EXTI_InitStruct {0}; NVIC_InitTypeDef NVIC_InitStruct {0}; // 使能GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 配置TRIG引脚为推挽输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // 配置ECHO引脚为浮空输入 GPIO_InitStruct.GPIO_Pin GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStruct); // 配置外部中断 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1); EXTI_InitStruct.EXTI_Line EXTI_Line1; EXTI_InitStruct.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger EXTI_Trigger_Rising_Falling; EXTI_InitStruct.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStruct); // 配置中断优先级 NVIC_InitStruct.NVIC_IRQChannel EXTI1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 0x0F; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0x0F; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); // 初始化定时器 TIM4_Init(); }HAL库版本void Ultrasonic_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 配置TRIG引脚 GPIO_InitStruct.Pin TRIG_Pin; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(TRIG_GPIO_Port, GPIO_InitStruct); // 配置ECHO引脚 GPIO_InitStruct.Pin ECHO_Pin; GPIO_InitStruct.Mode GPIO_MODE_IT_RISING_FALLING; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(ECHO_GPIO_Port, GPIO_InitStruct); // 配置中断优先级 HAL_NVIC_SetPriority(EXTIx_IRQn, 15, 15); HAL_NVIC_EnableIRQ(EXTIx_IRQn); // 初始化定时器 MX_TIM1_Init(); }3. 中断服务与定时器实现3.1 外部中断处理逻辑ECHO引脚的中断服务程序需要处理上升沿和下降沿两种事件上升沿表示超声波开始返回启动定时器下降沿表示超声波返回结束停止定时器并计算距离标准库中断处理void EXTI1_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line1) ! RESET) { uint8_t pin_state GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); if(pin_state Bit_SET) { // 上升沿 - 开始计时 TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); } else { // 下降沿 - 停止计时 TIM_Cmd(TIM4, DISABLE); distance (TIM_GetCounter(TIM4) * 0.034) / 2; // 单位: cm } EXTI_ClearITPendingBit(EXTI_Line1); } }HAL库中断处理void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin ECHO_Pin) { GPIO_PinState state HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin); if(state GPIO_PIN_SET) { // 上升沿 - 开始计时 __HAL_TIM_SET_COUNTER(htim1, 0); HAL_TIM_Base_Start_IT(htim1); } else { // 下降沿 - 停止计时 HAL_TIM_Base_Stop_IT(htim1); distance (__HAL_TIM_GET_COUNTER(htim1) * 0.034) / 2; } } }3.2 定时器配置定时器用于精确测量ECHO高电平持续时间。建议配置如下参数时钟源内部时钟预分频72-1 (72MHz/72 1MHz)周期65535 (最大计数值)计数模式向上计数标准库定时器初始化void TIM4_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct {0}; NVIC_InitTypeDef NVIC_InitStruct {0}; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); TIM_TimeBaseStruct.TIM_Prescaler 72 - 1; TIM_TimeBaseStruct.TIM_Period 0xFFFF; TIM_TimeBaseStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStruct); NVIC_InitStruct.NVIC_IRQChannel TIM4_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); TIM_Cmd(TIM4, DISABLE); }HAL库定时器初始化使用STM32CubeMX配置定时器时建议设置Prescaler: 72-1Counter Mode: UpPeriod: 65535Auto-reload preload: Enable4. 实际应用中的优化技巧4.1 软件滤波处理超声波测距容易受到环境干扰可采用以下滤波方法提高稳定性多次测量取平均连续测量3-5次去除最大值和最小值后取平均移动平均滤波维护一个测量值队列计算滑动窗口平均值异常值剔除当测量值超出合理范围时丢弃#define SAMPLE_SIZE 5 float ultrasonic_get_distance(void) { static float samples[SAMPLE_SIZE]; static uint8_t index 0; float sum 0; // 获取新样本 trigger_measurement(); samples[index] distance; index (index 1) % SAMPLE_SIZE; // 计算平均值 for(uint8_t i 0; i SAMPLE_SIZE; i) { sum samples[i]; } return sum / SAMPLE_SIZE; }4.2 超时处理机制为防止因信号丢失导致系统挂起应实现超时检测#define TIMEOUT_MS 100 void EXTI1_IRQHandler(void) { static uint32_t last_rise_time 0; if(EXTI_GetITStatus(EXTI_Line1) ! RESET) { uint8_t pin_state GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); if(pin_state Bit_SET) { // 上升沿 last_rise_time HAL_GetTick(); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); } else { // 下降沿 TIM_Cmd(TIM4, DISABLE); uint32_t pulse_width TIM_GetCounter(TIM4); // 检查是否超时 if(HAL_GetTick() - last_rise_time TIMEOUT_MS) { distance (pulse_width * 0.034) / 2; } else { distance -1; // 超时标志 } } EXTI_ClearITPendingBit(EXTI_Line1); } }4.3 低功耗优化对于电池供电设备可进一步优化功耗间歇工作模式非连续测量按需触发外设动态管理仅在测量时使能定时器和中断唤醒优化利用中断从低功耗模式唤醒void enter_low_power_mode(void) { // 禁用超声波相关外设 TIM_Cmd(TIM4, DISABLE); EXTI-IMR ~EXTI_Line1; // 进入停止模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新初始化 SystemClock_Config(); Ultrasonic_Init(); }5. 常见问题与调试技巧5.1 测量值为零的可能原因供电不足用万用表检查模块VCC引脚电压应≥4.5V接线错误确认TRIG和ECHO引脚连接正确中断未触发检查GPIO和EXTI配置确认中断服务程序被调用定时器未工作验证定时器时钟使能和计数器变化5.2 提高测量精度的建议温度补偿声速随温度变化可增加温度传感器校正float get_speed_of_sound(float temperature) { return 331.4 0.6 * temperature; // m/s }定时器优化使用更高精度的定时器或输入捕获功能硬件布局缩短信号线长度避免电磁干扰5.3 多模块协同工作当系统需要多个HC-SR04模块时可采用以下方案分时复用为每个模块分配不同的工作时间片硬件扩展使用多路复用器切换信号资源分配为每个模块配置独立的定时器和GPIOvoid measure_multiple_sensors(void) { // 传感器1 TRIG1_HIGH; delay_us(20); TRIG1_LOW; delay_ms(60); // 传感器2 TRIG2_HIGH; delay_us(20); TRIG2_LOW; delay_ms(60); // 处理中断数据... }在实际项目中这种基于中断和定时器的方案显著提高了系统稳定性。我曾在一个自动导航小车项目中使用这种方法即使其中一个超声波传感器意外脱落整个系统仍能继续工作其他传感器数据不受影响。

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

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

免费获取报价