3、PWM生成与定时器配置:高级定时器的PWM输出模式、互补PWM与死区时间插入、PWM频率与分辨率计算、占空比更新策略
好,咱们进入电机控制里最核心、也最绕不开的一环——PWM生成。
说实话,我见过不少新手工程师,代码写得挺溜,一上电机就冒烟。十有八九,问题出在PWM配置上。不是频率算错了,就是死区没设对,再不然就是占空比更新时机没卡准。
今天这一讲,咱们就把高级定时器这块硬骨头啃下来。你想想看,电机转得快不快、稳不稳、烫不烫,全看PWM伺候得怎么样。
3.1 高级定时器的PWM输出模式
工业MCU里,高级定时器(比如STM32的TIM1/TIM8)跟普通定时器最大的区别在哪?
我个人觉得,就两点:互补输出和刹车功能。这两样,是驱动三相电机必不可少的。
PWM输出模式,说白了就是定时器怎么把计数值跟比较值做对比,然后翻转电平。常用的有两种:
- 边沿对齐模式:计数器从0数到ARR,再跳回0。简单直观,适合单相或低端应用。
- 中央对齐模式:计数器先向上数到ARR,再向下数回0。这种模式产生的PWM对称性好,谐波小,电机运行更平稳。我做的伺服驱动项目,基本都用这个模式。
配置的时候,有个小细节要注意:
边沿对齐模式下,更新事件发生在计数器溢出时。
中央对齐模式下,更新事件可以发生在向上计数到ARR时,也可以发生在向下计数到0时,甚至两者都触发。
嗯,这里要提醒一下:如果你用中央对齐模式,并且开启了“重复计数”功能,那更新事件的频率会降低。我之前有个同事,调了半天电机转速不对,最后发现是重复计数器设成了2,导致更新频率减半。这种坑,踩过一次就记住了。
3.2 互补PWM与死区时间插入
互补PWM,就是一对信号,一个高另一个就低,反之亦然。用在H桥或三相逆变器里,控制上下桥臂的MOS管。
但问题来了——MOS管开关不是瞬间完成的。关断需要时间,导通也需要时间。如果上管还没完全关断,下管就导通了,那结果就是——直通短路,管子瞬间冒烟。
怎么解决?插入死区时间。
死区时间,就是故意让上下桥臂都关断的那一小段空白期。我习惯把它叫做“安全间隔”。
配置死区时间,通常通过定时器的BDTR寄存器(刹车和死区寄存器)来设置。比如STM32里,死区时间由DTG[7:0]位控制,具体公式要看数据手册,不同MCU略有差异。
我的经验:死区时间不是越大越好。设大了,波形失真,电机效率下降,还会发热。设小了,又有短路风险。一般建议从MOS管数据手册里的“关断延迟时间”+“下降时间”+一点余量来算。比如管子关断延迟50ns,下降时间30ns,那死区至少设100ns起步。
我曾经在一个高压项目里,死区设了500ns,觉得挺安全。结果电机一跑高速,电流波形毛刺特别多,效率掉了5%。后来把死区优化到200ns,问题就解决了。所以,死区时间要跟开关频率、负载电流匹配着调。
配置代码示例(以STM32 HAL库为例):
// 配置死区时间为 200ns(假设定时器时钟 72MHz)
// 死区时间计算公式:DT = DTG[7:0] * Tdtg
// 这里 Tdtg = 1/72MHz ≈ 13.89ns
// 200ns / 13.89ns ≈ 14.4,取整 DTG = 14
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_ENABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_ENABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 14; // 约 194ns
sBreakDeadTimeConfig.BreakState = TIM_BREAK_ENABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.BreakFilter = 0;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
3.3 PWM频率与分辨率计算
PWM频率和分辨率,是一对冤家。你频率设高了,分辨率就下来了;想要高分辨率,频率就得降。
为什么?因为定时器的计数时钟是固定的。假设定时器时钟72MHz,你想输出20kHz的PWM:
- 边沿对齐模式:ARR = 72MHz / 20kHz - 1 = 3599
- 分辨率 = log2(3600) ≈ 11.8位
如果你想要16位的分辨率,那ARR就得是65535,PWM频率就变成了:72MHz / 65536 ≈ 1.1kHz。这个频率对于电机控制来说太低了,会听到明显的啸叫声。
注意:电机控制常用的PWM频率在8kHz到20kHz之间。低于16kHz人耳能听到,会有噪音。高于20kHz,开关损耗会明显增加。我个人习惯用16kHz,兼顾噪音和效率。
那分辨率不够怎么办?有几种办法:
- 使用更高时钟频率的MCU——简单粗暴,但成本上去了。
- 使用HRTIM(高分辨率定时器)——有些工业MCU带这个外设,分辨率能到几百皮秒级别。
- 使用PWM重映射或双更新模式——通过软件技巧提高等效分辨率。
我记得有个项目,客户要求电机在极低速下运行得非常平稳。普通PWM分辨率根本不够,最后我用了HRTIM的延迟插入功能,把分辨率从12位提升到了16位,效果立竿见影。
3.4 占空比更新策略
占空比更新,看似简单——改一下比较寄存器的值就行了。但时机不对,电机就会抖。
关键点在于:什么时候更新比较值?
如果PWM周期内任意时刻都能更新,那一个周期里可能前半段是一个占空比,后半段是另一个占空比。这种“畸形”的PWM波形,会让电机电流产生畸变,严重时会导致转矩脉动。
正确的做法是:在PWM周期更新事件发生时,同步更新比较值。
高级定时器都支持“预装载”功能。你写入比较寄存器的值,不会立即生效,而是等到下一个更新事件(比如计数器溢出)才被锁存到实际工作的寄存器里。
核心原则:永远在更新事件中断或DMA中修改占空比,不要在PWM波形的中间时刻改。
代码示例(在更新中断中更新占空比):
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM1)
{
// 从FOC算法计算出的新占空比
uint16_t new_duty = pid_output_to_duty(current_pid_output);
// 更新比较值(预装载寄存器)
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_duty);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, new_duty);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, new_duty);
}
}
另外,还有一种更高效的策略——DMA更新。把占空比数据放在一个数组里,用定时器的更新事件触发DMA,自动把数据搬运到比较寄存器。这样CPU几乎不用干预,适合高速电机控制。
我曾经在一个48V/500W的BLDC项目里,用DMA方式更新占空比,PWM频率跑到了40kHz,CPU负载才不到10%。要是用中断方式,估计CPU就忙着进中断了。
避坑指南:使用DMA更新时,记得把DMA模式设为“循环模式”,并且确保数据缓冲区是双缓冲的。否则,你可能会遇到“数据被覆盖”或“更新不及时”的问题。我曾经因为DMA缓冲区没处理好,电机在高速时突然抖动了一下,查了两天才发现是DMA读到了脏数据。
最后总结一下这一节的核心:
- 高级定时器的PWM模式,电机控制首选中央对齐模式。
- 死区时间要精确计算,不能拍脑袋设。
- 频率和分辨率要平衡,16kHz是个不错的起点。
- 占空比更新,一定要同步到更新事件,用DMA更高效。
下一讲,咱们聊聊电流采样与ADC触发同步。这个跟PWM配置是紧密相关的,采样时机没卡好,FOC算法算出来的电流值就是错的。到时候见。