前言
如果你正在读这篇文章,大概率你已经接触过 ROS2 或 CyberRT,甚至可能正在维护一套自研的中间件系统。我也是这样------从事自动驾驶行业三年,从 ROS2 起步,到深度使用 CyberRT,再到参与 HyperFlow 的设计开发,对自动驾驶中间件有了不少踩坑和思考。
这个系列博客,我想把这些经验整理出来,聊聊如何从零搭建一套自动驾驶中间件。不是纸上谈兵的架构图,而是每个设计决策背后的工程考量和踩坑经验。
1. 先聊聊自动驾驶的"特殊性"
很多人会问:自动驾驶不就是机器人吗?为什么不能直接用 ROS2?
答案的关键在于自动驾驶对"确定性"的要求完全不同。
1.1 一个刹车数据的生命周期
考虑这个场景:前方 50 米有障碍物,自车时速 120km/h。
激光雷达检测到障碍物
→ 感知模块识别+分类 (~30ms)
→ 预测模块推演轨迹 (~10ms)
→ 规划模块生成避障路径 (~20ms)
→ 控制模块输出制动指令 (~5ms)
→ CAN 总线执行刹车 (~5ms)
──────────────────────────────────
总延迟:~70ms
在 120km/h 下,70ms = 车辆行驶 2.3 米
如果中间件的通信延迟再增加 10ms,刹车距离就多出 0.33 米。 在紧急场景下,这可能就是事故和安全的分界线。
这不是 ROS2 的 DDS 能轻松应对的------DDS 的序列化、发现协议、QoS 协商,每一层都在吃延迟。
1.2 自动驾驶的数据特征
| 特征 | 说明 | 对中间件的要求 |
|---|---|---|
| 高频传感器数据 | 激光雷达 10Hz、摄像头 30Hz、IMU 100Hz+ | 高吞吐、低延迟 |
| 固定拓扑 | 传感器→感知→预测→规划→控制,拓扑固定 | 不需要动态发现 |
| 多消费者 | 同一帧点云可能被感知、定位、可视化同时消费 | 零拷贝、一写多读 |
| 大尺寸数据 | 一帧点云 100KB~2MB,一帧图像 5~20MB | 避免数据拷贝 |
| 跨进程 | 安全隔离要求模块运行在不同进程 | 跨进程零拷贝通信 |
| 实时性 | 控制环路延迟 < 100ms | 可预测的调度延迟 |
1.3 嵌入式部署的现实
自动驾驶计算平台不是你的开发机:
- 算力受限:Orin NX 8GB 内存、8 核 A78,不像服务器可以随便开线程
- 温度约束:仪表盘下 85°C 环境,CPU 不能长时间满载
- 资源隔离:感知进程不能因为一个 bug 把控制进程的 CPU 抢光
- 车载诊断:需要对接 MCU 的 DTC 故障码体系
这些需求,ROS2 一个都不原生支持,CyberRT 覆盖了一部分,但没有一家能 100% 满足你的项目需求。
2. 三大框架的真实对比
我深度使用过这三个框架,说说我的真实感受(不是官方文档的搬运)。
2.1 ROS2 --- 优雅但不适合上车
优点:
- 生态无敌。rviz2、moveit2、nav2、rosbag2......你需要的工具基本都有
- DDS 标准化,理论上可以替换底层实现
- 社区活跃,遇到问题 Stack Overflow 能搜到
痛点:
- 延迟不可控:DDS 的发现协议(SDP)在启动时可能花数秒,运行中的通信延迟在 1~10ms 量级波动
- 序列化是硬伤:CDR 序列化对高频大数据(点云、图像)的开销不可忽视
- 进程内通信也要过 DDS:同一个进程的两个 Node 通信,数据还是要序列化→DDS→反序列化
- 没有调度:每个 Node 自己管线程,无法做全局调度优化
- 没有诊断:车载 DTC 体系完全需要自建
我的经历:在 ROS2 上做过原型验证,很爽。但一到性能优化阶段,就开始和各种 DDS 参数搏斗------SHM 传输配置、QoS 策略选择、零拷贝配置......每个 DDS 实现还不一样。
2.2 CyberRT --- 强大但封闭
优点:
- 共享内存通信:Intra-process 直接回调,跨进程走 SHM Transport,延迟很低
- 协程调度:Croutine 调度器,支持 Classic/Choreography 两种策略
- Protobuf:比 CDR 高效,代码生成也好用
- DAG 配置:可以通过 DAG 文件定义模块拓扑和调度策略
- 优先级调度:Choreography 策略支持优先级和 CPU 亲和性
痛点:
- 序列化仍然存在:Protobuf 序列化/反序列化在跨进程通信中仍有开销
- 闭源工具链:cyber_monitor、cyber_recorder 等工具不开源,出了问题没法调试
- 依赖 Apollo 全家桶:很难脱离 Apollo 生态独立使用
- 没有诊断系统:DTC 诊断需要自建
- 没有交互终端:运行时观测只能靠 cyber_monitor,无法交互操控
- 没有进程管理:依赖 Docker 容器做隔离,资源控制不够细粒度
我的经历:CyberRT 的调度设计确实牛,特别是 Choreography 策略。但 Protobuf 序列化在处理点云数据时还是能感受到开销,而且脱离 Apollo 单独使用 CyberRT 的文档几乎为零。
2.3 自研中间件 --- 适合你吗?
在决定自研之前,先问自己几个问题:
- 你的系统是否运行在嵌入式平台? 如果是 x86 服务器,ROS2 可能就够了
- 你是否需要 DTC 诊断? 如果不需要对接 MCU,CyberRT 可能就够了
- 你是否需要运行时交互调试? 如果能接受只有日志,ROS2 可能就够了
- 你的团队有多少人? 自研中间件至少需要 2-3 人持续维护
- 你是否需要极致的通信延迟? 如果 1ms 的波动可以接受,不需要自研
如果上述问题中你有 3 个以上的答案是"是",那么自研中间件值得考虑。
3. 自研中间件的设计哲学
在 HyperFlow 的设计中,我们坚持了三个核心原则:
3.1 零序列化
序列化是延迟的万恶之源。 在自动驾驶系统中,模块间传递的数据结构是固定的、平台是一致的,完全没有必要做序列化/反序列化。
传统方案: HyperFlow:
struct → serialize → bytes struct → 直接写入共享内存
bytes → deserialize → struct 共享内存 → 直接读取指针
代价是什么?跨平台兼容性。但自动驾驶的部署环境是固定的(Linux + ARM64/x86_64),这个代价完全可以接受。
3.2 零侵入
框架不应该侵入业务代码。 算法工程师不应该关心框架的存在。
- 数据读写:
Write<T>("data_name", data)/Read<T>("data_name", data) - 数据回灌:只需配置
"enable_snapshot": true,业务代码零改动 - 时间同步:回灌模式下
GetTimestamp()自动返回录制时间,业务代码无感知
3.3 可观测优先
运行时不知道系统在干什么是不可接受的。 嵌入式系统不像 Web 服务可以随时加日志。
- 内置 Telnet 终端:运行时可直接查看模块状态、数据统计、调整参数
- 内置诊断系统:DTC 故障码管理,直接对接 MCU
- 内置进程监控:CPU/内存实时统计,异常自动重启
4. 中间件的核心模块拆解
一个完整的自动驾驶中间件,至少需要以下模块:
┌─────────────────────────────────────────────────────┐
│ 自动驾驶中间件 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ 通信层 │ │ 调度层 │ │ 运维层 │ │
│ │ │ │ │ │ │ │
│ │ • 共享内存 │ │ • 协程调度 │ │ • 终端 │ │
│ │ • 零拷贝 │ │ • 事件驱动 │ │ • 诊断 │ │
│ │ • 跨进程 │ │ • 定时触发 │ │ • 监控 │ │
│ │ • 信号通知 │ │ • 触发器 │ │ • cgroup │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ 模块层 │ │ 数据层 │ │ 扩展层 │ │
│ │ │ │ │ │ │ │
│ │ • Module基类 │ │ • 数据录制 │ │ • 状态机 │ │
│ │ • 插件加载 │ │ • 数据回灌 │ │ • RPC │ │
│ │ • 工厂注册 │ │ • 数据快照 │ │ • 存储 │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
└─────────────────────────────────────────────────────┘
这个系列博客将逐一展开每个模块的设计与实现:
| 篇章 | 主题 | 核心内容 |
|---|---|---|
| (二) | 共享内存通信 | DataQueue 环形缓冲区、零拷贝读写、跨进程通知 |
| (三) | 事件驱动与协程调度 | marl 协程、Trigger 机制、定时/事件双模式 |
| (四) | 数据录制与回灌 | Snapshot 机制、MCAP 格式、frameid 匹配、时间源切换 |
| (五) | 状态机与诊断 | 层次化状态机、DTC 管理、进程监控与 cgroup |
| (六) | 交互式终端 | Telnet Server、命令树、按键状态机 |
5. 性能对比:数据会说话
为了给出一个直观的感受,我们在同一台机器上(Intel i7-12700H, 32GB DDR5, Ubuntu 22.04)做了简单的延迟对比测试:
注:以下数据为进程内通信的单次 round-trip 延迟,仅供参考。
| 框架 | 通信方式 | 平均延迟 | P99 延迟 |
|---|---|---|---|
| ROS2 (Humble) | Intra-process (FastDDS) | ~850μs | ~3.2ms |
| CyberRT | Intra-process (Intra) | ~45μs | ~120μs |
| HyperFlow | 共享内存零拷贝 | ~8μs | ~25μs |
差距的核心原因:
- ROS2:数据需要经过 DDS 层的序列化/反序列化,即使是进程内通信
- CyberRT:进程内通过直接回调避免序列化,但仍有调度开销
- HyperFlow:直接读写共享内存指针,无序列化、无额外调度层
当然,这个对比不完全公平------ROS2 有 QoS、CyberRT 有优先级调度,它们做的事情更多。但在自动驾驶的核心链路上,延迟就是安全。
6. 写在最后
自研中间件不是炫技,而是需求驱动。如果你的项目对延迟、资源、诊断有极致要求,现有方案无法满足,那就值得投入。
但也要清醒地认识到代价:没有生态、没有社区、所有工具都要自建。我的建议是从最小可用版本开始,逐步演进------先搞定通信和调度,再扩展诊断和终端。
下一篇,我们将深入 HyperFlow 的通信层,聊聊如何从零设计一套共享内存零拷贝通信机制。
下期预告:《从零搭建自动驾驶中间件(二):共享内存零拷贝通信的工程实践》