3、SVPWM调制技术:空间矢量脉宽调制原理、扇区判断、作用时间计算、代码实现

好,咱们今天来啃一块硬骨头——SVPWM。说实话,我刚入行那会儿,看到这个缩写就头大。什么空间矢量、什么扇区判断,感觉像天书一样。但后来我发现,这东西说白了就是一种让电机转得更顺、更高效的调制方法。你想想看,我们给电机通电,总不能一直开着全压吧?得有个巧妙的开关方式,SVPWM就是干这个的。

3.1 空间矢量调制的基本原理

先说说为什么叫“空间矢量”。传统的SPWM(正弦脉宽调制)是分别控制三相电压,让它们各自输出正弦波。但电机是个整体,三相电流合起来才产生旋转磁场。SVPWM的思路不一样——它直接盯着合成的旋转磁场矢量去控制。

我个人的理解是这样的:你把三相逆变器的六个开关管(上三下三)看成一组开关组合。每个开关状态对应一个电压矢量。六个非零矢量把平面分成六个扇区,再加上两个零矢量。我们要做的就是,用这些有限的矢量去“拼凑”出任意方向、任意大小的目标电压矢量。

核心思想:在一个PWM周期内,用相邻的两个非零矢量和零矢量,通过时间加权平均,等效出目标电压矢量。

嗯,这里要注意一点:SVPWM的直流电压利用率比SPWM高15%左右。我在做伺服驱动器项目时,就因为这个优势,把母线电压从310V降到了270V,成本省了不少。

3.2 扇区判断——第一步定位

要合成目标矢量,首先得知道它在哪个扇区。扇区判断其实不复杂,就是看目标电压在αβ坐标系下的分量关系。

假设我们已知目标电压的α轴分量Uα和β轴分量Uβ。定义三个中间变量:

B1 = Uβ
B2 = (√3/2)*Uα - (1/2)*Uβ
B3 = -(√3/2)*Uα - (1/2)*Uβ

然后根据这三个值的正负,就能确定扇区号:

N = sign(B1) + 2*sign(B2) + 4*sign(B3)
// 其中sign(x)当x>0时返回1,否则返回0

得到的N值(1~6)对应扇区I到VI。我曾经在调试时发现,如果Uα和Uβ的采样有噪声,扇区边界附近会频繁跳变。后来加了个小滞回区,问题就解决了。

N值 1 2 3 4 5 6
扇区 II VI I IV III V

小技巧:实际代码中,我习惯用查表法代替if-else判断,速度能快不少。尤其是中断服务程序里,能省一个指令周期是一个。

3.3 作用时间计算——核心算法

知道扇区后,就要算两个相邻矢量的作用时间了。以扇区I为例,相邻矢量是U4(100)和U6(110)。

计算公式如下:

// 先算三个通用变量
X = (√3)*Uβ * Ts / Udc
Y = ( (√3/2)*Uβ + (3/2)*Uα ) * Ts / Udc
Z = ( (√3/2)*Uβ - (3/2)*Uα ) * Ts / Udc

// 扇区I的作用时间
T1 = Z
T2 = Y

不同扇区的T1、T2对应关系如下:

扇区 I II III IV V VI
T1 Z Y -Z -X X -Y
T2 Y -X X Z -Y -Z

算完T1、T2后,还要做一件事——过调制处理。如果T1+T2 > Ts,说明目标电压超出了逆变器能输出的最大范围。这时候需要等比例缩小:

if (T1 + T2 > Ts) {
    T1 = T1 * Ts / (T1 + T2);
    T2 = T2 * Ts / (T1 + T2);
}

避坑指南:我曾经在过调制处理上吃过亏。当时没做限幅,结果电机在高速区运行时电流波形畸变严重,还伴随刺耳的啸叫声。后来加上限幅,世界清净了。

3.4 七段式与五段式SVPWM

作用时间算完了,接下来要安排开关序列。常见的有两种方式:

  • 七段式:每个周期内,零矢量被分成两段,分别放在开头和结尾。开关次数多,但谐波小。
  • 五段式:只用一个零矢量,开关次数少,损耗低,但谐波大一些。

我个人在大多数项目中都用七段式。虽然开关损耗稍高,但电流波形更干净,对位置环的精度有好处。只有在追求极致效率的场合(比如电动车),我才会考虑五段式。

七段式的开关顺序(以扇区I为例):

U0(000) → U4(100) → U6(110) → U7(111) → U6(110) → U4(100) → U0(000)

对应的三相占空比计算:

Ta = (Ts - T1 - T2) / 4
Tb = Ta + T1/2
Tc = Tb + T2/2

然后根据扇区,把Ta、Tb、Tc分配给A、B、C三相。这里也有个查表映射,我就不展开了。

3.5 代码实现——从理论到硬件

好了,理论说完了,咱们看看代码怎么写。下面是一个完整的SVPWM计算函数,我把它放在PWM中断里调用:

void svpwm_calc(float Ualpha, float Ubeta, uint16_t *pwmA, uint16_t *pwmB, uint16_t *pwmC) {
    float Ts = PWM_PERIOD;          // PWM周期
    float Udc = BUS_VOLTAGE;        // 母线电压
    
    // 1. 扇区判断
    float B1 = Ubeta;
    float B2 = 0.866f*Ualpha - 0.5f*Ubeta;
    float B3 = -0.866f*Ualpha - 0.5f*Ubeta;
    
    uint8_t sector = 0;
    if (B1 > 0) sector += 1;
    if (B2 > 0) sector += 2;
    if (B3 > 0) sector += 4;
    
    // 查表得到实际扇区
    const uint8_t sector_table[6] = {2, 6, 1, 4, 3, 5};
    sector = sector_table[sector];
    
    // 2. 计算X、Y、Z
    float X = 1.732f * Ubeta * Ts / Udc;
    float Y = (0.866f*Ubeta + 1.5f*Ualpha) * Ts / Udc;
    float Z = (0.866f*Ubeta - 1.5f*Ualpha) * Ts / Udc;
    
    // 3. 查表得到T1、T2
    float T1, T2;
    switch(sector) {
        case 1: T1 = Z; T2 = Y; break;
        case 2: T1 = Y; T2 = -X; break;
        case 3: T1 = -Z; T2 = X; break;
        case 4: T1 = -X; T2 = Z; break;
        case 5: T1 = X; T2 = -Y; break;
        case 6: T1 = -Y; T2 = -Z; break;
    }
    
    // 4. 过调制处理
    if (T1 + T2 > Ts) {
        float scale = Ts / (T1 + T2);
        T1 *= scale;
        T2 *= scale;
    }
    
    // 5. 计算三相占空比
    float Ta = (Ts - T1 - T2) * 0.25f;
    float Tb = Ta + T1 * 0.5f;
    float Tc = Tb + T2 * 0.5f;
    
    // 6. 根据扇区分配
    switch(sector) {
        case 1:
            *pwmA = (uint16_t)(Tb);
            *pwmB = (uint16_t)(Ta);
            *pwmC = (uint16_t)(Tc);
            break;
        case 2:
            *pwmA = (uint16_t)(Ta);
            *pwmB = (uint16_t)(Tc);
            *pwmC = (uint16_t)(Tb);
            break;
        // ... 其他扇区类似
    }
}

实际项目经验:这段代码里用了float运算,在STM32F4上没问题。但如果你用低端MCU,建议把系数转成Q15格式的定点数,速度能提升好几倍。我在一个Cortex-M0的项目里就这么干过。

3.6 调试与验证

代码写完了,怎么验证对不对?我的习惯是先用示波器看相电压波形。正常情况应该是马鞍形,中间凸起两边低。如果看到的是纯正弦波,那说明你还在用SPWM,不是SVPWM。

另一个验证方法是看相电流。SVPWM的电流波形应该比SPWM更平滑,谐波含量更低。我曾经用电流探头配合FFT功能看过,SVPWM的5次、7次谐波明显小很多。

嗯,最后提醒一句:SVPWM的代码实现虽然看起来复杂,但一旦你理解了“用相邻矢量合成目标矢量”这个核心思想,剩下的就是查表和算术运算了。多写几遍,自然就熟了。