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 完整配置流程:从时钟到波形
下面是我做项目时常用的配置步骤,按顺序来基本不会出错:
- 使能时钟:RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
- 配置时基:设置PSC(预分频器)和ARR(自动重装载),确定PWM频率。
- 配置输出比较模式:CCMRx寄存器选择PWM模式1或2。
- 配置死区时间:BDTR寄存器设置DTG和使能MOE。
- 使能互补输出:CCER寄存器使能CHx和CHxN,设置极性。
- 开启预装载:CCMRx的OCxPE位置1。
- 使能计数器:CR1的CEN位置1。
- 配置刹车输入(可选):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配置全景
下面这张图把整个配置流程串起来了,你可以对照着看:
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,再上示波器验证。