4、PWM输出原理:PWM模式、占空比与频率计算、互补输出与死区插入
PWM,说白了就是让单片机输出一串宽度可调的脉冲。你想想看,控制电机转速、调节LED亮度、驱动舵机,哪个离得开它?我最早接触PWM时,以为就是调调占空比那么简单。后来做电机驱动项目,才发现死区、互补输出这些坑,踩一个就够你折腾好几天。
这一节,咱们把PWM的底裤扒干净。从模式选择到频率计算,再到高级定时器才有的互补输出和死区插入,一步到位。
4.1 PWM模式:边沿对齐 vs 中央对齐
STM32的定时器支持两种PWM模式,我习惯叫它们「边沿对齐」和「中央对齐」。名字听着玄乎,其实看波形就懂了。
| 特性 | PWM模式1(边沿对齐) | PWM模式2(中央对齐) |
|---|---|---|
| 计数方式 | 向上计数(0 → ARR) | 向上/向下计数(0 → ARR → 0) |
| 输出波形 | 电平在CNT=CCR时翻转一次 | 电平在CNT=CCR时翻转两次 |
| 频率 | f = 定时器时钟 / (ARR+1) | f = 定时器时钟 / (2 × ARR) |
| 对称性 | 不对称,上升沿对齐 | 对称,波形中心对齐 |
| 适用场景 | 普通电机、LED调光 | H桥、三相逆变、高精度控制 |
我的习惯:普通应用直接用边沿对齐,简单高效。但做电机FOC控制时,我必用中央对齐模式——谐波更少,电流采样时机也更好安排。
4.2 占空比与频率计算
这两个参数是PWM的灵魂。频率决定了PWM的「快慢」,占空比决定了「强弱」。咱们直接上公式。
频率计算
// 定时器时钟 = 72 MHz(以STM32F1为例)
// 预分频器 PSC = 71 → 计数时钟 = 72M / (71+1) = 1 MHz
// 自动重装载 ARR = 999 → PWM频率 = 1M / (999+1) = 1 kHz
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC
TIM_TimeBaseStructure.TIM_Period = 999; // ARR
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
占空比设置
// 占空比 = CCR / (ARR+1) × 100%
// 想要50%占空比 → CCR = 500
TIM_SetCompare1(TIM2, 500); // 通道1输出50%占空比
// 想要25%占空比 → CCR = 250
TIM_SetCompare1(TIM2, 250);
核心公式:
- PWM频率 = 定时器时钟 / ((PSC+1) × (ARR+1))
- 占空比 = CCR / (ARR+1) × 100%
- 分辨率 = 1 / (ARR+1) × 100%
嗯,这里要注意。ARR越大,分辨率越高,但频率就越低。我在项目中遇到过需要20kHz PWM频率、同时要0.1%分辨率的情况。算下来ARR得999,PSC得3.5——没法整除。最后只能妥协到0.12%分辨率,ARR设799。
避坑指南:我曾经因为PSC和ARR算错,导致电机发出刺耳啸叫。后来养成习惯:先定频率,再算PSC,最后用ARR调分辨率。算完一定用示波器确认。
4.3 互补输出与死区插入
这部分是高级定时器的看家本领。做H桥驱动、半桥逆变、电机控制,必须用互补输出。为什么?因为一个通道要同时控制上下两个MOS管。
互补输出是什么?
简单说,就是定时器的一个通道,自动生成两路相反的PWM信号。一路叫OCx,另一路叫OCxN。你设一个CCR值,两路波形自动互补。
// 使能互补输出(以TIM1为例)
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 主输出使能
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // 互补输出使能
TIM_OCInitStructure.TIM_Pulse = 500; // CCR值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 主输出高有效
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; // 互补输出高有效
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset; // 空闲时低电平
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
死区插入——为什么需要它?
你想想看,上下两个MOS管如果同时导通,电源直接短路,瞬间冒烟。但实际电路中,MOS管关断比开通慢。如果互补信号完美对齐,就会出现「上管还没关死,下管已经开了」的尴尬局面。
死区就是故意插入一段「上下都关断」的时间。STM32高级定时器通过TIM_BDTR寄存器来配置。
// 死区时间配置(以TIM1为例)
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
// 死区时间 = DTG[7:0] × t_dts
// 假设 t_dts = 125ns(时钟8MHz)
// 想要1μs死区 → DTG = 1μs / 125ns = 8
TIM_BDTRInitStructure.TIM_OSSR = TIM_OSSR_Enable; // 运行模式关闭状态
TIM_BDTRInitStructure.TIM_OSSI = TIM_OSSI_Enable; // 空闲模式关闭状态
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; // 锁定级别
TIM_BDTRInitStructure.TIM_DeadTime = 8; // 死区时间 = 8 × 125ns = 1μs
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; // 刹车功能
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
经验值:死区时间不是越大越好。我做过一个电机驱动项目,死区设了3μs,结果电机效率掉了5%。后来优化到500ns,效率回来了,MOS管也不发热。一般建议:死区 = MOS管关断延迟 × 1.5 ~ 2倍。
4.4 知识结构总览
下面这张图,把PWM输出的核心脉络串起来了。从模式选择到参数计算,再到高级功能,一目了然。
4.5 实战要点总结
- 模式选型:普通应用用边沿对齐,高精度控制用中央对齐
- 频率与分辨率权衡:ARR越大分辨率越高,但频率会降低。根据实际需求折中
- 死区设置:参考MOS管数据手册的关断延迟,留1.5~2倍余量
- 互补输出:只有高级定时器(TIM1、TIM8)支持,通用定时器不行
- 刹车功能:配合死区使用,紧急情况下强制关闭所有输出
血的教训:我曾经在调试电机驱动板时,忘记使能TIM_CtrlPWMOutputs(TIM1, ENABLE)这个函数。结果折腾了两小时,PWM死活没输出。后来查手册才发现,高级定时器的互补输出通道需要额外使能主输出开关。嗯,这个坑我替你们踩过了。