2. STM32定时器与PWM输出:高级定时器配置、PWM模式设置、占空比调节、死区时间插入

电机控制的核心,说白了就是控制功率管的通断。而STM32的高级定时器,就是干这个活的专用硬件。我刚开始做电机驱动时,总觉得用普通定时器也能凑合,结果一跑起来就发现问题了——没有互补输出、没有死区插入,板子上的MOS管烧了一排。嗯,从那以后我再也不敢轻视高级定时器的配置了。

2.1 高级定时器 vs 通用定时器:别选错外设

STM32的定时器家族里,高级定时器(TIM1/TIM8)和通用定时器(TIM2~TIM5)最大的区别在哪?我列个表你就明白了:

功能特性 高级定时器 (TIM1/TIM8) 通用定时器 (TIM2~TIM5)
互补PWM输出 支持(CHx和CHxN) 不支持
死区时间插入 硬件自动插入 需软件模拟
刹车功能 支持(BKIN引脚) 不支持
编码器接口 支持 支持
重复计数 支持(RCR寄存器) 不支持

关键结论:做电机控制,必须用高级定时器。通用定时器没有互补输出通道,你没法直接驱动H桥或三相逆变器的上下管。

2.2 高级定时器核心寄存器:你得认识这几个

配置高级定时器,其实就是在操作几个关键寄存器。我个人习惯先把它们理清楚,再动手写代码:

  • TIMx_ARR(自动重装载寄存器):决定PWM周期。比如你想输出20kHz的PWM,ARR的值就是定时器时钟频率除以20kHz再减1。
  • TIMx_CCRx(捕获/比较寄存器):决定占空比。CCR的值除以ARR,就是占空比百分比。
  • TIMx_CCER(捕获/比较使能寄存器):控制通道输出极性、使能互补输出。
  • TIMx_BDTR(刹车和死区寄存器):死区时间、刹车配置都在这里。
  • TIMx_CR1(控制寄存器1):使能计数器、配置计数方向。

你想想看,这么多寄存器,其实核心逻辑就一条:ARR定周期,CCR定占空比,BDTR定死区。记住这个,配置起来就顺了。

2.3 PWM模式设置:边沿对齐 vs 中央对齐

高级定时器支持两种PWM模式,我分别说说:

2.3.1 边沿对齐模式(PWM模式1/2)

这是最常用的模式。计数器从0递增到ARR,然后跳回0。CCR决定翻转点。模式1是:CNT < CCR时输出有效电平,CNT >= CCR时输出无效电平。模式2正好相反。

// 配置TIM1为PWM模式1,通道1
TIM1->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;  // 模式1:110
TIM1->CCER |= TIM_CCER_CC1E;                          // 使能通道1输出

2.3.2 中央对齐模式(PWM模式1/2/3)

计数器先递增到ARR,再递减回0。一个周期内CCR会触发两次翻转。这种模式产生的PWM对称性好,谐波含量低。我在做永磁同步电机FOC控制时,电流环采样就喜欢用中央对齐——采样点正好在计数器的顶点和底点,电流纹波最小。

我的经验:方波驱动用边沿对齐就够了;正弦波驱动或FOC控制,建议用中央对齐模式。虽然中央对齐的PWM频率只有边沿对齐的一半(因为一个周期要来回走两趟),但电流质量好很多。

2.4 占空比调节:动态更新要注意时机

调节占空比就是修改CCR的值。但有个坑——什么时候改?

  • 预装载功能:CCR的值可以立即生效,也可以等到下一个更新事件(UEV)才生效。我建议开启预装载,避免PWM周期中间突然跳变,导致电机抖动。
  • 更新事件触发:计数器溢出(或设置为0)时,预装载的CCR值才会真正写入影子寄存器。
// 开启预装载
TIM1->CCMR1 |= TIM_CCMR1_OC1PE;

// 动态修改占空比(在中断或主循环中调用)
void set_duty(uint16_t duty) {
    TIM1->CCR1 = duty;  // 写入预装载寄存器,下一个UEV生效
}

我曾经踩过的坑:有一次没开预装载,直接在中断里改CCR。结果PWM波形在周期中间突然跳变,电机发出刺耳的啸叫声。后来用示波器一看,脉冲宽度在周期内突变,电流波形全是毛刺。所以,动态调占空比,一定要开预装载。

2.5 死区时间插入:保护MOS管的关键

死区时间,就是上下管同时关断的那段空白期。为什么需要它?因为功率管从导通到关断需要时间,如果上管还没完全关断,下管就导通了,那就直通短路——MOS管瞬间冒烟。

死区时间由TIMx_BDTR寄存器的DTG[7:0]位决定。STM32的死区计算有点绕,我直接给公式:

DTG[7:0]范围 死区时间计算公式 示例(t_dts=125ns @ 8MHz)
0x00 ~ 0x7F DT × t_dts 0x40 → 64 × 125ns = 8μs
0x80 ~ 0xBF (64 + DT[5:0]) × 2 × t_dts 0xA0 → (64+32) × 250ns = 24μs
0xC0 ~ 0xDF (32 + DT[4:0]) × 8 × t_dts 0xD0 → (32+16) × 1μs = 48μs
0xE0 ~ 0xFF (32 + DT[4:0]) × 16 × t_dts 0xF0 → (32+16) × 2μs = 96μs
// 配置死区时间:假设t_dts = 125ns,需要1μs死区
// 1μs / 125ns = 8,所以DTG = 0x08
TIM1->BDTR = TIM_BDTR_DTG_0 | TIM_BDTR_DTG_3;  // DTG = 0x08
TIM1->BDTR |= TIM_BDTR_MOE;                     // 使能主输出

我建议:死区时间不是越大越好。太大了,电机效率下降,波形失真;太小了,有直通风险。一般取功率管开关时间的1.5~2倍。比如IRFP460的关断延迟约200ns,我通常设500ns~1μs。具体值最好用示波器实测,调到临界值再留点余量。

2.6 完整配置流程:从时钟到波形

下面是我做项目时常用的配置步骤,按顺序来基本不会出错:

  1. 使能时钟:RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
  2. 配置时基:设置PSC(预分频器)和ARR(自动重装载),确定PWM频率。
  3. 配置输出比较模式:CCMRx寄存器选择PWM模式1或2。
  4. 配置死区时间:BDTR寄存器设置DTG和使能MOE。
  5. 使能互补输出:CCER寄存器使能CHx和CHxN,设置极性。
  6. 开启预装载:CCMRx的OCxPE位置1。
  7. 使能计数器:CR1的CEN位置1。
  8. 配置刹车输入(可选):BDTR的BKE、BKP等。
void TIM1_PWM_Init(uint16_t arr, uint16_t psc, uint16_t deadtime) {
    // 1. 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
    
    // 2. 时基配置
    TIM1->PSC = psc;
    TIM1->ARR = arr;
    TIM1->CR1 |= TIM_CR1_CEN;  // 使能计数器
    
    // 3. PWM模式1,通道1
    TIM1->CCMR1 |= (6 << 4);   // OC1M = 110 (PWM模式1)
    TIM1->CCMR1 |= TIM_CCMR1_OC1PE;  // 预装载使能
    
    // 4. 死区配置
    TIM1->BDTR = deadtime;
    TIM1->BDTR |= TIM_BDTR_MOE;  // 主输出使能
    
    // 5. 使能互补输出
    TIM1->CCER |= TIM_CCER_CC1E | TIM_CCER_CC1NE;
    
    // 6. 初始占空比50%
    TIM1->CCR1 = arr / 2;
}

2.7 知识结构图:高级定时器PWM配置全景

下面这张图把整个配置流程串起来了,你可以对照着看:

高级定时器PWM配置全景图 ① 使能时钟 (RCC) ② 时基配置 (PSC+ARR) ③ PWM模式 (CCMRx) ④ 死区时间 (BDTR) ⑤ 输出使能 (CCER) ⑥ 启动计数器 (CR1) 关键寄存器速查 • ARR → PWM周期 • CCR → 占空比 • DTG → 死区时间 • MOE → 主输出使能 • CC1E → CH1使能 • CC1NE → CH1N使能 • OC1PE → 预装载 • CEN → 计数器使能 ⚠ 常见错误 忘记使能MOE → 无输出 死区太小 → 直通烧管 未开预装载 → 波形抖动 配置顺序:时钟 → 时基 → 模式 → 死区 → 输出 → 启动 建议每次修改后都用示波器验证波形,特别是死区时间

2.8 调试技巧:用示波器验证波形

配置完代码,别急着跑电机。先拿示波器看波形,我一般检查这几个点:

  • 频率对不对:测量PWM周期,和ARR计算值对比。如果不对,检查PSC和时钟源。
  • 占空比准不准:CCR设为ARR的一半,看是不是50%。如果不是,检查CCER的极性设置。
  • 死区时间够不够:看CH和CHN波形之间有没有明显的空白期。用示波器的光标测量死区宽度。
  • 互补输出反不反:CH高电平时,CHN应该是低电平。如果反了,交换CCER的极性位。

我曾经犯过的错:有一次死区时间设了2μs,但示波器一看只有200ns。查了半天,发现是BDTR寄存器的DTG位写错了——我直接写了0x02,但忘了t_dts是125ns还是1μs。后来养成习惯,每次配置完先算一遍:死区时间 = DTG值 × t_dts,再上示波器验证。