4. 定时器PWM输出实战:PWM模式原理、捕获/比较寄存器(CCR)作用、配置呼吸灯效果

各位同学,今天我们来聊聊嵌入式开发里一个非常经典、也非常有成就感的话题——PWM输出。说白了,PWM就是通过快速开关信号,让负载(比如LED、电机)感受到一个“平均”的效果。你想想看,如果让LED以极快的频率闪烁,人眼是看不出来的,只会觉得亮度变了。这就是PWM的精髓。

4.1 PWM模式原理:从方波到模拟量

PWM的全称是Pulse Width Modulation,脉冲宽度调制。它的核心思想很简单:固定频率,改变占空比。

  • 频率:信号每秒钟变化的次数,决定了PWM的“快慢”。
  • 占空比:高电平时间占整个周期的比例,决定了“有效值”。

举个例子,一个周期是100μs,高电平持续30μs,那么占空比就是30%。如果驱动LED,它看起来就是30%的亮度。我在项目中遇到过用PWM控制舵机的情况,占空比稍微偏一点,舵机角度就不对,那时候真是调得头皮发麻。

核心公式:

占空比 = (CCR值) / (ARR值 + 1) × 100%

其中ARR是自动重装载寄存器,决定周期;CCR是捕获/比较寄存器,决定高电平宽度。

定时器内部是怎么工作的呢?它有一个计数器,从0一直数到ARR,然后归零重新开始。在这个过程中,计数器会不断和CCR的值做比较。当计数器的值小于CCR时,输出高电平;大于等于CCR时,输出低电平。这样,一个完整的PWM波形就生成了。

PWM生成原理示意图 计数器值(从0到ARR) CCR值 PWM输出波形 计数器 < CCR → 输出高电平 计数器 ≥ CCR → 输出低电平

4.2 捕获/比较寄存器(CCR)的作用

CCR,全称Capture/Compare Register,捕获/比较寄存器。这个名字其实暴露了它的双重身份:

  • 捕获模式:当外部信号发生变化时,CCR会“抓住”当前计数器的值,用于测量脉宽或频率。
  • 比较模式:CCR与计数器不断比较,当相等时触发事件,比如翻转输出引脚。

在PWM输出中,我们用的是比较模式。CCR的值直接决定了占空比。你想想看,如果ARR是999,CCR是500,那么占空比就是50%。如果CCR是0,占空比就是0%,LED全灭。如果CCR是999,占空比就是100%,LED全亮。

我的个人习惯:在配置PWM时,我会先把ARR和CCR的关系理清楚。ARR决定频率,CCR决定占空比。两者独立配置,互不干扰。但要注意,CCR的值绝对不能超过ARR,否则输出会一直保持高电平,占空比变成100%。

我曾经在一个项目中,因为CCR写成了ARR+1,结果LED死活不按预期亮度变化,查了半天才发现是寄存器溢出了。嗯,这种低级错误,犯过一次就再也不会忘了。

4.3 配置呼吸灯效果:从理论到实践

呼吸灯,说白了就是LED的亮度从暗到亮、再从亮到暗,循环往复,像呼吸一样。实现这个效果,我们需要做两件事:

  1. 固定PWM频率:比如1kHz,这样人眼不会看到闪烁。
  2. 动态改变CCR值:让占空比从0%逐渐增加到100%,再逐渐减少到0%。

下面是一个基于STM32的配置示例,我习惯用HAL库,但寄存器级别的理解更重要。

// 定时器基本配置
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;          // 72MHz / (71+1) = 1MHz
htim2.Init.Period = 999;            // 1MHz / (999+1) = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);

// PWM通道配置
TIM_OC_InitTypeDef sConfigOC;
sConfigOC.OCMode = TIM_OCMODE_PWM1;   // PWM模式1:递增时CCR以下为高
sConfigOC.Pulse = 0;                  // 初始占空比0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

// 启动PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

// 呼吸灯主循环
while (1) {
    // 从暗到亮
    for (int i = 0; i <= 999; i++) {
        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
        HAL_Delay(1);  // 每步延时1ms
    }
    // 从亮到暗
    for (int i = 999; i >= 0; i--) {
        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
        HAL_Delay(1);
    }
}

注意:HAL_Delay(1)的精度是1ms,如果你想让呼吸更平滑,可以用定时器中断或者更精确的延时。另外,CCR的变化步长也可以调整,步长越小,呼吸越细腻,但CPU负担也越大。

这段代码的逻辑很简单:主循环里用两个for循环,一个递增CCR,一个递减CCR。每次改变后延时1ms,这样整个呼吸周期大约是2秒(1000步×2×1ms)。

4.4 避坑指南与经验总结

在实际项目中,PWM配置有几个常见的坑,我给大家列一下:

常见问题 原因 解决方案
LED不亮或常亮 CCR值超出ARR范围 确保CCR ≤ ARR
LED闪烁明显 PWM频率太低 提高频率到1kHz以上
呼吸不平滑 CCR步长太大或延时不准 减小步长,使用定时器中断
引脚无输出 GPIO未配置为复用功能 检查GPIO模式,设置为AF_PP

我曾经在一个电机控制项目中,因为PWM频率设置得太低(只有50Hz),结果电机发出刺耳的啸叫声。后来查资料才知道,人耳对几百赫兹的声音特别敏感,最后把频率调到20kHz才解决问题。嗯,这种经验,书本上可不会告诉你。

总结一下:

  • PWM的核心是频率和占空比,由ARR和CCR决定。
  • CCR在比较模式下控制占空比,在捕获模式下测量信号。
  • 呼吸灯就是动态改变CCR,让占空比周期性变化。
  • 实际调试时,先用示波器看波形,确认频率和占空比是否正确。

好了,这一节的内容就到这里。PWM这个东西,说难不难,说简单也不简单,关键是要理解计数器、ARR和CCR三者之间的关系。你只要把这个三角关系搞清楚了,剩下的就是写代码的事了。