openpilot 感知到控制:四条数据流与 100 Hz 闭环

作者:

在

结论:openpilot 的感知到执行不是一个同步函数调用栈,而是 modeld、radard、plannerd、controlsd 四条进程经消息松耦合的数据流:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询节拍,controlsd 按 100 Hz 的 Ratekeeper 循环并最多等待 15 ms。同一轮控制里各输入可能来自不同采样时刻,因此不能按顺序函数调用推断一致性。本文为固定源码快照 c8fb906 的静态阅读,未加载模型、未连接车辆。

闭环与进程时序:四条数据流而不是一条调用栈

把整条链路理解成「摄像头 → 模型 → 规划 → 控制」的一次顺序调用,是最容易出错的心智模型。源码里 modeld 随图像帧运行,radard 和 plannerd 的 SubMaster 以 modelV2 为轮询主题,controlsd 则按 100 Hz 的 Ratekeeper 循环,以 selfdriveState 为轮询主题并有最多 15 ms 的更新等待。三条速率彼此不同,服务频率的定义见 services.py。

进程 驱动节拍 主要输入 主要输出
modeld 随图像帧 VisionIPC 相机流、CarParams、标定、车辆状态、驾驶员监测 modelV2、drivingModelData、cameraOdometry
radard 轮询 modelV2 carState、radarTracks、modelV2.leadsV3 radarState
plannerd 轮询 modelV2 modelV2、radarState、carState、CarParams longitudinalPlan、driverAssistance
controlsd 100 Hz carState、selfdriveState、modelV2、计划 controlsState、carControl

四者之间没有共享的同步函数栈,消息来自不同采样时刻是常态而非异常。这一点是后面所有边界讨论的前提:讨论「某一帧模型输出是否导致本轮转向」时,必须先确定这些消息是否属于同一次采样。

感知:modeld 从图像到结构化预测

main() 等待并连接 VisionIPC 相机流,读取 CarParams、标定、车辆状态与驾驶员监测信息,同步主相机与广角帧,计算图像变换和动作延迟,最后发布 modelV2、drivingModelData、cameraOdometry,并跟踪丢帧比例与相机时间戳。真正的模型装载在 ModelState.__init__():加载 tinygrad 序列化模型与 warp JIT,建立输入输出缓冲和递归状态对;模型文件、设备或编译任一步失败,初始化便无法继续。

函数或类 读入与处理 输出与边界
ModelState.pack_inputs() 将变换矩阵、动作条件、图像帧和状态输入装入连续缓冲 为外接 GPU 路径减少上传次数;缓冲布局本身是模型契约的一部分
ModelState.run() 复制 NV12 帧,按上升沿形成 desire 脉冲,上传输入,执行 warp 与模型,按切片解析输出 大模型路径显式拒绝非有限输出;可选保留原始预测
Parser.parse_outputs() 对轨迹、车道线、道路边缘、前车、意图和元信息执行 MDN / sigmoid / softmax 解码 结果供消息填充,不是最终车辆执行命令
get_action_from_model() 从 action 头或 plan 回退提取目标曲率/加速度,按速度与延迟平滑 生成 ModelDataV2.Action:desiredCurvature、desiredAcceleration、shouldStop

大模型选择逻辑在 main():检测到 Chestnut 后异步加载大模型,等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,交由进程管理层处理。这个回退是运行时选择,不能解释为对任意模型插件的兼容承诺。此外 extrinsics_calibration_seen 随标定数据更新并随模型消息发布,接入新相机时必须保留时间戳、图像几何和有效性语义。

前车融合与纵向规划:radard 与 plannerd

radard 不是「只转发雷达」的组件。即使没有雷达匹配,它也可能根据视觉预测产生 radar=False 的前车;反过来,低速场景可能用较近的雷达轨迹覆盖第一前车。Track.update() 按雷达 track ID 更新相对距离、横向位置、相对速度和滤波后的前车速度/加速度;match_vision_to_track() 用距离、横向位置与速度的概率分数选择轨迹;get_lead() 优先视觉与雷达匹配,无匹配但视觉概率足够时使用视觉前车。RadarD.update() 对齐自车速度与雷达延迟,结合 modelV2.leadsV3 形成 leadOne / leadTwo,RadarD.publish() 发布 radarState,有效位取自输入主题检查结果。

plannerd.main() 等待 CarParams,以 modelV2 为节拍更新纵向计划与车道偏离警告,分别发布 longitudinalPlan 与 driverAssistance。LongitudinalMpc.update() 依据两辆前车和驾驶风格设定跟车时距、障碍距离及约束,运行 acados MPC;run() 调求解器保存速度、加速度与 jerk 轨迹,非零求解状态会记录告警并 reset(),此时该轨迹不能当成成功解。LongitudinalPlanner.update() 从 MPC、巡航及实验模式的端到端动作候选中选择最小加速度作为目标,shouldStop 对各候选取逻辑或。

值得注意的是,所选加速度与 shouldStop 不一定来自同一候选,这种「最小加速度」合成是偏保守的候选合成,但不等于独立安全证明;求解器失败、输入无效、车型加速度能力和实际制动执行仍分别由其他层处理。车道偏离警告由 LaneDepartureWarning.update() 在高车速、近 5 秒未打转向灯且横向控制不活动时,结合车道线置信度、距离与变道意图触发,它不是转向控制命令。

controlsd:从计划到 carControl

Controls.__init__() 等待 CarParams,实例化车型接口、LongControl 与车辆动力学模型,并依转向控制类型或车型调参选择横向控制器;Controls.run() 以 100 Hz 调用 update()、state_control()、publish()。state_control() 计算使能、横向与纵向活动条件,执行纵向 PID 与横向控制,对目标曲率限幅,并将非有限执行器字段清零。

横向实现 算法要点 边界
角度接口 LatControlAngle.update() 用车辆模型把目标曲率转为目标转角,加入学习角偏置 执行器变化率仍由车型控制器与 panda 策略约束
曲率接口 LatControlCurvature.update() 从当前转角估计曲率,配置 PID 时做误差闭环,否则前馈 输出限到 MAX_CURVATURE 的 PID 范围,不能当扭矩
转角 PID LatControlPID.update() 以目标/当前转角误差闭环 安全限幅、驾驶员施力或低速时冻结积分
扭矩接口 LatControlTorque.update() 在横向加速度域闭环,补偿摩擦、坡度与响应延迟 使用 1 秒目标加速度环形缓冲与 jerk 滤波

state_control() 在 lateralManeuverPlan 有效时优先取其目标曲率,否则用 modelV2.action;停止横向活动时把目标重置为当前曲率以避免使能瞬间跳变。纵向 LongControl.update() 有 off / stopping / pid 三种状态,根据制动、巡航静止与 shouldStop 切换。需要特别注意的是,carControl 与 controlsState 的有效位当前仅赋值为 CS.canValid,源码注释指出若改为 sm.all_checks() 会形成循环依赖——因此消息有效位不能被误读为所有模型、计划与姿态输入都有效,下游状态机和 panda 安全门仍必须独立检查。

插件化时的接口边界

如果要按插件方式替换上述任一层,至少要保留该层的输入契约与有效性语义,而不是只替换一个数值输出。

可替换层 至少保留的契约 不应开放的能力
感知模型 VisionIPC 帧时间戳、相机标定、输出 schema、有效性/丢帧/延迟、启动预热与回退 直接发送车辆 CAN
雷达融合 radarTracks 与 modelV2 对齐、leadOne/leadTwo 来源标志、错误与时延 绕过状态机强制使能
规划器 longitudinalPlan 的轨迹、aTarget、停止/油门标志、求解状态和超时 把非成功解当成正常计划
横纵向控制器 CarParams 能力、活动条件、饱和反馈、非有限数处理、控制频率 修改 panda 安全模式或白名单

可迁移的路径是先建立消息契约与进程回放基线,再逐层替换算法实现。实车输出仍经 card 的车型接口和独立 panda 固件安全钩子,因此插件层的正确性并不等于执行链的完整性。

常见问题

为什么不能把 modelV2 到 carControl 当成一次函数调用?

因为各进程速率不同:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询主题,controlsd 按 100 Hz 循环。消息可能来自不同采样时刻,不存在保证它们同属一次采样的同步栈。

radard 是雷达数据的转发器吗?

不是。即使没有雷达匹配,也可能根据视觉预测产生 radar=False 的前车;低速场景还可能用较近的雷达轨迹覆盖第一前车。插件化时必须保留 track ID、延迟、误差和 present/radar 语义。

carControl.valid 为真就代表所有输入都有效吗?

不代表。当前 carControl 与 controlsState 的有效位仅取 CS.canValid;源码注释指出改成 sm.all_checks() 会形成循环依赖。因此它不能替代对模型、计划与姿态输入的独立检查。

大模型加载失败会怎样?

检测到 Chestnut 后异步加载大模型并等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,由进程管理层处理。这是运行时选择,不是对任意模型插件的兼容承诺。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(取用了四条进程数据流、modeld 感知链、radard 融合、纵向 MPC 与 controlsd 输出,以及插件化接口边界)。
  • 项目 02 —— openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(仅用于确认四种横向控制器与纵向执行状态的分层位置)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注