1. HC-SR04超声波模块基础认知
HC-SR04作为嵌入式领域最常用的超声波测距模块,其工作原理简单却暗藏玄机。模块正面并排的两个金属圆柱体,一个是发射器(T),一个是接收器(R),工作时就像蝙蝠的声波系统。当Trig引脚接收到10μs以上的高电平脉冲后,模块会自动发射8个40kHz的超声波脉冲,同时Echo引脚会拉高电平,直到接收到回波后才会拉低。这个高电平持续时间就是超声波往返时间,通过公式距离=(高电平时间×声速)/2就能算出实际距离。
市面上常见的有三种工作模式:
- GPIO模式:最基础的触发-回响方式,需要手动控制Trig和测量Echo高电平时间
- UART模式:模块内部集成处理芯片,直接输出距离数据帧
- I2C模式:通过地址寻址读取距离寄存器
我在实际项目中发现,GPIO模式虽然接线简单(只需VCC、GND、Trig、Echo四线),但不同厂家的模块性能差异很大。曾遇到过某批次模块在3.3V下测距不稳定,后来改用支持宽电压(3.3V-5V)的改良版才解决问题。模块背面那些0603封装的电阻就是模式选择的关键,焊接不同的组合可以切换通信方式。
2. 阻塞式轮询方案剖析
2.1 硬件连接与CubeMX配置
使用STM32F103C8T6开发板时,典型接线如下:
- VCC → 3.3V(注意模块电压范围)
- GND → 共地
- Trig → PB10(任意GPIO)
- Echo → PA3(需连接定时器通道)
在CubeMX中的关键配置步骤:
- 时钟树配置HCLK为72MHz(最大化定时器精度)
- 配置Trig引脚为GPIO_Output
- 选择TIM2通道1(对应PA3)为输入捕获模式
- 定时器预分频设为71(72MHz/(71+1)=1MHz,即1μs计数)
- 自动重装载值设为65535(16位最大值)
// 生成的部分初始化代码 GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); htim2.Instance = TIM2; htim2.Init.Prescaler = 71; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 65535; HAL_TIM_IC_Init(&htim2);2.2 阻塞式代码实现
传统轮询法的核心代码如下,这种实现简单粗暴但问题明显:
void GetDistance(void) { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); // 实测10us可能不够稳定 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 = HAL_GetTick(); while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET); uint32_t duration = HAL_GetTick() - start; float distance = duration * 0.034 / 2; // 声速340m/s→0.034cm/us }我在智能小车项目初期就采用这种方案,很快发现了三大致命缺陷:
- CPU资源浪费:两个while循环会独占CPU,实测在4米最大测距时可能阻塞30ms
- 多任务冲突:在RTOS环境中会阻塞其他任务运行
- 精度受限:依赖HAL_GetTick()的1ms分辨率,近距离测量误差大
3. 中断捕获方案进阶
3.1 定时器输入捕获原理
输入捕获是定时器的杀手锏功能,其工作原理就像精准的电子秒表:
- 配置定时器通道为输入捕获模式
- 上升沿触发时,硬件自动记录当前计数器值(CCR1)
- 下降沿触发时,再次捕获计数器值(CCR2)
- 两次捕获值之差即为高电平时间
CubeMX中需要额外开启两项配置:
- NVIC中使能TIM2全局中断
- 定时器设置中勾选"Input Capture direct mode"
3.2 中断驱动代码框架
创建hcsr04.h头文件定义数据结构:
typedef struct { uint8_t edge_state; // 0-等待上升沿 1-等待下降沿 uint16_t overflow_cnt; uint32_t rise_time; uint32_t fall_time; float distance_cm; } HCSR04_TypeDef;核心中断处理逻辑:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { uint32_t cnt = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if(hcsr04.edge_state == 0) { // 上升沿 hcsr04.rise_time = cnt; hcsr04.overflow_cnt = 0; __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); hcsr04.edge_state = 1; } else { // 下降沿 hcsr04.fall_time = cnt; uint32_t total = (hcsr04.overflow_cnt << 16) + hcsr04.fall_time - hcsr04.rise_time; hcsr04.distance_cm = total * 0.034 / 2; __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); hcsr04.edge_state = 0; } } } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { hcsr04.overflow_cnt++; } }3.3 性能对比实测
在72MHz主频的STM32F103上实测数据:
| 指标 | 阻塞轮询法 | 中断捕获法 |
|---|---|---|
| CPU占用率 | 最高100% | <1% |
| 最大响应延迟 | 30ms | 微秒级 |
| 测距精度 | ±1cm | ±0.3cm |
| 多任务兼容性 | 极差 | 优秀 |
特别在RTOS环境中,中断方案的优势更加明显。我在FreeRTOS项目中测试,即使创建5个任务并行运行,测距模块依然能稳定工作,而阻塞方案会导致系统明显卡顿。
4. 工程优化实践
4.1 软件滤波处理
超声波易受环境干扰,需要添加滤波算法。推荐组合方案:
- 中值滤波:连续采样5次,取中间值
- 滑动平均:保留最近10次记录求平均
- 野值剔除:超过±15%突变视为无效
#define FILTER_SIZE 10 float distance_buf[FILTER_SIZE]; float Filter_Distance(float raw) { static uint8_t index = 0; distance_buf[index++] = raw; if(index >= FILTER_SIZE) index = 0; float sum = 0; for(uint8_t i=0; i<FILTER_SIZE; i++) { sum += distance_buf[i]; } return sum / FILTER_SIZE; }4.2 低功耗优化技巧
对于电池供电设备,可采取以下措施:
- 动态调整采样率:近距离时100ms采样,远距离时500ms采样
- 模块电源管理:通过MOS管控制VCC供电,测量前才上电
- 定时器自动关闭:连续5次无回波自动休眠
void Power_Save_Mode(void) { HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port, PWR_CTRL_Pin, GPIO_PIN_RESET); HAL_TIM_Base_Stop_IT(&htim2); } void Wake_Up(void) { HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port, PWR_CTRL_Pin, GPIO_PIN_SET); HAL_Delay(50); // 等待模块稳定 HAL_TIM_Base_Start_IT(&htim2); }4.3 多模块协同方案
当需要多个HC-SR04时(如360°避障),推荐两种方案:
- 分时复用:每个模块间隔20ms触发,共用同一个Echo引脚
- 独立定时器:为每个模块分配独立定时器(如TIM2+TIM3+TIM4)
我曾用方案1实现四路超声波雷达,关键代码如下:
void Multi_Measurement(void) { static uint8_t current_module = 0; switch(current_module) { case 0: HAL_GPIO_WritePin(TRIG1_GPIO_Port, TRIG1_Pin, GPIO_PIN_SET); delay_us(12); HAL_GPIO_WritePin(TRIG1_GPIO_Port, TRIG1_Pin, GPIO_PIN_RESET); break; // 其他模块同理... } current_module = (current_module + 1) % 4; }在调试多模块时发现一个重要细节:必须确保前一个模块的回波完全结束后再触发下一个,否则会出现信号干扰。实测建议间隔至少15ms以上。