标签: openpilot

  • 相机帧号的三重编号:request_id、IFE 帧号与发布帧号为什么不能混用

    结论:相机链路里同时存在三个不同的编号——request_id(req_mgr 队列请求号)、内核 IFE frame_id(每次 SOF 递增的内核计数)与归一化后的published frame_id。只有第三个才是跨相机配对可用的键;直接拿原始 IFE 计数当配对键,在座舱与路面这类错相路之间会整体错位。三路 SOF 的预期关系是:非错相路之间相差 0,座舱与路面路之间相差 25 ms,误差阈值 0.2 ms。此外 timestamp_eof 是「SOF 加读出时间」推算的值,不是硬件 EOF 事件,processing_time 也不能当作曝光时长。以下均为固定快照的静态结构分析,未运行相机、模型或实板测试。

    三个编号:request_id、IFE 帧号与发布帧号

    handle_camera_event()从 req_mgr 的 SOF 事件里一次读出三样东西:request_id、内核 IFE frame_id 和内核 IRQ 时间戳。request_id 是队列请求号,IFE frame_id 是每次 SOF 递增的内核计数,两者都不是最终发布给模型的帧号。validateEvent()在 skip_expected 为假时分别检查两种序号是否各自连续;request_id == 0 会被跳过,连续无效超过环深加二就触发重排。

    真正决定跨相机对齐的是 syncFirstFrame():它为每一路记录首次 SOF 时间,并把 ife_frame_id + 1 存为该路的帧号偏移;等所有已启用相机都到齐且时间关系满足后,才设置共享的同步完成标志。processFrame()此后才计算 published frame_id = ife_frame_id - frame_id_offset。换句话说,进入同步状态的那一次调用仍返回 false,下一帧才可能成为首个发布帧。

    编号 来源 是否可作跨相机配对键
    request_id req_mgr SOF 事件读出的队列请求号 否;request_id == 0 会被跳过,仅供本路连续性检查。
    IFE frame_id 每次 SOF 递增的内核计数 否;三路 IFE 计数可以不同,不能直接当配对键。
    published frame_id ife_frame_id - frame_id_offset 是;偏移在首帧对齐时按路记录。

    还有一条恢复边界:若某路的 IFE 帧号已超过 40 且仍未对齐,代码会记错误并把共享同步标志置为已同步,但没有在该分支证明各路满足了 0.2 ms 关系。此时若某已启用路尚无首帧记录,它后续首次进入已同步分支时用的偏移状态需要另行动态验证。

    时间域:SOF、推算 EOF 与处理时间

    发布出去的几个时间字段语义差别很大。timestamp_sof 来自内核 SOF IRQ,是 modeld 配对窄路与广路图像的依据;timestamp_eof 则是 processFrame() 用 SOF 加 readout_time_ns 推算出来的,读出的只是估计值而非硬件 EOF 事件。

    字段 来源与含义 下游影响
    timestamp_sof handle_camera_event()读取的 SOF IRQ 时间 modeld 按它配对窄/广路图像。
    timestamp_eof processFrame() 用 SOF 加 readout_time_ns 推算 cameraOdometry 携带它,locationd 再减模型姿态延迟并做时间校验。
    processing_time 等待 fence 后,以当前单调时间减估计 EOF 写入相机状态;不能当曝光时长或端到端控制延迟。
    VisionIpcBufExtra sendFrameToVipc() 附带归一化帧号、SOF、EOF encoderd 核对缓冲帧号与通知帧号,失配时跳帧。

    读出时间本身是传感器相关常量:OS04C10 与 OX03C10 各有自己的取值。开发版的 webcamerad._send_yuv() 把 frame_id * 0.05 s 同时当作 SOF 和 EOF,状态消息只带帧号和单位变换——它能替代输入用于开发,却不能验证硬件时钟同源、真实读出时间或三路同步。

    同步建立:25 ms 错相、0.2 ms 阈值与 40 帧兜底

    同步阈值集中在一处配置里,含义需要分清「预期差值」与「允许误差」。非错相路之间预期相差 0;座舱路与路面路之间预期相差 25 ms;可接受的误差是 0.2 ms。这些是当前实现与测试里的常量,不是任意硬件的通用标准。

    量 取值 说明
    非错相路预期差 0 同相两路应同时曝光。
    座舱与路面预期差 25 ms 错相路的标称错位。
    同步误差阈值 0.2 ms 判断某路是否满足时间关系。
    未对齐放弃门 40 帧 超过后置同步标志,但不证明各路满足 0.2 ms。

    重排会记录 last_requeue_ts,并允许预期中的序号空洞出现。收到过旧 SOF、故障注入跳过、序号不连续,或 waitForFrameReady() 等待 IFE/BPS fence 失败时,当前帧都不会发布。这些行为的共同点是:链路并不保证「图像流始终连续」。

    消费者各用各的同步策略

    同一个相机流,三个消费方用的对齐策略并不统一。modeld 先取得比上一广角 SOF 至少晚 25 ms 的主帧,再读取广角帧;两路 SOF 相差超过 10 ms 时记录错误日志,但该分支仍会进入后续推理,没有直接丢弃这对图像。模型 frameId 来自 VisionIPC 主帧,frameIdExtra 来自另一路,而 frameAge 是拿异步收到的 narrowRoadCameraState.frameId 与主帧比较,不能误读成同一缓冲的自身年龄。

    encoderd.sync_encoders() 在启动时选跨路共同起始帧,并在 encoder_thread() 里按帧号决定切段,它不按 SOF 重新校正物理曝光时刻。因此相机帧号重置、断流重连或缺一路时,编码器启动门与模型配对门是否仍符合预期,需要单独验证。相机状态消息则先发布 VisionIPC、再填充发送状态消息,两条传输路径不应仅凭到达顺序关联,而应以帧号和时间戳核对。

    硬件插件接口与验收

    建议相机插件输出同一采集 epoch 下的 stream_id、request_id、原始硬件帧号、归一化帧号、单调时钟域 SOF、EOF 来源(实测或估计)、读出时间、缓冲有效性和同步状态,并把 sync_lost、重排代数与丢帧原因作为可观测事件。平台适配器负责时钟换算、帧号归一化和缓冲所有权;模型与编码插件只消费明确标注时间质量的帧,不可把 webcam 合成时间或日志发布时间暗中转成硬件 SOF。

    现有 test_camerad.py 是硬件门控测试,检查帧号连续、窄/广路 SOF 差小于 1.1 ms、座舱错相 20–30 ms、EOF 晚于 SOF,并有故障注入下的序号跳变检查;这些测试本轮未运行。新硬件还应在台架与实车分别测试启动缺一路、40 帧超时、掉帧与重排、时钟漂移、读出时间误差、IPC 复用以及编码与模型重连。

    常见问题

    IFE 帧号能直接当两路相机的配对键吗?

    不能。IFE frame_id 是三路各自递增的内核计数,彼此可以不同;跨相机配对要用减去首帧偏移后的 published frame_id,而偏移由 syncFirstFrame() 在首帧对齐时逐路记录。

    timestamp_eof 是硬件 EOF 事件吗?

    不是。它是 processFrame() 用 SOF 加 readout_time_ns 推算出来的估计值,读出时间随传感器不同(OS04C10、OX03C10 各有取值);processing_time 也不是曝光时长。

    座舱与路面路的错相预期是多少?

    非错相路之间预期相差 0,座舱路与路面路之间预期相差 25 ms,误差阈值 0.2 ms。这是当前实现与测试的常量,不是任意硬件的通用标准。

    两路 SOF 相差超过 10 ms,模型会丢帧吗?

    源码在该分支记录错误日志,但仍进入后续推理,并没有直接丢弃这对图像。实际的丢帧根因需要区分采集、IPC 与模型消费落后三种情况。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_FRAME_TIME_SYNC_CONTRACT_DEEP_DIVE.md(取用了三个编号的定义、首帧对齐偏移、25 ms/0.2 ms/40 帧阈值、SOF 与推算 EOF 字段表、消费者同步策略差异与硬件测试项)。

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

    更新记录

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

  • PyTorch PrivateUse1 前端:ATen 映射、别名回写与设备语义

    结论:这层 PyTorch PrivateUse1 前端是把 ATen 算子映射到 tinygrad Tensor 的开发兼容桥,不是车端实时推理后端的替身:导入即产生进程全局副作用(关闭 autograd 多线程、即时编译 C++ 扩展、把 PrivateUse1 重命名为 tiny 并注册后端),而设备守卫基本不做真实同步——不能从事件查询或流同步返回推导底层 tinygrad 工作已经完成。当前可见的车端模型进程使用 tinygrad ONNX 编译脚本并消费已捕获的模型,没有把 torch.device('tiny') 或本适配桥放入车端推理主链。本文为固定源码快照的静态阅读,未导入该前端,因此未触发扩展编译或改变本机 PyTorch 注册状态。

    导入与原生张量所有权

    实现点 函数级行为
    导入段 关闭 autograd 多线程,即时编译 C++ 扩展,把 PrivateUse1 重命名为 tiny,注册后端并生成设备方法;这些是进程全局副作用
    TinyBackend/设备守卫 Python 设备数读取 GPU 数(默认 1),C++ 设备数固定为 1;守卫的设备、流、事件操作基本不做实际同步
    wrap()/unwrap()/wrap_tensor() Python Tensor 作为安全对象被原生张量实现持有;Torch 元数据使用 tinygrad 的形状、重算的连续 strides 与存储偏移
    calculate_storage_offset()/update_metadata() 仅对收缩类操作累加基于连续布局的偏移;源码注明重排、扩展、填充、翻转等别名情形并不正确

    unwrap() 经动态类型检查取回原 Python 对象,不拥有独立的 Torch 存储。原生层允许后续更新尺寸、strides 与偏移,但元数据变化不自动证明真实存储视图语义正确;calculate_storage_offset() 的注释也明确指出,除收缩外的别名情形偏移计算并不正确。换句话说,Torch 侧并不拥有独立存储,视图与元数据都由 tinygrad 侧重算,因此不能假设两者可以各自独立演进。

    分发、视图与副作用

    入口 执行链与限制
    wrap_view_op()/视图操作表 视图、转置、压缩、选择、重排、切片等记录基础张量、派生视图弱引用与操作链,以便后续原位写入回放
    _view_write()/_apply_inplace() 纯 reshape 快路径直接改写基础张量;一般视图用索引张量计算扁平位置,克隆后 scatter,再替换基础并刷新派生视图
    wrap_out()/_inplace_op() out= 默认允许按输出 dtype 转换,随后断言形状、设备与类型并写回;原位操作按是否存在派生视图选择替换或别名传播
    包装器注册循环 扫描 ATen schema 的可写参数与视图原位标签,分别选择普通、首参原位、视图原位或 out 包装器;未识别写入模式直接抛错

    这里最需要留意的不是数值本身,而是别名语义:视图写入走「计算扁平位置、克隆、scatter、替换基础张量」的路径,重叠索引写入、复杂别名与异常中断后的对象关系应列入兼容性测试,不能假设完全等同 PyTorch 原生存储。out= 默认允许按输出 dtype 转换,意味着需要用数值测试确认与目标 PyTorch 版本的舍入语义;导入成功也不代表任意 ATen 算子都已实现。

    算子路径与兼容缺口

    算子表把数学、比较、归约、scatter、矩阵乘等 out= 形式与一般、原位、创建、随机、池化、排序和部分线性代数分开组织。其中两处静态可见差异是:empty_strided 明确忽略请求 stride 改为连续分配,logsumexp 映射只取第一个轴——因此不能拿「算子名已注册」当作数值等价证明。卷积正反向有独立映射,池化、填充和插值也有独立正反向;批量归一化管理运行统计量与梯度,必须同时验证训练和评估路径。

    路径 行为 兼容缺口
    归约与形状 empty_strided 改连续分配,logsumexp 只取首轴 语义与原生 PyTorch 有差异
    CPU 回退 部分索引写入、特征分解与插值反传显式搬到 CPU 再搬回 非阻塞参数没有独立异步语义
    设备互转 CPU 与 tiny 转换使用 NumPy 或复制 不能据名称承诺时限或零拷贝
    分解注册 为若干复合算子装配 PyTorch 分解,列表含禁用项与已知不适用路径 完整范围受版本、扩展编译与未覆盖算子影响

    该分解注册段还含注释中的禁用项与已知不适用路径;完整兼容范围受 PyTorch 版本、扩展编译、当前后端及未覆盖算子影响,需要以目标版本运行矩阵证明。非阻塞参数在对应转换函数里没有实现独立异步传输语义,不能据其名称承诺时限或零拷贝。总体而言,导入成功只说明注册流程跑通,可见的算子表与回退路径才是实际兼容边界。

    openpilot 与硬件插件边界

    当前可见的车端模型脚本使用 tinygrad ONNX 编译,运行时消费已捕获的模型,并没有把 torch.device('tiny') 或本适配桥放入车端推理主链。因此 Torch 前端及其动态 C++ 编译应限定在研发与训练进程,与 CAN、panda 安全配置和控制消息发布隔离。若把它作为硬件原型验证工具,必须记录实际执行设备、CPU 回退次数、数据搬运字节数、同步完成点、算子误差与最坏时延,不能仅凭 tiny 标签判断任务在目标加速器上完成。本轮逐段静态核对原生包装、别名与写入、注册机制、算子表和特殊回退,也未对每个 ATen 重载做版本化自动枚举。

    常见问题

    为什么 torch.device('tiny') 不能放进车端推理?

    因为这一层是开发兼容桥而非车端后端:导入会产生进程全局副作用,设备守卫基本不做真实同步,且部分算子有语义差异或在 CPU 回退。当前车端模型脚本使用 tinygrad ONNX 编译并消费已捕获的模型,没有把该桥放入推理主链,把它与 CAN、安全配置和控制发布隔离更稳妥。

    同步流返回就等于底层计算完成了吗?

    不能这样推导。设备守卫的设备、流、事件操作基本不做实际同步,因此不能从 PyTorch 事件查询或流同步返回推导底层 tinygrad 工作已经完成;需要记录实际同步完成点,并单独确认搬运与计算是否结束。

    视图写入和原生 PyTorch 存储一样吗?

    不能假设一致。一般视图的写入走「索引张量计算扁平位置、克隆、scatter、替换基础张量并刷新派生视图」的路径,源码也注明仅收缩类操作的偏移累加正确,重排、扩展、填充、翻转等别名情形的存储偏移并不正确,因此重叠索引写入与复杂别名应列入兼容性测试。

    empty_strided 和 logsumexp 有什么语义差异?

    empty_strided 明确忽略请求的 stride,改为连续分配;logsumexp 映射只取第一个轴。这些都是静态可见的语义差异,说明「算子名已注册」不等于数值等价,需要用目标版本做运行矩阵验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_TORCH_PRIVATEUSE1_ATEN_ALIAS_DEVICE_DEEP_DIVE.md(取用了导入注册、原生张量所有权、视图与别名写入、算子表与兼容缺口)。
    • 项目 02 —— openpilot-research/TINYGRAD_DATASET_TORCH_FRONTEND_INGRESS_BOUNDARY_DEEP_DIVE.md(仅用于确认前端入口与研发/车端隔离的边界)。

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

    更新记录

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

  • 仿真相机与传感器:图像缓冲、采样节拍及有效位

    结论:仿真感知链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。发布时戳不是物理曝光时戳——仿真把同一个 logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF;重复样本也不等于传感器频率——主循环每圈发 5 条加速度、5 条陀螺仪与 10 条 GPS。所有频率推导均以 SimulatorBridge 主循环达到标称 100 Hz 为前提,实际频率未测。固定快照基线为 c8fb906。

    图像从 MetaDrive 到 VisionIPC

    MetaDriveBridge.spawn_world() 为窄路相机及可选广角相机配置 RGB 图像源,分辨率取 W=1928, H=1208。MetaDriveWorld 用 multiprocessing.Array 暴露两块 RGB 内存;metadrive_process() 把渲染图像写入共享数组后对 image_lock.release() 计数;桥接侧另有目标 20 Hz 的相机线程,在 send_camera_images() 开头 acquire() 一次,再把当前数组转为 NV12 并发布。

    这里的 image_lock 实际是初值 0 的 multiprocessing.Semaphore,用于等待至少一帧,而非读写互斥:生产者写图前不获取它,消费者转换 RGB 时也不阻止下一次写入。因此静态上存在读取一幅图像过程中被生产者覆盖的窗口;若生产速度超过消费者,semaphore 计数积压,消费者可能对同一块「最新图像」连续取样。是否出现撕裂或重复帧需要像素哈希 / 帧号探针验证。

    环节 实现事实
    色彩转换 rgb_to_nv12() 用整数 BT.601 近似系数生成 Y,2×2 box filter 后交错 U / V;要求偶数宽高;调用方固定断言 1928×1208×3 且 uint8。
    缓冲 Camerad.__init__() 为每路建立 5 个 VisionIPC 缓冲;窄路总建,广角按 dual_camera 决定。
    发布 _send_yuv() 先构造外层 Event.valid=True 的 *CameraState,把同一个 dat.logMonoTime 同时当作 SOF / EOF,再发布独立递增的仿真帧号和单位变换矩阵。
    有效位 底层 VisionIpcServer.send() 另把 extra.valid 固定为 False;CameraState 外层有效位与 VisionIPC extra 有效位不同。

    三项原类探针确认单路只分配窄路五个缓冲、双路帧号分别从零递增、图像发送先于状态消息发布,且每帧 SOF = EOF。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 的色彩、曝光或 DMA 缓存行为。探针还确认:若假 VisionIPC 已接受图像而假 PubMaster.send() 抛错,cam_send_yuv_road() 尚未递增帧号;调用方若捕获异常并再次调用,会用同一帧号发送另一份图像、但产生新时间戳——这是函数边界的部分提交状态,不证明现有桥接线程会自动重试。

    IMU、GPS、驾驶员与外设占位

    SimulatorBridge._run() 以目标 100 Hz 的主循环调用 SimulatedSensors.update(),另起目标 20 Hz 相机线程。每次 update() 中,send_imu_message() 连续发布 5 条加速度和 5 条陀螺仪消息,数据都取同一份 SimulatorState.imu,各条的内层 timestamp 使用各自消息的 logMonoTime,并非模拟器采样时刻。

    消息 仿真节拍与标称值
    IMU(加速度 / 陀螺仪) 每圈各 5 条;100 Hz 主循环下约 500 条 / 秒 / 主题;服务表标称 104 Hz 不是实际硬件节拍。
    GPS 仅在 simulator_state.valid 时,每圈发布 10 条 gpsLocationExternal,约 1000 条 / 秒;服务表标称 10 Hz。
    驾驶员状态 update() 以 DT_DMON/2 检查驾驶员消息,以 0.25 秒检查外设状态,并非严格 20 / 4 Hz 定时器。

    send_gps_message() 把当前系统墙钟填成 unixTimestampMillis、固定 flags=1、精度常量和 source=ublox;这些重复包不能用于证明 GNSS 热启动、时钟校准、观测协方差或实际链路吞吐。send_fake_driver_monitoring() 把两侧脸概率置 1、分心置假、注意力置满;driverMonitoringState 外层显式有效,而 driverStateV2 使用 new_message() 默认外层 valid=False。不可把这些「健康」字段当作自研传感器或驾驶员监测的物理验证。

    假时钟探针还确认两个节拍门都使用严格 >:恰好到 0.25 秒时不发外设状态,到 0.251 秒才发;只有发送后才更新对应的上次发布时间。无效仿真状态并不阻止驾驶员 / 外设占位消息,且 driverStateV2.valid=False 与 driverMonitoringState.valid=True 会同时出现。

    退出、测试和插件化边界

    仿真启动脚本 launch_openpilot.sh 设置 NOBOARD=1、SIMULATION=1,屏蔽真实 camerad、encoderd、loggerd 等进程。主循环、相机线程和 MetaDrive 子进程的生命周期分散在桥接层;相机线程的 Semaphore.acquire() 没有超时,模拟器不再产帧时单靠线程 exit_event 不能立即唤醒它。现有 test_driving() 被 unittest.skip 标记,不能据此声称当前闭环已通过。

    自研硬件插件应分别验证图像采样帧号、读出时戳、消息外层 / 内层有效位、stride 与缓冲持有期,以及 IMU / GNSS 的独立采样时刻和真实频率。建议先在隔离命名空间记录每路源帧号、共享内存哈希、发布时戳和消费时戳,再注入掉帧 / 停发 / 速率偏差。

    常见问题

    仿真里 SOF 和 EOF 为什么不等于曝光时间?

    因为 _send_yuv() 把同一个 dat.logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF,表示「在软件发布时打的同一时刻」,不是传感器曝光 / 读出测量。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 行为。

    「CameraState 有效」和「图像有效」是一回事吗?

    不是。仿真里 *CameraState 外层 Event.valid=True,而底层 VisionIpcServer.send() 把 extra.valid 固定为 False。两者是不同层级的有效位,必须分别检查。

    仿真里 IMU 和 GPS 的频率是多少?

    取决于主循环实际频率。每次 update() 发 5 条加速度、5 条陀螺仪与 10 条 GPS;若主循环确为 100 Hz,则约 500 条 / 秒 / IMU 主题、约 1000 条 / 秒 GPS,而服务表标称 IMU 104 Hz、GPS 10 Hz。重复样本不等于传感器频率。

    仿真通过能代替传感器标定吗?

    不能。仿真链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。若要验证自研硬件,应分别记录源帧号、共享内存哈希、发布 / 消费时戳,并注入掉帧、停发与速率偏差。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SIM_CAMERA_SENSOR_TIMING_DEEP_DIVE.md(取用了图像缓冲与 semaphore 行为、IMU / GPS 仿真节拍、有效位与插件化边界分析)。

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

    更新记录

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

  • 系统时间自举:日期范围门、GNSS 校时与信任边界

    结论:openpilot 的系统时间自举建立在几个纯日期范围检查上:Python 侧的 system_time_valid() 用本地墙钟在固定下界 2025-02-21 与固定上界 2035-01-01 之间做严格开区间比较,若找到 systemd 可执行文件,还会把它的修改时间加一天并与下界取较大值。「有效」只表示当前日期落在区间内,不等于可信 UTC、证书有效或各传感器时钟已对齐:一台时钟在区间内但快慢数天的设备同样能通过。本文为固定源码快照 c8fb906 的静态阅读与有限隔离探针,未修改系统时钟、未连接 GNSS 或访问网络。

    公共日期门:范围检查而非可信时间

    MIN_DATE 与 MAX_DATE 分别为 2025-02-21 和 2035-01-01。min_date() 若发现 systemd 可执行文件,会把该文件的修改时间按本地时间加一天,再与固定下界取较大值;system_time_valid() 用本地 datetime.now() 做严格开区间比较。它没有读 NTP 同步状态、GNSS 精度、硬件 RTC 来源或 TLS 证书有效期。

    一个结构性边界是:若该文件的修改时间异常地进入未来,动态下界可能高于上界,导致整个有效区间为空。四项假路径探针确认了这一族行为:没有该文件或修改时间早于固定下界时仍用 2025-02-21;较新的修改时间会把下界抬到其本地日期加一天;恰好等于动态下界或 2035-01-01 上界均判无效;修改时间晚于上界可使整个有效区间为空。

    原生 C++ 侧还有一条不同的判断:它的固定下界是 2024-08-26,也考虑该文件修改时间加一天,但没有 2035 上界,安装器依赖这一路径。两端不能被当作同一个时间状态。

    项目 Python 侧 C++ 侧
    固定下界 2025-02-21 2024-08-26
    固定上界 2035-01-01 无
    动态下界 systemd 文件修改时间的本地时间加一天,与固定下界取大 同样考虑该文件修改时间加一天
    加一天的算术 先转本地无时区时间,再加一个日历日 直接对 epoch 秒加 86400,即固定 24 小时

    两端的「加一天」也不完全等价:在使用夏令时的本地时区且这一天跨越切换点时,两个下界可差一小时。离线以 America/New_York 的 2026-03-07 12:00 为修改时间,Python 得到次日 12:00,固定秒数得到次日 13:00;2026-10-31 12:00 则分别得到次日 12:00 与 11:00。这只验证时区算术差异,目标设备是否配置夏令时尚未验证。

    timed 的 GNSS 校时链

    定位服务按启动时的一个可用性参数在外部与内部定位主题之间选一个,timed.main() 创建一次订阅,不在主循环中动态切换。每轮先把当前墙钟写入时钟消息,并用上述范围判断设置外层有效位。只有所选主题本轮更新、消息单调时间距当前不超过两秒、定位有效且 GNSS 时间落在下界到上界之间,才调用校时函数;此路径没有检查所选定位消息的顶层有效位。偏差不足十秒直接跳过,更大的偏差通过系统命令设置,失败只记录异常。

    六项假消息与假命令探针补充了执行顺序证据:时钟发布发生在 GNSS 门控之前,即使本地墙钟无效也可用新鲜定位尝试校时;所选定位消息的外层无效不阻断调用。定位时间戳在检查「本轮更新、新鲜度、定位有效」之前即被转成日期时间,因此假消息即使标为未更新,超出可表示范围的时间戳仍会先抛异常,使本轮不进入其余门控。此外主循环在校时函数正常返回后无条件睡十秒,未根据实际校时成功与否区分退避。

    环节 实现事实 边界
    订阅选择 按启动时可用性参数在两条定位主题中选一条 主循环不动态切换
    新鲜度门 消息距当前单调时间不超过两秒 需定位有效且时间在范围内
    校时调用 偏差超过十秒才设置系统时间 不检查定位消息顶层有效位
    异常处理 设置失败只记录异常 主循环仍无条件睡十秒

    启动自举与下游行为

    时间门被多个网络与硬件路径复用:GNSS 相关服务仅在时间「有效」时给模组发送辅助时间,并在网络可用时才取辅助数据。另一方面 timed 又依赖 GNSS 首次有效定位来校准系统时间,所以自制硬件必须设计 RTC、NTP 或独立定位的冷启动路径,不能假设辅助时间与 GNSS 校时会自动互相解锁。

    下游同样依赖这个门:eSIM 的配置下载函数先检查系统时间范围,通知上传也有同一门;更新服务在时间无效时等待再重试;UI 配对与更新路径、以及取令牌的辅助函数也依赖它。部分初始设置在证书错误且本地时间无效时会尝试重启时间同步服务。这些调用说明「日期在范围内」是业务前置条件,却不能替代 HTTPS 自身的证书验证和服务端认证。

    还有两个容易混淆的语义:GNSS 时间戳被转为 UTC 后移除时区信息,再与本地时间下界比较,设备为 UTC 时一致、非 UTC 时边界会偏移;而外层有效位为真只表示当前墙钟通过了范围门,不是刚从 GNSS 成功校时。该进程也不会证明相机帧起始、IMU 中断与诊断日志时间戳已与墙钟完成共时钟标定。

    硬件与插件化要求

    时间服务应分别声明墙钟来源与可信级别、UTC 与闰秒、单调时钟域、观测时间戳、最后同步时间、估计误差、时钟跳变和上游服务状态。只有受信任的平台层可设置系统时间;GNSS 与网络插件应提交带来源与精度的候选时间,不能直接把范围检查当成「可信时间」,也不能自行运行系统校时命令。UI、更新与 eSIM 的网络门应区分「尚无可信时间、可信时间可用、时间失步」三种状态,避免把范围检查误认为完整安全检查。

    待验证边界

    尚未验证的场景包括:无 RTC 冷启动、真实镜像中该文件修改时间异常、时区非 UTC、GNSS 定位延迟或失效、NTP 与 GNSS 冲突,以及系统时间前后跳变对证书、令牌、日志与相机时序的影响。本轮未执行这些动态或实板测试。

    常见问题

    system_time_valid() 为真就代表时间可信吗?

    不代表。它只是一个纯日期范围检查,不读 NTP 同步状态、GNSS 精度、硬件 RTC 来源或 TLS 证书有效期。一台时钟在区间内但快慢数天的设备同样能通过,因此「日期合理」不等于可信 UTC。

    Python 与 C++ 的时间有效判断一致吗?

    不一致。Python 侧下界为 2025-02-21、上界为 2035-01-01;C++ 侧下界为 2024-08-26 且没有上界。两端的「加一天」算术也不同:Python 先转本地无时区时间再加一个日历日,C++ 直接对 epoch 秒加 86400,跨夏令时时可差一小时。

    只要 GNSS 有定位就会校准系统时间吗?

    不一定。只有所选主题本轮更新、消息距当前单调时间不超过两秒、定位有效且时间落在范围内才调用校时函数;偏差不足十秒会直接跳过。而且该路径不检查定位消息的顶层有效位。

    本地墙钟无效会不会阻止 GNSS 校时?

    不会。探针显示时钟发布发生在 GNSS 门控之前,即使本地墙钟范围检查不通过,也可用新鲜定位尝试校时;所选定位消息的外层无效同样不阻断调用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SYSTEM_TIME_BOOTSTRAP_TRUST_CONTRACT_DEEP_DIVE.md(取用了公共日期门、GNSS 校时链、下游消费者与硬件插件要求)。
    • 项目 02 —— openpilot-research/NATIVE_UTIL_IO_EXIT_CONTRACT_DEEP_DIVE.md(关联的原生工具专题,用于对照 C++ 与 Python 两套日期门差异,本文未展开其 I/O 实现)。

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

    更新记录

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

  • 网络状态到费用策略:路由 metric、RSSI 分档与计量位的回退边界

    结论:网络状态是一条「慢速快照 → 分类 → 费用策略」的链路:hardwared 约每 10 秒采集一次网络/调制解调器/USB 快照,分类函数从 /proc/net/route、wpa_supplicant 与 modem 状态文件 推导类型、强度与计费位,再由 uploader、Athena 与 updated 消费。关键边界有三处:网络接口切换本身不是立即刷新触发器;蜂窝缺 signal_quality 会落入 poor 而非 unknown;未知 Wi-Fi 计费可能被当成免费网络,不能作为上传费用授权的保守依据。本文未连接 Wi-Fi、蜂窝网络或运营商账单。

    来源与关键函数

    类型判断从 get_default_route_iface() 开始:它解析 /proc/net/route,只取目的地为 00000000 且带网关标志的条目,以 (metric, 接口名) 元组最小者为接口,不单独要求 UP 标志。默认路由名匹配 wlan* 即认作 Wi-Fi、eth* 即认作以太网,否则读取 modem 的 connected 与制式,未知返回 none。SSID 与 RSSI 来自 wpa_supplicant_cmd():它用抽象 UNIX datagram socket 发命令,跳过以尖括号开头的事件消息,收到 FAIL 前缀即返回空结果,超时异常向调用方传播。

    入口 实现语义
    wpa_supplicant_cmd() 抽象 UNIX datagram socket;跳过事件消息,FAIL 返回空字典,超时异常传播
    get_default_route_iface() 取默认目的地且带网关标志的条目,按 (metric, 接口名) 最小者;不要求 UP 标志
    get_network_type() wlan*/eth* 优先;否则读 modem connected 与制式,未知返回 none
    get_modem_state() 文件缺失或 JSON 语法错误返回 {};权限异常透传;合法 JSON 非对象不检查形状
    parse_strength() / get_network_strength() 四档阈值分类;Wi-Fi 用 RSSI,蜂窝用 signal_quality,以太网直接 great
    get_network_metered() 蜂窝取 GsmMetered;Wi-Fi 按 SSID 匹配 NetworkManager keyfile 的 metered,无明确值回退基类
    hw_state_thread() 约每 10 秒或 USB 拓扑变化构造快照;容量 1,队列满丢弃新状态
    hardware_thread() 复用最后成功快照发布 deviceState.network*,并把计费位写入持久 NetworkMetered

    路由、信号与计费的精确回退

    网络类型先按 /proc/net/route 的最低 metric 默认路由接口名判断;同 metric 时按接口名字典序选取,路由行只要求默认目的地和网关标志,不检查 UP 位。格式错误的行会让解析函数抛异常,get_network_type() 则吞掉它并尝试 modem,不会跳过坏行继续比较其余路由。若 modem 虽连接但制式不在 nr/lte/utran/umts/gsm 表内,仍返回 none。

    强度方面,get_network_strength() 对以太网固定给 great,并非链路质量实测。Wi-Fi 通过 wpa_supplicant 的 SIGNAL_POLL 读 RSSI,只有大于 -100 且小于等于 0 的 dBm 才映射为 120 + clamp(dBm, -100, -20),再按四档阈值分类;恰好 -100 dBm、缺 RSSI、非法值或 socket 异常保持 unknown。蜂窝缺 signal_quality 时默认数值 0,会进入 poor,不是 unknown;读取异常则因宽泛异常捕获返回 unknown。信号质量读取与 modem 网络分类仍不是一次原子快照。

    场景 结果 边界说明
    以太网强度 great 固定值,非链路质量实测
    Wi-Fi RSSI 有效 120 + clamp(dBm, -100, -20) 后四档 仅当 dBm 大于 -100 且小于等于 0
    恰好 -100 dBm / 缺 RSSI / 非法值 unknown socket 异常同样保持 unknown
    蜂窝缺 signal_quality poor 默认数值 0,不是 unknown
    蜂窝读取异常 unknown 由强度函数的宽泛异常捕获返回

    get_modem_state() 只把文件不存在及 JSON 语法错误转为 {},权限错误会向调用方传播;它不验证 JSON 顶层必须为对象——合法数组会原样返回,get_network_type() 在需要 modem 回退时访问 .get() 可抛 AttributeError,而 get_network_strength() 会捕获异常并返回 unknown。这是控制流差异,不能据此断言真实 modem 写入了这种文件。

    计费方面,蜂窝读取布尔参数 GsmMetered(源码默认真)。Wi-Fi 先读 wpa 的 STATUS SSID,再尝试把其转义形式转换为 NetworkManager keyfile 的文本或 byte;byte; 编码,在运行态与持久目录搜索配置。首个匹配 SSID 的配置若有 metered=1/2 分别返回真/假;若为默认或未知值,会跳出搜索并回退基类,后续同名配置不再检查。SSID 缺失、读取或解析失败也回退基类,而基类对 Wi-Fi、以太网和 none 返回非计费。因此「未知 Wi-Fi 计费」在此路径可能被当成免费网络,不能作为上传费用授权的保守依据。

    从慢速快照到上传决策

    hw_state_thread() 把类型、网络信息、强度、计费位分次调用取值,采集异常只记录、不发布该次快照。约 10 秒的采样周期可被 USB 拓扑变化提前触发,但网络接口切换本身不是立即刷新触发器;队列满时新快照会被丢弃。主线程沿用最后一次快照并以较高频率重复发布 deviceState,其消息 logMonoTime 不能代替这些网络字段的实际采样年龄。

    uploader.main() 消费 deviceState.networkType/networkMetered,Athena.upload_handler() 为禁止蜂窝的任务检查计费位,updated 则读 hardwared 写入的持久 NetworkMetered 决定自动拉取节奏。三者消费的是分类/配置状态,不是计费账单、当前传输 socket 的实际路由或全链路一致快照,状态切换窗口须靠传输层重新核验。

    自研硬件与插件接口

    网络插件至少输出 link_type、default_route、connectivity、metering 四个独立字段,并携带接口 ID、来源、采样单调时间、有效位和原因。metering=unknown 应由宿主费用策略保守处理,不应默认为免费;上传器还需在每次开始与传输过程中按实际出口接口和账单策略复核。蜂窝 eSIM/SIM 身份属于敏感信息,遥测与日志插件不应默认共享 ICCID/IMEI。

    验证层面,探针只覆盖隔离控制流;台架或隔离网络测试仍应覆盖双默认路由 metric 切换、Wi-Fi RSSI 缺失、SSID 转义、真实配置读取、蜂窝 GsmMetered 切换、慢快线程交错和队列满,并同时观察 deviceState、NetworkMetered、uploader/Athena/updated 决策与真实出站接口。

    常见问题

    蜂窝信号缺 signal_quality 时为什么不是 unknown?

    因为源文档说明默认数值为 0,会进入 poor,不是 unknown;只有读取异常才因强度函数的宽泛异常捕获返回 unknown。这一差异在费用与上传策略里很关键,因为 poor 与 unknown 的保守程度不同。

    未知 Wi-Fi 的计费状态会被当成什么?

    可能被当成免费网络。源文档指出:首个匹配 SSID 的配置若为默认或未知值,会跳出搜索并回退基类,而基类对 Wi-Fi、以太网和 none 返回非计费。因此 metering=unknown 应由宿主费用策略保守处理,不能作为上传费用授权的保守依据。

    网络接口切换会不会立刻刷新状态?

    不会。约 10 秒的采样周期可被 USB 拓扑变化提前触发,但源文档明确网络接口切换本身不是立即刷新触发器,且队列满时新快照会被丢弃;主线程会沿用最后一次快照重复发布,其消息时间不能代表网络字段的实际采样年龄。

    deviceState 的 logMonoTime 能当网络状态的采样时间吗?

    不能。主线程沿用最后一次快照并以较高频率重复发布 deviceState,其消息 logMonoTime 不能代替这些网络字段的实际采样年龄。网络插件应显式携带采样单调时间与有效位。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMA_NETWORK_ROUTE_SIGNAL_METERING_SNAPSHOT_DEEP_DIVE.md(取用了网络分类、强度与计费回退、慢速快照与上传决策边界及插件接口要求)。
    • 项目 02 —— openpilot-research/ATHENA_DEVICE_NETWORK_METADATA_RPC_DEEP_DIVE.md(仅用于确认设备网络元数据接口的上下文)。

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

    更新记录

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

  • panda 健康包 ABI:版本哈希、字段映射与无损镜像的边界

    结论:panda 的健康包 ABI 里,「签名匹配」「ABI 兼容」「传输成功」「本轮采样完整」是四件不同的事,不能合并成一个布尔位。固件 0xd2 返回完整的 health_t(按 packed 字段相加为 48 字节),0xc2 对合法总线号返回 64 字节的 can_health_t;版本哈希只是头文件原文的 SHA-256 前 4 字节,不是抗恶意替换的签名。此外 pandaStates 并不是原始 MCU health 的无损镜像——temperature_pkt 在 C++ 映射中未进入 PandaState,total_tx_checksum_error_cnt 在 cereal 中没有对应字段。本文为固定源码快照 c8fb906 的静态阅读与离线探针,未连接实板、未做坏包注入。

    组件与函数索引

    入口 作用 边界
    health_t / can_health_t packed 二进制结构,按字段宽度当前分别为 48、64 字节 CAN health 正好占满 64 字节控制包上限
    get_health_pkt() / comms_control_handler() MCU 填充整机状态;三路 CAN 状态按请求总线号选择,响应结构体原始字节 0xc2 非法总线号不填充响应
    MCU 0xdd 与构建哈希 返回 health.h 和 opendbc/safety/can.h 的 SHA-256 前 4 字节(小端整数) 注释变化也可能改哈希
    Panda.connect() / get_packets_versions() 连接时读版本;短于 8 字节则记为 (0,0) 空包或短包返回零值,需上层甄别
    ensure_version() / health() / can_health() 被装饰的读/收发入口按本地与设备哈希拒绝不匹配;CAN health 用固定 struct 格式解码 不是所有 Python 方法在连接瞬间统一失败
    Panda::get_state() / get_can_state() C++ 直接按本地 sizeof 请求,返回值只检查非负,不要求读满 短读可能被解读为后部字段为零的有效快照

    ABI 与版本门的实际差异

    固件 0xd2 返回当前 health_t 的完整原始结构,0xc2 对合法总线号返回 can_health_t。编译期断言两者不超过 64 字节,但只约束固件本地缓冲容量,不说明不同主机版本的字段布局相同。版本哈希由头文件原文字节计算,仅忽略 CR,它检测构建所用头文件是否一致,注释变化也可能改哈希,且 32 位截断值不是抗恶意替换的签名。Python panda 在连接时读两个版本,再由 ensure_health_packet_version/ensure_can_packet_version 包装相关 API。

    主机路径 版本门行为 不保证的内容
    Python panda 连接读两个版本,包装相关 API 拒绝用不匹配结构解释健康包或发送 CAN 不是所有方法在连接瞬间统一失败
    原生 C++ pandad 不调用 0xdd 校验哈希;通常由 Python 包装器刷写/核验签名,connect() 再检查签名是否匹配本地文件 属镜像身份门,不是每次读健康包前的显式 ABI 协商;调试环境变量可跳过
    CAN_HEALTH_STRUCT 手写 64 字节格式 头文件变更不会自动改变 Python 布局,字段顺序仍需随固件同步维护

    因此,若自制板只实现同名控制端点却使用不同结构布局,或部署时镜像/头文件不配套,仅凭端点返回非负值不足以证明安全解释。

    Python 解析与失败边界

    _parse_c_struct() 从头文件寻找精确的 struct __attribute__((packed)) health_t { 文本,只接受单行的 uint8_t、uint16_t、uint32_t、float 标量声明,并用小端无填充格式生成 Struct,当前 health_t 得到 48 字节;声明行尾注释、数组或新类型会在导入 Panda 类时抛 ValueError,缺失精确结构头还会在字符串切分处失败。

    函数 行为 失败表现
    get_packets_versions() 用 0xdd 请求 8 字节,恰好收到 8 字节才解为两个小端 uint32 空包或短包返回 (0, 0)
    health() / can_health() 版本不符时在控制 I/O 之前抛 RuntimeError;随后 Struct.unpack() 要求长度恰好为 48/64 短读或超长响应抛 struct.error,没有「部分有效」字典
    health() 缩放 interrupt_load_pkt 除以 255,temperature_pkt 减去 40,标志位拆为布尔值 插件不能把缩放值与原始包字节混用
    can_health() LEC 四个 LEC 字段直接按 0 至 7 的字典查找 未知值抛 KeyError 而非返回未知状态
    总线号 Python 入口没有先限定总线号,把 int(can_number) 原样传给 0xc2 的 param1 固件只在总线号小于 3 时填包,正常空响应会使解包失败

    需要强调:Python 路径的失败表现与 C++ pandad 的短读零填充不同,不能用其中一条主机路径的成功条件推断另一条路径安全。C++ get_state() 与 get_can_state() 先清零结构体,随后只用 err >= 0 接受,因此短读可能被解读为「后部字段为零的有效快照」。这些是源码条件风险,并非正常固件必然短读。

    字段映射与信息损失

    fill_panda_state() 把点火、控制许可、安全模式、故障、供电、SPI 错误等字段转换到 PandaState;fill_panda_can_state() 处理 bus-off、LEC、错误计数、速率与 CAN FD 参数。当前 MCU health_t.temperature_pkt 在 C++ 映射中未进入 PandaState,而 Python health() 会转换为摄氏度;can_health_t.total_tx_checksum_error_cnt 在 cereal PandaCanState 中没有对应字段,也未发布。故 pandaStates 并不是原始 MCU health 的无损镜像,诊断插件不能据此假定所有板级指标可用。当 get_state() 或任一路 get_can_state() 返回负值时,send_panda_states() 不发布本轮消息;但短读由于上述接受策略仍可能发布。事件 valid 主要由句柄 comms_healthy 决定,不能替代每一字段的完整性/新鲜度证明。

    自制硬件与插件契约

    新网关应先交换独立的协议版本、结构布局/字段集 ID、设备 UID 与固件镜像身份;每个响应须核对实际长度恰好等于协商长度,再做解析。不要把「签名匹配」「ABI 兼容」「传输成功」「本轮采样完整」合并为一个布尔位。版本升级应有逐字段单位、缩放、缺测语义和向后兼容规则;若插件需要 MCU 温度或 TX checksum 错误计数,应明确新增消息字段或只读扩展通道,并保留原始值与时间戳。离线验收应覆盖 47/48/49 字节整机响应、63/64/65 字节 CAN 响应、同长度字段重排、版本不匹配、非法总线号、返回长度超过请求上限和字段缺测;上车前还需在无车台架用固件/主机版本矩阵验证拒绝策略、读回和安全默认态。

    常见问题

    版本哈希匹配能证明固件未被替换吗?

    不能。版本哈希由头文件原文字节计算并截断为 32 位,注释变化也可能改哈希,它是构建一致性的检查而非抗恶意替换的签名。

    C++ pandad 会像 Python 那样做健康包版本协商吗?

    不会按同样方式。原生 pandad 不调用 0xdd 校验哈希,connect() 检查的是签名是否匹配本地文件,属镜像身份门,不是每次读健康包前的显式 ABI 协商,且调试环境变量可跳过。

    短读的健康包会不会被当成有效快照?

    可能会。C++ get_state() 与 get_can_state() 先清零结构体、只检查返回值非负,因此短读可能被解读为「后部字段为零的有效快照」;这是源码条件风险,并非正常固件必然短读。

    pandaStates 是原始 MCU health 的完整镜像吗?

    不是。temperature_pkt 在 C++ 映射中未进入 PandaState,total_tx_checksum_error_cnt 在 PandaCanState 中没有对应字段,因此部分板级指标在 pandaStates 里并不可用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_HEALTH_ABI_VERSION_HOST_MAPPING_DEEP_DIVE.md(取用了 ABI 与版本门差异、Python 解析与失败边界、字段映射信息损失、自制硬件契约与离线探针结论)。
    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(仅用于确认健康字段进入上层消息有效位的位置)。

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

    更新记录

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

  • 车型文档与线束清单:CarDocs、SupportType 与 CARS.md

    结论:车型支持文档系统是一条「car_docs → PLATFORMS/EXTRA_PLATFORMS → get_params_for_docs() → 品牌 init_make() + CarDocs.init() → 自然排序 → 两种 Markdown 输出」的生成链,它产出的是展示/采购文档,不是车型识别、上车认证或电气设计规范。三个状态必须分开:车型可展示、线束可购买、车型协议已识别、发布固件允许发送,CarDocs 与 SupportType 都不能直接授予 CAN 写入权。以下为固定提交 c8fb906 的静态阅读与隔离探针分析。

    数据与生成顺序

    get_all_car_docs() 汇合正常平台与 extra_cars.py 的额外目录,为每个平台调用 get_params_for_docs(),再对每个车型文档项依次执行品牌 init_make()、共同 CarDocs.init(),最后按自然顺序排序。一个平台可包含多个市场车型/年款条目,因而文档行数不等于车型接口数量。

    关键是 get_params_for_docs() 的输入:它以空 CAN 指纹、一个占位 CarFw、alpha_long=True、is_release=True、docs=True 调用车型 get_params();额外目录中没有运行时接口的平台使用 MOCK 参数。这样的 CP 是排版/说明输入,既未验证目标车辆固件,也不代表该车能执行控制。

    阶段 入口 产出与边界
    数据汇合 car_docs + PLATFORMS + EXTRA_PLATFORMS 正常平台与额外目录合并,文档行数不等于车型接口数量。
    模拟参数 get_params_for_docs() 空指纹、占位 CarFw、alpha_long=True、is_release=True;无接口平台用 MOCK。
    品牌钩子 品牌 init_make() 品牌级展示字段调整为排版输入,非运行时 CP。
    共同派生 CarDocs.init() 计算 ACC、最低速度、自动起步、转向星级、支持级别与详情句;展示值可能被文档字段覆写。

    展示字段与派生逻辑

    split_name()、get_year_list() 从名称分离品牌、型号、年款并展开年款范围,依赖名称格式和 20YY 结束年推断,不是 VIN/车型年款校验器。CarDocs.init() 从 CP 和文档覆写计算展示列,其中 min_steer_speed、min_enable_speed 等可由文档字段覆写,展示值未必直接等于运行时 CP。

    CarDocs.init() 在 CP.dashcamOnly 且 merged=True 时通常把支持级别降为 DASHCAM,但 CUSTOM 和 REVIEW 是例外;ExtraCarDocs 默认 merged=False、INCOMPATIBLE,社区/安全限制类型由额外目录的子类钩子改写。SupportType 的 UPSTREAM、REVIEW、DASHCAM、COMMUNITY、CUSTOM、INCOMPATIBLE 是文档分类,不是运行时安全模式。

    线束清单以 Device.four 及常见附件为基础,品牌/平台可加入特定接头和工具;CarHarness 枚举包含摄像头/网关/OBD 类接头,并有 has_connector 展示标记。CarParts.common() 会加入默认设备并按 remove 列表过滤,但不做去重;递归展开也可能包含共享子件,数量显示在 CarDocs.init() 统计结果中。这些清单列出的是营销/采购名称,不含供电、针脚、隔离、保险丝或失效态参数。

    两种 Markdown 输出

    输出 入口 范围
    opendbc docs/CARS.md opendbc.car.docs.generate_cars_md() 对传入的全部条目构造简化列,包含不同 SupportType 的支持级别。
    openpilot docs/CARS.md openpilot.selfdrive.car.docs.generate_cars_md() 只筛 SupportType.UPSTREAM,生成 ACC/ALC/硬件/视频等宽表及脚注。

    两个生成器都是模板替换,作为 CLI 执行时会覆写各自的输出文件;get_all_car_docs() 和生成函数的注释明确说明它们也被外部网站调用,因此字段语义变化有仓库外消费者。openpilot 生成器的 _build_cars_table() 按 Column 枚举顺序取列,把 Hardware Needed 表头替换为含宽图片的 HTML 片段,前三列左对齐、其余居中。**kwargs 被直接丢弃,是兼容调用签名而非扩展参数。

    脚注按 get_all_footnotes() 映射的键迭代顺序重新编号,不使用映射值中的编号;只移除字面量 </br>,不移除 <br>。模板使用严格 Template.substitute(),未定义占位符会抛 KeyError。build_sorted_car_docs_list() 只在文档对象尚无 row 时初始化它;后续对同一内存对象再次生成时,已派生的行不会随新的 CP 或脚注参数自动重算。这是生成过程的状态边界,适合把文档项当一次性构建对象。

    对自制硬件与插件化的含义

    源文档给出的是一组建议而非现有代码已实现的保障:把「车型可展示」「线束可购买」「车型协议已识别」「发布固件允许发送」定义为四个不同状态,插件目录只展示已验证字段,不用 CarDocs 或 SupportType 直接授予 CAN 写入权;自制硬件物料表应另存电气契约(接头 pinout、线序、额定电流、供电/点火检测、隔离、ESD、保险与断电默认态),并逐车/年款验证,文档 CarParts 只能作高层采购提示;文档生成改为按版本快照纯计算,记录来自哪个 CarParams、DBC、固件数据库和硬件 BOM。

    测试与缺口

    opendbc 文档测试检查年款重复、命名、部分星级假设和上游线束清单;openpilot 生成器测试主要确认模板能生成。四项原函数隔离探针以临时模板和假 CarDocs 核对上游筛选、列顺序/对齐、调用参数、脚注编号与严格占位符;没有导入完整 opendbc,也未执行真实车型目录生成或覆写归档 CARS.md。这些测试不验证真实线束电气连接、商城库存、车型硬件兼容性或实际发布固件的执行能力。

    常见问题

    被列为 UPSTREAM 就等于该车型已获安全许可吗?

    不等于。SupportType 是文档分类而不是运行时安全模式;文档生成用的是带空 CAN 指纹与占位 CarFw 的模拟参数,不能直接授予 CAN 写入权。

    get_params_for_docs() 得到的参数能上车吗?

    不能。它以空 CAN 指纹、一个占位 CarFw、alpha_long=True、is_release=True、docs=True 调用车型 get_params(),是排版/说明输入,既未验证目标车辆固件,也不代表该车能执行控制。

    线束清单能当电气设计依据吗?

    不能。CarParts.all_parts() 列出的是营销/采购名称,不含供电、针脚、隔离、保险丝或失效态参数;自制硬件应另存电气契约并逐车/年款验证。

    为什么再次生成文档时行不刷新?

    因为 build_sorted_car_docs_list() 只在文档对象尚无 row 时初始化它,已派生的行不会随新的 CP 或脚注参数自动重算,这是生成过程的状态边界。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_DOCS_SUPPORT_HARNESS_DEEP_DIVE.md(生成顺序、展示派生逻辑、两种 Markdown 输出与测试缺口)。
    • 项目 02 —— openpilot-research/ 同目录平台参数与自制硬件方案专题(作为相邻边界参考)。

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

    更新记录

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

  • camerad 原生相机请求环:缓冲生命周期与 enabled 门

    结论:camerad 的原生相机链是一条带请求环的资源生命周期:分配缓冲 → 每帧取槽(request_id % ife_buf_depth)→ 建立 IFE/BPS 同步栅栏 → 调度 req_mgr → 先 poke 传感器再配置 ISP → 消费 SOF 事件、等待栅栏 → 销毁栅栏并补排同槽。它对 Qualcomm/AGNOS 特定 ABI 强耦合,不是通用 V4L2 接口。最需要记住的边界是:enabled 不是运行时发布闸门,且「配置关闭但仍探测」的失败路径不会在析构时执行显式释放。

    一、重要组件与函数

    组件 关键入口 静态行为
    平台主句柄 SpectraMaster::init() 打开 req_mgr、cam_sync、ISP、ICP,查询 ISP/ICP 非安全 IOMMU 句柄并订阅内核 SOF 事件;这是 Qualcomm/AGNOS 特定 ABI,非通用 V4L2。
    命令内存 MemoryManager::alloc_buf()、free() 按精确大小缓存 req_mgr 分配的命令缓冲,取出时 memset;free() 只返还缓存,析构时才 munmap、关闭关联 FD 并释放内核 handle。
    相机开路 camera_open() 探测/获取传感器,计算输出尺寸,配置 ISP/ICP/CSIPHY、链接设备,分配并映射 RAW/VisionIPC 缓冲,预排一环请求;ife_buf_depth 取 VIPC_BUFFER_COUNT 且断言小于 20。
    图像缓冲 CameraBuf::init()、camera_map_bufs() 座舱分配 RAW VisionBuf,三路按各自 stream 创建 NV12 VisionIPC 缓冲,再将帧 FD 映射到 ISP、必要时 ICP 的 IOMMU。
    每帧请求 enqueue_frame()、sensors_poke() 用 request_id % ife_buf_depth 选槽,创建 IFE/BPS sync 对象,向 req_mgr 调度,随后提交传感器 NOP 包,最后配置 IFE 与可选 BPS;源码明确要求 poke 在调度之后。
    事件消费 camerad_thread()、handle_camera_event() poll(POLLPRI) 获取事件,按 session 分给对应相机;通过请求号/帧号校验与栅栏等待后处理帧,销毁当前槽 sync 对象并补排 request_id + ife_buf_depth。
    失败重排 clear_req_queue()、clearAndRequeue() flush ICP 和 req_mgr,请求环所有槽销毁 sync 对象,从新 request ID 预排一环,并以 last_requeue_ts 跳过旧 SOF;重新入队不保证图像时间连续。
    关闭 camera_close()、析构函数 open 为真才调用关闭;关闭内仅在 enabled 为真时 flush、停 ISP/ICP/PHY、unlink 和释放帧映射。

    这些资源的所有权也要分清楚:命令内存的缓存复用只用于 req_mgr 分配的命令缓冲,不等于图像帧缓冲可以无限复用。座舱分配 RAW VisionBuf,三路按各自 stream 创建 NV12 VisionIPC 缓冲,再把帧 FD 映射到 ISP、必要时 ICP 的 IOMMU。而 camera_close() 仅在 open 为真时调用,关闭内又仅在 enabled 为真时 flush、停 ISP/ICP/PHY、unlink 并释放帧映射,因此两个布尔门并不等价于完整的资源所有权。

    二、请求时序与故障语义

    validateEvent() 对 requestId=0 累计次数,超过深度加 2 后重排;正常帧若帧号或请求号跳跃且 skip_expected 为假,也重排。waitForFrameReady() 对 IFE 最多等 100 ms,对存在的 BPS 栅栏再等 50 ms;失败时 handle_camera_event() 不发布该帧,而是清队列重排,以避免读取可能尚未写完的图像。即使 processFrame() 因初始跨相机同步返回 false,只要栅栏完成,当前槽仍会销毁并补排;同步等待失败则由全环重排负责销毁。这些路径的帧缺口和新鲜度应在消费者侧单独观测。

    触发条件 处理方式
    requestId=0 累计超过深度加 2 全环重排。
    正常帧帧号或请求号跳跃且 skip_expected 为假 重排。
    IFE 等待超过 100 ms 或 BPS 栅栏超过 50 ms 不发布该帧,清队列重排。
    processFrame() 初始跨相机同步返回 false 但栅栏完成 当前槽仍销毁并补排。

    三、传感器 NOP 失败后的状态

    sensors_poke() 为当前请求构造无命令缓冲的 CAM_SENSOR_PACKET_OPCODE_SENSOR_NOP 包,它不是新一组曝光寄存器写入。enqueue_frame() 先调度 req_mgr,再调用 poke,最后提交 IFE/BPS 配置。若 device_config() 失败,poke 只记录错误并把 enabled=false,void 返回使调用方无法据此停止后续配置;即便先前的 req_mgr 调度失败,代码也没有跳过 poke 或 ISP 提交。

    enabled 不是运行时发布闸门:handle_camera_event() 没有检查该标志,栅栏完成后仍会补排;camerad_thread() 只按 session 匹配并在返回真时调用 CameraState::sendState(),后者也未按 enabled 阻断图像/状态发布。曝光函数会因 enabled=false 直接返回,但若内核继续给出有效事件,主机代码仍可能发布帧;反过来,若请求不再完成,也可能等待/重排。静态源码不能判定实板最终行为。新硬件适配器应把 NOP/调度/IFE 提交失败作为独立错误状态上报,统一停止排队、发布和清理,而不只改变一个布尔值。

    四、显式释放与硬件移植风险

    1. 双状态门不等价于资源所有权:构造器以配置中的 enabled 初始化状态,但 camera_open() 总先调用 openSensor();后者成功时已经创建 session、获取传感器句柄并写初始化表,再遇 !enabled 才返回,且尚未设置 open=true。因此「配置关闭但仍探测」的路径没有在析构中执行显式 CAM_RELEASE_DEV/DESTROY_SESSION。源码中也有 I2C 失败将 enabled=false 的分支;若在 open=true 后触发,则 camera_close() 会跳过 ISP/ICP/PHY、link 和帧映射的显式释放。进程退出或 FD 关闭后内核是否自动回收,须按驱动和故障注入验证。
    2. 命令失败与后续使用:enqueue_frame() 记录 sync 创建或调度失败,却仍继续 sensor poke、IFE/BPS 配置;后续等待路径断言 IFE sync 对象非零。另 camera_map_bufs() 对 YUV FD 映射结果只记录日志,没有像 RAW 映射一样断言成功。需在台架注入分配、映射和调度失败,验证实际错误传播与重启策略。
    3. 插件边界:图像插件可以提供带 frameId、SOF/EOF、格式和缓冲生命周期契约的帧,但 req_mgr、IOMMU 句柄、sync 对象及内核 ioctl 应由受控平台适配器独占。支持热拔插前,应把句柄获取/释放写成与 enabled 无关的资源状态机,并验证消费者在重排或摄像头禁用时不会把旧帧当新帧。

    常见问题

    ife_buf_depth 有上限吗?

    有。camera_open() 中 ife_buf_depth 取 VIPC_BUFFER_COUNT 且断言小于 20;每帧用 request_id % ife_buf_depth 选槽,因此该值同时决定请求环的槽数与补排步长。

    栅栏等待失败后会发生什么?

    waitForFrameReady() 对 IFE 最多等 100 ms、对存在的 BPS 栅栏再等 50 ms;失败时 handle_camera_event() 不发布该帧,而是清队列重排,以避免读取可能尚未写完的图像。重新入队不保证图像时间连续。

    enabled=false 会阻止帧发布吗?

    不会自动阻止。enabled 不是运行时发布闸门:handle_camera_event() 不检查该标志,sendState() 也未按它阻断发布。曝光函数会因 enabled=false 直接返回,但若内核继续给出有效事件,主机代码仍可能发布帧。

    配置关闭但仍然探测的路径会释放资源吗?

    不会执行显式释放。camera_open() 总先调用 openSensor(),它成功时已创建 session、获取句柄并写初始化表,再遇 !enabled 才返回且尚未设置 open=true;因此析构中不会执行显式的设备释放。内核是否自动回收须按驱动和故障注入验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_REQUEST_BUFFER_LIFECYCLE_DEEP_DIVE.md(取用了相机请求环组件清单、请求时序与故障语义、NOP 失败后的状态、显式释放与移植风险)。

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

    更新记录

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

  • 温区采样与热状态机:阈值迟滞、驻车捷径与启动退场门

    结论:热状态不是「一过热就立刻停机」,而是分层的:新启动要求热状态低于 overheated,而已在行车只要求低于 critical,因此允许在 overheated 暂时滞留,直到达到 critical 才撤销在途资格。状态机按当前档位的严格小于/大于判断,每轮最多移动一档,精确等于阈值时保持原档;另有驻车捷径——设备未在行车且满足「本次未曾进入行车、没有驻车起点、驻车超过 5 分钟」之一时,不含 PMIC 的驻车复合温度超过 mici 的 85 °C 或其他设备的 75 °C 就直接设为 critical。本文为固定源码快照 c8fb906 的静态追踪与临时探针,未运行真实设备温升、风扇或热故障注入。

    关键函数与配置

    组件 实现行为
    ThermalZone.read() 首次按温区 type 名称发现编号,之后缓存编号;读取 temp 并按默认 1000 缩放;temp 不存在时返回 0
    ThermalConfig.get_msg() 遍历平台配置的单温区或列表形成各温度字段;各项顺序读取,没有共同时戳
    HardwareComma.get_thermal_config() 提供 8 个 CPU、2 个 GPU、DSP、DDR、2 个 PMIC 温区;mici 额外配置进气、排气、GNSS、底部 SoC
    FirstOrderFilter.update() 两条滤波器用 0.5 秒采样步长与 5 秒时间常数,首个样本直接成为状态,随后固定系数推进
    THERMAL_BANDS mici 与其余设备使用不同的升降温阈值
    hardware_thread() 热状态分支 计算两类复合温度,先更新风扇目标,再执行驻车危险覆盖或逐级状态转换
    阈值 mici 其他设备
    正常升级到 overheated 大于 100 °C 大于 96 °C
    overheated 升级到 critical 大于 107 °C 大于 107 °C
    降级阈值 92 °C 与 98 °C 88 °C 与 94 °C
    驻车危险捷径 大于 85 °C 大于 75 °C

    采样与聚合边界

    ThermalZone 只缓存找到的编号,不跟踪该编号以后是否换了类型;五项临时系统探针确认同一编号从一种温区类型改为另一种后,旧对象仍读取原编号的温度。找不到名称时编号仍为负一,随后读取会得到文件不存在错误并返回 0;稍后出现该名称时,由于尚未缓存编号,又可重新发现。单个 temp 文件消失同样变成 0 °C 而不是缺测,但 temp 内容不是整数会抛值错误,发现阶段的类型文件缺失会抛文件不存在错。名称扫描与非「文件不存在」类异常不在读取函数的捕获范围内,因此不能概括成「所有传感器错误都回零」。

    复合温度上,驻车复合温度取内存、CPU 最大与 GPU 最大中的最大值,行车复合温度另加入 PMIC 最大值;DSP、进气/排气、GNSS 和底部 SoC 虽进入设备状态,却不参与这两条复合温度。两条滤波器互相独立,初始化为未就绪,第一次更新不从 0 慢慢爬升;之后每次用固定系数推进,若调用周期因消息或调度变化而不等于 0.5 秒,物理时间常数会偏离标称的 5 秒。缺测温区的 0 不会作为「未知」传给聚合器,若关键热源缺失,最大值可能被其他热源掩盖,也可能整体偏低。

    热带状态与行为门

    正常状态机按当前档位的下限与上限做严格小于/大于判断,每轮最多向相邻档移动一步。六项热带分支探针确认从正常遇到极高温先到 overheated,下一轮高温持续才到 critical,精确等于阈值时保持原档。降温时 critical 和 overheated 有不同下限,形成迟滞。驻车危险捷径是独立分支:设备未在行车且满足前述三条件之一时,不含 PMIC 的驻车复合温度超过对应阈值就直接设为 critical;已启动的设备即使驻车复合温度很高也不走该捷径,首次驻车可立即直升,已启动过的设备则要严格超过五分钟。

    资格门 条件 后果
    新启动 热状态低于 overheated overheated 与 critical 都阻止进入
    已在途 热状态低于 critical 可在 overheated 暂留,达 critical 才撤销在途
    告警与风扇 点火为真且温度阻止启动或在途 请求高温离车告警并把风扇目标覆盖为 100%

    告警还要求点火为真。热状态阻止启动或在途时,它会请求高温离车告警,并在目标计算后强制风扇目标为 100%;告警清除或点火消失时,这一覆盖停止,后续目标回到风扇控制器独立的积分与前馈状态。对外报告的热状态是档位,既不证明哪个温区有效,也不证明风扇已执行。

    测试边界与硬件插件验收

    相邻的风扇单测只验证控制器数值上下限,本目录未找到对温区缺失、档位迟滞、驻车捷径与在途退场组合的专门单测。五项系统探针执行原温区读取类,六项热带分支探针执行原阈值表、状态转换与两项资格门;它们不是完整设备进程测试,实车热惯量、传感器布局、目标板系统接口格式和恢复时序均待验证。自制硬件的温区插件应提供稳定传感器身份、单位、量程、采样时间、有效位和故障原因,聚合器不能把失效当成 0 °C;可信热安全层要独立规定多传感器不一致、全部缺测和超温时的降级策略,并把「禁止新启动、在途退出、风扇全速」作为分别可验证的动作。

    常见问题

    设备过热就直接停机吗?

    不是。热状态分两档门槛:新启动要求低于 overheated,已在途只要求低于 critical,所以在 overheated 允许暂时滞留,直到达到 critical 才撤销在途资格。这两个门不能合并成一条「过热就立刻停机」的规则。

    传感器读不到温度会怎样?

    单个 temp 文件消失或编号为负一时读取返回 0 °C,而不是缺测;但 temp 内容不是整数会抛值错误,发现阶段的类型文件缺失会抛文件不存在错,名称扫描等异常也不在捕获范围内。因此不能概括为「所有传感器错误都回零」,聚合器也不能把 0 当作未知。

    为什么驻车时也判 critical?

    有一条独立捷径:设备未在行车且满足「本次未曾进入行车、没有驻车起点、驻车超过 5 分钟」之一时,不含 PMIC 的驻车复合温度超过 mici 的 85 °C 或其他设备的 75 °C 就直升 critical。已启动的设备即使驻车复合温度很高也不走这条捷径。

    温度报的档位能代表风扇已执行吗?

    不能。对外报告的是档位,既不证明哪个温区有效,也不证明风扇已执行;风扇目标的 100% 覆盖只在点火为真且温度阻止启动或在途时生效,告警清除或点火消失后即停止覆盖,回到控制器独立的积分与前馈状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/THERMAL_ZONE_BAND_STARTUP_EXIT_CONTRACT_DEEP_DIVE.md(取用了温区采样、聚合边界、热带状态机、驻车捷径与两项资格门)。
    • 项目 02 —— openpilot-research/FAN_HOST_PANDA_FEEDBACK_CONTRACT_DEEP_DIVE.md(仅用于确认风扇指令与转速告警的分工位置)。

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

    更新记录

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

  • tinygrad QCOM/KGSL:代际门与外部相机指针映射

    结论:QCOM/KGSL 后端在入口处设了一道明确的代际门——gpu_id[:2] >= (7, 3) 直接抛出不支持异常,而 QCOMCLRenderer 的编译器构造器只接受 a630,二者说明「运行时接受的 GPU ID」不等于「该编译器可用」。外部相机指针映射有一条容易被误读的回退:_gpu_map() 仅当 errno=14 时才把原指针当 GPU 地址,这不能被当作任何普通 CPU 指针的通用零拷贝保证。此外 signal() 的 7.3+ 分支尚未实现,与设备初始化的代际门一致。以下为固定源码快照 c8fb906 及固定 tinygrad 归档的静态追踪,不是 Qualcomm GPU、AGNOS 或车辆上的运行验证。

    入口与编译选择

    AGNOS 上 load_oob() 选择 DEV=QCOM;构建模型的 SConscript 还设置 IMAGE=1、FLOAT16=1 等标志,因此运行产物的设备名、图像布局和编译器必须匹配,改选新 SoC 不能只替换设备字符串。QCOMDevice.__init__() 打开 /dev/kgsl-3d0,创建带优先级/细粒度抢占等标志的 KGSL 上下文,设置功率约束并查询 chip_id;gpu_id[:2] >= (7, 3) 直接抛出不支持异常。

    随后选 QCOMCLRenderer 或 IR3Renderer,安装栈、cache-flush dummy 与 border-color 的缓冲替换规则。QCOMCLRenderer 使用 QCOMCompiler,其构造器只接受 a630;非 aarch64 主机尝试下载编译环境并经 QEMU/Docker 服务编译,不能推断任意被运行时接受的 GPU ID 都能走该编译器。IR3 是另一条编译路径,其可用性同样需要目标平台验证。

    内存与外部相机指针

    QCOMAllocator._alloc() 在 external_ptr 非零时走 _gpu_map(),否则走 _gpu_alloc()。所有权随来源变化:自分配对象走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调用 KGSL 释放。因此主机析构不能替相机驱动释放底层图像,外部相机缓冲复用必须晚于 GPU 完成。

    函数 源码行为 对车载硬件的契约
    QCOMAllocator._alloc() external_ptr 非零走 _gpu_map(),否则走 _gpu_alloc() 外部指针必须有明确长度、来源和所有者;空指针不进入外部映射分支。
    _gpu_alloc() 按 4 KiB 对齐向 KGSL 分配 GPU 对象,并 mmap 成 CPU 可见地址,可选 uncached/清零 要验证目标驱动的分配标志、CPU/GPU 一致性和峰值内存。
    _gpu_map() 页对齐外部地址与跨度,先调用 CPU dcache_flush(),再尝试 KGSL_MAP_USER_MEM;仅 errno=14 时退回把原指针当 GPU 地址 errno=14 回退不是任何普通 CPU 指针的通用零拷贝保证,须确认指针为 GPU 可用地址且租约有效。
    _free()、_gpu_free() 释放前同步;自分配走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调 KGSL 释放 所有权随来源变化,外部相机缓冲复用必须晚于 GPU 完成。
    _ensure_stack_size() 设备共享私有栈,仅在需要增长时同步并重分配 内核首次触发更大栈可能引入帧延迟尖峰,须预热与量测。

    QCOM has_copy_queue=False,通用 get_enqueue_devs() 还将 QCOM COPY 排除 HCQ 入队;exec_copy() 会根据缓冲能力选 host 可见复制或其他分支。不能把 Buffer.copy_from() 一律算成 QCOM DMA,也不能只测 kernel 耗时作为整帧推理时延。

    二进制、参数与命令包

    QCOMCompiler.compile() 在 aarch64 上通过专用 LLVM 接口编译/链接,再取设备二进制;checked() 在错误时重建编译实例并抛错,非 aarch64 走编译服务,存在额外环境与网络依赖。QCOMProgramData 依所选 renderer 解析 NIR 或专有库的程序镜像、寄存器占用、私有/共享内存、图像和参数布局;qcom_build_program() 以二进制内容和设备集合缓存程序占位。

    QCOMComputeQueue.kernargs() 把普通缓冲地址、标量、常量、sampler、texture 和 image buffer 编成参数表,NIR 分支还按编译器给出的映射重排图像槽位。exec() 检查资源/维度、取得程序镜像和私有栈,随后设置 Adreno A6xx 寄存器、装载常量与 shader,分别使用 CP_EXEC_CS 或 CP_RUN_OPENCL 启动。cmd()、reg() 将其包装为 Type7/Type4 命令,动态地址由 HCQ 链接期或运行期 patch 处理。

    源码中 exec() 的维度异常条件为「某全局乘局部维度超 65536 且 某局部维度超 1024」;单独超出前者不会由这个条件拒绝。它还单独检查 max_threads < prod(local_size)。这是静态可见的输入检查范围,不等于目标驱动一定接受其余组合;移植测试应分别覆盖每一维边界、组合边界及驱动错误回报。

    提交、缓存与失效

    QCOMComputeQueue.submit() 为命令缓冲构造 kgsl_command_object 和 kgsl_gpu_command,经 ioctl(KGSL_GPU_COMMAND) 提交。memory_barrier() 发缓存写回、失效和等待;signal()、wait() 用 GPU 事件/内存轮询配合 HCQ 时间线,而 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致。

    QCOMDevice._wait_signal() 在共享信号未达目标值时先尝试 KGSL timestamp 等待,抑制该等待的 OSError/RuntimeError,再调用通用轮询检查;通用 Compiled.synchronize() 还等待 peer pending,并在运行时超时后调用 on_device_hang()。这不是车辆控制的故障降级策略;模型无输出、过期输出和 GPU 挂起应由上层状态/安全链处理,不得以成功提交命令代替有效推理结果。

    验证证据与待办

    验证项 归档测试覆盖 待补
    CPU dcache flush test/device/test_qcom.py 覆盖(仅 ARM64/Clang 条件) —
    KGSL 映射 未覆盖 缓冲租约与缓存一致性
    命令包/时间线/图像参数 未覆盖 帧级尾延迟、错误码与挂起超时
    模型数值 未覆盖 纯读输入与输出数值

    归档中的 test/device/test_qcom.py 仅有一个 ARM64/Clang 条件下的 CPU dcache flush 测试,不覆盖 KGSL 映射、图像参数、命令包、时间线或模型数值;本文没有运行 QCOM 硬件测试。硬件验收需要固定模型产物与目标驱动版本,依次验证缓冲租约/缓存一致性、纯读输入与输出数值、帧级尾延迟、错误码与挂起超时,再验证摄像头断流/重连和降级状态。任何上车步骤都应先在隔离台架执行,保持 panda 独立 safety 边界。

    常见问题

    QCOM 后端支持所有 Adreno GPU 吗?

    不是。QCOMDevice.__init__() 中 gpu_id[:2] >= (7, 3) 直接抛出不支持异常,这是一条明确的代际门;而 QCOMCompiler 构造器只接受 a630,运行时接受的 GPU ID 不等于该编译器可用。

    外部相机指针映射失败会怎样?

    _gpu_map() 先做 CPU dcache_flush() 再尝试 KGSL_MAP_USER_MEM,仅 errno=14 时退回把原指针当 GPU 地址。该回退不是任何普通 CPU 指针的通用零拷贝保证,必须确认指针为 GPU 可用地址且租约仍有效。

    exec() 一定拒绝超维度的启动吗?

    不完全是。维度异常条件为「某全局乘局部维度超 65536 且某局部维度超 1024」,单独超出前者不会被这个条件拒绝;它还单独检查 max_threads 小于 prod(local_size)。这只是静态可见的检查范围。

    7.3+ 代际的 GPU 时间线信号可用吗?

    源文档说明 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致;GPU 挂起与超时应由上层状态/安全链处理,不能以命令成功提交代替有效推理结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_QCOM_KGSL_RUNTIME_DEEP_DIVE.md(代际门、外部指针映射、命令包与提交同步)。
    • 项目 02 —— openpilot-research/ 同目录模型运行时与 HCQ 专题(作为相邻边界参考)。

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

    更新记录

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