☰
三车队列DMPC纵向协同控制MATLAB源码拆解与避坑
2026/10/23 12:21:49 网站建设 项目流程

简介:面向自动驾驶与车辆控制领域的学习者与研发人员,这份Matlab源码围绕三辆车的队列协同,给出分布式模型预测控制的实现示例,可用于理解多车队列在复杂交通环境下的稳定性与协同性设计。压缩包共2个文件,体积约2KB,其中.m文件承载核心算法与仿真入口,.txt文件提供配套说明,便于快速定位运行方式与参数含义。目前已有366人学习下载。读者可借此梳理分布式MPC在多车系统中的建模、预测、滚动优化与约束处理思路,观察三车如何通过局部信息交互实现队列协调,并在此基础上调整权重、预测时域或通信拓扑开展复现实验;说明文档也有助于减少环境配置和代码阅读中的障碍。适合作为课程设计、算法预研和智能交通系统构建的轻量参考。

1. 三辆车跟车,为什么头车一脚刹车后面就乱

某实验室的三车队列测试里,头车以 60 km/h 巡航,前方突然降到 40,第二辆车在 0.8 秒后跟上减速,第三辆车已经贴到 3 米以内——这是典型的队列纵向协同失效。集中式模型预测控制算得准,但三辆车一个 QP、通信全连通,扩到八辆车计算量就爆炸。这个 MATLAB 源码包做的事情很直接:把一个大优化问题拆成三个小 QP,用分布式模型预测控制(DMPC)解三车队列的间距保持与速度协同。它适合做自动驾驶纵向控制、车辆编队、分布式控制课程设计的人,跑通之后能清楚看到每辆车的预测时域、约束边界和邻居信息是怎么进代价函数的。

2. 三车队列的骨架:目录结构、车辆模型与参数配置

2.1 目录结构与各文件职责

拿到源码包先看目录,比急着按运行键有用。这类队列控制项目一般按「模型 / 优化 / 仿真」三层切分,每层只干一件事,改动的时候不容易牵一发动全身。我拆过的几个版本基本都长这样:

platoon_dmpc/ ├── main.m 仿真入口,跑完直接出图 ├── init_params.m 所有参数集中在这里,改车队规模改这里 ├── model/ │ ├── veh_model.m 三阶纵向动力学 + 离散化 │ └── spacing_policy.m 期望间距策略(固定间距 / 固定时距) ├── mpc/ │ ├── build_prediction.m 构造预测矩阵 Phi、Gamma │ ├── build_qp.m 代价函数展开成标准二次型 │ ├── solve_dmpc.m 迭代协调 + quadprog 求解 │ └── neighbor_info.m 从邻车取预测加速度序列 ├── sim/ │ ├── run_closed_loop.m 滚动时域主循环 │ └── plot_result.m 间距、速度、加速度三张图 └── config/ └── topo_3veh.m 三车通信拓扑与权重

数据流是单向的:main.m调init_params.m拿到结构体prm,交给run_closed_loop.m;循环里每辆车调一次solve_dmpc.m,后者先问neighbor_info.m要邻居上一轮的预测加速度,再让build_prediction.m和build_qp.m拼出 H、f、A、b,扔给quadprog;解出来的第一个控制量施加到车上,状态推进一步,进下一拍。把这条链路记住,后面改哪一块心里都有数。

2.2 三阶纵向动力学模型与离散化

队列控制里车辆模型的选择直接决定仿真像不像真车。二阶模型(状态只有位置和速度,加速度当输入)写起来省事,但把执行器滞后整个忽略了,做到 0.1 秒采样时后车会明显抖——这是踩过的坑。这个包用的是三阶模型,把加速度也当状态,输入是「期望加速度」,经过一个一阶惯性环节才变成实际加速度,时间常数 τ 取 0.5 秒左右,接近乘用车纵向执行器的响应。

连续时间下三阶模型的写法是位置导数等于速度、速度导数等于加速度、加速度导数等于 (-a + u)/τ。离散化用前向欧拉就够,采样 0.1 秒时精度完全够用,没必要上矩阵指数,那样反而让不熟 MATLAB 的人看代码时卡住。

function [Ad, Bd, Cd] = veh_model(Ts, tau) % 三阶纵向动力学:状态 [位置; 速度; 加速度],输入为期望加速度 % 连续方程: dp = v, dv = a, da = (-a + u)/tau Ac = [0, 1, 0; 0, 0, 1; 0, 0, -1/tau]; Bc = [0; 0; 1/tau]; Cc = [1, 0, 0]; % 前向欧拉离散,Ts 取 0.05~0.1 时误差可接受 Ad = eye(3) + Ts * Ac; Bd = Ts * Bc; Cd = Cc; end

tau是这套模型里最敏感的参数。设太小(0.1 秒)等于假设车能瞬间响应,仿真里间距曲线漂亮得不真实;设太大(1.0 秒以上)队列会明显迟钝,后车容易压到安全距离。我一般先在 0.3 到 0.7 之间扫一遍,看间距误差峰值落在哪,再定死。Cd只取位置输出,因为间距误差最终是用两车位置差算的,速度差另外单独取,这一点在下一章的增广状态里会讲清楚。

2.3 参数配置与期望间距策略

参数全部塞在init_params.m里,返回一个结构体。这种写法看着土,但改起来最省事——换车队规模、换权重、换约束,只动一个文件,不用满工程搜数字。核心参数如下:

参数含义典型取值
Ts采样周期0.1 s
tau执行器时间常数0.5 s
Np预测时域20 步(2 s)
Nc控制时域5 步
L车长4.5 m
d0静止安全间距5 m
h固定时距1.2 s
Q间距/速度误差权重diag([1, 0.5])
R控制量权重0.1
S控制增量权重0.05
a_lim加速度约束[-3, 2] m/s²
u_lim期望加速度约束[-4, 2.5] m/s²

期望间距用固定时距(CTH)比固定间距更贴近实际:d_des = d0 + h * v_i。车速越高跟得越远,这符合驾驶习惯,也能避免高速时安全裕度不够。spacing_policy.m里就三行,但它是整个代价函数的参考值来源,符号搞反的话后面所有调试都是白费力气。

预测时域 20 步覆盖 2 秒,是为了一辆以 60 km/h 行驶的车在 2 秒内能把速度降下 15 m/s 左右,这个窗口足够 MPC「看见」前车的减速动作。控制时域压到 5 步是为了减少决策变量个数,三辆车每辆 5 个变量,QP 规模小,单步求解在 10 ms 量级,实时性有余量。这个「Np 长、Nc 短」的搭配是队列控制里的常规做法,别把两个都设成 20,那样求解时间会翻好几倍而效果提升有限。

3. 分布式 MPC 的核心:预测矩阵、代价函数与 QP 构造

3.1 相对状态增广:把前车加速度当成可测扰动

如果每辆车只优化自己的绝对位置和速度,那三辆车就是三个互不相干的 MPC,间距根本约束不住。正确做法是把「与前车的相对量」做成状态。定义间距误差e_i = p_{i-1} - p_i - L - d_des,速度误差dv_i = v_{i-1} - v_i,再拼上本车加速度a_i和前车加速度a_{i-1},得到一个四维增广状态:

z_i = [e_i; dv_i; a_i; a_{i-1}]

前车加速度进状态,作用是把它当成一个「可测扰动」。在预测时域内先假设它保持不变,写出转移矩阵:

function [Az, Bz, Cz] = aug_model(Ad, Bd, Ts, tau) % 增广状态 z = [间距误差; 速度误差; 本车加速度; 前车加速度] Az = [1, Ts, -Ts^2/2, Ts^2/2; 0, 1, -Ts, Ts; 0, 0, 1-Ts/tau, 0; 0, 0, 0, 1]; Bz = [0; 0; Ts/tau; 0]; Cz = [1, 0, 0, 0; % 输出间距误差 0, 1, 0, 0]; % 输出速度误差 end

Az的第一行是从运动学推出来的:间距误差的变化量等于速度误差乘 Ts,再减去两车加速度差带来的二阶项。第二行是速度误差的变化量。第三行是本车加速度自己的一阶惯性收敛。第四行恒等,对应「前车加速度在预测窗口内不变」这个假设。Bz只有第三个分量非零,因为控制输入只影响本车加速度。

这个假设是整个分布式架构的代价:前车其实也在动,它的加速度在预测窗口里是变的。单靠这一步,后车算出来的最优解会偏保守。第 4 章的迭代协调就是专门修这个偏差的,别在这里就想着把模型做复杂,那样会失去分布式拆分带来的计算优势。

3.2 预测方程与 Gamma 矩阵的块构造

有了Az、Bz,把未来 Np 步的状态写成当前状态加控制序列的线性组合:Z = Phi * z0 + Gamma * U。Phi是 Az 的幂次堆叠,Gamma是下三角的分块 Toeplitz 矩阵,第 k 行第 j 列块表示第 j 个控制量对第 k 步状态的影响,等于Az^(k-j) * Bz。

手写双重循环最直观,也最不容易写错。当控制时域 Nc 小于预测时域 Np 时,Nc 之后的控制量保持为最后一个,用递推方式处理:

function [Phi, Gam] = build_prediction(Az, Bz, Np, Nc) n = size(Az, 1); m = size(Bz, 2); Phi = zeros(n*Np, n); Gam = zeros(n*Np, m*Nc); for k = 1:Np Phi((k-1)*n+1 : k*n, :) = Az^k; if k == 1 Gam(1:n, 1:m) = Bz; else % 先整体左乘 Az,相当于把上一行块往前推一步 Gam((k-1)*n+1 : k*n, :) = Az * Gam((k-2)*n+1 : (k-1)*n, :); if k <= Nc % 只有在前 Nc 步内才新增一个决策变量 Gam((k-1)*n+1 : k*n, (k-1)*m+1 : k*m) = Bz; end end end end

k从 1 到 Np 逐行填。第一行只有第一个控制块是Bz,其余为零。之后每一行先把上一行整体左乘Az,再在k <= Nc时把第 k 个块列设成Bz。这样当k > Nc时不再引入新变量,控制量自然保持住,等价于「最后一个控制量持续施加」。

这段代码看着短,但它是整个 MPC 里最容易写错的地方。常见错误是把Az的幂次写反,或者忘了Nc < Np时的保持逻辑,结果仿真出来车头的响应总是提前一步。写完拿Np=3, Nc=2手算一遍前三行去对,五分钟能省掉半天的调试。

3.3 代价函数展开成标准二次型

代价函数是三项之和:跟踪间距和速度误差、惩罚控制量幅值、惩罚控制量增量。写成矩阵形式是

J = (Z - Ref)' * Qbar * (Z - Ref) + U' * Rbar * U + (D*U - d0)' * Sbar * (D*U - d0)

Qbar是按预测步数块对角堆叠的Q,Rbar、Sbar同理。D是差分矩阵,用来把绝对控制量变成增量,d0里装的是上一拍实际施加的控制量,这样第一拍的增量惩罚才有参照。

把Z = Phi*z0 + Gamma*U代进去,按 U 整理,得到标准形式J = 0.5*U'*H*U + f'*U + const:

function [H, f] = build_qp(Phi, Gam, Cz, Qbar, Rbar, Sbar, z0, ref, D, d0) % 输出矩阵在预测时域上堆叠 Cbar = kron(eye(size(Phi,1)/size(Cz,1)), Cz); G = Cbar * Gam; PhiY = Cbar * Phi; Ref = repmat(ref, size(Phi,1)/size(Cz,1), 1); H = 2 * (G' * Qbar * G + Rbar + D' * Sbar * D); f = 2 * (G' * Qbar * (PhiY * z0 - Ref) - D' * Sbar * d0); % 数值正则化,避免 quadprog 报非正定 H = H + 1e-8 * eye(size(H)); end

Cbar把每步的二维输出扩展到2*Np维,kron加单位阵就能做到,比手写循环快。Ref是参考值,间距误差的参考恒为 0(保持期望间距),速度误差的参考也是 0(跟上前车速度)。H前面乘 2 是因为quadprog求解的是0.5*x'Hx + f'x形式,两边要对齐,这个系数错了的话求解出来的控制量会整体偏大一倍,而且从曲线上看不出明显异常,只是车跟得特别「急」。

最后那行 1e-8 正则化是我后加的。理论上G'*Qbar*G是半正定的,但浮点运算累积误差会让quadprog偶尔报 exitflag = -2,加一点点单位阵就能压住,代价可以忽略。

3.4 约束拼装与 quadprog 调用

约束有三类:间距误差不能小于负的安全裕度(也就是不能贴太近)、加速度不能超限、期望加速度不能超限。前两类都是状态约束,要转成对 U 的线性不等式。

function [Aineq, bineq, lb, ub] = build_constraints(Phi, Gam, z0, Np, n, lim) % 只约束间距误差,不约束速度误差 e_row = zeros(1, n); e_row(1) = 1; E = kron(eye(Np), e_row); % 间距误差下限:p_{i-1} - p_i - L - d_des >= -d_safe Aineq = -E * Gam; bineq = E * Phi * z0 + lim.d_safe * ones(Np, 1); lb = lim.u_min * ones(Np, 1); ub = lim.u_max * ones(Np, 1); end

d_safe取 3 到 5 米,表示允许贴到的极限距离。注意这里只约束了间距误差的下限,没有约束上限——后车落后太多不构成危险,强行约束反而会让 QP 不可行。加速度约束通过lb、ub施加在控制量上,比转成状态约束更直接,quadprog处理边界比处理一般不等式快。

求解调用很短:

opts = optimoptions('quadprog', 'Display', 'off', 'Algorithm', 'interior-point-convex'); [U, fval, exitflag] = quadprog(H, f, Aineq, bineq, [], [], lb, ub, [], opts); if exitflag <= 0 U = zeros(size(f)); % 求解失败就退回零控制,别让 NaN 污染状态 end

exitflag一定要判。曾经有一次约束冲突导致返回全 NaN,NaN 又进了车辆状态,整个仿真跑出满屏的NaN曲线,查了两个小时才发现是这里没兜住。退化成零控制虽然不优,但至少不会把仿真炸掉。

4. 通信拓扑与迭代协调:邻车信息怎么进代价函数

4.1 三车拓扑的邻接矩阵与权重

分布式的前提是每辆车只能拿到邻居的信息。三车队列里定义领航车为 1 号,跟随车为 2、3 号。邻接矩阵W的第 i 行第 j 列表示「i 车能从 j 车拿到信息」,取 1 表示有链路,0 表示没有:

function W = topo_3veh() % 1 领航,2、3 跟随;车道内单向通信,允许后车听前车 W = [0, 0, 0; % 领航车不听任何车,只跟参考速度曲线 1, 0, 0; % 2 号车只拿 1 号车的信息 0, 1, 0]; % 3 号车只拿 2 号车的信息 end

这是最稀疏的链式拓扑,也是实际车队里最常见的一种,因为车际通信的带宽和可靠性都不允许全连通。稀疏拓扑的代价是信息传播慢:1 号车刹车,2 号车一拍之后知道,3 号车要两拍。预测时域 20 步正好能覆盖这个传播延迟,这也是前面Np不能设太短的原因之一。

有些版本会用双向拓扑(后车也把状态发给前车),这样前车能「照顾」后车的跟车能力,间距误差峰值能降一些。代价是通信量翻倍,而且引入耦合后迭代收敛变慢。做纯跟随场景用单向链式就够,别一上来就上全连通。

4.2 Jacobi 式迭代:两轮交换就够

第 3 章留了个问题:预测时域内把前车加速度当成常数,可前车明明也在动。修法是迭代——每辆车先按上一轮的猜测算一遍,把算出来的加速度序列广播给邻居,下一轮邻居用这个新序列重新组装扰动,再解一次。

function [U, A_pred] = solve_dmpc(z0, prm, A_guess) A_pred = repmat(A_guess, 1, prm.Np); % 初始猜测:上一拍的加速度保持不变 for iter = 1:prm.maxIter % 用邻居的预测加速度序列,替换掉 Az 第四行的常数假设 [Phi, Gam, dvec] = build_prediction_with_disturbance(prm, A_pred); [H, f] = build_qp(Phi, Gam, prm.Cz, prm.Qbar, prm.Rbar, ... prm.Sbar, z0, prm.ref, prm.D, prm.d0); [H, f] = fold_disturbance(H, f, Gam, dvec, prm.Qbar); [Aineq, bineq, lb, ub] = build_constraints(Phi, Gam, z0, prm.Np, 4, prm.lim); U = quadprog(H, f, Aineq, bineq, [], [], lb, ub, [], prm.opts); if isempty(U), U = zeros(prm.Nc, 1); end A_new = simulate_accel_seq(U, z0(3), prm); if norm(A_new(:) - A_pred(:), inf) < prm.tol, break; end A_pred = 0.5 * A_pred + 0.5 * A_new; % 松弛更新,防止来回震荡 end end

几个关键点。A_guess是上一拍的实际加速度,用它当初始猜测比用零好得多,通常一两轮就收敛。maxIter设 3 到 5,别设太大,三车规模下两轮足够,再迭代收益很小。tol取 1e-3 量级,对应加速度的收敛精度。

0.5那个松弛系数是血泪经验。直接用新序列替换旧序列,在第 3 号车上容易出现两轮之间来回跳的现象——这一轮假设前车猛减速所以自己保守,下一轮前车发现自己被假设减速了于是调整,反复横跳,加速度曲线出来是锯齿状。加个 0.5 的阻尼,收敛慢一点但曲线干净。

4.3 主循环:滚动时域一步一步推

外层仿真循环负责把上面的东西串起来。领航车不走 MPC,直接跟一条预设的速度曲线;跟随车逐个求解,注意顺序——第 2 号车解完再解第 3 号车,这样 3 号车能立刻拿到 2 号车这一拍的最新预测,比并行求解收敛更快。

x = prm.x0; % N 辆车 × 3 维状态 A_hist = zeros(prm.N, prm.Np); % 上一拍的预测加速度,做迭代初值 U_last = zeros(prm.N, 1); % 上一拍实际施加的控制量 for k = 1:round(prm.T_total / prm.Ts) % 领航车跟踪参考速度 u1 = prm.kp * (prm.v_ref(k) - x(1, 2)); x(1, :) = (prm.Ad * x(1, :)' + prm.Bd * u1)'; for i = 2:prm.N % 组装增广状态 e = x(i-1,1) - x(i,1) - prm.L - (prm.d0 + prm.h * x(i,2)); dv = x(i-1,2) - x(i,2); z0 = [e; dv; x(i,3); x(i-1,3)]; [U, A_hist(i,:)] = solve_dmpc(z0, prm, A_hist(i,1)); u = U(1); % 只施加第一个 x(i, :) = (prm.Ad * x(i, :)' + prm.Bd * u)'; U_last(i) = u; end prm.d0 = U_last; % 增量惩罚用 log(k, :) = x(:)'; end

prm.d0每拍更新成上一拍的控制量,喂给增量惩罚项,这个细节漏了的话第一拍的增量惩罚会一直用初始零值,导致起步瞬间控制量冲得特别猛。日志log存全部状态,plot_result.m直接拿它画三条曲线:间距误差、速度、加速度。看间距误差曲线是最快的体检方式,收敛到零附近且没有持续振荡,基本就说明参数配得没大问题。

5. 避坑与排查:跑不通先看这五处

5.1 间距误差符号搞反,车越跟越远

现象:仿真跑起来三辆车越跑越散,间距一路涨到几百米,但速度曲线看着正常。

原因:e = p_{i-1} - p_i - L - d_des里前车减后车,改成后车减前车之后,代价函数里的参考方向还是按原来的写,MPC 就朝着「拉大间距」的方向优化,而且因为约束只卡下限,它拉大间距完全不违反约束,一路放飞。

解决:在init_params.m里加一句断言,assert(e > 0, 'e 应为正表示距离大于期望')这种思路,早期就拦住。符号约定只在一处定义,其他文件全部引用,不要每个函数里各写一遍。

5.2 quadprog 返回空解,曲线全是 NaN

现象:跑到某一拍U变成空矩阵或 NaN,之后所有状态都是 NaN,图上什么都没有。

原因:两种情况最常见。一是间距约束和安全间距撞车,前车急刹时后车在物理上已经无法满足d_safe,QP 不可行;二是H因为数值误差变成非正定,求解器直接放弃。

解决:分两步。H加1e-8 * eye(size(H))正则化;把d_safe从硬约束改成带松弛变量的软约束,松弛项加大权重惩罚,这样极端工况下求解器总能给出一个「尽力而为」的解,而不是直接崩掉。另外exitflag <= 0时退化成零控制,别让 NaN 进状态。

5.3 队列出现固定频率的振荡

现象:间距误差曲线上叠加了一个 0.4 到 0.6 Hz 的正弦,幅度不大但不衰减,怎么调权重都压不下去。

原因:tau和实际执行器的响应不匹配。tau设得太小,MPC 以为车能立刻响应,于是给出比较激进的控制量,实际执行器跟不上,等响应过来时已经过头了,下一拍再往回拉,形成极限环。

解决:把tau提到接近实际值,或者在控制量上加一阶低通滤波。我一般会把tau在 0.3 到 0.7 之间扫一遍,看哪个值下振荡幅度最小,通常 0.5 附近就干净了。别指望靠调Q、R解决这个问题,那只是把振荡掩盖住。

5.4 仿真很漂亮,加个通信延迟就崩

现象:理想仿真里间距误差峰值 0.3 米,加 100 ms 通信延迟之后峰值窜到 2 米以上,甚至追尾。

原因:迭代协调里邻居的预测加速度是同一时刻拿到的,隐含假设通信零延迟。实际链路有延迟时,后车用的是过时信息,前车已经在减速了它还在按老数据算。

解决:在neighbor_info.m里加一个长度为 1 到 2 的延迟缓冲区,后车取 N 拍之前的数据;同时把预测时域加长到覆盖延迟,Np从 20 提到 25。想做得更细的话,用状态预估补上延迟这段时间的变化,效果会好不少。

5.5 权重怎么调都不对劲

现象:Q里的间距误差权重调大,超调;调小,跟不住。反复横跳,找不到舒服的点。

原因:量纲不一致。间距误差单位是米,数值在 0 到 10 之间;速度误差单位是 m/s,数值在 0 到 15 之间;控制量是 m/s²,数值在 -4 到 2 之间。直接拿这几个数当权重,等于在比谁的数量级大。

解决:先归一化。Q = diag([1/d_max^2, 1/dv_max^2]),把每个误差除以其允许的最大值,控制量同理,然后再调相对比例。归一化之后权重就有物理意义了,调起来是单调的,不用再靠玄学试。

6. 二次开发:从三车扩到五车,以及换掉线性模型

扩车队规模是这套代码最常见的改造需求。改动点比想象中少:topo_3veh.m里把 3×3 的邻接矩阵换成 5×5,prm.N改成 5,主循环的for i = 2:prm.N不用动,代价函数一个字都不用改。真正要跟着调的是预测时域——链式拓扑下信息每传一辆车延迟一拍,五辆车首尾相隔四跳,Np至少得从 20 提到 30 才能让最后一辆「看见」头车的动作。

换非线性模型是另一个方向。三阶线性模型在常规工况下够用,但急加速、大坡度时误差明显。改法是把veh_model.m换成一个非线性函数,build_prediction.m里的线性预测矩阵失效,需要用fmincon直接对控制序列做非线性优化,或者每步在当前工作点重新线性化。前者代码改动小、求解慢,后者快但实现复杂。规模不大时我倾向fmincon,先在prm里加个开关,两种求解器并存,对比着看。

验证改造是否成功,我一般跑三条固定的速度曲线:匀速巡航、正弦波动、阶跃减速,各跑一次,记录三项指标。

指标合格线说明
间距误差峰值< 1.0 m阶跃工况下
收敛时间< 4 s从扰动到回到 ±0.2 m
单步求解耗时< 15 ms五车规模

三项都过,再去看加速度曲线有没有锯齿——锯齿说明迭代协调在振荡,回去把松弛系数调小一点。曾经有一次扩到五车之后间距曲线看着挺好,结果加速度曲线是锯齿状的,查了半天才发现是maxIter设成了 8,迭代次数多了反而在两组解之间来回跳。从那以后我每次改车队规模,都会先把加速度曲线拉出来单独看一眼,再去看间距——间距是给人看的,加速度才是给执行器看的。希望这些能帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询