第4章:定时器与PWM控制

4.1 STM32定时器原理——不只是数数那么简单

定时器这东西,说白了就是个计数器。但STM32的定时器,远比你想象的要复杂。

我个人习惯把STM32的定时器分成三类:基本定时器、通用定时器、高级定时器。基本定时器只能做简单的计时,通用定时器多了PWM输出和输入捕获,高级定时器还能做互补输出和死区插入——这些在电机控制里特别有用。

嗯,这里要注意:我们做MRI患者定位系统,步进电机控制主要用通用定时器。高级定时器当然也行,但有点杀鸡用牛刀的感觉。

定时器的核心是一个16位(或32位)的计数器。它会从0开始,一直数到我们设定的值,然后清零重新开始。这个「我们设定的值」就是自动重装载寄存器(ARR)。

举个例子:

// 定时器时钟频率 72MHz,预分频器 7200-1,ARR 10000-1
// 那么定时器频率 = 72MHz / 7200 / 10000 = 1Hz
// 也就是每秒钟产生一次更新事件

TIM_TimeBaseInitTypeDef tim_base;
tim_base.TIM_Prescaler = 7200 - 1;      // 预分频
tim_base.TIM_Period = 10000 - 1;        // 自动重装载值
tim_base.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数
TIM_TimeBaseInit(TIM2, &tim_base);

我在项目中遇到过一个问题:计数器溢出后,如果不及时处理中断,就会丢失一次计时。所以做精确定时的时候,我建议用DMA配合定时器,而不是单纯依赖中断。

4.2 PWM输出配置——让电压“变”出花样

PWM,脉冲宽度调制。说白了就是让一个方波的占空比可调。你想想看,如果我们用这个方波去控制电机,占空比越大,电机转得越快;占空比越小,电机转得越慢。

配置PWM输出,其实就三步:

  1. 设置定时器的ARR(决定频率)
  2. 设置比较值CCR(决定占空比)
  3. 配置输出通道为PWM模式

为什么会这样?因为定时器在计数过程中,会不断把当前计数值和CCR比较。计数值小于CCR时输出高电平,大于CCR时输出低电平。这样占空比就由CCR决定了。

// 配置TIM3的通道1输出PWM
// 频率 = 72MHz / (7200-1) / (1000-1) = 10kHz
// 占空比 = 500 / 1000 = 50%

TIM_OCInitTypeDef oc;
oc.TIM_OCMode = TIM_OCMode_PWM1;       // PWM模式1
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_Pulse = 500;                     // CCR值,决定占空比
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &oc);

// 启动定时器
TIM_Cmd(TIM3, ENABLE);

避坑指南:我曾经在配置PWM时忘记使能GPIO的复用功能,结果示波器上死活看不到波形。折腾了半小时才发现是GPIO没配置对。嗯,这种低级错误,犯过一次就记住了。

小技巧:调试PWM时,先用示波器看波形,确认频率和占空比都对,再接到电机驱动器上。直接接电机,万一频率不对,电机可能会发出刺耳的啸叫声。

4.3 控制步进电机速度——PWM频率是关键

步进电机和普通直流电机不一样。直流电机靠电压调速,步进电机靠脉冲频率调速。每给一个脉冲,步进电机就转一个步距角。脉冲频率越高,电机转得越快。

所以控制步进电机速度,本质上就是控制PWM的频率。而PWM频率由ARR决定:

// 假设步进电机步距角1.8°,细分设置为16
// 那么每转一圈需要 360 / 1.8 * 16 = 3200 个脉冲
// 如果目标转速是 60 RPM(每秒1圈)
// 那么PWM频率 = 3200 Hz

// 定时器配置
// 72MHz / (7200-1) / (3200-1) ≈ 3.125 Hz?不对!
// 这里要反过来算:ARR = 72MHz / 7200 / 3200Hz = 3.125
// 但ARR必须是整数,所以取3
// 实际频率 = 72MHz / 7200 / 3 ≈ 3333 Hz
// 实际转速 = 3333 / 3200 * 60 ≈ 62.5 RPM

uint16_t arr_value = (uint16_t)(72000000 / 7200 / target_freq);
TIM_SetAutoreload(TIM2, arr_value - 1);

这里有个坑:ARR必须是整数,所以实际频率和理论频率会有偏差。转速要求不高时无所谓,但MRI患者定位系统对位置精度要求很高,我建议用高精度定时器或者用DMA来产生精确的脉冲序列。

警告:步进电机启动时不要直接给最高频率。电机有「自启动频率」的限制,超过这个频率直接启动,电机会丢步甚至不转。我一般会做一个「梯形加速曲线」——先低频启动,再逐渐加速到目标频率。

4.4 呼吸灯实验——从亮到暗,从暗到亮

呼吸灯,说白了就是让LED的亮度像呼吸一样缓慢变化。这玩意儿虽然简单,但能很好地展示PWM占空比的动态调整。

原理很简单:不断改变PWM的CCR值,让占空比从0%逐渐增加到100%,再从100%逐渐减小到0%。如此循环,LED就会呈现出「呼吸」的效果。

// 呼吸灯主循环
uint16_t brightness = 0;
uint8_t direction = 1;  // 1: 变亮, 0: 变暗

while(1) {
    TIM_SetCompare1(TIM3, brightness);
    
    if(direction) {
        brightness += 5;
        if(brightness >= 1000) {
            brightness = 1000;
            direction = 0;
        }
    } else {
        if(brightness <= 5) {
            brightness = 0;
            direction = 1;
        } else {
            brightness -= 5;
        }
    }
    
    // 延时约10ms,让变化平滑
    delay_ms(10);
}

我个人习惯把呼吸周期控制在2-3秒。太快了像闪烁,太慢了像没电。当然,这个因人而异,你可以根据自己的喜好调整。

嗯,这里要注意:人眼对亮度的感知不是线性的。占空比从0%到10%的变化,人眼感觉很明显;但从90%到100%的变化,几乎看不出来。所以如果要做得更自然,可以用指数曲线来调整占空比,而不是简单的线性加减。

核心要点:
  • 定时器的ARR决定PWM频率,CCR决定占空比
  • 步进电机速度由PWM频率控制,注意启动时的加速曲线
  • 呼吸灯的核心是动态调整CCR值,建议用指数曲线更自然
  • 调试时先用示波器确认波形,再连接负载

最后说一句:定时器和PWM是嵌入式开发的基石。你想想看,从电机控制到LED调光,从音频输出到电源管理,哪一样离得开PWM?把这章吃透了,后面做闭环控制、做多轴联动,都会轻松很多。