☰
无感FOC位置估算工程实践:滑模观测器与锁相环的调参与排坑指南
2026/10/24 2:50:44 网站建设 项目流程

前阵子整理工作笔记,翻到去年调无感FOC的代码记录,发现整套电阻、电感和观测器参数的标定流程还挺完整,索性把位置估算这块源码重新梳理了一遍。永磁同步电机无感FOC位置估算,说白了就是不装编码器、不装霍尔,只靠相电流和母线电压把转子电角度实时算出来。很多刚接触FOC的朋友一看到坐标变换矩阵就头大,但实际上卡住他们的往往不是矩阵,而是“算出来的角度到底能不能用”。角度偏一点,电流环就开始发飘,角度抖一下,转速波形全是毛刺,严重的时候直接过流保护。这篇文章我会把这套代码的核心模块、数据流、调参顺序和实测中遇到的各种坑拆开讲,给准备做无感FOC或者已经踩在坑里的同行一个参考。

1. 为什么无感FOC位置估算是绕不过去的一道坎

1.1 有传感器方案到无感方案,省掉的到底是什么

FOC矢量控制的核心是坐标变换,而坐标变换的前提是知道转子磁极的位置。有传感器方案在电机轴端装了编码器或霍尔,电机一转,位置和速度直接出来,控制器只要读取就行,省事,但成本高、装配复杂,而且有些场景根本不给你装传感器的空间。

无感方案把省掉的成本转移到了算法头上。系统必须在每个电流环周期内,根据相电流和母线电压重建出反电动势,再从反电动势里把转子角度和速度提取出来。这个提取过程就是位置估算。实际项目里,用户不会关心你用的是编码器还是无感算法,他只看启动顺不顺、带载稳不稳、高速能不能跑上去。所以无感位置估算承担的压力比很多人想象的大得多——它不只是“估个角度”,而是整个控制链路的可信度来源。

省掉传感器之后,系统还少了一个很关键的“安全网”。有编码器的时候,角度错了立刻能检测出来;无感方案里,如果观测器受了扰动、参数偏了,位置会悄悄漂移,等到发现时可能已经过流失步。所以无感FOC对位置估算的实时性、平滑性和鲁棒性要求都相当苛刻。

1.2 位置估算里的“相位”和“转速”是什么关系

先理清一个容易混淆的概念:无感FOC需要的是“电角度”,不是“机械角度”。电机转子转一圈,电气信号就经历了若干对极周期。极对数是4时,电机转一圈对应的电角度变化是4×360度。所以代码里估算出来的角度,永远要先除以极对数才能换算成机械角度,而速度也是同理,电角速度除以极对数才是转子机械速度。

无感位置估算的核心方程是永磁同步电机的电压平衡方程。在αβ静止坐标系下,定子电压由电阻压降、电感压降和反电动势三部分组成:

v_alpha = R * i_alpha + L * di_alpha/dt + e_alpha v_beta = R * i_beta + L * di_beta/dt + e_beta

反电动势里面天然带着转子位置信息。永磁电机的反电动势可以写成:

e_alpha = -Ke * omega_e * sin(theta_e) e_beta = Ke * omega_e * cos(theta_e)

这里的theta_e就是转子电角度。只要能把e_alpha和e_beta估出来,角度和速度就都有了。但问题在于反电动势的幅值和转速成正比,转速越低,信号越弱,所以低速段是无感FOC天然难做的原因。后面我讲低速扩展时会再回到这个点。

简单说,位置估算模块的输入是电流和电压,中间产物是反电动势,输出是转子电角度和电角速度。这个三角关系是整个源码设计的骨架,代码里的每一环都在围绕它打转。

2. 方案选型:我在反电动势和滑模观测器之间怎么站队

2.1 反电动势直接积分,为什么在工程里活不下去

从方程上看,好像最直接的方法是:测出电压和电流,减去电阻压降和电感压降,剩下的不也就是反电动势吗?理论上可行,但实际上这条路线在工程里基本走不通。

问题首先出在电感微分项L*di/dt上。电流采样的噪声经过微分会被放大,10kHz的电流环里,你根本拿不到像样的电感压降。其次,如果试图用积分的方法重构磁链,积分器本身就有零漂问题,直流偏移会被无限累积,几分钟之后积分输出早就漂到不知道哪里去了。硬件上也很难做到完全无偏置,ADC总有过零误差,逆变器还有死区,这些都会污染积分结果。

更麻烦的是,直接计算出来的反电动势波形往往是毛刺的,里面混着开关频率噪声、死区谐波和使用重构电压时的误差。把这个信号直接去算atan2,得到的角度会抖得没法用。所以需要一种对模型误差和噪声不那么敏感的估算方法,这就轮到了状态观测器。

2.2 滑模观测器+锁相环,为什么是性价比最高的组合

在工业界和很多开源方案里,滑模观测器配锁相环几乎成了中高速段无感FOC的默认组合。滑模观测器的思路是在电流观测回路里强行加一个高频切换项,让观测电流快速逼近实际电流,而高频切换项的等效平均值正好逼近反电动势。

这个思路的好处是鲁棒性强。它不依赖精确的电机模型,电阻和电感的偏差在一定范围内都能扛得住。代价是会引入高频抖振,所以需要低通滤波器把反电动势平滑出来。滤波器会带来相位滞后,但相位滞后可以通过补偿或者增大带宽来缓解,这部分后面实测坑里细讲。

角度从反电动势里提取时,我不推荐直接用atan2。反电动势信号经过滤波后虽然平滑了,但依然有残留谐波,直接用atan2角度会跳。锁相环相当于一个带惯性的角度跟踪器,它天然对高频分量有抑制作用,同时还能顺便把转速估算出来。速度环需要速度反馈,观测器输出角度后求导当然也能得到速度,但噪声很大,锁相环积分输出速度要干净得多。

代价就是多一套PI参数要整定,但对10kHz电流环来说,PLL的计算量其实很小。综合代码复杂度和调参难度,SMO加PLL是我会反复推荐的起点方案。

3. 源码数据流:从三相电流到转子角度,代码里到底走了哪几步

3.1 工程目录与模块边界怎么划

源码分享不要把全部控制逻辑塞到一个中断里,那样看起来像流水账,维护起来也痛苦。我这套代码的模块划分很直接,每个模块只负责一件事:

src/ ctrl/ foc_ctrl.c // 电流环、速度环、启停逻辑 smo.c // 滑模观测器 pll.c // 锁相环角度速度提取 observer_top.c // 位置估算总调度,把SMO和PLL串起来 math/ clark.c // Clark变换,三相到αβ park.c // Park变换,αβ到dq svpwm.c // SVPWM调制 hal/ adc.c // 电流采样 pwm.c // PWM发波和定时器 gpio.c // 状态指示 user/ main.c // 主循环和状态机

位置估算模块只依赖四个量:i_alpha、i_beta、v_alpha、v_beta。v_alpha和v_beta一般不用测量电压传感器,而是直接用SVPWM的占空比和母线电压重构出来。这样模块边界很清晰,估算模块不碰寄存器,换芯片时只需要改HAL层,位置估算代码可以原封不动搬过去。

数据流的顺序也有讲究。我用的是PWM中心对齐,ADC在PWM计数顶峰触发采样。进入中断后先做电流重构和Clark变换,然后执行电流环PI,再反Park到αβ电压,送入SVPWM。这里有个关键点:送给观测器的电压必须用这一拍算出来的参考电压,但电流环用的角度还是上一拍估算的角度,这就有了一拍的延时。所以代码里我会额外做一个角度补偿,把延时角度加回去,否则转速高了以后电流环和电压会越来越不同步。

3.2 从电流采样到反Park,前置处理有哪些坑

电流采样这一步就隐藏着第一个大坑。三相电流采样不是采样三次再相加为零那么简单,ADC每个通道都有自己的偏置,必须在电机不转的时候做一次偏移校准。我这套源码里专门放了一个校准函数,启动前短路电流或堵转,把每个通道的零点采多次取平均,存进全局变量。

第二坑是电流重构。用单电阻采样时,在一个PWM周期内至少需要两个有效电流状态才能还原出三相电流,低调制区附近会出现重构盲区,这里最容易出现电流畸变。我实际用的方案是在重构失败时用上一拍的电流做保持,并且对SVPWM的插入零矢量时间做了最小限制。这个细节直接影响低速和轻载工况下的位置估算质量。

第三坑是死区补偿。MOS管或IGBT都有死区时间,死区会造成电压误差,尤其是电流过零点附近,反电动势估算波形会出现明显的削顶和畸变。位置估算最怕的就是低频谐波,所以源码里针对三相电流方向做了固定死区补偿,补偿量根据母线电压和开关周期换算成占空比修正值。

这些前置处理看起来不起眼,但如果没有做,后面调SMO参数时会非常痛苦。电流波形上有毛刺,观测器会把毛刺当成反电动势,PLL角度就会跟着抖。

3.3 估算结果如何进入电流环:数据流闭环

整个控制链路是这样的:ADC触发中断 → 重建三相电流 → Clark变换得到iα/iβ → 用当前估算角度做Park变换得到iq/id → 电流PI → 反Park得到vα/vβ → SVPWM发波 → 用vα/vβ和iα/iβ执行SMO → 用SMO输出的反电动势执行PLL → 更新下一拍角度和速度。

这个闭环必须在同一个中断里完成,而且SMO和PLL的采样周期和电流环要一致。我用的电流环频率是10kHz,即一个PWM周期100微秒执行一次完整控制,SMO和PLL也按同样的周期跑。如果把这些估算放到低速的定时器任务里,角度更新滞后会立刻让电流环失稳。

低速强拖和闭环切换时,角度来源会从外部给定切到PLL输出。切换不是瞬间切换,而是会通过一个角度混合变量过渡。先让强拖角度带着估算角度跑,再逐渐把角度控制权交给PLL。这个过渡逻辑写在observer_top.c里,后面源码拆解部分给关键片段。

4. 核心源码拆解:滑模观测器、PLL和角度切换究竟写了什么

4.1 滑模观测器的离散化实现

SMO的核心方程是建立一个电流观测器,让观测电流跟随实际电流。定义电流误差为观测电流减去实际电流,然后用滑模切换项强迫误差收敛。简化后的离散化代码如下:

typedef struct { float Rs; // 定子电阻 float Ls; // 定子电感(dq电感近似相等) float Ts; // 控制周期,单位秒 float h; // 滑模增益 float lpf_k; // 低通滤波系数 float i_alpha_est; // α轴观测电流 float i_beta_est; // β轴观测电流 float e_alpha_est; // α轴反电动势估算 float e_beta_est; // β轴反电动势估算 float z_alpha; // α轴滑模切换量 float z_beta; // β轴滑模切换量 } SMO_t; void SMO_Update(SMO_t *smo, float i_alpha, float i_beta, float v_alpha, float v_beta) { // 预测电流变化率 float d_i_alpha = (smo->z_alpha - smo->Rs * smo->i_alpha_est + v_alpha) / smo->Ls; float d_i_beta = (smo->z_beta - smo->Rs * smo->i_beta_est + v_beta) / smo->Ls; // 前向欧拉离散化,更新观测电流 smo->i_alpha_est += d_i_alpha * smo->Ts; smo->i_beta_est += d_i_beta * smo->Ts; // 滑模切换项:依据电流误差符号输出 ±h if (smo->i_alpha_est - i_alpha > 0) smo->z_alpha = smo->h; else smo->z_alpha = -smo->h; if (smo->i_beta_est - i_beta > 0) smo->z_beta = smo->h; else smo->z_beta = -smo->h; // 用一阶低通滤波从高频切换量中提取反电动势 smo->e_alpha_est += (smo->z_alpha - smo->e_alpha_est) * smo->lpf_k; smo->e_beta_est += (smo->z_beta - smo->e_beta_est) * smo->lpf_k; }

注意电压符号需要和自己的坐标变换对齐,不同工程里符号约定可能相反,这直接影响切换项方向和最终角度方向。我调试时会先用固定角度开环强拖,然后用示波器对比估算反电动势和实际角度的关系。

滑模增益h怎么选是个关键。h太小时电流误差压不住,估算电流追不上实际电流,反电动势信号失真;h太大时高频抖振严重,电流和电压重构噪声都会漏进等效反电动势。实际整定时,我会先给一个偏大的值让系统先“滑”起来,然后逐步减小h直到电流误差波形不再出现明显缺口为止。一般来说,h的单位要和电压方程匹配,大致取反电动势幅值的1.5~2倍左右比较安全。

低通滤波系数lpf_k也很重要。它决定反电动势提取的通频带,直接公式是lpf_k = 2π * lpf_cutoff * Ts,但离散化实现里需要注意数值范围,系数太小会让滤波后的信号严重滞后,太大又滤不掉抖振。我默认在10kHz控制频率下取lpf_cutoff在800Hz到1.2kHz之间,具体要看PLL带宽怎么配合。

4.2 锁相环角度/速度提取的写法

PLL的目标是从反电动势矢量里平滑地提取角度和速度。上一章推导过,反电动势矢量和真实转子角度之间存在90度的相位差,所以构造误差函数时要做坐标旋转对齐。核心思路是把反电动势投影到估算角度的垂线方向上,投影误差为零时就说明估算角度等于真实角度。

typedef struct { float theta; // 估算电角度,单位rad float omega; // 估算电角速度,单位rad/s float kp; // 比例系数 float ki; // 积分系数 float integral; // 速度积分项 float Ts; } PLL_t; void PLL_Update(PLL_t *pll, float e_alpha, float e_beta) { // 反电动势幅值,用于归一化 float ampl = sqrtf(e_alpha * e_alpha + e_beta * e_beta); // 归一化角度误差,消除转速和磁链对增益的影响 float err = (-e_alpha * cosf(pll->theta) - e_beta * sinf(pll->theta)) / (ampl + 1e-3f); // 积分项积分得到角速度 pll->integral += pll->ki * err * pll->Ts; // 限速,防止估算速度发散 if (pll->integral > pll->omega_max) pll->integral = pll->omega_max; if (pll->integral < -pll->omega_max) pll->integral = -pll->omega_max; pll->omega = pll->integral; // 角度更新 pll->theta += (pll->omega + pll->kp * err) * pll->Ts; // 角度归一到 [-pi, pi],防止浮点越界 if (pll->theta > 3.14159265f) pll->theta -= 6.28318530f; if (pll->theta < -3.14159265f) pll->theta += 6.28318530f; }

误差函数前面那个负号需要和SMO输出符号严格匹配。我习惯于在仿真和台架上先验证:给定固定角度强拖,估算角度应该能跟着给定角度稳定不动,如果PLL输出反向,就把反电动势符号或者误差式里的负号调过来。

PLL的kp和ki按二阶系统来配比较方便。线性化之后闭环特征方程是s² + kps + ki,所以可以取kp = 2zeta*omega_n,ki = omega_n²。我实际取值时,zeta取0.707,omega_n取100~300Hz对应的角频率,然后根据控制频率做离散化修正。这个带宽要明显大于速度环带宽,但又不能太大,否则PLL会跟着反电动势毛刺跑。速度环带宽如果是10~20Hz,PLL自然频率放在150~250Hz左右是比较舒服的范围。

4.3 低速开环向闭环切换的过渡代码

位置估算不是从零速就能直接闭环工作的。我使用的策略是先开环强拖,也叫I/F或V/F牵引,让电流角度自己转,电机跟着同步转起来。转速到达额定转速的5%到10%之后,再切到PLL闭环。

typedef enum { CTRL_OPENLOOP, CTRL_CLOSEDLOOP } CtrlMode_t; typedef struct { float theta_open; // 开环给定角度 float theta_est; // 闭环估算角度 float mix_gain; // 0-1,1表示完全闭环 } ObserverTop_t; float ObserverTop_GetTheta(ObserverTop_t *top, CtrlMode_t mode) { if (mode == CTRL_OPENLOOP) return top->theta_open; // 闭环前慢慢混合角度,避免切换瞬间角度跳变 if (top->mix_gain < 1.0f) top->mix_gain += 0.01f; if (top->mix_gain >= 1.0f) return top->theta_est; // 角度差归一化到 [-pi, pi] 后按比例混合 float err = top->theta_est - top->theta_open; if (err > 3.14159265f) err -= 6.28318530f; if (err < -3.14159265f) err += 6.28318530f; return top->theta_open + top->mix_gain * err; }

切换的核心原则是角度不能跳变。如果直接拿估算角度替换开环角度,两者的相位差往往有几十度,电流会瞬间冲击,轻则转速波动,重则过流保护。我的做法是在开环角度上逐步叠加估算角度和开环角度之差,混合增益每50到200个控制周期加一小步,整个过程大概20到50毫秒完成。这个过渡期间电流给定也要从单纯的q轴电流拉动,慢慢过渡到正常速度闭环。

5. 调参顺序与实测踩坑:角度歪、零点抖、重载失步的排查链路

5.1 第一步:把电流环和观测器先“解耦”

很多人一上来就调SMO,结果电流环还没调好,观测器看到的电流全是畸变,位置自然也估不准。我的调参顺序很固定:先把电流环单独调好,用固定角度堵转或者开环强拖来验证电流波形。角度不转的情况下,d轴和q轴电流的跟随性都可以直接测出来,这一步稳定了再谈观测器。

电阻和电感参数必须提前测准。R可以直接用万用表或电桥测线电阻,再除以2得到相电阻;电感可以用电桥在1kHz下测,或者用电压阶跃法自己算。这块差距不要指望算法去补偿,SMO虽然鲁棒,但电阻电感偏差太大会直接改变观测器极点,导致电流估计误差和反电动势相位偏移。

我还会先用开环强拖的方式跑起来,给定一个固定转速和电流幅值,然后观察SMO估算出的反电动势波形。理想情况下,两个正交反电动势应该是正弦波,幅值接近,相位差90度,且频率和开环转速一致。这一步看到的波形基本就决定了后面的参数方向。

5.2 第二步:滑模增益与PLL带宽怎么配

这里有一个容易走弯路的点:不是单独调SMO或单独调PLL,而是从整个角度环路看。SMO的输出给PLL,PLL的带宽需要比SMO低通滤波器的带宽低一些,否则PLL会把SMO没滤干净的高频切换噪声全放大。

我习惯先固定PLL带宽在200Hz左右,然后调h。h从小到大试,同时看估算电流误差。如果电流误差波形在每相过零点附近出现明显的毛刺,说明h太小,误差压不住;如果电流误差波形抖动严重,说明h可能偏大。h稳定后再调SMO的lpf_k,lpf_k增大时角度噪声会上升,减小时角度噪声下降但滞后增加。

下一步回到PLL,把PLL带宽从150Hz往250Hz方向微调。如果重载工况下角度抖动变小,说明PLL带宽合适;如果轻载工况下速度波形还有高频抖,就把PLL带宽降回一点。最终整定的效果是:低速段转速波动小,高速段相电流波形没有明显的相位抖动,重载切换时角度没有大的回落。

5.3 第三步:实测波形下的问题排查链路

我把自己实际遇到最多的几个问题整理成了排查顺序,供参考:

现象常见原因排查顺序
角度整体偏置,q轴电流和转矩对不上反电动势符号不对、坐标变换方向反、电流采样偏置未校准先查采样偏置,再查坐标变换符号,最后查PLL误差式符号
零点附近角度抖动,启动时来回震荡电流重构盲区、死区补偿不够、h过小、PLL带宽过高先看电流波形是否有畸变,再补死区,再调低PLL带宽,最后微调h
重载时速度掉下去后失步PLL滞后角大、速度环积分饱和、SMO增益不足先做角度滞后补偿,再看速度环限幅,最后提高h或增加低通带宽
高速时电流波形有刺耳噪声滑模抖振、低通滤波不够、PLL带宽过高降低h,降低PLL带宽,适当增加SMO低通滤波阶数

有一次排查角度偏置问题,我盯了一个下午。现象是零速附近开环强拖正常,一切闭环角度就偏了20度左右,q轴电流给上去电机却往外突突。后来发现在电流采样偏置校准的时机不对,校准电机还没完全静止,ADC零点带有残余电流,校准出来的偏置值本身就偏了。把校准逻辑改成启动前强制刹车并多次平均,角度偏置问题直接消失。

另一次重载失步的问题也很有代表性。风机类负载低速爬升时,电流上升很快,PLL估算速度跟不上实际速度,角度滞后越积越大,最后电流环饱和,电机转速瞬间掉到零。后来在速度闭环里加了积分限幅,又在PLL输出角度上按当前速度和SMO低通滞后时间做了相位补偿,失步现象才算彻底压住。相位补偿公式很简单,就是估算电角速度乘以低通滤波器的时间常数:

float phase_comp = pll->omega * (1.0f / (2.0f * 3.14159265f * lpf_cutoff)); compensated_theta = pll->theta + phase_comp;

这个补偿量在高速段尤其关键。lpf_cutoff如果取800Hz,电机跑到高频时,相位滞后可能达到30到45度,不补偿的话,q轴电压分量会让电流环明显“发软”。代码里我把这个补偿值做了平滑处理,防止速度突变时补偿角度突然跳变。

6. 源码之外:低速重载与参数鲁棒性还能怎么补

6.1 低速段和中高速段的处理差异

这套SMO加PLL方案在中高速段表现很稳,但必须诚实地说,低速段尤其是0到3%额定转速范围,单靠反电动势估算非常吃力。反电动势幅值和转速成正比,转速接近零时反电动势几乎为零,SMO的切换项再也提不出有效信号,PLL看到的全是噪声。

因此工程上常见做法是:低速段用I/F开环强拖或者高频注入,中高速段切回SMO闭环。代码框架里预留了切换状态机,低速时角度是程序自己转的,这时候电流环依然工作,但角度环路是开环的。启动完成后转速超过门槛,再按前面说的渐进方式切换到闭环,这套逻辑在风机、水泵、压缩机这类负载上已经验证过,启动比较顺畅。

如果项目要求在零速带额定负载,那高频注入就绕不过去了。高频注入的基本思路是在电压指令上叠加一个高频激励,再通过带通滤波器从电流响应里解调出凸极位置。这需要电机有一定的凸极性,表贴式电机做起来效果会差一点。这块不在本次源码范围内,但代码的模块接口已经预留了角度来源切换位置,后续接高频注入模块不需要改动电流环。

6.2 参数偏差对估算角度的敏感性

位置估算不是只看算法好不好,还看系统参数准不准。电阻Rs的变化主要影响低速段,温度升高电阻变大,如果模型里还用冷态电阻,低速时估算角度会偏。电感Ls的变化主要影响观测器动态,电感偏大或偏小都会让观测电流的收敛速度偏离预期。我实测过,Ls偏差超过20%时,角度误差会出现明显的转速相关偏移。

解决办法有两种思路。一种是被动鲁棒,把滑模增益调大一些,牺牲一点噪声性能换取对参数偏差的容忍度。另一种是主动辨识,在电机停机或低速阶段注入特定的电压序列,通过电流响应离线辨识出Rs和Ls,启动前更新一次参数表。更进一步的在线辨识可以用递推最小二乘或者模型参考自适应,但计算量会明显增加,普通单片机在10kHz周期里跑起来会比较紧张。

我在源码里放了一个参数结构体,电阻、电感、极对数和反电动势系数全部独立,方便按温度查表更新。实际项目里如果工作温度变化剧烈,做个两三点温度标定查表,比盲目推在线辨识算法划算得多。

6.3 后续可扩展的方向:在线辨识与无感全速域

这套源码的目标场景是中高速段稳定运行和常规负载启动,如果要覆盖更极端的工况,至少还有三个方向可以扩展。第一是高频注入,解决零速和极低速带载问题;第二是弱磁控制,高速段电压接近母线极限时,需要限制q轴电压或者加入超前角控制,否则位置估算会在电压饱和区失真;第三是参数在线辨识,让观测器在温度和老化影响下自动修正模型。

这几个方向现在也都有不错的开源资料可以参考,但很难直接抄过来用。原因多数不在算法本身,而在于硬件差异,比如电流采样精度、PWM频率、死区时间、母线电容容量,这些都会改变算法实际表现。唯一的捷径就是先把基础的无感FOC角度闭环调稳,再往两边扩展。如果后续我把在线辨识的调试记录也整理出来,会再和大家聊得更细一些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询