4. 扇区判断:扇区划分规则、基于αβ分量的扇区判断算法、扇区判断的C语言实现

好,咱们接着往下走。前面我们把 Clarke 变换讲清楚了,也知道了为什么要用 αβ 坐标系。现在问题来了:你拿到了 Uα 和 Uβ,怎么知道这个电压矢量落在哪个扇区?

说白了,扇区判断就是 SVPWM 的「导航系统」。你连自己在哪个区都不知道,怎么选基本矢量去合成?我当年刚学的时候,总觉得这步可有可无,心想反正六个扇区轮流来呗。结果一仿真,波形全是毛刺。嗯,从那以后我再也不敢跳过扇区判断了。

4.1 扇区划分规则

先看看扇区是怎么划分的。三相逆变器有六个开关管,上桥臂三个、下桥臂三个。六个非零基本矢量把空间分成了六个60°的扇区。

核心规则:扇区编号从 I 到 VI,逆时针排列。扇区 I 在 0°~60°,扇区 II 在 60°~120°,以此类推。

这里有个容易搞混的地方——扇区编号和基本矢量的对应关系。我列个表,你一看就明白:

扇区 角度范围 边界基本矢量 中间矢量
I 0° ~ 60° U₄(100), U₆(110) Uref 在 30° 方向
II 60° ~ 120° U₆(110), U₂(010) Uref 在 90° 方向
III 120° ~ 180° U₂(010), U₃(011) Uref 在 150° 方向
IV 180° ~ 240° U₃(011), U₁(001) Uref 在 210° 方向
V 240° ~ 300° U₁(001), U₅(101) Uref 在 270° 方向
VI 300° ~ 360° U₅(101), U₄(100) Uref 在 330° 方向

你想想看,每个扇区里都有两个非零矢量和一个零矢量。我们要做的,就是用这两个非零矢量去「拼」出目标电压矢量。

个人经验:我在做电机控制时,习惯把扇区 I 的起始角对齐到 U₄(100) 的位置。这样后面算作用时间时,公式会对称一些,不容易出错。

4.2 基于αβ分量的扇区判断算法

好,现在进入正题。怎么用 Uα 和 Uβ 来判断扇区?

最直接的方法是用反正切算角度,然后查表。但说实话,MCU 里算反正切太慢了,而且有精度问题。工业上常用的方法是——用三个中间变量做符号判断。

这个方法的核心思路是:把 αβ 坐标系下的电压分量,投影到三个互差120°的参考轴上。然后看这些投影值的正负,就能唯一确定扇区。

具体来说,定义三个中间变量:

// 定义三个参考轴投影
B1 = Uβ
B2 = -0.5 * Uβ + (√3/2) * Uα
B3 = -0.5 * Uβ - (√3/2) * Uα

然后根据 B1、B2、B3 的符号,查表得到扇区编号:

B1 符号 B2 符号 B3 符号 扇区编号
+ + - I
+ - - II
+ - + III
- - + IV
- + + V
- + - VI

为什么会这样?其实你画个矢量图就明白了。B1 对应 Uβ 的正负,B2 和 B3 分别对应两个斜向的投影。三个符号组合起来,刚好能覆盖六个扇区,一个不多一个不少。

注意:当 Uα 和 Uβ 都很小(接近零矢量)时,B1、B2、B3 可能同时接近零。这时候符号判断容易出错。我建议加一个死区判断,当模长小于某个阈值时,直接归到扇区 I 或者保持上一拍的值。

为了让你更直观地理解这个判断过程,我画了一张流程图:

扇区判断算法流程图 输入 Uα, Uβ 计算三个参考轴投影 B1=Uβ, B2=-0.5Uβ+0.866Uα, B3=-0.5Uβ-0.866Uα 判断 B1, B2, B3 的符号 查符号表 → 得到扇区编号 N 输出扇区 N (I~VI) 说明: 当 Uα 和 Uβ 模长很小时,建议增加死区判断,避免符号抖动导致扇区误判

4.3 扇区判断的C语言实现

理论说完了,咱们直接上代码。这是我在实际项目中用过的实现,经过多次优化,效率还不错。

/**
 * @brief 扇区判断函数
 * @param Ualpha  α轴电压分量 (标幺值)
 * @param Ubeta   β轴电压分量 (标幺值)
 * @return 扇区编号 (1~6)
 */
uint8_t SVPWM_SectorJudge(float Ualpha, float Ubeta)
{
    float B1, B2, B3;
    uint8_t sector = 0;
    
    // 1. 计算三个参考轴投影
    B1 = Ubeta;
    B2 = -0.5f * Ubeta + 0.8660254f * Ualpha;  // √3/2 ≈ 0.8660254
    B3 = -0.5f * Ubeta - 0.8660254f * Ualpha;
    
    // 2. 符号判断,组合成扇区编号
    if (B1 > 0.0f) sector |= 0x01;  // bit0
    if (B2 > 0.0f) sector |= 0x02;  // bit1
    if (B3 > 0.0f) sector |= 0x04;  // bit2
    
    // 3. 查表映射到实际扇区编号
    // 注意:这里的映射表是根据符号组合推导出来的
    static const uint8_t sectorMap[8] = {0, 1, 5, 0, 3, 2, 4, 0};
    sector = sectorMap[sector & 0x07];
    
    return sector;
}

这段代码有几个地方我想强调一下:

  • 系数 0.8660254 就是 √3/2 的浮点近似。如果你用定点 DSP,可以换成 Q15 格式的整数,效率更高。
  • 位运算组合:用三个 if 把符号信息拼成一个 3-bit 的数,然后查表。这样比一堆 if-else 要快得多。
  • sectorMap 数组:下标 0~7,但只有 6 个有效值。下标 0、3、7 对应 B1、B2、B3 全负或全正的情况,这些情况理论上不会出现,但为了安全我直接映射到 0(无效扇区)。

避坑指南:我曾经在 sectorMap 里写错过一个值,结果扇区 III 和扇区 V 老是搞混。查了两天才发现是查表映射错了。建议你写完代码后,用几个已知角度(比如 30°、90°、150°)手动验证一下输出对不对。

另外,如果你用的是定点 MCU,浮点运算太慢,可以改成这样:

// 定点版本,假设 Ualpha 和 Ubeta 是 Q15 格式
// 0.8660254 * 32768 ≈ 28378
#define SQRT3_OVER_2_Q15  28378

uint8_t SVPWM_SectorJudge_Fixed(int16_t Ualpha, int16_t Ubeta)
{
    int16_t B1, B2, B3;
    uint8_t sector = 0;
    
    B1 = Ubeta;
    // B2 = -0.5*Ubeta + 0.866*Ualpha
    // 注意:-0.5 用右移1位实现,但要小心负数右移
    B2 = (-(Ubeta >> 1)) + ((int32_t)Ualpha * SQRT3_OVER_2_Q15 >> 15);
    B3 = (-(Ubeta >> 1)) - ((int32_t)Ualpha * SQRT3_OVER_2_Q15 >> 15);
    
    if (B1 > 0) sector |= 0x01;
    if (B2 > 0) sector |= 0x02;
    if (B3 > 0) sector |= 0x04;
    
    static const uint8_t sectorMap[8] = {0, 1, 5, 0, 3, 2, 4, 0};
    sector = sectorMap[sector & 0x07];
    
    return sector;
}

注意:定点版本里,负数右移在不同编译器下行为可能不一样。我建议用 (Ubeta >> 1) 之前先判断符号,或者直接用除法 /2。虽然慢一点,但安全。