作为常年跟自动驾驶控制算法打交道的人,我最早接触模型预测控制(MPC,Model Predictive Control)时,第一反应其实是有点抗拒的。这玩意儿理论门槛不低,矩阵求逆、约束优化、实时性调优,每一项都够喝一壶。但真把MPC调通、让车稳稳跑上几圈之后,再回头看,我得说:MPC确实是当前无人驾驶车辆横纵向控制里,工程效果和理论优雅感平衡得最好的一种方案。
这个标题涉及的范围很典型:一辆无人车在道路上行驶,需要处理转向、速度、避障和轨迹跟踪。基础思路是建立车辆的动力学或运动学模型,再用模型预测未来一段时域内的状态,通过滚动求解优化问题来确定最优控制量。这篇内容我会从“为什么选MPC”开始,把车辆建模、预测方程、滚动优化、仿真调参以及实际踩过的坑这整条链路讲透,全程只讲干活要用的东西,不适合当数学教科书来读,但适合动手搭仿真时照着来。
1. MPC在无人驾驶路径跟踪中的定位与选型逻辑
1.1 为什么路径跟踪环节偏偏选了MPC
无人驾驶车辆的底层控制,核心任务就是路径跟踪:给一条目标轨迹(可以是全局路径规划出来的散点,也可以是局部运动规划模块给的平滑曲线),车辆的底层执行器要去把这个轨迹尽可能准地跑出来。
在这个环节,常被拿出来跟MPC对比的方案有几种。纯跟踪(Pure Pursuit)算法最简单,用一个前视距离找路径上的目标点,算曲率打方向盘。好处是几乎零计算量、调参直观,缺点是前视距离一调错就蛇行或切弯,对高速急弯场景控制精度不够。PID横向控制器工程实践多,但本质上是一个反应式的闭环调节,没有“预见”能力,速度突变、曲率突变时跟踪误差会明显放大。
MPC跟这两类做法有本质区别:它把控制问题变成一个带约束的有限时域优化问题。每到一个控制周期,算法基于当前车辆状态,用车辆模型往前预测未来N步状态,在预测时域内找一个满足约束(比如方向盘转角限制、加速度限制)的最优控制序列,然后只执行第一步,下个周期重新滚动求解。这个“滚”的动作,让它每一步都拿着最新状态往前看,比PID多了预判,比纯跟踪多了约束处理能力。
我当时选型时还有一个考虑点:它天然适合处理多约束问题。无人车不是只把路径追准就够了,还要保证舒适性(加加速度限制)、安全性(横向偏移限制)、执行器物理极限(转角、扭矩上限)。PID要处理这些约束,得一个一个硬编码逻辑,风扇式地叠判断;MPC只需要在优化问题里把约束写成不等式方程就行,维护起来干净得多。
1.2 运动学模型与动力学模型,工程上怎么选
MPC的预测能力来自模型。模型建得越精细,预测越准,但计算量也越大,还容易让优化问题变成病态。实践中,路径跟踪控制通常有两种建模路线。
运动学模型(Kinematic Model)假设车辆没有侧偏,轮子始终沿前进方向运动,把车简化成自行车模型加几何关系。它描述的基本量是位置、航向角、前轮转角、速度。这组方程简单、计算量小、参数少,适合低速园区物流车、室内AGV这类场景,速度一般不超过10-15m/s。对于普通城市工况,运动学模型在中等速度下表现也不错,因为控制周期短(通常20-50ms),模型误差还没有积累太多就被反馈校正了。
动力学模型(Dynamic Model)则考虑轮胎侧偏角、质心侧偏、横摆角速度、前后轮侧偏刚度,是一个二自由度的单轨模型。它能在较高速度(比如高速公路120km/h)和紧急操稳工况下预测出车辆的真实运动趋势,但因为引入了轮胎力非线性,优化问题的非凸性更强,求解时间也更难控制。
一个比较实在的工程思路是:先确认你的最高运行车速和场景复杂度。低速场景起步就运动学;高速场景别侥幸,老老实实上动力学模型,否则极限工况预测会失真。我见过有人硬拿运动学模型跑高速仿真,结果一个急弯直接冲出目标轨迹,偏差大得离谱,调半天参数都救不回来。
1.3 MPC的“滚动”与“预测”到底好在哪里
MPC的核心机制,我习惯拆成三步来看:预测、优化、滚动。
预测是指基于当前状态量和上一时刻控制量,用模型算出未来一段时域内的状态轨迹。这里的“未来一段时域”不是随便拍脑袋定的。预测时域太长,优化变量维度爆炸,实时性崩坏;太短,MPC会退化得跟短视的PID差不多。经验上预测步数Np常取15-30步,控制步数Nc通常小于或等于Np,具体数值跟控制周期和模型复杂度直接相关。
优化是指在每个周期,把“让预测轨迹尽量贴近参考轨迹”写成目标函数,把“控制量不能超限、状态不要越界”写成约束,求解一个非线性/二次规划问题。目标函数里一般有两个加权矩阵:状态误差权重Q和控制量权重R。Q越大,车越“较真”地贴近参考路径;R越大,执行器动作越平顺。平衡不好就会出现要么贴得紧但转向狂抽搐,要么动作斯文但弯道偏移严重。
滚动是MPC和开环最优控制最大的区别。优化解出来的控制序列只有第一个值被真正下发,后面的值只是“预留计划”。等到下一个控制周期,重新采样车辆的实际状态,重新预测、重新优化。这个闭环滚动的动作,是MPC能扛外部扰动和模型失配的根本原因。
2. 构建预测模型:无人车控制仿真的数学底座
2.1 状态量、控制量与参考轨迹的坐标约定
建模型前先要统一坐标系。车辆在全局坐标系下的运动,通常取车辆质心(或后轴中心,取决于建模假设)的位置 (X,Y) 和航向角 ψ 作为状态描述。控制量是前轮转角 δ_f 和纵向加速度 a(或油门/刹车指令)。如果是运动学自行车模型,状态量一般写:
- X:车辆在大地坐标系下的X坐标
- Y:车辆在大地坐标系下的Y坐标
- ψ:航向角
- v:纵向速度(往前进方向为正)
控制量:
- δ_f:前轮转角
- a:纵向加速度
参考轨迹则是一组预先生成的目标点集,每个点带目标位置、目标航向角、目标速度。仿真里常见的方式是把规划的轨迹以固定弧长间隔均匀离散,然后在MPC的目标函数里,让预测的Np个状态点分别和对应的目标点做误差计算。
这里有个容易踩坑的点:参考轨迹点的索引对应关系。MPC每一帧预测的是未来Np个周期内的状态,如果你的参考轨迹点间隔和控制周期对不上(比如轨迹点每0.5米一个,车速高的控制周期是50ms,一周期跑出去一米多),那么误差计算时参考点会错位。工程解法是:按车辆当前纵向位置在轨迹上搜索最近的参考点,再从该点往后按等时间间隔插值取参考点序列。这样预测步和参考步的时间语义就对齐了。
2.2 运动学自行车模型推导与离散化实操
运动学自行车模型把前后轮“合并”成单轨模型,并忽略轮胎侧偏角,假设前后轮速度方向与轮胎朝向一致。核心运动学关系是:
- X方向速度:Ẋ = v * cos(ψ + β),其中 β 是质心侧偏角的简化项,对于后轴中心为参考点时 β=0
- Y方向速度:Ẏ = v * sin(ψ + β)
- 横摆角速度:ψ̇ = v * cos(β) * tan(δ_f) / L,L是轴距
- 纵向加速度:v̇ = a
取后轴中心为参考点时,β取消掉,模型变成更简洁的形式。注意这个模型成立的前提是低速和中低速,侧向加速度一般不超过0.4g左右,轮胎工作在线性区内才对。
模型是连续微分方程,上控制器之前要离散化。最常用的是欧拉离散或二阶龙格-库塔离散。欧拉法简单:状态_next = 状态_current + 状态导数 * dt。特点是一阶精度,采样时间不超过50ms就算基本够用;要更稳就上龙格-库塔,纵向控制对加速度的积分敏感时,我倾向用二阶RK,避免积分漂移导致速度环误差变大。
这里给一个实际仿真用的离散参数基准(具体数值可根据车辆特性调整):
- 轴距 L:2.7m
- 采样时间 dt:0.03s
- 速度范围:0到20m/s
- 前轮转角范围:±0.5rad
- 最大加速度:±3m/s²
离散后的状态方程,就是MPC预测模型的基础。
2.3 动力学模型:什么时候必须上,参数怎么标定
当车速超过15m/s,或者场景里有紧急变道、连续S弯时,运动学模型预测出来的一条直线,真车可能已经因为侧滑拐不过去了。这时得上二自由度单轨动力学模型。
这个模型的状态量会再加上横摆角速度 γ 和质心侧偏角 β。核心关系是轮胎侧偏力:
- 前轴侧偏力:F_yf = -Cf * α_f,α_f是前轮侧偏角
- 后轴侧偏力:F_yr = -Cr * α_r,α_r是后轮侧偏角
其中Cf和Cr是前、后轴等效侧偏刚度,单位是N/rad。这两个参数是动力学模型里最关键的标定项。理论值可以从轮胎资料里查,但仿真里更需要的是识别或实测对照。比较糙但有效的做法:给定一个稳态转向输入,让仿真模型跑一圈,记录侧向加速度和横摆角速度,反推刚度值,跟真值对照调整。
动力学MPC的一个麻烦是:侧偏角表达式里有一个atan()非线性项,预测方程是强非线性的。每次预测都要重新计算雅可比矩阵做线性化,即所谓LTV-MPC(线性时变模型预测控制)。因为每帧都对当前工作点线性化一次,所以它在中高速工况下比定常线性MPC好用得多,计算量增加没有想象中那么恐怖。
3. 预测控制核心:优化目标、约束与滚动求解
3.1 目标函数怎么设计才不会“盯着误差发疯”
常见的设计方法是把每个预测步的跟踪误差平方和攒起来,再叠加上控制增量惩罚。目标函数写作:
J = Σ (预测状态 - 参考状态)ᵀ W_q (预测状态 - 参考状态) + Σ (控制增量)ᵀ W_r (控制增量)
这个控制增量的项很关键。它惩罚的是相邻两个控制量之间的差值delta_u,相当于禁止转向指令突变。如果没有这一项,MPC解出来的最优控制序列可能出现“高频抖动”,就是数学上最优但物理上执行器根本跟不上(或者乘员直接晕车)。加了增量惩罚,控制曲线会变得平滑。
我体会比较深的一个调整技巧是:不要盲目把Q调得非常大。很多人觉得“要让车贴线越紧越好”,于是把位置误差权重乘以100、把速度误差权重忽略。结果跑出来的轨迹要么贴着参考线但方向盘一顿一顿地细微修正,要么在弯道里跟参考线死磕导致横向加速度过大,舒适性指标直接崩。理性做法是先把位置权重和航向权重拉开一个合适比值(比如3:1),再把速度误差权重放到比位置误差小一个数量级,最后靠控制增量权重来压振荡。
3.2 约束条件:哪些该硬约束,哪些该软约束
MPC里约束写得不好,优化器很容易无解。我在实践中把约束分成两类对待。
硬约束:执行器物理极限,比如前轮转角不超过±0.5rad,这个必须硬约束,没有任何商量余地。加速度限制同理,油门刹车超过这个值,执行器也输出不了。硬约束写在不等式里,求解器必须在可行域内找解。
软约束:舒适性、安全边界的约束,比如最大加加速度、车道偏移界限。这类约束建议不要直接写死,因为一旦参考轨迹本身设计不当,硬约束会让问题在某个时刻直接无解,车辆控制进入混乱状态。常规做法是引入松弛变量:约束写成原不等式加上一个松弛变量 ≤ 上限,并在目标函数里对松弛变量加上高权重惩罚。这样约束在正常情况下严格满足,极端情况下退化为“尽可能靠近”。
实际操作中我还加了一个执行器变化率约束,也就是delta_δ的上限。这比在目标函数里叠惩罚项更强力,从数学上保证不会出现“上一帧右满舵、下一帧直接左满舵”的指令,保护转向机构。
3.3 滚动求解过程与C/GMRES这类实时算法的落地方案
MPC的实时性核心是求解优化问题。仿真里可以用通用的非线性优化器,比如NLopt或IPOPT,但这类通用求解器往往迭代步数不确定,实时性没有保证。工程上更常见的是用C/GMRES(基于广义最小残差法的连续/广义预测控制)或内点法等专门设计的算法。
C/GMRES是我在几套工程代码里实际跑得最顺的实时求解方案。核心思路是:把MPC在每个采样周期要解的KKT条件(最优性条件)看成一组关于状态和控制序列的非线性方程组,然后对时间变量做连续化,从上一个周期的解出发,用GMRES迭代求解当前周期的更新量。因为它天然利用了上一周期的解作为热启动,迭代次数很少,实时性非常稳。
用C/GMRES落地时要注意一个点:实现的时候需要对预测方程、目标函数梯度、约束雅可比做手写推导,编码量比直接调通用求解器大不少。好处是整套结构完全可控,采样周期能做到30ms以内;坏处是一开始调试梯度表达式时很容易掉进符号推导的坑里。我的建议是先用符号工具箱把梯度公式推出来对照手写实现,不要相信自己能心算对高阶雅可比。这个坑我踩过不止一次。
3.4 参数整定的逻辑顺序
MPC调参,我个人的流程是先调预测时域和采样周期,再调权重矩阵,最后处理约束松弛。
预测时域Np决定“看得多远”。太小过弯贴不准,太大计算量翻倍。一个可供起始参考的公式是:Np * dt 不小于车辆以当前速度从执行一个最大转向到完成变道动作所需的时间。以中等车速为例,dt取30ms,Np选20到30,预测视界大概0.6到0.9秒。视界太短,车辆过一个长弯道时中段会明显往外偏。
权重矩阵的初始值,我一般从等权开始,跑一次仿真看误差集中在哪里,再逐项放大对应权重。经验是:位置误差权重是最敏感的,一点点提高就能看到明显效果;航向权重调节的是过弯姿态,会影响出弯后回到直线段的快慢;控制增量权重是“降火”的,蛇行振荡严重、转向指令毛刺多,优先加这一项。
约束松弛变量权重我一般设成比最大状态权重再高一个数量级,确保正常情况下松弛量约等于零,只有极端危险工况下才被激活。
4. 仿真环境搭建与实操流程
4.1 选哪个仿真平台来验证MPC
当前主流的路径跟踪仿真路径有几类:一类是直接在Python/Matlab里写车辆模型和MPC,跑纯数字仿真;另一类是用高保真车辆仿真器配合控制算法的外部接口做联合仿真,比如Carla、SUMO搭配MATLAB/Simulink或ROS/C++节点。
如果是验证控制算法本身,我的建议是分两步走。第一步用Python或者Matlab做数值验证,车辆模型用运动学/动力学模型,参考轨迹用给定曲线,把所有理论逻辑跑通。这个阶段是调参最快、成本最低的,不要一上来就追求高保真环境,因为高保真环境引入的执行器延迟、传感噪声会掩盖算法层面的问题,新手很容易被带偏。
第二步再把算法嵌入到仿真器或ROS节点里做闭环验证。重点看两个东西:一是控制周期的实际耗时有没有打满预算,二是加入噪声和延迟后跟踪误差的退化程度。这一步更接近真实工程,但主要价值是暴露接口和时序问题,而不是验证控制理论本身。
4.2 实操:用Python搭一个最小MPC仿真闭环
为避免涉及任何具体商业产品,这里演示一个通用的Python搭建流程。核心步骤分四段。
第一步,建立车辆模型类。我通常用一个类封装状态更新函数,输入是当前状态、控制量、采样时间,输出是下一个状态。运动学模型的实现非常简单:
class KinematicBicycle: def __init__(self, wheelbase=2.7, dt=0.03): self.L = wheelbase self.dt = dt def step(self, state, u): x, y, psi, v = state delta, acc = u x_next = x + v * np.cos(psi) * self.dt y_next = y + v * np.sin(psi) * self.dt psi_next = psi + (v / self.L) * np.tan(delta) * self.dt v_next = v + acc * self.dt return np.array([x_next, y_next, psi_next, v_next])第二步,生成参考轨迹。为了让测试覆盖面够广,我习惯生成一段组合轨迹:直行段、一段定曲率圆弧、一个S弯。轨迹点带目标航向和目标速度。
第三步,实现MPC目标函数和梯度。把这部分定义成Python函数,输入当前状态、控制序列、参考轨迹点,输出代价函数值和梯度。这里有个重要的工程细节:把预测状态序列的计算做成向量化批量操作,不要用Python循环逐点推,否则仿真实时性会很差。原因很简单,MPC每帧要预测Np步,仿真动辄跑几千步,循环嵌套的成本累积起来非常可观。
第四步,搭主循环。每帧执行:用当前状态预测、求解优化问题、取最优控制序列第一个元素、施加到车辆模型、记录轨迹数据。
这里求解优化问题用的是现成的优化器,实际跑下来一个周期大约需要20-30ms。跟模型复杂度、预测步数有关,速度完全满足常规仿真验证。
4.3 从纯数字仿真切换到带噪声的闭环测试
纯数字仿真里MPC表现得再好,也不代表实车能用。因为数学仿真里的状态是精确的,而现实里你拿到的状态是带噪声的、有延迟的。所以仿真验证至少要做两轮:理想状态闭环,以及带测量噪声和控制延迟的闭环。
给状态加噪声很简单:在每个控制周期,给输入的x、y加均值为零、标准差分别为0.1m的高斯噪声;航向角加0.01rad噪声。控制延迟则用一个环形缓冲区模拟,命令发出后延迟两到三个周期才被执行。
这两样东西加上后,MPC的表现会有明显变化。说白了,MPC虽然对自己的模型很自信,但在积分过程中,噪声和延迟会让预测和实际产生偏差,如果权重参数设计得过于激进,高频分量会被后续周期反复放大。经过这轮测试,你才算是对算法的鲁棒性有了真实感觉。
4.4 核心参数速查参考表
我把仿真中常用的参数范围和初始值整理出来,方便第一阶段调参时直接抄作业,注意这些是起始值而非最优值。
| 参数 | 物理含义 | 常用起始值 | 调整方向说明 |
|---|---|---|---|
| dt | 控制周期 | 0.03s | 过小加重计算负担,过大跟踪误差增大 |
| Np | 预测步数 | 20-30 | 偏小过弯贴不稳,偏大实时性变差 |
| Nc | 控制步数 | 5-10 | 小于Np,增大可增强控制自由度 |
| Q_x, Q_y | 位置误差权重 | 2.0, 2.0 | 提高则贴线更紧,但过度会引发振荡 |
| Q_psi | 航向误差权重 | 0.8 | 影响弯道姿态和出弯回正速度 |
| Q_v | 速度误差权重 | 0.5 | 影响速度跟踪,过大可能牺牲横向精度 |
| R_delta | 控制增量权重 | 0.1 | 减小转向抖动的主要旋钮 |
| R_acc | 加速度增量权重 | 0.05 | 增加则纵向更平顺 |
这套参数面对标准城市双车道工况,可以跑出一个合理的基础表现。具体数值会因为轴距、轮胎、速度范围不同而要重新标定,但它能让你在第一轮仿真里不至于“完全发散”,后面微调起来有方向感。
5. 仿真和调参过程中的典型问题与排查实录
5.1 车辆轨迹发散、飞出参考路径的排查顺序
这是最常见的崩溃现场。轨迹发散的原因往往是多种叠加,排查顺序比直接调参更重要。
我的习惯是先确认模型本身没写错。把车辆模型的离散方程单独拿出来,给一个恒定控制量输入,看它跑出来的轨迹是否符合直觉。比如给定恒定前轮转角0.3rad、速度10m/s,车应该画一个有固定半径的圆弧。如果圆弧半径跟理论值偏差太大,说明离散或坐标关系有误,调参数没有任何意义。
接着检查参考轨迹的对齐逻辑。参考点查找是否用了最近的弧长索引、插值是否保持了连续性。如果对齐逻辑有跳变,MPC会看到一段“错误的未来轨迹”,被“误导”着做出奇怪的动作,表现上跟模型错误非常像。
然后检查约束是否导致问题不可解。把约束一一去掉,看是哪条约束让求解器找不到可行解。常见的是速度约束和转角约束冲突,比如速度太高、转角上限不够,模型预测无论如何也转不过弯来,这时要放的是速度约束或重新设计参考轨迹,而不是死磕权重。
最后才动权重。顺序是:先让车能走起来不飞,再逐步收紧位置误差权重。
5.2 高频振荡与“蛇行”的破解思路
蛇行问题的本质是误差权重和控制增量权重失去平衡,或者预测时域太短导致MPC“只见树木不见森林”。
破解方法排序:第一优先加控制增量权重,这是最直接的。比如从0.1调到0.5,振荡幅值一般会肉眼可见地下降。第二,检查参考轨迹本身有没有高频抖动,如果参考轨迹点本身就是带噪声的手工描点,MPC会忠实地去追这些噪声,表现就是车在抖而算法没错,这种时候要平滑轨迹而不是调权重。第三,增大预测步数或者降低控制步数,让MPC看得更远、决策更稳。
我遇到过一种特别隐蔽的情况:控制周期和参考轨迹的离散间隔不匹配,导致参考序列在相邻帧之间有跳变,MPC每帧都在追一个“移动的目标”,也会产生细碎修正式蛇行。这种排查起来非常耗时间,一度以为是优化器问题,最后查出来是轨迹插值没有做等时间间隔重采样。
5.3 成本函数病态与矩阵条件数的实际问题
MPC的优化问题在状态量纲差异大时会变得病态。位置量单位是米,航向角是弧度,速度是m/s,数值范围差了好几个数量级。比如位置误差是0.1量级,速度误差是0.5量级,那在目标函数里误差平方后,速度项的数值带权重乘以0.25,位置项是0.01,量纲差异会让低幅值的高价值项被忽略。
工程解法是对状态做归一化:把位置误差、航向误差、速度误差分别除以各自的特征尺度。常见特征尺度设置:位置取5m、航向取0.5rad、速度取10m/s。归一化后权重数值的物理含义就变成了“倍数关系”,调整起来直觉更强,条件数也大大改善。
这个坑的隐蔽性在于,不归一化的情况下算法也能跑,只是调参时的手感极其怪异:你加了很大的权重,响应却纹丝不动,是因为量纲差异把这个权重的效果“稀释”掉了。白折腾几个小时,最后归一化一上,问题瞬间清晰。
5.4 实时性瓶颈与预测步数的取舍心得
如果你把MPC放到ROS节点或者真实控制器里跑,实时性就是绕不过去的坎。一个控制周期里,从传感器数据到达,到状态估计、轨迹重规划、控制求解、指令下发,整个链路有时间预算,分配给MPC的往往只有10到30毫秒。
如果实测求解时间超了预算,我的降负顺序是这样的:先砍控制时域Nc,因为它在提升解自由度方面的边际收益通常不如维持长时间预测视界重要;再检查预测方程里有没有冗余计算,比如重复计算了不随优化变量变化的项,应该预计算并缓存;最后实在不行才砍Np,因为预测视界一缩短,MPC的前瞻性优势会快速流失,变得更接近短视控制器。
另外,如果用的是C/GMRES这类带热启动的求解器,不要每次迭代都从零开始。将上一周期的最优解作为初值传入,迭代次数通常可以砍掉一半以上。这是实时性优化里最实在的一个收益点。
6. 写在最后的几条实际建议
刚开始搭MPC仿真时,别指望一个模型能通吃所有场景。我的做法是让仿真环境跑三个固定用例:直道匀速、定曲率弯道变速度、连续S弯加噪声。这三类用例能覆盖大多数路径跟踪问题,每个用例分别记录横向误差、航向误差、转向指令平滑度和单周期求解耗时。
把这三个用例的日志留下来当基准,每次改参数都在同一套用例上跑,用数字对比说话,而不是靠肉眼看轨迹图。很多调参经验不是靠感觉积累的,是靠对照实验才排除了不少干扰因素。尤其是权重矩阵,凭感觉拧参数,过两天就会忘记之前为什么能跑好。
最后想聊一下我从做MPC仿真到真正理解它的过程。最初我陷入纯理论推导,沉迷于各种矩阵和最优性条件的证明,但真正让我对MPC建立起工程感的,是仿真里一次次的发散、振荡和死锁。每次排查到最后,大多不是算法不够高级,而是模型和接口在一个不起眼的细节上对不上。所以如果你在仿真里遇到离奇现象,先怀疑自己建立的模型和接口,再怀疑调参逻辑,最后再怀疑算法设计本身。这个顺序能帮你少走很长的弯路。