基础概念:规划模块与控制模块的接口定义

说实话,很多做规划控制的同学,一开始最容易栽跟头的地方,就是搞不清规划到底要给控制传什么。我见过不少项目,规划模块吭哧吭哧算了一大堆,结果控制模块根本用不上——说白了,接口没对齐。

今天我们就来把这个接口定义彻底讲清楚。我个人习惯把这个问题拆成两个层面来看:数据层面和语义层面。

一、规划模块输出什么?

规划模块的核心任务,是生成一条可执行的轨迹。注意,是「轨迹」,不是「路径」,也不是「速度曲线」。这三者的区别,我当年刚入行时也迷糊过一阵。

1. 路径(Path)

路径就是一系列空间点,描述的是「车往哪走」。每个点通常包含:

  • x, y:位置坐标
  • θ:航向角
  • κ:曲率(这个很重要,控制模块会用到)

路径本身不包含时间信息。它只告诉你「这条路长什么样」,不告诉你「什么时候该到哪」。

我的经验: 路径的曲率一定要平滑。我曾经在一个项目中,路径规划出来的曲率跳变太大,导致控制模块的MPC一直在震荡。后来加了曲率约束,问题就解决了。

2. 速度(Speed Profile)

速度曲线是在路径的基础上,给每个点分配一个期望速度。它描述的是「车开多快」。常见的形式是:

  • s-t 曲线:路程 s 随时间 t 的变化
  • v-s 曲线:速度 v 随路程 s 的变化

速度曲线里通常还包含加速度和加加速度(jerk)的约束。嗯,这里要注意:加速度的连续性直接影响乘客的舒适度。

3. 轨迹(Trajectory)

轨迹 = 路径 + 速度。它是一个时间序列,每个时间戳对应一个完整的状态。典型的轨迹点包含:

字段 含义 单位
t 时间戳 s
x, y 位置 m
θ 航向角 rad
v 速度 m/s
a 加速度 m/s²
κ 曲率 1/m

控制模块真正需要的是轨迹,而不是单独的路径或速度。为什么?因为控制模块需要知道「在什么时间点,车应该在哪、以什么速度、朝哪个方向」。你想想看,如果只给路径不给速度,控制模块怎么知道该加速还是减速?

二、控制模块需要什么?

控制模块的输入,说白了就是两个东西:参考轨迹和车辆状态。

1. 参考轨迹(Reference Trajectory)

这就是规划模块传过来的轨迹。但控制模块并不是全盘接收的。以我常用的MPC为例,它只会取未来N个时间步内的轨迹点作为参考。这个N就是预测时域。

关键点: 控制模块通常只关心轨迹的 前向一段,而不是整条轨迹。所以规划模块输出的轨迹要足够长,一般建议覆盖 5-10 秒的行驶距离。

2. 车辆状态(Vehicle State)

车辆当前的状态,包括:

  • 位置:x, y, θ
  • 速度:v
  • 横摆角速度:ω
  • 方向盘转角:δ(这个通常是控制输出,但也会反馈回来)

控制模块拿这些状态和参考轨迹做对比,算出偏差,然后输出控制量。

三、接口定义的核心原则

我在多个项目中总结出几条接口设计的原则,分享给你:

  1. 频率要对齐:规划模块的输出频率通常 10-20Hz,控制模块的运行频率通常 50-100Hz。控制模块需要插值来获取中间时刻的参考值。
  2. 坐标系要统一:规划模块常用 Frenet 坐标系,控制模块常用笛卡尔坐标系。接口处一定要做转换。我曾经吃过这个亏——规划给的是 Frenet 坐标,控制以为是笛卡尔坐标,结果车直接往路边冲。
  3. 时间戳要同步:轨迹点的时间戳必须是单调递增的,而且不能有太大的跳变。否则控制模块的插值会出问题。
  4. 异常处理:规划模块可能输出空轨迹或无效轨迹。控制模块要有兜底策略,比如保持上一帧的轨迹,或者紧急制动。
避坑指南: 我曾经在一个项目中,规划模块的轨迹点时间戳出现了重复。控制模块的插值逻辑直接崩溃,导致车辆急刹。后来我们在接口层加了一个时间戳校验,重复点直接丢弃。

四、接口数据流图

下面这张图展示了规划模块和控制模块之间的数据流动关系。我特意用 SVG 画出来,方便你理解整个链路。

规划模块 路径规划 速度规划 控制模块 PID / MPC 方向盘/油门/刹车 轨迹点序列 (t, x, y, θ, v, a, κ) 车辆状态反馈 (x, y, θ, v, ω) 🚗 车辆 控制指令 定位/感知信息

这张图里,规划模块输出的是轨迹点序列,控制模块接收后结合车辆状态反馈,计算出控制指令发给车辆。虚线表示反馈回路,这是闭环控制的关键。

五、代码层面的接口示例

我习惯用结构体来定义接口。下面是一个 C++ 风格的示例,实际项目中可以根据语言调整:

// 轨迹点定义
struct TrajectoryPoint {
    double t;       // 时间戳,单位 s
    double x, y;    // 位置,单位 m
    double theta;   // 航向角,单位 rad
    double v;       // 速度,单位 m/s
    double a;       // 加速度,单位 m/s²
    double kappa;   // 曲率,单位 1/m
};

// 轨迹定义
struct Trajectory {
    std::vector<TrajectoryPoint> points;
    double start_time;  // 轨迹起始时间
    double duration;    // 轨迹总时长
};

// 车辆状态定义
struct VehicleState {
    double x, y;
    double theta;
    double v;
    double omega;   // 横摆角速度
};

// 控制指令定义
struct ControlCommand {
    double steer;       // 方向盘转角,单位 rad
    double throttle;    // 油门开度,0-1
    double brake;       // 刹车压力,0-1
};

这个接口定义看起来简单,但实际项目中往往会有很多细节。比如轨迹点的时间间隔,我建议固定为 0.1s,这样控制模块插值起来方便。如果规划模块输出的是变时间间隔,控制模块就要做更复杂的插值处理。

我的习惯: 在接口层加一个 is_valid 标志位。如果规划模块算出来的轨迹不可行(比如碰撞检测失败),就把这个标志位置 false。控制模块看到 false 就直接进入安全模式。这个小设计救过我一次——有一次规划模块的求解器没收敛,输出了垃圾轨迹,幸好控制模块检测到标志位 false,直接刹停了。

六、PID 和 MPC 对接口的不同需求

PID 和 MPC 对轨迹接口的要求其实不太一样。我分别说一下:

  • PID 控制:通常只需要当前时刻的参考点,以及未来一小段的前馈信息。比如横向控制用的 Stanley 方法,只需要最近路径点的曲率作为前馈。
  • MPC 控制:需要未来 N 步的完整轨迹点。N 越大,计算量越大,但控制效果也越好。我一般取 N=20~30,步长 0.1s,也就是 2-3 秒的预测时域。

所以,如果你的控制模块是 MPC,规划模块输出的轨迹长度一定要足够覆盖预测时域。否则 MPC 算到后面没有参考点了,控制效果会变差。

好了,关于规划模块和控制模块的接口定义,就讲到这里。核心就是记住:轨迹是带时间戳的路径+速度,接口设计要关注频率、坐标系、时间戳和异常处理。下一节我们会深入讲 PID 的参数整定技巧,到时候会用到今天讲的这些接口知识。

可可精品资料库