STM32定时器的PWM模式,从捕获/比较寄存器开始理解

你有没有想过,一个定时器的PWM输出,说到底就是比较两个寄存器的值?先看代码:

TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 71; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 999; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

这段代码配置完,PA0引脚上会输出一个1kHz、占空比50%的方波。Prescaler=71把72MHz的系统时钟先除以72,得到1MHz的计数时钟。Period=999意味着计数器从0走到999正好1000个计数步——1MHz / 1000 = 1kHz,没错。Pulse=500就是高电平持续500步,占空比50%。

一步步来,先搞清楚PWM的底层是怎么运作的。

核心硬件:一个计数器CNT,一个自动重装载寄存器ARR,一个捕获/比较寄存器CCR。

CNT在每个时钟脉冲后加1,从0累加到ARR的值,然后溢出回0,如此循环。CCR里存的是比较阈值——硬件比较器在每一个时钟周期里比较CNT和CCR的大小。在PWM1模式下,CNT < CCR时输出有效电平,CNT >= CCR时输出无效电平。

所以方才那个配置里,CNT从0走到499时输出高,从500走到999时输出低。下一周期CNT回到0,又重新开始。周期由ARR决定,占空比由CCR决定。这两个寄存器的组合把PWM的频率和占空比完全解耦了——改频率只动ARR,改占空比只动CCR,互不影响。

一个有意思的地方是,CCR可以在运行时随时写入新值。这意味着占空比可以动态调整,而且在修改的瞬间生效。做呼吸灯的话只需要在主循环里逐次改变Pulse的值:

uint16_t brightness = 0; int8_t direction = 1; while (1) { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, brightness); brightness += direction; if (brightness >= 999) direction = -1; if (brightness == 0) direction = 1; HAL_Delay(2); }

每次循环做一次寄存器写入,LED的亮度就从暗到亮再回到暗。整个过程没有中断参与,完全是硬件在背后默默地做比较。CPU每秒要跑成百上千次其他任务,而PWM的精度丝毫不受影响——这就是硬件外设存在的意义。

深入一步看。STM32的一个定时器里,多个通道共享同一个ARR,但每个通道拥有独立的CCR。这意味着我们可以用同一个定时器输出多路频率相同、占空比各自独立的PWM。驱动四路舵机或者一组RGB LED时,这个设计特别实用——频率统一由ARR决定,每路的脉宽单独调。

不妨这样考虑:如果反过来,固定CCR、去读CNT的值,捕获/比较单元就成了输入捕获——测量外部信号的脉宽或者周期。同一个寄存器块,方向颠倒一下,功能完全不同。PWM输出也好,输入捕获也好,一个定时器可以同时工作在多种模式下,关键是理解它的工作机制。

HAL库里有一个宏值得注意:

#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \ (*(__IO uint32_t *)&((__HANDLE__)->Instance->CCR1) \ + (__CHANNEL__)) = (__COMPARE__)

本质上就是一次对CCR寄存器的内存写入。没有函数调用栈的开销,没有参数校验,没有状态检查。在控制BLDC电机或者做数字电源这类对响应时间有要求的场景里,用这个宏直接写寄存器比调用HAL_TIM_PWM_SetPulseAnd duty要快得多。一层层函数调用下去,最终做的是同一件事——往CCR写值。

更深入一点:高级定时器TIM1和TIM8支持互补PWM输出和可编程死区插入。做H桥驱动时,上下管绝对不能同时导通,死区时间就是上下管切换之间那个"两路都关断"的安全窗口。STM32的硬件死区发生器能精确到几个时钟周期,比自己用软件延时去折腾可靠得多。

从捕获/比较寄存器的视角去理解PWM,整个机制其实很简洁的。频率由ARR控制,占空比由CCR决定,动态修改CCR就能实时调脉宽。无论用HAL库、LL库还是直接操作寄存器,底层逻辑没有区别。换一个系列的MCU,只要理解了比较器的工作方式,上手只是换个寄存器名字的事。