前阵子有个做电机驱动的朋友问我:低速无感FOC是不是都在用高频注入?我说,会用的说容易,真正从代码上看明白的没几个。高频注入(HFI)看起来不过是在d轴电压上叠一个正弦、再把电流过几个滤波器,可一旦想让它从零速平稳转起来,IIR滤波、位置估计、磁极极性判断全都牵连在一起,任何一个环节不对,电机就会原地抖给你看。这篇文章我打算用实际调过的C代码整理一条完整链路,权当一次项目复盘。
这篇内容会有一定门槛,建议至少跑过基础FOC电流环再往下看。如果你接触过STM32上实现FOC、看过PWM波形和ADC触发,那基本能无缝对接;如果只是刚接触无感控制,也可以先把整体流程看明白,后面代码部分再慢慢啃。
1. 高频注入无感FOC的整体思路拆解
1.1 为什么低速不能只靠反电动势
无感FOC最常见的方案就是滑膜观测器、龙贝格观测器这一类基于反电动势的估算方法。反电动势的幅值和电角速度成正比,电机在低速甚至零速时,反电动势只剩下几十毫伏甚至更低,早就淹没在PWM开关噪声、死区影响和ADC量化误差里了。这时候观测器输出的角度会带着很大的畸变,电流环一闭环就相当于对着一个错误角度干活,轻则电流波形难看,重则直接失步。
所以无感FOC在低速段必须换思路。高频注入(HFI)不是等反电动势出来再估算,而是主动在某个方向上施加一个高频电压激励,通过测量电流对电感的响应来反推转子位置。转子位置信息本质上被“编码”在电感上,高频信号只是把这段编码读出来的载波。这也是为什么HFI能覆盖到零速的原因——你一开机就注入,位置信息马上就有了,不需要电机先转起来。
1.2 凸极效应与高频注入的基本盘
很多人一提到凸极就以为是内置式PMSM(IPM),表贴式PMSM(SPM)没有凸极性,所以不能用HFI。这是最常见的误解。表贴式电机在结构上确实是隐极,但磁路在饱和之后会出现交叉饱和凸极效应。简单理解:当转子磁极方向和注入磁场方向一致时,磁路更容易饱和,等效电感变小;方向垂直时,磁路相对不饱和,电感变大。这个电感差异虽然只有百分之几,但足以让高频电流的响应带上位置信息。
高频注入的具体形式有旋转电压注入和脉振电压注入两类。旋转注入通常注射在静止坐标系上,适合内置式电机;脉振注入则在估算d轴方向施加高频电压,对表贴式电机更常用,因为它可以利用饱和凸极,而且不需要知道准确的初始位置。下面我的代码示例就是用脉振注入,因为这套方案在低速启动场景下最普适,调试起来也相对直观。
1.3 从零速到高速的完整无感流程
HFI不是无感FOC的全部,它只管低速和零速这一亩三分地。实际项目里,完整的启动流程通常是这样的:
开机后先做初始位置检测,用HFI或者定频脉冲估算一次转子角度,再做磁极极性判断,确认N极方向;然后切到HFI闭环,让电流环用估算角度跑起来,带着电机从零低速加速;当转速升高到反电动势信噪比足够高的阈值后,再切到滑膜或龙贝格观测器。整个过程就是一个低速用高频注入观察、中高速用反电动势观察的接力赛。
所以你现在知道为什么标题里把“位置估计”和“磁极极性判断”并列了。前者解决角度是多少,后者解决角度是加0°还是180°,两个问题缺一个,电机转不起来。
2. 从代码看高频信号注入与IIR滤波的实现
2.1 注入高频电压信号:藏在电流环输出里的正弦
脉振高频注入的信号很简单,在估算d轴的电压指令上叠加一个固定频率、固定幅值的高频余弦波。大部分代码会放在电流控制器输出之后、逆Park变换之前,因为注入信号跟随估算角度旋转,省去额外坐标变换。
// HFI信号生成 float hfi_theta = 0.0f; // 高频信号相位 float hfi_freq_rad = 2.0f * PI * HFI_FREQ_Hz; float hfi_voltage_V = 0.08f * VBUS_V; // 幅值取8%母线电压,具体后面讲 // 在电流环每个PWM周期调用一次 void HFI_InjectDaxis(float* vd_ref, float* vq_ref, float dt) { hfi_theta += hfi_freq_rad * dt; if (hfi_theta > 2.0f * PI) hfi_theta -= 2.0f * PI; *vd_ref += hfi_voltage_V * cosf(hfi_theta); }这里有几个关键细节。第一,注入的余弦频率要远高于基波电频率,但又要远低于PWM开关频率。一般采样率10kHz时,注入频率放在1kHz左右比较合适,采样率20kHz时可以用2kHz。第二,注入幅值不要一上来就拉满,我习惯从5%母线电压起步,看到高频电流响应后再慢慢加,加到纹波电流在额定电流5%以内为止。第三,这个函数必须在电流环输出后、最终电压限幅前调用,否则注入信号会被限幅逻辑削平。
有些方案会在q轴也加一个正交信号,目的是避免交叉耦合影响。实际项目里,表贴式电机用单d轴脉振注入就够,q轴注入反而会增大转矩波动,得不偿失。
2.2 坐标变换:从三相电流里取出i_d和i_q
高频注入之后,电流环采到的相电流里会有基波、PWM谐波和高频响应电流三层信息。我们需要先把三相电流变到估算dq坐标,方便后面分离高频分量。
typedef struct { float i_alpha; float i_beta; float i_d; float i_q; } CurrentFrame; // Clarke变换 void ClarkeTransform(float ia, float ib, float ic, CurrentFrame* cur) { cur->i_alpha = ia; cur->i_beta = (ia + 2.0f * ib) * 0.57735027f; // 1/sqrt(3) } // Park变换,theta为当前估算电角度 void ParkTransform(CurrentFrame* cur, float theta) { float sin_t = sinf(theta); float cos_t = cosf(theta); float id = cur->i_alpha * cos_t + cur->i_beta * sin_t; float iq = -cur->i_alpha * sin_t + cur->i_beta * cos_t; cur->i_d = id; cur->i_q = iq; }这里有个容易踩的坑:坐标变换用的角度必须是估算角度,而不是实际角度(实际角度咱们正在估呢)。所以Park变换要和HFI注入共用同一个theta_est,不能自己另搞一个角度。有些新手先把电流变到静止坐标系再过滤波器,最后再用外差法解调,也可以,但计算量稍微大一点。
2.3 IIR带通滤波器:为什么是它,系数怎么来
从变换后的i_d、i_q里提取高频分量,典型做法是带通滤波器。用FIR带宽直,群延迟线性,但阶数动不动就是几十阶,单片机跑起来很肉。工程上几乎都用二阶IIR带通,两个SCL存储,跑一次只有五次乘加运算,算力开销基本可以忽略。
IIR带通的标准差分方程是:
y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]具体系数可以通过双线性变换生成。我在调试中常用一个参数初值:中心频率1kHz、采样率10kHz、品质因数Q取0.75~1.2,然后系数固定死放常量表。下面是一个可以直接用的二阶带通滤波器实现:
typedef struct { float b0, b1, b2; float a1, a2; float z1, z2; } Biquad; float Biquad_Process(Biquad* f, float x) { float y = f->b0 * x + f->z1; f->z1 = f->b1 * x - f->a1 * y + f->z2; f->z2 = f->b2 * x - f->a2 * y; return y; }为了防止初始化瞬间产生冲击,滤波器系数先清零,z1、z2也要清零。IIR滤波器存在相位延迟,而且中心频率附近的延迟尤其大,后面解调时要做角度补偿,这个我会在第4章详细说。
为什么不用陷波器?因为我们要保留高频信号本身,不能把注入频率“挖”掉。如果只是想滤掉高频PWM噪声,用陷波器可以;但在HFI链路里,高频信号是你要的信息,所以要用带通把1kHz周围搂出来,同时把基波和低频甩开。有些方案先用高通滤波丢掉直流量,再做同步解调,也可以,但带通信噪比更好。
2.4 解调出位置误差信号
提取到d/q轴的高频电流分量之后,还需要做解调才能变成位置误差信号。解调思路是:高频电流i_qh本身的包络携带了角度误差信息,但它的频率是高频信号频率,直接用积分会得到零均值,所以要先乘上一个同频参考信号,再用低通滤波器把高频载波滤掉。
// 全局变量 static Biquad g_bpf_id; static Biquad g_bpf_iq; static Biquad g_lpf_eps; // 每周期调用一次,theta_hfi 与注入正弦同相 float HFI_ExtractError(float id_hfi_in, float iq_hfi_in) { float id_h = Biquad_Process(&g_bpf_id, id_hfi_in); float iq_h = Biquad_Process(&g_bpf_iq, iq_hfi_in); // 同步解调:乘高频余弦,再用低通取直流分量 float demod = iq_h * 2.0f * cosf(hfi_theta); float epsilon = Biquad_Process(&g_lpf_eps, demod); return epsilon; }系数2.0是解调增益,用来补偿相乘之后产生的0.5倍因子。这个值不是必须的,但把信号尺度放大到和真实角度误差相似的量级,后面调PLL带宽会舒服很多。低通滤波器带宽我一般取50~200Hz,太小会拖慢响应,太大会残留高频纹波,导致估算角度抖动。
3. 位置估计与磁极极性判断的完整代码逻辑
3.1 外差法把高频dq电流变成角度差
脉振注入法的核心结论是:当估算角度和实际角度之间存在误差Δθ时,q轴高频电流幅值会近似正比于sin(2Δθ)。所以解调出的ε就是一个比例系数乘以sin(2Δθ)。在小角度时sin(2Δθ)≈2Δθ,这就是一个天然的角度误差检测器。
如果你把动态坐标系推导完整,会看到同时存在一个与位置无关的偏置项,来自d、q轴电感差异的不对称。实际工程里这个偏置会造成角度估计带固定偏移,所以很多人会在解调之后加一个自适应补偿项,或者直接在做极性判断之后进行角度偏移标定。我第一次调的时候没管这个偏置,结果锁出来的角度差了大概十几度电角度,电流倒是能转,但效率奇低。后来我在调试界面里同时看HFI输出角度和手动顺转到角度,记下偏差,在观测器输出端减去这个常数偏移,问题就解决了。
如果需要更精确,可以再加上一个正交解调通道:
float demod_sin = iq_h * 2.0f * sinf(hfi_theta); float demod_cos = iq_h * 2.0f * cosf(hfi_theta); // demod_sin 经过LPF得到幅值,demod_cos经过LPF得到零偏 // 用幅值归一化后求atan2(demod_cos, demod_sin),得到的小量就是2Δθ相位偏移不过现场调试中,大多数工程师不会真做完整atan2,而是直接把同步解调后的信号当作误差值扔给PLL。速度角度来自PLL积分,稳态误差很小,没必要做那么精细。
3.2 PLL观测器:让角度自己转起来
位置误差信号ε进入锁相环(PLL),PLL输出电角速度和电角度。PLL的好处是天然对噪声有积分平滑作用,而且不会像直接积分那样漂移。
typedef struct { float kp; float ki; float integral; float theta_est; float speed_est; float max_speed; } PllObserver; void Pll_Update(PllObserver* pll, float angle_error, float dt) { pll->integral += pll->ki * angle_error * dt; float speed = pll->kp * angle_error + pll->integral; if (speed > pll->max_speed) speed = pll->max_speed; if (speed < -pll->max_speed) speed = -pll->max_speed; pll->theta_est += speed * dt; pll->speed_est = speed; // 角度归一化到 (-pi, pi] if (pll->theta_est > PI) pll->theta_est -= 2.0f * PI; if (pll->theta_est < -PI) pll->theta_est += 2.0f * PI; }PLL带宽选择是个平衡。带宽太窄,对负载突变的响应跟不上,启动时会听见电机闷哼一下才动;带宽太宽,会把解调出来的高频纹波直接当速度用,转速估计上全是毛刺。我通常先把PLL的转折频率设在100Hz左右,然后观察稳态角度波形,再慢慢往上推。结合传递函数估算:kp = 2*带宽,ki = 带宽²,如果带宽100Hz,对应角频率约628rad/s,那么kp≈1256,ki≈394000左右。这些值不绝对,但量级是对的。
3.3 磁极极性判断:差180°时怎么办
脉振注入的位置观测器天然存在180°模糊。因为sin(2Δθ)在Δθ=0和Δθ=π时都等于0,PLL可能锁在真实角度,也可能锁在真实角度加180°。如果带着错180°的角度直接闭环,电机要么反转,要么卡死不转。所以极性判断是开工前必须做的一步。
工程上最常用的是电压脉冲法。思路是:转子磁极饱和程度和磁链方向有关,N极方向上磁场叠加互感更强,等效电感更小,电流上升更快;反方向则等效电感更大,电流上升慢。所以我们在估算角度方向上施加正、负两个相同的短电压脉冲,比较两次电流变化斜率。变化快的那次对应的方向就是N极方向。
#define HFI_POL_PULSES 6 bool HFI_PolarityCheck(float theta_est, float* corrected_theta) { float slope_pos = 0.0f; float slope_neg = 0.0f; float v_pulse = 0.15f * VBUS_V; // 脉冲电压幅值 float t_on = 0.0005f; // 脉冲宽度 500us for (int i = 0; i < HFI_POL_PULSES; i++) { // 先施加正方向脉冲,实测电流斜率并累加 slope_pos += ApplyVoltagePulse(theta_est, v_pulse, t_on); // 再施加负方向脉冲,实测电流斜率 slope_neg += ApplyVoltagePulse(theta_est, -v_pulse, t_on); // 两次脉冲之间留一段零电压时间,让电流衰减回零 WaitCurrentDecay(); } slope_pos /= HFI_POL_PULSES; slope_neg /= HFI_POL_PULSES; if (slope_pos > slope_neg * 1.1f) { *corrected_theta = theta_est; return true; } else if (slope_neg > slope_pos * 1.1f) { *corrected_theta = theta_est + PI; // 翻180度 return true; } return false; }注意脉冲宽度不能太长,否则电机可能被推动,也不能太短,否则ADC采不到足够的斜率样本。5%额定电压、500微秒宽度在大多数小功率PMSM上是个可靠的起点。极性判断不成功时不能盲目闭环,要重新注入或者报错。我碰到过一个情况:负载锁在电机轴上,正负脉冲电流都差不多,判断阈值又严,结果一直报错。后来我把判断次数从3次提到6次,稍微放宽10%判据,才稳定。但也别放宽太多,否则真的会反转。
3.4 HFI状态机:从开机到正常运行的顺序
HFI不是随便在电流环里加个函数就行,它需要按顺序推进。我常用的状态机分四段:
IDLE -> START_ESTIMATION -> HFI_RUN -> OBSERVER_SWITCH- IDLE:等待启动命令,滤波器复位。
- START_ESTIMATION:注入高频信号约几十到几百毫秒,同时做带通和解调,得到初始估算角度。
- POLARITY_CHECK:执行脉冲极性判断,修正初始角度。
- HFI_RUN:角度修正后直接进入HFI闭环,启动电机从零加速。
- OBSERVER_SWITCH:运行到设定转速以上,且反电动势观测器角度和HFI角度误差小于阈值,切换到滑膜观测器。
typedef enum { HFI_IDLE, HFI_START_EST, HFI_POLARITY, HFI_RUN, HFI_SWITCH, } HfiState; void HFI_StateMachine(HfiState* state, float* theta, float* speed, float rpm, float dt) { switch (*state) { case HFI_IDLE: if (start_cmd) { HfiResetFilters(); *state = HFI_START_EST; } break; case HFI_START_EST: HFI_InjectDaxis(vd_ref, vq_ref, dt); HFI_RunPll(); if (est_time > 200e-3f) { HFI_PolarityCheck(*theta, theta); *state = HFI_RUN; } break; case HFI_RUN: HFI_InjectDaxis(vd_ref, vq_ref, dt); HFI_RunPll(); if (rpm > switch_speed) *state = HFI_SWITCH; break; case HFI_SWITCH: // 交给反电动势观测器 break; } }这个状态机的好处是每个阶段的进入、退出条件非常清楚,调试时加断点、看日志都很方便。很多量产项目里还会在START_EST阶段同时估计相电阻,用高频电流幅值推算初始电感,进一步校准极性判断阈值。
4. 高频注入参数怎么调:计算过程与调试实录
4.1 四个关键频率的配合关系
HFI整套系统里有四个频率需要布置:PWM采样频率、高频注入频率、带通中心频率、低通截止频率。它们必须拉开梯度,否则互相打架。
我以10kHz采样率为例做过一组常用参数配置:
| 频率量 | 数值 | 为什么这么选 |
|---|---|---|
| PWM/采样频率 | 10kHz | 单片机FOC控制基频 |
| 高频注入频率 | 1kHz | 采样率的1/10,既能分离基波,又不会过采样 |
| 带通中心频率 | 1kHz | 和注入频率一致 |
| 低通截止频率 | 100Hz | 只保留角度误差直流包络 |
如果把注入频率提高到2kHz,采样率还是10kHz,不是不行,但ADC需要保证在注入高频一个周期内采至少5到10个点,解调分辨率才够。如果PWM和ADC同步,10kHz采样率下2kHz也够,但留给电流环执行的时间就少了,容易在中断里超时。所以我个人保守一点,先上1kHz。
注入频率和基波频率之间最好也保持至少5倍以上。如果电机额定转速很高,基波频率上千赫兹,那HFI频段设计就要重新评估,可能得提高PWM频率。
4.2 一个二阶IIR带通系数的完整计算
很多代码直接塞一堆系数让人仿真,看得一头雾水。这里演示一次完整的计算过程。假设采样率fs=10000Hz,中心频率f0=1000Hz,Q=1。先算归一化角频率:
w0 = 2 * PI * f0 / fs = 2 * PI * 0.1 = 0.62832 rad alpha = sin(w0) / (2Q) = sin(0.62832) / 2 = 0.58779 / 2 = 0.29389 cos_w0 = cos(0.62832) = 0.80902然后按RBJ双二阶带通公式:
b0 = alpha = 0.29389 b1 = 0 b2 = -alpha = -0.29389 a0 = 1 + alpha = 1.29389 a1 = -2 * cos_w0 = -1.61803 a2 = 1 - alpha = 0.70611代码里实际使用的是归一化后的系数,要用a0除以各项:
b0' = b0 / a0 = 0.22719 b1' = 0 b2' = -0.22719 a1' = a1 / a0 = -1.25050 a2' = a2 / a0 = 0.54574把这五个数填进2.3节的Biquad结构体里就跑起来了。如果你用Q=0.75,带宽会稍微宽一点,阶跃响应更灵敏;Q=1.2,带宽窄,选择性好但相位延迟更大。我通常先Q=1,再根据启动响应微调。
4.3 调试波形的三个观察点
调HFI千万别上来就调参数,先把波形抓对。我习惯同时观察三个点:
第一,i_q高频带通滤波后的波形。正常情况应该是比较干净的正弦包络,频率和注入频率一致。如果这里已经乱七八糟,后面解调根本无从谈起。第二,解调后的ε信号。它应该是一条随转子位置缓慢变化的曲线,而不是等幅高频正弦。如果在静止状态,ε应该近似为0(实际有偏置),用手转动转子,ε会跟着角度变化。第三,PLL输出的估算角度,用手慢慢转动电机,角度应该平滑跟随,没有突跳。如果跳了180°,极性判断还没做或者做反了。
波形不对时优先怀疑坐标系问题。我见过一个案例,Park变换用的theta是编码器反馈,不是估算角度,HFI基线就一直在抖,电流环还发疯。这类问题在代码评审时盯着算两分钟就能发现,但现场调的时候特别容易绕进去。
4.4 常见问题排查速查表
我把调HFI过程中最常遇到的问题整理成一张表,都是现场踩过的:
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 启动后电机只震动不转 | 位置误差信号过小或极性错180° | 调大注入幅值,确认极性判断结果 |
| 估算角度有固定偏移 | 滤波器相位延迟、dq轴耦合偏置 | 标定角度补偿,或查解调相位 |
| HFI频段听到尖锐噪音 | 注入幅值偏大、频率接近谐振 | 降低幅值或微调频率 |
| 启动加速到某个转速就失步 | 高频注入带宽不够,基波进入带通 | 提高注入频率,或提前切滑模 |
| 极性判断偶尔失败 | 负载突变、脉冲次数不足、阈值过严 | 增加脉冲平均次数,扩宽判据 |
这些坑在仿真里看不出来,因为仿真模型用的是线性电感,没有饱和、没有死区,噪声也理想。真机调试才是HFI最残酷的考场。
5. 从HFI到反电动势切换:几个容易忽略的工程细节
5.1 切换前的角度同步和速度平滑
HFI在低速段好用,但在中高速段要继续用它就显得笨重。所以完整代码里还要有观测器切换逻辑。切换最忌讳生切,也就是把HFI角度瞬间替换成反电动势观测器角度。两个角度源从不同机理估出来,总会存在几度偏差,生切会给电流环来一记“急刹车”,轻则转速波动,重则过流保护。
我通常在HFI_RUN阶段同时运行滑膜观测器,但它的输出不参与控制,只和一个经过低通处理的切换标志比较。当转速和角度差都满足条件时,给切换角度做一阶低通过渡,比如一个采样周期内从HFI角度线性过渡到滑膜角度,过渡时间控制在10~50ms。这样转矩波动会小很多。
5.2 死区PWM对高频观测的影响
PWM死区是高频注入的隐形杀手。死区时间会在线电压上叠加误差,这个误差的频谱很宽,包含高频分量,和注入频段重叠后会产生额外偏置。电机相电流越小,死区影响越明显,而HFI恰恰在低速小电流阶段工作最多,所以必须做死区补偿,至少要用符号函数补偿开关换相时间。
如果你的平台上没有死区补偿,一个变通办法是提高注入电压幅值到明显超过死区误差的程度,让高频电流响应盖过死区扰动。但幅值不能无限加,噪音和IGBT发热摆在那里。所以最好还是在FOC底层把死区补偿这块补上。
5.3 我踩过的一个坑:滤波器相位延迟补偿
最后讲一个最容易被忽视的细节。IIR带通滤波器在中心频率处不改变幅值,但会引入一明显相位延迟。如果解调参考信号还是用注入时的余弦函数,相当于用不完全同相的载波去乘,输出误差会带一个常数偏置,导致PLL锁定的角度偏移。
解决办法有两种。第一种是在解调参考信号里人为补偿滤波器相位,比如提前量设为滤波器的相位角;第二种是直接改用“双路同步解调+atan2”方案,把相位信息变成幅值信息,不依赖参考相位。我在量产代码里用的是第一种,因为计算量小。具体做法是在调试界面里把滤波器在中心频率处的相位求出来,然后对hfi_theta加一个补偿角,再用它做cos解调。补偿计量好后写死即可。
另一个更实用的技巧是:在启动阶段连续保存解调误差的平均值,标定一次零偏,之后在解调输出里减去这个零偏。因为很多偏置是固定方向的,这样的标定能很快让位置误差对中,比反复调滤波器相位更省事。
这套HFI代码链看起来不复杂,真正落地的坑在频率配合、极性判断和切换细节。如果你在项目里调到了类似问题,希望这份复盘能让你少走两趟弯路。