结论: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 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,由进程管理层处理。这是运行时选择,不是对任意模型插件的兼容承诺。
延伸阅读
- openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么 —— 承接本文
controlsd的Ratekeeper循环与更新等待。 - 横纵向控制算法与状态边界 —— 逐项展开本文四种横向控制器与纵向 MPC 的实现细节。
- 进程生命周期、通信与设备服务 —— 解释本文四条数据流依赖的 manager、cereal 与 msgq 基础。
- CAN 报文解析的「健康位」在说什么:校验、计数器与总线空窗 —— 对照本文
radarState与carControl的有效位来源。 - 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 说明
carControl之后为何还有独立安全门。
参考资料
- 项目 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
发表回复