4、系统架构设计:感知-决策-控制三层架构详解,模块划分与接口定义

好,咱们进入正题。

车道保持系统,说白了就是让车自己待在车道里别跑偏。听起来简单?我当年第一次做这个项目时,天真地以为写个PID控制就完事了。结果车在高速上画起了龙,吓得测试工程师直拍窗户。

为什么会这样?因为缺少一个清晰的系统架构。

今天我就带你拆解这套经典的「感知-决策-控制」三层架构。这是我参与过三个量产项目后,觉得最靠谱的分层方式。

4.1 为什么非得是三层?

你想想看,一个完整的车道保持流程是这样的:

  1. 摄像头拍到车道线
  2. 算出车偏离了多少
  3. 决定要不要打方向盘
  4. 给转向系统发指令

这三件事性质完全不同。混在一起写,后期维护就是噩梦。我在项目中遇到过一位同事,把图像处理和方向盘控制写在一个函数里——嗯,后来那个模块没人敢动。

核心原则:每一层只关心自己的事,通过定义好的接口跟上下层通信。

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 核心功能

说白了,决策层就做三件事:

  1. 状态评估:车现在偏了多少?偏得快不快?
  2. 模式判断:是直道保持、弯道跟随,还是即将偏离需要紧急干预?
  3. 目标生成:算出期望的横向位置或期望的航向角
// 决策层输出接口定义
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 接口设计的三个铁律

最后,分享我总结的三个接口设计原则:

  1. 接口要稳定:一旦定下来,尽量不要改。改接口意味着上下两层都要改,牵一发动全身。
  2. 接口要自包含:每个结构体都带时间戳和有效性标志。别让调用方去猜「这个数据能不能用」。
  3. 接口要可测试:每一层的输入输出都能被单独录制和回放。我调试时经常录一段真实数据,然后反复喂给决策层看它的反应。

好了,架构设计就聊到这儿。下一章我们开始写真正的代码——从感知层的车道线检测算法入手。到时候我会手撕一段C代码,带你看看实际工程里是怎么处理那些坑的。