第三课:定时器与PWM输出——让按摩仪“动”起来

好,我们继续往下走。上一课我们把GPIO玩转了,电机能转,但说实话,那种“要么全速、要么停止”的控制方式,用在按摩仪上太粗糙了。你想想看,谁受得了按摩头一直用最大力气怼着?

这一课,我们就来解决这个问题。核心就是两个东西:定时器PWM。说白了,定时器是STM32的“心跳”,PWM则是用这个心跳去控制电机的“力道”。

定时器基本原理——芯片里的“节拍器”

定时器这东西,我刚开始学的时候觉得挺玄乎。后来想明白了,它就是一个会自己数数的计数器。

STM32的定时器分几种:基本定时器、通用定时器、高级定时器。咱们做按摩仪,用通用定时器就足够了。它的核心逻辑是这样的:

  • 有一个时钟源,比如72MHz
  • 有一个预分频器(PSC),把时钟频率降下来
  • 有一个自动重装载寄存器(ARR),设定计数上限
  • 计数器从0数到ARR,然后清零,再来一遍

嗯,这里要注意:定时器的精度,直接决定了PWM的细腻程度。我在项目中遇到过,有人把预分频设得太大,结果PWM频率低到电机“咔咔”响,那体验简直了。

核心公式:

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

比如72MHz时钟,PSC=71,ARR=999,那么溢出频率 = 72M / 72 / 1000 = 1000Hz = 1ms中断一次。

STM32定时器配置——动手写代码

我个人习惯用STM32CubeMX生成初始化代码,但关键参数还是得自己心里有数。咱们以TIM2为例,配置一个1kHz的定时器,用于PWM输出。

配置步骤其实就这几步:

  1. 使能TIM2时钟
  2. 设置预分频器PSC和自动重装载ARR
  3. 配置PWM模式
  4. 设置占空比初始值
  5. 使能定时器

来看看代码,我加了详细的注释:

// 定时器PWM初始化函数
void PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    // 1. 使能时钟
    __HAL_RCC_TIM2_CLK_ENABLE();    // TIM2时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();   // PA0作为PWM输出引脚
    
    // 2. 配置GPIO为复用推挽输出
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;      // 复用推挽
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 3. 配置定时器基础参数
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 72MHz / (71+1) = 1MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;            // 1MHz / (999+1) = 1kHz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim2);
    
    // 4. 配置PWM通道
    sConfigOC.OCMode = TIM_OCMODE_PWM1;          // PWM模式1
    sConfigOC.Pulse = 500;                        // 初始占空比50%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;   // 高电平有效
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
    
    // 5. 启动PWM输出
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

小技巧: 我一般会把PSC和ARR的计算公式写在注释里,方便以后改参数。比如要改成500Hz,直接改ARR=1999就行,不用重新算一遍。

PWM模式详解——两种模式的区别

STM32的PWM有两种模式:PWM1和PWM2。区别其实很简单:

模式 计数小于比较值 计数大于等于比较值
PWM1 有效电平(通常是高) 无效电平(低)
PWM2 无效电平(低) 有效电平(高)

说白了,PWM1是“先高后低”,PWM2是“先低后高”。我们一般用PWM1,因为更符合直觉——占空比越大,高电平时间越长,电机转速越快。

为什么会这样设计?我猜是为了适应不同的驱动电路。有些电机驱动需要反向逻辑,这时候PWM2就派上用场了。

通过PWM控制电机转速

好了,配置完了,怎么控制转速?其实就是改一个寄存器:比较值CCR

在HAL库中,用这个函数:

// 设置占空比,duty范围0-1000
void Set_Motor_Speed(uint16_t duty)
{
    if(duty > 1000) duty = 1000;  // 限幅保护
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
}

调用起来很简单:

Set_Motor_Speed(0);     // 停止
Set_Motor_Speed(250);   // 25%转速,轻柔模式
Set_Motor_Speed(500);   // 50%转速,中等力度
Set_Motor_Speed(750);   // 75%转速,强力模式
Set_Motor_Speed(1000);  // 全速,最大力度

我曾经犯过一个低级错误:在中断里频繁调用这个函数,结果导致PWM波形抖动。后来才意识到,改占空比的操作要放在主循环或者任务里,不要在中断里频繁改

避坑指南: 电机启动时,不要直接从0跳到1000。我遇到过电机直接“飞”出去的情况。正确的做法是:用for循环慢慢增加占空比,比如每次加50,延时10ms。这叫“软启动”,对电机和驱动电路都友好。

实现多级按摩力度调节

按摩仪的力度调节,说白了就是映射关系。用户按“+”键,力度加一档;按“-”键,力度减一档。

我一般这样设计:

// 力度等级定义
#define LEVEL_OFF   0   // 关闭
#define LEVEL_1     1   // 轻柔
#define LEVEL_2     2   // 适中
#define LEVEL_3     3   // 强力
#define LEVEL_4     4   // 强劲
#define LEVEL_5     5   // 最大

// 每个等级对应的占空比
const uint16_t duty_table[6] = {
    0,      // 关闭
    200,    // 20%
    400,    // 40%
    600,    // 60%
    800,    // 80%
    1000    // 100%
};

uint8_t current_level = LEVEL_OFF;

// 增加力度
void Level_Up(void)
{
    if(current_level < LEVEL_5)
    {
        current_level++;
        // 软启动:逐步增加到目标占空比
        for(uint16_t i = 0; i <= duty_table[current_level]; i += 50)
        {
            Set_Motor_Speed(i);
            HAL_Delay(10);  // 每步延时10ms
        }
    }
}

// 减小力度
void Level_Down(void)
{
    if(current_level > LEVEL_OFF)
    {
        current_level--;
        Set_Motor_Speed(duty_table[current_level]);
    }
}

这里有个细节:增加力度时用软启动,减小力度时直接跳变。为什么?因为电机减速本身就有惯性,直接跳变不会有冲击感。但加速时如果不软启动,电机会“突”地一下冲出去,用户体验很差。

嗯,这个经验是我在调试第一版按摩仪时踩坑踩出来的。当时用户反馈说“按一下加力键,按摩头像被踢了一脚”,后来加了软启动就解决了。

小结一下

这一课我们干了三件事:

  • 搞懂了定时器就是个会自己数数的计数器
  • 学会了配置PWM输出,用代码控制占空比
  • 实现了多级力度调节,还加了软启动保护

下一课,我们要开始处理用户输入了——按键和触摸。到时候,按摩仪才能真正“听话”。

对了,如果你在调试PWM时发现电机不转,先别急着怀疑代码。拿示波器看看PA0引脚有没有波形。没有波形?检查时钟使能。有波形但电机不转?检查驱动电路的电源和使能引脚。我当年就干过这种蠢事——焊好了板子,发现电机不转,查了半天,结果是驱动芯片的使能引脚没拉高。

好,这一课就到这儿。动手试试吧。