一、问题背景:溢出累积误差从何而来

在STM32中,用输入捕获(Input Capture)测频率的常规方法是:

  • 捕获两个上升沿,记录CCR1值,计算差值得到周期。
  • 若信号周期大于定时器溢出周期(如72MHz下16位定时器最大周期约0.91ms),则需在溢出中断中累加溢出次数,最终周期 = 溢出次数 × 溢出周期 + 两次捕获差值。

这种方法存在一个致命缺陷:溢出中断响应延迟。当捕获边沿与溢出中断几乎同时发生时,软件可能先处理捕获再处理溢出,或反之,导致溢出计数多算或少算1,引入一个完整溢出周期的误差。对于低频信号(如1Hz),误差可达100%。

二、解决思路:从模式复位计数器

STM32定时器支持从模式(Slave Mode),其中复位模式(Reset Mode) 允许外部触发信号(如TI1FP1)自动将计数器CNT清零。

核心思想:将输入捕获通道配置为触发源,并开启从模式复位。这样,每个捕获边沿到来时,硬件自动将CNT清零,无需软件干预。两次捕获之间的计数值直接就是周期对应的时钟数,完全不需要溢出中断,从根本上消除了累积误差。

原理要点:

  • 捕获边沿同时触发CNT复位,因此CCR1中保存的是“从上次复位到本次捕获”的计数值。
  • 若信号周期小于定时器溢出周期,则一次捕获即可得到完整周期;若大于,则需结合溢出次数,但此时溢出次数是精确的(因为复位由硬件同步完成)。
  • 更常用的做法是:测量两个相邻捕获值之差,但复位模式下每次捕获后CNT归零,因此直接读取CCR1即为周期(单位:时钟数)。

三、配置步骤(以STM32F103 TIM2 CH1为例)

3.1 硬件连接

  • 信号输入至TIM2_CH1(PA0)。

3.2 CubeMX配置

  1. 选择TIM2,Clock Source设为Internal Clock。
  2. Channel1设为Input Capture direct mode。
  3. 参数设置:
    • Prescaler = 71(72MHz/72=1MHz,即1us计数)。
    • Counter Period = 65535(16位最大值)。
    • Input Capture Channel1: Rising Edge,无滤波(或根据信号质量设置)。
  4. 触发源(Trigger Source)选择TI1FP1。
  5. 从模式(Slave Mode)选择Reset Mode。
  6. 使能TIM2中断(用于捕获中断,非溢出中断)。

3.3 关键寄存器配置(手动代码)

若使用标准库或寄存器操作,核心配置如下:

// 1. 时基配置
TIM2->PSC = 71;          // 1MHz计数
TIM2->ARR = 0xFFFF;      // 最大重载值

// 2. 输入捕获配置(CH1,上升沿)
TIM2->CCMR1 |= (1 << 0); // CC1S=01,映射到TI1
TIM2->CCMR1 |= (0 << 4); // 无滤波
TIM2->CCER |= (1 << 0);  // 使能捕获
TIM2->CCER &= ~(1 << 1); // 上升沿捕获

// 3. 从模式复位配置
TIM2->SMCR |= (1 << 2);  // TS=101,触发源TI1FP1
TIM2->SMCR |= (7 << 0);  // SMS=111,复位模式

// 4. 使能捕获中断
TIM2->DIER |= (1 << 1);  // CC1IE
NVIC_EnableIRQ(TIM2_IRQn);

// 5. 启动定时器
TIM2->CR1 |= (1 << 0);

四、完整代码示例(HAL库)

#include "stm32f1xx_hal.h"

TIM_HandleTypeDef htim2;
volatile uint32_t capture_value = 0;
volatile uint8_t  capture_flag = 0;

void TIM2_Init(void) {
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig = {0};
    TIM_IC_InitTypeDef sConfigIC = {0};
    TIM_SlaveConfigTypeDef sSlaveConfig = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFF;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    HAL_TIM_Base_Init(&htim2);

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);

    HAL_TIM_IC_Init(&htim2);

    // 输入捕获配置
    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 0;
    HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);

    // 从模式复位配置
    sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
    sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
    HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig);

    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}

// 捕获中断回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        capture_value = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        capture_flag = 1;
    }
}

// 主循环中计算频率
float get_frequency(void) {
    if (capture_flag) {
        capture_flag = 0;
        // 计数时钟为1MHz,capture_value即为周期(us)
        if (capture_value > 0) {
            return 1000000.0f / capture_value; // Hz
        }
    }
    return 0.0f;
}

说明:由于从模式复位,每次捕获后CNT自动清零,因此capture_value直接就是信号周期对应的计数值(单位:1us)。无需任何溢出处理。

五、注意事项

  • 信号频率范围:该方法要求信号周期小于定时器溢出周期(本例为65535us,约15.26Hz以上)。若信号频率更低,需降低预分频或使用32位定时器(如TIM5)。
  • 捕获边沿选择:确保触发源与捕获通道极性一致(如TI1FP1对应上升沿)。
  • 中断优先级:捕获中断应设置合理优先级,避免被其他高优先级中断长时间阻塞,导致捕获值被覆盖(但复位模式下即使覆盖,下一次捕获仍准确)。
  • 输入滤波:若信号有噪声,适当配置ICFilter(如0x0F)可防止误触发复位。
  • 首次捕获:启动后第一次捕获值可能不准确(因CNT初值不确定),建议丢弃第一次结果。
  • 从模式复位与预分频:复位信号会清零CNT,但不会影响预分频器,因此计数时钟始终一致。

六、总结

利用STM32定时器的从模式复位功能,将输入捕获边沿作为硬件复位信号,可以彻底消除软件累加溢出次数带来的累积误差。该方法实现简单、精度高,特别适合中低频信号的频率测量。掌握这一技巧,能显著提升嵌入式测量系统的可靠性。