3. STM32定时器配置:时钟源选择、时基单元配置、输出比较通道配置、PWM输出使能

定时器配置,是PWM输出的地基。地基没打好,后面优化全是白费力气。我见过不少工程师,上来就抄寄存器配置,结果PWM频率不对、相位乱跳,查了半天才发现是时钟源选错了。

这一节,咱们把定时器配置拆成四个步骤。每一步我都会结合项目经验,告诉你哪些坑不能踩。

3.1 时钟源选择:别让PWM跑偏了

STM32的定时器时钟,不是随便选的。它跟系统时钟树有直接关系。

先看这张图,理清时钟源的整体流向:

STM32定时器时钟源选择与配置流程 时钟源选择 内部时钟 (CK_INT) 外部时钟模式1 (TIx) 外部时钟模式2 (ETR) PSC 预分频器 时基单元 (CNT/ARR) 输出比较通道 (CCRx) PWM 输出使能 PWM输出 = 时钟源 → 分频 → 计数 → 比较 → 输出

我个人习惯,99%的PWM应用都用内部时钟(CK_INT)。它直接来自APB总线,稳定可靠。

但要注意一个细节:定时器的时钟频率不一定等于APB总线频率。如果APB预分频系数不是1,定时器时钟会翻倍。举个例子,APB1预分频为4,那么挂载在APB1上的定时器,实际时钟是APB1的2倍。

我曾经踩过的坑: 用STM32F103输出50Hz的PWM,怎么算都不对。后来发现APB1预分频设成了2,定时器时钟自动翻倍到72MHz。我按36MHz算的,自然全错。所以,配置定时器前,一定先确认APB预分频系数。

外部时钟模式1和模式2,一般用在需要同步外部信号的场景。比如电机编码器输入、PWM同步锁相。普通PWM输出,用内部时钟就够了。

3.2 时基单元配置:频率和精度的博弈

时基单元,说白了就是三个寄存器:PSC(预分频器)、ARR(自动重装载)、CNT(计数器)。

PWM频率的计算公式很简单:

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

举个例子,定时器时钟72MHz,想要20kHz的PWM:

72,000,000 / (PSC + 1) / (ARR + 1) = 20,000
假设 PSC = 71,则 (ARR + 1) = 72,000,000 / 72 / 20,000 = 50
所以 ARR = 49

这里有个权衡:PSC越大,ARR越小,PWM分辨率越低。分辨率就是ARR+1的值,它决定了你能调节多少个占空比等级。

场景 PSC ARR 分辨率 适用场景
高频率 小 小 低 电机驱动(20kHz以上)
高精度 大 大 高 LED调光、模拟输出
平衡 适中 适中 适中 通用PWM

我的建议: 先确定你需要的PWM频率,再根据分辨率需求反推PSC。如果分辨率不够,就提高定时器时钟(比如用TIM1/TIM8,它们挂载在APB2上,频率更高)。

配置代码(以TIM3为例,内部时钟,向上计数模式):

// 假设定时器时钟为72MHz,目标PWM频率20kHz,分辨率1000级
// 72MHz / (PSC+1) / (ARR+1) = 20kHz
// 设 ARR+1 = 1000,则 PSC+1 = 72,000,000 / 20,000 / 1000 = 3.6 → 取整

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 3;      // PSC = 3,实际分频系数为4
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 999;        // ARR = 999,分辨率1000级
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

嗯,这里要注意:PSC和ARR都是16位的,最大值65535。如果算出来超过这个范围,要么降低分辨率,要么换32位定时器(STM32F4系列有32位定时器)。

3.3 输出比较通道配置:占空比的核心

输出比较通道,就是CCR寄存器。它跟CNT比较,决定PWM的占空比。

配置输出比较通道,主要做三件事:

  1. 选择输出模式:PWM1还是PWM2?
  2. 设置极性:高电平有效还是低电平有效?
  3. 使能预装载:要不要影子寄存器?

PWM1和PWM2的区别,说白了就是极性相反:

  • PWM1模式:CNT < CCR时输出有效电平,CNT ≥ CCR时输出无效电平
  • PWM2模式:CNT < CCR时输出无效电平,CNT ≥ CCR时输出有效电平

我一般用PWM1模式,配合极性配置,可以灵活控制输出波形。

配置代码示例:

// 配置TIM3通道1为PWM1模式,高电平有效
TIM_OCInitTypeDef TIM_OCInitStructure;
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_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);

// 使能预装载寄存器(重要!)
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);

我曾经犯过的错: 没使能预装载寄存器,结果在运行时修改CCR值,PWM波形出现毛刺。因为CNT正在计数,CCR突然变了,比较结果瞬间跳变。使能预装载后,CCR只在CNT归零时更新,波形就平滑了。

3.4 PWM输出使能:最后一步,别漏了

配置完时基和通道,还得让定时器真正输出PWM。这一步分两个层面:

  • 定时器使能:让CNT开始计数
  • 输出使能:让GPIO引脚输出PWM信号

代码很简单:

// 使能定时器
TIM_Cmd(TIM3, ENABLE);

// 如果是高级定时器(TIM1/TIM8),还需要使能主输出
// TIM_CtrlPWMOutputs(TIM1, ENABLE);

GPIO配置也要注意:PWM输出引脚必须配置为复用推挽输出,频率建议设为50MHz。

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;      // TIM3_CH1对应PA6
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

一个小技巧: 调试时,可以先让PWM输出50%占空比,用示波器看波形。如果频率不对,检查时钟源和PSC/ARR;如果波形没有,检查GPIO复用功能和定时器使能顺序。

3.5 完整配置流程总结

把上面四步串起来,就是完整的PWM配置流程:

  1. 时钟配置:使能定时器时钟和GPIO时钟
  2. GPIO配置:复用推挽输出
  3. 时基配置:PSC、ARR、计数模式
  4. 输出比较配置:PWM模式、极性、CCR值、预装载
  5. 输出使能:TIM_Cmd + 高级定时器主输出

嗯,这套流程我用了快十年,基本没出过问题。你只要按这个顺序来,PWM输出就能稳定工作。