第三章 单片机PWM输出:STM32定时器配置、PWM输出模式设置、GPIO复用功能

好,咱们进入实战环节了。前面聊了那么多PWM的原理和调光算法,现在终于要跟硬件打交道了。这一章,我带你手把手把STM32的PWM输出调通。

说实话,我最早接触STM32的PWM时,也被那一堆寄存器搞晕过。后来发现,其实核心就三件事:定时器怎么配、PWM模式怎么选、GPIO怎么切到定时器上去。搞明白这三步,剩下的就是调参数了。

3.1 STM32定时器:PWM的心脏

STM32的定时器,说白了就是个会数数的硬件计数器。它从0开始,一直数到某个设定值,然后翻回来重新数。这个过程中,我们可以设置一个比较值——当计数值等于比较值时,输出引脚的电平就翻转一下。这就是PWM的底层原理。

我个人习惯把定时器分成三类:

  • 基本定时器(TIM6、TIM7):只能计时,不能输出PWM。别在这上面浪费时间。
  • 通用定时器(TIM2~TIM5):最常用的,有4个独立通道,每个通道都能输出PWM。我项目中90%的情况都用它。
  • 高级定时器(TIM1、TIM8):功能最全,支持互补输出、死区插入。做电机驱动、H桥电路时必用。
我的小建议:刚开始学PWM,先用通用定时器。别一上来就碰高级定时器,那玩意儿寄存器多到你怀疑人生。我在第一个项目中就踩过这个坑,花了三天才把TIM1的互补输出调通。

3.2 定时器配置:三步走

配置定时器输出PWM,我总结了一个三步走的套路。你按这个来,基本不会出错。

第一步:确定时钟源

STM32的定时器时钟来自APB总线。一般来说,APB1上的定时器(TIM2~TIM7)时钟是72MHz,APB2上的(TIM1、TIM8)也是72MHz。但要注意,如果APB分频系数不是1,定时器时钟会翻倍。嗯,这个细节容易忘,我当年就因为这个算错过PWM频率。

第二步:设置自动重装载值(ARR)和预分频器(PSC)

这两个参数决定了PWM的频率。公式很简单:

PWM频率 = 定时器时钟 / ((PSC + 1) * (ARR + 1))

举个例子,我想要20kHz的PWM频率,时钟是72MHz:

72000000 / ((PSC + 1) * (ARR + 1)) = 20000
假设 PSC = 71,那么 ARR + 1 = 72000000 / (72 * 20000) = 50
所以 ARR = 49

重要:ARR的值决定了PWM的分辨率。ARR越大,你能调节的占空比档位就越多。比如ARR=49,你只有50档可调;如果ARR=999,就有1000档。LED调光的话,我建议ARR至少设到255以上,不然调光时能看到明显的跳变。

第三步:配置捕获/比较寄存器(CCR)

CCR就是比较值。当定时器计数值小于CCR时,输出高电平;大于等于CCR时,输出低电平(或者反过来,取决于极性设置)。所以占空比就是:

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

比如ARR=999,CCR=500,占空比就是50%。

3.3 PWM输出模式设置

STM32的PWM有两种模式:PWM模式1和PWM模式2。区别很简单:

模式 计数值 < CCR 计数值 >= CCR
PWM模式1 有效电平(通常是高) 无效电平(通常是低)
PWM模式2 无效电平 有效电平

说白了,模式1是向上计数时先高后低,模式2是先低后高。我99%的情况都用模式1,因为更符合直觉——CCR越大,高电平时间越长,灯越亮。

我曾经踩过的坑:有一次我配置完PWM,发现灯死活不亮。查了半天,原来是极性设置反了。STM32的GPIO输出极性可以单独设置,如果你发现输出跟预期相反,检查一下TIM_OCInitStructure.TIM_OCPolarity这个参数。

3.4 GPIO复用功能配置

定时器的PWM信号要通过GPIO引脚输出。你不能直接把GPIO设成推挽输出就完事了——得把它复用成定时器的功能。

具体步骤:

  1. 使能GPIO时钟:比如用GPIOA,就调用RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)。
  2. 使能定时器时钟:比如用TIM2,就调用RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE)。
  3. 配置GPIO为复用推挽输出:GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP。
  4. 选择复用功能:对于STM32F1系列,用GPIO_PinRemapConfig()配置引脚映射;对于F4系列,用GPIO_PinAFConfig()选择AF编号。

这里有个容易搞混的地方:不同型号的STM32,同一个定时器的通道可能映射到不同的引脚。比如TIM2的通道1,在F103C8T6上是PA0,但在F103ZET6上可能是PA0或者PA15。所以一定要查数据手册的引脚定义表。

我的习惯:画PCB之前,先把所有要用到的定时器通道和引脚对应关系列个表。省得板子打回来发现引脚冲突,那叫一个酸爽。

3.5 完整代码示例

说了这么多,来段完整的代码吧。这是一个用TIM2的通道1在PA0上输出1kHz、50%占空比PWM的配置:

void PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 2. 配置GPIO为复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. 配置定时器时基
    TIM_TimeBaseStructure.TIM_Period = 999;        // ARR = 999
    TIM_TimeBaseStructure.TIM_Prescaler = 71;      // PSC = 71
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 4. 配置PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;           // CCR = 500, 占空比50%
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    // 5. 使能定时器
    TIM_Cmd(TIM2, ENABLE);
}

这段代码跑起来,PA0上就能测到1kHz的PWM波形了。频率怎么算的?72MHz / (71+1) / (999+1) = 1000Hz,没错吧?

3.6 调试技巧与常见问题

最后分享几个我实际调试中遇到的问题:

  • 波形出不来?先检查GPIO的复用功能有没有配对。用示波器量一下引脚,如果一直是高或低,多半是复用没配好。
  • 频率不对?检查PSC和ARR的计算。我建议先算好理论值,再用逻辑分析仪抓一下实际波形对比。
  • 占空比调不到0%或100%?有些定时器在CCR=0或CCR=ARR+1时会有异常。解决办法是软件里做限幅,比如CCR最小设1,最大设ARR。
  • 多个通道互相影响?每个通道的CCR是独立的,但ARR是共享的。所以如果你要用不同频率的PWM,得用不同的定时器。

核心总结:STM32的PWM输出,记住三个关键参数——PSC决定计数步长,ARR决定周期长度,CCR决定占空比。配好时钟、选对模式、设对引脚,剩下的就是调CCR的事了。

下一章,我们会把这些PWM信号真正用到LED上,实现平滑的调光效果。到时候你会看到,前面这些配置工作,都是为后面的算法打基础的。