
很多搞自动驾驶控制的人第一次在Apollo源码里看到MRAC心里都是同一个反应这又是个劝退的缩写。Model Reference Adaptive Control模型参考自适应控制教科书上倒是见过可真要把它和一辆会跑的车联系起来大脑会瞬间空白。我当初啃Apollo控制模块时也被这个模块卡了很久后来在模拟器、实车上反复调参数才慢慢把这东西和“纵向控制里的不确定性补偿”这件事画上等号。这篇文章不打算堆一堆晦涩的推导而是想用大白话把Apollo里的MRAC讲清楚从控制思想、核心公式、代码实现到调参踩坑一次性说完。不管你是刚开始看Apollo代码的学生还是在实车上跟速度振荡较劲的工程师读完后应该都能对MRAC有个立体的理解。1. 先把MRAC这件事说人话1.1 一个“抄作业”式的控制思路MRAC这个名字听起来很高端但核心思想其实非常朴素给真实系统找一份“标准答案”然后让真实系统不断朝这份标准答案靠拢。更直白一点就是抄作业式的控制。想象一个考生他每次做模拟卷都觉得不太理想但每次都有一份标准答案可以参考。他拿到自己的答卷和标准答案一对比发现差在哪就赶紧调整解题思路下一次模拟考就更接近标准。真实车辆就是这位考生参考模型就是标准答案而MRAC里的自适应律就是那位不断帮你调整复习策略的教练。在Apollo的纵向控制场景里这个“考生”的性格还不稳定。今天空载车辆很轻油门稍微一踩速度就起来了明天满载同样的油门却拖泥带水再遇到一个坡道外部阻力又变了。固定参数的PID就像一位死记硬背的考生不管题目怎么变都用同一套套路MRAC则是考完一次就复盘一次根据实际表现动态调整参数确保下一次尽量贴近参考模型。1.2 参考模型到底在“参考”什么很多人会误解“参考模型”三个字以为它是某种理想控制器或者是一条最优轨迹。其实参考模型只是一个“标准动态系统”描述的是我们希望被控对象具有的动态响应。举纵向控制的例子。我们希望车辆在收到目标加速度后实际加速度的响应像一个一阶惯性环节时间常数取0.3秒那么参考模型就是a_m (1 / (0.3s 1)) * a_req这个式子说的是你给一个目标加速度理想的车辆不会瞬间达到而是按照0.3秒的时间常数平滑爬上去。这个“平滑爬上去”的过程就是参考模型的输出。但真实车辆有惯性、有阻力、有坡度干扰实际加速度并不会完美复现这个曲线。MRAC做的事情是不断比较“实际车辆加速度”和“参考模型输出”之间的误差然后在线调整控制量让实际加速度尽量像参考模型那样响应。也就是说MRAC不是直接去追踪目标速度而是先把真实车辆“掰成”参考模型那个好脾气再去控制这个好脾气的模型。1.3 标准MRAC的数学骨架虽然工程实现经常加各种保护逻辑但MRAC的数学骨架是相对固定的。我把最经典的一阶结构写出来再把每个符号的含义讲清楚。被控对象一般可以写成一个一阶系统x_p_dot a_p * x_p b_p * (u f)这里x_p是实际状态比如车速u是控制输入比如加速度请求f是未知的外部扰动或建模误差比如坡度、风阻、额外载重。对应参考模型x_m_dot a_m * x_m b_m * rx_m是参考模型的状态r是外部参考输入。理想情况下我们希望x_p跟着x_m走。控制律可以设计成u k_r * r k_x * x_p u_mrac前面两项用来匹配参考模型动态最后一项u_mrac是MRAC补出来的扰动抵消项。误差定义为e x_p - x_m然后设计自适应律让控制器参数随着误差变化。最常见的形式是k_dot -Gamma * e * phi其中Gamma是自适应增益phi是由状态和参考输入组成的回归向量。这个式子的物理意义很直观误差越大参数调整越猛误差为零参数就不再更新。为什么这样设计能稳定因为可以取一个李雅普诺夫候选函数比如误差平方加上参数估计误差平方再除以二倍增益沿着系统轨迹求导后是负半定的。这等价于说只要参数更新方向正确误差和参数误差都不会无限增长。教科书上那种一大堆推导本质就是在证明这句话。2. Apollo为什么偏偏要用MRAC2.1 车辆模型没有那么“诚实”在Apollo控制模块工作久了会发现车辆模型是所有控制方案里最不诚实的一环。平时算法仿真里给了一个固定质量实车却会因为乘客数量和载物变化出现百分之二三十的差异。更麻烦的是道路环境。同样一段路上坡和下坡对纵向控制来说完全是两个世界。坡度带来的重力和滚动阻力变化会在油门刹车控制上直接体现为持续的外部扰动。还有风速、路面附着系数、轮胎温度这些因素叠加起来任何一个固定参数模型都不可能覆盖所有工况。如果控制器里写死一份车辆质量空载时刹车可能偏重满载时油门可能偏小。我在一次测试里遇到过很有意思的现象固定前馈参数在平路上表现完美一到长下坡就持续超速方向盘都在帮着减速。原因就是控制器完全不知道下坡带来的额外加速度扰动一直用平路的逻辑在补偿。2.2 Apollo控制模块里MRAC的位置Apollo的控制模块通常分成横向控制和纵向控制两块。横向控制处理方向盘转向纵向控制处理油门和刹车。MRAC在纵向控制链路里扮演的是“自适应补偿器”的角色而不是一个独立站在最前面的控制器。简化后的纵向控制链路是这样的规划目标速度/加速度 ↓ 纵向控制器PID/前馈 ↓ MRAC自适应补偿叠加 ↓ 油门/刹车指令 ↓ 车辆底盘响应规划模块给目标是“我想跑多快”纵向控制器先把目标和当前速度做差产生一个基础的加速度请求然后MRAC根据实际车辆响应与参考模型之间的偏差生成一个补偿量叠加在请求上。这样设计有个好处MRAC不动PID的主框架只是在旁边打辅助因为模型误差和外部扰动本身就不是PID能直接处理的。2.3 和LQR/PID相比MRAC的优势不是“精度”而是“适应性”控制算法圈子里经常有个误解好像MRAC比PID精度高很多。其实MRAC的优势不在静态精度而在对模型失配和外部扰动的适应能力。下面这个表格可以比较直观地看出区别控制方法是否依赖精确模型是否在线自适应主要弱点PID不依赖靠误差反馈无固定增益不适应工况变化LQR依赖线性化模型无模型失配时性能大幅下降MPC依赖预测模型无计算量大模型质量直接影响控制MRAC需要参考模型有自适应律设计和调参复杂注意MRAC和LQR/MPC不是替代关系它更多是给一个已经能工作的控制系统增加“自愈能力”。Apollo里常见做法是LQR控制转向PID加前馈控制纵向MRAC在纵向控制中处理那些说不清道不明的阻力和质量变化。它不追求让每一帧误差都归零而是追求让真实车辆的动态特性和参考模型越来越接近。3. Apollo MRAC的核心实现与关键参数3.1 参考模型的设计Apollo里使用MRAC时第一个要定的是参考模型。这个模型不能拍脑袋随便写它直接决定了车辆应该表现成什么样子。如果做纵向控制参考模型一般就取成一阶惯性环节关键是确定时间常数tau。tau选得小参考模型响应快车辆就激进tau选得大响应慢车辆就肉。根据我个人的经验乘用车纵向加速度控制的时间常数通常在0.2到0.5秒之间对应到带宽大概就是2到5 rad/s。太激进的话车辆的传动间隙、悬挂振动等高频未建模动态会被MRAC放大结果就是速度振荡太保守的话车辆加速和刹车都慢半拍跟车体验很差。实际操作时不要直接猜可以先用仿真或者实测做一次阶跃响应。给车辆一个固定的目标加速度记下实际加速度爬升到63%左右所用的时间这个时间大致就是真实系统的时间常数。把参考模型时间常数定在比它略快一点的位置比如真实系统0.4秒参考模型取0.3秒这样MRAC有空间去补偿又不会要求真实系统做做不到的事。3.2 自适应律总要加一点“保险丝”标准教科书里的自适应律非常理想但在实车上直接搬过来用一定会出问题。最大的问题来自噪声和未建模动态传感器一抖误差不为零自适应律就开始积分猜出来的参数会在真实值附近乱飘。工程上至少要加两样东西死区和投影。死区是让自适应律在误差很小时冻结不给噪声积累机会。投影是把估计参数限制在物理合理的范围内比如车辆质量不能小于空载质量更不能是负数阻力系数也不能无限大。这两个保护看起来简单却是MRAC能不能在实车上跑起来的关键。我在调一辆测试车时就遇到过参数发散问题。当时没加投影MRAC估计出来的“额外阻力”到了离谱的数值车辆在平路上也被迫持续给油门后来又突然刹车。加上参数范围约束之后估计量稳定在了一个物理合理的区间问题立刻消失。所以看Apollo源码时如果你看到类似clip、filter、constrain这样的逻辑别嫌它们啰嗦那都是在实车上用教训换来的。3.3 从代码角度看MRAC的核心流程以我接触过的Apollo版本为例MRAC虽然涉及配置文件、控制器类、参数结构体但核心计算流程可以压缩成一个很短的伪代码循环void MracController::Compute(double error, double dt) { // 1. 计算参考模型的状态 x_m UpdateReferenceModel(target); // 2. 计算实际状态与参考模型的偏差 e x_p - x_m; // 3. 构造回归向量 phi GetRegressor(); // 4. 自适应律带死区和投影 if (fabs(e) dead_zone) { theta - Gamma * e * phi * dt; theta Project(theta, theta_min, theta_max); } // 5. 生成补偿控制量 u_mrac phi.dot(theta); }注意这一步求的是“把真实车辆拉近参考模型”的补偿量不是最终油门刹车开度。Apollo里通常还会对这个补偿量做限幅避免在极端情况下叠加出过大的加速度请求。有的工程实现里还会有低通滤波把高频抖动滤掉。这段代码是理解MRAC的钥匙。看懂了它再看Apollo里一堆配置项就不会晕了无非就是Gamma对应自适应增益dead_zone对应死区阈值theta_min/theta_max对应参数投影范围tau对应参考模型时间常数。3.4 MRAC和PID协同工作的几个关键细节MRAC和PID同时作用在同一个被控对象上很容易出现“打架”的情况。原因很简单PID已经在用误差反馈修正偏差MRAC也在用误差修正参数两个回路带宽重叠时会互相放大。我总结出来两个关键细节。第一MRAC的补偿分量最好以“慢回路”的形式工作目标是把中低频的模型误差和外部扰动抵消掉高频瞬态响应还是交给PID去处理。实际操作中会给自适应律加滤波让参数估计不要跟随每一次瞬时误差跳变。第二PID的积分项和MRAC的补偿项不要同时抢同一个频率段的控制权。如果PID的I增益很大MRAC会显得“多余”而且可能被PID的积分作用推成反向补偿如果PID的I增益太小MRAC又要扛下所有稳态误差压力很大。比较好的做法是调完PID之后再让MRAC去补偿残余误差。还有一个容易忽略的细节在控制模式切换、PID参数热更新、或者车辆经过剧烈工况时最好重置MRAC的历史状态。否则旧的参数估计会带着“上辈子的记忆”进入新工况不仅不能尽快收敛反而可能造成短时间的大幅补偿。4. 从仿真到实车MRAC调参与踩坑实录4.1 初步调参的四个步骤MRAC调参最怕一步到位直接乱试。我在项目里总结出一个相对稳妥的流程分四步走。第一步先把参考模型确定下来。在仿真环境里给目标加速度阶跃对比实际车辆加速度响应调整参考模型的时间常数和阻尼比让参考模型输出符合预期。第二步关闭MRAC只保留PID和前馈。跑一组平路和坡道工况记录稳态跟踪误差和动态误差。这一步的目的是拿到一个“不依赖MRAC也能稳定行走”的基线。第三步打开MRAC但把自适应增益给到非常小比如Gamma 0.001这个量级。观察补偿量和参数估计是否在缓慢收敛有没有朝不合理的方向漂移。这个阶段只验证自适应律的稳定性不求效果。第四步逐步放大Gamma每次翻倍或者加50%。在仿真里找到临界振荡点后再把Gamma回退到临界值的一半。实车上做最终验证时再保守一点回退到临界值的30%。这个方法看起来慢但能避免一上来就把参数调飞。MRAC最大的坑就是“仿真很好、实车突然炸”主要原因就是实车的噪声和执行器延迟让自适应律过度反应。4.2 常见问题速查表MRAC在Apollo里出问题翻来覆去就那么几类。下面这个表格是我平时排查问题时的速查清单现象可能原因排查思路处理建议车辆速度持续振荡自适应增益过大参考模型太激进执行器延迟查看MRAC补偿量是否在振荡降低Gamma给补偿量加低通滤波估计参数发散缺少投影约束噪声太大打印参数估计曲线加参数范围投影加死区增大滤波器时间常数坡道上补偿过猛投影范围设置过宽输出限幅太大对比上下坡时的补偿量收紧投影范围对MRAC输出限幅正常巡航稳定急加速时振荡执行器饱和导致自适应律误判查看油门刹车指令是否频繁饱和饱和时冻结自适应更新加入饱和检测逻辑MRAC和PID互相打架PID积分项与MRAC补偿带宽重叠分别观察PID输出和MRAC补偿量降低PID积分增益让MRAC主攻中低频这张表不是万能的但大部分我遇到过的实车问题都能在里面找到影子。尤其是“估计参数发散”这一条几乎每个MRAC项目都会经历一次。4.3 一个典型调参案例我印象很深的一次调试是在一个长下坡路段。那辆车空载坡度大概5%定速巡航目标60km/h。固定前馈加PID控制时车辆下坡初期会超速3到5km/h然后PID慢慢拉回来但整个过程油门刹车切换很频繁车内能明显感觉到一顿一顿的。我先在Apollo仿真里复现了这个场景把MRAC配置打开。一开始Gamma设得比较小参数收敛很慢但10秒后MRAC估计出了一个约-0.8m/s^2的外部加速度扰动也就是说它“猜到”了车辆在下坡有一股持续往前的力。补偿量叠加到控制指令里后实际速度误差从3km/h以上降到了0.5km/h以内油门刹车的切换频率也明显降低了。后来我把Gamma从0.002逐渐调到0.01收敛时间从15秒缩短到了5秒左右但继续往上调就开始出现轻微振荡。最终我放在了0.008也就是临界值附近偏保守的位置。这个案例说明MRAC调参的核心不是追求收敛越快越好而是找到一个“能在合理时间内跟上工况、又不至于被噪声带偏”的平衡点。5. 几个容易忽略的工程细节5.1 配置生效与动态开关Apollo控制模块支持动态配置更新意思是不用重启整个模块只要往控制配置对应的topic里发布新的配置控制算法就能在运行中重新读取参数。MRAC也不例外。我习惯在调参前先把原始配置存一份然后把每轮调试涉及到的参数改动和对应曲线都记录下来。这里强烈建议用配置管理和录包工具把这些参数、日志都保存好。每次只改一个参数改完就记录对应波形这比一次改五个参数然后根本分不清是谁起的作用要高效太多。另外注意运行中动态更新配置时MRAC状态不一定自动清零。如果正在实车上测试下发新配置后最好观察几秒必要时手动触发一次控制器重置再继续下一轮测试。5.2 如何用数据判断MRAC是否在工作很多人在Apollo里开了MRAC但根本不知道它有没有在工作只看速度误差确实很难判断。正确做法是盯住MRAC的补偿量输出和参数估计值。一个健康的MRAC补偿量应该随着工况变化而变化。平路上它趋近于一个稳定的小值上坡时它会变成正的补偿帮忙多给一点加速度下坡时它变成负的补偿帮忙收着点速度。这些补偿量应该在物理合理的范围内比如纵向加速度补偿在正负1m/s^2以内是比较常见的。如果MRAC估计出的扰动持续增长或者长期顶在限幅值上说明一定有地方出了问题。可能是参考模型选得太严苛车辆根本达不到也可能是传感器标定有问题导致误差里包含了非模型误差。这时候不要继续调Gamma要先回到底层确认车辆动态和参考模型的关系。5.3 什么时候该关掉MRACMRAC不是万能的有些工况下它反而会帮倒忙。紧急制动是完全需要关掉或者旁路MRAC的场景之一。ABS介入、轮胎接近附着极限时车辆纵向动力学高度非线性MRAC基于“模型失配但有界”的假设不再成立继续自适应更新只会让参数乱跳。低速蠕行也要注意。车辆在1m/s以下走走停停油门刹车响应非线性特别强死区又不容易覆盖MRAC很容易把蠕行抖动误判成模型误差并放大补偿。还有执行器饱和时比如油门已经踩到底车辆还是达不到目标加速度MRAC会持续增加估计参数导致退出饱和后出现大幅过冲。好的工程实现都会在这些边界条件下冻结自适应更新甚至直接把MRAC输出切到旁路。从我的角度看判断是否需要关掉MRAC和判断是否需要开启它同样重要。一个成熟的控制系统不是在所有工况下都调用所有模块而是知道什么场景下哪个模块该退避。最后再分享一个小经验。我刚开始用MRAC时总觉得自适应增益越大越高级结果在实车上被振荡教育了好几次。后来我养成了一个习惯每次调MRAC参数时只改一个变量并且一定会保存对应的配置和录包数据。这套方法虽然笨但非常可靠。如果你也在和Apollo的MRAC较劲不妨先别急着上大增益把参考模型和投影约束设计好再让自适应慢慢收敛它会给你意想不到的回报。