STM32 定时器输入捕获测频:边沿同步与溢出处理的工程细节
STM32 定时器输入捕获测频:边沿同步与溢出处理的工程细节
用 STM32 测外部信号频率,最省资源的办法不是进中断数脉冲,而是定时器的输入捕获(Input Capture):把被测信号接到 TIMx_CHx,硬件在跳变边沿自动把 CNT 寄存器的值锁存到 CCRx 里,软件只需要在中断里做两次捕获值的差分。听起来简单,实际工程里有三个容易翻车的地方。
1. 捕获时钟与分辨率的取舍
输入捕获的分辨率由定时器计数时钟决定。假设 TIM2 挂在 APB1 上,APB1 定时器时钟 84MHz(STM32F4 双倍频后),预分频 PSC=83,计数时钟就是 1MHz,单次计数分辨率 1μs。测 1kHz 信号时相邻两次捕获差值约 1000 个计数,量化误差 ±1 个计数,即 ±0.1%。若信号频率低到 10Hz,差值 100000 个计数,分辨率反而更高——低频信号恰恰是输入捕获的强项。
但预分频不能无脑设小:PSC=0 时计数时钟 84MHz,16 位计数器 65536 个计数只撑得住 780μs,1kHz 信号两次捕获之间会溢出 1.28 次,差分直接错。所以第一条工程规矩:预估被测频率下限,按最长周期 × 1.5 选预分频,保证单周期内至多溢出 0 次。
// TIM2_CH1 输入捕获初始化(STM32F4,PA0)
void TIM2_Cap_Init(uint16_t psc)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
GPIO_InitTypeDef g;
g.GPIO_Pin = GPIO_Pin_0;
g.GPIO_Mode = GPIO_Mode_AF;
g.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init(GPIOA, &g);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2);
TIM_TimeBaseInitTypeDef t;
t.TIM_Prescaler = psc;
t.TIM_CounterMode = TIM_CounterMode_Up;
t.TIM_Period = 0xFFFF;
t.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &t);
TIM_ICInitTypeDef ic;
ic.TIM_Channel = TIM_Channel_1;
ic.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
ic.TIM_ICSelection = TIM_ICSelection_DirectTI;
ic.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic.TIM_ICFilter = 0x03; // 数字滤波,见第 2 节
TIM_ICInit(TIM2, &ic);
TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
2. 毛刺与数字滤波器 ICFilter
被测信号如果来自机械触点或长线传输,边沿附近常有几十纳秒的毛刺。一次毛刺就会多锁存一个 CCR 值,差分结果直接腰斩。STM32 的捕获通道自带数字滤波器 TIM_ICFilter(0x0~0xF),原理是对输入做 N 个采样时钟的连续一致性判决。注意滤波时钟是定时器内部时钟 f_DTS(由 CKD 分频决定),不是计数时钟。
经验值:CKD=TIM_CKD_DIV1 时 f_DTS 等于定时器时钟,ICFilter=0x3 表示 8 个 f_DTS 周期连续一致才认边沿——84MHz 下约 95ns 的毛刺被滤掉,对 1kHz~1MHz 的被测信号边沿延迟可忽略。高频测频(>1MHz)把滤波设 0x0,低频抗干扰设 0x3~0x6,不要一个值打天下。
3. 溢出处理:Update 中断不是摆设
两次捕获值做差分时必须考虑计数器溢出。标准做法是开 Update 中断维护一个 32 位扩展计数:
volatile uint32_t tim2_ovf = 0; // 溢出次数
volatile uint32_t cap_last = 0;
volatile uint8_t cap_ready = 0;
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
tim2_ovf++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) {
uint32_t now = (tim2_ovf << 16) | TIM_GetCapture1(TIM2);
// 注意:读 CCR 前先处理可能同时挂起的 Update 标志,
// 否则溢出恰好发生在捕获边沿时会差 65536
if ((TIM2->SR & TIM_FLAG_Update) && (TIM_GetCapture1(TIM2) < 0x8000))
now += 0x10000;
if (cap_last != 0) {
uint32_t diff = now - cap_last;
// freq = 计时时钟 / diff,此处计时时钟 1MHz
measured_freq = 1000000.0f / diff;
cap_ready = 1;
}
cap_last = now;
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
}
}
上面那个 if 是关键细节:溢出中断和捕获中断可能在同一个边沿同时挂起。如果先读了 CCR、后处理 Update 标志,扩展计数会多加 65536。判断方法是:若 Update 标志还挂着且本次捕获值落在计数器低半区(<0x8000),说明溢出就发生在本次边沿附近,手动补一次。反之捕获值在高半区则说明溢出是上一个周期结束时的正常溢出,中断里已经加过,不能重复加。
4. 高频信号的替代方案:捕获预分频
当被测频率接近计数时钟(比如用 1MHz 计数时钟测 800kHz),两次捕获差值只有 1~2 个计数,量化误差巨大。这时用 TIM_ICPrescaler = TIM_ICPSC_DIV8,让硬件每 8 个边沿才捕获一次,等效把差值放大 8 倍再除以 8,量化误差降为 1/8。这是硬件层面的平均,零 CPU 开销,比软件累加 N 个周期再平均更干净。
小结
输入捕获测频的完整链条是:预分频按最低频率选 → 滤波按噪声环境选 → 32 位扩展计数处理溢出 → 高频用捕获预分频降量化误差。四步都做到,1Hz~数 MHz 的信号一块 TIM2 就能稳定测量,CPU 只在每次捕获中断里做一次减法。这也是为什么产品级的转速表、频率计几乎都用输入捕获而不是 GPIO 中断计数——后者在高频下中断开销会把主循环拖死。
- 点赞
- 收藏
- 关注作者
评论(0)