STM32定时器输入捕获测量PWM占空比的误差来源与补偿方法
👁 5 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,利用STM32定时器输入捕获功能测量PWM信号的频率和占空比是常见需求,但实际测量结果常因硬件限制、软件时序和信号质量等因素产生误差。本文深入剖析这些误差的根源,包括捕获分辨率、中断延迟、边沿抖动和计数器溢出等,并给出实用的补偿策略,如多周期平均、边沿滤波、DMA批量捕获和软件校准。通过原理讲解、配置步骤和完整代码示例,帮助开发者提升测量精度,适用于电机控制、电源监测等嵌入式应用场景。
# STM32定时器输入捕获测量PWM占空比的误差来源与补偿方法
在嵌入式系统中,测量PWM信号的占空比是电机调速、LED调光或电源监控中的核心任务。STM32的定时器输入捕获功能提供了硬件级的时间戳记录,但直接使用往往达不到理想精度。本文面向有一定基础的开发者,深入分析误差来源,并给出可落地的补偿方案。
## 一、输入捕获的基本原理
STM32定时器在输入捕获模式下,会检测引脚上的边沿(上升沿或下降沿),并将当前计数器的值锁存到捕获寄存器(CCRx)。通过连续捕获两次边沿,即可计算周期或高电平时间。
- 测量周期:捕获两次上升沿,差值即为周期(TIM_Period + 1 个计数周期)。
- 测量占空比:捕获上升沿和下降沿,差值即为高电平时间,除以周期得到占空比。
典型配置如下(以TIM2为例,使用CH1):
```c
// 初始化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);
```
中断回调中读取捕获值:
```c
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个采样周期的毛刺。
```c
TIM_ICInitStruct.ICFilter = 0x05; // 根据实际噪声调整
```
注意:滤波会增加信号延迟(约几个采样周期),但可显著提高稳定性。
### 3. 多周期平均法
连续测量N个周期,累加后取平均,可消除随机抖动误差。在中断中只记录捕获值,主循环中计算平均值。
```c
#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循环传输。
```c
// 配置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自动捕获周期和占空比,并计算平均值。
```c
// 全局变量
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测量。