简介:面向学习最优化与模型预测控制的研究生、工程师,这份 MATLAB MPCC 经典代码包围绕互补约束优化问题的建模与求解展开,覆盖目标函数与互补约束表达、fmincon 等工具箱调用、自定义迭代算法、数值稳定性处理以及仿真绘图等环节,适合具备一定 MATLAB 基础、希望深入掌握 MPCC 建模与算法实现的中高级读者。压缩包共 94 个文件,约 14.99MB,以 28 个 .m 脚本、20 个 .cpp 与 20 个 .h 源码为核心,辅以 jpg 图示、json 配置、md 说明、mat 数据和 sh 脚本,便于对照 MATLAB 与 C++ 实现理解算法结构。已有 585 人学习下载。资源中还包含状态增广、车辆边界处理、模型参数管理、离散线性化、MPC 矩阵生成、预测结果绘图及多种求解器接口等模块,可帮助读者把互补约束模型落到代码层面,用于课程学习、科研复现、车辆控制与工程原型验证,并借鉴其目录组织、接口抽象和调试思路。
1. 电流环换成预测以后,最先变的不是精度而是「敢不敢给阶跃」
很多人第一次打开这份 matlab mpcc 经典代码,第一反应是“这不就是把 PI 换成一个 for 循环吗”。真跑起来才会发现差别在时序上:PI 电流环是拿上一拍的误差去修下一拍的占空比,天生慢半拍;MPCC 是拿当前电流和电机方程,把 8 个基本电压矢量挨个代进去,算清楚每个矢量作用一个周期后电流会落到哪,再挑一个离给定最近的直接打到开关管上。它把调制环节整个吞掉了,代价是开关频率不固定、纹波偏大。这份代码把三相两电平逆变器、表贴式永磁同步电机、100 微秒控制周期、代价函数加权和一拍延时补偿串成了一条能跑的链路,适合刚接触有限集模型预测电流控制、想要一份干净基线去改自己拓扑的开发者。
2. 工程骨架:四层结构里,预测层为什么把调制吞掉了
2.1 目录分层与一次控制周期的数据流
先把结构说清楚,不然拿到一堆 .m 文件会不知道从哪下手。这类 MPCC 工程常见的组织方式是按“给定层 / 预测层 / 执行层 / 对象层”切,而不是按“算法 / 工具”切。给定层给你 id_ref、iq_ref,来源可能是转速外环 PI,也可能是一个手写的阶跃序列;预测层就是 MPCC 本体,输入三相电流、电角度、电角速度和给定值,输出一整组开关状态;执行层是逆变器桥臂和死区处理;对象层是电机的连续域模型。四层之间的数据流是单向的:采样 → 变换 → 预测 → 寻优 → 输出,没有回环,这一点对后面做代码生成很关键。
和 FOC 最大的结构差异在这里:FOC 的电流环输出的是电压给定,后面必须跟一个 SVPWM 把电压翻译成开关时间;MPCC 直接输出开关状态,调制层被合并进了代价函数。所以你在工程里找不到“电流环 PI 参数”这种东西,取而代之的是一张电压矢量表和一组权重。我第一次把 PI 工程改成 MPCC 时,花了半天在找一个不存在的模块,血泪经验就是:先接受“没有调制”这个设定,再看代码。
目录我一般会这么组织,大致如下:
mpcc/ ├── run_sim.m % 一键入口,纯脚本闭环 ├── params/ │ └── mpcc_params.m % 电机、逆变器、控制器参数集中管理 ├── controller/ │ ├── mpcc_step.m % 有限集 MPCC 单步求解 │ ├── sabc2dq.m % 开关状态 -> dq 电压 │ ├── pmsm_predict.m % 前向欧拉电流预测 │ └── mpcc_deadbeat.m % 无差拍 + SVPWM 变体 ├── plant/ │ ├── pmsm_model.m % 连续域 dq 模型 │ └── inverter_model.m % 桥臂 + 死区 └── utils/ ├── i_dq2abc.m └── thd_calc.m这套分法的好处是换拓扑时你只动 controller 和 params 两个目录。矢量表换成三电平的 27 个组合,plant 一行都不用改。我在别的项目里见过把所有东西塞进一个 .m 文件的写法,改一版拓扑要全局搜索替换三十多处,翻车概率极高。
2.2 一个 100 微秒周期里的时序预算
控制周期 Ts 是整个工程的节拍器。经典配置是 Ts = 100 us,也就是 10 kHz 中断,采样点落在 PWM 计数器的下溢处,保证采到的是开关周期中点的电流。这个 100 us 里要干完四件事:ADC 采样与滤波、Clarke/Park 变换、8 个矢量的两步预测、代价比较与寄存器装载。放到定点控制器上,我一般按下面这个量级估预算,心里有个数再决定要不要降 Ts:
| 环节 | 典型耗时(100 MHz 定点) | 说明 |
|---|---|---|
| ADC 采样 + 一阶滤波 | 2 us | 两相或三相电流 + 母线电压 |
| Clarke / Park | 1.5 us | 三角函数走查表,别直接调库 |
| 8 矢量双步预测 | 12 us | 每矢量 2 次预测,共 16 次乘加 |
| 代价比较与输出 | 2 us | 含死区装载、最小脉宽检查 |
| 合计 | 17.5 us | 约占 Ts 的 17.5% |
留 30% 余量是底线。你要是把 Ts 压到 50 us,光预测部分就吃掉一半时间,再想加观测器、加弱磁、加多步预测就没地方放了。另一个隐性开销是三角函数:实机上如果每个矢量都重新算一次 sin/cos,耗时直接翻倍,正确做法是一个周期只算一次 theta 的正余弦,或者干脆用定点查表加线性插值。仿真里看不出这个差别,上板子就是另一回事。
2.3 参数集中管理:别让 0.0042 散落在二十个文件里
参数表的写法看着琐碎,其实是整个工程能不能被人接手的分水岭。我见过电感值在五个文件里各写一遍、改了一个忘了另四个的工程,波形怎么调都对不上。正确姿势是一个结构体走天下,所有函数都接这个结构体,不允许出现裸数字。
function p = mpcc_params() %MPCC_PARAMS 集中管理电机、逆变器与预测控制器参数 % 全部使用国际单位制,换拓扑或换电机只改这一个文件 p.Udc = 311; % 直流母线电压 (V),单相 220 V 整流后的典型值 p.Rs = 0.55; % 定子相电阻 (ohm) p.Ld = 4.2e-3; % d 轴电感 (H) p.Lq = 4.2e-3; % q 轴电感 (H),表贴式电机取与 Ld 相同 p.psi_f = 0.092; % 永磁体磁链 (Wb) p.pole_pairs = 4; % 极对数 p.Ts = 100e-6; % 控制周期 (s),对应 10 kHz p.I_max = 8; % 过流惩罚门限 (A) p.lambda_sw = 0.05; % 开关切换权重,单位 A^2 p.lambda_lim = 10; % 过流惩罚权重,无量纲 end这里每个量的量纲必须统一,否则代价函数里几项根本没法相加。lambda_sw = 0.05的含义是“一次开关动作等价于 0.22 A 的电流误差”,因为它的单位是 A²,开根号就是折算电流。这个换算方法后面第 3 章还会用到,是调权重最实用的抓手。Udc从 311 V 改成 540 V 时,矢量幅值跟着变,但代价函数的权重不用动,因为它们都是电流量纲,这一点设计对了能省很多事。
3. 核心算法拆解:预测模型、代价函数与两步延时补偿
3.1 电流预测模型:从 dq 方程到前向欧拉
预测模型是整个算法的心脏,写错了后面全白搭。表贴式永磁同步电机在 dq 轴上的电压方程是:
ud = Rs*id + Ld*did/dt - we*Lq*iq uq = Rs*iq + Lq*diq/dt + we*Ld*id + we*psi_f把它挪成导数形式,用前向欧拉离散一个 Ts,就得到代码里那两行。这里有个选择要做:前向欧拉、双线性、还是矩阵指数精确离散。Ts = 100 us、L/R 时间常数约 7.6 ms 的场合,Ts 只有时间常数的 1.3%,前向欧拉的误差可以忽略,代码也最短。但如果你的电机电感只有几百微亨,或者硬把 Ts 拉到 200 us 以上,前向欧拉会开始发散,那时候就得上精确离散。
function [id_next, iq_next] = pmsm_predict(id, iq, ud, uq, we, p) %PMSM_PREDICT 前向欧拉离散的 dq 轴电流预测 % 输入均为标量,角度用弧度,we 为电角速度 % 注意:we 在推导中应取区间平均值,这里简化为当前采样值 id_next = id + p.Ts/p.Ld * ( ud - p.Rs*id + we*p.Lq*iq ); iq_next = iq + p.Ts/p.Lq * ( uq - p.Rs*iq - we*p.Ld*id - we*p.psi_f ); end参数说明:id、iq是预测起点电流(A),ud、uq是候选矢量在当前角度下的 dq 分量(V),we是电角速度(rad/s),p.Ts是控制周期。两个交叉耦合项we*Lq*iq和we*Ld*id千万不能因为“看起来小”就删掉:1500 rpm、4 极对时 we = 628 rad/s,耦合项幅值约 13 V,相比母线电压的三分之一(约 104 V)已经是 12%,删了以后高速段电流会系统性偏移,而且偏移量随转速线性增长,非常典型。
反电动势项we*psi_f同理,它是 q 轴方程里最大的一项,在这个工作点上约 58 V。很多人第一次写预测模型时只写了uq = Rs*iq + Lq*diq/dt,仿真里电机“能转”,但稳态 iq 永远追不上给定,差的那一截就是被漏掉的反电动势。
3.2 八个基本矢量的枚举与代价函数加权
两电平逆变器只有 8 种合法开关组合,六路有效矢量加两路零矢量,穷举一遍的代价比任何优化算法都低。矢量表直接写成常量矩阵,别在循环里现算:
function [ud, uq] = sabc2dq(Sabc, theta, Udc) %SABC2DQ 开关状态 -> 三相相电压 -> alpha/beta -> dq % Sabc 为 1x3,取值 0/1;theta 为电角度 (rad) Sa = Sabc(1); Sb = Sabc(2); Sc = Sabc(3); ua = Udc/3 * ( 2*Sa - Sb - Sc); ub = Udc/3 * (-Sa + 2*Sb - Sc); uc = Udc/3 * (-Sa - Sb + 2*Sc); alpha = (2/3) * ( ua - 0.5*ub - 0.5*uc ); beta = (2/3) * ( sqrt(3)/2 * (ub - uc) ); ud = cos(theta)*alpha + sin(theta)*beta; uq = -sin(theta)*alpha + cos(theta)*beta; end这里用的是等幅值 Clarke 变换,系数 2/3,配合等幅值 Park。如果你的工程里用了等功率变换(系数 sqrt(2/3)),代价函数里的电流平方项量纲会差一个常数倍,权重得整体重调,这是换库时最常见的隐性坑。相电压公式Udc/3 * (2Sa - Sb - Sc)来自三相三线制中点浮空的前提,接成四线制或带中点钳位的拓扑就不能直接套。
代价函数由三项组成:电流跟踪误差、开关切换次数、过流罚项。
g_track = (id_ref - id2)^2 + (iq_ref - iq2)^2; % 单位 A^2 g_sw = sum(abs(Sabc - Sabc_prev)); % 无量纲,取值 0~3 g_lim = max(0, sqrt(id2^2 + iq2^2) - p.I_max)^2; g = g_track + p.lambda_sw*g_sw + p.lambda_lim*g_lim;g_track用平方而不是绝对值,是为了让代价函数对误差可导、在最小值附近更平滑,计算上也省掉一次开方。g_sw统计的是相对上一拍实际施加矢量的切换管数,一次换一相记 1,三相同换记 3。g_lim是软约束,超过I_max才开始罚,罚项开平方再平方,等价于超多少罚多少的平方,和跟踪项量纲一致。硬限幅当然也要有,但那是保护逻辑,不该混进优化目标里,否则一旦触发限幅,代价函数的排序会突然失去意义。
权重的调法我一般这么走:先把lambda_lim设成一个明显偏大的值(比如 100),保证过流永远排在最前面;再固定lambda_sw = 0,看纯跟踪效果,确认模型没问题;最后从 0.01 往上加,加到相电流 THD 明显下降但 iq 阶跃响应开始变钝为止,取中间值。
3.3 两步预测:把拍延时这把刀提前接住
这是新手最容易漏、漏了必然翻车的一步。真实系统里,k 时刻采样得到电流,算完最优矢量,要等到 k+1 时刻的 PWM 更新点才真正施加,中间隔了一整个 Ts。如果你直接拿 k 时刻的电流预测 k+1 时刻、然后施加,那你优化的其实是“已经过去的那一拍”,高速段电流会明显振荡甚至发散。
正确做法是分两步。第一步不看候选矢量,用上一拍实际施加的矢量(Sabc_prev)把状态从 k 推到 k+1;第二步才拿 k+1 的状态去遍历 8 个矢量,预测 k+2:
% ---- 第一步:延时补偿,用已施加的矢量推到 k+1 ---- [ud0, uq0] = sabc2dq(Sabc_prev, theta_e, p.Udc); [id1, iq1] = pmsm_predict(id_k, iq_k, ud0, uq0, w_e, p); % k+1 时刻的角度,一个周期转过 w_e*Ts theta1 = theta_e + w_e * p.Ts; % ---- 第二步:遍历 8 个矢量,预测 k+2 ---- for n = 1:8 Sabc = V(n, :); [ud, uq] = sabc2dq(Sabc, theta1, p.Udc); [id2, iq2] = pmsm_predict(id1, iq1, ud, uq, w_e, p); % 代价计算同上 end注意theta1这一行。预测 k+2 时刻的电流,用的必须是 k+1 时刻的角度,否则高速时电压矢量的 dq 分量会整体转一个角度,等效于给了一个错误的电压方向。1500 rpm、Ts = 100 us 时,这个角度误差是 0.063 rad,约 3.6°,看着不大,但 dq 轴电压本来就是靠角度分解的,3.6° 的偏差在弱磁区足以让 id 跑偏。
还有一个细节:第一步用的Sabc_prev必须是桥臂实际输出的状态,不是上一拍算出来的最优解。如果中间有死区插入、最小脉宽丢弃、或者过流保护强制关断,算出来的和打出去的不是一回事,拿错的矢量去补偿等于没补。血泪经验是:把实际状态单独存一个变量,跟“计算值”分开,别图省事复用同一个。
3.4 输出与调制接口:直接给开关量还是转占空比
最朴素的 FCS-MPCC 每个 Ts 只施加一个矢量,输出的就是三位开关量,直接写比较寄存器。优点是简单,缺点是开关频率完全跟着最优矢量的跳变走,平均开关频率可能只有 Ts 倒数的三分之一到二分之一,电流纹波偏大。想压低纹波,就得往“双矢量”或“无差拍 + SVPWM”上改。
双矢量的思路
本文还有配套的精品资源,点击获取