做PMSM永磁同步电机的矢量控制(FOC),最绕不开的就是那套藏在中断里的软件栈。电流采样、坐标变换、PI调节、SVPWM发波,一层套一层,看起来就是些数学公式和寄存器操作,可真正把它从开环强拖调到闭环稳速、再扛住载和堵转,才会发现每一个模块背后都有工程约束。这篇内容适合刚接触FOC的嵌入式工程师,也适合已经把Demo转起来但还想把软件栈吃透的开发者。我按自己实际调试电机驱动模块的路径,梳理出一份可以照着排查和实现的软件栈解析,希望能帮你少走一些弯路。
1. FOC软件栈要解决的核心问题:从三相电流到电磁转矩
1.1 为什么PMSM必须走FOC这条路
永磁同步电机的转子是永磁体,定子绕组通三相交流电后会产生旋转磁场,电磁转矩本质上是定子磁场和转子永磁磁场相互作用的结果。这个转矩的大小不仅仅取决于定子电流的幅值,还取决于定子电流矢量与转子磁极之间的角度。三相电流在空间上互差120度,它们的合成电流矢量会在电机内部形成一个旋转磁场,如果这个磁场与转子磁场的夹角不合适,转矩就会抖动、失控,甚至让电机失步。
FOC的思路是把三相静止坐标系下的交流量,变换到与转子同步旋转的坐标系里。这里把转子磁极方向设为d轴,超前d轴90度电角度定义为q轴。在旋转坐标系下,定子电流被分解成励磁分量id和转矩分量iq,二者解耦之后,控制方式就和直流电机一样透明:iq给定多大,转矩基本就多大;id通常控制在0附近,避免产生无用的磁场去影响永磁体。问题是这些交流量、旋转坐标变换、PI调节、空间矢量调制,全都得在几十到几百微秒的一个控制周期里实时算完,这就是FOC软件栈的底层逻辑。
有人可能会问,能不能不搞坐标变换,直接用标量控制调电压频率?能转,但动态响应、低速转矩、过载保护都满足不了现代伺服和电动设备的需求。FOC之所以成为主流,核心在于它把交流电机的非线性强耦合转矩控制,转变成了一组线性解耦的直流控制回路。
1.2 软件栈的整体分层与数据流
一套完整的FOC软件栈,从代码结构上可以拆成五个清晰层次。
| 层次 | 主要职责 | 典型执行频率 | 常见问题 |
|---|---|---|---|
| 采样与调理层 | ADC触发、三相电流采样或重构、母线电压采样、温度采样、零偏校准 | 与PWM频率同步,通常10kHz-20kHz | 电流零漂、采样毛刺、触发时刻不对 |
| 坐标变换层 | Clarke变换、Park变换、反Park变换、电角度处理 | 每个PWM周期 | 变换符号错、角度不同步 |
| 闭环控制层 | 电流环PI、速度环PI、弱磁控制、前馈补偿 | 电流环10kHz-20kHz,速度环1kHz左右 | 积分饱和、带宽分配不当 |
| 调制输出层 | 扇区判断、七段式SVPWM、死区补偿、PWM更新 | 每个PWM周期 | 扇区映射错、占空比越限 |
| 状态机与保护层 | 启停、故障、过流过压欠压保护、堵转检测 | 循环任务,周期不确定 | 保护误触发、状态切换竞争 |
数据流很清楚:PWM中心对齐时触发ADC,采到至少两相电流,第三相用基尔霍夫电流定律算出来;电流值经过Clarke变换得到iα和iβ,再结合当前电角度做Park变换得到id和iq;id和iq分别与目标值比较,经过两个电流环PI得到vd和vq;vd和vq反Park变换回到vα和vβ;SVPWM模块根据vα、vβ和母线电压算出三相占空比,最后更新到定时器比较寄存器。速度环的数据流更慢,它读编码器或观测器算出的速度,输出一个iq_ref作为电流环的q轴目标。
这种分层设计最大的好处是每一层都可以独立验证。我在调试时就习惯先只开SVPWM不做闭环,给固定电压矢量看电机是否正常动作;再单独灌电流看坐标变换对不对;最后才把速度环、电流环串起来。哪一环出了问题,只需要在那一层的输入输出处打印波形,就能很快定位。
2. 坐标变换与电流环PI调节器的工程实现细节
2.1 Clarke和Park变换的离散化实现
课本上的Clarke变换是连续公式,到MCU上就得考虑定标和整型运算。工程上最常见的做法是把物理量统一标定成Q格式定点数。比如电流采样值经过ADC后左移12位,把原始码值放大到合适的数值范围,然后所有变换都用32位整数完成,只在最后输出PWM时除以定标系数。
我常用的Clarke变换是这样的:
typedef struct { int32_t i_alpha; int32_t i_beta; } clarke_out_t; clarke_out_t clarke_transform(int32_t ia, int32_t ib) { clarke_out_t out; // 假设 ADC 采样值已经左移 12 位,电流定标为 1LSB = 1mA // K0 = (1<<15) / 1.732 = 18918 左右,具体按实测定标修正 const int32_t K_BETA = 18918; // ia 直接作为 i_alpha out.i_alpha = ia; // i_beta = (ia + 2*ib) / sqrt(3),用定标系数近似 int32_t sum = ia + (ib * 2); out.i_beta = (sum * K_BETA) >> 15; return out; }必须注意,不同的MCU里定标方式差别很大。如果你是在仿真或浮点平台验证算法,直接套公式没问题;一旦落到定点芯片,sqrt(3)不能写成浮点常量,要转成Q15或Q12格式的整数系数,否则每次调用都会引入浮点运算开销。Park变换相对简单,用查表或CORDIC得到sin和cos后做乘加:
void park_transform(int32_t i_alpha, int32_t i_beta, int32_t sin_theta, int32_t cos_theta, int32_t *i_d, int32_t *i_q) { // sin/cos 使用 Q15 定标,即 32767 对应 1.0 int32_t d = (i_alpha * cos_theta + i_beta * sin_theta) >> 15; int32_t q = (-i_alpha * sin_theta + i_beta * cos_theta) >> 15; *i_d = d; *i_q = q; }这里有一处容易翻车:Park变换的符号约定。不同教材对反Park和Park使用的矩阵有正负号差异,具体取决于q轴方向是超前还是滞后d轴。如果电流环已经调好但电机就是使不上劲,多半是这里差了一个负号。
坐标变换层还有一个容易被低估的问题:ADC零漂。如果电流反馈在零电流时读数不是0,变换出来的id和iq会随电角度摆动,表现在波形上就是id/iq出现与转速相关的高频波动。解决办法是在初始化和每次停机后做零偏校准,把零电流状态下的采样均值存下来,正式采集时减掉。
2.2 电流环PI的积分抗饱和与积分饱和处理
电流环PI在每个PWM周期的中断服务函数里运行。教科书式的位置式PI很简单,但工程上最麻烦的就是积分饱和。当输出电压已经到达母线电压允许的限幅值时,如果误差仍然很大,积分项会持续累加,导致PI输出维持在一个很高的数值上,等到误差反向时输出却迟迟退不下来,动态响应就会表现出明显的过冲和振荡。
我的做法是条件积分法:只在PI输出没有进入饱和区间时才正常积分;如果输出已经饱和且误差方向仍然会加深饱和,就冻结积分项。写成代码大致是这个样子:
int32_t pi_controller(pictrl_t *pi, int32_t err) { int32_t out_unclamped; int32_t out; pi->integral += pi->ki * err; out_unclamped = pi->kp * err + pi->integral; if (out_unclamped > pi->upper_limit) { out = pi->upper_limit; // 反向误差时允许积分衰减 if (err < 0) { pi->integral = out_unclamped - pi->kp * err; } } else if (out_unclamped < pi->lower_limit) { out = pi->lower_limit; if (err > 0) { pi->integral = out_unclamped - pi->kp * err; } } else { out = out_unclamped; } pi->out = out; return out; }还需要注意积分限幅。如果不给积分项单独的限幅,即使输出限幅,积分中间值依然可能很大。更稳妥的做法是把积分项也限幅在输出限幅的一定比例内,避免恢复过程中出现过冲。
2.3 速度环与电流环的带宽分配逻辑
速度环和电流环不是随便设置的,它们必须拉开带宽差距。电流环负责快速跟随电流指令,需要在几个毫秒内完成调节,所以采样周期通常取100μs左右;速度环负责让转速稳定,带宽天然要低,采样周期一般取1ms左右。如果两个环带宽靠得太近,速度环的输出波动会直接激励电流环,形成两个环路之间的耦合振荡。
我的经验是速度环带宽取电流环带宽的五分之一到十分之一。例如电流环带宽做到500rad/s,速度环带宽就控制在50到100rad/s。速度反馈的计算也直接影响带宽,用M法测速时,低速下相邻脉冲间隔很长,单位时间内位置变化量化步数很少,速度值会出现阶梯感。这种情况下速度环不能盲目提高增益,否则机械系统会发出嗡嗡的低频噪声。可以在速度环前面加中值滤波或者低速时延长时间窗口,再配合速度PI的抗饱和。
速度环的输出也要限幅,它限的是峰值电流。比如电机的最大相电流是20A,速度环输出就应该限到对应20A的iq_ref。否则一旦堵转,速度环会请求几倍于允许的电流,把功率器件烧掉。
3. 电压输出模块:扇区判断、七段式SVPWM与死区处理
3.1 扇区判断的简化方法
SVPWM的思想是:逆变器只有六个基础电压矢量(包括零矢量),想要输出任意方向和大小的电压矢量,就先用相邻的两个基础矢量各作用一段时间,再用零矢量填充剩余时间。第一步就是判断目标电压矢量落在哪个扇区。
工程上常用三值判断法。设目标电压在αβ坐标系下为Uα和Uβ,构造三个参考量:
- V1 = Uβ
- V2 = √3/2 * Uα - 1/2 * Uβ
- V3 = -√3/2 * Uα - 1/2 * Uβ
每个参考量大于0记1,否则记0,然后按二进制组合得到扇区号。实际代码可以这样写:
uint8_t get_svpwm_sector(int32_t u_alpha, int32_t u_beta) { uint8_t sector = 0; int32_t ref1 = u_beta; int32_t ref2 = (SQRT3_2 * u_alpha - u_beta / 2) >> 15; int32_t ref3 = (-SQRT3_2 * u_alpha - u_beta / 2) >> 15; if (ref1 > 0) sector |= 1; if (ref2 > 0) sector |= 2; if (ref3 > 0) sector |= 4; return sector; }这里SQRT3_2是sqrt(3)/2的Q15定标值。
拿到扇区号之后,要查表得到该扇区对应的两个基础矢量作用时间T1和T2。初学者最容易在这里把扇区编号和三相桥臂的物理映射搞错,因为不同MCU的PWM输出引脚和逆变器桥臂连接关系不同。我的建议是:不管在哪个平台,先在开环模式下给一个固定的电压矢量,对照示波器看三相PWM波形,确认扇区编号和实际输出方向是否一致,这一步做完再往下写闭环。
3.2 七段式SVPWM的占空比计算与死区补偿
七段式SVPWM的核心是让零矢量拆成两段,分别放在扇区切换的开始和中间。这样在一个周期内,三相桥臂的开关次数最少,谐波也相对小。
计算时引入中间变量X、Y、Z:
int32_t x = u_beta * K_SVPWM; // K_SVPWM 与母线电压和周期相关 int32_t y = (SQRT3_2 * u_alpha + u_beta / 2) * K_SVPWM >> 15; int32_t z = (-SQRT3_2 * u_alpha + u_beta / 2) * K_SVPWM >> 15;然后根据扇区从X、Y、Z中挑选T1和T2。例如扇区1常用T1=z、T2=y;扇区2常用T1=y、T2=-x;实际映射要跟自己的扇区编号配合起来。
这里有一个关键点:T1和T2加起来必须小于PWM周期。当电压矢量幅值接近六边形边界时,T1+T2会大于周期,需要对T1和T2按比例缩放,否则会进入过调制区。限幅方法很简单:
if (t1 + t2 > t_pwm) { int32_t scale = t_pwm * 32768 / (t1 + t2); t1 = (t1 * scale) >> 15; t2 = (t2 * scale) >> 15; }死区时间也必须处理。为了防止同一桥臂上下管直通,需要在切换瞬间加入一段封锁时间,通常在几百纳秒到几微秒。但死区会让实际输出电压比参考电压低,尤其在电流过零附近,非线性会被放大,产生电流波形畸变和转矩脉动。
最简单的死区补偿方法是基于电流方向:判断三相电流的极性,如果某相电流为正,就在该相占空比中加一个补偿值;电流为负则减一个补偿值。补偿值跟母线电压和死区时间直接相关,按下式近似估算:
comp = dead_time * pwm_frequency * (1024 / vdc_ref) / 2;注意死区补偿不能盲目加,电流方向判断本身有噪声,如果方向判断抖动,补偿量会乱跳。实际工程中我会给电流方向判断加滞环,或者只在电流幅值大于一定阈值时才启用补偿。
4. 转子位置与速度获取:编码器对齐与无感观测器的软件实现
4.1 编码器零位校准与电角度对齐
FOC依赖电角度,而电角度是从机械角度乘以极对数再加一个偏置得到的。增量式编码器只有相对位置,上电时无法直接知道转子磁极在哪,所以必须先做零位校准。
我常用的方法是d轴强制对齐。进入校准模式后,设定id_ref为正的小电流,iq_ref置0。电流环会让定子磁场强行对齐到d轴方向,转子最终停在和d轴重合的位置。此时读取编码器的原始计数值,记为offset。之后运行中,电角度就是:
electrical_angle = (encoder_raw - offset) * pole_pairs * ENCODER_TO_ANGLE;这个做法看起来很直接,但有几个细节容易踩坑。第一,校准电流不要给太大,一般给额定电流的10%到20%即可,太大会使转子产生振动,甚至会带着负载撞到机械限位。第二,必须等转子完全稳定再记录offset,判断稳定的方式可以是连续几次读数增量小于一个阈值。第三,如果电机带有减速机构或负载,校准后转子不一定精确停在理想零点,这时需要把校准过程重复几次取平均值,或者用后续磁极位置辨识做补偿。
还有一种常见做法是利用编码器Z脉冲回零,但Z脉冲对应的是编码器机械零点,和电角度零点没有任何关系。所以即使捕获了Z脉冲,也依然要跑一遍电气对齐,把Z脉冲时刻对应的电角度记下来,之后每次过Z都可以重新对齐。
4.2 无感FOC中反电动势观测器的软件实现要点
无感方案去掉编码器,位置和速度都通过软件估算。高速段最常用的是基于反电动势的观测器。基本原理是,PMSM旋转时转子磁链会在定子绕组中感应出反电动势,这个反电动势的方向和转子的位置直接相关。如果在αβ坐标系下建立电流状态方程,通过观测器估计出反电动势分量eα和eβ,那么转子角度就可以从反正切中得到。
核心方程可以写成:
// 电流估计方程,Ts是控制周期,Ls是相电感,Rs是相电阻 i_alpha_est += (Ts / Ls) * (v_alpha - Rs * i_alpha_est - e_alpha_hat); i_beta_est += (Ts / Ls) * (v_beta - Rs * i_beta_est - e_beta_hat);观测器利用电流实测值和估计值的偏差构造校正项,让它逐步逼近真实电流,同时估算出反电动势。滑模观测器在校正项中用的是符号函数,实现简单,但输出含有大量高频抖振,必须加低通滤波器。低通滤波会引入相位延迟,所以还要做相位补偿。我实际调试时发现,与其在低通滤波上反复补相位,不如直接改成龙伯格观测器配合锁相环,锁相环对反电动势的相位延迟不那么敏感,还能同时得到转速。
但无论哪种无感方案,低速下反电动势幅值几乎为零,根本无法可靠提取位置。所以无感FOC不能像有感那样直接从零速闭环运行。常规启动流程是先预定位,让转子对齐到已知角度;然后用I-F控制或压频控制开环拖起来,转速上升到可以辨识反电动势的阈值后,再切换到闭环FOC。这个切换过程是很多项目最大的痛点:切早了角度发散,切晚了电流冲击大。我的建议是在切换前先让观测器在线运行但不参与控制,把估算角度和实际角度放到同一个显示窗口比较,确认误差收敛再切。
5. 调试与参数整定:从开环强拖到闭环稳速的关键路径
5.1 电流环PI初值计算:电阻电感法
电流环的调试不必纯靠试凑,用电阻电感法可以快速得到一个可用的初值。忽略反电动势扰动后,定子电压方程近似为:
[ v = L \frac{di}{dt} + R i ]
被控对象是一阶惯性环节。采用PI控制器并做零极点对消,把控制对象的时间常数L/R抵消掉。设期望的电流环时间常数为τ(也就是期望的闭环带宽对应的时间常数),则PI参数可以这样取:
[ K_p = L / \tau ] [ K_i = R / \tau ]
举个例子:某电机相电感L=200μH,相电阻R=0.2Ω,希望电流环时间常数τ=1ms,那么Kp=0.2,Ki=200。这组参数在连续域看起来没问题,落到定点代码时还要乘以电流反馈定标系数和输出系数,所以软件里的数值会是另一个量级。但至少它给出了一个合理的起点。
实际运行中,反电动势会造成电流环跟踪误差。如果发现高速时电流环增益不足,可以加入电压前馈,把反电动势和电阻压降的估计值直接加到PI输出上。前馈量不需要太精确,能把稳态误差削掉大部分就够了。
5.2 从开环到闭环的调试路径
我调试FOC时从不直接上速度闭环,那样出了问题根本分不清是电流环还是位置反馈的问题。推荐的顺序是开环强拖-电流闭环-速度闭环-带载测试。
开环强拖很简单,给定一个固定幅值和固定角速度的旋转电压矢量,直接通过SVPWM输出,让电机转起来。这时候不读位置反馈,目的只有一个:验证PWM、逆变器和电机相序是否正确。如果电机反转、抖动、相电流异常,说明SVPWM或相序有问题,先在这里解决掉。
接下来做电流环调试。把转子锁住或者低速拖动,给定id_ref和iq_ref,观察实际id/iq是否跟踪。此时要重点观察坐标变换角度对不对。如果角度错,电流环输出会发散,表现为电流失控。调试时可以把id/iq和帕克变换前的iα/iβ一起录波形,看解耦是否有有效。
电流环稳定后再切速度环。速度环给一个低的固定转速指令,比如300rpm,观察速度阶跃响应。慢慢增加给定,同时记录电流波形。如果速度环响应出现振荡,先降低Kp,再检查速度反馈滤波是否过重。
带载测试最后做。要覆盖额定负载、过载、堵转恢复、母线电压跌落这些场景。堵转时电流环应该立刻限幅,速度环的积分项不应该无限增大,否则一解除堵转电机会猛冲。
5.3 实测中常见的几个坑与对应处理
我把自己踩过的坑整理成一张表,方便你排查时对照:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| id/iq波形随电角度波动 | ADC零漂未校准 | 软件零偏校准,做均值减除 |
| 电机低速爬行或抖动 | 电流采样毛刺、死区非线性 | 采样时刻调整、死区补偿加滞环 |
| 高速时转矩受限 | 反电动势前馈不足、弱磁未介入 | 增加电压前馈,检查SVPWM过调制 |
| 电流环PI输出饱和退不出来 | 积分抗饱和没做好 | 条件积分或back-calculation抗饱和 |
| 速度环低速噪声大 | 测速量化误差、编码器滤波不够 | 延长测速窗口、速度反馈中值滤波 |
| 过流保护误触发 | 采样窗口靠近开关边沿、比较阈值过小 | 调整ADC触发时刻,增大滤波窗口 |
还有一个很容易被忽略的点:PWM更新寄存器的影子寄存器。很多MCU要求先写比较值,等到PWM周期边沿才真正加载。如果误以为写寄存器立即生效,在调试时会觉得控制输出总是慢半拍。这种延迟和相位滞后会对电流环带宽产生明显影响,做高频调试时一定要把它计算进去。
我个人的体会是,FOC软件栈看着复杂,其实每一层都能单独验证。比起急着背代码,更重要的是建立一套自己的调试工具链:能录电流、角度、占空比的波形窗口,能单步执行的调试器,能在运行时修改PI参数的通信通道。这些东西齐全之后,再回头啃文档里的公式,很多疑问都会自己解开。最后再分享一个小经验:给每一层模块都留一个可开关的旁路测试点,比如可以强制让Park变换输出的角度等于一个常量,或者让SVPWM输出固定占空比。这些“软件测试钩子”平时不起眼,出问题的时候比示波器还好用。