1. 项目背景与硬件准备
第一次接触超声波测距是在大学机器人社团,当时用Arduino驱动HC-SR04模块做避障小车,结果发现测量结果总是飘忽不定。后来转用STM32配合状态机编程,稳定性直接提升了一个量级。这次就用最经典的STM32F103C8T6(蓝色药丸开发板)带大家实现高精度测距。
必备硬件清单:
- STM32F103C8T6核心板(某宝20元左右)
- HC-SR04超声波模块(注意区分新旧版本)
- ST-Link V2下载器
- USB-TTL模块(推荐CH340G)
- 杜邦线若干(建议用20cm长度的)
特别提醒:HC-SR04有GPIO/UART/IIC三种模式,老版本只支持GPIO模式。我实测发现某宝有些商家混着发货,购买时一定要确认版本。模块背面印有RCWL-9206芯片的是新版,老版则是裸电路板加几个分立元件。
2. HC-SR04工作原理深度解析
这个模块的工作原理其实特别有意思——就像蝙蝠回声定位。当Trig引脚收到10μs以上的高电平脉冲后,模块会发射8个40kHz的超声波脉冲(这个频率人耳听不到),同时Echo引脚会拉高。当超声波遇到障碍物返回被接收器检测到,Echo引脚就会拉低。
关键的时间差测量:Echo高电平持续时间就是从发射到接收的往返时间T。声速在25℃空气中约346m/s(温度补偿后面会讲),所以距离 = (T * 346) / 2。实测发现模块在2cm-450cm范围内精度可达±3mm,但超过3米后反射信号会明显衰减。
硬件连接注意点:
- VCC接5V(虽然STM32是3.3V电平,但模块内部有电平转换)
- Trig和Echo引脚建议接10KΩ上拉电阻
- 避免将模块安装在金属表面,超声波会被反射干扰
3. CubeMX工程配置详解
打开CubeMX选择STM32F103C8系列,具体配置步骤如下:
时钟树配置:
- 外部晶振选择8MHz
- 系统时钟设为72MHz(APB1总线36MHz)
- 保证TIM2的时钟是72MHz(用于输入捕获)
GPIO设置:
- Trig引脚配置为GPIO_Output(推挽输出,无上下拉)
- Echo引脚配置为GPIO_Input(上拉输入模式)
定时器关键配置:
// TIM2输入捕获参数 Prescaler = 71 // 72MHz/(71+1)=1MHz计数频率 Counter Mode = Up Period = 0xFFFF // 最大计数值 AutoReloadPreload = Disable // 通道1配置 ICPolarity = Rising Edge ICSelection = Direct TI ICPrescaler = No divided ICFilter = 0串口配置:
- USART1异步模式
- 波特率115200
- 8位数据位,无校验位
生成代码前记得勾选"Generate peripheral initialization as a pair of .c/.h files"选项,这样外设代码会更清晰。
4. 状态机编程实现测距流程
传统轮询方式在超声波测距中会有严重缺陷——阻塞式等待Echo信号会导致系统卡死。这里采用状态机将测距过程分解为四个阶段:
状态转移图:
IDLE → TRIGGER → WAIT_ECHO → CAPTURE → CALCULATE具体代码实现(精简版):
typedef enum { STATE_IDLE, STATE_TRIGGER, STATE_WAIT_RISING, STATE_WAIT_FALLING, STATE_CALCULATE } SR04_State; void SR04_Handler(void) { static SR04_State state = STATE_IDLE; static uint32_t rise_time, fall_time; switch(state) { case STATE_IDLE: if(measure_flag) { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); state = STATE_TRIGGER; trigger_timer = HAL_GetTick(); } break; case STATE_TRIGGER: if(HAL_GetTick() - trigger_timer >= 10) { // 维持10us高电平 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); state = STATE_WAIT_RISING; } break; // 其他状态处理... } }实测发现加入超时判断特别重要。我在回调函数中添加了超时检测:
if(HAL_GetTick() - echo_start_time > 38) { // 38ms超时 HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_1); state = STATE_IDLE; error_count++; }5. 高精度时间测量技巧
定时器输入捕获是精度关键,这里有几个优化点:
时钟分频优化:
- 1MHz计数频率(每计数=1μs)
- 最大可测距离 = 65535μs * 346m/s / 2 ≈ 11.3米
捕获中断处理:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint32_t capture_buf[2]; if(htim->Instance == TIM2) { switch(capture_state) { case 0: // 上升沿捕获 capture_buf[0] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); capture_state = 1; break; case 1: // 下降沿捕获 capture_buf[1] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1); high_time_us = capture_buf[1] - capture_buf[0]; capture_state = 0; measure_ready = 1; break; } } }- 温度补偿算法:
float get_compensated_distance(uint32_t time_us, float temp_C) { float speed = 331.4f + (0.606f * temp_C); // 声速公式 return (time_us * 1e-6f * speed) / 2.0f; }6. 常见问题与调试技巧
问题1:测量结果跳动大
- 检查电源稳定性(示波器看5V纹波)
- 添加10μF电解电容并联在模块VCC-GND
- 软件端采用滑动平均滤波:
#define FILTER_SIZE 5 float distance_buf[FILTER_SIZE]; float filtered_distance = 0; void update_distance(float new_val) { static uint8_t index = 0; distance_buf[index++] = new_val; if(index >= FILTER_SIZE) index = 0; float sum = 0; for(int i=0; i<FILTER_SIZE; i++) { sum += distance_buf[i]; } filtered_distance = sum / FILTER_SIZE; }问题2:超过3米后数据不准
- 调整模块安装角度(建议15°倾斜)
- 在接收端添加聚音罩(可用3D打印)
- 提高发射功率(需修改模块硬件)
问题3:偶尔出现70米等异常值
- 添加超时判断(前文已提及)
- 增加信号校验(检查时间差是否合理)
- 硬件上在Echo脚加100pF电容滤波
7. 进阶优化方向
- 多模块协同:
// 使用一个定时器驱动多个模块 void multi_SR04_measure(void) { static uint8_t current_module = 0; switch(current_module) { case 0: trigger_module1(); break; case 1: trigger_module2(); break; } current_module = (current_module + 1) % MODULE_COUNT; }- DMA传输优化:
- 配置定时器触发DMA传输捕获数据
- 减少CPU中断处理开销
- RTOS集成:
- 创建专用测距线程
- 使用消息队列传递测量结果
实际项目中,我将这个方案用在了智能仓储机器人上,配合卡尔曼滤波算法,实现了±1mm的测量精度。最远在5米距离仍能稳定检测货架位置,整套代码已经稳定运行超过2000小时。