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 或者保持上一拍的值。
为了让你更直观地理解这个判断过程,我画了一张流程图:
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。虽然慢一点,但安全。