第四章 定时器与PWM生成

各位同学,今天我们来聊聊电机控制里最核心的部分——定时器和PWM。说实话,我刚开始做嵌入式那会儿,总觉得定时器就是个计时的玩意儿,没什么大不了的。直到有一次做窗帘电机项目,电机转起来咔咔响,速度还忽快忽慢,我才意识到:定时器用不好,电机就玩不转

4.1 系统滴答定时器(SysTick)实现延时

SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是个硬件计时器,专门给操作系统或者裸机程序提供心跳。我个人习惯把它当作延时工具来用,简单又可靠。

核心原理:SysTick每来一个时钟脉冲,计数值减1。减到0时,触发中断(或者置位标志位)。我们只要设置好重装载值,就能得到精确的延时。

举个例子,系统时钟72MHz,我想延时1ms:

// SysTick配置函数
void SysTick_Init(void)
{
    // 重装载值 = 系统时钟 / 1000 - 1
    // 72MHz / 1000 = 72000,减1是因为从0开始计数
    SysTick->LOAD = 72000 - 1;
    SysTick->VAL = 0;                    // 清空当前计数值
    SysTick->CTRL = 0x01;                // 使能定时器,不使能中断
}

// 毫秒级延时函数
void delay_ms(uint32_t ms)
{
    for(uint32_t i = 0; i < ms; i++)
    {
        SysTick->VAL = 0;                // 每次重新清零
        // 等待COUNTFLAG置位(计数值减到0)
        while(!(SysTick->CTRL & (1 << 16)));
    }
}

我的经验:用SysTick做延时,记得每次都要清空VAL寄存器。我曾经踩过坑——不清VAL直接等标志位,结果第一次延时正常,第二次就翻倍了。原因?VAL在上次延时结束后可能不是0,导致计数起点不对。

4.2 通用定时器配置

SysTick虽然好用,但功能太单一。真正要控制电机,还得靠通用定时器。STM32的通用定时器(比如TIM2、TIM3)有4个独立通道,可以输出PWM、测量脉宽、做输入捕获等等。

配置通用定时器,我一般按这个步骤来:

  1. 使能时钟——定时器挂在APB1或APB2总线上,先开时钟
  2. 设置预分频器——把系统时钟分频到想要的频率
  3. 设置自动重装载值——决定PWM的周期
  4. 配置输出通道——选择PWM模式、极性等
  5. 使能定时器——开始干活
// 以TIM3为例,配置输出PWM
void TIM3_PWM_Init(void)
{
    // 1. 使能TIM3时钟
    RCC->APB1ENR |= (1 << 1);
    
    // 2. 设置预分频器:72MHz / 72 = 1MHz
    TIM3->PSC = 72 - 1;
    
    // 3. 设置自动重装载值:1MHz / 1000 = 1KHz
    TIM3->ARR = 1000 - 1;
    
    // 4. 配置通道1为PWM模式1
    TIM3->CCMR1 |= (6 << 4);    // 输出比较模式:PWM模式1
    TIM3->CCMR1 |= (1 << 3);    // 预装载使能
    TIM3->CCER |= (1 << 0);     // 使能通道1输出
    
    // 5. 使能定时器
    TIM3->CR1 |= (1 << 0);
}

注意:预分频器PSC和自动重装载值ARR都是16位的,最大值65535。如果你要产生很低的频率(比如0.1Hz),需要把PSC设大一点。我遇到过有人把ARR设成100000,结果溢出后定时器直接罢工了。

4.3 PWM输出原理

PWM,全称脉冲宽度调制。你想想看,电机要转起来,需要的是电压。但单片机只能输出0或3.3V,怎么模拟出中间值?答案就是:快速开关

PWM的核心参数就两个:

  • 频率——开关的快慢。频率太低,电机会嗡嗡响;频率太高,MOS管发热严重。窗帘电机一般用1KHz~20KHz。
  • 占空比——高电平时间占整个周期的比例。占空比50%,等效电压就是1.65V。
占空比 等效电压(3.3V系统) 电机状态
0% 0V 停止
25% 0.825V 微转
50% 1.65V 半速
75% 2.475V 中速
100% 3.3V 全速

为什么会这样?因为电机是感性负载,电流不能突变。PWM波经过电机线圈的滤波作用,就变成了平滑的直流电。占空比越高,平均电压越高,电机转得越快。

4.4 电机速度控制(占空比调节)

控制电机速度,说白了就是改占空比。在STM32里,改占空比就是改捕获/比较寄存器(CCR)的值。

// 设置占空比函数
void Motor_SetSpeed(uint8_t duty)
{
    // duty范围:0~100
    // 对应CCR值:0~999(因为ARR=999)
    uint16_t ccr_value = (uint16_t)(duty * 10);
    TIM3->CCR1 = ccr_value;
}

嗯,这里要注意:占空比不能突变。我见过有人直接让电机从0%跳到100%,结果电流瞬间飙升,把MOS管烧了。正确的做法是:

  • 启动时:占空比从0逐渐增加到目标值(软启动)
  • 停止时:占空比从当前值逐渐减到0(软停止)
  • 变速时:每次只改变5%~10%,中间加一点延时

避坑指南:我曾经在量产测试时发现,同一批电机,同样的占空比,转速居然不一样。后来查了半天,发现是电机个体差异导致的。解决办法:加一个闭环控制,用霍尔传感器测实际转速,然后微调占空比。

4.5 呼吸灯效果实现

呼吸灯,说白了就是让LED的亮度像呼吸一样慢慢变亮再慢慢变暗。这个效果在窗帘电机上有什么用?嗯,其实没什么实际用途。但用来展示PWM控制能力,效果非常直观。

实现思路很简单:

  1. 定时器产生PWM波,驱动LED
  2. 每隔一段时间改变占空比
  3. 占空比从0增加到100%,再从100%减到0%
  4. 循环往复
// 呼吸灯主循环
void BreathLight(void)
{
    uint8_t duty = 0;
    int8_t step = 1;    // 步进方向:1为增加,-1为减少
    
    while(1)
    {
        Motor_SetSpeed(duty);
        delay_ms(10);    // 每10ms改变一次占空比
        
        duty += step;
        
        // 到达边界时反转方向
        if(duty >= 100)
        {
            step = -1;
        }
        else if(duty <= 0)
        {
            step = 1;
        }
    }
}

我的小技巧:呼吸灯要想效果好,步进值不能是固定的。人眼对亮度的感知不是线性的——低亮度时变化敏感,高亮度时变化迟钝。所以我会用指数曲线来映射占空比:

// 指数映射,让呼吸更自然
uint8_t duty_map[101] = {0, 1, 2, 4, 6, 9, 13, ...};

这样看起来,呼吸就柔和多了。

好了,这一章的内容就到这里。定时器和PWM是电机控制的基石,你把这些搞明白了,后面讲PID控制、速度闭环什么的,就轻松多了。下一章我们聊聊电机驱动电路——嗯,那又是另一个有意思的话题。