做FOC电机控制的人,最后都得过SVPWM这一关。不管你是用ST的MCSDK、TI的InstaSPIN,还是拿着论文从零写驱动,电流环输出的Vd、Vq经过反Park变换变成Vα、Vβ之后,最终都要靠SVPWM把这些“数学世界的电压”翻译成6个MOS管的开关动作。这篇文章是我自己从零实现标准SVPWM的完整记录,重点讲清楚扇区判断后的中间变量X、Y、Z是怎么推导出来的,同时把扇区判断、作用时间计算、七段式PWM生成、STM32定时器配置、调试验证这些环节一起串起来。适合正在读FOC源码、准备自己实现矢量控制,或者代码能跑但波形不对、想老老实实把原理搞明白的工程师。
1. FOC链路里,SVPWM到底站在什么位置
1.1 从电流环到桥臂开关的“翻译官”
很多刚开始看FOC代码的朋友会有个误区:觉得SVPWM只是某个函数、某段查表,把它当成“工具人”。但实际上,SVPWM是整个FOC控制链路的最后一级执行器,直接决定了MOS管怎么开关、母线电压怎么变成三相交流电压。
回顾一下典型的FOC控制框图:电流采样得到Ia、Ib/Ic,经过Clark变换得到Iα、Iβ,再经过Park变换得到Id、Iq;Id、Iq分别和参考值做PI调节,输出Vd、Vq;Vd、Vq再经过反Park变换得到Vα、Vβ。到这里,一切还都发生在“数学坐标系”里。Vα、Vβ是一个以母线电压为参考的二维电压矢量,频率和相位对应电机的电角度。然后SVPWM上场,把Vα、Vβ这个二维矢量,转换成三相桥臂在一个PWM周期内的占空比。
不管位置信息是来自编码器、磁编码器,还是无感算法里的滑模观测器、高频注入,SVPWM都不关心。它只负责一件事:给我一个电压矢量,我还你一组开关时间。这也是为什么SVPWM可以独立出来学习和调试,它本身并不依赖你用了什么位置估计算法。
1.2 为什么标准SVPWM比SPWM更“顶用”
既然SVPWM最终输出的是三相占空比,那直接用三个正弦波和三角载波比较的SPWM不也行吗?行,但利用率差一截。SPWM的线性调制区里,相电压峰值最多到Udc/2;而SVPWM把三相看作一个整体,在αβ平面上直接合成空间电压矢量,线性区最大能到Udc/√3,比SPWM高了大约15%。对电池供电的无人机、电动车、机器人关节来说,这15%直接意味着更高的转速和力矩,谁都不想白白浪费。
这背后的本质是:SVPWM输出的三相调制波并不是纯正弦,而是注入了一个零序分量。这个零序分量在三相之间相互抵消,不会出现在线电压里,却能让相电压更好地利用母线电压。你可以简单理解为SVPWM是在SPWM的基础上自动叠加了类似三次谐波的成分,所以母线利用率更高,谐波表现也更好。
标准SVPWM一般指七段式SVPWM,也就是在一个PWM周期内,开关序列按“零矢量-非零矢量1-非零矢量2-零矢量-非零矢量2-非零矢量1-零矢量”对称排列。这种对称结构开关次数固定、输出谐波小,是工业驱动里最常用的做法。
2. 三相逆变器的8个开关状态与空间矢量
2.1 六个非零矢量和一个六边形
三相全桥逆变器,每相桥臂上下两个MOS管互补导通,所以每相输出要么接母线正(记为1),要么接母线负(记为0),总共2的3次方等于8种状态:000、100、110、010、011、001、101、111。
其中000和111是两个零矢量,三相输出都接同一电位,负载上没有电压差。剩下6个非零矢量,对应三相输出接正负母线的不同组合。把每个非零矢量对应的三相相电压(相对于母线中点)转换到αβ坐标系,会得到一个幅值为2/3·Udc、角度依次相差60度的矢量,围成一个正六边形。这6个矢量就是SVPWM的“基本词汇”。
举个例子,100状态表示A相上桥臂导通,B、C相下桥臂导通,输出相电压在α轴方向产生一个正矢量。110状态则是在60度方向。其他状态以此类推。零矢量在坐标原点,不会让电机产生旋转磁场,但它在七段式里负责“填时间”。
2.2 伏秒平衡:用一个周期合成任意电压矢量
SVPWM的思想并不复杂:在一个PWM周期Ts里,如果期望输出的电压矢量正好落在六边形内,就可以用它所在扇区相邻的两个非零矢量以及零矢量,按照作用时间加权平均,从效果上等效出这个期望矢量。这就是伏秒平衡,或者叫冲量等效原理。
具体到七段式,假设电压矢量落在第一扇区(0到60度之间),那么一个周期的开关序列是:000、100、110、111、110、100、000。其中100和110是第一扇区相邻的两个非零矢量,000和111是零矢量。前半段和后半段完全对称,这样能保证一个周期结束和下一个周期开始时,桥臂状态不变,不会出现额外的开关切换。
这里有一个很重要的边界:SVPWM能线性合成的电压矢量,只能落在六边形的内切圆内,这个内切圆的半径就是Udc/√3。一旦期望矢量超过这个半径,就进入过调制区,波形会失真,这个后面会专门讲处理办法。
3. 扇区判断:为什么要用符号判断而不是atan2
3.1 用三个代数式确定扇区编号
知道了SVPWM的基本原理,接下来第一步就是判断期望电压矢量落在哪个扇区。很多初学者第一反应是用atan2算出角度,再除以60度取整。但这有两个问题:一是有个反正切计算,在DSP或者普通MCU上不是最划算的;二是边界处理容易出幺蛾子。
更工程化的做法是直接算三个量:
- Vβ
- √3·Vα - Vβ
- -√3·Vα - Vβ
分别判断这三个量的正负号。比如Vβ大于0,说明矢量在上半平面;√3·Vα - Vβ大于0,说明矢量在以60度为界的右侧;-√3·Vα - Vβ大于0,说明矢量在以120度为界的左侧。三个正负号组合起来,就能唯一确定扇区。
具体实现上,用三个标志位组合成一个N值:N = A + 2B + 4C,其中A、B、C分别是上面三个量的符号位。我用到N和扇区的映射关系是:
| N | 扇区 | 角度范围 |
|---|---|---|
| 3 | 1 | 0° ~ 60° |
| 1 | 2 | 60° ~ 120° |
| 5 | 3 | 120° ~ 180° |
| 4 | 4 | 180° ~ 240° |
| 6 | 5 | 240° ~ 300° |
| 2 | 6 | 300° ~ 360° |
这里要特别提醒一句:N值和扇区的映射关系不是唯一的,不同资料、不同代码里的定义可能不一样。一定要保证你自己的扇区判断逻辑和后面的查表逻辑是配套的,否则会出现“判断的扇区和计算的时间对不上”这种非常隐蔽的bug。
3.2 引入中间变量X、Y、Z
扇区判断完成之后,接下来需要计算当前扇区内两个非零矢量的作用时间。如果每个扇区都单独列方程解一遍,代码会又长又容易错。更好的做法是先定义三个公共中间变量:
- X = √3·Vβ·Ts / Udc
- Y = (3/2·Vα + √3/2·Vβ)·Ts / Udc
- Z = (√3/2·Vβ - 3/2·Vα)·Ts / Udc
这三个量把Vα、Vβ、Ts、Udc全部吸收进去了。到具体某个扇区时,T1、T2就是X、Y、Z的正负组合。这个做法不仅代码简洁,还有利于嵌入到定点MCU里,因为中间变量的计算只需要加法和乘法,没有反正切、开方这类运算。
注意,我这里X、Y、Z的定义和网上某些版本的代码可能有差异,定义不同,后面的扇区表也会不同。关键是整套逻辑自洽,混用就会翻车。下面进入最核心的推导环节。
4. 重点:两个非零矢量作用时间的中间变量推导
4.1 从第一扇区的伏秒平衡方程解出T1、T2
以第一扇区为例,期望电压矢量Vref落在0到60度之间,相邻的两个非零矢量是100(方向0度)和110(方向60度)。设T1是100的作用时间,T2是110的作用时间,T0是两个零矢量的总时间,那么:
T1 + T2 + T0 = Ts
然后写α轴和β轴的伏秒平衡方程。100矢量在α轴的贡献是(2/3)·Udc,在β轴的贡献是0;110矢量在α轴的贡献是(2/3)·Udc·cos60° = (1/3)·Udc,在β轴的贡献是(2/3)·Udc·sin60° = (√3/3)·Udc。
所以:
Vα·Ts = (2/3)·Udc·T1 + (1/3)·Udc·T2 Vβ·Ts = (√3/3)·Udc·T2
先看β轴方程,直接解出T2:
T2 = √3·Vβ·Ts / Udc
这个式子刚好就是X。再看α轴方程,把T2代入,整理一下得到:
T1 = (3/2·Vα - √3/2·Vβ)·Ts / Udc
而根据Z的定义:Z = (√3/2·Vβ - 3/2·Vα)·Ts / Udc,所以T1 = -Z。
你看,第一扇区很快就推出来了:T1 = -Z,T2 = X。这里的关键是舍得动手把伏秒平衡方程列出来,不要拿网上的表直接抄。自己推一遍之后,后面所有扇区都只是同一个套路换了个角度而已。
4.2 其他五个扇区的完整查表
其余扇区完全同理,比如第二扇区(60到120度)用110和010两个矢量,第三扇区用010和011,第四扇区用011和001,第五扇区用001和101,第六扇区用101和100。每个扇区都做一遍伏秒平衡方程求解,整理后得到的完整表格如下:
| 扇区 | 非零矢量(起始边/结束边) | T1(起始边矢量) | T2(结束边矢量) |
|---|---|---|---|
| 1 | 100 / 110 | -Z | X |
| 2 | 110 / 010 | Y | Z |
| 3 | 010 / 011 | X | -Y |
| 4 | 011 / 001 | -Y | -X |
| 5 | 001 / 101 | -Y | -Z |
| 6 | 101 / 100 | -X | -Z |
这里说的“起始边”和“结束边”,是指扇区角度范围的低边界矢量和最终边界矢量。需要注意,这个顺序不一定等于实际PWM开关序列中非零矢量的出现顺序,后面代码里我会再解释。
4.3 过调制处理:比很多人想的更重要
查表算完T1、T2之后,有一个动作极其容易被忽略:判断T1 + T2是否超过了Ts。如果超过了,说明期望电压矢量的幅值已经超出了六边形内切圆,进入了非线性区。这时候不能简单地把T1、T2各自钳位到某个上限,那样会改变合成矢量的方向,让电机的旋转磁场产生畸变,轻则电流谐波变大,重则闭环直接不稳。
正确做法是等比缩放,保持方向不变,只把幅值压回到线性区的边界:
if (T1 + T2 > Ts) { float scale = Ts / (T1 + T2); T1 *= scale; T2 *= scale; }缩放完成之后,零矢量时间T0 = Ts - T1 - T2,很自然地变成0。这时候一个PWM周期内没有纯零矢量时间,实际上就是过调制运行状态,波形会有一点削顶,但方向始终正确。
4.4 用Python快速验证你的SVPWM表是否自洽
我强烈建议在写代码之前,先写一段Python把所有扇区扫一遍,验证自己的X/Y/Z定义和扇区表是否自洽。别问我为什么这么强调,我见过太多人从网上随便复制一个表,结果N映射对不上,芯片上电后电机不转还嗡嗡叫。
下面是一段可以在电脑上直接运行的验证脚本,随机取几个角度,计算作用时间后反推合成电压,看看是否能还原原始的Vα、Vβ:
import math Udc = 1.0 Ts = 1.0 n2sector = {3: 1, 1: 2, 5: 3, 4: 4, 6: 5, 2: 6} sector_vec = { 1: (0, 60, '100', '110'), 2: (60, 120, '110', '010'), 3: (120, 180, '010', '011'), 4: (180, 240, '011', '001'), 5: (240, 300, '001', '101'), 6: (300, 360, '101', '100'), } def calc(v_alpha, v_beta): X = math.sqrt(3) * v_beta * Ts / Udc Y = (1.5 * v_alpha + 0.866 * v_beta) * Ts / Udc Z = (0.866 * v_beta - 1.5 * v_alpha) * Ts / Udc A = 1 if v_beta > 0 else 0 B = 1 if math.sqrt(3) * v_alpha - v_beta > 0 else 0 C = 1 if -math.sqrt(3) * v_alpha - v_beta > 0 else 0 N = A + 2 * B + 4 * C sector = n2sector[N] if sector == 1: T1, T2 = -Z, X elif sector == 2: T1, T2 = Y, Z elif sector == 3: T1, T2 = X, -Y elif sector == 4: T1, T2 = -Y, -X elif sector == 5: T1, T2 = -Y, -Z else: T1, T2 = -X, -Z if T1 + T2 > Ts: scale = Ts / (T1 + T2) T1 *= scale T2 *= scale start_angle, end_angle, _, _ = sector_vec[sector] v_sum_alpha = 0.0 v_sum_beta = 0.0 v_sum_alpha += (2.0 / 3.0) * T1 * math.cos(math.radians(start_angle)) v_sum_beta += (2.0 / 3.0) * T1 * math.sin(math.radians(start_angle)) v_sum_alpha += (2.0 / 3.0) * T2 * math.cos(math.radians(end_angle)) v_sum_beta += (2.0 / 3.0) * T2 * math.sin(math.radians(end_angle)) return sector, v_sum_alpha, v_sum_beta for deg in [15, 45, 90, 150, 210, 270, 330]: vmag = 0.4 v_alpha = vmag * math.cos(math.radians(deg)) v_beta = vmag * math.sin(math.radians(deg)) sector, v_sum_alpha, v_sum_beta = calc(v_alpha, v_beta) err = math.hypot(v_sum_alpha - v_alpha, v_sum_beta - v_beta) print(f"angle={deg} sector={sector} err={err:.2e}")如果输出误差都在1e-12量级,说明你的表是自洽的。这时候再把逻辑搬到MCU上,心里才有底。
5. 七段式SVPWM的切换点计算与PWM生成
5.1 从T1、T2到三个切换时刻
作用时间算出来,接下来要把时间转成中心对齐PWM的比较值。在七段式SVPWM里,一个PWM周期被分成七段,很多文章直接给三个切换时刻的公式:
- te0 = T0 / 4
- te1 = te0 + T_first / 2
- te2 = te1 + T_second / 2
这里的T_first和T_second,指的是在七段式开关序列中,第一个出现的非零矢量和第二个出现的非零矢量。这一步是很多实现出错的地方:你把第一扇区方程里解出来的T1、T2当成开关序列里的先后顺序了。
实际上,为了保证七段式序列“相邻开关状态只切换一个桥臂”,每个扇区两个非零矢量在序列里的先后顺序是固定的,但不一定等于扇区起始边、结束边的顺序。以第二扇区为例,如果直接按“起始边110先、结束边010后”来排,从000切换到110需要同时动作A相和B相,这就违背了每次只切换一个桥臂的原则。标准做法是先010再110,所以T_first和T_second的顺序在部分扇区会交换。
5.2 各扇区开关序列与比较值分配
结合七段式对称序列,我给出一份完整映射表,注意这里的T1、T2仍然是我推导表里的“起始边/结束边”时间:
| 扇区 | 前半段序列 | 翻转顺序 | T_first | T_second | 比较器分配 |
|---|---|---|---|---|---|
| 1 | 000 → 100 → 110 → 111 | A → B → C | T1 | T2 | CCR_A=te0, CCR_B=te1, CCR_C=te2 |
| 2 | 000 → 010 → 110 → 111 | B → A → C | T2 | T1 | CCR_A=te1, CCR_B=te0, CCR_C=te2 |
| 3 | 000 → 010 → 011 → 111 | B → C → A | T1 | T2 | CCR_A=te2, CCR_B=te0, CCR_C=te1 |
| 4 | 000 → 001 → 011 → 111 | C → B → A | T2 | T1 | CCR_A=te2, CCR_B=te1, CCR_C=te0 |
| 5 | 000 → 001 → 101 → 111 | C → A → B | T1 | T2 | CCR_A=te1, CCR_B=te2, CCR_C=te0 |
| 6 | 000 → 100 → 101 → 111 | A → C → B | T2 | T1 | CCR_A=te0, CCR_B=te2, CCR_C=te1 |
代码里可以这样组织,先算te0,再根据扇区确定T_first、T_second、te1、te2,最后按表给三个通道比较器赋值。我在STM32上通常用一个switch实现,很直观。
5.3 STM32定时器配置要点
STM32用SVPWM,一般选带互补输出的高级定时器,比如TIM1或TIM8。关键配置是中心对齐模式,因为SVPWM要求三角载波,和中心对齐PWM天然匹配。PWM频率和定时器周期ARR的关系是:
f_pwm = Tclk / (2 × (ARR + 1))
比如Tclk=72MHz,想要20kHz的PWM频率,ARR大概就是1799。注意中心对齐模式下计数方向为先增后减,一个完整周期会经历两次计数溢出,所以实际PWM周期是2 × (ARR+1)个时钟周期。
死区时间也要在这里配置。SVPWM输出的是三相上下桥臂互补PWM,如果死区设置太小,母线可能会直接短路;设置太大,电流波形在过零点会出现明显畸变。我一般从1us左右开始调,看相电流波形再微调。
6. 调试验证:从开环到FOC闭环
6.1 先开环,别急着闭环
我见过很多朋友一上来就把电流环、速度环全跑起来,波形一团乱麻,根本不知道问题出在SVPWM还是PI参数上。正确顺序是先单独验证SVPWM。
第一步,给一个固定的Vα、Vβ,比如Vα=0.1Udc/√3,Vβ=0,相当于给电机施加一个固定方向的电压矢量。正常情况下,电机转子会转到这个矢量对应的角度并锁住。你用手去拨转子,它会自己回到平衡位置。如果这步都不对,直接查相序、PWM极性和死区。
第二步,给一个旋转的电压矢量,也就是Vα=Vmag·cos(θ),Vβ=Vmag·sin(θ),θ从0开始不断累加。Vmag保持在线性区内,比如0.3Udc/√3。这时候电机应该会跟着磁场方向稳定转起来,相电流波形接近正弦。用示波器看线电压Uab,能看到明显的六步台阶,每个台阶对应一个扇区。
6.2 电流采样窗口和FOC联调
SVPWM在FOC闭环里的一个特殊要求是电流采样时刻。常见的做法是用两相采样电阻重构三相电流,ADC触发点放在PWM周期的中心位置。因为在中心点,七段式SVPWM一定处于零矢量状态(要么000,要么111),此时下桥臂采样电阻上的电流就是真实的相电流,不会受到开关切换瞬间的干扰。
实际写代码时,用定时器的更新事件或者中心对齐溢出中断来触发ADC采样,并且要把采样时刻避开死区。如果采样点离开关切换太近,ADC会打到振铃,波形满屏毛刺。
联调时如果电流波形有规律地每60度出现一次畸变,基本可以断定是扇区切换相关的映射问题,优先检查N和扇区的映射表、比较器分配表是不是配套的。
7. 常见问题与排查技巧实录
7.1 波形异常的快速定位表
调试SVPWM的过程中会遇到不少经典问题,我整理了一个速查表:
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 电机啸叫、母线电流很大 | 死区时间不够或互补PWM极性配反 | 示波器测上下管驱动波形,确认死区 |
| 能转但力矩小、带载掉速 | 母线电压Udc参数和实际不符,或过调制被简单截断 | 确认软件里的Udc是实际母线电压 |
| 相电流波形60度一次畸变 | 扇区映射或比较器分配表不配套 | 用第4.4节Python脚本验证表 |
| 扇区切换瞬间有尖峰电流 | 比较值更新不同步 | 开启预装载,在更新事件统一更新CCR |
| 电流采样噪声极大 | ADC采样点离开关切换太近 | 把采样点移到PWM中心,并避开死区 |
| 电机不转但电压波形看起来正常 | 相序反了,或SVPWM后三相不是接入电机UVW | 交换任意两相,或者交换PWM输出通道 |
7.2 复制SVPWM代码最容易翻车的三个细节
最后说几个我在实际项目中栽过跟头的地方:
第一,中间变量的定义和扇区表必须配套。网上版本很多,X、Y、Z定义差一个符号,扇区表就会全部错位。不要东拼西凑,要么全抄一套,要么自己推导一套,然后务必用脚本验证。
第二,T1+T2大于Ts时,一定要等比缩放。很多人图省事直接写成if (T1 > Ts) T1 = Ts,这会让合成电压矢量方向偏移。低速没感觉,一旦高速出过调制,谐波和电流噪声会让人怀疑人生。
第三,比较寄存器的更新要使用影子寄存器,也就是预装载。SVPWM一个PWM周期内开关状态在切换,如果中途被错误地更新了比较值,会立刻产生一次多余的开关动作,轻则电流尖峰,重则开关器件应力超标。配置定时器时,把CCR的预装载使能打开,在定时器溢出中断里统一更新。
我在实际调试中还有一个体会:SVPWM这个环节,看着原理很简单,就一个查表加几行公式,但真正把它调稳,最花时间的往往不是算法本身,而是“表跟表之间没对上”。所以别怕动手推公式,花半小时把伏秒平衡方程自己列一遍,比在示波器前面挠头三个小时划算得多。