问题现象与背景

使用STM32定时器输入捕获测量PWM占空比时,常见异常:占空比读数偶尔跳变到0%或100%,或出现固定偏移。根本原因往往是溢出中断与捕获中断的优先级配置不当,导致计数器溢出与捕获事件在时间上竞争,捕获值被错误覆盖或计算基准错乱。

  • 典型场景:TIM2_CH1捕获PWM,同时开启更新中断(溢出)用于扩展计数范围。
  • 冲突表现:捕获中断服务程序中读取的计数器值已因溢出中断更新而失效。

原理分析:为什么会产生冲突

输入捕获测量占空比通常需要捕获两个边沿(上升沿和下降沿),并记录两次捕获时的计数器值。若PWM周期较长,定时器可能溢出多次,因此需要开启更新中断累计溢出次数。

关键竞争点:

  • 当捕获事件与更新事件同时发生时,若捕获中断优先级低于更新中断,CPU会先响应更新中断,导致捕获寄存器(CCR)的值可能被后续捕获覆盖,或溢出计数已增加但捕获值未及时读取。
  • 若在捕获中断中读取计数器值,而更新中断恰好插入,则读取到的CNT可能属于溢出后的新周期,造成计算错误。

正确做法:

  • 捕获中断优先级应高于更新中断,确保捕获事件被优先处理。
  • 在捕获中断中,先读取CCR寄存器(硬件自动锁存),再读取溢出计数,避免数据不一致。
  • 使用捕获中断的“捕获/比较”标志与更新标志的清除顺序也需注意。

配置步骤

  1. 定时器基础配置:设置预分频器与自动重装载值,使定时器周期略大于PWM最大周期,减少溢出次数。
  2. 输入捕获通道配置:选择捕获边沿(先上升沿后下降沿),开启捕获中断。
  3. 更新中断配置:开启更新中断用于溢出计数。
  4. NVIC优先级设置:捕获中断抢占优先级设为0(最高),更新中断设为1或更低。
  5. 中断服务程序编写:捕获中断中读取CCR并计算,更新中断中仅递增溢出计数。

完整代码示例(基于STM32 HAL库)

// 全局变量
volatile uint32_t overflow_cnt = 0;
volatile uint32_t capture_val1 = 0, capture_val2 = 0;
volatile uint8_t capture_flag = 0;
volatile uint32_t duty_cycle = 0;

// 定时器初始化(以TIM2为例,72MHz)
void TIM2_Init(void) {
    TIM_HandleTypeDef htim2;
    TIM_IC_InitTypeDef sConfigIC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 1MHz计数频率
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFF;         // 最大计数值,减少溢出
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_IC_Init(&htim2);

    // 输入捕获配置:通道1,上升沿捕获
    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);

    // 开启捕获中断和更新中断
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
    __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);

    // NVIC优先级设置:捕获中断抢占优先级0,更新中断抢占优先级1
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
}

// 中断服务程序
void TIM2_IRQHandler(void) {
    // 捕获中断处理
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_CC1) != RESET &&
        __HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_CC1) != RESET) {
        __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_CC1);

        if (capture_flag == 0) {
            // 第一次捕获:上升沿
            capture_val1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
            // 切换为下降沿捕获
            __HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
            capture_flag = 1;
        } else {
            // 第二次捕获:下降沿
            capture_val2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
            // 计算占空比(考虑溢出)
            uint32_t period, high_time;
            if (capture_val2 >= capture_val1) {
                high_time = capture_val2 - capture_val1;
            } else {
                high_time = (0xFFFF - capture_val1) + capture_val2 + 1;
            }
            // 假设PWM周期已知或通过两次上升沿测量,此处简化
            // 实际应测量完整周期,这里仅示例占空比计算逻辑
            duty_cycle = (high_time * 100) / 0xFFFF; // 需根据实际周期调整
            // 切换回上升沿捕获,准备下一次测量
            __HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
            capture_flag = 0;
        }
    }

    // 更新中断处理(溢出)
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET &&
        __HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET) {
        __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
        overflow_cnt++;
    }
}

注意:上述代码中占空比计算简化了周期测量,实际应捕获两次上升沿获得周期,再计算高电平时间。

注意事项与排查技巧

  • 优先级设置:务必通过HAL_NVIC_SetPriority将捕获中断抢占优先级设为高于更新中断。若使用标准库,直接配置NVIC_IPR寄存器。
  • 中断标志清除顺序:先清除捕获标志再读取CCR,避免清除过程中新事件覆盖。更新中断中仅做计数,不进行耗时操作。
  • 临界区保护:在读取overflow_cnt与CCR时,可短暂关闭中断(__disable_irq())确保数据一致性,但时间应极短。
  • 溢出计数扩展:若PWM周期远大于定时器周期,可考虑使用32位定时器或提高预分频,减少溢出中断频率。
  • 调试方法:使用逻辑分析仪观察捕获引脚与中断响应时序;在中断中翻转GPIO,用示波器测量中断延迟。
  • 常见误区:不要依赖HAL_TIM_ReadCapturedValue的返回值判断边沿,应通过极性切换和状态变量管理。

通过合理配置中断优先级与严谨的中断处理逻辑,可彻底解决占空比测量中的跳变问题,提升嵌入式系统的可靠性。