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,兼顾噪音和效率。

那分辨率不够怎么办?有几种办法:

  1. 使用更高时钟频率的MCU——简单粗暴,但成本上去了。
  2. 使用HRTIM(高分辨率定时器)——有些工业MCU带这个外设,分辨率能到几百皮秒级别。
  3. 使用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算法算出来的电流值就是错的。到时候见。