开头插电式混合动力车辆PHEV的能量管理策略说到底是解决一个“分配问题”整车需求功率摆在那儿发动机和电池电机两条能量路径怎么分。传统规则策略比如CD-CS模式经验性强但离最优解有距离动态规划DP能拿到全局最优却又依赖未来工况信息实车跑起来没法用。做研究的时候经常要在这两个方向之间找平衡点。最近我把ADMM交替方向乘子法和CVX工具箱组合起来用凸优化方法求解模型预测控制MPC框架下的PHEV能量管理问题Matlab代码全部实现。这篇博文就把这个方案的思路、建模细节、ADMM拆分方式、Matlab实现要点和踩过的坑完整记录下来供做车辆能量管理、MPC算法研究的朋友参考复现。适合谁看正在做PHEV/HEV能量管理策略研究方向的学生或工程师对凸优化和MPC有一定了解、想落地代码的读者以及想了解ADMM在工程MPC问题里怎么用的朋友。1. 为什么要用凸优化和ADMM从问题本质讲起先看PHEV能量管理在MPC框架下的原始问题到底长什么样。以并联式PHEV为例每个控制步长内我们需要决定发动机输出转矩 ( T_{eng} )、电机转矩 ( T_{mot} )以及是否有挡位选择约束条件是[ T_{req}(k) T_{eng}(k) T_{mot}(k) ][ T_{eng,min} \leq T_{eng}(k) \leq T_{eng,max} ][ T_{mot,min}(SOC(k)) \leq T_{mot}(k) \leq T_{mot,max}(SOC(k)) ]电池SOC的演化是一个动态过程[ SOC(k1) SOC(k) - \frac{I_{bat}(k) \cdot T_s}{Q_{bat}} ]其中电池电流由电功率 ( P_{bat} T_{mot} \cdot \omega_{mot} \cdot \eta_{elec} ) 决定而 ( P_{bat} ) 和 ( SOC ) 之间有非线性关系。目标函数要平衡三个东西燃油消耗、SOC偏差电量维持、以及控制量变化率避免发动机频繁启停。于是得到典型的带状态约束和控制约束的最优控制问题[ \min \sum_{k0}^{N-1} \left[ \dot{m}{fuel}(k) \lambda \cdot (SOC(k) - SOC{ref})^2 \Delta u(k)^T R \Delta u(k) \right] ]1.1 这个问题的“非凸”到底出在哪如果直接用非线性规划NLP求解器去解这个MPC问题会有几个麻烦部件效率MAP图通常是非线性的燃油消耗率 ( \dot{m}{fuel}(T{eng}, \omega_{eng}) ) 是个二维查表非线性函数。这直接导致目标函数非凸。电机效率 ( \eta_{mot}(T_{mot}, \omega_{mot}) ) 同理非凸。电池开路电压 ( V_{oc}(SOC) ) 和内阻 ( R_{int}(SOC) ) 是SOC的函数SOC的变化进入约束后导致可行域可能非凸。非凸问题最大的问题是你不知道求到的是局部最优还是全局最优。对MPC这种滚动优化框架来说每次求解都要求快、求稳非凸求解器一是慢二是初值敏感三是可能跳出不可行解这在实时控制里是不能接受的。1.2 凸化改造把工程问题变成“可证明最优”的问题我们的目标不是“严格物理精确”而是“在可接受精度下得到凸问题”。具体做法燃油消耗率凸化把发动机油耗MAP发动机转速—转矩—油耗率拟合成一个凸函数典型做法是用二次型或者分段线性凸函数逼近。实测中发动机BSFC在大多数运行区域确实呈近似凸的碗形用凸二次型拟合[ \dot{m}{fuel} \approx a_0 a_1 T{eng} a_2 T_{eng}^2 a_3 \omega_{eng} a_4 \omega_{eng}^2 a_5 T_{eng} \omega_{eng} ]拟合时注意需要在MAP的凸包范围内拟合超出边界的外插值容易破坏凸性。电机/电池凸化电池电功率作为决策量的函数通常经过Fersey-Chen逼近将非线性关系转化为关于SOC和( P_{bat} )的凸二次型约束[ SOC(k1) SOC(k) - \frac{T_s}{Q_{bat}} \cdot f_{approx}(SOC(k), P_{bat}(k)) ]其中 ( f_{approx} ) 是一个凸二次逼近可以通过离线最小二乘拟合得到。1.3 为什么ADMM适合这个MPC场景MPC的滚动优化特点是每个时间步都要解一个有限时域优化问题下一时刻要热启动。传统SQP算法每步要求解KKT系统、做线搜索碰到强耦合约束时迭代不稳定。而ADMM的核心思想是把原问题分解成若干个子问题再通过一致性约束协调这些子问题每轮迭代只做一次简单更新从根本上规避了“牵一发动全身”的耦合性难题。ADMM对于带线性约束的凸优化问题——特别是目标函数可分、约束可分的问题——被证明能以O(1/k)的收敛速率稳定收敛。对PHEV能量管理这种每步MPC求解时域一般在10-20步以内的问题ADMM的迭代次数完全可控而且热启动效果特别好。通俗一点理解ADMM像个“分包”——把总任务拆成发动机子模块和电池子模块各自独立算然后由一个协调者一致性约束把它们的结果拉回来对齐。每个分包干自己的活协调者只用处理简单的线性等式约束所以比胡子眉毛一把抓快得多。2. 系统建模与问题重构MPC中的状态空间与约束2.1 控制变量与状态变量的选取这个问题的状态变量是SOC一个标量为的是MPC里预测电池电量的变化。控制变量一般取发动机转矩 ( T_{eng} )电机转矩 ( T_{mot} ) 通过转矩平衡方程确定为 ( T_{req}(k) - T_{eng}(k) )未考虑挡位变化简化处理。这样处理后一个MPC步长的问题中决策变量只剩发动机转矩序列 ( {T_{eng}(0), \dots, T_{eng}(N-1)} )SOC通过动态方程关联起来形成一个标准的凸最优控制问题——发动机转矩序列作为独立控制序列SOC作为状态轨迹。2.2 目标函数各项的具体形式工程上目标函数需要量纲匹配。燃油消耗项量纲是“克/秒”SOC偏差项量纲是“无量纲的平方”控制量变化率项量纲是“转矩平方”。三个量纲完全不同的量相加是没有意义的必须引入权重系数。实际操作中我采用如下形式[ J \sum_{k0}^{N-1} \left[ \dot{m}{fuel}(k) \cdot T_s \lambda{soc} \cdot (SOC(k) - SOC_{ref})^2 \lambda_{delta} \cdot (T_{eng}(k) - T_{eng}(k-1))^2 \right] ]其中( T_s ) 是采样时间比如1秒把燃油消耗率积分起来得到每步的油耗量。( \lambda_{soc} ) 是SOC维持权重决定了电池“电量保持”的紧程度。设大了MPC会严格把SOC拉回参考值设小了SOC会自由漂移更像电量消耗模式。( \lambda_{delta} ) 是发动机转矩变化惩罚项抑制发动机转矩突变。权重调节的经验先固定 ( \lambda_{delta} ) 为一个较小值比如0.01然后扫描 ( \lambda_{soc} )看SOC轨迹和油耗量的取舍关系。具体做法是折衷曲线的拐点处取值——既不让SOC大幅漂移也不让发动机频繁启停。每个工况的最优权重略有不同但同一辆车在同一典型工况下权重的敏感度并不高取个中间偏小的值即可。2.3 发动机启停逻辑的处理MPC里如果直接引入发动机开关的二进制变量 ( \delta_{eng} \in {0,1} )问题就变成混合整数规划MILP或MIQP求解复杂度陡增。凸优化的框架下处理方式有两种松弛法发动机转矩下限改为可变下限——当SOC充足时下限抬高到0发动机不工作SOC低时下限降低到最小稳定转矩。这本质上是把启停决策嵌入SOC约束避免了二进制变量。后处理法MPC求得连续解后对低转矩时段做阈值处理低于某个转矩阈值就关停发动机。第一种方法更优雅但需要精心设计SOC相关的转矩下限函数我实际做了第二种——后处理法简单直接对燃油经济性影响较小低转矩区间的发动机效率本来就很差关掉反而省油。3. ADMM如何拆解MPC问题原理与推导3.1 原问题到ADMM形式的标准转化上面的MPC问题写成紧凑形式[ \min_{x} ; f_{eng}(x) f_{soc}(z) f_{delta}(x) ][ \text{s.t.} \quad x z ]这里 ( x ) 代表决策变量发动机转矩序列以及与之相关的SOC动态表达式( z ) 是复制变量用来隔离不同目标函数项。更具体一点原问题可以写成状态空间的形式[ \min_{u, x} \sum_{k0}^{N-1} f_k(u_k, x_k) ][ \text{s.t.} \quad x_{k1} A x_k B u_k w_k ][ x_k \in \mathcal{X}_k, \quad u_k \in \mathcal{U}_k ]ADMM的第一步引入辅助变量 ( z_k x_k )将问题重写为[ \min_{u, x, z} \sum_{k0}^{N-1} f_k(u_k, x_k) ][ \text{s.t.} \quad x_{k1} A x_k B u_k w_k, \quad x_k z_k ][ x_k \in \mathcal{X}_k, \quad u_k \in \mathcal{U}_k, \quad z_k \in \mathcal{Z}_k ]这里的巧妙之处在于把实现对 ( x_k ) 的约束拆到 ( z_k ) 上而状态动态方程仍然保留在 ( x ) 子问题上。因为 ( \mathcal{Z}_k )比如SOC上下限约束往往比 ( \mathcal{X}_k ) 简单很多这样拆分后每个子问题结构简单、可高效求解。3.2 两个子问题的交替求解写出增广拉格朗日函数[ \mathcal{L}{\rho}(x, z, \mu) \sum{k0}^{N-1} f_k(u_k, x_k) \mu^T (x - z) \frac{\rho}{2} | x - z |^2 ]ADMM的迭代分三步第1步x-更新固定 ( z, \mu )求解[ x^{k1} \arg\min_{x} \sum_{k0}^{N-1} f_k(u_k, x_k) \mu^T (x - z) \frac{\rho}{2} | x - z |^2 ]这一步其实就是一个带动态方程约束的二次规划因为目标函数里包含了发动机转矩的凸二次项和SOC偏差项而且由于动态方程是线性的这个子问题只是线性二次调节器LQR类型的问题可以直接用Riccati递推求解或者用CVX建模。第2步z-更新固定 ( x, \mu )求解[ z^{k1} \arg\min_{z} \mu^T (x - z) \frac{\rho}{2} | x - z |^2 ][ \text{s.t.} \quad z_k \in \mathcal{Z}_k ]这一步就简单到极点——它本质上是一个带盒约束的最小二乘问题直接在SOC上界下界做截断投影即可[ z^{k1} \min(\max(x^{k1} \mu / \rho, z_{min}), z_{max}) ]第3步对偶变量更新[ \mu^{k1} \mu^k \rho (x^{k1} - z^{k1}) ]3.3 这个拆分好在哪先看x-更新它处理的是动态耦合和发动机的凸目标函数问题结构清晰、海森矩阵固定可以用预分解好的KKT矩阵做极速求解。再看z-更新它只处理SOC的上下限约束和电池的凸成本项投影操作O(N)复杂度一轮迭代里这一步几乎不花时间。整体迭代每轮只需要一次矩阵乘法和一次投影计算比直接用QP求解器快出一个量级而且对热启动特别友好——上一时刻MPC的解直接作为下一时刻初值通常3-5次ADMM迭代就收敛到工程可用的精度。这个拆分方式的价值在于不用牺牲问题凸性去换求解速度也不需要在每个MPC时步内调用重型求解器。ADMM把一个大QP拆成了几个更容易手算和代码化的子步骤非常适合嵌入式实时控制场景。4. Matlab实现CVX建模与ADMM代码框架4.1 顶层数据流设计Simulink/数据加载 ↓ 预处理读取驱动工况车速-时间序列、计算需求功率 ↓ MPC控制器循环 for each time step: ↓ (1) 计算需求功率 P_req(k) F_trac(k) * v(k) / η_drivetrain (2) 初始化热启动变量上一次ADMM解的x, z, μ (3) 调用ADMM迭代内含CVX求解x子问题 (4) 取解的第一个控制量 T_eng(0) 作用于系统 ↓ 仿真推进 → 电机转矩跟随 → SOC状态更新 → 进入下一时刻4.2 CVX求解x子问题的代码模板ADMM里x-更新子问题需要用到CVX建模。核心代码结构如下function [x_new] solve_x_subproblem(z, mu, rho, P_req_seq, SOC_ref, u_prev, params) % x子问题带线性动态约束的凸二次规划 N params.N; T_s params.T_s; Q_bat params.Q_bat; SOC0 params.SOC0; % 动态方程系数SOC(k1) a * SOC(k) b * P_bat(k) c % 其中 b 是电池功率对SOC变化的映射系数已做凸逼近处理 cvx_begin quiet variable SOC(1, N1) variable T_eng(1, N) % 目标函数 fuel_cost 0; for k 1:N % 发动机油耗凸逼近 w_eng params.w_seq(k); % 当前转速 fuel_cost fuel_cost params.fuel_coeffs(1) * T_eng(k) ... params.fuel_coeffs(2) * T_eng(k)^2 ... params.fuel_coeffs(3) * w_eng^2; % SOC偏差项 fuel_cost fuel_cost params.lambda_soc * (SOC(k) - SOC_ref)^2; % 控制量变化率惩罚 if k 1 fuel_cost fuel_cost params.lambda_delta * (T_eng(k) - T_eng(k-1))^2; else fuel_cost fuel_cost params.lambda_delta * (T_eng(1) - u_prev)^2; end end minimize(fuel_cost mu * (SOC(2:end) - z) (rho/2) * sum_square(SOC(2:end) - z)) subject to % SOC动态方程 for k 1:N P_bat P_req_seq(k) - T_eng(k) * w_eng * params.eta_eng2mech; SOC(k1) SOC(k) - T_s/Q_bat * (P_bat params.battery_loss_coeff * (P_bat)^2); end % 发动机转矩约束 T_eng_min T_eng T_eng_max; % 电机功率约束避免电池过放 P_bat_min P_req_seq - T_eng .* w_eng * params.eta_eng2mech P_bat_max; cvx_end x_new SOC(2:end); % x变量对齐z的维度 end这个代码块里有两个细节要特别说明。第一SOC(k1)的动态方程里加了params.battery_loss_coeff * (P_bat)^2一项这是为了把电池内阻损耗凸化进去。电池的充放电效率损失本质上可以用一个关于 ( P_{bat} ) 的凸二次项近似这一项放进约束里不破坏凸性但明显提升了SOC预测精度。第二mu * (SOC(2:end) - z) (rho/2) * sum_square(SOC(2:end) - z)是增广拉格朗日项写在目标函数里。注意CVX要求所有表达式必须是凸的这里线性项加平方项天然满足。rho是ADMM惩罚参数默认取1实际调的时候根据SOC轨迹的振荡情况调整。4.3 ADMM主循环代码function [T_eng_sol, SOC_traj] admm_mpc(P_req_seq, SOC0, u_prev, params) % 完整的ADMM-MPC求解器 N params.N; rho params.rho; % 初始化 z SOC0 * ones(1, N); % 辅助变量初始化为SOC0 mu zeros(1, N); % 对偶变量初始化 x SOC0 * ones(1, N); % 状态变量初值 % 预计算SOC上下限 SOC_min 0.3 * ones(1, N); SOC_max 0.8 * ones(1, N); % ADMM迭代 for iter 1:params.max_iter % x-更新调用CVX子问题 x_new solve_x_subproblem(z, mu, rho, P_req_seq, params.SOC_ref, u_prev, params); % z-更新带上下限的投影 z_new min(max(x_new mu/rho, SOC_min), SOC_max); % 对偶变量更新 mu mu rho * (x_new - z_new); % 收敛判断 r_norm norm(x_new - z_new); s_norm norm(-rho * (z_new - z)); if r_norm params.tol s_norm params.tol break; end x x_new; z z_new; end % 取第一个控制量用于系统推进 SOC_traj x_new; % 发动机转矩求解从SOC轨迹反推 w_eng params.w_seq(1); P_bat - (SOC_traj(1) - SOC0) * Q_bat / T_s; T_eng_sol (P_req_seq(1) - P_bat) / (w_eng * params.eta_eng2mech); T_eng_sol max(min(T_eng_sol, params.T_eng_max), params.T_eng_min); end4.4 热启动的实现细节MPC每步之间解的结构高度相似ADMM热启动能大幅减少迭代次数。我实现的方式是保存上一个MPC步长最终的z和mu作为当前步的初始值。实测下来冷启动大概需要15-20轮ADMM迭代达到残差 (10^{-4})热启动只需要3-5轮。这个差距在整车仿真中是很可观的——冷启动跑NEDC工况全程一个MPC周期耗时约120ms热启动能压到40ms以内已经达到dSPACE快速控制原型实时仿真的门槛。5. 仿真结果与参数敏感性分析5.1 测试工况与基础参数我用的是某款并联PHEV参数整备质量1650kg发动机最大功率90kW电机峰值功率50kW电池容量6.5AhSOC工作区间0.3-0.8。测试工况选了WLTC低中高段以及NEDCMPC预测时域N15采样时间 ( T_s1s )。5.2 核心结果对比策略综合油耗L/100kmSOC终值求解耗时每MPC步/ms规则CD-CS5.80.420.1DP离线最优4.20.40离线MPC-CVX直接求解4.40.40250MPC-ADMMCVX本文方法4.40.4145关键发现ADMM求解的MPC油耗和直接凸优化求解的MPC几乎一致恶化在0.5%以内说明ADMM收敛到了工程可接受精度。相比规则策略MPCADMM在WLTC工况下节油约24%这个收益主要来源于工况前瞻——规则策略只能在当前时刻做模式判断而MPC能预判未来15秒的需求功率变化提前决定是否充电/放电。求解耗时45ms/步在实际嵌入式平台如TI TMS320F28379D跑到200ms/步就够实时留了足够余量。5.3 ADMM惩罚参数rho的敏感度rho这个参数很有意思——它控制原始残差和对偶残差的收敛比例。我扫了从0.1到10的范围发现( \rho0.1 ) 时对偶残差收敛快但原始残差很慢需要30次迭代。( \rho1-2 ) 时原始和对偶残差相对均衡15次迭代到收敛精度。( \rho10 ) 时原始残差很快就满足但对偶残差振荡明显卡在边界上反复横跳。实际建议是 ( \rho ) 取1-2之间并通过数次试验微调。如果发现SOC轨迹在边界附近振荡可以适当增大rho并配合松弛因子 ( \alpha1.5 ) 加速。5.4 权重参数SOC维持强度的影响lambda_soc从0.1调到10的过程中我观察到一个有意思的现象lambda_soc低于1时SOC终值明显偏离参考值比如从0.4漂到0.55油耗虽然略微降低但电池电量状态不可控超过5时SOC轨迹被死死钉在参考值附近但发动机频繁调节导致油耗反而上升0.8%。这个现象的本质是SOC维持约束和最优燃油经济性之间存在天然的折衷——过度追求SOC维持会牺牲发动机工作在高效区的时间窗口。所以我的做法是让lambda_soc在一个中等偏小的值比如1.5-2同时配合SOC上下界约束作为硬约束兜底。这样既能发挥MPC的电量规划能力又把SOC控制在一个安全区间内。6. 常见问题与调试经验6.1 CVX报错“Disciplined convex programming error”这是我最早踩的坑。原因通常是在CVX的约束表达式里写进了非凸项。比如把电池效率查表函数interp2直接写进约束或者把SOC动态方程里出现SOC * T_eng这样的双线性项两个变量相乘。解决办法把所有查表函数离线拟合为多项式确保只有凸二次项、线性项、常数项。检查SOC动态方程里是否存在变量乘积如果有就必须做变量替换或者线性化——比如把 ( SOC \cdot P_{bat} ) 项通过Fersey-Chen逼近转化为 ( P_{bat} ) 和 ( SOC ) 的分离凸项。一个调试技巧把CVX里每个约束单独拿出来检查逐个注释掉定位出错的位置。6.2 ADMM收敛慢或者振荡表现原始残差和对偶残差交替下降又反弹形成锯齿。常见原因和处理办法问题rho与问题缩放不匹配。解决按前述扫描方式调整rho。问题SOC上下界投影频繁处于激活状态导致z更新在边界处反复横跳。解决加入ADMM松弛项即改z_new alpha * (x_new mu/rho) (1-alpha) * zalpha取1.5-1.8标准做法。问题热启动变量没有跟上工况突变比如大油门加速段。解决设置一个自适应迭代上限当残差超过阈值时默认迭代次数翻倍而不是固定迭代上限。这样保证动态工况下仍然收敛。6.3 MPC预测时域N的选取N太短小于5时MPC看不到前方工况退化为近似瞬时最优节油效果打折N太长30以上时问题规模线性变大每步求解时间飙升而且过远的预测信息本身误差也大。我的经验是N10-15之间最优。以1秒采样时间对应10-15秒的前瞻窗口这个时间跨度刚好覆盖加速/滑行/制动的典型事件周期又不至于因为驾驶员意图预测过远而失真。6.4 Matlab版本兼容性CVX和某些新版本Matlab尤其是2023之后的版本偶有兼容性问题。建议固定使用Matlab 2020b-2022b配合CVX 2.2这个组合最稳。如果遇到cvx_solver报错可以试cvx_solver sedumi或cvx_solver sdpt3切换求解器。另外代码里可以用cvx_begin quiet跳过详细求解日志加快迭代速度。6.5 从仿真到实车控制器移植的注意事项仿真里用CVX每步调库求解没问题但实车ECU上不可能安装CVX。到了产品化阶段需要把CVX建模的凸二次规划问题离线展开成标准QP矩阵形式然后部署到内点法求解器如qpOASES、OSQP上进行求解。这一步的坑在于CVX自动推导的KKT矩阵稀疏结构和其他求解器格式不同需要手动做一次模型转换和矩阵归一化。7. 实际使用中的几点体会最后聊几个我实际操练这个方案后的个人体会。第一ADMM不是万能的。它的强项是处理“结构可分”的问题如果问题本身耦合性强、目标函数到处交叉强行套ADMM反而比直接上QP求解器更折腾。PHEV能量管理恰好是那种“天然可分”的问题——发动机子系统和电池子系统在物理上就是能拆开的ADMM的拆分逻辑和物理结构对上了效果才会好。硬套一个不匹配的算法收敛慢、迭代乱是方法选型的问题不是算法不好。第二凸化逼近那一步是最不能省的。很多人拿到问题直接往CVX里塞原始MAP查表数据CVX报错之后才开始做逼近来回折腾费时间。我的经验是先做离线MAP数据的凸性检测画三维图看碗形对不对再决定用二次还是分段线性拟合最后再进MPC循环。这个前置工作看起来多花一天时间但后续调试省下的时间远不止一天。第三权重调参要有耐心而且要带物理直觉。lambda_soc和lambda_delta这两个权重不是拍脑袋定的。我习惯先跑一遍不加SOC项的纯油耗优化看SOC自由漂移的轨迹和最终油耗再逐步加大SOC权重看SOC回到参考值的代价油耗增加量。这个折衷曲线画出来之后你就能直接给非专业的项目负责人解释为什么百公里油耗和SOC维持不能都要最好——这是一条下凸的帕累托前沿选择就是一个工程取舍。后面我还想把这套ADMM-MPC方案扩展到带挡位决策的PHEV构型——那就要引入一个整数变量挡位问题会变成混合整数凸优化。我打算用一个双层ADMM来处理外层ADMM协调挡位内层ADMM解决连续凸优化问题。等代码跑通了再回来更新。