STM32定时器输入捕获测量PWM占空比的误差来源与补偿方法

在嵌入式系统中,测量PWM信号的占空比是电机调速、LED调光或电源监控中的核心任务。STM32的定时器输入捕获功能提供了硬件级的时间戳记录,但直接使用往往达不到理想精度。本文面向有一定基础的开发者,深入分析误差来源,并给出可落地的补偿方案。

一、输入捕获的基本原理

STM32定时器在输入捕获模式下,会检测引脚上的边沿(上升沿或下降沿),并将当前计数器的值锁存到捕获寄存器(CCRx)。通过连续捕获两次边沿,即可计算周期或高电平时间。

  • 测量周期:捕获两次上升沿,差值即为周期(TIM_Period + 1 个计数周期)。
  • 测量占空比:捕获上升沿和下降沿,差值即为高电平时间,除以周期得到占空比。

典型配置如下(以TIM2为例,使用CH1):

// 初始化GPIO和定时器
GPIO_InitTypeDef GPIO_InitStruct = {0};
TIM_IC_InitTypeDef TIM_ICInitStruct = {0};

__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

__HAL_AFIO_REMAP_TIM2_ENABLE(); // 根据具体型号可能不需要

TIM_ICInitStruct.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
TIM_ICInitStruct.ICSelection = TIM_ICSELECTION_DIRECTTI;
TIM_ICInitStruct.ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.ICFilter = 0; // 先设为0,后面讨论滤波
HAL_TIM_IC_Init(&htim2);
HAL_TIM_IC_ConfigChannel(&htim2, &TIM_ICInitStruct, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);

中断回调中读取捕获值:

uint32_t ic_val1, ic_val2;
uint8_t capture_index = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        if (capture_index == 0) {
            ic_val1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
            // 切换捕获极性为下降沿
            TIM_RESET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1);
            TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
            capture_index = 1;
        } else if (capture_index == 1) {
            ic_val2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
            // 切换回上升沿
            TIM_RESET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1);
            TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
            capture_index = 0;
            // 计算高电平时间和周期(需要处理溢出)
            uint32_t high_time = ic_val2 - ic_val1;
            // 周期需要再捕获一次上升沿,此处略
        }
    }
}

二、误差来源分析

1. 捕获分辨率限制

定时器计数器以时钟频率递增,因此捕获值只能精确到1个计数周期。例如,72MHz时钟下,分辨率约为13.9ns。若PWM频率较高(如100kHz),周期为10us,则量化误差可达0.14%,占空比测量误差随高电平时间变短而增大。

2. 中断响应延迟

在中断中切换捕获极性时,从捕获事件发生到执行切换指令存在延迟(通常几微秒)。若PWM边沿在此延迟内再次到来,会导致捕获极性错误,产生严重偏差。此外,中断嵌套或高优先级任务会加剧延迟。

3. 信号边沿抖动

实际PWM信号可能因电路噪声或驱动芯片产生边沿抖动,导致捕获时间戳提前或滞后,引入随机误差。

4. 计数器溢出处理不当

当定时器为16位时,计数器溢出(从65535回到0)会导致捕获差值计算错误,需在中断中处理溢出标志,否则测量值会突变。

5. 软件处理开销

在中断中执行复杂计算(如除法)会占用大量时间,影响后续捕获,甚至丢失事件。

三、补偿方法

1. 提高分辨率:使用更高时钟或32位定时器

  • 若定时器时钟可倍频(如使用PLL),提高TIMxCLK。
  • 使用32位定时器(如TIM2/TIM5)在STM32F4等系列上,可避免溢出问题,但分辨率仍受时钟限制。

2. 硬件滤波:配置输入滤波器

STM32的输入捕获通道自带数字滤波器(ICF位),可滤除短于设定宽度的脉冲,抑制边沿抖动。例如,设置ICF=0b0101(采样频率为fDTS/16,采样8次)可滤除宽度小于8个采样周期的毛刺。

TIM_ICInitStruct.ICFilter = 0x05; // 根据实际噪声调整

注意:滤波会增加信号延迟(约几个采样周期),但可显著提高稳定性。

3. 多周期平均法

连续测量N个周期,累加后取平均,可消除随机抖动误差。在中断中只记录捕获值,主循环中计算平均值。

#define N 10
uint32_t capture_buffer[N];
uint8_t buffer_index = 0;
uint32_t sum = 0;

// 在捕获中断中存储值,不进行计算
capture_buffer[buffer_index] = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
buffer_index = (buffer_index + 1) % N;

// 主循环中计算平均周期
uint32_t average_period = 0;
for (int i = 0; i < N-1; i++) {
    average_period += capture_buffer[i+1] - capture_buffer[i];
}
average_period /= (N-1);

4. 使用DMA批量捕获

将捕获值通过DMA直接存储到内存,无需CPU干预,减少中断延迟。配置定时器为PWM输入模式(同时捕获周期和占空比),并使用DMA循环传输。

// 配置PWM输入模式
TIM_SlaveConfigTypeDef sSlaveConfig = {0};
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig);

TIM_IC_InitTypeDef sICConfig = {0};
sICConfig.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sICConfig.ICSelection = TIM_ICSELECTION_DIRECTTI;
sICConfig.ICPrescaler = TIM_ICPSC_DIV1;
sICConfig.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim2, &sICConfig, TIM_CHANNEL_1);

sICConfig.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
sICConfig.ICSelection = TIM_ICSELECTION_INDIRECTTI;
HAL_TIM_IC_ConfigChannel(&htim2, &sICConfig, TIM_CHANNEL_2);

// 启动DMA
uint32_t capture_values[2];
HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_1, &capture_values[0], 1);
HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_2, &capture_values[1], 1);

5. 软件校准:测量已知信号

用精密信号源产生已知占空比的PWM,测量后建立误差表或线性拟合,修正后续测量值。例如,实际占空比 = 测量值 * 斜率 + 偏移。

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

以下代码实现PWM输入模式,使用DMA自动捕获周期和占空比,并计算平均值。

// 全局变量
TIM_HandleTypeDef htim2;
uint32_t g_capture_buffer[2]; // [0]周期,[1]高电平时间
volatile uint32_t g_period, g_duty;

// 定时器初始化
void MX_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 = 0; // 72MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFFFFFF; // 32位定时器,避免溢出
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_IC_Init(&htim2);

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

    // PWM输入模式:从模式复位,触发源TI1FP1
    sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
    sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
    sSlaveConfig.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sSlaveConfig.TriggerFilter = 0;
    HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig);

    // 通道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);

    // 通道2捕获下降沿(高电平时间)
    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
    sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI;
    HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_2);

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIM_MasterConfig(&htim2, &sMasterConfig);
}

// 启动测量
void start_pwm_measurement(void) {
    HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_1, &g_capture_buffer[0], 1);
    HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_2, &g_capture_buffer[1], 1);
}

// DMA传输完成回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        g_period = g_capture_buffer[0];
        g_duty = g_capture_buffer[1];
        // 可在此处计算占空比,但建议在主循环中处理
    }
}

// 主循环中计算占空比
void calculate_duty_cycle(void) {
    if (g_period != 0) {
        float duty = (float)g_duty / g_period * 100.0f;
        printf("Duty: %.2f%%\r\n", duty);
    }
}

五、注意事项

  • 中断优先级:若使用中断方式,确保捕获中断优先级足够高,避免被其他中断延迟。
  • DMA模式:使用DMA时,需确保缓冲区大小足够,且处理完数据后重新启动DMA(循环模式可自动重载)。
  • 滤波器设置:滤波值过大会导致信号失真,需根据PWM频率和噪声特性权衡。
  • 溢出处理:即使使用32位定时器,若PWM频率极低(周期>2^32/时钟),仍需处理溢出。
  • 代码优化:避免在中断中做浮点运算或复杂计算,可先存储原始值,主循环中处理。

六、总结

STM32输入捕获测量PWM占空比的误差主要来源于分辨率、中断延迟和信号抖动。通过合理配置硬件(如滤波器、DMA)、采用软件算法(如平均、校准),可以显著提升测量精度。实际应用中,应根据信号频率和精度需求选择合适的策略,必要时结合示波器验证。希望本文能帮助你在嵌入式项目中实现更可靠的PWM测量。