STM32输入捕获驱动HC-SR04:OLED实时显示测距精解
1. HC-SR04超声波模块工作原理
HC-SR04超声波测距模块是嵌入式项目中常用的低成本测距方案。这个模块只有四个引脚:VCC、GND、Trig和Echo。它的工作原理其实很简单,但需要精确的时序控制。
模块工作时,首先需要给Trig引脚一个至少10微秒的高电平脉冲。这个脉冲就像是发令枪,告诉模块:"现在开始发射超声波!"模块收到这个信号后,内部会发射8个40kHz的超声波脉冲,同时Echo引脚会拉高。当模块检测到超声波回波时,Echo引脚会拉低。这个高电平的持续时间就是从发射超声波到接收到回波的时间差。
计算距离的公式很简单:距离 = (高电平时间 × 声速) / 2。为什么要除以2?因为超声波是往返距离,我们只需要单程距离。声速在常温下大约是340m/s,但更精确的计算需要考虑环境温度的影响。实际项目中,我们常用一个简化公式:距离(cm) = 高电平时间(us) / 58。
我在实际项目中发现,HC-SR04有几个需要注意的地方:
- 测量周期最好大于60ms,避免上次测量的回波干扰下次测量
- 模块最小测量距离约2cm,最大测量距离约4-5米
- 测量角度约15度,被测物体最好正对模块
- 柔软或吸音材料可能无法有效反射超声波
2. STM32输入捕获模式详解
STM32的定时器输入捕获功能是测量脉冲宽度的利器。相比用外部中断+定时器的方式,输入捕获不仅精度更高,还能减轻CPU负担。我们以TIM10为例,详细讲解如何配置和使用这个功能。
首先需要配置TIM10的基本参数:
- 时钟源选择内部时钟
- 预分频器(PSC)设置为71,这样当主频为72MHz时,计数器时钟为1MHz(每个计数1us)
- 自动重装载值(ARR)设置为65535(16位定时器的最大值)
- 计数模式选择向上计数
输入捕获通道的配置更为关键:
- 选择输入捕获通道(TIM10只有通道1)
- 设置输入滤波器和分频器,一般不需要滤波
- 设置捕获极性:初始设置为上升沿捕获
- 开启捕获中断
输入捕获的工作原理是这样的:当指定边沿(上升沿或下降沿)到来时,定时器会立即将当前计数器值(CNT)锁存到捕获比较寄存器(CCR)中,并产生中断。我们可以通过交替设置上升沿和下降沿捕获,来测量高电平的持续时间。
在实际编程中,我遇到过几个常见问题:
- 忘记清除中断标志位导致重复进入中断
- 没有正确处理计数器溢出情况
- 中断优先级设置不当导致测量不准确
- 没有及时改变捕获极性导致错过边沿
3. 完整代码实现与解析
下面我们结合HAL库,详细讲解如何实现超声波测距的全流程。我会把代码分成几个关键部分,并解释每个细节。
首先是硬件初始化部分:
void MX_TIM10_Init(void) { htim10.Instance = TIM10; htim10.Init.Prescaler = 71; // 1MHz计数频率 htim10.Init.CounterMode = TIM_COUNTERMODE_UP; htim10.Init.Period = 65535; htim10.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_IC_Init(&htim10) != HAL_OK) { Error_Handler(); } TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; sConfigIC.ICFilter = 0; if (HAL_TIM_IC_ConfigChannel(&htim10, &sConfigIC, TIM_CHANNEL_1) != HAL_OK) { Error_Handler(); } }超声波启动函数需要产生10us以上的触发脉冲:
void Ultrasonic_Start(void) { HAL_TIM_Base_Start(&htim10); HAL_TIM_IC_Start_IT(&htim10, TIM_CHANNEL_1); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); delay_us(2); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COUNTER(&htim10, 0); }最重要的捕获中断回调函数:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint8_t captureState = 0; if(htim->Instance == TIM10) { if(captureState == 0) // 上升沿捕获 { capture[0] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); captureState = 1; } else // 下降沿捕获 { capture[1] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING); captureState = 0; distanceReady = 1; HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1); HAL_TIM_Base_Stop(htim); } } }4. OLED显示优化技巧
得到距离数据后,如何在OLED上清晰稳定地显示也很关键。我分享几个提升显示效果的实用技巧。
首先是显示刷新策略。不建议每次测量都刷新整个屏幕,这样会导致闪烁。更好的做法是:
- 只刷新数值变化的部分
- 添加适当的滤波处理,避免数值跳动
- 设置合理的刷新间隔(100-200ms为宜)
OLED显示函数可以这样优化:
void Display_Distance(float distance) { static float lastDistance = 0; char buffer[16]; // 数值变化超过0.5cm或1秒未更新时才刷新 if(fabs(distance - lastDistance) > 0.5 || HAL_GetTick() - lastDisplayTime > 1000) { sprintf(buffer, "%.2f cm", distance); OLED_ShowString(0, 4, "Distance: ", 16); OLED_ShowString(80, 4, buffer, 16); lastDistance = distance; lastDisplayTime = HAL_GetTick(); } }对于显示内容布局,我建议:
- 固定显示标题和单位
- 数值区域预留足够宽度
- 添加测量状态指示(如"测量中...")
- 可以显示历史最小/最大值
如果发现OLED显示有残影,可以:
- 适当降低I2C时钟频率
- 在两次刷新之间添加短暂延迟
- 使用OLED自带的清除函数而不是全屏填充
5. 常见问题与调试方法
在实际项目中,超声波测距可能会遇到各种问题。这里总结几个常见问题及其解决方法。
问题1:测量结果不稳定,数值跳动大解决方法:
- 添加软件滤波,如移动平均或中值滤波
- 增加测量间隔,避免声波反射干扰
- 检查电源是否稳定,必要时增加滤波电容
- 确保被测物体表面平整,避免漫反射
问题2:测量距离与实际距离有偏差解决方法:
- 校准声速参数,考虑温度补偿
- 检查定时器时钟配置是否正确
- 验证输入捕获是否准确,可以用示波器对比
- 注意模块的测量盲区(约2-3cm)
问题3:偶尔出现极大或极小异常值解决方法:
- 添加数据合理性检查,丢弃明显异常值
- 设置超时机制,超过预期时间未收到回波则放弃本次测量
- 检查硬件连接,确保Echo信号稳定
调试时可以分步骤验证:
- 先用示波器观察Trig和Echo信号
- 单独测试输入捕获功能,用已知脉冲验证
- 测试OLED显示,确保显示功能正常
- 最后整合全部功能
6. 性能优化与进阶应用
当基本功能实现后,我们可以进一步优化系统性能和扩展应用场景。
性能优化方面:
- 使用DMA传输OLED数据,减少CPU占用
- 采用中断+定时器的方式实现多模块测量
- 添加温度传感器补偿声速
- 实现低功耗模式,间歇性测量
进阶应用场景:
- 多超声波模块组网,实现区域监测
- 结合电机控制,实现自动跟踪
- 添加无线传输模块,远程监控距离数据
- 实现历史数据记录和分析功能
一个实用的优化技巧是动态调整测量频率:
void Adjust_Measure_Frequency(float distance) { static uint32_t measureInterval = 100; // 默认100ms if(distance < 50.0) // 近距离时加快测量 measureInterval = 50; else if(distance > 200.0) // 远距离时减慢测量 measureInterval = 200; osDelay(measureInterval); }对于需要更高精度的场合,可以考虑:
- 使用更高精度的定时器(如32位定时器)
- 提高定时器时钟频率
- 添加硬件滤波电路
- 使用时间数字转换器(TDC)等专用芯片
