1. 项目概述
最近在调试一个需要精确测量脉冲宽度的项目,发现STM32的输入捕获功能真是个神器。这个功能在工业控制、电机测速、超声波测距等场景中应用广泛,但很多初学者对它的理解还停留在"能测脉冲"的层面。今天我就结合自己踩过的坑,详细拆解STM32输入捕获的实现原理和实战技巧。
输入捕获的本质是利用定时器记录信号边沿发生的时刻。当检测到指定边沿(上升沿/下降沿)时,当前定时器计数值会被锁存到捕获寄存器,同时可能触发中断。通过比较两次捕获的计数值差,就能计算出脉冲宽度或频率。STM32的定时器外设通常支持多通道输入捕获,配合DMA还能实现高效的无CPU干预测量。
2. 硬件设计要点
2.1 定时器选型策略
STM32系列有基本定时器(TIM6/7)、通用定时器(TIM2-5)和高级定时器(TIM1/8)。输入捕获必须使用通用或高级定时器:
- 基础款推荐TIM2/TIM3:资源丰富,所有型号都有
- 需要4通道时选TIM5(部分型号)
- 高频测量用TIM1/TIM8:支持更高时钟频率
关键参数:定时器时钟频率直接影响测量精度。以72MHz系统时钟为例,若APB1分频系数≠1,TIM2-5时钟会倍频到72MHz。
2.2 信号输入电路设计
常见问题:信号抖动导致误触发。解决方案:
- 硬件滤波:RC低通滤波(如1kΩ+100nF组合)
- 软件消抖:配置输入捕获滤波器(TIMx_CCMRx寄存器ICxF位)
- 电平转换:5V信号需加电平转换电路(如分压电阻)
实测案例:测量10kHz方波时,未加滤波导致±2μs的抖动误差,加入100ns硬件滤波后误差<0.5μs。
3. 软件实现详解
3.1 初始化流程关键点
c复制// 定时器时钟使能(以TIM3为例)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 时基配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_TimeBaseStruct.TIM_Prescaler = 71; // 72MHz/(71+1)=1MHz
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStruct.TIM_Period = 0xFFFF; // 16位最大值
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);
// 输入捕获配置(通道1)
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 每个事件都捕获
TIM_ICInitStruct.TIM_ICFilter = 0x0F; // 最大滤波
TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 中断配置
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
NVIC_EnableIRQ(TIM3_IRQn);
// 启动定时器
TIM_Cmd(TIM3, ENABLE);
3.2 中断服务程序优化
c复制volatile uint32_t captureCount = 0;
volatile uint32_t pulseWidth = 0;
void TIM3_IRQHandler(void) {
if(TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) {
static uint32_t firstCapture = 0;
uint32_t currentCapture = TIM_GetCapture1(TIM3);
if(captureCount == 0) {
firstCapture = currentCapture;
// 切换为下降沿捕获
TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling);
} else if(captureCount == 1) {
pulseWidth = (currentCapture > firstCapture) ?
(currentCapture - firstCapture) :
(0xFFFF - firstCapture + currentCapture);
// 切换回上升沿捕获
TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising);
captureCount = -1; // 复位计数器
}
captureCount++;
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
}
}
避坑指南:计数器溢出处理是关键。当currentCapture < firstCapture时,说明发生了定时器溢出,需要特殊处理计数值差。
4. 精度提升技巧
4.1 时钟源优化方案
- 使用外部晶振:8MHz晶振经PLL倍频比HSI更稳定
- 开启定时器同步:多个定时器时,配置主从模式同步时钟
- 温度补偿:高精度应用需考虑温度漂移影响
实测数据对比:
| 时钟源 | 1秒累计误差 |
|---|---|
| HSI(8MHz) | ±50ppm |
| HSE(8MHz) | ±10ppm |
| HSE+温补 | ±2ppm |
4.2 动态调整预分频器
对于宽范围频率测量,可动态修改TIMx_PSC寄存器:
c复制void AdjustPrescaler(uint32_t expectedFreq) {
uint32_t newPsc = SystemCoreClock / (0xFFFF * expectedFreq) - 1;
TIM_PrescalerConfig(TIM3, newPsc, TIM_PSCReloadMode_Immediate);
}
5. 典型应用场景实现
5.1 超声波测距模块
HC-SR04模块的测量逻辑:
- 触发脚输出10μs高电平
- 模块发出8个40kHz脉冲
- 回波脚输出高电平,持续时间与距离成正比
关键代码段:
c复制// 测量回波脉冲宽度
float GetDistance(void) {
TriggerSensor(); // 发送触发信号
while(captureCount < 2); // 等待捕获完成
return pulseWidth * 0.017; // 340m/s声速换算
}
5.2 电机转速测量
光电编码器输出处理方案:
- 双通道输入捕获(TIMx_CH1/CH2)
- 配置为双边沿触发
- 计算单位时间内脉冲数
c复制void TIM_Config_EncoderMode(void) {
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
TIM_SetCounter(TIM3, 0);
TIM_Cmd(TIM3, ENABLE);
}
uint32_t GetRPM(void) {
uint32_t pulseCount = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);
return (pulseCount * 60) / PULSES_PER_REV; // 转换为转速
}
6. 常见问题排查
6.1 捕获值异常问题
现象:捕获到的脉冲宽度忽大忽小
排查步骤:
- 用示波器确认实际信号质量
- 检查GPIO配置是否正确(应设为浮空输入)
- 调整输入捕获滤波器参数
- 确认中断优先级是否被其他任务抢占
6.2 高频率测量丢数
解决方案:
- 降低定时器预分频值提高时间分辨率
- 使用DMA连续传输捕获值
- 改用定时器从模式+外部时钟输入
DMA配置示例:
c复制DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&TIM3->CCR1;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)captureBuffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = BUF_SIZE;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
DMA_Init(DMA1_Channel6, &DMA_InitStruct);
TIM_DMACmd(TIM3, TIM_DMA_CC1, ENABLE);
7. 进阶应用:PWM输入模式
STM32的定时器还支持特殊的PWM输入模式,可自动测量周期和占空比:
c复制void TIM_PWMInput_Config(void) {
TIM_ICInitTypeDef TIM_ICInitStruct;
// 通道1捕获上升沿(周期)
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICFilter = 0x0;
TIM_PWMIConfig(TIM3, &TIM_ICInitStruct);
// 通道2捕获下降沿(占空比)
TIM_SelectInputTrigger(TIM3, TIM_TS_TI2FP2);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable);
TIM_Cmd(TIM3, ENABLE);
}
测量结果获取:
- TIM_GetCapture1() 返回周期值
- TIM_GetCapture2() 返回高电平时间
最后分享一个调试小技巧:在GPIO初始化后,可以临时将捕获引脚配置为推挽输出,通过输出测试方波来验证定时器配置是否正确。这个方法帮我省去了很多硬件调试时间。
