第三章 SVPWM原理与实现:从理论到代码
SVPWM,全称空间矢量脉宽调制。说白了,它就是FOC控制里那个把「虚拟电压矢量」变成「真实MOS管开关动作」的桥梁。
我刚开始接触FOC时,觉得Clark变换和Park变换才是核心,SVPWM不过是底层驱动。直到有一次,我在调试一个高速电机时,发现电流波形里总有奇怪的毛刺,电机嗡嗡响,效率上不去。折腾了两天,最后发现是SVPWM的扇区判断写错了。嗯,从那以后我再也不敢小看它了。
这一章,我们就把它彻底讲透。
3.1 空间矢量调制的基本原理
先问一个问题:三相逆变器只有6个MOS管,开关组合一共就8种状态(6个有效矢量 + 2个零矢量)。你怎么用这8种状态,合成出任意方向、任意大小的电压矢量?
答案就是:在一个开关周期内,用相邻的两个有效矢量,分别作用不同的时间,再插入零矢量填满剩余时间。这就是「伏秒平衡」原理。
举个例子。假设我想合成一个指向30°方向、幅值为U的电压矢量。那我就在一个周期T内,让U₄(100,指向0°)作用T₄时间,让U₆(110,指向60°)作用T₆时间,剩下的时间插零矢量。只要满足:
- T₄ + T₆ + T₀ = T
- U₄ × T₄ + U₆ × T₆ = U × T
这个合成的效果,在宏观上就等价于直接输出U矢量。
核心思想:用有限个离散的开关状态,通过时间平均,合成连续的电压空间矢量。
下面这张图,是我自己画的SVPWM整体流程。你看一遍,心里就有谱了。
3.2 扇区判断
拿到Uα和Uβ之后,第一件事就是判断目标矢量落在哪个扇区。六个扇区,每个60°,怎么判断?
我见过有人用查表法,也有人用角度atan2。但工程上最常用的,是三值判断法。速度快,没有浮点开销。
定义三个中间变量:
B₁ = Uβ
B₂ = (√3/2) × Uα - (1/2) × Uβ
B₃ = -(√3/2) × Uα - (1/2) × Uβ
然后根据这三个值的符号,查表得到扇区号:
| B₁ 符号 | B₂ 符号 | B₃ 符号 | 扇区 |
|---|---|---|---|
| + | - | - | I |
| + | + | - | II |
| - | + | - | III |
| - | + | + | IV |
| - | - | + | V |
| + | - | + | VI |
小技巧:实际代码里,我习惯把B₁、B₂、B₃的符号位打包成一个3位的二进制数,然后直接查一个长度为6的数组。这样连if-else都省了,一个周期就出结果。
3.3 作用时间计算
扇区判断完了,接下来算T₁和T₂。这两个时间,就是相邻两个有效矢量的作用时长。
不同扇区对应的T₁、T₂计算公式不一样。但别慌,它们有统一的模式。我直接给你一个通用公式:
X = (√3 × Uβ × T) / Udc
Y = ( (√3/2) × Uβ + (3/2) × Uα ) × T / Udc
Z = ( (√3/2) × Uβ - (3/2) × Uα ) × T / Udc
然后根据扇区查表:
| 扇区 | T₁ | T₂ |
|---|---|---|
| I | -Z | X |
| II | Z | Y |
| III | X | -Y |
| IV | -X | Z |
| V | -Y | -Z |
| VI | Y | -X |
算完之后,记得做一次限幅处理。如果T₁ + T₂ > T,就按比例缩放:
if (T₁ + T₂ > T) {
scale = T / (T₁ + T₂);
T₁ *= scale;
T₂ *= scale;
}
注意:这个限幅处理很多人会忘。如果不做,过调制时PWM会饱和,电流波形直接畸变。我曾经在量产前测试时发现电机在高速区抖动,查了两天才发现是这里漏了限幅。切记!
3.4 七段式SVPWM
有了T₁和T₂,接下来要生成三相的PWM占空比。七段式,顾名思义,一个开关周期被分成7段。
它的特点是:每次切换只改变一相的状态。这样开关损耗小,谐波也小。
以扇区I为例(U₄→U₆→U₇→U₆→U₄),三相占空比计算如下:
T₀ = T - T₁ - T₂
Ta = T₀ / 4
Tb = Ta + T₁ / 2
Tc = Tb + T₂ / 2
然后根据扇区,把Ta、Tb、Tc映射到实际的A、B、C三相比较值上。映射关系如下:
| 扇区 | 比较值 A | 比较值 B | 比较值 C |
|---|---|---|---|
| I | Ta | Tb | Tc |
| II | Tb | Ta | Tc |
| III | Tc | Ta | Tb |
| IV | Tc | Tb | Ta |
| V | Tb | Tc | Ta |
| VI | Ta | Tc | Tb |
你看,其实就是把Ta、Tb、Tc三个值,按扇区重新排列一下。代码实现就是一个switch-case或者查表。
3.5 五段式SVPWM
五段式和七段式的区别在哪?说白了,五段式只插入一个零矢量(要么U₇,要么U₀),而不是两个都用。
这样做的好处是:每个周期少两次开关动作,开关损耗降低约1/3。
代价呢?谐波含量会大一些,电流纹波也大一点。
我个人的经验是:大功率、对效率要求高的场合,用五段式。小功率、对电流波形质量要求高的场合,用七段式。
五段式的占空比计算也简单。以扇区I、只插U₇为例:
T₀ = T - T₁ - T₂
Ta = T₀ / 2
Tb = Ta + T₁
Tc = Tb + T₂
映射到三相的方式,和七段式完全一样。
3.6 七段式 vs 五段式:对比
我整理了一个表格,方便你对比选择:
| 对比项 | 七段式SVPWM | 五段式SVPWM |
|---|---|---|
| 开关次数/周期 | 6次 | 4次 |
| 开关损耗 | 较高 | 较低(约降1/3) |
| 谐波含量 | 较低 | 较高 |
| 电流纹波 | 较小 | 较大 |
| 适用场景 | 小功率、高精度 | 大功率、高效率 |
3.7 代码实现
说了这么多,直接上代码。这是我在STM32上跑过的实现,C语言,定点数运算,没有浮点。
// 输入:Ualpha, Ubeta(Q15格式,范围-32768~32767)
// 输出:比较值到TIM寄存器
void SVPWM_Calc(int16_t Ualpha, int16_t Ubeta) {
int32_t B1, B2, B3;
int32_t X, Y, Z;
int32_t T1, T2, T0;
int32_t Ta, Tb, Tc;
uint8_t sector;
int32_t pwmA, pwmB, pwmC;
// 1. 扇区判断
B1 = Ubeta;
B2 = (int32_t)((int32_t)Ualpha * 0.866f) - (Ubeta >> 1);
B3 = -(int32_t)((int32_t)Ualpha * 0.866f) - (Ubeta >> 1);
sector = 0;
if (B1 > 0) sector |= 0x01;
if (B2 > 0) sector |= 0x02;
if (B3 > 0) sector |= 0x04;
// 2. 计算X, Y, Z
X = (int32_t)((int32_t)Ubeta * 1.732f);
Y = (int32_t)((int32_t)Ualpha * 1.5f + (int32_t)Ubeta * 0.866f);
Z = (int32_t)((int32_t)Ualpha * 1.5f - (int32_t)Ubeta * 0.866f);
// 3. 查表得到T1, T2
switch (sector) {
case 1: T1 = Z; T2 = Y; break; // II
case 2: T1 = Y; T2 = -X; break; // VI
case 3: T1 = -Z; T2 = X; break; // I
case 4: T1 = -X; T2 = Z; break; // IV
case 5: T1 = X; T2 = -Y; break; // III
case 6: T1 = -Y; T2 = -Z; break; // V
default: T1 = 0; T2 = 0; break;
}
// 4. 限幅
if ((T1 + T2) > PWM_PERIOD) {
int32_t scale = PWM_PERIOD / (T1 + T2);
T1 = (T1 * scale) >> 15;
T2 = (T2 * scale) >> 15;
}
// 5. 计算占空比(七段式)
T0 = PWM_PERIOD - T1 - T2;
Ta = T0 >> 2;
Tb = Ta + (T1 >> 1);
Tc = Tb + (T2 >> 1);
// 6. 扇区映射
switch (sector) {
case 1: pwmA = Tb; pwmB = Ta; pwmC = Tc; break; // II
case 2: pwmA = Ta; pwmB = Tc; pwmC = Tb; break; // VI
case 3: pwmA = Ta; pwmB = Tb; pwmC = Tc; break; // I
case 4: pwmA = Tc; pwmB = Tb; pwmC = Ta; break; // IV
case 5: pwmA = Tc; pwmB = Ta; pwmC = Tb; break; // III
case 6: pwmA = Tb; pwmB = Tc; pwmC = Ta; break; // V
default: pwmA = 0; pwmB = 0; pwmC = 0; break;
}
// 7. 写入TIM寄存器
TIM->CCR1 = pwmA;
TIM->CCR2 = pwmB;
TIM->CCR3 = pwmC;
}
关于代码的几点说明:
- 代码里用了浮点乘法(0.866f、1.732f),实际工程中建议提前算成定点数,或者用查表法替代。
- PWM_PERIOD是定时器的自动重装值,对应开关周期T。
- 如果想改成五段式,只需要把第5步的占空比计算改成五段式公式即可,其他不变。
3.8 避坑指南
最后,分享几个我踩过的坑:
- 死区补偿:SVPWM本身不处理死区。如果你发现轻载时电流波形有「台阶」,大概率是死区效应。需要额外做死区补偿。
- 过调制区:当目标电压超过六边形边界时,必须做限幅。否则输出电压会失真,电机电流会失控。
- 定时器对齐:SVPWM要求使用中心对齐PWM模式(中央对齐计数)。边沿对齐模式会导致扇区切换时刻不对称,产生谐波。
- 浮点转定点:MCU没有FPU时,尽量把所有系数转成Q15或Q31格式。我见过有人直接在中断里跑浮点,结果PWM频率只能跑到2kHz,电机响得跟警报一样。
嗯,SVPWM这部分就讲到这里。代码可以直接拿去用,但建议你对着数据手册把定时器配置再确认一遍。毕竟,细节决定成败。