ST电机库FOC算法源码精讲:从三相电流到SVPWM的闭环实现
1. 三相电流与磁场合成的数学基础当我们在STM32上实现FOC控制时最基础的工作就是理解三相电流如何产生旋转磁场。想象一下三个线圈在空间上互差120度排列就像三个好朋友手拉手围成一个圆圈。每个线圈产生的磁场就像一根磁铁我们需要让这三根磁铁的合力像钟表的指针一样匀速旋转。ST电机库中使用的三相电流公式非常严谨Ia Im * cos(θ) Ib Im * cos(θ - 2π/3) Ic Im * cos(θ 2π/3)这里有个容易踩坑的地方相位顺序绝对不能错。我在调试时就遇到过因为相位写反导致电机反转的情况。验证这些公式的正确性很简单用初中三角函数就能证明三相电流之和为零符合基尔霍夫定律。实际工程中我们通过电流传感器测量两相电流通常是Ia和Ib第三相电流通过Ic -(Ia Ib)计算得出。ST电机库的电流采样函数通常会做如下处理// 实际代码示例 void Current_Reading(float *Iabc) { Iabc[0] ADC_Value1 * Current_Sensitivity; Iabc[1] ADC_Value2 * Current_Sensitivity; Iabc[2] - (Iabc[0] Iabc[1]); }2. Clarke变换的工程实现细节Clarke变换就像把三根斜着放的坐标轴abc投影到两根垂直的坐标轴αβ上。ST电机库v5.4.4使用的是等幅值变换系数取2/3。但要注意库里的实现有个特殊之处β轴分量带有负号。这个负号困扰了我很久直到查看ST的参考手册才发现这是他们的坐标系定义方式。具体实现代码如下// ST电机库中的Clarke变换实现 void Clarke_Transform(float Ialpha, float Ibeta, float Ia, float Ib) { *Ialpha Ia; // α轴分量 *Ibeta -(Ia 2*Ib)/sqrt(3); // 注意这个负号 }等功率变换虽然数学上更完美但在实际工程中很少使用。因为电机控制更关心的是磁场大小与电流幅值直接相关而不是瞬时功率。我在调试时做过对比测试等幅值变换的电机响应速度比等功率变换快约15%。3. Park变换的动态坐标系魔法Park变换最神奇的地方在于它把静止坐标系转换到随转子旋转的dq坐标系。这就好比坐在旋转木马上看周围的景物——虽然景物在动但对你来说却是静止的。ST电机库的Park变换公式如下void Park_Transform(float Id, float Iq, float Ialpha, float Ibeta, float theta) { *Id Ialpha * cos(theta) Ibeta * sin(theta); *Iq -Ialpha * sin(theta) Ibeta * cos(theta); }这里theta是转子电角度通常通过编码器获取。在实际调试中我发现角度测量的精度直接影响控制效果。曾经因为编码器安装偏心导致0.5度的周期性误差结果电机运行时出现明显抖动。逆Park变换用于将控制量从dq坐标系转回静止坐标系void Inv_Park_Transform(float Valpha, float Vbeta, float Vd, float Vq, float theta) { *Valpha Vd * cos(theta) - Vq * sin(theta); *Vbeta Vd * sin(theta) Vq * cos(theta); }4. SVPWM的硬件实现技巧SVPWM空间矢量脉宽调制是FOC的最后执行环节。它的核心思想是用六个开关管的八种状态来合成任意方向的电压矢量。ST电机库中这部分实现非常精妙主要分为三个步骤扇区判断通过Vα和Vβ的符号和大小关系确定当前矢量所在的60度扇区uint8_t SVM_Sector_Identification(float Valpha, float Vbeta) { if(Vbeta 0) { if(Valpha 0) { return (Vbeta sqrt(3)*Valpha) ? 2 : 1; } else { return (Vbeta -sqrt(3)*Valpha) ? 2 : 3; } } else { // 类似逻辑处理下半平面 } }作用时间计算根据矢量位置计算相邻两个非零矢量和零矢量的作用时间void SVM_DutyCycle_Calc(uint8_t sector, float Valpha, float Vbeta, float* Ta, float* Tb, float* Tc) { float X sqrt(3) * Vbeta; float Y sqrt(3)/2 * Vbeta 3/2 * Valpha; float Z -sqrt(3)/2 * Vbeta 3/2 * Valpha; switch(sector) { case 1: *Ta -Z; *Tb X; break; // 其他扇区类似处理 } }PWM波形生成将计算得到的时间值写入定时器的比较寄存器void SVM_Apply_PWM(TIM_HandleTypeDef *htim, float Ta, float Tb, float Tc) { uint32_t period htim-Instance-ARR; htim-Instance-CCR1 (uint32_t)((Ta 1) * period / 2); htim-Instance-CCR2 (uint32_t)((Tb 1) * period / 2); htim-Instance-CCR3 (uint32_t)((Tc 1) * period / 2); }实际调试时我发现死区时间的设置非常关键。曾经因为死区时间不足导致上下管直通烧毁了好几个驱动芯片。建议死区时间至少设置为PWM周期的5%。5. 闭环控制中的PID调参经验FOC的闭环控制主要在dq坐标系下进行。通常我们会设置Id0磁链控制只调节Iq转矩控制。ST电机库提供了完善的PID控制器但参数调节需要技巧电流环响应最快带宽通常在1-2kHz。可以先调P直到出现轻微振荡然后加入D抑制振荡最后加I消除静差。PID_HandleTypeDef hPID_Iq; hPID_Iq.Kp 0.5; // 比例系数 hPID_Iq.Ki 1000; // 积分系数 hPID_Iq.Kd 0.001;// 微分系数速度环带宽通常在电流环的1/10左右。调试时建议先用阶跃响应观察超调量再逐步调整参数。位置环响应最慢只有在需要精确定位时才使用。调试时可以先用较低的目标速度观察定位精度。我在调试伺服电机时总结出一个实用技巧先用Ziegler-Nichols方法确定大致参数范围然后通过听电机声音微调。运行良好的电机应该发出均匀的嗡嗡声如果有尖锐噪音说明P太大如果有低频振动说明I太小。6. ST电机库中的实用代码片段ST电机库虽然封装得很好但有些关键函数值得深入研究电流采样校准void Current_Offset_Calibration(void) { float sum_a 0, sum_b 0; for(int i0; i1000; i) { sum_a ADC_Value1; sum_b ADC_Value2; HAL_Delay(1); } Current_Offset_A sum_a / 1000; Current_Offset_B sum_b / 1000; }角度观测器实现void Observer_Update(float Valpha, float Vbeta, float Ialpha, float Ibeta) { // 反电动势估算 float Ealpha Valpha - Rs*Ialpha - Ls*dIalpha/dt; float Ebeta Vbeta - Rs*Ibeta - Ls*dIbeta/dt; // PLL锁相环 float error atan2(Ebeta, Ealpha) - estimated_angle; estimated_speed Kp * error Ki * integral_of_error; estimated_angle estimated_speed * dt; }启动策略void Motor_Startup(void) { // 初始位置检测 Align_Rotor(); // 开环启动 for(int i0; i1000; i) { Set_OpenLoop_Angle(i * 0.01f); HAL_Delay(1); } // 切换到闭环 Switch_To_FOC(); }7. 常见问题排查指南在实际项目中我遇到过各种奇怪的问题这里分享几个典型案例电机抖动不转检查编码器接线是否正确确认Park变换的角度输入是否正确测量三相电流波形是否平衡高速运行时失控检查电流采样是否跟得上高速变化确认PWM频率是否足够高建议10kHz以上观察电源电压是否跌落启动时有异响调整启动时的电流限制检查初始角度检测是否准确尝试不同的启动策略有个特别隐蔽的bug让我记忆犹新由于PCB布局不合理电流采样信号受到PWM干扰导致电机在中速区间总是失控。最后通过给ADC输入加RC滤波和在软件中做移动平均才解决问题。