做电机控制的同行一定都有过这样的经历:明明把FOC的思想翻来覆去读了好几遍,书上、视频里、论文里的公式也都能默写,可真到自己动手写程序的时候,却总卡在电流环这里——pi参数的Kp和Ki到底怎么给?为什么电流环一加大增益电机就开始嗡嗡叫?为什么照搬别人的参数到自己板子上就完全不是那回事?
这篇文章我就把手上的“升魂浩荡FOC驱动”项目里最核心的电流环推导过程、程序框架和调试心得完整盘一遍。内容不会只停在“公式给你”的层面,我会把每一步推导的缘由、每一个参数背后的物理意义、每一段代码在中断里跑的时序都拆开讲清楚。不管你是刚接触FOC的新手,还是已经能跑通开环但困在闭环调试的进阶玩家,这篇文章都能给你一套可以立刻上手的方案。
1. FOC整套思路拆开看:它到底在解决什么问题
1.1 为什么三相电机的力矩这么难控
在动手写FOC程序之前,我得先花点篇幅把“为什么非要绕这么一大圈”这件事讲透。如果你只是想照着例程抄代码,那可以跳过这一节;但如果你想真正独立调试、遇到问题能自己分析,这部分是你绕不过去的地基。
先回到最朴素的直流电机。直流电机的力矩公式是T = Kt·I,电流I直接就是力矩,控制策略极其简单——PID一下就完事。但三相永磁同步电机(PMSM)不是这样,它的三相绕组在空间上互差120°分布,通入三相正弦电流后,定子会产生一个旋转的磁场,这个旋转磁场拉着转子上的永磁体同步转动。问题在于:三相正弦电流的幅值、频率和相位时刻在变化,而力矩是由定子磁场和转子永磁体的相对位置共同决定的。同一组电流,转子在不同的角度,产生的力矩方向可能完全不一样。
这就是交流电机控制的本质难题:我们想要控制的其实是力矩,但直接能施加的是三相电压(或电流),而这三相电流和力矩之间隔了一个转子的实时位置。用大白话说,就是“你手里有一杆枪,但目标在转盘上,你得跟着转盘转才能瞄准”。如果只是简单地给三相绕组通正弦电压,电机能转,但力矩利用率很低,动态响应也差,负载一变就容易失步,更别说精确定位了。
FOC的核心思路,就是想尽一切办法把三相交流电机“伪装”成直流电机来控:通过坐标变换,把定子电流分解成独立的励磁分量(id)和力矩分量(iq),让它们都变成直流量的形式,然后像控制直流电机一样分别对这两个值做PI闭环。
1.2 Clarke、Park两个变换,装一次就忘不掉
既然要“把目标从转盘上拿下来”,那就要建立一个跟着转子一起旋转的坐标系。这个坐标系叫dq旋转坐标系。从三相静止的abc坐标系转换到dq旋转坐标系,中间要经过两步:
第一步叫Clarke变换。把三相静止坐标系(a轴、b轴、c轴,空间上互差120°)投影到一个固定的两相直角坐标系(αβ轴)上。相当于把三根互相响应的“斜线”折算到两根垂直的“横竖线”上。等幅值变换的公式是这样的:
iα = ia - (1/2)·ib - (1/2)·ic iβ = (√3/2)·(ib - ic)
只要ia + ib + ic = 0(星形连接且没有中线电流的电机天然满足),公式还能简化成:
iα = ia iβ = (1/√3)·(ia + 2·ib)
第二步叫Park变换。把这个静止的αβ坐标系再旋转θ电角度,让d轴对准转子永磁体的N极方向,就得到了dq旋转坐标系:
id = iα·cosθ + iβ·sinθ iq = -iα·sinθ + iβ·cosθ
到这里,原本是正弦波的三相电流,在dq坐标系下就变成了两个直流量。id代表的是定子电流中“励磁”方向的分量,iq代表的是“力矩”方向的分量。对表贴式PMSM而言,控制的目标一般是让id=0(全部电流都用来出力),然后只用iq一个直流量去控力矩。
我当年第一次看这几个公式的时候觉得很简单,但真正理解“为什么要转这么两下”是花了一些时间的。你可以在脑子里想象一个旋转木马:站在地面上看,旁边那匹小马在绕着圈跑(正弦波,一直在变);但如果你自己也骑一匹马跟它同步绕圈,那看过去它就像是静止的(直流量)。Clarke+Park就是让你“骑上去”的那个动作。
2. 电流环推导:这是整个FOC里最“数学”的部分
2.1 PMSM的dq轴电压方程,每一相都是有物理含义的
电流环推导的起点,是PMSM在dq旋转坐标系下的电压方程。这一步是整个电流环的数学模型基础,后面所有的整定公式都从这里长出来的。我直接写出我项目里用的表贴式PMSM(Ld=Lq=L)的电压方程:
ud = R·id + L·(d_id/dt) - ωe·L·iq uq = R·iq + L·(d_iq/dt) + ωe·L·id + ωe·ψf
这看起来只是两个公式,但每一项背后都有明确的物理意义,我在调试时就是靠逐项理解来排查问题的:
第一项R·id和R·iq是定子电阻压降,很好理解,就是欧姆定律。
第二项L·(d_id/dt)和L·(d_iq/dt)是电感上电流变化引起的感应电动势,专业说法叫脉动电动势,也就是电感“阻碍电流变化”的那部分能力。这一项是电流环被控对象里最核心的“动态项”,电流环调节的本质就是在克服这项“惯性”。
第三项-ωe·L·iq和+ωe·L·id是交叉耦合项。d轴电流的变化会通过旋转在q轴感应出电动势,反之亦然。也就是说,id和iq这两条轴并不是独立的——你猛拉iq,会在d轴上产生一个干扰。这就是为什么电流环需要前馈解耦,后面我会详细讲。
第四项+ωe·ψf是永磁体磁链旋转产生的反电动势(EMF),它只出现在q轴方程里。电机转速越高,这一项越大,如果不补偿,它对电流环来说就是一个随转速变化的强扰动。
如果只从电流环的角度看这个方程,其实可以把它套成更通用的形式:ud' = R·id + L·(d_id/dt),其中ud' = ud + ωe·L·iq,uq' = R·iq + L·(d_iq/dt),其中uq' = uq - ωe·L·id - ωe·ψf。
这样处理后,d轴和q轴就变成了结构完全相同的“一阶RL串联电路”,也就是一个标准的惯性环节。电流环所有的PI整定,都是基于这个一阶模型来做的。这一步化简特别关键,它能让你瞬间看透电流环的本质:不管外部条件变化多大,电流环始终面对的是一个R和L串联的简单对象。
2.2 电流环PI参数推导:极点对消整定法
有了上面的被控对象模型,接下来就可以推导PI参数了。这是我个人认为做FOC必会的一步。电流环的开环传递函数(被控对象)是:
G(s) = 1 / (L·s + R)
这是一个典型的一阶惯性环节。我们要设计一个PI调节器:
C(s) = Kp + Ki / s = Kp·(s + Ki/Kp) / s
PI调节器的零点为s = -Ki/Kp。如果让这个零点恰好与被控对象的极点s = -R/L重合,也就是令Ki/Kp = R/L,那零点和极点就可以对消。此时系统开环传递函数变成:
C(s)·G(s) = Kp / (L·s)
闭环传递函数为:
Gc(s) = Kp / (L·s + Kp) = 1 / ((L/Kp)·s + 1)
这是一个一阶惯性环节,它的带宽是:
ωbw = Kp / L
所以,只要我根据自己的需求定了电流环带宽ωbw,就能反推出Kp和Ki:
Kp = L·ωbw Ki = R·ωbw
这个公式我在实际项目中用了无数次,简洁高效。但要注意的是:这里的R和L都是电机参数,ωbw是电流环的闭环带宽,单位是rad/s。
我项目里用的电机参数是:定子电阻R=0.5Ω,电感L=1mH(1e-3H),期望电流环带宽ωbw = 2π·2000 rad/s(也就是2kHz带宽)。代入公式算出来:
Kp = 1e-3 × 2π×2000 = 12.57 Ki = 0.5 × 2π×2000 = 6283
注意,这里的Kp是电压/电流(V/A),Ki是V/(A·s)。具体在程序里怎么标定成整数,取决于你的电流和电压标幺值。比如电流用Q15定点表示,母线电压是12V,电流传感器量程是15A,那你算出来的Kp、Ki还要经过量纲换算才能直接填进程序。这也是很多人照抄别人的PI数字不工作的原因之一——你没看到他的标幺值体系。
我个人的经验是:先按上面的公式算出理论值,然后在调试界面里观察id和iq电流波形,在理论值基础上做微调。如果电流跟踪不上且波形有滞后,就适当增大Kp;如果高频噪声大,就降低Kp。如果稳态误差消不掉,就增大Ki。但一般极点对消整定出来的Ki已经足够消除稳态误差了,所以问题更多出在和实际电机参数不一致上。
2.3 前馈解耦:为什么只有PI还不够
我这里遇到的大坑之一,就是只做了PI闭环、没做前馈解耦,结果电机转速一上来,电流环就出现明显的超调和振荡。回头分析,问题就出在前面电压方程里那两个交叉耦合项和反电动势项上。
PI调节器只能对被控对象的输出误差“事后”补偿,它无法处理一个时刻在变化的系统干扰。电机高速运转时,q轴的反电动势(ωe·ψf)很大,如果不提前把它抵消掉,PI必须做出更大的输出才能把电流拉回目标值,这会导致动态响应变慢、超调变大,严重时甚至调不稳。
解决方法是前馈解耦。在电流环PI计算完成后,叠加前馈补偿项:
ud_ref = ud_pi - ωe·L·iq uq_ref = uq_pi + ωe·L·id + ωe·ψf
注意符号:PI输出的是“为了克服电阻和电感惯性”所需的基础电压,前馈项则把已知的系统扰动直接“喂”给执行器,这样PI只需要补偿模型误差就可以了。
前馈解耦实现起来非常简单,代码里就多两行乘法和加法的事,但它对高速段动态性能的提升是质变级的。我做这一步的时候,把速度从低速一直往上加,没有前馈解耦时,1500rpm之后电流波形开始畸变;加上前馈后,到3000rpm依然平稳。如果你在做高速电机的高动态响应控制,这个前馈项尤其不能省。
2.4 控制周期与离散化:从传递函数到嵌入式代码
前面推导的都是连续域的传递函数,但MCU里跑的是离散量。所以还得把PI调节器离散化。我用的开关频率是20kHz,控制周期Ts = 50μs,这个频率下电流环离散化带来的误差已经很小了。
我推荐用增量式PI,因为它天然自带抗积分饱和的优势,不存在一般位置式PI积分累加溢出之后要单独做的复杂处理。增量式PI的C语言实现如下:
// PI控制器结构体 typedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float out_max; // 输出限幅 float integral; // 积分累加值 float err_prev; // 上一次误差 float out; // 当前输出 } PI_Controller; // 增量式PI计算,注意输出范围限幅 float pi_update(PI_Controller *pi, float ref, float fdb) { float err = ref - fdb; float u_p = pi->Kp * err; float u_i = pi->Ki * err + pi->integral; float out = u_p + u_i; if (out > pi->out_max) { out = pi->out_max; } else if (out < -pi->out_max) { out = -pi->out_max; } pi->integral = u_i; pi->err_prev = err; pi->out = out; return out; }这里有一个经验问题:Kp、Ki的值应该怎么跟离散化结合?我常用的做法是把连续域算出来的Kp和Ki直接代入,在这个20kHz控制周期下,极点和零点离散化后与连续域的差距在1%以内,基本可以忽略。如果你用的是1kHz或更低的控制频率,那就要用双线性变换(Tustin法)重新算了,不能直接套连续域参数,否则实际闭环带宽会明显低于设计值。
3. FOC程序框架:从采样到PWM的全链路
3.1 中断里只放“快”逻辑,主循环里跑“慢”逻辑
FOC程序的高性能之处,就在于把实时性要求高的电流环全放进中断里,而把不需要那么实时的速度环、位置环、状态管理、通信等放进主循环。这是FOC程序框架的骨架。
我的中断服务函数(ISR)执行频率是20kHz,每次进入中断按顺序执行:
- 读取相电流ADC转换结果(两相或三相)
- 读取编码器位置,计算电角度
- Clarke变换(abc -> αβ)
- Park变换(αβ -> dq)
- 电流环PI(d轴和q轴各一个PI)
- 前馈解耦补偿(叠加到PI输出上)
- 反Park变换(dq -> αβ)
- SVPWM计算并更新PWM比较寄存器
这一步一步下来,在72MHz主频的Cortex-M3上大约耗时10~15微秒,留给20kHz中断是绰绰有余的。你的MCU如果更快,还能进一步优化控制周期。
主循环里做这些事:
while (1) { // 1. 速度环计算,输出iq_ref // 2. 外部通信报文解析(CAN/UART) // 3. 状态机逻辑(比如运行/停止/故障) // 4. 上位机调试数据回传 // 5. 过流、过压、堵转等保护判断 }主循环的周期不固定,所以凡是涉及“时间积分”的逻辑(比如速度计算、积分环节)都在中断里做,主循环里只做状态切换和通信,避免时间抖动影响核心控制。
3.2 电流采样时刻和一拍延迟补偿
这一节是程序能不能稳定运行的隐藏细节,很多人跑不通闭环就是栽在这里。
电流采样不能想采就采。PWM的开关动作会在三相桥臂中点产生巨大的dv/dt,采样电阻上的波形在开关瞬间会剧烈振荡。我的做法是用PWM定时器的中心对齐模式(中心计数),让ADC触发时刻对齐到PWM计数器的周期中点,也就是下桥臂全部导通、电流续流相对平稳的时刻触发采样。这个时候采样到的相电流最接近实际值。
采样完电流、算完PI、更新占空比,整个过程的执行时间是一个控制周期。也就是说,我在第k周期采到的电流,要经过一段运算延迟后才生效到第k+1周期的PWM上,这称为“一拍延迟”。如果不对这一拍延迟做补偿,它在高频段会引入相角滞后,降低系统相位裕度。
补偿方法有两种:一种是在电流环模型里显式加入e^(-sTs)延迟项,然后重新整定PI参数;另一种工程上更常用的是用“预估器”,先根据上一拍的速度和位置预测当前拍的电流,再用预测值和实际值的误差做补偿。对于大多数应用,只要控制频率足够高(>10kHz),一拍延迟的影响已经很小,可以直接忽略,但你要知道自己是在忽略它。
3.3 转子初始位置检测:不找到N极,什么都白搭
电流环闭环的关键前提之一,是Park变换的参考角θ要准确对到转子的d轴。如果你用增量式编码器,上电时根本不知道转子在哪,那这第一口“气”就运不上来。FOC的转子初始位置检测是团队里新同事踩坑最多的地方之一,所以我专门展开说一下。
最常用的三种方案:
第一种是预定位法。给q轴施加一个固定的电压矢量(或直接给特定ABC相组合通电),把转子强制拉到一个已知角度。这个方法实现最简单,但是转子会转动,不适合带负载、有运动约束的场合。只适合对位置精度要求不高的风扇、泵类负载。
第二种是高频信号注入法。在估计的d轴注入一个高频电压信号,通过检测电流响应来判断转子真实位置。这种方法在零低速下效果好,适合电梯抱闸、机械臂关节这类需要静止状态下就能识别位置的场景。实现复杂度高,需要对信号做带通滤波和锁相环。
第三种是用绝对编码器。这是最省事的路,上电读一次位置就完事。但很多低成本方案用的都是增量编码器,所以前两种方法依然是刚需。
我项目里用的是增量编码器,所以做了预定位,再结合一个简单的对齐脉冲处理:上电先给q轴一个定值电压(方向有正负之分),让转子转过一定的角度,记录下编码器在这个位置的计数,把它作为电角度零点。预定位完成后,再切入正常的FOC闭环。这个方案在静态负载很小的空载/轻载场合完全够用。如果你要做带大负载启动,真得老老实实上高频注入或换绝对值编码器。
3.4 SVPWM输出与过调制处理
电流环输出的体量是dq轴电压,要真正驱动电机,还得把d、q轴的电压参考变换回静止坐标系,再通过SVPWM(空间矢量PWM)生成三相桥臂占空比。
SVPWM的核心思想是:在一个PWM周期内,用8个基本电压矢量(6个有效矢量和2个零矢量)的时间加权组合来“平均”出一个期望的电压矢量。它的实现步骤是:
- 根据Uα和Uβ判断参考矢量落在哪个扇区
- 计算相邻两个有效矢量的作用时间T1、T2
- 计算零矢量时间T0 = Ts - T1 - T2
- 按七段式排列在一个周期内生成中心对称的PWM波形
SVPWM比SPWM的母线电压利用率高出约15%,这是它成为FOC标配的原因。程序实现我不在这里贴完整代码了,核心就是那几步扇区判断和时间计算,网上开源代码一抓一大把。但我得提一个容易被忽略的坑:当你输出的参考电压接近母线电压极限时,T1 + T2会超过PWM周期Ts,这就是过调制区。
过调制处理我一般分两级:第一级是简单的等比例缩放,把T1、T2按比例缩小到T1 + T2 = Ts,保证波形不发生畸变;第二级是如果应用允许,再牺牲一点谐波性能,进入方波模式换取最大输出功率。第一级在绝大多数应用里都够用了,第二级是那些极速追求母线电压利用率的场合才需要的。
4. 我踩过的坑:FOC调试常见问题排查实录
4.1 电流环一加大PI就啸叫,波形全是毛刺
这个现象太经典了,几乎每个做FOC的人都会遇到。一开始我以为是PI参数给大了,后来才发现其实是采样环节的问题。
电流采样的PCB布局和软件滤波是冤家。如果你把PWM干扰直接耦合到采样电阻和运放上,ADC采出来的数据自带正弦波上的一堆毛刺,PI调节器看到这些毛刺会“拼命”去补偿它,结果就是输出抖动,电机发出尖锐的高频啸叫。
排查手段:先别动PI参数,用示波器在电流环只开环(不给闭环,直接给固定占空比)的情况下观察ADC采样值和实际电流波形。如果采样值上毛刺大,优先在硬件上处理:加RC滤波电容(位置要靠近MCU引脚)、缩短采样引线。软件上也可以配合“中值滤波”或“均值滤波”把毛刺压下去。
另外一个高频啸叫的常见原因是控制频率不够。电流环开环传函的相位裕度在某一频段会掉得很厉害,如果你的环路增益刚好在这个频段过0dB,就会出现振荡。提高控制频率能直接提高相位裕度,用20kHz控制频率做2kHz电流环带宽是安全的组合,降到5kHz控制频率再做2kHz带宽就很容易振。
4.2 电机能转,但跟力矩完全对不上,甚至堵转发热
这个场景通常发生在你刚把闭环跑起来、电机“空转转得很欢”的时候——但一加载,电机就掉速度,甚至磁铁直接反转堵转发热。我遇到这种问题的第一反应是查电角度方向反了没有。
电角度方向错位的表现是:轻载时电机能转起来,但电流很大;加载后容易失步或者振动。原因可能是编码器安装方向反了,也可能是编码器零点没对齐。可以把电机静置,手动缓慢转动一圈,同时用上位机记录实际电角度θ和反电动势相位,确认电角度是否跟转子的N极对齐,偏移量是多少。这个偏移量可以在程序里以固定角度补偿进去,补偿值用“相位-峰值力矩法”来标定:给定一个恒定的iq值,反复调整零点偏移角,记录输出力矩(或电流)最大的那个点,就是电角度的正确零点。
4.3 高速时力矩越来瓤,母线电压直接拉低
转速上到一定值后,反电动势越来越高,能加在电感上的剩余电压就越来越少,电流环的输出会顶到限幅值——这属于物理极限,不是程序的问题。但有一种情况是能通过软件改善的:母线电容太小,或母线电压采样没有滤波,导致电压跌落和纹波进入电流环,前馈项(与Udc有关的SVPWM计算)就跟着乱跳。
我处理这个问题做了三件事:电源端加大电解电容+高频瓷片电容组合;断领域降低母线电压采样滤波的截止频率,确保进入控制环的电压估计值平滑;在代码里加入母线电压跌落保护,一旦检测到低于设定阈值就限制iq_ref,避免把电源系统拉垮。
4.4 常见问题速查表
我把之前踩坑的典型问题、可能原因和排查手段整理成了一个速查表,方便你做调试时对照着看:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 电流环啸叫、波形毛刺 | 采样噪声大、控制频率低 | 示波器看PWM开关毛刺;加RC滤波;提高控制频率 |
| 电机堵转发热、振动 | 电角度方向反、零点未对齐 | 手动转动转子,检查电角度实际方向;做相位对齐标定 |
| 高速掉力矩、母线电压崩 | 反电动势饱和、母线电容小 | 增加母线配置;限制iq_ref;增加电压跌落保护 |
| 稳态误差消不掉 | Ki太小或积分被限幅 | 增大Ki;检查积分限幅值是否过小 |
| 启动瞬间过流报警 | 初始位置检测不准 | 改用预定位加阈值判定;或引入高频注入 |
| 空载正常加载振荡 | 机械谐振、速度环带宽过高 | 在速度环增加低通滤波器或陷波器 |
| id和iq波动大且成对 | 编码器位置读数有跳变 | 编码器信号滤波;检查线缆接触是否可靠 |
4.5 调试过程中的一点体会
做FOC调试,最忌讳的就是“玄学调参”。Kp、Ki、带宽、控制频率、采样时序,这些参数之间都有明确的物理关系,你每动一个参数都应该知道自己为什么动它、预期会有什么效果。比如你调大Kp是提高响应速度,但同时会放大噪声和降低相位裕度;你调低Ki是让系统更稳,但也意味着对抗恒力干扰的能力变差。如果每次调参都是“试试看”,那你的系统永远调不稳。
我还有一个特别的习惯:每次调试都会同时记录id、iq、电机转速和母线电压四组数据,并且标上时间戳。很多看起来像是电流环的问题,其实是速度环或者电源的问题,没有完整的数据记录,你很难一眼看出来。单独看iq波形异常,可能就忽略掉它其实是母线电压先跌落引发的现象。
电流环做通了之后,整个FOC就完成了一大半。后面不管是做速度环、位置环,还是做无感FOC(无位置传感器控制),都是在电流环这个“内环”的地基上添砖加瓦。从这个角度看,花时间把电流环推导和程序实现吃透,是一个非常划算的投资。我当时做完这个项目之后,最大的感受就是:那些公式和代码背后是有“手感”的,这个手感只能靠自己在调试中慢慢积累。希望这篇记录能帮你少走一些弯路,早点找到自己的手感。