☰
基于MPC的车辆纵向速度跟踪与Carsim/Simulink联合仿真实现
2026/10/24 2:47:35 网站建设 项目流程

做纵向速度跟踪有一阵子了,早先项目里用的是PID,平路低速工况下勉强能用,可一旦遇到连续坡道或者速度变化比较剧烈的参考曲线,误差就开始来回摆,加速度冲击也大。后来我把方案换成了MPC模型预测控制,在Simulink里用2级S函数封装滚动优化,再和Carsim做联合仿真,速度跟踪的稳定性和舒适性明显上了一个台阶。这篇就来聊聊从车辆纵向模型、联合仿真链路、S函数实现到参数整定的完整过程,给想用MPC做速度跟踪或正在搭Carsim/Simulink联合仿真环境的朋友一个可以直接参考的落地样本。

1. 纵向速度跟踪用MPC,到底解决了什么问题

1.1 从PID到MPC:多工况下控制器的差异

车辆纵向速度跟踪不只是“把车速稳住”这么简单。真实场景里,空气阻力随车速平方增长,坡度阻力会随着道路起伏突变,动力总成还存在明显的响应延迟,这些都是PID不太好处理的因素。我最早用PI控制器,标定一组参数后,在30到60 km/h、平直路面上效果还行,误差能控制在1 km/h以内。但把参考速度改成连续加减速曲线,或者把Carsim道路改成带坡度的工况后,PI参数就不够用了:要么超调,要么恢复时间特别长,严重时还会出现油门和制动来回切换的振荡。

MPC的思路和PID不一样。PID是根据当前误差决定控制量,本质上是“看到问题再纠偏”;MPC则是用一个预测模型把未来一段时间内的车辆状态推演出来,在满足约束的前提下,找一条让误差和能耗都最小的控制序列。每一步都重新优化一遍,所以它天然带有“前瞻性”。这个区别在纵向速度跟踪里特别关键:当参考速度要从60降到30时,MPC会在降速开始前就规划好减速度轨迹,而不是等车速已经冲过参考值之后才猛踩刹车。

用MPC还有一个直接好处:约束可以显式写进优化问题里。加速度上限、冲击度上限、油门范围这些东西,PID虽然可以通过限幅去限制,但那是“事后限制”,控制器仍然可能在限幅边缘反复动作。MPC在优化时就能预见到未来某一步会碰到约束,从而提前调整控制量,最终输出的曲线要平滑得多。

1.2 车辆纵向模型的三种简化写法

在开始写S函数之前,得先确定预测模型长什么样。车辆纵向动力学按由繁到简有三层写法。

最底层的是完整行驶方程:

m·dv/dt = Ft − (Fr + Fa + Fg)

其中Ft是驱动力,Fr是滚动阻力,Fa是空气阻力,Fg是坡度阻力。滚动阻力可以写成fr·m·g,空气阻力是0.5·ρ·Cd·A·v²,坡度阻力是m·g·sinθ。这个模型物理意义最清晰,适合用来理解车辆受力,但直接放进MPC预测模型里会有两个麻烦:一是Ft和执行机构(油门/制动)之间的映射不好精确建模,二是方程带非线性项v²,在线优化要么线性化,要么走非线性求解,实时性很吃亏。

实际做速度跟踪MPC,我更推荐第二种写法:把期望加速度作为控制量,并假设动力总成有一个一阶惯性延迟:

da/dt = (u − a) / τ

这里u是期望加速度,a是实际加速度,τ是动力总成响应时间常数,一般取0.3到0.5秒。这个近似的意思是:驾驶员或者控制器给一个加速度请求,发动机/制动系统不会瞬间达到这个值,而是按照一个惯性环节逐步逼近。这个模型用来做预测已经足够准确,而且线性,后续推导状态方程非常方便。

第三种写法是在第二种基础上把状态改成速度误差。取状态x1 = v − v_ref,x2 = a,控制量u = a_des,可得连续状态空间方程:

dx1/dt = −x2 dx2/dt = (u − x2) / τ

离散化后写成:

x(k+1) = A_d·x(k) + B_d·u(k)

MPC的代价函数直接惩罚x1,也就是速度误差,非常直观。注意一点,如果参考速度v_ref本身随时间变化,模型里会出现v_ref的导数项,我通常把参考加速度当作前馈量加在控制输出上,或者把v_ref_dot作为已知扰动放进预测模型,否则等速跟踪没问题,变速跟踪时会多一个稳态误差。

1.3 约束为什么要“显式”写

纵向控制里最常见的约束有三个:加速度上下界、冲击度上下界、执行机构饱和范围。加速度太大会让乘客不舒服,冲击度太大会让人感觉一顿一顿的,油门和制动本身也有物理极限。PID时代,我一般是在控制器输出后面接一个Saturation模块,把加速度夹在±3 m/s²之间。看起来约束是起作用了,但控制器本身并不知道这个限制存在,它仍然按照无约束的逻辑算控制量,经常出现的情况是:控制量顶到限幅值后,积分项还在继续累积,产生严重的积分饱和,等误差反转时控制量迟迟退不下来。

MPC就完全不一样。约束直接进优化问题,控制器算出来的控制序列本来就满足约束,不存在“事后截断”的问题。比如参考速度阶跃降低时,MPC会给出一个刚好在加速度约束边界内、但不触碰边界的减速度曲线,而不是PID那样先给出一个很大的负控制量再被限幅削掉。这也是我把纵向控制方案换成MPC之后,感觉最明显的一点。

2. Carsim与Simulink联合仿真链路的搭建

2.1 I/O通道配置:从Carsim里取什么、给什么

Carsim和Simulink联合仿真时,Carsim负责提供高保真车辆动力学,Simulink负责跑控制器。首先要做的事是配置Carsim的I/O接口。在Carsim主界面的Simulink接口设置里,输入通道我选了油门开度和制动主缸压力。油门开度用0到1之间的数值表示,制动压力用MPa,这两个量基本覆盖了纵向执行机构的全部控制方式。

输出通道这边,我选择的是纵向车速Vx和纵向加速度Ax,另外把发动机转速也带出来了,虽然控制器用不上,但后面做信号监视和验证时很方便。这里有一个必须警惕的坑:Carsim里很多变量的默认单位是km/h和km/h/s,而我们Simulink模型里统一用m/s和m/s²。如果忽略单位换算,MPC算出来的控制量从一开始就是错的,而且很难察觉,因为仿真中车速曲线看起来还是光滑的,只是数值上差了一个2.78倍的系数。

常用的Carsim输出信号和单位我列在下面:

信号名称Carsim常见单位换算为国际单位
纵向车速Vxkm/h除以3.6
纵向加速度Axm/s²通常无需转换
横摆角速度deg/s乘以π/180
油门开度0~1无需转换
制动主缸压力MPa取决于制动模型

我在Carsim界面里直接把输出单位改成m/s,省去了Simulink里额外做增益换算的麻烦。如果你用的版本不支持改单位,就在信号进入S函数前加一个Gain模块。

2.2 采样步长匹配和求解器选择

联合仿真的步长匹配是另一个容易翻车的地方。Carsim内部的求解步长通常非常小,我习惯把Simulink模型里的定步长求解器设为0.001秒,这样和Carsim的输出步长可以对齐,但缺点是0.001秒步长下,模型跑完一个30秒仿真要等好几分钟,调试效率很低。

后来我把Simulink步长调大到0.01秒,Carsim在每一个Simulink步长内部会自动做子步进,从现象上看两者可以正常工作,车速曲线没有明显毛刺。需要留意的是,Carsim的S函数模块里有精度设置选项,如果选了快速模式,在一些瞬态比较剧烈的工况下输出可能会有小幅抖动。我最后选择了高精度模式,牺牲一点仿真时间换来稳定输出。

关于求解器类型,Simulink侧必须用定步长求解器(我用的离散求解器),不能用可变步长。原因很简单:MPC的预测周期和采样时间是固定的,离散控制器的逻辑要求每个采样周期严格等长,变步长会增加S函数内部时间处理的复杂度,而且Carsim生成的S函数也不建议搭配变步长求解器使用。

2.3 顶层模型怎么连

搭建联合仿真顶层模型时,我注意到一个容易忽略的细节。Carsim生成的S函数块和自写的MPC S函数块,两者都放在同一个Simulink模型里,但信号连接不能简单直连。原因是Carsim输出的当前车速被MPC用来计算控制量,而控制量又立刻送回Carsim影响下一时刻的车速,如果MPC的S函数被判定为直接馈通且采样时间属性设置不当,Simulink会报告代数环问题。

我的解决方案是在MPC输出端加一个Unit Delay模块,让控制量滞后一个采样周期再送回Carsim。一个采样周期带来的延迟在10ms级别,对纵向控制来说影响很小,但换来的稳定性很值。实际上如果MPC的S函数采样时间明确设置为离散采样,Simulink会自动按离散块处理,代数环问题不一定出现,但加一道Unit Delay能让逻辑更清晰,排查问题的时候也少一些干扰因素。

3. 用2级S函数把MPC滚动优化写进Simulink

3.1 为什么写Level-2而不是Level-1

Simulink里S函数分两种,一级是旧接口,二级是后来推荐的接口。如果只是给一个简单模块封装逻辑,一级S函数也能跑,但做MPC控制器会遇到几个实际痛点。一级S函数的输入输出数据类型支持很有限,对矩阵信号的支持很别扭,而且要手动管理很多内部状态变量,代码结构不清晰。二级S函数提供了setup、Outputs、Update、Terminate等结构化的回调方法,还支持Dwork向量做内部存储,配合Carsim的S函数放在同一个模型里也更稳定。

我这次直接用二级S函数写MPC,理由很实际:MPC控制器内部至少需要保存上一个时刻的优化结果、当前状态估计、可能还有参考速度序列,这些状态放到一级S函数里会非常混乱,而在二级S函数里有专门的Dwork向量管理机制,代码可维护性好很多。二级S函数的错误处理也更自然,仿真中如果检测到输入维度不对,可以明确报错而不是静默出错。

3.2 setup与回调:一个能跑起来的S函数骨架

二级S函数的基本结构是初始化时调用setup,然后注册各个回调函数。我写MPC控制器时,setup部分是这样的:

function setup(block) block.NumInputPorts = 1; block.NumOutputPorts = 1; block.SetPreCompInpPortInfoToDynamic; block.SetPreCompOutPortInfoToDynamic; block.InputPort(1).DatatypeID = 0; % double block.InputPort(1).Dimensions = 2; % [v; v_ref] 两个输入 block.InputPort(1).DirectFeedthrough = 1; % MPC需要直接用当前车速 block.OutputPort(1).DatatypeID = 0; block.OutputPort(1).Dimensions = 1; % 期望加速度 block.SampleTimes = [0.01 0]; % 10ms 采样周期 block.RegBlockMethod('PostPropagationSetup', @PostPropagationSetup); block.RegBlockMethod('InitializeConditions', @InitializeConditions); block.RegBlockMethod('Outputs', @Outputs); block.RegBlockMethod('Update', @Update); block.RegBlockMethod('Terminate', @Terminate); end

这里面有几个关键点。DirectFeedthrough必须设成1,因为MPC的当前输出控制量依赖于当前车速反馈。如果设成0,Simulink会认为这个模块的输出不直接依赖输入,模块调度顺序可能出错,控制量会晚一个采样周期才响应。SampleTimes设成[0.01 0]意味着离散采样,周期10毫秒。我一开始用[0 0]表示连续,结果Simulink在仿真步长一跳时反复调用Outputs,MPC求解次数暴增,仿真慢得离谱,改成离散采样后症状消失。

另外一个需要注意的点是Dwork向量的使用。我要在PostPropagationSetup里注册Dwork向量来保存上一时刻的最优控制序列:

function PostPropagationSetup(block) block.NumDworks = 1; block.Dwork(1).Name = 'u_prev'; block.Dwork(1).Dimensions = Nc; % 控制时域长度 block.Dwork(1).DatatypeID = 0; block.Dwork(1).Complexity = 'Real'; end

这样在Outputs里就可以用block.Dwork(1).Data读取上一拍的控制序列作为热启动初值。

3.3 滚动优化的核心步骤与热启动

Outputs回调函数是MPC控制器的核心。每个采样周期,这个回调要做四件事:读取当前状态输入、构造标准二次规划问题、调用优化求解器、输出第一个控制量。

我把MPC问题的标准形式构造如下。预测模型用离散状态空间方程:

x(k+1) = A_d·x(k) + B_d·u(k)

代价函数:

J = Σᵢ₌₀^{Np−1} [x(k+i|k)ᵀ·Q·x(k+i|k) + Δu(k+i)ᵀ·R·Δu(k+i)]

其中Δu是相邻两个控制量的差值,惩罚Δu可以有效限制冲击度。把状态和控制序列展开,经过简单的矩阵变换,可以改写成标准的QP形式:

min 0.5·Uᵀ·H·U + fᵀ·U s.t. Lb ≤ U ≤ Ub

这里的U是未来Nc步的控制序列。由于预测模型是线性的,H和f都可以在初始化时预处理一部分,在线求解时只需要根据当前状态重新计算f。

实际调用quadprog的片段大致这样:

function u_opt = solve_mpc(x_now, u_prev, params) % params 里包含预处理好的 H, A_d, B_d, 约束矩阵等 U0 = u_prev; % 热启动初值 options = optimoptions('quadprog', 'Display', 'off', ... 'Algorithm', 'interior-point-convex', ... 'MaxIterations', 60); [U, ~, exitflag] = quadprog(params.H, f, ... params.Aineq, params.bineq, ... params.Aeq, params.beq, ... params.lb, params.ub, U0, options); if exitflag > 0 u_opt = U(1); else % 求解失败时的兜底逻辑 u_opt = u_prev(1); % 保持上一拍控制量 end end

两个实操心得。第一,热启动非常重要。第一拍没有上一时刻的解,可以用零向量或者参考加速度作为初值,之后每一拍都用上一次的最优控制序列做初值,quadprog的迭代次数能减少一半以上。第二,一定处理exitflag小于等于0的情况。仿真中难免遇到约束太紧导致无解的时刻,我的兜底逻辑是保持上一拍的控制量,同时在下一次迭代时把约束稍微放宽一点,这样可以避免仿真中途崩溃。

4. 预测时域、权重和约束的整定经验

4.1 先定Np再定Nc

参数整定是MPC项目里最花时间的环节。我的习惯是先把采样周期Ts固定,再定预测时域Np,最后定控制时域Nc。

采样周期要和联合仿真步长匹配,我选了0.01秒,这样Carsim的瞬态响应能保留。纵向速度控制对象的时间常数大约在2到5秒(和车速、车辆质量有关),预测时域至少要覆盖时间常数的1到2倍,也就是总预测长度在4到10秒之间。如果Np取500步,QP问题规模会很大,每个采样周期求解时间不可接受。所以我把控制周期放宽到0.05秒,也就是每50毫秒优化一次,Np取80,总预测长度4秒,计算量大幅下降。

实际整定时,可以按下面这个顺序试:

参数初始尝试值调整方向
采样周期Ts0.01s如果计算量太大,放宽到0.02~0.05s
预测时域Np40步响应慢就增加,计算慢就减少
控制时域NcNp的1/5一般5~15,太大收益有限

Nc取值可以更小一些,因为控制序列后面几步的差异对总体性能影响不大。控制自由度太多反而容易让控制量出现不必要的摆动。

4.2 Q、R参数的调节手感

Q是状态误差权重,R是控制变化权重。调这两个参数其实就是在“让误差更小”和“让动作更平缓”之间找平衡。我给的初始值是Q = diag([1, 1]),R = 0.1,意思是速度误差和加速度误差同等重视,控制变化量稍微约束一下。

如果发现跟踪误差一直偏大,先不要急着加大Q,而是先看是不是模型参数τ给大了。τ直接决定预测模型对车辆响应速度的估计,如果τ估得过大,MPC会认为车辆响应很慢,控制量偏保守,实际车辆响应快,就会出现误差偏大但控制动作很稳的状态。

Q和R的调整效果大致是:

调节动作观察到的效果典型副作用
Q增大误差峰值减小控制量波动加剧、冲击度变大
R增大控制量更平滑响应变慢、误差增大
Q和R同时增大无明显变化需要注意数量级比例

出现高频小幅抖动时,优先增加R而不是减少Q。减少Q虽然也能减弱抖动,但会让误差整体抬高。抖动多半是控制量变化太快造成的,加大R可以直接限制Δu。

4.3 约束的软硬说法

加速度约束我设置为±3 m/s²,这是舒适性范围内比较通用的一个值。冲击度通过限制Δu来间接约束,比如每个周期控制量变化不超过0.5 m/s²。油门开度约束则是0到1之间的物理边界。

一开始我把所有这些约束都写成硬约束,仿真在大多数工况下没问题,但遇到参考速度突变特别剧烈的场景,优化器会出现无解。原因很简单:参考速度一步降了20 km/h,预测模型算出来即使加速度到达上限也无法在有限时域内跟上,这时候硬约束和动态模型之间产生矛盾,QP就找不到可行解。

解决办法是把加速度约束从硬约束改成软约束。具体做法是在代价函数里增加松弛变量ξ,约束变成:

a_min − ξ ≤ a ≤ a_max + ξ

然后代价函数里加上对ξ的惩罚,权重设得很大但有限。这样优化器在极端工况下可以“稍微”超出加速度约束换取可解性,正常情况下由于惩罚权重高,不会主动触碰约束边界。这个改动让仿真在各种极端工况下都没有再崩溃过。

5. 仿真跑了哪些工况,结果怎么评估

5.1 三个典型工况设计

联合仿真链路搭建好之后,我设计了三个典型工况验证MPC控制器的性能。

第一个是阶跃工况:车辆从30 km/h开始,2秒时参考速度阶跃到60 km/h,观察跟踪的上升时间、超调量和稳态精度。这个工况用来验证MPC对阶跃变化的响应能力。

第二个是坡度干扰工况:让Carsim道路在前10秒保持平路,10秒后开始进入5%上坡并持续5秒,再过渡到平路。这个工况用来验证外部扰动的抑制能力,坡度阻力变化对车速的影响在仿真中会被真实车辆模型放大,控制器的鲁棒性可以看得很清楚。

第三个是正弦参考速度工况:参考速度在40到60 km/h之间按正弦变化,周期10秒。这个工况更接近实际驾驶中的动态任务,能暴露控制器在高频跟踪下的相位滞后和控制振荡问题。

5.2 结果分析和数据对比

为了有个参照,我把同一个车辆模型里的控制器换成之前调好的PID,用相同工况跑了一遍。结果对比大致如下:

工况MPC最大误差(km/h)PID最大误差(km/h)
阶跃30→60 km/h0.451.30
5%坡度干扰0.602.10
正弦跟踪0.351.10

从数据上看,MPC在三个工况下的最大误差都明显小于PID,尤其是在坡度干扰下,PID的误差超过2 km/h,而MPC把误差压在0.6 km/h以内。原因不难解释:PID在受到坡度干扰后要先看到速度跌落才能加大油门,响应滞后;而MPC通过预测模型能提前感知到坡度阻力对车速的影响趋势,控制量可以更早地做出调整。

另外一个值得关注的点是控制量的平顺度。PID在阶跃工况下出现了典型的超调后回摆,油门开度曲线有来回波动;MPC的输出则是一条平滑上升的曲线,基本贴着加速度约束边界走,没有多余的振荡。从车上乘员的角度感受差异会很明显,这也是MPC的舒适性优势所在。

5.3 从仿真到实车还要注意什么

仿真结果看起来不错,但距离实车应用还有几个必须补上的环节。执行器延迟是第一个问题,真实油门和制动系统存在几十毫秒到上百毫秒的延迟,我在S函数里额外加了一个延迟环节模拟这种特性,结果发现Np需要适当加大才能维持稳定。传感器噪声是第二个问题,仿真里车速信号是理想值,实车上速度传感器有噪声和采样延迟,要么在信号进入MPC前做好滤波,要么在状态估计里加入观测器。第三个是模型参数的不确定性,τ和车辆质量在不同工况下会变化,更严谨的做法是在MPC外层再加一个参数辨识模块,但那是另一个更大的话题了。

6. 联合仿真中的踩坑记录与排查链路

6.1 速度单位不统一导致状态量飞掉

仿真第一次跑通后,车速曲线看起来没问题,但MPC输出的期望加速度一直偏大,误差始终压不下去。我先怀疑是参数整定问题,试了好几组Np和Q都没改善。后来把Scope接到MPC的输入端口上,发现Carsim输出的Vx是52.6,而Simulink模型里期望速度是15 m/s,数值差了离谱。仔细查了Carsim界面,发现Vx的单位仍然是km/h,15 m/s对应54 km/h,误差源就在这里。

排查这类问题,建议先手动给Carsim一个恒定油门输入,让车速达到某个稳定值,然后检查Simulink侧接收到的数值和Carsim界面上显示的是否一致。如果不一致,优先检查单位换算和信号缩放比例。一次看似复杂的控制问题,最后只是因为一个3.6的系数。

6.2 direct feedthrough与代数环的连锁反应

有一次我把MPC的SampleTimes设成连续采样时间,结果Simulink报出代数环警告,仿真速度骤降。我当时还不太明白原因,后来理解了:Carsim S函数的输出和MPC S函数的输入之间存在瞬时依赖关系,而MPC的输出又立刻影响Carsim下一时刻的输出。连续采样下,Simulink需要在一个步长内迭代求解这个循环,代数环求解器反复迭代,仿真效率自然掉下来。

排查过程是这样的:我先用Simulink的代数环高亮工具标出了循环路径,发现就在Carsim块和MPC块之间。解决办法有两个,要么把MPC改成离散采样时间,要么在反馈路径上加Unit Delay。我两个都做了:MPC保持离散采样,输出端再加Unit Delay,代数环警告彻底消失,仿真速度恢复正常。

6.3 优化求解失败与无解:约束过硬的补救

仿真跑到第14秒时,quadprog偶尔报错,说问题不可行。我最初以为是代码写错了,反复检查约束矩阵之后发现没有问题。后来在Outputs里加了exitflag监控,看到确实存在某些采样周期优化器返回的exitflag值小于等于0。

问题出在硬约束和极限工况的冲突上。参考速度在阶跃工况里从60降到30,一步变化太大,预测模型在满足加速度硬约束的情况下,确实无法在预测时域内让状态落到参考值附近。处理方式是前面提到的软约束方案,对加速度边界引入松弛变量并加大惩罚,同时对控制变化量保留硬约束,这样极端工况下优化器总能找到解,控制器的输出行为也更平滑。

6.4 初值不一致:MPC状态和Carsim初始速度的握手

这个问题发生在把参考速度从0起步改成30 km/h起步之后。仿真曲线显示MPC在仿真刚开始的0.5秒内输出了一小段异常控制量,随后恢复正常。排查发现是MPC的内部预测状态x1在InitializeConditions里初始化为0,但Carsim初始车速是30 km/h,期望车速也是30 km/h,速度误差实际就是0。理论上0没毛病,坏在加速度状态x2初始化为0,而Carsim车辆起步阶段实际加速度不为0,导致预测模型的初值和真实状态对不上。

解决方式是在InitializeConditions阶段,把Carsim输出的初始车速和初始加速度读出来,赋值给MPC内部状态。具体做法是让MPC的第一个输入端口同时在初始化阶段可读,或者直接在Carsim参数里把初始速度设为固定值,并在S函数初始化时用该值对齐。这个坑很容易被忽略,因为只影响仿真最初的一小段,但如果你关心起步阶段的跟踪性能,就必须处理。

最后再分享一个调参和排查的体会。MPC控制器参数多,耦合性强,最容易陷入“同时动两个参数然后看效果”的误区。我个人的做法是严格遵守单参数调试原则:固定Np和Nc,只调Q和R;调R时保持Q不动,每次只改一个数量级,记录最大误差、超调量、稳定时间和控制量变化幅度这几个指标。靠这套流程,我大约用了两天时间就把参数确定下来,后续换工况基本不需要大改。这套联合仿真链路跑通之后,任何纵向参考曲线都可以在这个框架上快速验证,后续再往横摆控制、协同控制方向扩展,MPC的架构优势会体现得更明显。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询