第4章 定时器与PWM输出:STM32定时器配置、PWM模式选择、输出通道映射
好,咱们进入实战环节了。前面讲了PWM的原理,说白了就是让高低电平按一定比例出现。那在STM32上怎么实现?核心就是定时器。我刚开始学的时候,觉得定时器就是个计数器,后来才发现,这玩意儿简直就是个微型CPU,能干的事太多了。
4.1 STM32定时器家族
STM32的定时器分三类:基本定时器、通用定时器、高级定时器。做PWM输出,最常用的是通用定时器和高级定时器。
| 定时器类型 | PWM通道数 | 主要用途 |
|---|---|---|
| 基本定时器(TIM6/TIM7) | 0 | 纯计时,不能输出PWM |
| 通用定时器(TIM2~TIM5) | 4 | 标准PWM输出,够用了 |
| 高级定时器(TIM1/TIM8) | 4+互补通道 | 带死区控制的PWM,电机控制专用 |
做按摩仪,通用定时器完全够用。我做过一个项目,用TIM3的四个通道分别控制四个按摩头,效果很好。没必要一上来就上高级定时器,除非你要做电机控制。
4.2 定时器配置三步走
配置定时器输出PWM,其实就三步。我习惯这么记:分频、重装、模式。
第一步:时钟分频
定时器的时钟源一般是系统时钟。比如STM32F103,APB1上的定时器时钟是72MHz。这个频率太高了,直接计数的话,一个数就是13.8ns,太精细了。我们需要分频。
// 设置预分频器,72MHz / 72 = 1MHz
TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // 注意:实际值是Prescaler+1
这里有个坑,我刚开始就踩过。分频值写的是71,实际分频是72。因为寄存器是从0开始计数的。你想想看,如果写0,那就是不分频,1分频。所以想要72分频,就得写71。
第二步:自动重装值
自动重装值决定了PWM的周期。比如我们想要20kHz的PWM,时钟已经是1MHz了,那重装值就是:
// 1MHz / 20000 = 50
TIM_TimeBaseInitStructure.TIM_Period = 49; // 同样,实际值是49+1=50
这样算下来,PWM频率就是1MHz / 50 = 20kHz。嗯,这里要注意,重装值也不能太大,16位定时器最大是65535。如果你要极低频率的PWM,比如1Hz,那就得把分频设得很大。
第三步:配置PWM模式
这一步最关键。STM32的PWM有两种模式:PWM1和PWM2。区别很简单:
- PWM1模式:计数值小于比较值时输出有效电平,大于时输出无效电平
- PWM2模式:正好反过来
我个人习惯用PWM1模式,配合极性设置,逻辑比较直观。你看代码:
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 25; // 占空比50%,因为Period=49
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
这里TIM_Pulse就是比较值。当计数器从0数到49,在25这个点翻转电平。占空比就是25/50=50%。
核心公式:
PWM频率 = 定时器时钟 / (Prescaler+1) / (Period+1)
占空比 = Pulse / (Period+1) × 100%
4.3 输出通道映射
定时器配置好了,PWM信号从哪个引脚出来?这就涉及到通道映射了。每个定时器的每个通道都对应固定的GPIO引脚。
以TIM3为例:
| 通道 | 对应引脚 | 复用功能 |
|---|---|---|
| 通道1 | PA6 | TIM3_CH1 |
| 通道2 | PA7 | TIM3_CH2 |
| 通道3 | PB0 | TIM3_CH3 |
| 通道4 | PB1 | TIM3_CH4 |
配置GPIO时,要设置为复用推挽输出。我见过有人设成普通推挽,结果PWM死活出不来。原因就是没开复用功能。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 将PA6映射到TIM3_CH1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3);
我的小技巧:画原理图时,我会把定时器通道和引脚的关系列个表贴在旁边。这样Layout时就不会搞混。曾经有一次,我为了省事,随便选了个引脚,结果发现那个引脚没有定时器复用功能,只能重新改板子,教训深刻。
4.4 完整配置示例
好了,把上面这些串起来,就是一个完整的PWM输出配置。我写个标准流程给你看:
void PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3);
// 3. 配置定时器
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72分频
TIM_TimeBaseStructure.TIM_Period = 49; // 50个计数周期
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 4. 配置PWM通道
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 25; // 50%占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
// 5. 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
注意:如果你用的是高级定时器TIM1或TIM8,别忘了还要调用TIM_CtrlPWMOutputs(TIM1, ENABLE)来使能主输出。这个坑我踩过,配置了半天没波形,最后发现是主输出没开。
4.5 动态调整占空比
按摩仪需要实时调整力度,也就是占空比。怎么调?直接修改比较寄存器就行:
// 设置占空比为30%
TIM_SetCompare1(TIM3, 15); // 15/50 = 30%
// 设置占空比为80%
TIM_SetCompare1(TIM3, 40); // 40/50 = 80%
这里要注意,修改比较值时,最好在定时器中断里做,或者确保修改时定时器正在运行。否则可能会出现一个完整的PWM周期内占空比突变的情况。不过对于按摩仪这种应用,这点小波动根本感觉不到,放心改就行。
好了,这一章的内容就这些。说白了就是:分频定频率,比较定占空比,映射定引脚。记住这三点,STM32的PWM配置你就掌握了。下一章我们讲怎么用PWM控制按摩电机,到时候会用到这里的所有知识。