STM32超声波测距实战:HC-SR04模块避坑指南(附完整代码)
在智能小车和避障机器人开发中,超声波测距模块因其成本低廉、使用简单而广受欢迎。HC-SR04作为最常见的超声波模块之一,虽然原理简单,但在实际STM32项目应用中却存在不少"坑"。本文将从一个实际项目案例出发,分享我在使用STM32驱动HC-SR04模块时积累的经验和解决方案。
1. HC-SR04模块工作原理与硬件连接
HC-SR04超声波测距模块通过发射40kHz的超声波并接收回波来测量距离。其工作流程可分为三个关键阶段:
- 触发阶段:向Trig引脚发送至少10μs的高电平脉冲
- 发射接收阶段:模块自动发射8个40kHz超声波脉冲并等待回波
- 回波检测阶段:Echo引脚输出高电平,其持续时间与距离成正比
典型硬件连接方式:
| STM32引脚 | HC-SR04引脚 | 说明 |
|---|---|---|
| 5V | VCC | 电源正极 |
| GND | GND | 电源地 |
| GPIO输出 | TRIG | 触发信号输入 |
| GPIO输入 | ECHO | 回波信号输出 |
注意:虽然HC-SR04标称工作电压为5V,但ECHO引脚输出信号为5V TTL电平,直接连接3.3V STM32可能损坏IO口。建议使用电平转换电路或电阻分压。
常见硬件问题排查:
- 测量不准确:检查VCC供电是否稳定(建议使用示波器观察)
- 无响应:确认Trig信号脉冲宽度≥10μs
- 数据跳动:确保GND连接良好,必要时增加滤波电容
2. 三种驱动方案对比与选择
根据项目需求和资源占用情况,STM32驱动HC-SR04主要有三种实现方式:
2.1 轮询方式
// 简化示例代码 void GetDistance_Polling(void) { // 发送触发信号 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); // 开始计时 uint32_t start = TIM5->CNT; while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET); uint32_t end = TIM5->CNT; // 计算距离 float distance = (end - start) * 0.034 / 2; // 单位:cm }适用场景:简单测试、对实时性要求不高的应用
2.2 外部中断+定时器方式
// 中断处理示例 void EXTI15_10_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(ECHO_Pin) != RESET) { if(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin)) { // 上升沿:开始计时 __HAL_TIM_SET_COUNTER(&htim3, 0); HAL_TIM_Base_Start(&htim3); } else { // 下降沿:停止计时并计算距离 HAL_TIM_Base_Stop(&htim3); uint32_t pulse = __HAL_TIM_GET_COUNTER(&htim3); g_distance = pulse * 0.034 / 2; } __HAL_GPIO_EXTI_CLEAR_IT(ECHO_Pin); } }优势:响应及时,CPU占用率低
2.3 输入捕获方式(推荐)
// 定时器输入捕获配置 void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 1MHz计数频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFFFFFF; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(&htim2); TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; sConfigIC.ICFilter = 0; HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1); } // 获取距离 float GetDistance_InputCapture(void) { // 发送触发信号 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_TIM_GET_FLAG(&htim2, TIM_FLAG_CC1) == RESET); uint32_t rise = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1); // 配置为下降沿捕获 TIM2->CCER ^= TIM_CCER_CC1P; // 等待下降沿 while(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_CC1) == RESET); uint32_t fall = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1); // 恢复上升沿捕获 TIM2->CCER ^= TIM_CCER_CC1P; // 计算距离 return (fall - rise) * 0.034 / 2; }推荐理由:精度高,硬件自动记录时间,适合精确测量
3. 精度优化与误差处理
在实际项目中,HC-SR04的测量结果常受以下因素影响:
3.1 温度补偿
声速随温度变化,修正公式:
声速 = 331.4 + 0.6×温度(℃) m/s实现代码:
float GetDistanceWithTempComp(float temperature) { float speed = 331.4 + 0.6 * temperature; // m/s return (g_pulse_width * speed) / 20000.0; // 单位:cm }3.2 数字滤波算法
常用滤波方法对比:
| 滤波算法 | 适用场景 | 实现复杂度 | 效果 |
|---|---|---|---|
| 滑动平均 | 平稳环境 | 低 | 一般 |
| 中值滤波 | 突发干扰 | 中 | 较好 |
| 卡尔曼滤波 | 动态环境 | 高 | 优秀 |
滑动平均实现示例:
#define FILTER_SIZE 5 float distance_filter[FILTER_SIZE]; uint8_t filter_index = 0; float MovingAverageFilter(float new_distance) { distance_filter[filter_index] = new_distance; filter_index = (filter_index + 1) % FILTER_SIZE; float sum = 0; for(int i = 0; i < FILTER_SIZE; i++) { sum += distance_filter[i]; } return sum / FILTER_SIZE; }3.3 异常值处理
常见问题及解决方案:
- 超量程读数:增加超时检测,超过400cm返回无效值
- 多次反射干扰:设置最小有效距离(通常2cm)
- 信号丢失:增加重试机制,连续多次失败后报错
4. 实际项目集成技巧
在智能小车等实际应用中,还需考虑以下问题:
4.1 多模块协同工作
当使用多个HC-SR04模块时:
- 分时触发不同模块,避免超声波互相干扰
- 为每个模块分配独立的定时器资源
- 增加模块间触发间隔(建议≥50ms)
4.2 实时显示与通信
典型数据输出格式:
// 通过串口输出JSON格式数据 void SendDistanceData(float distance) { printf("{\"sensor\":\"ultrasonic\",\"distance\":%.2f,\"unit\":\"cm\"}\r\n", distance); }4.3 低功耗优化
对于电池供电设备:
- 仅在需要测量时给模块供电
- 降低采样频率(根据应用需求调整)
- 使用DMA传输减少CPU唤醒时间
5. 完整示例代码(基于HAL库)
以下是一个经过实际项目验证的完整驱动实现:
hc_sr04.h
#ifndef __HC_SR04_H #define __HC_SR04_H #include "stm32f1xx_hal.h" #define TRIG_PIN GPIO_PIN_0 #define TRIG_PORT GPIOA #define ECHO_PIN GPIO_PIN_1 #define ECHO_PORT GPIOA void HC_SR04_Init(void); float HC_SR04_GetDistance(void); void HC_SR04_Delay_us(uint16_t us); #endifhc_sr04.c
#include "hc_sr04.h" #include "tim.h" volatile uint32_t pulse_start = 0; volatile uint32_t pulse_end = 0; volatile uint8_t measurement_done = 0; void HC_SR04_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_PORT, &GPIO_InitStruct); // ECHO引脚配置 GPIO_InitStruct.Pin = ECHO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING; GPIO_InitStruct.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(ECHO_PORT, &GPIO_InitStruct); // 配置外部中断 HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI1_IRQn); // 初始化定时器 HAL_TIM_Base_Start(&htim2); } float HC_SR04_GetDistance(void) { // 发送触发脉冲 HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); HC_SR04_Delay_us(20); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); // 等待测量完成 uint32_t timeout = 100000; // 超时计数 while(!measurement_done && timeout--); if(!measurement_done) { return -1.0f; // 超时返回错误 } measurement_done = 0; uint32_t pulse_width = pulse_end - pulse_start; // 计算距离(cm) return pulse_width * 0.034 / 2; } void EXTI1_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(ECHO_PIN) != RESET) { if(HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN)) { // 上升沿 pulse_start = __HAL_TIM_GET_COUNTER(&htim2); } else { // 下降沿 pulse_end = __HAL_TIM_GET_COUNTER(&htim2); measurement_done = 1; } __HAL_GPIO_EXTI_CLEAR_IT(ECHO_PIN); } } // 简易微秒延时 void HC_SR04_Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim3, 0); while(__HAL_TIM_GET_COUNTER(&htim3) < us); }在调试过程中发现,模块的安装位置对测量结果影响很大。最好将传感器安装在远离电机等干扰源的位置,并确保检测方向无障碍物遮挡。