3、CACC核心算法:车辆动力学模型、经典ACC控制算法(PID、MPC)、CACC协同控制策略、队列稳定性分析

好,我们进入正题。这一章是CACC的“心脏”。说白了,你V2X通信搞得再好,如果控制算法拉胯,那车队的跟车效果就是一坨浆糊。我个人习惯把这一章拆成四个部分来讲:先搞清楚车怎么动(动力学模型),再搞定单车的ACC控制(PID和MPC),然后引入V2X变成CACC,最后用数学验证一下这个车队稳不稳。

3.1 车辆动力学模型:你得知道车会怎么动

做控制,第一步就是建模。你想想看,连被控对象的行为都描述不清楚,你怎么设计控制器?

在CACC里,我们最常用的是纵向动力学模型。我不建议搞得太复杂,什么悬架、轮胎侧偏全塞进去,那是做车辆动力学仿真的人干的。做控制,我们关注的是“给油”和“刹车”怎么影响车速和位置。

一个经典的简化模型长这样:

// 纵向动力学简化模型
// 加速度 a 由驱动力 F_drive、阻力 F_resist 决定
// m * a = F_drive - F_resist

// 阻力通常包括:空气阻力 + 滚动阻力 + 坡度阻力
// F_resist = 0.5 * ρ * Cd * A * v^2 + Cr * m * g * cos(θ) + m * g * sin(θ)

嗯,这里要注意。在实际项目中,我一般会把空气阻力项做线性化处理。为什么?因为非线性项会让后面的MPC求解变得很慢。我在一个量产项目里就吃过这个亏,模型太精确,结果控制器算不过来,周期跑飞了。后来我改成在平衡点附近做泰勒展开,用线性模型近似,效果反而更好。

对于CACC,我们更常用的是三阶状态空间模型

状态变量 物理意义 单位
p_i 第i辆车的绝对位置 m
v_i 第i辆车的速度 m/s
a_i 第i辆车的实际加速度 m/s²

控制输入通常是期望加速度 u_i,而实际加速度 a_i 和 u_i 之间有一个一阶惯性环节:

// 执行器延迟模型
// τ 是时间常数,通常取 0.1~0.5s
// a_i_dot = (1/τ) * (u_i - a_i)

这个τ值很关键。我记得有一次在实车测试时,发现跟车总是有0.3秒的延迟震荡,查了半天,就是τ值标定错了。避坑指南:τ一定要通过实车数据辨识,别拍脑袋给

3.2 经典ACC控制算法:PID与MPC

搞定了模型,我们来看看怎么控制单车。ACC的核心目标就两个:跟上车速保持车距

3.2.1 PID控制:简单粗暴,但够用

PID在ACC里是最常见的。你想想看,一个距离误差,一个速度误差,组合一下就是控制量。

// 经典ACC的PID控制律
// e_d = d_actual - d_desired  (距离误差)
// e_v = v_ego - v_target     (速度误差)

// 控制量 u = Kp * e_d + Kd * e_v + Ki * ∫e_d dt

// 其中 d_desired = d0 + t_gap * v_ego
// d0 是静止安全距离,t_gap 是时距

我个人习惯把时距t_gap设成1.2秒到2.0秒之间。为什么?太短了急刹车容易追尾,太长了容易被加塞。我在高速测试时试过1.0秒,结果前车急刹,我这边差点没刹住,冷汗都出来了。

但PID有个硬伤——它不知道未来。说白了,它只能根据当前的误差做反应。如果前车突然急减速,PID的响应天生就慢半拍。这就是为什么我们需要MPC。

3.2.2 MPC控制:看得更远,算得更准

MPC(模型预测控制)的核心思想是:基于当前状态,预测未来N步,然后求解一个最优控制序列

对于ACC,MPC的代价函数通常长这样:

// MPC代价函数
// min J = Σ (w1 * e_d(k)^2 + w2 * e_v(k)^2 + w3 * u(k)^2)

// 约束条件:
// v_min ≤ v(k) ≤ v_max
// a_min ≤ a(k) ≤ a_max
// u_min ≤ u(k) ≤ u_max
// d(k) ≥ d_safe  (安全距离约束)

我在项目中遇到过一个问题:MPC的预测时域选多长?选太短(比如5步),效果跟PID差不多;选太长(比如30步),计算量爆炸,嵌入式芯片根本跑不动。我建议预测时域取10~15步,控制时域取3~5步,这是一个比较折中的选择。

我的小技巧: 在MPC里加入“终端代价”项。就是在预测的最后一步,强制让状态收敛到平衡点。这样即使预测时域不够长,系统也不会发散。我试过,效果立竿见影。

3.3 CACC协同控制策略:V2X加持下的新玩法

好了,单车的ACC搞定了。现在加上V2X,我们就能玩出花来。

CACC和ACC最大的区别是什么?ACC只能“看”前车,CACC能“看”前前车,甚至头车。你想想看,如果你能提前知道前前车在刹车,你的反应时间是不是就多了零点几秒?

经典的CACC控制策略有两种:

  1. 前馈+反馈结构:用V2X接收前车的加速度作为前馈项,再用雷达测距做反馈修正。
  2. 一致性协议:所有车辆共享状态信息,通过图论和一致性算法让整个车队的速度和间距趋于一致。

我个人更推荐第一种,因为它对通信丢包更鲁棒。我在一个demo项目里试过第二种,结果通信一断,整个车队就乱套了。

前馈+反馈的CACC控制律可以写成:

// CACC控制律(前馈+反馈)
// u_i = K1 * (p_{i-1} - p_i - d_desired) 
//     + K2 * (v_{i-1} - v_i)
//     + Kff * a_{i-1}   // 前馈项,来自V2X

// 注意:a_{i-1} 是前车的实际加速度,通过V2X广播获得

这个Kff前馈增益很关键。理论上,如果模型完全准确,Kff=1就能完美补偿前车的加减速。但实际上,由于执行器延迟和模型误差,我一般把Kff设在0.7~0.9之间,留点余量。

警告: 千万不要完全依赖V2X的前馈信息!我曾经在测试时遇到过V2X丢包,前馈项突然变成0,结果控制量突变,车猛地顿了一下。一定要加一个“通信超时保护”,如果超过100ms没收到V2X数据,自动切回纯ACC模式。

3.4 队列稳定性分析:你的车队会不会“抖”起来?

这是CACC里最容易被忽视,但也是最要命的问题。队列稳定性(String Stability)指的是:一个扰动在车队中传播时,会不会被放大?

你想想看,如果头车轻轻点了一下刹车,结果传到第10辆车时变成了急刹车,那这个车队就是“不稳定”的。现实中,这种“幽灵堵车”就是这么来的。

判断队列稳定性的数学条件,通常用传递函数的H∞范数

// 队列稳定性条件
// 定义相邻两车的间距误差传递函数:
// G(s) = E_i(s) / E_{i-1}(s)

// 稳定的条件是:||G(jω)||∞ ≤ 1
// 即:对于所有频率ω,|G(jω)| ≤ 1

说白了,就是误差从上一辆车传到下一辆车时,幅值不能放大。这个条件在频域里很好验证。

对于CACC,由于有了前馈项,我们可以把传递函数写成:

// CACC的间距误差传递函数
// G(s) = (s^2 + K2*s + K1) / (s^2 + K2*s + K1 + Kff*s^2*e^{-τs})

// 其中τ是通信延迟

嗯,这里要注意。通信延迟τ是队列稳定性的“杀手”。我做过仿真,当τ超过200ms时,即使Kff调得再好,高频段也会出现幅值放大。所以,CACC对V2X的延迟要求非常苛刻,通常要求端到端延迟小于100ms

核心结论: 队列稳定性的本质是“误差不放大”。CACC通过前馈信息,理论上可以做到比ACC更好的队列稳定性,但前提是通信延迟足够小、模型足够准。如果做不到,还不如用ACC。

最后,我分享一个实战经验。在做队列稳定性分析时,不要只看理论条件,一定要做蒙特卡洛仿真。把通信延迟、模型参数误差、传感器噪声都随机化,跑上千次,看看最坏情况下的误差放大倍数。我在一个项目里就是这么发现问题的——理论分析说稳定,但仿真一跑,有5%的概率会发散。后来加了鲁棒性约束才解决。

好,这一章的内容就到这。下一章我们会讲具体的CACC代码实现,包括如何在嵌入式平台上部署MPC求解器。到时候我会把踩过的坑一个一个说清楚。