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 定时器核心工作原理

不管哪类定时器,内部都有一套相似的“计数器-比较器”机制。我画个图帮你理解:

STM32定时器PWM生成原理图 STM32定时器PWM生成原理 时钟源 (CK_INT) PSC 预分频器 CNT 计数器 ARR 自动重装载 CCR 捕获/比较 输出控制 GPIO 引脚 工作流程: 1. 时钟经PSC分频后驱动CNT计数 2. CNT从0递增到ARR后溢出归零 3. CNT与CCR比较,决定输出电平翻转

嗯,这里要注意:ARR(自动重装载寄存器)决定PWM周期,CCR(捕获/比较寄存器)决定占空比。CNT从0数到ARR,每数一次就和CCR比一次——比CCR小输出高电平,比CCR大输出低电平。这就是PWM的底层逻辑。

3.3 PWM模式配置:手把手教你写代码

配置PWM输出,我一般走四步。以通用定时器TIM2输出到PA0为例:

  1. 使能定时器和GPIO时钟
  2. 配置GPIO为复用推挽输出
  3. 设置定时器时基(PSC、ARR)
  4. 配置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的软启动斜坡,再没出过问题。

完整的开环调速流程:

  1. 初始化定时器PWM输出(频率1kHz~20kHz,电机常用10kHz)
  2. 设置CCR = 0(电机停止)
  3. 用户输入目标速度(映射到0~ARR的占空比值)
  4. 执行软启动,逐步增加CCR
  5. 运行中可随时修改CCR改变速度
  6. 停止时同样软减速,避免急停反电动势损坏MOS管

安全提醒:电机驱动电路一定要加续流二极管!我刚开始做时忘了加,关断瞬间的感应电压直接击穿了MOS管。另外,调试时先用小功率电机,确认PWM波形正常再上大电机。

嗯,到这里你已经掌握了定时器输出PWM的核心方法。从基本定时器的计时,到通用定时器的PWM,再到高级定时器的电机专用功能——这套知识在后续的传感器读取、中断协同中都会反复用到。记住:ARR定周期,CCR定占空比,PSC定时钟,三个寄存器玩转了,PWM就没什么秘密了。