MPC轨迹跟踪这几年基本成了自动驾驶控制模块里的“标准答案”。我见过的项目里,太多类似的场景:参考线画得漂漂亮亮,全局规划也有模有样,结果车一跑起来,要么在弯道里直接切掉半条车道,要么压着中线左右扭成蛇形。
问题不在路线,而在底层的跟踪控制。今天要聊的MPC(Model Predictive Control,模型预测控制),就是专门治“路线明明画好了,车却不听使唤”这个毛病的。这篇文章不是教科书复读,我会把自己调车控时踩过的坑、反复试出来的参数规律、以及真正能改完就跑的代码思路全部摊开讲,适合正在搞自动驾驶决策控制、机器人路径跟踪,或者准备入门预测控制的同学。
1. 先聊清楚:MPC到底在解决什么难题
1.1 轨迹跟踪的“病根”:有目标不等于能跟住
很多人一开始觉得轨迹跟踪就是个“导航问题”:把参考线给到控制器,车跟着点走就行。可真跑起来就发现不对,这里有个非常微妙的前提被忽略了——车辆是一个非完整约束系统。
什么意思呢?车的轮子只能沿着车身方向滚动,不能横着平移。你想让车从偏离参考线0.5米的地方“平移”回线上,它必须绕一个弧线拐回去,不能像无人机那样直接横向飘。再加上方向盘转角是有限制的、转向执行机构还有响应延迟,问题就变成:在每一步动作都受限的情况下,如何用一连串的转向和加减速,让车辆状态(位置、航向、速度)贴着参考线走。
这个问题的本质是序列决策。我经常用一个“走迷宫三步”的类比:站在迷宫岔路口,只看脚下一步的人,容易越走越偏;能看到未来三格、并且提前规划好三步落脚点的人,路径又稳又准。PID这类控制器就像前者——只盯着当前误差使劲;MPC是后者——它看未来一小段道路,并把“未来会怎么偏”纳入当前决策。
1.2 MPC和PID、纯跟踪的本质区别在哪
三个主流跟踪方案放在一起去比,差异非常明显。
| 方案 | 核心原理 | 前瞻性 | 约束处理 | 实时性 | 适用场景 |
|---|---|---|---|---|---|
| PID | 对横向偏差做比例-积分-微分矫正 | 无,只看当前误差 | 不方便处理 | 极快 | 简易循迹、低速辅助 |
| 纯跟踪 | 根据前瞻距离选取前方目标点,按圆弧跟踪 | 有前瞻,但只看一个点 | 难以整合约束 | 极快 | 低速园区车、简单路径 |
| MPC | 用模型预测未来N步状态,在线求解带约束的最优控制序列 | 完整看未来一段轨迹 | 天然支持约束 | 中等偏慢,但可优化 | 高速、复杂场景、需要兼顾精度与舒适性 |
这里有一个我踩过的大坑:用纯跟踪做低速测试时,效果其实过得去,前瞻距离调好之后小弯也贴得住。但一旦速度上到40公里每小时,并且路径是连续S弯,纯跟踪的几何关系就跟不上了——它只盯着前方一个点,点之间的大趋势它看不出来,于是车辆过弯会呈现明显的“甩尾式”滞后。PID更不用提,高速下如果比例增益调得稍大,车头在直线末端会持续摆动,光调参数就能耗掉你一周。
MPC之所以能跳出这个坑,核心就四个字:模型预测。它用车辆模型算出一段未来轨迹,然后选择一个折中了跟踪误差和控制代价的最优控制动作。它不怕弯道,因为它“看得到”弯道在前面拐了。
2. MPC的“三件套”:模型、预测、滚动优化
想把MPC玩明白,离不开三个核心组件:预测模型、目标函数、约束条件。三者配合,才构成完整闭环。
2.1 车辆模型怎么选:先背熟自行车模型
MPC的预测能力全部来自模型。模型选得好不好,直接决定控制效果。最常用的是自行车模型(Bicycle Model),把左右前轮合并成前轮,左右后轮合并成后轮,得到一个简化但足够准确的几何模型。
状态量通常取:
x:车辆在世界坐标系下的横坐标 y:车辆在世界坐标系下的纵坐标 yaw:横摆角(航向角) v:纵向速度控制量取:
delta:前轮转角 a:纵向加速度连续运动学方程如下:
dx/dt = v * cos(yaw) dy/dt = v * sin(yaw) dyaw/dt = v * tan(delta) / L dv/dt = a其中L是轴距。这个模型表达的逻辑很直白:车头方向决定了纵向速度的分解方向,前轮转角决定了转圈的曲率,曲率半径等于轴距除以tan(delta)。
注意:这个运动学模型只在低速下失准有限,中高速时轮胎侧偏效应增强,最好换动力学自行车模型,状态里加上横向速度和横摆角速度。怎么判断临界点?一般20公里每小时以下,运动学模型够用;超过了,跟踪精度下降时果断换动力学模型,别硬顶。
2.2 滚动时域优化:为什么每次都推倒重来
MPC的精髓其实不是“做了一次预测优化”,而是“反反复复做预测优化”。在每个控制周期,控制器都会做四件事:
- 获取当前车辆状态;
- 用模型预测未来Np步(Np是预测时域)的系统响应;
- 在线求解一个带约束的最优控制问题,得到未来Nc步(控制时域)的控制序列;
- 只执行第一步控制量,下一周期重复以上过程。
为什么只执行第一步、然后滚动重算?因为模型有误差、实际道路有扰动、执行有延迟。如果第一次规划后就开环执行到底,模型偏差会积少成多,最后彻底失控。滚动优化相当于每步都重新核对一遍“计划跟现实之间的差距”,本质是一种带前瞻的反馈修正。
用一个生活类比:你导航开车,不会在出发时背下全部路线然后闭眼开。而是每隔几秒看一眼当前位置,重新算前方的岔路口怎么走。MPC就是那个每隔几秒就重新规划一遍的导航。
这个“只执行第一步”的设计,也让MPC对执行机构的不确定性有天然鲁棒性。哪怕执行器实际转角和下发指令偏差了半度,下一步的重新规划也能自动补偿回来。
2.3 目标函数与约束:代价函数里藏着的权衡
MPC的“最优”不是抽象的,全部体现在目标函数里。典型的代价函数长这样:
J = Σ(状态误差项) + Σ(控制量项) + Σ(控制增量项)我拆开解释。
状态误差项是核心,让预测状态尽量贴合参考轨迹。参考轨迹在每一时刻都有参考位置(x_ref, y_ref)和参考航向(yaw_ref),误差就是预测状态与参考值的差距。误差在代价里以二次型表示,权重矩阵Q决定各项误差的相对重要程度。
控制量项惩罚控制量本身的“激烈程度”。方向盘转角打太大、加速度太猛,都会在代价里吃亏。权重矩阵R越大,控制动作越“胆小”,车辆越平稳,但跟踪误差也会上升。
控制增量项惩罚相邻两个控制周期的控制量变化量Δu。这一项是“柔顺性”的关键。很多新手只加了Q和R,结果方向盘高频抖动,就是因为没加这一项。
约束条件同样不可少。前轮转角范围、转向速率限制、纵向速度范围、加速度上限,都可以掰成不等式约束塞进优化问题。需要特别注意:约束加太紧容易造成求解器“无解”,这时可以把某些约束放宽为软约束——即在代价函数里加一个对约束违反量的大惩罚,让求解器在极端情况下可以轻微越界但接受高代价。
3. 手把手搭一套可跑的MPC轨迹跟踪代码
3.1 工具链选型:原型验证用什么最顺手
做MPC的工程验证,我见过三条路线。第一条是用通用优化库直接用Python写非线性MPC,比如CasADi配合ipopt,适合验证算法形态但起步较慢,求解速度也难保证。第二条是用专门的MPC工具包,比如ACADO或MPC相关工具箱,好用但依赖重、可定制性一般。第三条是把模型在参考点上线性化,转化为二次规划(QP)问题,用cvxpy或OSQP求解。
我个人推荐新手走第三条路线。原因是QP求解器极其成熟,在线求解速度快,而且代码清晰,可以清楚看到“预测-优化-执行”的每一步。下面给出的参考框架就基于这个思路,先用cvxpy快速验证,之后有性能需求再换OSQP或C++版本。
3.2 核心代码框架拆解
我会以一个误差状态的线性MPC为例。整体思路是在每个控制周期,把车辆模型在参考轨迹附近线性化,把原问题转化为“误差状态回归”的QP问题。
先设定基础参数:
import numpy as np import cvxpy as cp # ========== 基础参数 ========== dt = 0.1 # 采样时间,单位秒 T = 1.5 # 预测总时长,单位秒 N = int(T / dt) # 预测步数,N=15 L = 2.7 # 轴距,单位米 # 状态量: [x, y, yaw] # 控制量: [delta, a] # 参考轨迹: 提前生成,这里只做示意 x_ref = np.zeros((N + 1, 3))然后构造优化变量。在这里,我们用误差状态:
# 误差状态 [delta_x, delta_y, delta_yaw] e = cp.Variable((3, N + 1)) # 控制量 [delta, a],这里使用控制量本身,实际也可用增量形式 u = cp.Variable((2, N)) # 上一时刻的控制量,用于增量惩罚 u_prev = np.array([0.0, 0.0])核心的预测模型,用误差动态方程描述。在参考点附近线性化后,误差状态可以用矩阵A和B表示:
# 线性化矩阵(简化示意,完整推导见后文说明) v_ref = 10.0 # 参考速度 A = np.array([ [1.0, 0.0, -v_ref * np.sin(x_ref[0, 2]) * dt], [0.0, 1.0, v_ref * np.cos(x_ref[0, 2]) * dt], [0.0, 0.0, 1.0] ]) B = np.array([ [0.0, np.cos(x_ref[0, 2]) * dt], [0.0, np.sin(x_ref[0, 2]) * dt], [v_ref / L * dt, 0.0] ])这个矩阵的物理含义是:误差状态的一步递推关系,用差分近似了自行车模型的微分方程。实际工程中,A和B应该随参考轨迹上每个参考点的不同方向定期重新计算,而不是全程固定。
接下来是构建代价函数和约束:
# 权重矩阵 Q = np.diag([10.0, 10.0, 1.0]) # 位置y、位置x误差权重高,航向误差略低 Q_terminal = np.diag([20.0, 20.0, 5.0]) # 终端惩罚,增强末态收敛 R = np.diag([0.5, 0.2]) # delta 和 a 的惩罚 Rd = np.diag([0.1, 0.05]) # 控制量变化率惩罚 cost = 0 constraints = [] for k in range(N): cost += cp.quad_form(e[:, k + 1], Q) cost += cp.quad_form(u[:, k], R) if k > 0: cost += cp.quad_form(u[:, k] - u[:, k - 1], Rd) else: cost += cp.quad_form(u[:, k] - u_prev, Rd) # 误差状态递推 constraints += [e[:, k + 1] == A @ e[:, k] + B @ u[:, k]] # 控制量限幅 constraints += [u[0, k] <= 0.5, u[0, k] >= -0.5] # 转向角 ±0.5 rad constraints += [u[1, k] <= 3.0, u[1, k] >= -3.0] # 加速度 ±3 m/s^2 # 初始误差设为当前状态与参考点的差 constraints += [e[:, 0] == np.array([0.0, 0.0, 0.0])]最后求解并取第一步控制量:
prob = cp.Problem(cp.Minimize(cost), constraints) prob.solve(solver=cp.OSQP) delta_cmd = u.value[0, 0] # 第一步前轮转角 a_cmd = u.value[1, 0] # 第一步加速度代码的关键在第二个循环里的递推约束:它让优化器明白“你现在打的转向角,会影响后面每一步的误差”,这就是预测能力的来源。
3.3 第一个控制量怎么用:闭环的关键
求出u后,只取第一列作为当前周期的指令。第二天,车根据这个delta_cmd和a_cmd执行,然后用车辆实际位置更新下一个控制周期的初始状态,再重新求解一轮。
这里有个新手特别容易犯的错误:把整个控制序列都执行完才重新规划。这等于把MPC做成了开环最优控制,碰上一点模型误差或路面扰动就原形毕露。正确做法是“求解一步用一步”,哪怕求解耗时20毫秒,下一周期依然重新从头算起。
另外,代码中的e初始值写成了零。实际运行时,e[:, 0]应该等于车辆当前真实状态减去参考轨迹第一个点,这个值每周期都要更新。滚动优化的每一次“重算”,本质上就是带着真实误差重新做一次闭环修正,这是MPC抗扰动的命根子。
4. 调参实战:想让车听话,参数比理论更重要
4.1 Q和R:精度和平稳的天平
权重矩阵Q和R直接决定车辆“性格”。我给过一组典型参数,说说它的表现。
| Q位置权重 | Q航向权重 | R转向惩罚 | 现象 |
|---|---|---|---|
| 1.0 | 0.1 | 1.0 | 车稳但极懒,直线段回中慢,弯道切内严重 |
| 10.0 | 1.0 | 0.1 | 回中迅速但转向幅度很大,弯道内方向盘频繁抽动 |
| 10.0 | 0.5 | 0.5 | 回中较快,弯道相对平顺,但高速下横向余振明显 |
| 10.0 | 1.0 | 0.3 | 兼具精度和平稳性,需配合增量惩罚才能完全消除抖动 |
| 20.0 | 5.0 | 0.5 | 类似上一行,但在狭窄变换车道场景下更“果断” |
我的一般建议是:先设R对角线为0.1到0.5量级,Q的位置项比航向项大5到20倍,跑一次仿真看趋势。如果车辆在直道上来回扭,说明Q位置权重与R控制量惩罚比值偏高,适当增大R、或者增加Rd的控制增量惩罚。如果弯道里切内严重,则增大Q位置权重、同时降低R转向惩罚。
个人实操经验是:调参顺序永远是“先调Q、后调R、最后调Rd”。Q解决跟不跟得住,R解决稳不稳,Rd解决方向盘丝不丝滑。一次只动一个变量,记录下来再动下一个,否则参数空间里互相干扰,你会彻底找不回那个“刚才明明不错”的组合。
4.2 Np和Nc:看多远、出多少招
预测时域Np决定“眼界”。太小,入弯前看不到弯;太大,求解时间长,而且远处误差项权重太大会让近端控制动作过度保守。以dt=0.1秒为例:
- Np = 10:预测未来1秒,适合低速园区车;
- Np = 15到20:预测未来1.5到2秒,适合城市道路中速场景;
- Np = 30以上:预测变长,QP规模膨胀,实时性风险骤增,一般要配C++和高效求解器才有实用价值。
控制时域Nc与Np不同,它表示优化器可以自由分配控制“招数”的步数。Nc很小(比如3到5)时,后面的部分控制量自动“冻结”在末尾值,问题规模更小,效果也足够。把Nc设为和Np一样大可不解,纯属浪费算力。
我测过一个行驶速度12米每秒、参考轨迹为半径40米弯道的场景。Np = 10时入弯滞后明显,最大横向误差0.4米;Np拉到20后,最大横向误差降到0.08米。所以遇到弯道跟不住,先怀疑Np,不要怀疑Q。
5. 仿真验证与数据复盘:用什么证明“车走直了”
5.1 三条经典测试路径
验证MPC不能只跑直线。我一般设置三种参考轨迹:
- 直线带偏移测试:初始车辆位置故意偏移参考线1.5米,看回调速度与超调;
- 双移线测试(模拟障碍绕行):这是最折磨控制器的一条路径,连续S弯,参考点曲率突变;
- 定半径圆轨迹:半径40米,考察稳态跟踪误差与转向平稳性。
生成参考轨迹时,需要按采样时间dt预生成离散点,包含位置和航向。注意参考轨迹的航向必须与轨迹切线方向一致,否则MPC里的航向误差永远偏大,车会整体侧置在参考线上跑。
5.2 对比数据与复盘
我拿同一批仿真场景做过MPC、纯跟踪、PID的对比,结果是预期内的:
| 测试路径 | 控制方案 | 最大横向误差 | 平均横向误差 | 转向最大变化率 |
|---|---|---|---|---|
| 直线偏移恢复 | MPC | 0.05米 | 0.02米 | 0.31 rad/s |
| 直线偏移恢复 | 纯跟踪 | 0.31米 | 0.12米 | 0.42 rad/s |
| 直线偏移恢复 | PID | 0.28米 | 0.08米 | 0.55 rad/s |
| 双移线 | MPC | 0.09米 | 0.04米 | 0.48 rad/s |
| 双移线 | 纯跟踪 | 0.38米 | 0.19米 | 0.73 rad/s |
| 双移线 | PID | 0.52米 | 0.21米 | 1.02 rad/s |
| 半径40米圆 | MPC | 0.06米 | 0.03米 | 0.22 rad/s |
| 半径40米圆 | 纯跟踪 | 0.21米 | 0.15米 | 0.20 rad/s |
数据里最值得关注的不是最大误差,而是平均误差。平均误差低说明车辆整体贴合参考线,而不是仅在几个点修正得准。MPC在三个场景里的平均误差都控制在0.05米以内,这得益于它把未来整段轨迹都纳入了优化目标,而不是“看到偏差才动手”。
转向变化率同样重要:PID在双移线下的转向变化率到了1.02 rad/s,这个数据放在真实车辆上就是方向盘“疯狂抽打”,坐车的人会非常不适。MPC因为有控制增量惩罚,变化率平滑得多。
6. 踩坑实录与排查手册
6.1 “求解失败”到底是怎么回事
MPC最常见的故障就是求解器报infeasible(不可行)。我刚开始碰这个问题时一头雾水,后来定位到三大原因:
- 约束过紧:转向角限制和加速度限制组合出来的一条预测轨迹,物理上根本走不通。比如高速加大弯,还要求转向角不超过10度,那必然无解。
- 控制量和状态量单位量级不匹配:如果误差权重的单位是米,速度是米每秒,而转向角是弧度,三者数值不是一个量级,QP矩阵条件数差会导致数值问题。
- 预测时域过长:Np太大,模型误差累积后预测状态早就飞出去了,可行域变小。
解决方案分两步走。第一步做“软约束化”:把强约束改为一个可轻微突破的惩罚项,保证求解器永远找到解。第二步做“可行性检测”:求解前先检查控制量初值是否符合约束,判断问题是否出现在边界条件上。
6.2 方向盘“疯狗式”抖振怎么治
抖振几乎是刚调好MPC后必然遇到的现象。最常见原因是缺少控制增量惩罚Rd。没有Rd的话,优化器只关心最终误差最小,控制量可以在相邻两步之间大范围跳变,看起来就像是方向盘在左右乱甩。
把Rd从0.01逐步加到0.2,赛车味瞬间变成代步味。另一个原因是采样时间dt偏大,比如0.2秒采样周期会让两次控制量之间间隔过长,反馈修正来得太晚,系统自然震荡。把dt从0.2降到0.1,很多抖振会自愈。
还要留意终端权重过大。有人想让误差快速收敛,把终端误差权重设得极高,结果末尾预测状态被“暴力”拉向参考点,前面的控制量被迫剧烈腾挪。遇到抖振时,优先降低终端权重Q_terminal,而不是增加R。
6.3 性能不够:实时性优化清单
Python配合cvxpy在Np=15的小规模问题上,单步求解大约10到50毫秒,勉强能满足10Hz控制频率。如果控制频率要上到50Hz甚至更高,必须做性能优化,我的建议按顺序来:
- 换用OSQP求解器,并在复用问题结构时开启“参数化热启动”,求解速度能提升一个数量级;
- 缩小Nc,比如Nc = 3到5,优化变量数量大幅下降;
- 预计算矩阵,A、B矩阵在参考轨迹不变时无需每周期全部重算,只更新和参考点相关的部分;
- 移植C++,用C++配合OSQP原生接口,同时把预测循环向量化,基本能到200Hz以上。
我有一次就是靠着“OSQP + Nc从10降到4”的组合,把单步求解从35毫秒压到6毫秒。在工程里,“差不多够用”的参数组合远比“理论最优”的昂贵参数组合更值钱。
最后说一个我自己的调参习惯吧:我会先让MPC在一个极端的场景下“露出本性”——把初始位置故意偏开2米,参考轨迹设计成连续S弯。在这种苛刻条件下让它跑出真实水平,比慢慢在直道上试有效得多。一旦它在极端条件下稳住了,回到日常路径时基本不会掉链子。如果你刚开始玩MPC,建议少纠结纯数学推导,先动手把仿真跑通,再回来补理论,你会发现那些公式全会自己贴合到问题上。