在无刷直流电机仿真这条路上折腾了小半年,我发现自己最上头的画面,不是板子第一次转起来的瞬间,而是仿真模型里那条转速曲线稳稳咬住设定值的那一刻。没错,就是仿真。不少朋友不理解,放着现成的电机库不用,偏要自己手搓一套无刷直流电机仿真模型,又是坐标变换又是SVPWM又是PI整定,这不是跟自己过不去吗?但我的体会恰恰是,这段“手搓”的经历,比直接拖现成模型学到的东西多了一个量级。
这篇就把我从电机方程一路搭到转速闭环的完整过程记录下来,电机的参数、每个环节的原理、整定过程和踩过的坑都尽量交代清楚,给那些也想自己搭一套无刷直流电机仿真、又不想用黑盒模型的朋友一条可以直接上手的路。文章里所有代码和参数都是我实测跑过的最小可用版本,不追求花哨,只求你能跟着搭出来。
1. 为什么放着现成工具不用,非要自己搭电机模型
1.1 现成模型像罐头,省事但出事不好查
很多初学者起步都会选现成方案:在Simulink环境里找现成的电机模块,配一个封装好的FOC库,把母线电压、极对数、相电阻填进去,双击一跑,三相电流、转速、电磁转矩曲线就出来了。风平浪静,什么都好看。
可一旦你要做真项目,就会发现“能跑”和“能用”之间隔着好几条街。现成模型里的参数往往是理想化的,你手上那颗实际电机的磁链、电感、反电动势系数具体是多少,库里根本没有地方告诉你。更麻烦的是,如果转速响应和电流波形跟预期对不上,你没法判断问题出在模型本身、控制器参数还是坐标变换里面。黑盒模型挡在中间,定位问题跟隔着一层雾似的。
自己手搓就不一样了。每个状态量怎么算出来的、每个环节的带宽多少、哪个滤波引入的相位延迟有多大,你心里一清二楚。转速闭环一振荡,我基本能在十分钟内定位到是哪一环带宽没拉开,哪一级滤波器滞后太多。这种判断力,靠拖现成模型是练不出来的。
1.2 拆开看,一套手搓模型就是六块积木
搭模型的时候,我把整个系统拆成六块:电机本体模型、逆变器桥、PWM调制(SVPWM)、电流采样/滤波、电流环、转速环。
电机本体是“被控对象”,逆变器和PWM是“执行器”,电流环和转速环是“大脑”。这个分层特别重要——每一块都能单独验证,拼起来之后一旦出问题,能快速隔离到具体模块。
我的开发顺序是这样的:先用Python脚本做快速验证,把电机方程和PI控制器写成函数跑通,再接上SVPWM和逆变器逻辑,最后才把整套东西搬进Simulink环境搭最终版。为什么这么做?脚本调参方便,改一个参数重跑一遍只要几秒钟,而且报错信息比图形界面里拖线直观太多。
2. 电机本体建模:没搞清楚反电动势之前别动手
2.1 BLDC的反电动势,不能随手当正弦波处理
无刷直流电机从结构上说是永磁同步电机的一种,但它的驱动方式有讲究。BLDC的反电动势波形是梯形波,普通PMSM是正弦波。如果你不管三七二十一按正弦波建模,反电动势的谐波部分会直接体现成转矩脉动,电流环设计也会偏乐观。
不过实际项目里,在梯形波反电动势条件下用FOC算法也大有人在。原因在于你控制的是电流的基波平均值,谐波部分会被电流环带宽压制一部分,剩下的变成转矩脉动。我在模型里仍然按正弦反电动势近似,但对转速环这种慢动态来说完全够用,这也是大多数仿真教程的通用做法。
2.2 三相方程和坐标变换:三个变量变两个
最直接的三相静止坐标系方程长这样,每一相都包括电阻压降、电感压降和反电动势三项:
u_a = Rs * i_a + Ls * di_a/dt + e_a u_b = Rs * i_b + Ls * di_b/dt + e_b u_c = Rs * i_c + Ls * di_c/dt + e_c三相量之间有120度相位差,三个PI控制器如果直接控制交流量,静态误差很难消除。所以要干两件事:
- Clarke变换:把三相静止的ia、ib、ic投影到两相静止坐标αβ
- Park变换:再把αβ投影到跟转子同步旋转的dq轴
这两步合在一起,本质上是把三个纠缠在一起的交流量,变成两个在稳态下恒定的直流量。在dq坐标系里,电流是直流,用PI就能做到无静差跟踪。这就是整个FOC的精髓。
坐标变换之后的dq轴电压方程简洁很多:
u_d = Rs * i_d + Ld * di_d/dt - ωe * Lq * i_q u_q = Rs * i_q + Lq * di_q/dt + ωe * Ld * i_d + ωe * λpm方程里ωe是电角速度(等于极对数乘以机械角速度),λpm是转子磁链。反电动势项出现在u_q方程的ωe * λpm上,这就是为什么转速越高,你需要的端电压越高。
电磁转矩公式也顺手列出来。对于表贴式电机Ld等于Lq,所以:
Te = 1.5 * Pp * λpm * i_q = Kt * i_qKt是扭矩常数,对表贴式BLDC来说就是1.5乘以极对数再乘以磁链。这是个特别重要的换算关系,后面调转速环、算负载转矩全靠它。
2.3 我用的电机参数和一个能跑的核心代码
为了后面所有计算都能对上,我先给出一组我仿真时用的电机参数,之后所有PI整定、带宽计算都基于这组数:
| 参数 | 符号 | 数值 | 单位 |
|---|---|---|---|
| 母线电压 | Udc | 48 | V |
| 极对数 | Pp | 4 | |
| 定子电阻 | Rs | 0.20 | Ω |
| d/q轴电感 | Ld=Lq | 0.15 | mH |
| 转子磁链 | λpm | 0.02 | V·s/rad |
| 转动惯量 | J | 8e-5 | kg·m² |
| 阻尼系数 | B | 5e-5 | N·m·s/rad |
| 额定转速 | n_N | 3000 | rpm |
扭矩常数Kt算出来是0.12 N·m/A。额定转速3000rpm对应机械角速度约314 rad/s,电角速度就是约1256 rad/s。所以额定点附近的反电动势压降大约在25V左右,加上电阻压降,48V母线还是撑得住的。
电机模型的核心更新代码,我用Python写了一个最简版本:
import numpy as np # 电机参数 Rs = 0.20 Ld = 0.15e-3 Lq = 0.15e-3 flux = 0.02 Pp = 4 J = 8e-5 B = 5e-5 def motor_step(x, udq, TL, dt): # x = [id, iq, omega_m] # udq = [ud, uq] id_, iq_, omega_m = x omega_e = Pp * omega_m # dq轴电流微分 did_dt = (udq[0] - Rs * id_ + omega_e * Lq * iq_) / Ld diq_dt = (udq[1] - Rs * iq_ - omega_e * Ld * id_ - omega_e * flux) / Lq # 电磁转矩和机械微分 Te = 1.5 * Pp * flux * iq_ dome_dt = (Te - TL - B * omega_m) / J # 显式欧拉积分 new_id = id_ + dt * did_dt new_iq = iq_ + dt * diq_dt new_omega = omega_m + dt * dome_dt return np.array([new_id, new_iq, new_omega])这段代码里电机是机械和电气方程联立的。为了让读者能直接复现,我用的是最简单的显式欧拉积分。欧拉法在步长足够小的情况下可以稳定跑,但对步长很敏感,后面我会专门讲步长选择的问题。实际做长时间仿真时我换成的是四阶龙格库塔(ode4),同样的参数,波形平滑度和稳定性都好很多。
3. 执行器也得搭得像样:逆变器、SVPWM和时序
3.1 逆变器模型:理想开关加死区就够用
做系统级仿真时,完全没必要把MOSFET的开通关断过程、米勒平台、体二极管反向恢复这些细节全建模。那些属于器件级仿真,留给专门做功率半导体的人去纠结。
我的逆变器模型就是三个半桥的理想开关,外加一个死区时间参数。写代码的时候,逻辑就是根据PWM比较值判断每相上管还是下管导通,然后把对应的相电压赋给电机模型。要模拟死区就在换相瞬间插入一个两个管子都关断的小时间段。
系统级模型的重点是控制环,把开关瞬态省掉之后,仿真速度能快一个数量级。如果后面要做损耗分析、EMI评估,再单独把开关模型加进去不迟。
3.2 SVPWM的扇区判断和作用时间
SVPWM的全称是空间矢量脉宽调制。它的核心思想不是分别调制三相,而是把三相合成一个旋转电压矢量,然后用六个非零基本电压矢量和两个零矢量去逼近它。
实现时最常用的就是七段式发波,每个PWM周期内插入两段零矢量,开关次数均匀,谐波畸变最小。关键步骤只有两步:
第一步,判断当前目标电压矢量落在哪个扇区。已知目标矢量在αβ轴的分量Uα和Uβ,可以通过三个中间量的正负组合判断扇区:
A = Uβ B = (sqrt(3)/2) * Uα - (1/2) * Uβ C = -(sqrt(3)/2) * Uα - (1/2) * Uβ三个量的正负组合对应六个扇区。常见的判断逻辑是:
| 条件组合 | 扇区号 |
|---|---|
| A>0, B>0, C≤0 | I |
| A>0, B≤0, C≤0 | II |
| A≤0, B≤0, C≤0 | III |
| A≤0, B≤0, C>0 | IV |
| A≤0, B>0, C>0 | V |
| A>0, B>0, C>0 | VI |
第二步,计算相邻两个基本矢量的作用时间。设Uref是目标电压矢量幅值,Ts是PWM周期,θ是矢量在扇区内的角度,Udc是母线电压,相邻两个矢量的作用时间分别用下面两个表达式计算:
T1 = sqrt(3) * Uref * Ts * sin(π/3 - θ) / Udc T2 = sqrt(3) * Uref * Ts * sin(θ) / Udc剩下的时间分给两个零矢量,而且零矢量要对称分配在周期头和周期尾,这样输出电压波形是对称的PWM。这就是七段式SVPWM的标准套路。
3.3 仿真步长、PWM周期和控制周期,三者千万别混为一谈
这是很多新手容易绕晕的地方。我用的配置是:
| 项目 | 数值 |
|---|---|
| 仿真步长 | 1 μs |
| PWM周期 | 50 μs(20 kHz) |
| 电流环更新周期 | 50 μs |
| 转速环更新周期 | 500 μs |
这样选的逻辑很直接。仿真步长是数值积分的采样间隔,必须比PWM周期小一个数量级,否则开关切换时刻落在积分间隔之外,电流波形会出现锯齿状误差。PWM周期由硬件决定,20kHz是电机驱动器的主流频率,兼顾开关损耗和电流纹波。电流环跟着PWM周期走,在一个周期内更新一次PI输出,这是单片机能轻松承受的频率。转速环比电流环慢10倍,因为机械时间常数远大于电气时间常数,没必要跑那么快。
有些人在仿真里把仿真步长设成跟PWM周期一样,结果电流波形全是高频毛刺,然后怀疑是算法问题。其实不是,是时间尺度的分辨率不够。我一开始也踩过,后面详细说。
4. 调PI像调火锅底料,光给结论不给过程是耍流氓
4.1 电流环参数:用零极点对消做一次计算
电流环的被控对象是电机绕组,它的传递函数近似是一个一阶惯性环节:
G(s) = 1 / (Ls + Rs)PI控制器的传递函数是Kp加Ki/s。整定的经典思路是让PI的零点去对消电机的极点,这样闭环就变成一个纯一阶系统,带宽由PI比例系数决定。推导下来得到两条工程公式:
Kp = L * ωc Ki = Rs * ωc其中ωc是期望的电流环截止角频率。截止频率通常取开关频率的10分之1到20分之1。我的开关频率是20kHz,取截止频率1.5kHz,对应ωc约9425 rad/s。
代入我的电机参数:
L = 0.15e-3,Rs = 0.20 Kp = 0.15e-3 * 9425 ≈ 1.41 V/A Ki = 0.20 * 9425 ≈ 1885 V/(A·s)这两个数就是电流环PI的起点。跑仿真之后电流波形如果抖动,先把Kp往下调而不是急着调Ki。电流环是内环,它稳不住,转速环做得再漂亮也没用。
4.2 转速环参数:先Kp后Ki,单位换算是重灾区
转速环的被控对象比电流环简单,可以近似成一个纯积分环节加上扭矩常数Kt:
G(s) = Kt / (Js)期望的转速环带宽通常取电流环带宽的10分之1到10分之1,我取150Hz,对应角频率约942 rad/s。忽略阻尼项,Kp的估算公式是:
Kp = J * ωc_ω / Kt Kp = 8e-5 * 942 / 0.12 ≈ 0.63 A/(rad/s)注意这个单位。速度环的输出是q轴电流指令,转速误差单位是rad/s,所以Kp单位是“安培/每弧度每秒”。如果你把转速看成rpm,数值上就要换算,差了60/2π还不自知的话,调出来的参数会让人崩溃。我后来把转速环里所有量统一用SI单位:转速一律用rad/s,力矩一律用N·m,电流一律用A,避免反复换算出错。
Ki先不急着给,我的习惯是只加Kp空载启动,看阶跃响应。如果转速收敛不到设定值,说明积分作用不够,再慢慢加Ki。加Ki时注意设置积分限幅,我最终用的速度环参数是Kp=0.65,Ki=55,输出限幅±12A,积分限幅±6A。这两个限幅值看着不起眼,作用却很大,后面讲积分饱和时会专门说。
4.3 空载启动加负载突变,观察曲线咬住设定值
我最常用的调试场景是这样:0时刻给阶跃目标转速3000rpm,空载启动;0.3秒的时候突然加上1.0 N·m负载转矩,看转速跌落后能不能拉回来;0.5秒时再撤掉负载,看会不会过冲。
整定好之后的仿真行为大致是这样的:启动阶段电流先顶到限幅值12A,电磁转矩约1.44N·m,加速度稳定,转速线性上升。到接近3000rpm时电流回落,转速无超调地贴在目标值上。0.3秒突加负载后转速跌落到约2940rpm,然后转速环积分作用把它拉回3000rpm,恢复时间约40毫秒,稳态误差在0.5rpm以内。
那种曲线稳稳咬住设定值的感觉,确实像标题说的,跟吃火锅配冰阔落一样畅快。但为了这个结果,我整整调了两天,而且大部分时间浪费在错误方向上了。
5. 转速咬上去之前,我踩过的几个大坑
5.1 极对数问题:电角度和机械角度互相背叛
第一个让我怀疑人生的坑来自极对数。电机机械转一圈,电角度实际转了P圈。我在坐标变换里用Park变换需要电角度,但在机械方程里积分出来的是机械角度。如果忘了乘极对数,转速反馈信号全乱,PI控制器会输出一个高频振荡的电流指令。
这个错误非常隐蔽,因为模型还是能跑出转速曲线,只是波形带明显抖动。我当时的排查方式是打印udq和idq波形,发现id和iq在稳态下根本不恒定,而是叠加了一个高次波动,这才意识到角度信号频率不对。排查过程花了整整一晚上,最后不过是把Pp乘了进去。
5.2 反电动势系数单位换算:搜到的公式和模型参数对不上
第二个坑是反电动势系数的单位。网上资料里有人给V/(krpm),有人给V/(rad/s),还有人给V·s/rad。这三个单位之间差着系数,直接套用会差好几个数量级。
我在模型里用的磁链λpm单位是V·s/rad,反电动势项就是电角速度乘以磁链。但如果你拿电机的实测反电动势系数(比如标称多少V每千转每分钟)去反推磁链,一定要先统一单位。3000rpm对应的机械角速度是314rad/s,电角速度是1256rad/s,漏掉任何一个换算系数都会让模型里的反电动势跟实机差36倍。
我的建议是,模型内部所有物理量统一使用SI标准单位,外部标定数据进入模型前先做一次单位转换,并且把转换过程写在注释里。这样哪怕后面忘了,回头看代码也能想起来。
5.3 仿真步长太大,电流波形全是毛刺
第三个坑最为迷惑。最初我把仿真步长设成10μs,PWM周期50μs,心想这不比PWM快五倍了吗,应该够了吧。结果跑出来的电流波形上叠加了一层明显的锯齿状毛刺,转速环在稳定点附近小幅振荡。
排查了很久,最后把步长砍到1μs,毛刺消失,波形变得非常干净。原因是电感电流的动态速度太快了,理想化处理下电流的上升速率接近电压除以电感,量级能到几十万安培每秒,显式欧拉法在10μs步长下数值误差积累得太严重。
所以保守建议是:仿真步长至少要比PWM周期小一个数量级。如果模型里开关时刻是离散的,还要保证步长跟PWM周期是整数倍关系,否则不同步的周期性误差会变成低频干扰。
5.4 积分饱和带来的启动冲击和转速超调
最后一个坑也是最“经典”的:积分饱和。启动时电流长时间顶在12A限幅值,转速环误差一直很大,积分项疯狂累积,等转速真正到达3000rpm时,积分项已经把输出推到极其夸张的数值。转速是到了,积分器却还在持续输出,等于让电机继续加速,最终转速冲到3150rpm才被反向拉回来,形成一次明显的过冲。
解决方案是给速度环PI加积分限幅,并且当输出达到限幅时停止积分累加,这就叫抗积分饱和。我用的积分限幅是±6A,跟输出限幅±12A错开一半。加了之后启动过冲消失,转速曲线像用尺子画的一样服帖地贴上设定值。
6. 模型跑顺之后,离实机上电还有哪些功课
6.1 仿真模型里故意“忽略”的那些东西
仿真通过很容易让人过度自信,但我要提醒你:模型里忽略了不少东西。我列一下我仿真时根本不关心、但实机绕不开的量:
- MOS管导通压降和死区效应导致的电压偏差
- 母线电压波动和电容纹波
- 铁损、永磁体温度变化带来的磁链漂移
- 摩擦转矩的非线性(静摩擦和粘滞摩擦不是常数)
- 编码器零位误差和霍尔换相误差
这些在仿真模型里都被我当成理想条件,但实机上它们一个都不会缺席。所以仿真结果的绝对数值不要直接当实机性能预测,它更多是用来验证控制方案、参数量级和逻辑正确性的。
6.2 上板子前,参数至少要重新估一遍
我自己的流程是:在用仿真参数上硬件之前,先按实机重新测一遍关键参数。相电阻用LCR表或者直流压降法测量;相电感在锁住转子的情况下测;反电动势系数用对拖法,转速和开路电压做一个比值;转动惯量靠加减速实验或自由停车曲线辨识。
还有一个特别容易被忽略的环节:电角度零点标定。仿真模型里Park变换的角度是理想对齐的,但实机编码器零点跟电机磁极位置存在一个固定偏移。如果这个偏移不标定,id会混入非零值,电流环会产生严重的直轴电流浪费,降低效率。
6.3 最小可行验证路线:别一上来就闭环
最后分享一个我呕心沥血总结出来的上电顺序。不要一上来就切转速闭环跑阶跃,那样一出问题就是大烟花。先做一个开环电压斜坡测试,让电机缓慢转动,确认驱动和旋变/编码器逻辑正常;然后单独跑电流环,给定一个小电流指令,验证id和iq的解耦;再切转速环,给定低速目标,逐步往上加。
每次切换只改一个环节,确认稳定后再进行下一步。这个顺序帮我排除了不少低级问题,比如相位接反、编码器方向反了、PWM极性错误等等。仿真里没人接反线,实机可能犯的错,仿真根本发现不了。
这也是为什么我始终建议先把仿真做到“每个模块都能自己解释清楚”,再上实机。仿真帮不了你排除接线错误,但它能帮你排除算法错误。等算法层面的坑都被仿真消化掉,实机上电时剩下来的问题就只有物理世界自己的胡闹了。