
前阵子整理工作笔记翻到去年调无感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_nki omega_n²。我实际取值时zeta取0.707omega_n取100~300Hz对应的角频率然后根据控制频率做离散化修正。这个带宽要明显大于速度环带宽但又不能太大否则PLL会跟着反电动势毛刺跑。速度环带宽如果是10~20HzPLL自然频率放在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-11表示完全闭环 } 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的输出给PLLPLL的带宽需要比SMO低通滤波器的带宽低一些否则PLL会把SMO没滤干净的高频切换噪声全放大。我习惯先固定PLL带宽在200Hz左右然后调h。h从小到大试同时看估算电流误差。如果电流误差波形在每相过零点附近出现明显的毛刺说明h太小误差压不住如果电流误差波形抖动严重说明h可能偏大。h稳定后再调SMO的lpf_klpf_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角度闭环调稳再往两边扩展。如果后续我把在线辨识的调试记录也整理出来会再和大家聊得更细一些。