简介:这是一份面向STM32G4平台的FOC三环电机控制驱动器完整工程源码包,定位为电机控制算法学习与嵌入式实战参考,适合正在研究矢量控制、需要自己手写驱动代码的开发者。内容覆盖位置环(仅P参数跟随)、速度环(按刚性等级调节响应)与电流环(PI参数基于带宽整定),并包含基于零序注入的SVPWM/SPWM实现、零点电角度识别方法,整体控制结构简洁,便于理解三环串联关系与参数调节逻辑。资源还包含HAL库定时器相关文件与ARM数学库,基于STM32G4工程可直接对照学习。压缩包共1338个文件,以C/H源码为主,另有.s汇编、.txt说明、.ioc引脚配置、.uvprojx工程、.a数学库、.hex/.axf可执行输出、PDF文档及MAP/LST映射列表等,整体约39.46MB,目录结构完整。目前已有1881人学习浏览,适合希望通过完整工程源码快速掌握FOC三环调试思路的开发者。
1. 手搓FOC驱动器:三环控制没有想象中那么玄
先把结论放在前面:FOC电机控制算法的三环(位置、速度、电流)没你想象中那么难调,前提是每一环都用对简化规则。我这套手搓FOC驱动器把电流环PI参数直接按带宽公式算,速度环PI参数按刚性等级查表,位置环干脆只留一个P参数,SVPWM放弃了扇区判断,改用等效的零序注入SPWM来实现——整体代码量少了近两成,控制效果却没缩水。整套东西能在主流MCU上跑起来,电流环10kHz、速度环和位置环1kHz调度,适合正在手搓FOC驱动器、想绕开复杂PI整定又不想牺牲三环性能的开发者。下面从三环架构原理讲到每个参数怎么设、坑在哪,附上可直接套用的代码片段和调参验证方法。
2. 三环架构与电流环带宽调参:先让内环立住
2.1 从dq坐标到三环嵌套:每一环到底在算啥
FOC的本质,是通过坐标变换把定子电流分解到转子旋转的dq坐标系上,其中d轴电流对应励磁分量、q轴电流对应力矩分量。这套手搓方案里,三环叠成了一个典型的串级结构:最外层位置环输入位置指令、输出速度参考;中间的转速环跟踪速度参考、输出q轴电流参考;最内层电流环同时控制id和iq,直接算出d/q轴电压。电流环输出经过逆Park变换得到αβ电压,最后丢给调制模块生成PWM占空比。
电流环必须跑得最快,因为它是整个串级控制系统的基础。如果内环带宽不够,外环无论怎么调都只会激发振荡。我实际的调度结构是这样的:
// 10kHz 电流环中断,速度环和位置环按 1kHz 分频调度 void ADC_IRQHandler(void) { // 1. 电流采样与坐标变换 read_currents(&ia, &ib); // 双电阻/三电阻采样 ic = -ia - ib; // 三相电流之和为零 Clarke(ia, ib, &i_alpha, &i_beta); // 静止坐标系 Park(i_alpha, i_beta, theta_elec, &id, &iq); // 旋转坐标系 // 2. 外环低频调度 static uint16_t cnt = 0; if (++cnt >= 10) { // 每10个中断 = 1kHz cnt = 0; theta_now = encoder_get_theta(); // 机械角度 omega_ref = pos_ctrl(theta_ref, theta_now); // 位置环P iq_ref = speed_ctrl(omega_ref, omega_now); // 速度环PI } // 3. 内环:两个PI,电流环 10kHz 执行 vd_out = id_pi_update(id_ref, id, Kp_i, Ki_i); vq_out = iq_pi_update(iq_ref, iq, Kp_i, Ki_i); // 4. 逆Park + 调制 invPark(vd_out, vq_out, theta_elec, &valpha, &vbeta); svpwm_zeroseq(valpha, vbeta, &Ta, &Tb, &Tc); write_compare(Ta, Tb, Tc); // 更新三路PWM比较值 }这个调度方式有几个关键点。首先,电流环固定跑10kHz,不跟外环同步,这样能保证d/q轴电流的跟踪带宽;其次,位置环和速度环合并在同一个分频分支里,先算位置环再算速度环,因为位置环的输出就是速度环的输入,在同一个时间点上顺序执行可以避免半个周期的额外延迟。如果你想把位置环进一步降到500Hz,可以在cnt>=20时再更新一次,只是响应会变肉。
外环频率不是越高越好。位置环、速度环跑得太快有两个坏处:一是占用CPU时间,二是外环输出的噪声——比如编码器量化噪声——会直接进到电流环参考里,激发出电流环带宽内的谐振。1kHz对位置环和速度环来说是足够用的,即使后面要做前馈也一样。另外,ADC触发点我强烈建议放在PWM定时器的中心计数匹配点上,也就是与载波中心对齐。此时三相桥臂中点电压等于母线电压的一半,续流二极管不导通,采样到的电流是每个PWM周期内的平均电流,毛刺最小。这是电流环能不能跑起来的第一道坎。
2.2 电流环PI参数基于带宽:从一阶模型推出来的两个公式
电流环的对象是一相绕组,电气模型可以写成 V = Ri + L·di/dt。在这个方程里,反电动势在d/q轴已经被解耦项抵消,于是被控对象近似为一阶惯性环节。对一阶对象用PI控制器做闭环,带宽ωc和PI参数之间有非常干净的关系:
Kp = L × ωc,Ki = R × ωc
这两个公式在连续域成立,放到离散域只需要把Ki乘以采样周期。我做这一步时直接写成下面的计算:
// 电流环PI参数:基于带宽ωc计算(连续域) // 电机参数:相电感 Ls=120uH,相电阻 Rs=0.12Ω #define Ls 0.00012f #define Rs 0.12f #define TS_I 0.0001f // 电流环采样周期 10kHz float calc_current_bandwidth(float fc_hz) { float wc = 2.0f * 3.14159f * fc_hz; float Kp_i = Ls * wc; // 比例增益,量纲为 V/A float Ki_i = Rs * wc * TS_I; // 离散化后的积分增益 return Kp_i; // Ki_i 通过全局变量带出 }带宽选多少?我这套方案的默认值是2kHz。用小电阻的航模电机测试时1kHz也够;用大电感且开关频率高时,可以拉高到3kHz。但再往上就要小心采样延迟和逆变器死区引入的相位滞后。一个经验限制:电流环带宽和电流环执行频率之间至少要留5倍以上余量,即10kHz时带宽不要超过3.5kHz。换电机时,只要量出L和R,再定一个带宽,参数就出来了,不用盲调。
还要注意抗积分饱和。电流环PI输出v_ref如果超过逆变器能输出的最大电压,积分项应该停止累加,否则会出现积分饱和——指令突变时电流要过冲很久才能恢复。我的处理是:每次累加积分前先看v_ref是否在被限幅的状态,若是就只累加与限幅方向一致的积分量。另外,d轴和q轴的PI参数可以共用一份,因为两轴电气参数相同;但如果你的电机凸极率明显(Ld≠Lq),那就分开算,Kp和Ki各按对应的电感电阻取值。
2.3 基于零序注入的SPWM:不查扇区的SVPWM等效实现
这一节是很多人最容易踩坑的地方。经典SVPWM需要判断电压矢量所在扇区、计算相邻矢量作用时间、再做七段式发波,代码量大且分支多。但其实SVPWM有一个成熟的等效关系:它等于在SPWM的三相正弦调制波上同时注入一个零序分量。最常用的零序分量是三相电压中点偏移:
Vzero = -(Vmax + Vmin) / 2
注入后的三相调制波整体平移,等效于把电压矢量限制在最大的内切圆内,母线电压利用率比纯SPWM提高了约15%,达到与SVPWM相同的结果。我的实现长这样:
// 等效SVPWM:零序注入的SPWM调制 void svpwm_zeroseq(float valpha, float vbeta, float *Ta, float *Tb, float *Tc) { // 逆Clarke:αβ -> 三相正弦波 float Va = valpha; float Vb = -0.5f * valpha + 0.866f * vbeta; float Vc = -0.5f * valpha - 0.866f * vbeta; // 零序注入,等效SVPWM的线性区算法 float Vmax = fmaxf(Va, fmaxf(Vb, Vc)); float Vmin = fminf(Va, fminf(Vb, Vc)); float Vzero = -0.5f * (Vmax + Vmin); Va += Vzero; Vb += Vzero; Vc += Vzero; // 归一化到中心对齐PWM占空比 float Vbus = 24.0f; // 母线电压,单位V *Ta = 0.5f + Va / Vbus; *Tb = 0.5f + Vb / Vbus; *Tc = 0.5f + Vc / Vbus; }这个实现的优点是:没有扇区判断、没有查表、没有分支,代码路径恒定,在任何MCU上都能做到确定性执行。Va、Vb、Vc的幅值范围在注入零序后整体平移,但每个相电压仍然落在[0, Vbus]有效范围内,最终输出的是中心对齐PWM波形。如果你移植过经典SVPWM,会发现两者生成的桥臂导通时间在数值上完全一致。
纯SPWM的线性区只到Vbus/2,等效SVPWM把线性区扩展到Vbus/√3。也就是说,24V母线电压下,纯SPWM相电压峰值只能到12V,而零序注入方案能到13.86V,这个差异在高转速区间是决定性的。要注意的是,这里的Vzero注入公式只适用于线性调制区。一旦进入过调制区,注入后三相波可能会被限幅截断,输出波形会失真。对一般驱动器来说,把最大电压限制在Vbus/√3以内,就能稳定跑在SVPWM线性区,不用过度设计。
3. 速度环刚性等级与位置环P控制:外环调参的简化路径
3.1 速度环刚性等级:从惯量比选参数
速度环的被控对象是机械系统,模型远比电流环复杂,涉及转子惯量、负载惯量、阻尼和摩擦。如果也按带宽公式来,至少要准确知道转动惯量J,而这在现场是拿不到的。所以这套方案做了一个工程化妥协:把速度环PI参数按「刚性等级」查表。等级越高,速度环越硬,抗扰动能力越强,但对机械共振越敏感。
我一般把刚性等级分为1到4档,对应不同的负载惯量比:
| 刚性等级 | 适用场景 | Kp_s | Ki_s |
|---|---|---|---|
| 1 | 大惯量负载(转盘、悬臂) | 0.002 | 0.05 |
| 2 | 中等惯量(常规电机直驱) | 0.005 | 0.12 |
| 3 | 小惯量(轻载高速) | 0.010 | 0.25 |
| 4 | 极低惯量(空心杯电机) | 0.015 | 0.40 |
表里的Kp_s和Ki_s都是以速度误差(rad/s)为输入、输出q轴电流参考值。等级提高时,比例和积分同时加大,相当于把速度环带宽往上推。但刚性等级4只适合机械刚性极好的场合,如果联轴器有弹性、负载惯量又偏大,速度环一硬就容易在机械共振频率上啸叫。
查表不是终点,我还会根据稳态速度波动微调Ki_s。一个非常实用的判断方法:给定一个恒定速度参考,观察q轴电流的波动幅度。如果电流在平均值附近高频抖动,说明积分增益偏大;如果速度稳态偏差持续存在但电流平稳,说明积分增益不够。把Ki_s调到一个刚能消除稳态差又不抖动的值,速度环就算定住了。在线切换刚性等级时注意不要热切换——速度环积分器里的值是按旧参数累积的,直接换新参数会让iq_ref突跳。我一般先把速度环挂到空闲状态再改等级,或者把积分器清零。
// 速度环PI:按刚性等级查表 + 输出限幅 static const float speed_pi_lut[4][2] = { {0.002f, 0.05f}, // 等级1:大惯量 {0.005f, 0.12f}, // 等级2:中等惯量 {0.010f, 0.25f}, // 等级3:小惯量 {0.015f, 0.40f} // 等级4:极低惯量 }; float speed_ctrl(float omega_ref, float omega_now) { int level = get_stiffness_level(); // 由用户配置,默认2 float Kp_s = speed_pi_lut[level-1][0]; float Ki_s = speed_pi_lut[level-1][1]; float err = omega_ref - omega_now; speed_integrator += Ki_s * err; // 离散积分 clamp(&speed_integrator, -IQ_MAX, IQ_MAX); // 抗积分饱和 float iq_ref = Kp_s * err + speed_integrator; clamp(&iq_ref, -IQ_MAX, IQ_MAX); // 输出限幅,保护电流环 return iq_ref; }速度环的输出限幅非常重要。iq_ref的极限值应该等于电流环允许的最大q轴电流。如果这里不限幅,位置环瞬间给一个大速度误差,iq_ref会冲到几十安培,轻则过流保护动作,重则烧功率管。
注意:速度环积分器限幅值务必与电流环最大允许电流一致,否则过流时速度环会「抱住」电流环,故障后恢复时间会变得很长。
3.2 位置环只留P参数:为什么能稳定
位置环在整个串级系统里是最外层,它的被控对象是速度环闭环之后的等效环节。速度环闭环基本是一个一阶惯性环节,位置环再对速度做积分得到位置,所以位置环的对象近似是「一阶惯性 + 积分」的二阶系统。对这种结构,用纯P控制已经能保证稳态无差——因为被控对象本身含有一个积分器,位置误差在稳态时会被速度环的积分作用吃掉。
这就是为什么位置环不需要I项。加上积分反而会引入额外的相位滞后,让系统在指令跟踪时出现超调甚至低频振荡。我只给位置环配一个比例系数Kp_p:
// 位置环:纯P控制,输出限幅到速度环允许范围 float pos_ctrl(float theta_ref, float theta_now) { float err = theta_ref - theta_now; float omega_ref = Kp_p * err; // 速度参考 // 限幅:速度环能跟踪的最大速度 float omega_max = 30.0f; // rad/s,根据电机额定转速定 omega_ref = clamp(omega_ref, -omega_max, omega_max); return omega_ref; }Kp_p的量纲是1/s,物理含义是:每1rad位置误差,速度环获得多少rad/s速度参考。假设Kp_p=20,那么位置偏差1rad时指令速度是20rad/s,偏差0.1rad时就只有2rad/s。这形成了一个自然的减速逼近过程——越接近目标,速度越小,到位冲击也就越小。
Kp_p的整定不需要复杂规则,从速度环带宽的1/5到1/10起步。假设速度环闭环带宽能做到50rad/s,Kp_p先从5试,看点位响应有没有超调,再往上加。Kp_p和速度环带宽的关系是硬约束:速度环带宽只能做到30rad/s时,位置环Kp_p超过12就很容易振荡;速度环带宽提升到60rad/s后,Kp_p可以安全地加到大几十。如果Kp_p加到大十几,电机在目标点附近出现来回微动,那是典型的极限环振荡,先降回一半再说。
3.3 完整调参流程:从电流环到位置环的分步操作
三环串级系统有一条铁律:先调内环,后调外环。我每次都会按这套顺序走:
第一步,电流环闭环。电机堵转或带轻负载,给一个id_ref=1A、iq_ref=0的指令,观察d轴电流是否快速稳定地跟踪。用示波器看电流波形,如果过冲小、跟踪稳,说明Kp_i和Ki_i没问题。这一步不用管速度环和位置环,把它们都旁路掉。
第二步,速度环空载闭环。把电流环合上,给定一个恒定速度参考,比如500rpm,观察转速波动。波动超过±5rpm就降低Ki_s;有高频啸叫就降低Kp_s。空载状态最容易调,因为此时机械系统没有负载扰动的干扰。
第三步,带载验证速度环。接上实际负载,重复第二步。这一步通常会发现,空载调好的刚性等级在带载后速度会偏软,这时把刚性等级提高半档即可,但注意不要跳级,否则容易激起机械共振。
第四步,位置环整定。让驱动器执行点动定位,观察电机从起始位置到目标位置的运动过程。重点是看有没有超调。如果有超调,说明Kp_p偏大,按比例降到原来的一半再试。位置环的调试就这一个动作,比速度环简单得多。
| 步骤 | 操作对象 | 给定信号 | 观察对象 | 调参判据 |
|---|---|---|---|---|
| 1 | 电流环 | id=1A,iq=0 | d轴电流阶跃响应 | 超调<10% |
| 2 | 速度环空载 | 500rpm | 转速波动、q轴电流 | 波动±5rpm内,电流不抖 |
| 3 | 速度环带载 | 500rpm带载 | 转速跌落恢复时间 | 恢复快,不振荡 |
| 4 | 位置环 | 30°点动 | 位置阶跃响应 | 无超调,稳定时间<0.5s |
这套流程做完,三环参数就全定下来了。后面换工况,只需要微调刚性等级档位,不需要重新整定电流环。
4. 零点电角度识别与避坑排查:四个高频翻车现场
4.1 零点电角度识别:开环强拖对齐法的工程细节
FOC里所有坐标变换都依赖转子电角度θe。如果电角度不准,Park变换出来的id/iq就是错的,电流环算出来的电压方向也会偏,轻则效率下降,重则根本转不起来。对带增量编码器但没有绝对零点索引的电机,上电后第一步必须做零点电角度识别。
最简单可靠的方案是开环强拖对齐:把电角度固定在0,向d轴注入电流,让转子自己转到d轴与A相轴线对齐的位置。转子转到位后,编码器当前的机械角度就对应电角度0,之后每次只需要从编码器读数上减去这个offset:
// 零点电角度识别:开环强拖对齐 void zero_electrical_angle_align(void) { theta_elec = 0.0f; // 固定电角度 id_ref = 0.5f * IA_RATED; // 50%额定电流,力矩足够但不过流 iq_ref = 0.0f; // 保持500ms,给机械系统足够时间 for (int i = 0; i < 500; i++) { delay_ms(1); if (encoder_get_speed() < 0.01f) { // 转速已降到接近0,转子锁死在对齐位置 break; } } id_ref = 0.0f; theta_offset = 0.0f - encoder_get_mech_angle(); // 记录零位偏移 }对齐电流的大小要选得合适。太小,转子可能被负载力矩或齿槽转矩挡住,转不到位;太大,对齐速度太快,冲到目标位置后会在零位附近振荡。我一般取额定电流的30%~50%,并且建议对齐完成后保持力矩100ms再退出,让转子真正停稳。编码器读出的零位偏移存到Flash里,下次上电直接载入,不需要每次都重新对齐。如果在某些设备上有机械防转限制,每次强制重新对齐会看到电机顶着限位发出嗡嗡声,这时就改为上电后检测编码器零位是否合法,合法则跳过对齐。
这个方案只适用于转子初始位置任意、但没有大负载力矩的场景。如果系统带抱闸或大静态负载,开环对齐可能拉不动转子,这时就要改用高频注入法或状态观测器来识别初始位置,但那套复杂度就上来了。
4.2 坑一:电流波形毛刺让PI算出来的电压畸形
现象:示波器上看相电流波形,每个PWM周期中心附近都有一堆毛刺,尤其在过零点附近特别明显。电机运行声音发闷,电流环输出的电压波形有明显的高频成分。
原因:ADC采样时刻不对。如果ADC在PWM比较值翻转的瞬间触发采样,正好采到桥臂切换产生的dv/dt噪声,电流毛刺直接进到PI里,PI算出来的电压自然就抖。
解决:把ADC触发点改成PWM定时器的中心计数匹配位置,也就是载波顶点或谷底。此时三相桥臂处于相同的导通状态,续流二极管全部截止,采样值才是真实的相电流平均值。另外,把采样窗口往后挪几十ns,避开开关边沿的振铃。若主控支持多次采样取平均,在每个PWM周期采两次做均值,效果更稳。
注意:ADC采样点务必放在载波中心对齐位置,否则后面所有波形验证都会失真,你会以为是PI参数问题,实际是采样问题。
4.3 坑二:零点对齐时转子没转到位
现象:上电执行零点对齐后,电机启动瞬间力矩方向不对,或者慢速转动时有轻微顿挫。检查编码器零位偏移值,发现每次上电读到的值都不一样。
原因:对齐电流太小,转子被齿槽转矩或负载摩擦挡住,只转到了某个中间位置就停了。这种情况在低速齿槽转矩大的无刷电机上特别常见。
解决:把对齐电流从30%额定提到50%额定,对齐时间从500ms加到1000ms。如果电机在停电状态下可以自由旋转,先用预定位方式给一个旋转磁场让转子转几圈,再进入对齐,消除齿槽转矩带来的位置不确定性。核心原则是让转子以一个确定的方向、确定的位置停下来。
4.4 坑三:零序注入后母线电压利用率上不去
现象:空载高速运行时,转速最高只能到额定值的一半,再往上加电压就报过流或者失控。用示波器看相电压波形,发现三相调制波的顶部和底部被削平了。
原因:零序注入之后,如果直接对注入后的三相电压做限幅再输出,限幅会截断零序分量,等效调制波回到纯SPWM的状态,母线利用率白白损失15%。这个坑的本质是限幅和注入的顺序搞反了。
解决:先根据SVPWM线性区范围限幅,再注入零序;或者把限幅值设成Vbus/√3而不是Vbus/2。
// 正确顺序:先限幅再注入零序 float Vmax_raw = fmaxf(fmaxf(Va, Vb), Vc); float Vmin_raw = fminf(fminf(Va, Vb), Vc); float Vrange = Vmax_raw - Vmin_raw; if (Vrange > Vbus / 1.732f) { // 进入过调制区,按比例压缩,保持波形形状 float scale = (Vbus / 1.732f) / Vrange; Va *= scale; Vb *= scale; Vc *= scale; } // 然后再注入零序这个改法能让高速区的电压输出平滑进入饱和,而不是突然削顶。如果还需要更高的电压利用率,那就得再上过调制算法,但那是另一个复杂度级别的事。
4.5 坑四:位置环P过大引起极限环振荡
现象:位置模式点动定位完成后,电机没有安静停下来,而是围绕目标位置以几赫兹的频率来回微动,伴随轻微的嗡嗡声。位置误差在±1个编码器计数附近反复跳变。
原因:位置环P太大,位置误差一点点就会产生很大的速度参考,速度环跟踪这个参考时又产生过冲,位置过冲后再反向修正,如此反复形成极限环。在机械间隙和编码器量化误差存在的情况下,纯P位置环特别容易出这个问题。
解决:把Kp_p降到原来的一半,同时给速度环目标值加一个死区:当位置误差小于编码器的几个LSB时,直接令速度参考为0。死区能斩断极限环的振荡条件,电机停得更安静。
// 位置环死区:误差小于3个编码器计数时输出0 int32_t err_lsb = theta_ref_lsb - theta_now_lsb; if (abs(err_lsb) < 3) { omega_ref = 0.0f; } else { omega_ref = Kp_p * (float)err_lsb / ENCODER_CPR; }这个坑的重点在于,位置环参数不是越大越好。Kp_p过大的另一个副作用是,在机构有弹性形变时,位置环会让机构一直顶着弹性负载,电机发热明显增加。
5. 用示波器验证三环响应:判断参数是否到位的三组波形
纸上调参完了,最终还是要靠波形说话。我的习惯是接好三个量:相电流(电流探头)、编码器速度(DAC输出或CAN回传)、位置误差(DAC输出),然后跑三组测试。
第一组测电流环:手动阶跃改变id_ref,从0跳到1A,观察d轴电流跟踪。2kHz带宽对应的典型上升时间大约0.2ms,也就是2个电流环周期。如果电流像过阻尼一样慢吞吞爬,说明带宽被采样延迟拖低了;如果振荡两个周期以上才稳定,Kp_i可以适当减一点。第二组测速度环:给定速度参考从500rpm阶跃到1000rpm,观察速度波形。好的响应是快速上升、无超调或极小超调,稳态误差在±3rpm以内。第三组测位置环:执行一次30°点动,观察位置误差波形。理想曲线是单调归零,没有反向过冲,稳定时间在0.3~0.5s。
| 测试 | 阶跃量 | 理想波形特征 | 参数动作 |
|---|---|---|---|
| 电流环 | id 0→1A | 上升约0.2ms,无振荡 | Kp_i过大→减小 |
| 速度环 | 500→1000rpm | 无超调,稳态±3rpm | 刚性等级过高→降档 |
| 位置环 | 30°点动 | 单调归零,无反向 | Kp_p过大→减半 |
三组波形是连动的,外环有任何异常先怀疑内环没调好,不要一上来就动外环参数。我有一次给某设备的转轴模组调参,空载时三环都看着很好,一接上负载速度环就嗡嗡响,排查半天发现是电流环带宽设太高,负载扰动通过电流环放大到了机械共振点上。从那以后我每次调完参数都强制走一遍「空载—半载—满载」的三段验证流程,电流环带宽、速度环刚性等级、位置环P参数各记一次档,换工况也不再从头来。希望帮到你。
本文还有配套的精品资源,点击获取