简介:这份FOC学习笔记面向正在入门或进阶电机控制的工程师与学生,围绕磁场定向控制中从数学推导到Simulink及代码实现的完整链路展开,帮助读者理清Clark、Park及逆变换、SVPWM与PID控制之间的逻辑关系。内容覆盖等幅值与恒功率变换系数、SVPWM扇区判断、非零矢量和零矢量作用时间、过调制处理、扇区切换点、马鞍波成因与零序分量注入SPWM,也整理了位置式和增量式PID、参数整定、积分抗饱和及磁链圆限制的程序实现思路。压缩包为1个PDF文件,大小7.34MB,适合按章节对照推导和算法代码逐步学习。已有1920人学习,可作为FOC课程复习、项目调试和算法实现的参考笔记。
1. 从一次上电抖动说起:这份 FOC 笔记里都有什么
自己画的第一块驱动板上电,电机不转,只有“哒哒”的抖动和一声过流保护。查了两天硬件,最后发现是 Park 变换里那个电角度晚了一拍。这类问题翻芯片手册是翻不出来的,只能回到数学本身。这份 FOC 学习笔记就是从那阵子开始攒的:超前角控制为什么能出最大转矩、Clarke 与 Park 正逆变换怎么推、SVPWM 的扇区判断和作用时间两种算法、过调制与马鞍波的来历、PID 位置式和增量式的差别与积分抗饱和、磁链圆限制里 MAX_MODULE 和 START_INDEX 怎么定,每一块都配了能直接抄进工程的 C 代码。
它更适合已经在 Simulink 里搭过电机模型、正准备把算法落到 MCU 上的人,也适合刚接手一套无感 FOC 代码、看 SVPWM 那段注释发懵的人。纯新手拿它当第一本书会有点吃力,但配合着调一次板子,收获会比读十篇综述实在得多。
2. 坐标系三连击:Clarke、Park 与逆 Park 的推导和 C 实现
2.1 Clarke 变换:把三相 120° 掰成两维正交
三相电流 Ia、Ib、Ic 在空间上互差 120°,任何一个时刻三者之和为零(基尔霍夫电流定律,电机三相星形连接没有中线)。这一条约束很关键,它意味着三个变量里只有两个是独立的,第三个永远可以被另外两个表示出来——Clarke 变换本质上就是这次降维。
把三相轴上的电流投影到一个静止的 αβ 正交坐标系上,α 轴与 A 相重合,β 轴超前 90°:
Iα = k * ( Ia - Ib*sin30° - Ic*sin30° ) = k * ( Ia - Ib/2 - Ic/2 ) Iβ = k * ( Ib*sin60° - Ic*sin60° ) = k * ( √3/2*Ib - √3/2*Ic )代入 Ic = -Ia - Ib,式子立刻塌缩成两项:
Iα = k * (3/2) * Ia Iβ = k * (√3/2) * (Ia + 2*Ib)这里 k 是变换系数,取 2/3 就是等幅值变换,取 √(2/3) 就是恒功率变换。选定 k 之后,两式就变成了两条乘加,MCU 上一个周期只要几个指令。顺手也能把反变换写出来,做电流重构或者离线验证波形时要用:
Ia = Iα Ib = -Iα/2 + (√3/2)*Iβ Ic = -Iα/2 - (√3/2)*Iβ拿一组数验一下:Ia=1、Ib=-0.5、Ic=-0.5(标准的 A 相峰值时刻)。等幅值变换下 Iα=1、Iβ=(1-1)/√3=0,幅值原封不动,方向就在 α 轴上,符合直觉。这就是等幅值变换好用的地方——你算出来的 Iαβ 幅值,和示波器上钳出来的相电流峰值是同一个数,调试的时候不用在脑子里再做一次换算。
2.2 系数怎么选:等幅值还是恒功率
这一步选错,后面所有的限幅阈值、PI 参数、SVPWM 调制比全要跟着重算,是最容易埋雷的地方。两种系数的差别整理成一张表更清楚:
| 变换方式 | 系数 k | αβ 幅值与原三相幅值的关系 | 功率关系 | 实际用途 |
|---|---|---|---|---|
| 等幅值变换 | 2/3 ≈ 0.6667 | 相等,Iαβ 峰值 = 相电流峰值 | 变换后功率偏小 2/3 | FOC 工程默认 |
| 恒功率变换 | √(2/3) ≈ 0.8165 | 放大约 √(3/2) 倍 | 功率守恒 | 理论推导、Simulink 建模 |
笔记里的结论很直白:实际 FOC 设计一般用等幅值变换。原因在 SVPWM 那一环——如果 Clarke 用恒功率系数,合成出来的电压矢量幅值会被抬高,很容易顶出空间矢量的圆形边界,直接进过调制区,波形削顶、电流畸变,然后你会在电流环上找半天原因。
注意:笔记里提到的“1.5 倍”这个数字,取决于你在 SVPWM 那一步把基本矢量幅值定义成 2/3·Udc 还是别的写法。工程上不必纠结这个具体倍数,只要记住一条:Clarke 系数、SVPWM 矢量幅值定义、磁链圆限幅阈值这三者必须成套使用,中途换一个,另外两个就得跟着改。
还有一个工程上的小便宜:因为 Ia+Ib+Ic=0,采样电路只需要两路电流(通常采 A、B 两相),C 相用减法算出来。省一路运放、省一个 ADC 通道,成本就是这么抠出来的。
2.3 Park 变换:把交流量变成两个直流量
Clarke 之后虽然只有两个变量了,但它们仍是随时间做正弦变化的交流量,PI 控制器对交流量没法做到零稳态误差——比例项永远滞后,积分项在过零点附近来回补。Park 变换做的事,是把参考坐标系跟着转子一起转,站到转子上去看定子电流。对转子而言,定子磁场是静止的,于是原本的正弦量在 dq 轴上变成了两条直线。
数学上就是一次旋转投影,θ 是转子当前的电角度:
Id = Iα*cosθ + Iβ*sinθ Iq = -Iα*sinθ + Iβ*cosθ反过来,PI 算完的 Ud、Uq 要送回静止坐标系去生成 PWM,用的是逆变换。因为 Park 的变换矩阵是正交矩阵,逆矩阵等于转置,所以逆变换的形式就是把符号挪一下:
Iα = Id*cosθ - Iq*sinθ Iβ = Id*sinθ + Iq*cosθ关于 θ,笔记里有一段我觉得比公式更值钱的论述:电角度本身怎么定义都行,可以是 d 轴和 α 轴的夹角,也可以是 q 轴和 A 相的夹角,甚至可以是某个角平分线。因为电角度存在的唯一意义,就是告诉算法“转子现在在哪个方向”,好让定子磁场超前它 90°。之所以大家都定义成 d 轴与 α 轴的夹角,纯粹是为了让 Park 和反 Park 里乘的系数正好是 sinθ、cosθ,算起来最省事。
超前 90° 这个事是 FOC 的核心:转子永磁体会努力转到内部磁力线与外部磁场方向一致的位置,当定子磁场超前转子 90° 时,转矩取到最大值 F·r(θ=90° 时 sinθ 取到 1)。超前角控制的全部内容就是这个。
2.4 三份能直接抄进工程的 C 实现
笔记里给的三段代码非常接近量产代码的样子,值得逐行看。先看 Clarke:
alphaBeta_t Clark(abc_t input) { alphaBeta_t output; output.i_alpha = input.ia; // Iα = Ia,等幅值变换的直接结果 output.i_beta = (input.ia + 2 * input.ib) * 0.577350269f; // Iβ = (Ia + 2Ib)/√3 return output; }这个 0.577350269 就是 1/√3。之所以不写成除法,是因为定点 MCU 上除法代价高,编译器对常量乘法的优化也更彻底。输入是三相结构体,输出是两相结构体,全程不涉及三角函数,一个控制周期里跑几十次也不心疼。
再看 Park 和逆 Park,这两段共用一份三角函数查表:
dq_t Park(alphaBeta_t input, int16_t theta) { dq_t output; Trig_Components TempSinCos; float cos_da, sin_da; TempSinCos = Trig_Functions(theta); // 查表得到 Q15 格式的 sin/cos cos_da = (float)(TempSinCos.hCos * DIV_Q15); // DIV_Q15 = 1/32768,还原成 -1~1 sin_da = (float)(TempSinCos.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 TempSinCos; float cos_da, sin_da; TempSinCos = Trig_Functions(theta); cos_da = (float)(TempSinCos.hCos * DIV_Q15); sin_da = (float)(TempSinCos.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; }几个参数值得单独说清楚。theta是 int16_t,说明角度用的是定点表示,常见的做法是把 0~2π 映射到 0~65535(int16_t 溢出后回绕正好对应角度绕圈),也有的实现映射到 0~32767 再在查表函数里左移一位,具体看Trig_Functions的实现。hSin、hCos是 Q15 定点数,取值范围 -32768~32767,代表 -1~1,所以要乘DIV_Q15还原。查表而不是调sinf(),一是快,二是很多 M0/M3 上根本没有硬件浮点,库函数的耗时不可控。
提示:查表精度直接决定电流环的谐波含量。256 点表在 20kHz 载波、低电角度转速下够用,但如果你的电机极对数高、转速快,一个控制周期内电角度跳动可能超过好几个表项,这时要么加线性插值,要么换 512/1024 点表。我一般会先把表长和插值方式定死,再谈别的优化。
3. SVPWM 落地:扇区判断、作用时间与马鞍波的来历
3.1 平均值等效:六个矢量拼出一个圆
SVPWM 的理论基础是平均值等效原理:在一个开关周期内,用若干个基本电压矢量的组合,让它们的平均值等于你想要的参考电压矢量。三相逆变桥的六个开关管一共有 8 种有效开关状态,对应 6 个非零矢量(U4、U6、U2、U3、U1、U5,相邻间隔 60°)和 2 个零矢量(U0、U7)。每个非零矢量的幅值是 2/3·Udc,方向固定在六个角度上。
参考矢量落在哪个 60° 扇区里,就用这个扇区相邻的两个非零矢量加零矢量去合成,按伏秒平衡:
Uref * Ts = Ux * T1 + Uy * T2 + U0 * T0 Ts = T1 + T2 + T0以扇区 I 为例,U4 在 0°、U6 在 60°,参考矢量与 U4 的夹角为 θ,用正弦定理能解出两个作用时间:
T1 = (√3 * Ts / Udc) * |Uref| * sin(60° - θ) = (√3 * Ts / Udc) * (√3/2*Uα - 1/2*Uβ) T2 = (√3 * Ts / Udc) * |Uref| * sin(θ) = (√3 * Ts / Udc) * Uβ T0 = T7 = (Ts - T1 - T2) / 2定义调制比 m = √3·|Uref|/Udc,线性区要求 m ≤ 1,也就是 |Uref| ≤ Udc/√3 ≈ 0.577·Udc。而传统 SPWM 的线性区上限是 0.5·Udc,两者一比就是 2/√3 ≈ 1.1547,母线电压利用率高出 15.47%。这就是 SVPWM 在电机驱动里几乎完全取代 SPWM 的原因,白捡的 15% 电压。
3.2 扇区判断:N = A + 2B + 4C 是怎么来的
直接用 atan2 求角度再判断扇区当然可以,但在 MCU 上既慢又没必要。笔记里的做法是构造三个辅助变量:
Uref1 = Uβ Uref2 = (√3*Uα - Uβ) / 2 Uref3 = (-√3*Uα - Uβ) / 2这三个变量其实就是参考矢量在三条 60° 分界线法线上的投影。分别判断它们的符号:Uref1>0 则 A=1,Uref2>0 则 B=1,Uref3>0 则 C=1。然后令 N = A + 2B + 4C,N 与扇区的对应关系是:
| N | 3 | 1 | 5 | 4 | 6 | 2 |
|---|---|---|---|---|---|---|
| 扇区 | I | II | III | IV | V | VI |
这个映射不是随手编的,是三个符号位排列组合的必然结果。验证一下扇区 I(θ=30°,Uα=0.866、Uβ=0.5):Uref1=0.5>0 得 A=1;Uref2=(1.5-0.5)/2=0.5>0 得 B=1;Uref3=(-1.5-0.5)/2=-1<0 得 C=0;N=1+2=3,对应扇区 I。再验扇区 V(θ=270°,Uα=0、Uβ=-1):A=0、Uref2=0.5>0 得 B=1、Uref3=0.5>0 得 C=1,N=2+4=6,对应扇区 V。符号判断加两次乘加,比查三角函数表快一个数量级。
3.3 作用时间计算:六个扇区折叠成三个中间量
笔记里给了两种算法。方法一是老老实实按扇区把公式展开,六个扇区六组式子,代码里就是一个大 switch,可读性好、便于对照推导,缺点是代码长、每个分支都要单独验证。方法二是把六个扇区的表达式折叠成三个中间量,先统一算出来再按扇区取用:
// 三个中间量,Ts 为 PWM 周期,Udc 为母线电压 float X = (1.7320508f * Ts / Udc) * Ubeta; float Y = (Ts / Udc) * (1.5f * Ualpha + 0.8660254f * Ubeta); float Z = (Ts / Udc) * (-1.5f * Ualpha + 0.8660254f * Ubeta);这三个量不是拍脑袋凑的,随便挑两个扇区推一遍就能对上。比如扇区 II(U6 在 60°、U2 在 120°),把 θ 换成 θ-60° 重新套正弦定理,化简出来正好是 T6 = Y、T2 = Z。六个扇区推完,得到下面这张表,代码里就是一个 6 分支的 switch,每个分支只有两次赋值:
| 扇区 | 先作用矢量及其时间 | 后作用矢量及其时间 |
|---|---|---|
| I | U4,T4 = -Z | U6,T6 = X |
| II | U6,T6 = Y | U2,T2 = Z |
| III | U2,T2 = X | U3,T3 = -Y |
| IV | U3,T3 = Z | U1,T1 = -X |
| V | U1,T1 = -Y | U5,T5 = -Z |
| VI | U5,T5 = -X | U4,T4 = Y |
实现的时候有一个必须小心的点:T1、T2 谁是先作用、谁是后作用,取决于扇区的开关切换顺序,不能混。笔记里专门强调过“T1、T2 的顺序给定是按照开关切换顺序给的”,这句话在调波形的时候会救你一命。写反了不会报错,只是电流波形的谐波分布变差、开关次数变多,很难第一时间发现。
3.4 过调制处理与切换点的确定
实际运行中 |Uref| 会短暂超出线性区(突加负载、母线跌落、速度环积分冲高),这时 T1+T2 > Ts,如果直接往下算,T0 会变成负数,比较值倒挂,输出一堆畸变脉冲。工程上有两种处理:
float Tsum = T1 + T2; if (Tsum > Ts) { // 等比例缩放,保持矢量角度不变 T1 = T1 * Ts / Tsum; T2 = T2 * Ts / Tsum; Tsum = Ts; } float T0 = (Ts - Tsum) * 0.5f; // 零矢量均分给 U0 和 U7等比例缩放是“最小幅值误差”的做法,实现最简单,代价是角度不变但幅值被压。另一种是六边形过调制,让矢量贴着六边形边界走,电压利用率更高,但要做分段处理,代码复杂度上去了。多数低成本驱动器用前一种就够了。
切换点(也叫比较值)的确定,对应七段式对称 PWM。以扇区 I 为例,开关顺序是 0→4→6→7→7→6→4→0,三相比较值分别是:
Tcm1 = (Ts - T1 - T2) / 4 // 最先开通的那一相 Tcm2 = Tcm1 + T1 / 2 Tcm3 = Tcm2 + T2 / 2这三个值再按当前扇区分配给 A、B、C 三相。分配表在笔记里是列全了的,抄的时候建议直接把表做成二维数组,TcmA = Tcm[扇区][0]这样取,比一堆 if-else 好维护。对称分配零矢量的好处是 PWM 波形关于周期中点对称,谐波分量明显更低。
3.5 马鞍波:相电压为什么不是正弦
第一次看 SVPWM 的相电压波形会疑惑:不是说要生成正弦磁场吗,怎么相电压是马鞍形的?原因在于 SVPWM 等效于在正弦调制波里注入了零序分量(主要是三次谐波)。三次谐波在三相里同相位,线电压里互相抵消,所以线电压仍然是标准的正弦,电机看到的磁链也仍然是圆的;但相电压的峰值被“削”掉了一块,等于在不增加母线电压的前提下多榨出了调制深度。
自己实现一版零序注入 SPWM 是理解这件事最快的路径:
// 三相对称正弦调制波,m 为调制比 float ua = m * sinf(theta); float ub = m * sinf(theta - 2.0943951f); // -2π/3 float uc = m * sinf(theta + 2.0943951f); // +2π/3 // 注入零序:取三相中的最大最小值的中点 float umax = fmaxf(ua, fmaxf(ub, uc)); float umin = fminf(ua, fminf(ub, uc)); float u0 = -(umax + umin) * 0.5f; float da = (ua + u0) * 0.5f + 0.5f; // 归一化到 0~1 占空比 float db = (ub + u0) * 0.5f + 0.5f; float dc = (uc + u0) * 0.5f + 0.5f;(umax + umin)/2这一项就是零序分量,把它从三相里减掉,中间那一段被拉平,波形就成了马鞍。也可以用k*sin(3θ)的显式三次谐波注入,k 取 1/6 附近效果接近,但极值点对齐不如最大最小值法精确。这两条路殊途同归,最后都能把母线利用率推到 Udc/√3。
注意:零序注入和 SVPWM 在数学上等价,但在代码里不等价。SVPWM 走的是扇区和矢量作用时间,天然带过调制处理;零序注入走的是占空比,超调时只能靠占空比钳位。选哪条路要看你后面还想不想加死区补偿和过调制策略。
4. PID 与积分抗饱和:电流环从能转到转得稳
4.1 位置式与增量式:离散化之后差在哪
连续域的 PID 是 u(t) = Kp·e(t) + Ki∫e dt + Kd·de/dt,落到 MCU 上要按采样周期 Ts 离散。位置式直接把积分换成累加:
u(k) = Kp*e(k) + Ki*Ts*Σe(j) + (Kd/Ts)*(e(k) - e(k-1))增量式算的是这一次输出相对上一次的增量:
Δu(k) = Kp*(e(k)-e(k-1)) + Ki*Ts*e(k) + (Kd/Ts)*(e(k)-2e(k-1)+e(k-2)) u(k) = u(k-1) + Δu(k)两者的差别不只是写法。位置式有显式的积分累加器,好处是输出直接对应绝对量、多路输出切换时不会有突变,坏处是这个累加器一旦被顶到饱和,退出来要很久,这就是积分饱和的根源。增量式没有独立累加器(u(k-1) 本身隐含了历史),天然抗饱和一部分,但如果执行机构有行程限制,增量式同样会“算出来的增量执行不了”,累积误差依然存在。
| 对比项 | 位置式 PID | 增量式 PID |
|---|---|---|
| 积分实现 | 显式累加器 | 隐含在 u(k-1) 里 |
| 积分饱和风险 | 高,必须做抗饱和 | 较低,但仍需限幅 |
| 手动/自动切换 | 冲击大,要做无扰切换 | 冲击小 |
| 计算量 | 略小 | 多一次历史误差存储 |
| 典型用途 | 电流环、速度环 | 步进/阀门等带积分的执行器 |
FOC 的电流环和速度环基本都是位置式,因为你要的就是一个绝对电压指令或转矩指令,而且必须配抗饱和。
4.2 参数整定:先 P 后 I 的顺序不能反
电流环的对象近似是一阶 RL 环节,带宽可以算:Kp = L·ωc,Ki = R·ωc,其中 L、R 是相电感和相电阻,ωc 是期望闭环带宽(一般取开关频率的 1/10 到 1/20)。这套公式能给你一个靠谱的起点,但真正的定值还是得上电调。
我一般会按这个顺序走:先把 Ki 置 0、Kd 置 0,只留 Kp,给一个阶跃电流指令,看上升时间和超调;Kp 加到响应够快但还没开始振荡,再往回退一点;然后慢慢加 Ki,把稳态误差压掉,加到阶跃响应的超调开始变大就停;最后看需不需要 D,电流环通常不用 D,噪声会被放大,速度环有时会加一点点。
有个很常见的坑:Ki 的量纲里带着 Ts。你把控制频率从 10kHz 改到 20kHz,Ki 必须减半,否则积分作用直接翻倍,原本稳的环就振起来了。移植代码时这一条最容易忘。
4.3 积分抗饱和:三种写法和它们各自的适用面
积分饱和的现象很好认:启动或者堵转的瞬间,误差 e 一直很大,积分项一路累加到一个巨大的值;等实际电流追上来了,误差反向,积分器还要花好几个周期才能把这个大数消化掉,输出的表现就是电流冲得很高再慢慢回落,甚至来回振荡。
三种常用的处理方式:
一是积分限幅,给累加器加一个上下界,简单粗暴,绝大多数场合够用。二是积分分离,误差绝对值超过
本文还有配套的精品资源,点击获取