第4章 STM32定时器与PWM输出:定时器配置、PWM模式设置、占空比调节、频率与分辨率计算

做电机控制,说白了就是跟定时器打交道。PWM信号是电机调速的“语言”,而STM32的定时器就是翻译官。这一章,我带你把这套东西彻底搞明白。

4.1 定时器家族:你该用哪一个?

STM32的定时器分好几类,别一上来就选高级定时器,杀鸡用牛刀。

定时器类型 位数 典型用途 我常用的场景
基本定时器(TIM6/TIM7) 16位 定时中断、DAC触发 做时间基准,很少用于PWM
通用定时器(TIM2~TIM5) 16/32位 PWM输出、输入捕获、编码器接口 电机控制的主力,我用得最多
高级定时器(TIM1/TIM8) 16位 互补PWM、刹车功能、死区插入 无刷直流电机(BLDC)驱动必备
我的习惯:做直流有刷电机调速,通用定时器就够了。做FOC或者BLDC,必须上高级定时器,因为死区时间不设好,MOS管会炸。

4.2 PWM模式:边沿对齐 vs 中央对齐

STM32的PWM有两种模式,很多人用了几年都没搞清楚区别。

  • 边沿对齐模式(Mode 1 / Mode 2):计数器从0数到ARR,然后跳回0。PWM波形只有一边在变化。占空比更新快,适合普通调速。
  • 中央对齐模式(Mode 1 / Mode 2 / Mode 3):计数器从0数到ARR,再倒着数回0。PWM波形两边都在变化。谐波更少,适合对噪声敏感的场合。

我做过一个项目,电机低速运行时嗡嗡响。换成中央对齐模式,噪声立刻降了3dB。为什么?因为中央对齐的PWM等效频率翻倍,谐波分量更小。

4.3 频率与分辨率:鱼和熊掌怎么兼得?

这是新手最容易踩坑的地方。PWM频率和分辨率是矛盾的,你想想看:

公式:

PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
分辨率 = 1 / (ARR + 1) × 100%

ARR越大,分辨率越高,但频率越低。反过来,频率高了,分辨率就下来了。

实战经验:我做电机调速时,通常把PWM频率设在10kHz~20kHz。低于10kHz能听到啸叫,高于20kHz开关损耗变大。分辨率至少保证200级(ARR≥199),否则电机低速会抖动。

举个例子,STM32F103主频72MHz,我想用TIM2输出20kHz PWM:

// 目标:20kHz,分辨率250级
// 72MHz / (PSC+1) / (ARR+1) = 20000
// 设 ARR = 249(分辨率250级)
// 则 PSC+1 = 72000000 / 20000 / 250 = 14.4 → 取14

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseStructure.TIM_Prescaler = 13;      // PSC = 13 → 分频系数14
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 249;         // ARR = 249
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;              // 初始占空比0%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);

TIM_Cmd(TIM2, ENABLE);
我曾经踩过的坑:PSC和ARR都设成0,以为频率最高。结果定时器直接溢出,PWM波形乱跳。记住,PSC和ARR至少有一个不为0。

4.4 占空比调节:在线修改的两种方式

电机调速需要实时改变占空比。STM32提供了两种方法:

  1. 直接写CCR寄存器:TIM_SetCompare1(TIM2, duty); 简单粗暴,但要注意更新时机。
  2. 利用预装载寄存器:使能TIM_OCPreload,CCR值在更新事件时才生效。避免PWM周期中途突变。

我建议你用预装载方式。有一次我在中断里直接改CCR,结果PWM波形中间出现了一个窄脉冲,电机猛地抖了一下。开了预装载,问题消失。

// 使能预装载
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);

// 在线修改占空比(在中断或主循环中调用)
void set_motor_speed(uint16_t duty) {
    if(duty > 249) duty = 249;  // 限幅
    TIM_SetCompare1(TIM2, duty);
}

4.5 知识体系:定时器PWM核心逻辑

下面这张图,把整个配置流程串起来了。我每次教新人,都先让他们看这个。

STM32定时器PWM配置流程 1. 使能定时器时钟 2. 配置PSC和ARR 3. 设置PWM模式与极性 4. 使能预装载(可选) 5. 使能定时器 关键参数计算 PWM频率 = 时钟 / (PSC+1) / (ARR+1) 分辨率 = 1 / (ARR+1) × 100% 占空比 = CCR / (ARR+1) × 100% 常见PWM频率选择 电机调速:10kHz ~ 20kHz LED调光:100Hz ~ 1kHz

4.6 避坑指南:我踩过的三个雷

  • 雷区一:GPIO复用功能没配置。定时器通道对应的GPIO必须设为AF_PP(复用推挽输出)。我见过有人设成GPIO_Mode_Out_PP,PWM死活出不来。
  • 雷区二:高级定时器忘了使能主输出。TIM1和TIM8有个TIM_CtrlPWMOutputs函数,不调用它,PWM引脚一直是低电平。我第一次用TIM1时查了三天。
  • 雷区三:占空比突变导致电机抖动。占空比每次变化不要超过50级,最好做一下斜率限制。我写了个ramp函数,每次只增加或减少5级,电机运行丝滑多了。
// 占空比渐变函数,防止电机抖动
void smooth_set_duty(uint16_t target_duty) {
    static uint16_t current_duty = 0;
    if(target_duty > current_duty + 5) {
        current_duty += 5;
    } else if(target_duty < current_duty - 5) {
        current_duty -= 5;
    } else {
        current_duty = target_duty;
    }
    TIM_SetCompare1(TIM2, current_duty);
}

4.7 本章小结