1. 为什么要在代码层面死磕高频注入无感 FOC很多人第一次接触无感 FOC都是从滑膜观测器或者龙伯格观测器入门的。反电动势观测这条路在中小转速区间确实好用但一旦转速掉到零或者极低速反电动势幅值小到几乎被噪声淹没观测器就彻底失明了。这时候高频注入HFI就成了绕不开的方案——它不依赖反电动势而是主动往电机里“打”一个高频信号靠凸极效应产生的响应电流来反推转子位置。但问题在于网上讲 HFI 的文章大多停留在公式推导层面什么高频电压矢量、d-q 轴电感差异、载波频率注入公式写得漂漂亮亮真到写代码的时候却不知道从哪下手。我自己当初啃这块的时候最大的困惑就是理论上的“高频响应电流”在代码里到底长什么样IIR 滤波器怎么设计才能把高频分量干净地提取出来位置估计出来的角度为什么总是差那么一点磁极极性判断又是在哪个环节做的这篇内容就是把我从零实现一套 HFI 无感 FOC 的完整过程拆开来讲重点放在代码实现和调试细节上。涉及的核心模块包括高频方波注入、IIR 带通滤波提取响应电流、基于锁相环的位置估计、以及磁极极性判断。适合已经了解 FOC 基本框架、想进一步搞懂无感低速控制的嵌入式工程师也适合正在调试 HFI 方案但被各种噪声和角度偏差折磨的同行。我用的平台是 STM32G4 系列配合 DRV8323 驱动板和一台 2204 云台电机7 对极相电感约 0.3mH相电阻约 12Ω。代码框架基于 ST 的 MCSDK 改的但 HFI 部分基本是自己重写的。下面所有代码片段都是实际跑通的不是伪代码。2. 高频注入的代码落地从注入信号到电流响应2.1 注入方式选型旋转电压矢量还是方波HFI 的注入方式主要有两种旋转高频电压矢量和脉振高频电压矢量。旋转注入是在 α-β 轴上注入一个旋转的电压矢量响应电流里同时包含正序和负序分量需要额外的滤波和坐标变换来分离。脉振注入则是在估计的 d 轴或 q 轴上注入高频信号响应电流只在估计轴附近波动解调逻辑更直接。我最终选了脉振方波注入原因有三个。第一方波注入的频谱集中在注入频率及其奇次谐波不像正弦注入那样能量分散信噪比更高。第二方波注入不需要生成正弦表直接交替给正负电压就行代码极简。第三方波注入的解调可以用简单的符号运算代替乘法省 CPU。具体做法是在估计的 d 轴方向上以载波频率的一半交替注入正负电压。假设 PWM 频率是 20kHz那么注入频率就是 10kHz每个 PWM 周期切换一次注入极性。注入电压幅值取母线电压的 10% 到 20%我这里取 15%。// 高频方波注入在估计d轴方向交替注入正负电压 #define HFI_VOLTAGE_AMP 0.15f // 注入电压幅值标幺值 static float hfi_sign 1.0f; // 注入极性符号 void HFI_Inject(float *vd_est, float *vq_est, float theta_est) { // 每个PWM周期翻转极性 hfi_sign -hfi_sign; float v_inject HFI_VOLTAGE_AMP * hfi_sign; // 将注入电压变换到估计的d-q轴 // 注入在d轴q轴不注入 *vd_est v_inject; *vq_est 0.0f; }这里有个细节要注意注入电压是加在估计的 d 轴上的不是真实的 d 轴。因为我们不知道真实转子位置只能用估计角度做坐标变换。估计角度和真实角度的偏差会体现在响应电流的幅值上这正是位置估计的物理基础。2.2 电流采样时机与注入频率的配合方波注入对电流采样时机非常敏感。如果采样时刻正好落在电压切换的瞬间采到的电流会包含大量开关噪声。正确的做法是在每个 PWM 周期的中点采样此时电流已经稳定开关噪声也衰减得差不多了。STM32G4 的 ADC 可以配置成由定时器的中点触发。我用的中心对齐 PWM 模式计数器从 0 到 ARR 再回到 0中点就是计数器等于 ARR 的时刻。把 ADC 触发源设成这个时刻采到的就是注入电压作用半个周期后的稳定电流。// 定时器中点触发ADC采样配置以TIM1为例 void ADC_Trigger_Config(void) { // 选择TIM1作为触发源 ADC1-CFGR | ADC_CFGR_EXTSEL_0 | ADC_CFGR_EXTSEL_1; // 触发边沿上升沿 ADC1-CFGR | ADC_CFGR_EXTEN_0; // TIM1中点触发 TIM1-CR2 | TIM_CR2_MMS_1; // 更新事件作为触发输出 }实测下来采样时刻偏差哪怕只有几百纳秒响应电流的幅值就会有明显变化。我一开始没注意这个采到的电流波形毛刺特别大后来把采样点精确对齐到中点波形立刻干净了很多。2.3 响应电流的物理含义与提取目标在估计 d 轴注入高频电压后由于电机的凸极效应d 轴和 q 轴电感不等响应电流里会包含一个与转子位置误差相关的分量。具体来说估计 d 轴上的高频电流响应可以近似表示为i_dh ≈ V_inject × sin(2Δθ) / (2 × ω_h × L_d × L_q) × (L_q - L_d)其中 Δθ 是估计角度与真实角度的偏差ω_h 是注入频率L_d 和 L_q 分别是 d 轴和 q 轴电感。当 Δθ 趋近于零时sin(2Δθ) 也趋近于零响应电流幅值最小。这就是位置估计的目标让估计角度跟踪真实角度使响应电流的特定分量趋于零。但实际采到的电流里混了大量东西基波电流、PWM 开关噪声、注入频率的响应、还有各种谐波。我们需要把注入频率附近的响应电流提取出来这就是 IIR 滤波器的任务。3. IIR 滤波器在 HFI 里的实战设计3.1 为什么选 IIR 而不是 FIR在 HFI 场景下滤波器的作用是把注入频率附近的电流分量提取出来同时抑制基波和开关噪声。FIR 滤波器可以实现线性相位但阶数高、计算量大。IIR 滤波器用较低的阶数就能实现陡峭的过渡带计算量小适合在中断里实时跑。我选的是二阶 IIR 带通滤波器中心频率设在注入频率10kHz带宽约 2kHz。这个带宽要足够窄以抑制基波又不能太窄否则响应电流的相位延迟太大影响位置估计的动态性能。3.2 双线性变换法设计带通滤波器系数IIR 滤波器的设计我用的是双线性变换法。先在模拟域设计一个带通滤波器然后通过双线性变换映射到数字域。具体参数中心频率 f0 10kHz采样频率 fs 20kHz品质因数 Q 5。模拟域带通滤波器的传递函数为H(s) (ω0/Q × s) / (s² ω0/Q × s ω0²)其中 ω0 2π × f0。用双线性变换 s 2fs × (1 - z⁻¹) / (1 z⁻¹) 代入整理后得到数字域系数。我直接在 MATLAB 里算好系数然后硬编码到代码里。// 二阶IIR带通滤波器系数 // 中心频率10kHz采样频率20kHzQ5 // 系数由MATLAB双线性变换法生成 #define IIR_B0 0.2345f #define IIR_B1 0.0f #define IIR_B2 -0.2345f #define IIR_A1 -0.5310f #define IIR_A2 0.5310f typedef struct { float x1, x2; // 输入历史 float y1, y2; // 输出历史 } IIR_Filter_t; float IIR_BandPass(IIR_Filter_t *f, float input) { // 直接II型结构 float output IIR_B0 * input IIR_B1 * f-x1 IIR_B2 * f-x2 - IIR_A1 * f-y1 - IIR_A2 * f-y2; // 更新历史 f-x2 f-x1; f-x1 input; f-y2 f-y1; f-y1 output; return output; }这里用的是直接 II 型结构只需要两个历史状态内存占用小。系数用 float 存储STM32G4 有硬件浮点单元跑起来完全不费力。3.3 滤波器相位延迟对位置估计的影响IIR 滤波器不是线性相位的在中心频率附近有相位延迟。这个延迟会直接体现在位置估计的角度偏差上。我实测下来二阶带通滤波器在 10kHz 处的相位延迟大约是 30 度左右对应到电角度上大概是几度的偏差。补偿方法有两种一种是在位置估计的输出端加一个固定的角度补偿另一种是在锁相环的环路里把滤波器的相位特性考虑进去。我用的第一种简单直接。补偿角度通过实验标定给定一个已知的转子位置看估计角度差多少然后把这个差值补回去。// 滤波器相位延迟补偿 #define HFI_PHASE_COMP 0.05f // 约3度电角度实测标定 float theta_compensated theta_est HFI_PHASE_COMP;这个补偿值不是固定的不同电机、不同注入频率下会有差异。建议在调试阶段用示波器同时看真实角度和估计角度手动标定这个值。3.4 滤波器在中断里的执行时序HFI 的整个解调过程都在 PWM 中断里跑时序非常紧张。我的中断频率是 20kHz也就是每 50 微秒执行一次。在这 50 微秒里要完成电流采样、Clarke 变换、Park 变换、IIR 滤波、位置估计、电流环计算、SVPWM 生成。IIR 滤波器的计算量很小两个乘加运算加上历史更新大概几十个时钟周期就搞定了。真正耗时的是 Park 变换和 SVPWM 里的三角函数。我用了查表法加线性插值来加速把中断执行时间控制在 30 微秒以内留出足够的余量。注意IIR 滤波器的历史状态在每次中断里都要更新不能因为中断嵌套或者优先级问题导致状态丢失。我把 HFI 相关的变量都放在全局静态区确保不会被栈溢出影响。4. 位置估计从滤波后电流到角度输出4.1 锁相环结构的选择与参数整定提取出高频响应电流后下一步是从中解调出位置误差信号然后通过锁相环PLL跟踪出转子角度和速度。PLL 的结构是一个二阶环路包含一个比例积分控制器和一个积分器。误差信号的提取方式取决于注入方式。对于脉振方波注入响应电流的包络与位置误差的正弦值成正比。具体做法是把滤波后的电流乘以注入极性的符号然后取直流分量。这个直流分量就是位置误差信号。// 位置误差信号提取 float i_dh_filtered IIR_BandPass(hfi_filter, i_d_est); float error i_dh_filtered * hfi_sign; // 乘以注入极性符号 // 低通滤波提取直流分量 error IIR_LowPass(error_filter, error);这个 error 信号在真实角度附近近似与角度偏差成正比。PLL 的任务就是把这个 error 驱动到零。PLL 的参数整定是个细活。比例增益决定了跟踪速度积分增益决定了稳态误差。我一开始参数给大了角度估计抖得厉害给小了又跟踪不上。最后用试凑法先把积分增益设为零调比例增益到角度能跟上但略有超调然后慢慢加积分增益消除稳态误差。// 二阶锁相环 typedef struct { float kp; float ki; float integrator; float theta; float omega; } PLL_t; void PLL_Update(PLL_t *pll, float error, float dt) { // 比例项 float proportional pll-kp * error; // 积分项 pll-integrator pll-ki * error * dt; // 速度估计 pll-omega proportional pll-integrator; // 角度积分 pll-theta pll-omega * dt; // 角度归一化到[-pi, pi] if (pll-theta M_PI) pll-theta - 2.0f * M_PI; if (pll-theta -M_PI) pll-theta 2.0f * M_PI; }我用的参数是 kp 200ki 2000dt 50 微秒。这套参数在 2204 电机上跑下来角度跟踪误差在正负 5 度以内速度估计也比较平滑。4.2 角度收敛的实测波形分析调试阶段我用 DAC 把估计角度和真实角度同时输出到示波器上看。真实角度来自编码器调试时临时装的估计角度来自 PLL。刚开始的时候估计角度完全跟不上波形乱七八糟。排查后发现两个问题一是 IIR 滤波器的中心频率设错了实际注入频率是 10kHz 但我设成了 8kHz二是 PLL 的积分增益太大导致角度振荡。把滤波器中心频率改对之后估计角度开始能跟上真实角度了但还有明显的滞后和抖动。进一步调整 PLL 参数减小积分增益抖动明显改善。最后把滤波器相位补偿加上滞后也基本消除了。实测波形上估计角度和真实角度基本重合只在电机突然加减速的时候有短暂的偏差恢复时间大概几十毫秒。这个性能对于风扇、泵类应用完全够用了。4.3 低速段的估计精度与切换策略HFI 在低速段0 到额定转速的 10%表现最好因为反电动势还没起来凸极效应是唯一可用的位置信息。但到了中高速段反电动势幅值增大HFI 的注入信号反而成了干扰这时候应该切换到反电动势观测器。切换策略我用的是一段过渡区间的加权融合。在 5% 到 15% 额定转速之间HFI 估计的角度和反电动势观测器估计的角度按转速线性加权。低于 5% 完全用 HFI高于 15% 完全用观测器。// 角度融合 float weight (speed - speed_low) / (speed_high - speed_low); weight fmaxf(0.0f, fminf(1.0f, weight)); float theta_fused (1.0f - weight) * theta_hfi weight * theta_observer;这个融合策略的关键是过渡区间要足够宽避免切换时角度跳变。我试过窄过渡区间结果在切换点附近电机有明显的抖动加宽之后平滑了很多。5. 磁极极性判断一个容易被忽略但绕不过去的环节5.1 为什么 HFI 无法区分 N 极和 S 极HFI 依赖的是电机的凸极效应也就是 d 轴和 q 轴电感不等。但凸极效应是对称的d 轴电感比 q 轴小这个差异在转子 N 极朝向和 S 极朝向时是一样的。换句话说HFI 能估计出转子的轴线方向但无法判断这条轴线上哪一端是 N 极。这个问题在电机启动时特别关键。如果极性判断错了电机可能反转或者启动失败。我一开始没做极性判断结果电机有 50% 的概率启动时反转虽然不会烧电机但体验极差。5.2 基于饱和效应的极性判断代码实现判断极性的经典方法是利用铁芯的饱和效应。在估计的 d 轴方向分别注入正负方向的电压脉冲由于永磁体的存在N 极方向的铁芯饱和度更高电感更小响应电流更大。通过比较正负脉冲的响应电流幅值就能判断出 N 极方向。具体实现在电机静止时先在估计 d 轴正方向注入一个短时电压脉冲比如 1ms记录响应电流幅值 I_pos然后在负方向注入同样幅值的脉冲记录响应电流幅值 I_neg。如果 I_pos I_neg说明估计 d 轴正方向就是 N 极反之则需要把角度补偿 180 度。// 磁极极性判断 float I_pos 0.0f, I_neg 0.0f; // 正方向脉冲 HFI_Inject_Pulse(HFI_POLARITY_AMP, theta_est); delay_ms(1); I_pos Get_Current_Magnitude(); // 负方向脉冲 HFI_Inject_Pulse(-HFI_POLARITY_AMP, theta_est); delay_ms(1); I_neg Get_Current_Magnitude(); // 判断极性 if (I_pos I_neg) { // 估计角度需要补偿180度 theta_est M_PI; if (theta_est M_PI) theta_est - 2.0f * M_PI; }这里有个坑脉冲幅值不能太大否则电机会动也不能太小否则饱和效应不明显。我试下来脉冲幅值取额定电压的 30% 左右脉冲宽度 1ms效果比较稳定。另外两次脉冲之间要留足够的间隔至少 5ms让电流完全衰减到零否则会互相干扰。5.3 极性判断的可靠性验证与失败处理极性判断不是 100% 可靠的特别是在负载变化或者温度变化的情况下。我做了多次重复测试发现大约有 5% 的概率判断错误。为了提高可靠性我加了多次判断取多数的逻辑连续做三次极性判断取多数结果。// 多次判断取多数 int polarity_votes 0; for (int i 0; i 3; i) { if (Check_Polarity() POLARITY_POSITIVE) { polarity_votes; } delay_ms(10); } if (polarity_votes 2) { theta_est M_PI; // 多数判断为负补偿180度 }如果三次判断结果不一致说明饱和效应不明显这时候可以适当增大脉冲幅值再试。如果始终判断失败那可能是电机凸极率太低HFI 方案本身就不适用需要考虑其他启动方式。6. 调试过程中踩过的坑与实测经验6.1 注入频率与 PWM 频率的整数倍关系我一开始把注入频率设成 7kHzPWM 频率 20kHz结果响应电流里出现了奇怪的拍频。后来才想明白注入频率和 PWM 频率如果不是整数倍关系每个 PWM 周期里注入信号的相位都在变解调出来的误差信号会包含一个低频振荡。改成 10kHz 之后拍频消失了。所以注入频率一定要选 PWM 频率的整数分之一最好是二分之一或者四分之一。我用的是二分之一也就是每个 PWM 周期翻转一次极性。6.2 电流采样噪声对 HFI 的致命影响HFI 的响应电流幅值通常只有基波电流的百分之几对采样噪声非常敏感。我一开始用的 ADC 采样没有做任何滤波采到的电流里全是开关噪声IIR 滤波器根本提取不出有用的信号。后来加了硬件 RC 滤波截止频率约 100kHz软件上又在 ADC 中断里做了简单的滑动平均噪声才降下来。另外电流采样电阻的布局也很关键要走差分线远离功率回路。我重新画了一版 PCB 之后噪声明显改善。6.3 不同电机凸极率对 HFI 效果的影响HFI 的效果高度依赖电机的凸极率L_q / L_d。我试过三台电机2204 云台电机凸极率约 1.3效果不错一台 2804 电机凸极率约 1.1效果勉强还有一台表贴式电机凸极率接近 1.0HFI 完全失效。所以选电机的时候要注意表贴式永磁电机SPM凸极率低不适合 HFI内嵌式永磁电机IPM凸极率高HFI 效果好。如果手头的电机凸极率太低可以考虑用高频电流注入代替电压注入或者改用其他无感方案。6.4 参数整定的顺序与经验值HFI 调试的参数很多我总结了一个整定顺序先调 IIR 滤波器中心频率和带宽确保响应电流提取干净再调 PLL 的比例增益让角度能跟上然后调积分增益消除稳态误差最后标定相位补偿消除角度偏差。经验值方面注入电压幅值取母线电压的 10% 到 20%注入频率取 PWM 频率的二分之一IIR 滤波器 Q 值取 3 到 10PLL 比例增益取 100 到 500积分增益取 1000 到 5000。这些值不是绝对的要根据电机参数和实际效果调整。7. 代码框架的模块化组织与移植建议7.1 HFI 模块的接口设计为了让 HFI 模块能方便地移植到不同项目我把所有 HFI 相关的代码封装成一个独立的模块对外只暴露几个接口初始化、注入、解调、获取角度。// HFI模块接口 typedef struct { float theta_est; // 估计角度 float omega_est; // 估计速度 float i_dh; // 高频d轴电流 float i_qh; // 高频q轴电流 uint8_t polarity; // 极性判断结果 } HFI_Output_t; void HFI_Init(HFI_Config_t *config); void HFI_Update(float i_alpha, float i_beta, float v_alpha, float v_beta); HFI_Output_t HFI_GetOutput(void);这样设计的好处是主控制循环只需要调用 HFI_Update然后从输出结构体里读角度和速度不需要关心内部实现细节。移植到新平台时只需要改底层 ADC 和 PWM 的驱动HFI 算法本身不用动。7.2 从 STM32G4 移植到其他平台的注意事项STM32G4 有硬件浮点单元和三角函数加速器CORDIC跑 HFI 很轻松。如果移植到没有 FPU 的平台上比如 STM32F1需要把浮点运算改成定点运算或者用查表法代替三角函数。定点化的时候要注意精度损失。IIR 滤波器的系数很小定点化之后可能会损失有效位数。建议用 Q15 格式并且在累加的时候用 32 位中间变量防止溢出。另外不同平台的 PWM 定时器配置方式不同但核心逻辑是一样的中心对齐模式、中点触发 ADC、中断里执行 HFI 算法。把这三件事做好移植就不难。7.3 实测性能数据与适用场景总结我在 2204 电机上实测的数据最低稳定转速约 50 RPM额定 2000 RPM 的 2.5%角度估计误差在正负 5 度以内速度估计误差在正负 2% 以内启动时间约 200ms从静止到稳定跟踪。CPU 占用方面HFI 算法本身约占 15% 的 CPU 时间20kHz 中断频率下。这套方案适合风扇、水泵、云台这类对低速性能有要求但不需要极高动态响应的场景。如果应用需要快速加减速或者高精度位置控制HFI 可能不够需要结合其他观测器或者加编码器。我个人在实际操作中的体会是HFI 的难点不在算法本身而在调试。参数整定、噪声抑制、极性判断每一个环节都需要耐心地试。但只要把这几块都调通了无感 FOC 的低速性能会有质的提升。后续如果要做更宽速度范围的无感控制可以把 HFI 和反电动势观测器结合起来低速用 HFI高速用观测器中间平滑过渡。