基础概念:规划模块与控制模块的接口定义
说实话,很多做规划控制的同学,一开始最容易栽跟头的地方,就是搞不清规划到底要给控制传什么。我见过不少项目,规划模块吭哧吭哧算了一大堆,结果控制模块根本用不上——说白了,接口没对齐。
今天我们就来把这个接口定义彻底讲清楚。我个人习惯把这个问题拆成两个层面来看:数据层面和语义层面。
一、规划模块输出什么?
规划模块的核心任务,是生成一条可执行的轨迹。注意,是「轨迹」,不是「路径」,也不是「速度曲线」。这三者的区别,我当年刚入行时也迷糊过一阵。
1. 路径(Path)
路径就是一系列空间点,描述的是「车往哪走」。每个点通常包含:
- x, y:位置坐标
- θ:航向角
- κ:曲率(这个很重要,控制模块会用到)
路径本身不包含时间信息。它只告诉你「这条路长什么样」,不告诉你「什么时候该到哪」。
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就是预测时域。
2. 车辆状态(Vehicle State)
车辆当前的状态,包括:
- 位置:x, y, θ
- 速度:v
- 横摆角速度:ω
- 方向盘转角:δ(这个通常是控制输出,但也会反馈回来)
控制模块拿这些状态和参考轨迹做对比,算出偏差,然后输出控制量。
三、接口定义的核心原则
我在多个项目中总结出几条接口设计的原则,分享给你:
- 频率要对齐:规划模块的输出频率通常 10-20Hz,控制模块的运行频率通常 50-100Hz。控制模块需要插值来获取中间时刻的参考值。
- 坐标系要统一:规划模块常用 Frenet 坐标系,控制模块常用笛卡尔坐标系。接口处一定要做转换。我曾经吃过这个亏——规划给的是 Frenet 坐标,控制以为是笛卡尔坐标,结果车直接往路边冲。
- 时间戳要同步:轨迹点的时间戳必须是单调递增的,而且不能有太大的跳变。否则控制模块的插值会出问题。
- 异常处理:规划模块可能输出空轨迹或无效轨迹。控制模块要有兜底策略,比如保持上一帧的轨迹,或者紧急制动。
四、接口数据流图
下面这张图展示了规划模块和控制模块之间的数据流动关系。我特意用 SVG 画出来,方便你理解整个链路。
这张图里,规划模块输出的是轨迹点序列,控制模块接收后结合车辆状态反馈,计算出控制指令发给车辆。虚线表示反馈回路,这是闭环控制的关键。
五、代码层面的接口示例
我习惯用结构体来定义接口。下面是一个 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 的参数整定技巧,到时候会用到今天讲的这些接口知识。