4、系统架构设计:感知-决策-控制三层架构详解,模块划分与接口定义
好,咱们进入正题。
车道保持系统,说白了就是让车自己待在车道里别跑偏。听起来简单?我当年第一次做这个项目时,天真地以为写个PID控制就完事了。结果车在高速上画起了龙,吓得测试工程师直拍窗户。
为什么会这样?因为缺少一个清晰的系统架构。
今天我就带你拆解这套经典的「感知-决策-控制」三层架构。这是我参与过三个量产项目后,觉得最靠谱的分层方式。
4.1 为什么非得是三层?
你想想看,一个完整的车道保持流程是这样的:
- 摄像头拍到车道线
- 算出车偏离了多少
- 决定要不要打方向盘
- 给转向系统发指令
这三件事性质完全不同。混在一起写,后期维护就是噩梦。我在项目中遇到过一位同事,把图像处理和方向盘控制写在一个函数里——嗯,后来那个模块没人敢动。
核心原则:每一层只关心自己的事,通过定义好的接口跟上下层通信。
4.2 感知层:让车「看见」车道
感知层的任务很纯粹:从传感器数据中提取出车道线信息。
4.2.1 输入与输出
| 项目 | 内容 |
|---|---|
| 输入 | 摄像头图像(YUV420格式,30fps) |
| 输出 | 车道线参数(多项式系数、置信度、线类型) |
| 接口形式 | 结构体 LaneLine_t |
// 感知层输出接口定义
typedef struct {
float poly_coeff[4]; // 三次多项式系数: y = c0 + c1*x + c2*x^2 + c3*x^3
float confidence; // 0.0 ~ 1.0
uint8_t line_type; // 0:虚线 1:实线 2:双实线
float start_x; // 车道线起始x坐标(像素)
float end_x; // 车道线终止x坐标(像素)
} LaneLine_t;
我的经验:置信度这个字段特别重要。我曾经见过一个系统,明明车道线都模糊得看不清了,还硬输出一组假数据,结果车子直接往护栏冲。后来我要求:置信度低于0.6时,直接给决策层发「无效」信号。
4.2.2 模块内部划分
- 图像预处理:去畸变、灰度化、ROI裁剪
- 特征提取:边缘检测、霍夫变换或深度学习分割
- 车道线拟合:RANSAC或最小二乘法
- 跟踪与验证:卡尔曼滤波、时间一致性检查
4.3 决策层:判断「要不要管」
决策层是系统的「大脑」。它不关心像素,只关心状态。
4.3.1 核心功能
说白了,决策层就做三件事:
- 状态评估:车现在偏了多少?偏得快不快?
- 模式判断:是直道保持、弯道跟随,还是即将偏离需要紧急干预?
- 目标生成:算出期望的横向位置或期望的航向角
// 决策层输出接口定义
typedef struct {
float target_lateral_offset; // 期望横向偏移量(米), 正数表示偏左
float target_yaw_rate; // 期望横摆角速度(rad/s)
uint8_t control_mode; // 0:保持 1:跟随 2:紧急修正 3:退出
float urgency_level; // 紧急程度 0.0~1.0
} ControlTarget_t;
避坑指南:我曾经犯过一个错误——决策层直接输出方向盘角度。后来发现不同车型的转向比不一样,换辆车就得改代码。正确的做法是输出物理量(横向偏移、横摆角速度),让控制层去适配具体执行器。
4.3.2 状态机设计
决策层内部通常是一个有限状态机。我习惯用枚举定义状态:
typedef enum {
LKA_IDLE, // 未激活
LKA_ACTIVE, // 正常保持
LKA_APPROACHING, // 接近车道线
LKA_EMERGENCY, // 即将偏离,紧急干预
LKA_FAILED // 系统故障
} LKA_State_t;
每个状态都有进入条件、执行逻辑和退出条件。嗯,这里要注意:状态切换一定要有滞回区间,否则系统会在边界来回跳。我见过一个demo车,在车道线附近来回修正,乘客坐完直接晕车。
4.4 控制层:让车「动起来」
控制层是执行者。它拿到决策层的目标,算出具体的控制量发给执行器。
4.4.1 控制策略
我个人比较推荐的做法是:
- 前馈控制:根据道路曲率预判需要的转向角
- 反馈控制:PID或LQR修正偏差
- 补偿控制:处理转向系统死区、延迟
// 控制层输出接口定义
typedef struct {
float steering_angle; // 方向盘角度(度)
float steering_angle_rate; // 方向盘角速度(度/s)
float torque_request; // 扭矩请求(Nm)
uint8_t safety_check; // 安全检查通过标志
} ActuatorCommand_t;
我的习惯:控制层一定要加限幅和速率限制。你想想看,如果感知层突然跳了一下,决策层跟着跳,控制层直接打满方向盘——那画面太美我不敢看。我一般会在控制层加一个低通滤波器,截止频率设在2Hz左右。
4.5 层间通信与时序
三层架构跑起来,时序是关键。我见过一个项目,感知层跑30fps,决策层跑100fps,控制层跑50fps——结果数据全是乱的。
| 层级 | 推荐频率 | 最大延迟 |
|---|---|---|
| 感知层 | 30 Hz | 33 ms |
| 决策层 | 50 Hz | 20 ms |
| 控制层 | 100 Hz | 10 ms |
为什么控制层要跑最快?因为转向执行器有响应延迟,控制频率低了车会晃。决策层不需要那么快,因为车道线变化没那么剧烈。感知层受限于摄像头帧率,30fps是主流。
关键设计原则:每一层都使用独立的时间戳。不要依赖「上一帧」这种相对概念,直接用绝对时间戳做插值和同步。这是我被坑过好几次才学乖的。
4.6 接口设计的三个铁律
最后,分享我总结的三个接口设计原则:
- 接口要稳定:一旦定下来,尽量不要改。改接口意味着上下两层都要改,牵一发动全身。
- 接口要自包含:每个结构体都带时间戳和有效性标志。别让调用方去猜「这个数据能不能用」。
- 接口要可测试:每一层的输入输出都能被单独录制和回放。我调试时经常录一段真实数据,然后反复喂给决策层看它的反应。
好了,架构设计就聊到这儿。下一章我们开始写真正的代码——从感知层的车道线检测算法入手。到时候我会手撕一段C代码,带你看看实际工程里是怎么处理那些坑的。