简介:面向电机控制初学者以及需要动手实现FOC算法的工程师,这份笔记以数学推导为主线,把磁场定向控制从概念到代码串成一条完整链路。内容先讲清超前角控制的核心思想,以及电角度在坐标变换中的作用;再逐项推导Clark变换的等幅值系数、恒功率系数,以及Park变换及其逆变换,并给出可以对照的C程序实现。SVPWM部分涵盖扇区判断、非零矢量与零矢量作用时间的两套算法、过调制处理、切换点确定,以及马鞍波形成原因与零序分量注入SPWM。控制环节则整理PID离散化、位置式与增量式之间的差异、参数整定、积分饱和的负面影响与抗饱和方法,最后落到磁链圆限制中MAX_MODULE与START_INDEX的取值与程序实现。包内为1个PDF文件,压缩包约7.34MB,适合按章节顺序通读,也可作为调试时随手翻查的公式手册;已有1920人学习,配合Simulink仿真可以较快完成从公式到代码的验证。
1. FOC 到底在控制什么:电角度只是个“位置播报员”
很多人啃 FOC 的数学推导,都以为难点在 Clarke 和 Park 那一堆三角函数。真正反直觉的结论是:整套 FOC 算法从头到尾只干一件事——让定子磁场始终超前转子磁链 90 度。电角度本身不构成角度,它更像一个位置播报员:告诉算法转子此刻朝哪,好把那个超前 90 度的目标磁场分解回固定的三相坐标轴。想清楚这一层,后面所有变换、SVPWM、Simulink 仿真里的每一个模块,都能在脑子里连成一条因果链,而不是一堆要背的公式。
2. Clark 变换:为什么采两相电流就够,2/3 这个系数是怎么来的
Clark 变换在教材里常被描述成“三相到两相的坐标变换”,这句话太轻,容易让人以为它只是个换元技巧。它真正干的事是降维解耦:三相绕组里三个电流受基尔霍夫电流定律约束,任意时刻只有一个自由度被“占用”,把它们摊到互相正交的 α、β 轴上,才可能用两个独立的 PI 环分别控制。
2.1 从 KCL 出发,那一步降维是怎么发生的
先摆事实:三相星形接法、中性点不引出时,任一时刻流入节点的电流之和等于流出节点电流之和,也就是
Ia + Ib + Ic = 0这一条约束就是 Clark 变换能降维的全部底气。三个变量被一条线性方程绑死,独立变量只剩两个。
把三相轴在平面上画出来,A 相在 0 度,B 相在 120 度,C 相在 240 度,然后把每个相电流往 α 轴和 β 轴上投影:
Iα = k · (Ia − Ib·sin30° − Ic·sin30°) = k · (Ia − Ib/2 − Ic/2) Iβ = k · (Ib·sin60° − Ic·sin60°) = k · (√3/2)·(Ib − Ic)这里的 k 是待定系数,选不同的 k 得到不同的变换族。注意一个细节:Iβ 的式子里没有 Ia,因为 A 相轴正好垂直于 β 轴,投影为零。这个“消失”不是巧合,是坐标轴摆放方式决定的,也是后面“只采两相”的伏笔。
把 KCL 的 Ic = −Ia − Ib 代进 Iα 的式子:
Iα = k · (Ia + Ib/2 + Ia/2 + Ib/2) = k · (3/2)·Ia代进 Iβ:
Iβ = k · (√3/2)·(Ib + Ia + Ib) = k · (√3/2)·(Ia + 2Ib)到这里 k 还没定,接下来就是两个流派的分岔口。
2.2 等幅值系数 2/3 和恒功率系数 √(2/3),各自在算什么
选 k 的方法很朴素:拿一组特殊的输入代进去,看输出应该等于多少。
等幅值变换的要求是“变换前后矢量幅值相等”。取 Ia = 1、Ib = Ic = −0.5(一个幅值为 1 的纯 A 相电流),代入上式:
Iα = k · 1.5 = 1 ⇒ k = 2/3 Iβ = k · (√3/2)·(1 − 1) = 0于是等幅值变换的完整形式是:
Iα = (2/3)·(Ia − Ib/2 − Ic/2) Iβ = (2/3)·(√3/2)·(Ib − Ic) = (Ib − Ic)/√3恒功率变换的要求是“变换前后功率相等”。同样取 Ia = 1、Ib = Ic = −0.5,三相总有功(设每相阻抗为 R)是:
P3 = Ia²R + Ib²R + Ic²R = (1 + 0.25 + 0.25)·R = 1.5R两相侧:
P2 = Iα²R + Iβ²R = (1.5k)²·R + 0 = 2.25k²·R令两者相等,2.25k² = 1.5,得 k² = 2/3,即 k = √(2/3) ≈ 0.8165。
两个系数差 22%,看起来只是个数,落到工程上却会改变整条链路的标幺基准。选等幅值,d/q 电流的数值直接等于相电流峰值,电流环的限幅值、采样电路的放大倍数、示波器上读到的数,全都可以用同一套刻度;选恒功率,矢量的尺度和相电压峰值不再一一对应,SVPWM 那边的限幅圆半径必须跟着换算,换算漏了就是过调制。
原始笔记里那句判断很关键:FOC 实现里一般用等幅值变换,因为一旦用等功率变换,合成矢量的尺度会变成相电压峰值的 1.5 倍,直接顶出空间矢量的六边形圆圈,表现为过调制。这不是数学错误,是两套标幺基准没对齐。
2.3 代码里为什么只出现 ia 和 ib
把 k = 2/3 代回前面整理过的式子,会得到一个非常“干净”的结果:
Iα = Ia Iβ = (Ia + 2·Ib)/√3Ic 彻底消失了。这不是化简的巧合,是把 KCL 用掉之后的必然结果——三个量本来就只有一个约束,你只要知道其中两个,第三个是被算出来的。采样电路因此可以省掉一路运放、一路 ADC、一路偏置校准,BOM 成本和 PCB 面积都省下来了。
对应的 C 实现短得不像话:
typedef struct { float ia; float ib; float ic; } abc_t; typedef struct { float i_alpha; float i_beta; } alphaBeta_t; alphaBeta_t Clark(abc_t input) { alphaBeta_t output; /* KCL 保证了 ic = -ia - ib,所以这里根本不用 input.ic */ output.i_alpha = input.ia; /* Iα = Ia */ output.i_beta = (input.ia + 2.0f * input.ib) * 0.577350269f; /* 1/√3 */ return output; }0.577350269f就是 1/√3。用乘常数而不是除 sqrt(3),是因为在中断里跑,除法比乘法慢一个数量级,编译器也不一定帮你把1.0f/sqrtf(3.0f)折成常量。这类细节在 20kHz 的电流环里是实打实的 CPU 占用。
Clark 反变换同样短,由正变换矩阵求逆得到:
abc_t Rev_Clark(alphaBeta_t input) { abc_t output; output.ia = input.i_alpha; output.ib = -0.5f * input.i_alpha + 0.8660254f * input.i_beta; /* √3/2 */ output.ic = -0.5f * input.i_alpha - 0.8660254f * input.i_beta; return output; }注意ib + ic = -ia恒成立,这是反变换自洽性的一个便宜检验点。写完顺手验一下,能挡住大部分手滑写错符号的问题。
回头看这一整套推导,Clark 变换的价值不在于公式本身,而在于它把一个“三相耦合、每相都在正弦变化”的问题,变成了“两个正交轴上的独立分量”。但 α、β 上的量依然是正弦,PI 控制器追正弦会有稳态误差,所以还得再变一次。
3. Park 变换:把旋转的交流量掰成直流量,定点数又是怎么算 sin/cos 的
Clark 之后,α、β 上的电流仍然是频率为电频率的正弦量。PI 调节器对阶跃信号能做到无静差,对正弦信号不行,总会滞后一个相位。Park 变换的动机就一句话:跳到转子身上去看,让坐标系跟着转子一起转,正弦量在旋转坐标系里就变成了一条直线。
3.1 旋转坐标系为什么能把正弦掰成直线
站在地面看一个匀速转动的轮子上的标记点,它的高度随时间走正弦曲线;坐在轮子上看同一个点,高度恒定不变。Park 变换做的就是“换个观察位置”这件事,只不过用的是投影。
α-β 轴是钉在定子上的静止坐标系,d-q 轴是钉在转子上的旋转坐标系,两者夹角就是电角度 θ。把 α、β 上的量往 d、q 轴上投影:
Id = Iα·cosθ + Iβ·sinθ Iq = −Iα·sinθ + Iβ·cosθ写成矩阵形式:
[ Id ] [ cosθ sinθ ] [ Iα ] [ Iq ] = [ −sinθ cosθ ] [ Iβ ]这个 2×2 矩阵是正交矩阵(旋转矩阵),行列式为 1,所以它的逆矩阵就是它的转置,一步就能写出逆变换:
Iα = Id·cosθ − Iq·sinθ Iβ = Id·sinθ + Iq·cosθ这就是 Park 逆变换。它的工程意义很直接:PID 算出来的 Ud、Uq 是“希望施加的电压在转子坐标系里的分量”,但逆变器只认三相开关状态,所以必须先逆变换回 α、β,再交给 SVPWM 去合成。
这里有个容易被忽略的点:θ 的零点定义。原始笔记里有一段很通透的论述——电角度可以是 A 相轴与 d 轴的夹角,也可以是 A 相轴与 q 轴的夹角,甚至可以是与两者角平分线的夹角,因为电角度存在的唯一意义是“指示转子在什么方向”,进而生成超前 90 度的磁场。之所以业界普遍选 d 轴(或 q 轴)与 A 相轴的夹角,纯粹是因为这样投影时乘的系数正好是标准三角函数的 sin/cos 值,Park 和逆 Park 的表达式最干净。
这句话解决了一个长期困扰:为什么不同厂家的代码里 θ 差 90 度,电机照样能转?因为超前角的关系是相对的,只要“目标磁场超前转子磁链 90 度”这个约束不变,坐标系的零点爱定在哪定在哪。但如果你要在别人的代码上做修改,第一件事就是确认他用的零点定义,否则 Ud/Uq 会互换,电流环方向反掉,一给转矩指令电机就飞车,这类问题在调试现场是很常见的失误。
3.2 Park 与逆 Park 的实现,以及 Q15 定点三角函数
先看浮点版本,和公式一一对应:
typedef struct { float id; float iq; } dq_t; #define DIV_Q15 (1.0f / 32768.0f) dq_t Park(alphaBeta_t input, int16_t theta) { dq_t output; Trig_Components sc = Trig_Functions(theta); /* 查表得到 Q15 格式的 sin/cos */ float cos_da = (float)sc.hCos * DIV_Q15; /* 还原到 [-1, 1] */ float sin_da = (float)sc.hSin * DIV_Q15; output.id = input.i_alpha * cos_da + input.i_beta * sin_da; output.iq = -input.i_alpha * sin_da + input.i_beta * cos_da; return output; } alphaBeta_t Rev_Park(dq_t input, int16_t theta) { alphaBeta_t output; Trig_Components sc = Trig_Functions(theta); float cos_da = (float)sc.hCos * DIV_Q15; float sin_da = (float)sc.hSin * DIV_Q15; output.i_alpha = input.id * cos_da - input.iq * sin_da; output.i_beta = input.id * sin_da + input.iq * cos_da; return output; }Trig_Functions(theta)是一个查表函数,输入是电角度索引,输出是 Q15 格式的 sin/cos,值域 ±32768。DIV_Q15的作用是把整数还原成浮点小数。这套写法在 MCU 上非常典型:sin/cos 一个周期通常被切成 2 的整数次幂份(比如 1024 或 4096),用角度索引直接命中表项,避免调用sinf()/cosf()。
参数和边界值得单独说清楚:
theta是int16_t,说明角度被归一化到某个整数范围(常见是 0~65535 对应 0~2π,用uint16_t更自然;用int16_t时要注意负数索引的回绕)。范围选小了,角度分辨率不够,电流环会出现周期性抖动;选大了,查表乘法可能溢出。这是个需要结合 PWM 频率和电频率一起算的参数。sc.hCos和sc.hSin是int16_t,乘DIV_Q15时编译器会做 float 转换。如果 MCU 没有 FPU,这一步的开销不小,实际工程里经常干脆把整个 Park 变换都写成 Q15 定点乘法,用移位代替浮点。- 加减号和公式必须逐字对齐。
id用的是+sin,iq用的是-sin,逆变换的两个符号又反过来。这地方写反不会有编译错误,只会让电机抖动或者原地打转。
把 Clark 和 Park 串起来看,整条链路的本质是这样的:三相电流 → 两相静止 → 两相旋转 → 得到两个直流量 Id、Iq。Id 管励磁,Iq 管转矩,两个量各自接一个 PI,输出再逆变换回 α、β。这才是 FOC 能“像控制直流电机一样控制交流电机”的全部秘密。
4. SVPWM:扇区判断、作用时间与 1.1547 倍母线利用率从哪来
Park 逆变换交出来的是一对 α、β 电压,逆变器没法直接吃。它能做的只有八种开关组合:六个非零矢量(V1~V6,分别对应 100、110、010、011、001、101 六种开关状态)和两个零矢量(V0 = 000、V7 = 111)。SVPWM 要做的是,在一个 PWM 周期里按伏秒平衡把这几块积木拼一拼,让平均效果等于你想要的电压矢量。
4.1 平均值等效:为什么八种状态能拼出任意方向
理论依据是平均值等效原理:在一个开关周期 Ts 内,让相邻两个非零矢量和零矢量各作用一段时间,使它们的伏秒积之和等于目标矢量的伏秒积:
Uref · Ts = Ux · Tx + Uy · Ty + U0 · T0 Tx + Ty + T0 = Ts六个非零矢量的幅值都是 2/3·Udc,在平面上均匀分布,把空间切成六个 60 度的扇区。目标矢量落在哪个扇形里,就用该扇形的两条边界矢量去拼。为什么每个扇区最少要两个矢量?因为两个不共线的矢量才能张成一个平面区域,单个矢量只能覆盖一条射线。
为什么非零矢量的幅值是 2/3·Udc,而不是 Udc?从等效电路推一遍就清楚了。设三相桥臂中点为 A/B/C,母线负端为 N,A 相上桥臂开通时 Ua = Udc,下桥臂开通时 Ua = 0。再考虑三相星形负载中性点 O 的浮动:
Ua + Ub + Uc = 3·UO只有一相上桥、两相下桥时(比如 A 相开),Ua − UO 算出来是 2/3·Udc,Ub − UO 和 Uc − UO 都是 −1/3·Udc。这三者再按 Clark 变换投影到 α-β 平面,合成矢量的长度就是 2/3·Udc。这也是为什么前面强调等幅值变换要和 SVPWM 的矢量尺度对齐——两边用的不是同一个标幺基准,限幅就会失效。
4.2 扇区判断:三个参考量把平面切成六块
先解决“目标矢量在哪”。把 Uα、Uβ 做一次线性组合,构造三个参考量:
Uref1 = Uβ Uref2 = (√3/2)·Uα − (1/2)·Uβ Uref3 = −(√3/2)·Uα − (1/2)·Uβ这三个式子的几何含义是:把目标矢量分别投影到三条与扇区边界垂直的直线上,看落在哪一侧。每个参考量给出一个符号位,三个符号位组合成 N:
A = (Uref1 > 0) B = (Uref2 > 0) C = (Uref3 > 0) N = A + 2B + 4CN 只有六种有效取值,对应关系是:
| N | 3 | 1 | 5 | 4 | 6 | 2 |
|---|---|---|---|---|---|---|
| 扇区 | I | II | III | IV | V | VI |
这段判断在代码里就是三次比较加一次加法,成本极低:
uint8_t Sector_Detect(float Ualpha, float Ubeta) { float Uref1 = Ubeta; float Uref2 = 0.8660254f * Ualpha - 0.5f * Ubeta; /* √3/2 Uα − 1/2 Uβ */ float Uref3 = -0.8660254f * Ualpha - 0.5f * Ubeta; uint8_t A = (Uref1 > 0.0f) ? 1 : 0; uint8_t B = (Uref2 > 0.0f) ? 1 : 0; uint8_t C = (Uref3 > 0.0f) ? 1 : 0; return (uint8_t)(A + 2 * B + 4 * C); }注意 N 的取值顺序(3,1,5,4,6,2)不是 1~6 递增,这是二进制编码的副产物。很多初学者在这里翻车:把 N 直接当扇区号去查表,结果电机转起来一顿一顿的。判断方法很简单——在 Simulink 里让 Uα、Uβ 走一圈单位圆,把 N 打印出来,看它是不是按 3→1→5→4→6→2 的顺序循环。
4.3 作用时间的方法一与方法二:逐扇区硬编码 vs 统一公式
拿到扇区号之后,需要算出两个相邻非零矢量各作用多久。基础推导用正弦定理:
T1 = (√3·Ts/Udc)·(sin(k·60° − θ)·|Uref|) T2 = (√3·Ts/Udc)·(sin(θ − (k−1)·60°)·|Uref|) T0 = Ts − T1 − T2其中 k 是扇区号。把 |Uref|·sinθ 和 |Uref|·cosθ 分别换成 Uβ 和 Uα,就能把每个扇区的表达式写成 Uα、Uβ 的线性组合。
方法一是逐个扇区把六个式子抄下来,每个扇区两个时间变量。它的好处是直观,坏处是六个分支,代码臃肿,且符号错误极难排查。原始笔记里把六个扇区的结果整理成了一张表:
| 扇区 | 先动作矢量 | 后动作矢量 | 先动作时间 t1 | 后动作时间 t2 |
|---|---|---|---|---|
| I | U4 | U6 | k·Uref2 | k·Uref1 |
| II | U2 | U6 | −k·Uref2 | −k·Uref3 |
| III | U2 | U3 | k·Uref1 | k·Uref3 |
| IV | U1 | U3 | −k·Uref1 | −k·Uref2 |
| V | U1 | U5 | k·Uref3 | k·Uref2 |
| VI | U4 | U5 | −k·Uref3 | −k·Uref1 |
其中k = √3·Ts/Udc,Uref1~3 就是扇区判断时用的那三个参考量。
方法二的动机来自对这张表的观察:六个扇区的表达式其实只有三个基本形态,加上正负号和分配顺序的差别。把 K 提出来之后,可以统一写成
Tx = (√3·Ts/Udc)·|Ux|也就是六个分支用同一组公式,只是取哪两个分量、要不要取反不同。这在嵌入式里意义很大——省掉一个 switch 的开销,代码量减半,最重要的是不会因为某个case里少写一个负号而出现只在某个扇区才发作的故障。这类“只在特定角度抖动”的故障极难定位,因为示波器上看电流波形整体正常,只有转到固定相位才毛刺一下。
还有一条原始笔记里特意标注的细节:t1、t2 的命名顺序必须跟开关切换顺序一致。扇区 I 的开关序列是 0→4→6→7→7→6→4→0,所以 t1 是 U4 的时间,t2 是 U6 的时间。这两者调换,波形依然对称,电机依然能转,只是转矩脉动变大,很容易被误判成“参数没调好”而白白耗费几天去调整电流环。T1、T2 的大小写和下标也值得留意——数学推导里 T1 指扇区边界的第一个矢量,代码里可能指第一个动作的矢量,两者不一定对应。
4.4 七段式开关顺序与马鞍波:1.1547 倍的来源
扇区判完、时间算完,最后一步是把时间分配到三相开关上。分配原则只有两条:
- 每次状态切换只改变一相的开关状态,把开关损耗压到最小;
- 零矢量时间对半分配在周期首尾,让 PWM 波形对称,降低谐波。
满足这两条的最优解就是七段式:0 → Vx → Vy → 7 → Vy → Vx → 0。以扇区 I 为例,中间三相比较值可以直接写出来:
/* k 把电压量换算成时间量;Ts 为 PWM 周期,Udc 为母线电压 */ float k = 1.7320508f * Ts / Udc; /* 扇区 I 的两个作用时间(t1 对应 U4=100,t2 对应 U6=110) */ float Uref1 = Ubeta; float Uref2 = 0.8660254f * Ualpha - 0.5f * Ubeta; float t1 = k * Uref2; float t2 = k * Uref1; /* 过调制处理:两个非零矢量总时长不得超过一个周期 */ float t_sum = t1 + t2; if (t_sum > Ts) { float scale = Ts / t_sum; t1 *= scale; t2 *= scale; } /* 七段式:0 → 4 → 6 → 7 → 7 → 6 → 4 → 0 * A 相在 t1、t2 段都为高;B 相只在 t2 段为高;C 相全程为低 */ float Ta = (Ts + t1 + t2) * 0.5f; float Tb = (Ts - t1 + t2) * 0.5f; float Tc = (Ts - t1 - t2) * 0.5f;Ta ≥ Tb ≥ Tc恒成立,这个不等式是七段式正确性的一个免检判据。上电前在调试器里看一眼三个比较值,如果顺序不符,基本可以确定扇区映射表写错了。
现在回答那个最常被问到的问题:为什么 SVPWM 的母线利用率能到 1.1547?
线性调制区里,SVPWM 能合成的最大相电压峰值是六边形的内切圆半径,即 Udc/√3。而正弦波调制(SPWM)不注入任何共模分量,相电压峰值最多到 Udc/2,再往上就进入过调制,波形削顶。
(Udc/√3) / (Udc/2) = 2/√3 ≈ 1.1547高出 15.47%。这 15% 不是从数学技巧里挤出来的,本质是 SVPWM 隐式注入了零序分量,把相电压波形压成了马鞍形。
马鞍波的出现原因:SVPWM 的相电压参考等于“正弦基波 + 零序分量”,零序分量取三相瞬时值的最大值和最小值的负中点,即−(max+min)/2。因为电机中性点不引出,零序电流无通路,零序电压不出现在线电压里,所以注入它不影响转矩,只是把三相参考整体“压扁”,让峰值从 Udc/2 降到 Udc/√3。三相各自看是顶被削平的马鞍形,线电压依然是纯正弦。
这也解释了一个常被混淆的事:基于零序分量注入的 SPWM 和 SVPWM 在数学上等价。前者从调制波角度出发,给三相正弦加−(max+min)/2;后者从矢量角度出发,用两个相邻矢量加零矢量去拼。两条路走到同一个占空比上。工程上倾向直接实现零序注入,因为不用做扇区判断,代码更短,也不会因为扇区表写错而出现只在某个区域发作的故障。
5. 常见问题与排查:积分饱和、过调制、Q15 溢出与相位对不齐
前面四章是“应该怎么做”,这一章是“实际做的时候会在哪儿出问题”。下面五条都是能复现、现象明确、原因可倒推的,按现象、原因、解决三段写。
5.1 五条踩坑记录:从现象倒推机制
第一条:启动瞬间冲到限幅,稳定后长时间振荡回不来。
现象是给一个阶跃给定,输出直接顶到限幅值,之后给定撤掉或者反向,输出要等好几秒才退下来,期间电机明显过冲。
原因是积分饱和。位置式 PID 里积分项是Ki·Σe·dt,误差大的时候积分项一直在累加,即使输出已经顶到限幅,积分器并不知道,还在闷头往上冲。等误差反号了,积分项里积攒的那一大坨要花很长时间才能被抵消掉,这段时间输出就一直卡在限幅上。
解决办法是把积分项从总输出里拆出来单独限幅,限制范围取±out_max,而不是只限总输出:
p->i_term += p->Ki * err * dt; /* 关键:积分项自己也要限幅,否则它会在输出饱和期间持续累加 */ if (p->i_term > p->out_max) p->i_term = p->out_max; if (p->i_term < -p->out_max) p->i_term = -p->out_max; float out = p->Kp * err + p->i_term + p->Kd * (err - p->err_prev) / dt;更彻底的两种做法是积分分离和反向计算。积分分离是设一个误差阈值,误差超过阈值时干脆不积分,只走 PD;反向计算是把“输出被削掉的那部分”乘一个系数反灌回积分器,让积分项主动往回退。电流环这种给定变化剧烈的场合,积分分离更常用,因为实现简单,且不会引入额外的时间常数。
第二条:母线电压一高,电流波形顶部就削平,转矩脉动变大。
现象是低速轻载正常,一加重载或者提高母线电压,相电流峰顶开始变平,示波器上能明显看到削顶。
原因是过调制。给定矢量的幅值超过了六边形内切圆,t1 + t2 > Ts,零矢量时间被算成负数,占空比超出 [0,1] 后被硬件钳位,等效成波形削顶。很多人第一反应是去改电流环参数,方向完全错了,参数再调也调不出被削掉的电压。
解决办法是在算完 t1、t2 之后立刻做比例缩放,也就是 4.4 节代码里那段if (t_sum > Ts)。缩放的好处是保持矢量方向不变,只压缩幅值,转矩方向不丢;坏处是缩放瞬间相当于给电压环加了个非线性环节,如果给定本身在边界附近抖动,会引起周期性畸变。更稳的做法是前面加一级磁链圆限制,把超限的矢量提前压回圆内,别让 SVPWM 去处理边界情况。
第三条:Q15 三角函数表查出来的值偶尔跳变,电流环出现周期性毛刺。
现象是电机匀速转,电流上叠加一个和电频率同频的小毛刺,改 PID 参数无效。
原因通常是角度索引越界或者回绕处理错误。Trig_Functions(theta)的入参如果是int16_t,负角度会变成大正数,查表落到周期末尾,sin 和 cos 的相位关系瞬间反转。另一种是角度累加溢出:theta += delta用 16 位整数累加,转几圈之后回绕,如果查表函数没做取模,就会访问到表外内存。
解决办法是把角度归一化做在累加的那一侧,而不是查表那一侧:每次累加后立刻对 2π 对应的整数周期取模,保证进入查表函数的值恒在合法区间。同时把hSin/hCos的类型和DIV_Q15的换算关系写死在一个宏里,别在多个文件里各自写一份 1/32768,改一处漏一处。
第四条:电机能转,但一给转矩就飞车或者原地抖。
现象是开环能给电角度转起来,切到闭环立刻失控,或者给定很小的 Iq 就全速旋转。
原因基本锁定在相位上:要么 θ 的零点定义和 Park 变换假设的不一致(差 90 度),要么 Iq 的正负号和编码器/霍尔的方向相反,要么电流采样两相的对应关系搞反了。这三个错误的表现高度相似,靠示波器看波形分不出来,只能靠逻辑排除。
排查顺序建议固定:先确认 θ 的零点(手动把转子拖到某个已知位置,读 θ 值),再确认 Iq 的正负(给一个小的正 Iq,转子应该往正方向产生转矩),最后确认电流采样通道(给纯 A 相电流,看 Clark 输出的 Iβ 是不是 0)。三步走完还没解决,再回头怀疑参数。
第五条:低速时电流环正常,一到高速就失稳。
原因是采样和控制延时。电流环的带宽受限于 PWM 频率和 ADC 采样时刻,转速上去之后,一个 PWM 周期内电角度已经转过了可观的角度,用上个周期的 θ 去算这个周期的 Park 变换,等于引入了一个随转速增长的相位滞后。转速越高,滞后越大,相位裕度被吃光就振荡。
解决办法有两个方向:一是把 θ 做一阶或二阶外推,用θ + ω·Tdelay补偿这个滞后,代价是需要知道延时具体是多少个周期;二是降低电流环带宽,把 Kp 调小,用性能换稳定。工程上通常两个一起用,先把延时补偿做到位,再留一点裕量。
5.2 用一组自检把问题挡在上电之前
上面五条里有四条是“上电才发现”,其实大部分可以在上电前用数学自检挡住。我习惯在代码里留一组编译期开关的自检函数,主要查三件事:
第一,查 Clark 和 Park 的往返一致性。随便给一组 (Ia, Ib, Ic),走一遍 Clark → Park → 逆 Park → 逆 Clark,看能不能原样回来。误差应该在浮点精度量级,如果差出 1% 以上,说明某个变换的系数或者符号写错了。
第二,查扇区判断的连续性。让 Uα、Uβ 沿单位圆扫 360 度,记录 N 值的序列,应该严格是 3→1→5→4→6→2 循环,中间不跳变、不停留。这个检查成本极低,却能挡住扇区映射表这一类最典型的错误。
第三,查占空比边界。给定一组极端工况(最大调制比、最小母线电压),把 Ta、Tb、Tc 打出来,确认都在 [0, Ts] 区间内且满足Ta ≥ Tb ≥ Tc(扇区 I)。边界检查放在算法内部而不是靠硬件钳位,好处是能定位到具体哪一步算错了,而不是只看到一个削顶的波形。
6. 等幅值还是恒功率:一个系数背后的取舍与验证方法
整条链路走完,回头再看开头那个 2/3 和 √(2/3) 的选择,会发现它不是一个数学问题,而是一个贯穿全链路的标幺基准问题。
选等幅值,好处是 d/q 电流的数值直接等于相电流峰值,电流采样电路的放大倍数、PI 的限幅值、示波器上读到的数,全是同一套刻度,调试时脑子里不用做换算。SVPWM 那边也省事,最大矢量幅值就是 Udc/√3,限幅圆半径直接写死。代价是变换不保功率,如果你要在 Simulink 里做功率核算,得自己补一个 3/2 的系数。
选恒功率,好处是功率守恒,能量流向上自洽,适合做理论分析或者需要严格功率计算的场合。代价是所有量的尺度都变了,SVPWM 的限幅圆半径要跟着换算,换算漏一处就是过调制。原始笔记里那句“用等功率变换会导致矢量超出圆圈”,根子就在这里。
| 对比项 | 等幅值变换(k = 2/3) | 恒功率变换(k = √(2/3)) |
|---|---|---|
| 变换前后幅值 | 相等 | 不相等 |
| 变换前后功率 | 不相等 | 相等 |
| d/q 电流数值 | 等于相电流峰值 | 偏小约 18% |
| SVPWM 限幅圆 | Udc/√3 | 需换算 |
| 典型场合 | 嵌入式 FOC 实现 | 理论分析、功率核算 |
如果一定要我给建议:做嵌入式实现,用等幅值,别犹豫。理由是它把“物理量”和“代码里的数”对齐了,对齐之后所有的限幅、标定、波形观察都能直接用直觉判断;而恒功率带来的那点理论优雅,在中断服务函数里一分钱不值。
验证方法也很朴素。在 Simulink 里搭一条最小链路:三相正弦电流源 → Clark → Park(θ 用理想斜坡)→ 逆 Park → 逆 Clark,看输出和输入是否重合。然后把 θ 停住不动,给一个恒定的 Id、Iq,看逆变换出来的 α、β 是不是在画圆。最后把 SVPWM 接上,让它驱动一个一阶惯性环节模拟电机,看相电流是不是正弦、线电压峰值是不是 Udc。三张图看完,整条链路的尺度关系就全对上了。
我自己的教训是:刚开始学的时候嫌推导繁琐,直接抄了一份代码,跑起来电机也转,就以为懂了。后来换了个母线电压,电流波形立刻削顶,查了两天才发现是限幅圆半径用的是相电压峰值而不是 Udc/√3——那个 2/3 的系数在代码里被漏掉了。从那以后我每次移植 FOC 代码,都强制走一遍“喂已知输入、看已知输出”的往返验证,哪怕只是改了一个常数。希望帮到你。
本文还有配套的精品资源,点击获取