3. 定时器原理与PWM输出:STM32定时器架构与电机开环调速
定时器这东西,说白了就是单片机的“心跳节拍器”。你想想看,无论是精确延时、测量脉冲宽度,还是生成PWM波去控制电机,背后都离不开它。我刚开始学STM32时,总觉得定时器模块太复杂——基本、通用、高级,光名字就让人头大。后来做项目踩过几次坑才明白,其实它们的核心逻辑是相通的。
3.1 STM32定时器家族:三兄弟各司其职
STM32的定时器分三类,我习惯把它们比作“三兄弟”:
| 类型 | 位数 | 主要功能 | 典型应用场景 |
|---|---|---|---|
| 基本定时器 (TIM6/TIM7) | 16位 | 仅基础计时、触发DAC | 产生固定时间中断、做系统心跳 |
| 通用定时器 (TIM2~TIM5) | 16/32位 | 计时、输入捕获、输出比较、PWM | 普通PWM输出、脉冲测量、舵机控制 |
| 高级定时器 (TIM1/TIM8) | 16位 | 全部通用功能 + 互补输出、刹车、死区 | 电机FOC控制、三相逆变、H桥驱动 |
核心区别一句话:基本定时器只能计时;通用定时器能输出PWM;高级定时器在通用基础上加了电机控制专用的“安全锁”——死区插入和刹车功能。
我在项目中遇到过一件事:用通用定时器驱动直流电机,一切正常。后来换成无刷电机,发现换相时上下桥臂直通短路,烧了两个MOS管。查了半天才意识到——高级定时器的死区功能不是摆设。从那以后,只要涉及电机驱动,我至少用通用定时器,带H桥的一定上高级定时器。
3.2 定时器核心工作原理
不管哪类定时器,内部都有一套相似的“计数器-比较器”机制。我画个图帮你理解:
嗯,这里要注意:ARR(自动重装载寄存器)决定PWM周期,CCR(捕获/比较寄存器)决定占空比。CNT从0数到ARR,每数一次就和CCR比一次——比CCR小输出高电平,比CCR大输出低电平。这就是PWM的底层逻辑。
3.3 PWM模式配置:手把手教你写代码
配置PWM输出,我一般走四步。以通用定时器TIM2输出到PA0为例:
- 使能定时器和GPIO时钟
- 配置GPIO为复用推挽输出
- 设置定时器时基(PSC、ARR)
- 配置PWM模式1/2,设置CCR
我的习惯:先把PSC和ARR算好,再调CCR。这样频率固定,只改占空比,调试起来思路清晰。
来看代码,我用的是STM32标准库,HAL库思路一样:
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置PA0为TIM2_CH1复用输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 定时器时基配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_TimeBaseStruct.TIM_Prescaler = 71; // PSC = 71
TIM_TimeBaseStruct.TIM_Period = 999; // ARR = 999
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
// 4. PWM模式配置
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 500; // CCR = 500
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStruct);
// 5. 启动定时器
TIM_Cmd(TIM2, ENABLE);
曾经踩过的坑:高级定时器TIM1/TIM8配置完后,必须调用 TIM_CtrlPWMOutputs(TIM1, ENABLE) 才能输出。我第一次用TIM1时,查了半天寄存器,最后发现是这行没写——输出引脚一直是低电平。
3.4 占空比与频率计算:算清楚再动手
频率和占空比的计算公式其实很简单:
| 参数 | 公式 | 说明 |
|---|---|---|
| 定时器时钟 | TIM_CLK = SYSCLK / (PSC + 1) | 假设SYSCLK=72MHz,PSC=71 → 1MHz |
| PWM频率 | F_PWM = TIM_CLK / (ARR + 1) | ARR=999 → 1MHz / 1000 = 1kHz |
| 占空比 | Duty = CCR / (ARR + 1) × 100% | CCR=500 → 50% |
| 分辨率 | Resolution = 1 / (ARR + 1) × 100% | ARR=999 → 0.1% |
举个例子:你想让电机以10kHz频率运行,占空比从0%到100%可调,步进0.1%。
- 先定ARR = 999(分辨率0.1%)
- 计算TIM_CLK = 10kHz × (999+1) = 10MHz
- PSC = 72MHz / 10MHz - 1 = 6.2 → 取整为6
- 实际TIM_CLK = 72MHz / 7 ≈ 10.286MHz
- 实际频率 = 10.286MHz / 1000 ≈ 10.286kHz
注意:PSC和ARR都是整数,算出来有小数时只能取整。实际频率会有微小偏差,但只要偏差在5%以内,电机调速基本不受影响。我一般用PSC把时钟降到1MHz附近,再用ARR调频率,这样计算方便。
3.5 电机开环调速实战:让电机转起来
开环调速,说白了就是“我发指令,电机执行,不反馈”。你给多少占空比,电机就跑多快——当然,负载变化时速度会飘,但入门阶段够用了。
我常用的调速策略是软启动:
// 软启动:占空比从0逐步增加到目标值
uint16_t target_duty = 800; // 目标占空比 80%
uint16_t current_duty = 0;
while (current_duty < target_duty) {
current_duty += 10; // 每次增加1%
TIM_SetCompare1(TIM2, current_duty);
delay_ms(50); // 每50ms调整一次
}
为什么软启动?直接给100%占空比,电机启动电流可能达到额定电流的5-10倍。我有个项目没做软启动,电源直接保护了。后来加了500ms的软启动斜坡,再没出过问题。
完整的开环调速流程:
- 初始化定时器PWM输出(频率1kHz~20kHz,电机常用10kHz)
- 设置CCR = 0(电机停止)
- 用户输入目标速度(映射到0~ARR的占空比值)
- 执行软启动,逐步增加CCR
- 运行中可随时修改CCR改变速度
- 停止时同样软减速,避免急停反电动势损坏MOS管
安全提醒:电机驱动电路一定要加续流二极管!我刚开始做时忘了加,关断瞬间的感应电压直接击穿了MOS管。另外,调试时先用小功率电机,确认PWM波形正常再上大电机。
嗯,到这里你已经掌握了定时器输出PWM的核心方法。从基本定时器的计时,到通用定时器的PWM,再到高级定时器的电机专用功能——这套知识在后续的传感器读取、中断协同中都会反复用到。记住:ARR定周期,CCR定占空比,PSC定时钟,三个寄存器玩转了,PWM就没什么秘密了。