简介:面向双电机电池电动汽车的扭矩分配优化,这里提供一套完整的非线性模型预测控制器Simulink实现方案。资源内容从整车与双电机动力学建模出发,覆盖预测模型构建、滚动优化求解、控制律分配与闭环验证等核心环节,同时附带项目报告、运行说明、系统架构图和仿真结果图,便于读者对照复现和修改调试,适合车辆工程、控制科学等方向的研究生、工程师以及对预测控制感兴趣的初学者学习使用。压缩包共包含27个文件,其中有7个MATLAB脚本文件、3个Simulink模型文件(整车虚拟模型、电气系统架构模型、可配置被控对象模型),另有2份PDF文档、11张图片以及配置说明与许可文档,资源总大小仅为4.44MB,小巧但结构清晰,便于下载和本地运行。当前已有33人学习浏览,读者可依据模型与代码快速搭建双电机纯电动汽车的能量管理仿真环境,结合文档理解非线性模型预测控制在扭矩分配中的设计思路、优化目标与调试方法,从而有效缩短前期研究与开发时间。
1. 双电机电动汽车的扭矩分配,为什么偏偏用 NMPC
双电机电池电动汽车的扭矩分配,在实车上就是一场毫秒级的拉锯战:前轴电机和后轴电机各有独立的效率区间,SOC 不同、车速不同,最优分配点随时在变。查表法能稳住低速匀速工况,一到爬坡、急加速和能量回收叠加出现的场景,规则之间就开始互相冲突。非线性模型预测控制(NMPC)的价值在于把未来一段时间的整车功率需求、电机效率边界、电池放电约束都卷进滚动优化,每步重新算一次前后轴扭矩指令。这套工程包正好把整车建模、电气架构和 NMPC 控制器拆成三个可以独立打开的 Simulink 模型,适合整车控制方向的工程师当基准实现,也适合做策略对比时参考。
2. 先拆工程:三份 slx 文件在 Simulink 里各管什么
2.1 虚拟车辆模型和整车被控对象的边界
压缩包里除去源代码和文档,核心内容是三份.slx模型:ConfiguredVirtualVehicleModel.slx、ConfiguredSimulinkPlantModel.slx、ElectricalSystemArchitecture.slx。文件名里都带 Configured,说明作者交付的是参数已经配好、加载即可仿真的工程,不是只有框架的空模型。这一点很重要,因为 NMPC 这类控制器对模型参数的一致性极度敏感,哪怕效率曲面差两个百分点,优化结果都会偏。
ConfiguredVirtualVehicleModel.slx是整车级被控对象,里面包含纵向车速动力学、轴荷转移、车轮和坡度阻力,负责把前后轴扭矩指令换算成整车响应。ConfiguredSimulinkPlantModel.slx则更偏向控制器视角,通常会把电机、电池、传动效率折进去,NMPC 做内部预测时用的简化模型也经常从这一层抽。两者的关系可以类比台架上的实车和试验用的标定模型:前者产生仿真真值,后者给控制器当「内模」。调试时如果只改其中一个,很容易出现控制器的预测和虚拟车辆的真值对不上。
2.2 电气系统架构与数据流
ElectricalSystemArchitecture.slx描述的是电池包、DC-DC、两台电机逆变器之间的功率链路。这一层决定了代价函数里的能耗项从哪来:如果只看电机轴端效率而忽略电池内阻损耗,大功率工况下分配结果会偏向某一台电机,实际电池电流反而变大。打开这个文件时,重点看电池模型的 Thevenin 等效内阻和电压外特性曲线,它们是能量管理优化里最容易被忽略的约束。
三个模型之间的信号流通常是:控制器把需求扭矩指令发给虚拟车辆模型,虚拟车辆模型回传车速、SOC 和当前挡位状态,电气系统架构层输出电池电流和母线电压。实际操作时我一般会用Simulink.Signal给总线里的Demand_Nm、Actual_Nm、SOC_pct显式命名并标注单位,不然换个人接手,前后轴的扭矩命令很容易接反,控制器以为在优化能效,实际上在做反向充放电。
2.3 用脚本把模型结构先打印出来再动手
拿到别人的仿真工程,最忌讳直接按 Run。模型里有多少 Goto/From 块、哪些子系统藏了硬编码扭矩限制,双击一层层翻很难看全。我习惯先用一段 MATLAB 脚本把模型结构和关键参数扫一遍,再决定从哪里下手改。
mdl = 'ConfiguredVirtualVehicleModel'; % 模型名要和 slx 文件名一致 load_system(mdl); % 只加载模型,不弹界面 % 列出顶层所有模块,确认接口命名 topBlocks = find_system(mdl, 'SearchDepth', 1, 'Type', 'Block'); disp(topBlocks); % 抓出所有 Gain 模块,排查硬编码的扭矩系数和传动比 params = find_system(mdl, 'FollowLinks', 'on', 'LookUnderMasks', 'all', ... 'BlockType', 'Gain'); for i = 1:length(params) gainVal = get_param(params{i}, 'Gain'); fprintf('%s -> %s\n', params{i}, gainVal); end这段脚本里,load_system只加载不打开界面,适合批量处理;find_system配合FollowLinks,on和LookUnderMasks,all,能把封装子系统内部的 Gain 参数也挖出来。对于那些藏在 Mask 里的峰值扭矩限制、传动比、扭矩滤波系数,用这个办法能在跑仿真之前就暴露出来,避免后面改了半天发现限制条件根本没生效。
下表把三份模型的定位和检查顺序整理清楚,建议按这个顺序打开核对:
| 文件 | 在系统中的角色 | 打开后重点看 | 检查顺序 |
|---|---|---|---|
| ElectricalSystemArchitecture.slx | 电池-逆变器-电机功率链路 | 电池内阻、电压外特性 | 1 |
| ConfiguredVirtualVehicleModel.slx | 整车纵向动力学被控对象 | 车速计算、轴荷转移 | 2 |
| ConfiguredSimulinkPlantModel.slx | 控制器内部使用的预测模型 | 需求扭矩到轴扭矩的折算 | 3 |
先核对电气架构再核对整车模型,最后看控制器的内模,能避开一种典型错误:在 Plant Model 里改了电机效率,却忘了同步 Virtual Vehicle Model,仿真曲线看起来优化有效,实际两套模型眼里的根本不是同一台车。
3. NMPC 的预测模型、代价函数和约束怎么落到代码上
3.1 预测模型:从整车纵向动力学到双电机效率 Map
NMPC 的预测模型不是越精细越好,而是在保留关键非线性和保证每步优化能算完之间取折中。扭矩分配问题里必须保留的非线性有两处:一是电机在不同转速和扭矩点下的效率曲面,二是电池内阻随 SOC 和温度的变化。前者用二维 Lookup Table 表达,后者用一维查表或多项式拟合都可以。
常见做法是把整车纵向动力学缩成一个离散状态方程,状态量取车速v和电池 SOC,控制量只取前轴扭矩T_f,后轴扭矩由需求扭矩减掉T_f得到。这样双输入问题被压成单输入,预测时域的维度和约束数量同时下降,求解难度明显降低。我在实际搭建时用的预测函数如下。
function x_next = predictDualMotor(x, u, p) % x = [v; SOC], u = [T_f] v = x(1); soc = x(2); T_d = p.T_demand; % 当前驾驶员需求扭矩 T_r = T_d - u(1); % 后轴扭矩由差值决定 % 按转速限制前后轴扭矩边界,防止优化器把控制量推到物理极限外 T_f_max = interp1(p.speedAxis, p.frontTqMax, v, 'linear', 'extrap'); T_r_max = interp1(p.speedAxis, p.rearTqMax, v, 'linear', 'extrap'); u(1) = max(min(u(1), T_f_max), -T_f_max); T_r = max(min(T_r, T_r_max), -T_r_max); % 查电机效率 Map,注意单位:扭矩 Nm,转速 rpm eff_f = interp2(p.tqAxis, p.speedAxis, p.frontEffMap', u(1), v); eff_r = interp2(p.tqAxis, p.speedAxis, p.rearEffMap', T_r, v); % 电池功率:两台电机的机械功率分别除以各自效率 Pb = u(1)*v/p.wheelR/eff_f + T_r*v/p.wheelR/eff_r; x_next = zeros(2,1); x_next(1) = v + p.dt/p.m * (T_d/p.wheelR - p.F_res(v)); % 纵向动力学 x_next(2) = soc - p.dt*p.Uoc/(3600*p.Qbatt) * Pb; % SOC 更新 end这段代码里interp2用的效率数组必须和 Simulink 模型里的 Lookup Table 保持一致,这是工程里最容易出偏差的地方。x_next(1)的更新默认轮胎处在附着极限内,如果工况包含低附着路面,需要在约束里再加轴荷利用率项,否则预测的加速能力比实车乐观。x_next(2)用恒定开路电压近似内阻损耗,精度不足时把它替换成 Thevenin 模型的端电压表达式,代价是多一个状态和一个非线性方程。
3.2 代价函数:扭矩偏差、能耗与电机损耗的加权
优化问题写成标准形式,就是滚动求解一个带约束的最小化问题。目标函数一般取三项:扭矩跟踪性、能量经济性、扭矩变化平滑性。三项权重的量纲完全不同,不能直接比较数值大小,要先各自归一化再调参,否则调w1时根本不知道它和能耗项的量级差。
| 代价项 | 表达式 | 默认权重 | 调参方向 |
|---|---|---|---|
| 扭矩跟踪 | (T_f + T_r - T_d)^2 | w1 = 1 | 起步加速阶段可适度减小 |
| 能量消耗 | Pb * dt | w2 = 50 | 续航测试工况下加大 |
| 扭矩变化率 | (u_k - u_{k-1})^2 | w3 = 2 | 出现抖动时加大 |
w2给到 50 不是随意写的,电池功率的量级通常是几十千瓦,不放大它,在平方项里根本压不过百牛米级的扭矩偏差。实车标定时我一般固定w1,把w2从 1 开始十倍往上试,观察 SOC 下降曲线和扭矩跟随误差的拐点,找到「能耗明显改善但跟踪误差还能接受」的区间,再回头细调w3抑制抖动。三个权重之间不是独立关系,调完w2后w3的效果会被放大,需要回头复验一轮。
提示:代价函数里的每项都要除以对应的归一化系数,比如扭矩项除以
T_d^2,能耗项除以Pb_nominal,否则不同工况下权重比例会漂移。
3.3 求解器选型与实时性约束
NMPC 每步都要解一次非线性优化,实时性是这个工程落地时最大的坎。小规模问题(两三个状态、单个控制量)用fmincon的内点法就能跑,纯离线仿真完全够用。如果要上硬件在环,就得换 CasADi 配 IPOPT,或者退一步把预测模型在当前工作点线性化,退化成基于 LTV 的 MPC,用二次规划求解器把单步求解时间压到 10 ms 以内。
Simulink 里最常见的接法是用 MATLAB Function 模块包住优化求解,函数内部用coder.extrinsic声明fmincon,这样普通仿真能跑,将来做 Embedded Coder 代码生成时也留了替换入口。但要注意fmincon每次调用都要传函数句柄,内部会反复触发效率查表,仿真步长设得太小时这一步会成为整环瓶颈。我的做法是把预测模型写成独立的predictDualMotor函数,优化器只负责迭代调用,这样既能单独调试预测模块,也方便后续把效率表换成 Response Surface 或者神经网络回归来加速查表。
4. 冷启动与排错:把仿真跑通并让控制器稳定工作
4.1 按顺序加载模型再运行
工程包里有How to run simulation.pdf,但在按文档操作之前,我建议先在脚本里把三份模型按依赖顺序加载一遍。电气架构模型定义总线类型和物理参数,虚拟车辆模型依赖它,控制器模型又依赖车辆模型的接口,顺序错了会报「未定义的总线对象」。
models = {'ElectricalSystemArchitecture', ... 'ConfiguredVirtualVehicleModel', ... 'ConfiguredSimulinkPlantModel'}; for i = 1:numel(models) if bdIsLoaded(models{i}) close_system(models{i}, 0); % 不保存关闭,防止动到原工程 end load_system(models{i}); end set_param('ConfiguredVirtualVehicleModel', 'StopTime', '50'); simOut = sim('ConfiguredVirtualVehicleModel');close_system(models{i}, 0)的第二个参数0表示关闭时不保存改动,这一点很关键,调试过程中改的采样时间、求解器设置都会被丢弃,原工程文件保持干净。跑完后simOut里能拿到logsout和yout等默认输出,具体变量名要看模型里的 Signal Logging 配置,下一步验证时直接引用就行。
4.2 求解器配置和采样时间
双电机整车模型横跨机械和电气两个时间尺度,车速动态是秒级,电池电流和逆变器开关动态是毫秒级。仿真步长选不好,就会出现「车速没变化、电池电流高频振荡」的怪现象。这类模型我一般不用默认的ode45,而是按下面的表来配。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Solver | ode15s 或 ode23t | 模型含刚性环节时比 ode45 稳 |
| Max Step Size | 0.01 s | 限制电气振荡对数值积分的影响 |
| Filter Time Constant | 0.05 s | 给扭矩指令加一阶滤波,减少抖动 |
| NMPC 采样周期 | 0.02 ~ 0.05 s | 决定滚动优化的执行频率 |
如果模型刚性不强,用ode45配合Max Step Size也能跑,但在急加速或重启动变工况时容易报「步长在 t=.. 处小于最小步长」。看到这个提示先别急着改最小步长限制,把Max Step Size从auto改成0.01通常能消掉大部分报错。NMPC 采样周期和求解器步长之间没有强制关系,但建议 NMPC 的更新周期是求解器最大步长的整数倍,否则每次优化结果在两次采样之间会被零阶保持器切得很难看。
4.3 常见报警:代数环、S 函数版本和效率 Map 越界
第一次跑这类工程,三个报错出现频率最高。第一个是代数环:扭矩指令经过效率计算反查电池电流,电流影响电压,电压又回算功率,Simulink 检测到直接求值回路就会报警。处理办法是在反馈路径上放一个 Memory 块,或者把电气模型改成用上一时刻的电压,代价是损失一点精度,但能有效切断代数环。
第二个是 S 函数和代码生成版本问题。如果 MATLAB Function 里用了coder.extrinsic,老版本编译器在做代码生成时报「不支持该函数」很常见。先确认仿真模式能跑通,再考虑生成代码,不要一上来就奔着生成目标去。第三个是效率 Map 越界:电机转速或扭矩超过查表区间上界时,interp2默认返回 NaN,NaN 一路传播进优化器,fmincon直接失败。给查表加'linear', 'extrap'只是权宜之计,正规做法是在模型里加饱和模块,把预测用的转速和扭矩钳在表格范围内,同时在代价函数里对越界量加惩罚项,双保险才稳。
5. 从仿真数据反推控制效果:一个可复用的验证技巧
5.1 把 logsout 信号整理成可对比的表格
仿真跑通只是开始,真正判断 NMPC 有没有优于规则分配,要用同一段工况分别跑 NMPC 和 rule-based 策略,再对比 SOC 下降量和扭矩跟随误差。前提是信号记录足够规范。目标信号建议在 Signal Logging 里统一命名为v_meas、T_front、T_rear、SOC_pct、Pb_w,后处理脚本可以直接复用。
logsout = simOut.get('logsout'); v = logsout.getElement('v_meas').Values.Data; Tf = logsout.getElement('T_front').Values.Data; Tr = logsout.getElement('T_rear').Values.Data; soc = logsout.getElement('SOC_pct').Values.Data; Pb = logsout.getElement('Pb_w').Values.Data; splitRatio = Tf ./ (Tf + Tr + eps); % 前轴扭矩占比 totalEnergy = trapz(Pb) / 3600; % 积分得到总能耗,单位 kWhsplitRatio是判断控制器行为最直观的指标:规则分配在恒扭矩区间是平坦的,NMPC 的曲线会随车速和 SOC 动态起伏,起伏的幅值和频率反映优化器对效率边界的响应。trapz对功率采样点做数值积分,算出的总能耗要和 SOC 变化量交叉验证,如果两者方向矛盾,说明电池模型的内阻损耗设置有问题,查完模型再回来谈策略优劣。
5.2 用前轴占比和能耗做横向对比
把不同权重配置下的结果叠在同一张图里,比逐条看曲线高效得多。对比时固定三个指标:平均跟踪误差mean(abs(T_d - Tf - Tr))、总能耗、扭矩波动率,也就是前后轴指令变化量的标准差。这三个指标正好映射代价函数里的跟踪项、能耗项和平滑项,权重改没改到位,一眼就能看出来。
滚动优化里一个常被忽略的细节是初始控制序列的选择。如果每步都用上一时刻的解当初始点,急加速工况下优化器容易困在局部解,表现为扭矩分配突然跳到边界值再弹回来。我一般会把「零控制量」和「上一时刻解」各算一次代价,取较小者作为当前步的起点,计算量翻倍但稳定性明显改善。这个技巧在 MATLAB Function 里改动只有几行,却是仿真曲线从锯齿状变平滑的分水岭。权重、采样周期、初始序列三点调完,再回头看splitRatio曲线是否保留清晰的物理含义——前轴占比应该在高 SOC 时偏向高效区,低 SOC 时偏向内阻损耗更小的电机,而不是随工况无规律乱跳。
本文还有配套的精品资源,点击获取