这是系列第五篇。前四篇聊了通信、调度、回灌这些"运行时"能力,这篇聊三个让系统从"能跑"变成"可靠"的关键能力:状态机、诊断、运维。
1. 为什么需要这三个能力?
自动驾驶系统不是跑一次就结束的批处理任务,它是一个7×24 运行的嵌入式实时系统。这意味着:
- 系统必须有明确的状态:INIT → STANDBY → AUTONOMOUS → EMERGENCY → ...,每个状态对应不同的行为
- 必须能检测故障:传感器失效、通信超时、算法异常......必须第一时间发现
- 必须能远程管控:进程崩溃要自动重启、资源超限要能限制、状态异常要能远程切换
没有这三个能力,你的系统在实验室里跑得再好,一上车也是"裸奔"。
2. 状态机:给系统一个"大脑"
2.1 自动驾驶系统为什么需要状态机?
自动驾驶系统的行为因状态而异:
INIT 状态:
- 所有模块初始化
- 传感器自检
- 等待 EOL(End of Line)信号
STANDBY 状态:
- 传感器正常工作
- 算法模块就绪
- 等待驾驶员触发"启动自动驾驶"
AUTONOMOUS 状态:
- 完全自动驾驶模式
- 执行感知→预测→规划→控制全链路
EMERGENCY 状态:
- 最小风险策略(靠边停车)
- 降级运行
- 等待人工接管
没有状态机的系统就像一个不知道自己在干嘛的机器人------它不知道什么时候该干什么,也无法在异常发生时做出正确的响应。
2.2 HyperFlow 的层次化状态机
HyperFlow 实现了一个层次化有限状态机(HFSM),以 Module 插件形式集成。
层次化意味着一个状态可以包含一个子状态机:
RootStateMachine
├─ INIT ──── InitSubMachine
│ ├─ SELF_TEST
│ └─ WAIT_EOL
├─ STANDBY
├─ AUTONOMOUS ──── DrivingSubMachine
│ ├─ CRUISE
│ ├─ LANE_CHANGE
│ └─ INTERSECTION
└─ EMERGENCY
子状态机让复杂的状态转换变得清晰------根状态机只关心大状态之间的跳转,子状态机负责细粒度的内部状态管理。
2.3 三种转换触发方式
方式一:事件触发
cpp
// 业务代码中触发事件
state_machine_module->Fire(EVENT_EMERGENCY);
方式二:超时自动转换
json
{
"name": "init_timeout",
"event_id": 2,
"to_state": 5,
"timeout": 5000,
"condition": "power_on == 1"
}
INIT 状态停留超过 5 秒且 power_on == 1,自动跳转到 NORMAL 状态。
方式三:条件表达式守卫
json
{
"name": "eol_event",
"event_id": 1,
"to_state": 2,
"condition": "speed == 0 && power_on == 1"
}
只有当 speed == 0 且 power_on == 1 时,事件 1 才能触发从 INIT 到 EOL 的跳转。
条件表达式基于 muParser 引擎,支持数学运算和逻辑表达式:
speed == 0 && power_on == 1
speed > 60
gear == 4 && brake_pressure < 10
2.4 状态机与框架的集成
状态机不仅仅是个独立的状态管理器,它与 HyperFlow 的通信和触发机制深度集成:
┌────────────────────────────────────────────────────────┐
│ StateMachineModule │
│ │
│ 状态变更时: │
│ ├── Write("sys.state", current_state_id) → 共享内存 │
│ │ 其他模块: Read("sys.state") → 获取当前状态 │
│ │ │
│ └── Notify("sys.state_changed") → Trigger │
│ 其他模块: Wait("sys.state_changed") → 被唤醒 │
│ │
│ 表达式变量更新: │
│ ├── SetValue("speed", 60.0) │
│ └── 下次 OnGuard() 自动使用新值 │
└────────────────────────────────────────────────────────┘
这意味着:
- 感知模块可以通过
Read("sys.state")获取当前系统状态,根据状态调整算法参数 - 规划模块可以通过
Wait("sys.state_changed")等待状态变更,切换规划策略 - 状态机的表达式变量(如 speed)可以从共享内存中的车辆数据实时更新
2.5 一个自动驾驶状态机的配置实例
json
{
"name": "system_state",
"type": "sys.StateMachineModule",
"interval": 100,
"expr_values": {
"speed": 0,
"power_on": 0,
"emergency_flag": 0
},
"state_machines": [
{
"name": "root",
"is_root": true,
"init_state": 1,
"states": [
{
"id": 1,
"name": "INIT",
"transitions": [
{
"name": "eol",
"event_id": 1,
"to_state": 2,
"condition": "speed == 0 && power_on == 1"
},
{
"name": "init_timeout",
"event_id": 2,
"to_state": 4,
"timeout": 30000,
"condition": "power_on == 0"
}
]
},
{
"id": 2,
"name": "STANDBY",
"transitions": [
{
"name": "engage",
"event_id": 3,
"to_state": 3,
"condition": "speed > 0 && emergency_flag == 0"
}
]
},
{
"id": 3,
"name": "AUTONOMOUS",
"transitions": [
{
"name": "disengage",
"event_id": 4,
"to_state": 2
},
{
"name": "emergency",
"event_id": 5,
"to_state": 4,
"condition": "emergency_flag == 1"
}
]
},
{
"id": 4,
"name": "EMERGENCY",
"transitions": [
{
"name": "recovered",
"event_id": 6,
"to_state": 2,
"condition": "emergency_flag == 0 && speed == 0"
}
]
}
]
}
],
"notify": ["sys.state_changed"],
"output": ["sys.state"]
}
2.6 运行时调试
通过 Telnet 可以实时操控状态机:
$ telnet localhost 8800
> cd modules/system_state/state_machine
# 查看当前状态
> state
1
# 查看所有表达式变量
> value
speed = 0
power_on = 0
emergency_flag = 0
# 设置变量值
> set power_on 1
done
> set speed 0
done
# 触发事件
> fire 1
switched
# 确认状态已切换
> state
2
这在实车调试时非常实用------不需要修改代码、不需要重启进程,一个 Telnet 命令就能切换系统状态。
3. 诊断系统:让故障无处遁形
3.1 为什么需要诊断?
自动驾驶系统中,故障是常态:
- 激光雷达被遮挡
- 摄像头帧率下降
- GPS 信号丢失
- CAN 总线超时
- 算法输出异常
问题不在于故障是否发生,而在于能否及时发现并处理。
3.2 AUTOSAR 风格的诊断
HyperFlow 的诊断系统参考了 AUTOSAR 的 DTC(Diagnostic Trouble Code)管理:
┌──────────────────────────────────────────────────────────┐
│ DiagModule (集中式诊断管理) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ DiagInfo 1 │ │ DiagInfo 2 │ │ DiagInfo 3 │ │
│ │ 传感器检查 │ │ 通信超时 │ │ 算法异常 │ │
│ │ stable: 3次 │ │ stable: 5次 │ │ stable: 2次 │ │
│ │ priority:HIGH│ │ priority:MED │ │ priority:LOW │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Step(): 周期检查所有 DiagInfo │
│ ├── 状态变更 → 更新 DTC 位图 │
│ ├── 故障上报 → ReportFail(event_id, priority) │
│ ├── 恢复上报 → ReportResume(event_id, priority) │
│ └── DTC 更新 → SendDtcData() → MCU │
└──────────────────────────────────────────────────────────┘
3.3 稳定性判断
诊断系统最重要的概念是稳定性判断:一次异常不能直接报故障,必须连续多次异常才确认。
时间轴: t1 t2 t3 t4 t5
传感器: OK FAIL FAIL FAIL OK
↑
连续 3 次 FAIL → 确认故障
stable_count = 3
json
{
"diag_infos": [
{
"enable": true,
"enable_report": true,
"stable_count": 3,
"stable_time": 1000,
"check_time": 100
}
]
}
stable_count:连续异常多少次确认故障stable_time:异常持续多久确认故障(ms)check_time:检查间隔(ms)
3.4 DiagComponent:模块内诊断
算法模块通过 DiagComponent 上报故障和恢复:
cpp
class PerceptionModule : public Module {
DiagComponent diag_;
void Step() override {
const auto* cloud = nullptr;
auto ec = Get("lidar_points", cloud);
if (ec || cloud == nullptr) {
diag_.ReportFail(EVENT_LIDAR_FAILURE, Priority::HIGH);
} else {
diag_.ReportResume(EVENT_LIDAR_FAILURE, Priority::HIGH);
}
}
};
ReportFail/ReportResume 的信息会通过共享内存传递给 DiagModule,由 DiagModule 统一管理 DTC 状态。
3.5 DTC 故障码
DTC 是车载诊断的标准格式,用于与 MCU 通信:
cpp
struct DtcData {
uint32_t dtc; // DTC 类型
uint32_t bits[32]; // 故障位图
bool CheckDtcType(uint32_t type);
bool CheckDtcBit(uint32_t bit);
};
DiagModule 维护一个 DTC 位图,每当诊断状态变更时更新位图,并通过 SendDtcData() 虚函数发送到 MCU:
cpp
class MyDiagModule : public DiagModule {
void SendDtcData() override {
// 通过 CAN/SOME-IP 发送 DTC 到 MCU
can_bus_->SendDtc(current_dtc_);
}
};
4. 进程管理:让系统自愈
4.1 为什么需要进程管理?
自动驾驶系统通常由多个进程组成:
Process: perception (CPU: 4核, 内存: 4GB)
Process: planning (CPU: 2核, 内存: 2GB)
Process: control (CPU: 1核, 内存: 512MB)
Process: lidar_driver (CPU: 1核, 内存: 256MB)
如果 perception 进程因为一个 segfault 崩溃了:
- 没有进程管理:整个系统挂掉,车失控
- 有进程管理:自动重启 perception,其他进程不受影响
4.2 ProcessManager
HyperFlow 的 ProcessManager 负责子进程的生命周期管理:
cpp
struct ProcessInfo {
string name;
string exe; // 可执行文件路径
string work_dir; // 工作目录
string args; // 启动参数
string envs; // 环境变量
string cgroup; // cgroup 资源限制
pid_t pid;
ProcessStatus status; // INIT / RUNNING / STOP / QUIT / ERROR
float cpu_usage;
uint64_t vm_rss_kb;
uint64_t vm_size_kb;
uint32_t wait_time; // 重启等待时间
bool enable;
};
核心能力:
- 自动启动:按配置顺序启动所有子进程
- 异常重启:检测到进程退出后自动重启
- 优雅停止:按依赖关系的逆序停止
- 状态监控:实时统计 CPU 和内存使用
4.3 cgroup 资源限制
在嵌入式平台上,必须防止某个进程吃光所有资源:
json
// process_manager.json
{
"cgroup": {
"enable": true,
"config": "config/cgconfig.conf"
}
}
conf
// cgconfig.conf
group perception {
cpu {
cpu.shares = 512;
cpu.cfs_quota_us = 400000; // 4核 × 100ms
cpu.cfs_period_us = 100000;
}
memory {
memory.limit_in_bytes = 4G;
memory.swappiness = 10;
}
}
group control {
cpu {
cpu.shares = 256;
cpu.cfs_quota_us = 100000; // 1核 × 100ms
cpu.cfs_period_us = 100000;
}
memory {
memory.limit_in_bytes = 512M;
memory.swappiness = 0;
}
}
为什么给 control 更高的优先级?
控制模块虽然 CPU 占用低,但对实时性要求最高。通过 cpu.shares 保证它能在需要时优先获得 CPU 时间。
4.4 ProcessMonitor
ProcessMonitor 是一个 Module,周期检查子进程状态:
cpp
void Step() override {
check_running(); // 检查进程是否存活
check_stop_timeout(); // 检查停止超时
stat_process(); // 统计进程 CPU/内存
stat_system_resources(); // 统计系统级资源
process_command(); // 处理远程控制命令
}
check_running() 的实现:
cpp
void check_running() {
for (auto& [name, info] : processes_) {
if (!info.enable) continue;
if (info.status != ProcessStatus::RUNNING) continue;
int status;
pid_t ret = waitpid(info.pid, &status, WNOHANG);
if (ret == 0) continue; // 进程正常运行
// 进程已退出
if (WIFEXITED(status)) {
LOG(WARNING) << "Process " << name << " exited with code " << WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
LOG(WARNING) << "Process " << name << " killed by signal " << WTERMSIG(status);
}
// 等待一段时间后重启
info.status = ProcessStatus::QUIT;
info.wait_time = 5; // 5秒后重启
}
}
4.5 远程进程控制
通过 ProcessControl 模块可以远程启停子进程:
cpp
// 在任何模块中
ProcessComponent proc;
proc.Init(this);
proc.StopProcess("perception"); // 停止感知进程
proc.StartProcess("perception"); // 重新启动
proc.GetProcessStatus("perception", &status); // 查询状态
结合 Terminal,可以在运行时远程控制进程:
$ telnet localhost 8800
> cd process
> ls
- status
- start
- stop
> status
perception: RUNNING, CPU=45%, MEM=2.1GB
planning: RUNNING, CPU=12%, MEM=512MB
control: RUNNING, CPU=3%, MEM=128MB
> stop perception
done
> start perception
done
5. 三者的协同:状态驱动的故障响应
状态机、诊断、进程管理不是孤立的,它们协同工作构成了完整的"可观测、可控制"体系:
┌──────────────────────────────────────────────────────────────┐
│ 故障响应流程 │
│ │
│ 1. DiagModule 检测到激光雷达连续 3 帧数据异常 │
│ └── ReportFail(EVENT_LIDAR_FAILURE, HIGH) │
│ │
│ 2. DiagModule 更新 DTC 位图 │
│ └── DTC[lidar_failure] = 1 │
│ │
│ 3. 状态机模块检测到故障条件 │
│ └── expr_values: emergency_flag = 1 │
│ └── State: AUTONOMOUS → EMERGENCY │
│ └── Notify("sys.state_changed") │
│ │
│ 4. 规划模块被唤醒,切换到最小风险策略 │
│ └── Read("sys.state") → EMERGENCY │
│ └── 执行靠边停车 │
│ │
│ 5. ProcessMonitor 记录事件日志 │
│ └── 日志: 故障时间、DTC码、状态转换记录 │
│ │
│ 6. 驾驶员接管后,故障恢复 │
│ └── ReportResume(EVENT_LIDAR_FAILURE, HIGH) │
│ └── emergency_flag = 0 │
│ └── State: EMERGENCY → STANDBY │
└──────────────────────────────────────────────────────────────┘
6. 与 ROS2 / CyberRT 的运维能力对比
| 维度 | HyperFlow | CyberRT | ROS2 |
|---|---|---|---|
| 状态机 | 内建 HFSM(层次化、条件表达式、超时) | 无 | 无(需 Smach 等外部库) |
| 诊断系统 | 内建 DTC 管理 | 无 | 无 |
| 进程管理 | ProcessManager + cgroup | Docker 容器 | launch 文件 |
| 进程监控 | ProcessMonitor(CPU/内存/异常重启) | 无 | 无 |
| 资源限制 | cgroup CPU/内存限制 | Docker 容器限制 | 无 |
| 远程控制 | ProcessControl + Terminal | cyber_launch | ros2 lifecycle |
| 故障恢复 | 自动重启 + 状态机切换 | 容器级恢复 | 节点 lifecycle |
HyperFlow 在运维能力上的核心优势:将状态机、诊断、进程管理深度集成,形成完整的故障检测→状态切换→恢复链路。
7. 踩坑总结
- 状态机的重入保护 :在
OnEnter/OnExit回调中不能调用Fire(),否则会触发重入检测。解决方案是用Notify延迟到下一个 Step - 诊断的稳定性判断 :
stable_count设太小容易误报,设太大又响应慢。建议 HIGH 优先级设 2~3 次,LOW 优先级设 5 次 - cgroup 在容器中的使用 :Docker 容器内默认无法操作 cgroup,需要
--privileged或手动挂载 cgroup 文件系统 - 进程重启的竞态 :进程崩溃后立即重启可能因为共享内存未清理而失败,建议
wait_time至少 3 秒 - 状态机的 Graphviz 可视化 :
StateMachine::ToGraphviz()可以生成 DOT 格式的状态拓扑图,对理解复杂状态机非常有帮助
8. 小结
状态机、诊断、运维是让自动驾驶系统从"能跑"变成"可靠"的三个关键能力:
- 状态机:给系统一个"大脑",让它在正确的时机做正确的事
- 诊断:给系统一双"眼睛",让故障无处遁形
- 运维:给系统一双"手",让它能自我修复
HyperFlow 将三者深度集成,形成了完整的"检测→切换→恢复"链路。这是 ROS2 和 CyberRT 都不具备的能力。
下一篇,也是最后一篇,我们将聊一个常被忽视但极其重要的功能------交互式终端。
下期预告:《从零搭建自动驾驶中间件(六):交互式终端------为嵌入式系统注入灵魂》