第三课:定时器与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输出。
配置步骤其实就这几步:
- 使能TIM2时钟
- 设置预分频器PSC和自动重装载ARR
- 配置PWM模式
- 设置占空比初始值
- 使能定时器
来看看代码,我加了详细的注释:
// 定时器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引脚有没有波形。没有波形?检查时钟使能。有波形但电机不转?检查驱动电路的电源和使能引脚。我当年就干过这种蠢事——焊好了板子,发现电机不转,查了半天,结果是驱动芯片的使能引脚没拉高。
好,这一课就到这儿。动手试试吧。