一、问题现象与背景

在使用STM32定时器输入捕获模式测量PWM频率时,许多开发者会遇到频率值偶尔跳变、测量结果不稳定甚至完全错误的情况。典型现象包括:

  • 低频信号测量时频率值突然翻倍或减半
  • 高频信号测量时数据随机跳动
  • 系统运行一段时间后测量完全失效

这些问题往往源于溢出中断与捕获中断的优先级冲突。本文将以STM32F103系列为例,深入分析冲突机理并提供完整解决方案。

二、原理讲解:输入捕获与溢出中断的协作机制

2.1 输入捕获测量频率的基本原理

输入捕获通过记录两次捕获事件之间的定时器计数值差来计算频率:

频率 = 定时器时钟 / (捕获差值 + 溢出次数 × 溢出周期)

当PWM信号周期较长时,定时器计数值会在两次捕获之间发生溢出,因此需要溢出中断来累计溢出次数。

2.2 中断优先级冲突的根源

STM32的NVIC支持中断嵌套,但若配置不当会导致:

  • 捕获中断被溢出中断打断:在读取捕获值时发生溢出,导致计数值错乱
  • 溢出中断丢失:捕获中断执行时间过长,错过溢出事件
  • 共享变量竞争:两个中断同时修改溢出计数变量

关键矛盾在于:捕获中断需要快速响应以记录精确时间戳,而溢出中断需要可靠执行以确保计数准确。

三、配置步骤:优先级与中断服务程序设计

3.1 定时器基础配置

以TIM2通道1为例,配置为输入捕获模式,测量PWM频率:

void TIM2_Capture_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // PA0作为TIM2_CH1输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 时基配置:72MHz/72=1MHz计数频率,周期65535
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 输入捕获配置:上升沿捕获,不分频
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x0;
    TIM_ICInit(TIM2, &TIM_ICInitStructure);

    // 使能捕获中断和溢出中断
    TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE);

    // 关键:捕获中断优先级高于溢出中断
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 捕获中断
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_Cmd(TIM2, ENABLE);
}

3.2 中断服务程序优化设计

错误做法:在捕获中断中直接计算频率,导致执行时间过长。

正确做法:捕获中断只记录时间戳,主循环计算频率。

volatile uint32_t capture_value = 0;
volatile uint32_t overflow_count = 0;
volatile uint8_t capture_flag = 0;
volatile uint32_t total_ticks = 0;

void TIM2_IRQHandler(void)
{
    // 先处理捕获中断(高优先级)
    if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
        
        uint32_t current_capture = TIM_GetCapture1(TIM2);
        
        // 原子操作:读取溢出计数并计算总 ticks
        __disable_irq();  // 短暂关中断,确保数据一致性
        total_ticks = overflow_count * 0x10000 + current_capture;
        __enable_irq();
        
        capture_value = current_capture;
        capture_flag = 1;
    }
    
    // 后处理溢出中断(低优先级)
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        overflow_count++;
    }
}

3.3 主循环频率计算

float measure_frequency(void)
{
    static uint32_t last_total_ticks = 0;
    float freq = 0;
    
    if (capture_flag)
    {
        capture_flag = 0;
        
        uint32_t current_total = total_ticks;
        uint32_t diff = current_total - last_total_ticks;
        last_total_ticks = current_total;
        
        if (diff > 0)
        {
            // 定时器计数频率1MHz,diff即为周期(us)
            freq = 1000000.0f / diff;
        }
    }
    return freq;
}

四、完整代码示例与验证

4.1 完整工程结构

// main.c
#include "stm32f10x.h"
#include "stdio.h"

volatile uint32_t capture_value = 0;
volatile uint32_t overflow_count = 0;
volatile uint8_t capture_flag = 0;
volatile uint32_t total_ticks = 0;

// 初始化代码同上...

int main(void)
{
    SystemInit();
    USART1_Init(115200);
    TIM2_Capture_Init();
    
    while(1)
    {
        float freq = measure_frequency();
        if (freq > 0)
        {
            printf("Frequency: %.2f Hz\r\n", freq);
        }
        Delay_ms(500);
    }
}

4.2 验证方法

使用信号发生器输入1kHz方波,观察串口输出:

  • 正常情况:频率稳定在1000Hz±1Hz
  • 冲突未解决:频率在500Hz~2000Hz之间跳变

五、注意事项与最佳实践

  • 优先级配置原则:捕获中断抢占优先级必须高于溢出中断,建议捕获设为1,溢出设为2
  • 临界区保护:读取多字节共享变量时使用__disable_irq()/__enable_irq()或原子操作
  • 中断服务程序精简:捕获中断中只做数据记录,复杂计算移至主循环
  • 溢出计数清零时机:每次成功捕获后根据应用需求决定是否清零溢出计数
  • 定时器时钟配置:确保预分频值使计数频率适中,避免高频信号溢出频繁
  • 调试技巧:使用GPIO翻转配合示波器观察中断响应时间,验证优先级配置
  • 备选方案:对于极低频信号,可考虑使用定时器从模式(复位模式)自动清零计数器

通过以上配置与优化,可彻底解决输入捕获测量PWM频率时的中断优先级冲突问题,实现稳定可靠的频率测量。