标签: openpilot

  • Toyota 雷达适配:A/B 槽聚合触发与健康发布的边界

    结论:Toyota 雷达接口不是「每帧到达就出结果」,而是跨轮聚合 A 槽、以最后一个 B 槽作为发布触发的观测链:只有 B 槽到达时才调用 _update() 并清空集合,否则返回 None。这带来三个可复核边界:触发长期缺失时不会生成新 RadarData 或错误事件;can_valid 只在 _update() 时被读取;无解析器时基类每五次调用输出空点,且外层 valid 为真并不代表实体雷达正常。ECU 禁用是独立的通信控制动作,与雷达观测插件必须分权。

    可用性由 DBC 决定,与 ECU 通信控制是两件事

    radarUnavailable 由 DBC 配置决定:TSS2 配置默认含雷达 DBC,而 RADAR_ACC 平台配置会移除雷达总线 DBC。源文档明确,radarUnavailable 表示当前适配没有可解析的雷达 DBC,不等于物理雷达不存在。该路径实例化的 RadarInterface 没有解析器,落入基类定期输出空点的路径。

    与可用性并列、但属另一层的是 ECU 通信控制:RADAR_ACC 且打开实验纵向时设置 DISABLE_RADAR,初始化时向 ECU 发通信控制,控制器周期发送 tester present。源文档强调这是车辆通信与纵向配置动作,不是 RadarInterface 的运行时健康开关,不能在没有车型、线束和安全审查时照搬。

    A/B 槽聚合与发布触发

    _create_radar_can_parser() 按车型选地址组与总线:TSS2 用 A 0x180..0x18f、B 0x190..0x19f;传统用 A 0x210..0x21f、B 0x220..0x22f。A/B 注册 20 Hz,STATUS_MSG 注册 10 Hz,总线为 1。

    解析组 地址范围 名义频率 总线
    TSS2 A 槽 0x180..0x18f 20 Hz 1
    TSS2 B 槽 0x190..0x19f 20 Hz 1
    传统 A 槽 0x210..0x21f 20 Hz 1
    传统 B 槽 0x220..0x22f 20 Hz 1
    STATUS_MSG 源文档未给出具体地址 10 Hz 1

    RadarInterface.update() 把解析器本轮成功解析地址并入跨轮集合;仅当最后一个 B 槽出现时才调用 _update() 并清空集合,否则返回 None。_update() 查询 can_valid 与状态/预故障,只处理累计集合里的 A 槽,按 VALID、LONG_DIST、NEW_TRACK、valid_cnt、对应 B 槽 SCORE 新建、延续或删除点。

    两处细节值得单独指出:B 的 SCORE 从 vl 当前缓存读,源文档指出它未要求 B 与 A 同周期;VALID 为真时直接满足发布条件,SCORE>50 仅为替代路径。A 槽中 NEW_TRACK 或距离越界会先清 valid_cnt,有效近距测量再加一;NEW_TRACK 新建 trackId,否则保留槽位现有点。

    健康与发布的三个缺口

    缺口 静态表现 结论边界
    B 触发长期缺失 无 B 触发时不发布新消息,也不生成错误事件;历史地址仍在集合里 适配层不会主动汇报该故障
    健康门只在 _update() 读取 can_valid 与 STATUS_MSG 只在生成结果时变成 RadarData.errors 触发丢失时错误来不及成为事件
    无解析器的空点路径 基类每五次调用产生空 RadarData;card 仅非 None 时发布 外层 valid 为 errors 全假,不代表实体雷达正常

    CANParser.update() 只返回本轮成功解析地址集,vl 保留各信号最近值;can_valid 检查注册消息缺失/超时和计数器,并有失效计数门。源文档明确 can_valid 只在 _update() 时被读取。无解析器时,RadarInterfaceBase.update() 每五次调用产生空 RadarData,card 仅在返回非 None 时发布 radarTracks,外层 valid 为 errors 全假。因此空点且外层 valid=True 不是「实体雷达正常」的证明,订阅者还要同时看 CarParams 与数据年龄。

    几何约定与插件重构验收

    几何输出为 dRel=LONG_DIST、yRel=-LAT_DIST、vRel=REL_SPEED。插件移植要锁定坐标约定、单位、测量时刻和传感器安装位姿,不能只复制数值字段。上游 radard 以 modelV2 为轮询源,读取最近的 radarTracks 和车况后发布 radarState;源文档提醒,上游不发布时融合循环仍按模型帧推进,不能把输出频率理解成雷达采样频率。

    插件输出应包含 sensor_id、车型/DBC 版本、采样时戳、扫描周期 ID、A/B 配对依据、点年龄、健康原因码和观测状态,并区分「不适用、被禁用、未收到、CAN 不健康、状态故障、正常空点」。接入 radard 前,用只读回放验证 16 组 A/B 顺序错乱、B 槽丢帧、状态报文丢失、NEW_TRACK 复用、距离 255 门槛、VALID 与低 SCORE 组合以及总线超时。

    源文档还给出触发缺失时的要求:应由独立 deadline 产生超时健康事件,而不是等待某个 B 地址恢复后才汇报;历史槽位与 SCORE 必须有最大年龄。这与「以最后一个 B 槽作为发布触发」的机制互补,提醒订阅者不能只依赖适配层的自行汇报。

    最后是分权:ECU 禁用/恢复与雷达观测插件分权,属受信任车辆网关能力,不要把诊断写操作授权给普通传感器插件。本篇未运行测试;上述风险是源码条件推论,不是已复现的实车缺陷。

    常见问题

    radarUnavailable 是不是代表车上没有雷达?

    不代表。源文档明确 radarUnavailable 由 DBC 配置决定:RADAR_ACC 平台配置移除了雷达总线 DBC,所以该路径没有可解析的 DBC,这不等于物理雷达不存在。

    为什么雷达偶发不出结果却不报故障?

    因为发布触发是最后一个 B 槽。源文档指出,B 触发长期缺失时不会生成新的 RadarData 或错误事件,历史地址仍在集合里;can_valid 也只在 _update() 时被读取,所以触发丢失时适配层不主动发布故障。

    没有雷达解析器时输出空点且 valid 为真,能认定雷达正常吗?

    不能。无解析器时基类每五次调用产生空 RadarData,card 仅在返回非 None 时发布,外层 valid 为 errors 全假。源文档明确这不能作为实体雷达正常的证明。

    DISABLE_RADAR 是雷达观测的健康开关吗?

    不是。源文档指出它是车辆通信与纵向配置动作:RADAR_ACC 且打开实验纵向时设置,并向 ECU 发通信控制、周期发送 tester present,与 RadarInterface 的运行时健康状态是两件事。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_RADAR_ADAPTER_DEEP_DIVE.md(取用了地址组与名义频率表、A/B 槽聚合触发语义、健康发布缺口与插件重构验收清单)。

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

    更新记录

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

  • 回放三入口:可视化订阅与 CAN 硬件回放的边界完全不同

    结论:openpilot 的「回放」有三个入口,用途完全不同:C++ replay 可执行文件发布历史消息与 VisionIPC 视频并驱动终端 UI;Python ui.py 只订阅数据做调试可视化;而 can_replay.py 会直接向 Panda Jungle 发 CAN 帧,是唯一对硬件有直接副作用的入口。三者中最容易被误用的就是把 --allow 当成 CAN TX 防火墙,或把可视化订阅当成只读安全——can_replay.py 不是只读分析工具。

    本文基于固定源码快照,静态梳理命令行、终端 UI、Python 调试画面与独立的 CAN 硬件回放脚本。仅做静态阅读,未运行任一回放命令、下载数据、连接/重置/刷写硬件或发送 CAN。CAN 报文解析的健康语义见相邻专题。

    三类入口:能力边界必须先分清

    C++ Replay 是开发进程,启动时会把日志中的 CarParams 写入本地 Params,并可切换 ZMQ=1 让其他 UI/PlotJuggler 订阅。--prefix 为实例指定前缀,但仅靠服务过滤或视觉界面并不能隔离同机所有消息、参数和设备资源,必须在独立开发环境核对具体命名空间。

    入口 实现 能力 关键边界
    C++ replay 可执行文件 main.cc 发布历史消息与视频、终端 UI、benchmark 启动改写本地 Params;--cache 最终仍被钳制为至少五段
    Python 可视化 ui.py raylib 窗口、相机画面、模型轨迹、告警曲线 只订阅数据;曲线是调试可视化,不是严格多主题时间对齐采样
    CAN 硬件回放 can_replay.py 直接向 Jungle 发 CAN 帧 对板卡状态有直接副作用,绝非只读分析工具

    parseArgs() 解析 --allow/--block、段缓存数、起点、倍速、--auto、--data_dir、--prefix、视频与 benchmark 标志,参数转换用 atoi/atof;缓存和倍速在 main 中按正数过滤,起点由 Replay::start() 处理。未知路段、无可用片段仍在后续加载失败。

    main() 随后可选创建 OpenpilotPrefix,实例化 Replay,设置缓存与倍速并加载路段:普通模式启动 ConsoleUI,benchmark 模式直接等待并打印时间线。两个边界要记牢:--cache 最终仍由 setSegmentCacheLimit() 钳制为至少五段;benchmark 只是发布吞吐统计,不是控制算法正确性验证。

    终端 UI 与 Python 调试画面

    ConsoleUI::exec() 约 20 Hz 刷新终端:空格暂停、s/m 前后跳、e/d/t/i/w/c 按时间轴跳、+/- 变速、回车输入绝对秒数。这里的 seek 输入会先暂停再恢复,但未检查 scanw 返回值;错误输入会保持初始 choice=0 并尝试 seek。这是 UI 层行为,不等同于引擎 seek 的参数校验。

    函数 功能 边界
    handleKey() 空格暂停、+/- 变速、回车输入绝对秒数 未检查 scanw 返回值,错误输入仍会 seek
    updateStatus() 从 carState/vehicleParameters 展示车速与转向参数 数据来自回放主题,不是实时车辆状态
    updateTimeline() 从 qlog 生成的 Timeline 绘制使能与告警 缺 qlog 时时间轴可不完整

    Python 侧 ui_thread() 创建 raylib 窗口,用 CameraView("camerad", NARROW_ROAD) 读取 VisionIPC,相机画面与消息图层分开获取;SubMaster 订阅车况、规划、控制、雷达、标定、模型、告警等主题。一个明确的窄边界:get_arg_parser() 接受 ip_address 和 --frame-address,但主入口只用 ip_address 设置 ZMQ 并传给 SubMaster,--frame-address 在本文件中没有被传入 CameraView。远端消息可连接不代表远端帧可显示,该参数在当前实现中无效。

    CAN 硬件回放:独立且危险的边界

    can_replay.py 的四个阶段各有副作用。load_route() 读取 carParams 和所有 can 消息转为逐帧 CAN 列表,无入参时使用默认公开路段;脚本没有依据目标车型、总线拓扑或插入设备自动校验安全适配。connect() 持续枚举 Jungle,每个序列号启动独立发送线程——新连接设备会自动进入发送流程。

    阶段 源码行为 安全含义
    载入 load_route() 读 carParams 与全部 can 消息 来源与内容必须固定、审计;不自动校验安全适配
    发现 connect() 枚举 Jungle 并起发送线程 新设备自动进入发送流程,不是只读工具
    设备配置 send_thread() 在 FLASH 存在时调用 flash(),随后 reset()、清缓冲、设 500 kbps、开点火与供电 对板卡状态有直接副作用;绝不能连实车总线运行
    发报 以 DT_CTRL 节拍按记录帧组循环,仅保留 bus 不高于 2 仅过滤总线号不构成安全认证;依赖物理隔离台架

    FLASH 是「存在即触发」,不是解析布尔值。发送循环仅过滤总线号,空日志会在取模位置失败,USB 超时被吞掉,不能把循环存活视作实际发送成功。

    插件化落点与验证顺序

    把三类入口分开:ReplayControl 只管理路线、时间、速率和服务白名单;ReplayVisualization 只订阅数据且不得取得 CAN 发送能力;HardwareCanStimulus 必须是独立、显式启用的台架插件,含设备白名单、物理隔离确认、报文/总线过滤、发送上限、急停和全量审计。

    1. 不能让通用插件注册机制自动发现并启用硬件刺激插件。
    2. 不能把 --allow 当作 CAN TX 防火墙。
    3. 验证先从固定日志与 mock sink 开始,再查 Params/ZMQ/VisionIPC 隔离、CLI 异常输入、UI 缺主题/缺帧、消息时序。
    4. CAN 输出仅在独立 HIL 台架上用目标硬件与外部监测完成。

    当前目录的 test_route.cc 只覆盖路线范围解析;本文没有运行 GUI、benchmark 或 HIL 测试,也未发现针对上述入口分支的同目录测试。

    常见问题

    can_replay.py 是只读分析工具吗?

    不是。connect() 会持续枚举 Jungle,每个序列号启动独立发送线程;send_thread() 在 FLASH 存在时调用 flash(),随后 reset()、清 CAN 缓冲、设 500 kbps、打开点火与供电。它对板卡状态有直接副作用,绝不能在连接实车总线时运行。

    replay 的 –allow 能当 CAN 发送防火墙吗?

    不能。--allow/--block 只是服务过滤;can_replay.py 的发送循环仅按记录帧组循环发报、只保留 bus 不高于 2,仅过滤总线号不构成安全认证,脚本依赖物理隔离的 Jungle 台架。

    为什么 ui.py 的 –frame-address 不生效?

    源码里 get_arg_parser() 接受 ip_address 和 --frame-address,但主入口只用 ip_address 设置 ZMQ 并传给 SubMaster,--frame-address 在本文件中没有被传入 CameraView,所以远端消息可连接不代表远端帧可显示。

    replay 曲线是严格时间对齐采样吗?

    不是。曲线历史仅在 carState 更新时前移,其余主题值写入最后一行,因此它是调试可视化,不是严格的多主题时间对齐采样;终端时间轴缺 qlog 时也会不完整。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/REPLAY_FRONTENDS_CAN_DEEP_DIVE.md(取用了 C++/Python/CAN 三类入口的功能矩阵、终端 UI 边界与硬件回放四阶段副作用)。
    • 项目 02 —— openpilot-research/REPLAY_ENGINE_DEEP_DIVE.md(仅用于确认回放引擎的消息发布与加载边界,未取用其未公开内容)。

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

    更新记录

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

  • JotPluggler 布局会话:前态入栈与草稿遮蔽

    结论:JotPluggler 的布局不只是视觉数据——曲线可携带 custom_python,因此加载布局不应自动授予代码执行、文件、网络或 CAN 权限。撤销栈入栈的是修改前快照,由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A],此时 Undo 得到 A、Redo 也只能得到 A,不能恢复 B。草稿按文件名 stem 放在统一 .jotpluggler_autosave/ 目录,不同目录同名布局可能串用同一草稿。以下为静态源码分析,未编译、未运行 GUI、未验证断电恢复。

    布局模型与加载

    布局以 JSON 描述 tabs 与工作区递归 split 树,叶节点带 curves 时形成 pane。加载路径先解析文本再建模型,二者都不构成信任验证。

    组件 / 函数 源码行为 需要注意
    resolve_layout_path() 直接 JSON 路径优先;XML 路径有同名 JSON 时改用 JSON;否则按名称查内置 layouts/ 路径解析不是信任验证;外部布局中的代码见下文
    parse_layout() 解析 tabs、current_tab_index(兼容 currentTabIndex);无 tab 抛错,当前页索引夹到有效范围 未在该入口做布局大小、递归深度或来源签名限制
    parse_workspace_node() / parse_tab() 递归把带 curves 的叶节点加入 pane 数组,内部节点保存横/纵分割与尺寸 非法节点可能形成空工作区;未做畸形 JSON 压测
    parse_dock_area() / parse_curve() 恢复图表、地图、缩略图、相机类型及视窗范围;曲线可携带变换参数与 custom_python 布局并非纯视觉数据
    run() / reload_layout() 启动及切换布局时,若对应草稿存在则优先加载草稿,再重置 UI 与撤销状态 同名草稿可以遮蔽用户选定的正式布局

    解析入口并未限制布局大小、递归深度或来源签名,也未做畸形 JSON 压测;resolve_layout_path() 只是定位文件,本身不构成信任验证。启动与切换布局时,若对应草稿存在会优先加载草稿,再重置 UI 与撤销状态,因此同名草稿可以遮蔽用户选定的正式布局。

    编辑链与 Dock 树

    编辑入口围绕 pane 树与 tab 两组操作。split_pane_node() 把叶 pane 改成两个子节点,或在方向相同的父节点中插入新 pane;remove_pane_node() 删除 pane 后整理尺寸与索引。split_pane() 与 close_pane() 是上层入口,apply_pane_menu_action() 处理分割、重置视窗、清空及关闭,apply_pane_drop_action() 处理浏览器曲线、已有曲线和地图/相机等特殊 pane 的拖放。修改后经 commit_tab_layout_change() 更新 pane UI 状态、记录历史并写草稿。

    tab 侧由 draw_inline_tab_editor() 提交重命名,另一分支新建、复制和关闭 tab,并调用同一历史/草稿机制。build_dock_tree() 递归把布局树映射为 ImGui Dock 节点,ensure_dockspace() 删除旧节点后按树重建。

    这里要区分两种所有权:模型树是持久化状态,ImGui Dock 节点是运行时投影,不应把二者混成插件的同一所有权对象。

    撤销、草稿与正式保存

    撤销栈保存最多 50 个快照。UndoStack 在加载/重置时把当前布局放入;编辑路径在修改完成后调用 push(before_layout),入栈的是修改前快照而不是当前结果。apply_undo()/apply_redo() 从历史取布局,再由 restore_undo_layout() 同步 UI 并保存草稿。由 reset(A) -> 编辑成 B -> push(A) 可直接推导历史为 [A,A];这是源码逻辑结论,尚未通过 GUI 测试。

    撤销与草稿之间存在联动:加载或重置时 UndoStack.reset() 会把当前布局放入历史;每次编辑入栈的是修改前快照;Undo/Redo 从历史取布局后由 restore_undo_layout() 同步 UI 并写草稿。另一方面,mark_layout_dirty() 直接触发 autosave_layout(),记录当前 tab、序列化全布局并置 layout_dirty,因此草稿在用户显式保存之前就已经落盘。重构时应先补状态历史单测,再规定「当前状态入栈」或「前态/后态命令」的统一语义。

    阶段 行为 边界
    标记脏状态 mark_layout_dirty() 直接调用 autosave_layout(),记录当前 tab、序列化全布局 修改即写草稿,不等用户保存
    草稿路径 autosave_path_for_layout() 按文件名 stem 清洗后放入统一 .jotpluggler_autosave/ 不同目录同名布局、甚至清洗后同名 stem 会映射到同一草稿文件(静态推断)
    正式保存 save_layout_json() 写 tab、分割树、pane 视窗和非运行时曲线;curve_to_json() 保留 Python 代码 保留代码意味着布局文件不是纯数据
    写入方式 write_file_or_throw() 使用 O_TRUNC 直接写目标文件 未见临时文件、原子 rename 或 fsync,不能保证崩溃/断电后一致性
    保存与重置 save_layout() 写正式路径后清草稿与 dirty;reset_layout() 删草稿并重载正式布局 草稿损坏时优先解析草稿的路径会报错,代码未见自动回退正式布局

    对自制硬件与插件架构的约束

    布局可作为可视化配置插件,但其 custom_python 必须拆成独立的代码执行能力:加载布局不应自动授予文件、网络、Params 或 CAN TX 权限。车端部署建议把此类工具与控制进程隔离,默认只读日志/回放,外部布局先做来源确认、版本/schema 校验和执行授权。

    草稿键应基于规范化布局身份(例如完整路径哈希或稳定 UUID),并对正式文件与草稿采用临时文件、校验、原子替换及可回退恢复流程。插件 API 应分开 LayoutModel、PaneRenderer、DataSeriesProvider 与 ExecutableTransform;渲染插件只接收经校验的数据视图,不直接获得设备总线或安全配置写权限。这是重构建议,并非现有实现。

    同目录测试只检查 -h 输出,不能覆盖布局解析、拖放、撤销重做、草稿碰撞、损坏恢复或断电一致性。本篇未执行测试;绘图、地图、视频/缩略图、日志浏览器与导出仍待分别拆解。

    常见问题

    加载一个布局就会执行里面的代码吗?

    布局解析会保留 custom_python,路线加载还会刷新自定义曲线;但按边界设计,加载布局不应自动授予文件、网络、Params 或 CAN TX 权限,代码执行应作为单独能力另行授权。

    Undo 之后再 Redo,能回到被撤销的状态吗?

    不能一概而论。由 reset(A) -> 编辑成 B -> push(A) 可推导历史为 [A,A]:Undo 得到 A,Redo 也只能得到 A,无法恢复 B。这是源码逻辑结论,尚未通过 GUI 测试。

    为什么不同布局会串用同一个草稿?

    因为草稿路径由 autosave_path_for_layout() 按文件名 stem 清洗后放在统一目录;不同目录中的同名布局,甚至清洗后同名的 stem,会映射到同一个草稿文件,存在跨布局串用风险(静态推断)。

    保存布局是原子写吗?

    不是。write_file_or_throw() 使用 O_TRUNC 直接写目标文件,此路径未见临时文件、原子 rename 或 fsync,因此不能据此保证崩溃/断电后的一致性。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_LAYOUT_SESSION_DEEP_DIVE.md(布局解析、编辑链、撤销栈与草稿持久化)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_DATA_PIPELINES_DEEP_DIVE.md(布局依赖的数据管线,作为本文的补充阅读)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_EXTRACTION_CUSTOM_CODE_DEEP_DIVE.md(自定义代码的执行链,支撑本文的代码能力拆分建议)

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

    更新记录

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

  • 两套 Filter 与 PID:同名类为何不能直接替换 import

    结论:openpilot 里同时存在两套控制原语:opendbc 车型侧与 openpilot 主机侧的同名 FirstOrderFilter 与 PIDController。它们在一阶低通更新公式上一致,但在参数顺序、前馈倍率、限幅与积分回退上并不等价,因此同名类不等于可替换 API,也不能仅靠替换 import 完成迁移。

    两套原语并存:车型侧与主机侧

    opendbc 侧的实现位于 opendbc/car/common/filter_simple.py 与 opendbc/car/common/pid.py,服务车型控制器;openpilot 主仓库的同名实现在 common/filter_simple.py 与 common/pid.py,服务 controlsd、风扇控制等主机侧调用方。两者都用源码 diff 与三项原 PID 数值探针核对过,但未运行完整车型控制、CAN 或实车。

    把两套类当成一个接口,最直接的后果是第三位置参数含义错位、前馈被多乘或少乘一个倍率、限幅修改后的内部积分状态分叉。这些差异在静态等式一致时看不出来,只有在动态响应里才暴露。

    函数级对照:滤波与 PID 的差异

    行为 opendbc 实现 openpilot 实现 差异后果
    一阶低通 update() 用 alpha=dt/(rc+dt);update_dt() 可重算系数。 同一更新式,但只有 update_alpha(rc),dt 是公开字段。 当前相同参数下公式一致;插件若依赖 update_dt() 不能直接换成主仓库类。两者都不会自动读取真实采样间隔。
    第二种滤波 HighPassFilter.update() 返回快、慢两个低通输出之差,构造时要求 rc2 > rc1。 BounceFilter.update() 是带速度状态的 UI 动画原语。 高通类严格说是带通;与弹性滤波不是同类用途。
    PID 构造 参数顺序 k_p,k_i,k_f,k_d,...;标量用 numbers.Number 包装。 参数顺序 k_p,k_i,k_d,...;标量用 (int,float) 判断。 第三个位置参数在两套实现中含义不同,必须显式命名与适配。
    PID 前馈 update() 计算 f = feedforward * k_f,另有 override。 update() 直接 f = feedforward,没有 override。 相同 feedforward 值不保证相同输出;更新方法第四位置参数也分别是 override 与 feedforward。
    PID 积分 积分可按 0.3/rate 向零回退,普通更新按 1/rate 积分并按扣除 P/D/前馈后的输出裕度限幅。 对候选总输出越界方向抑制积分增长,同时用输出 neg_limit/pos_limit 直接夹积分项;允许 set_limits() 动态改限。 正下限及运行时收紧限幅会产生不同的内部状态,输出暂时相同不代表后续响应相同。
    PID 附加状态 error_integral 返回 i/k_i,reset() 清空分量。 reset() 清空分量,但无该属性。 当插值 k_i=0 时读取此属性会除零;属性值也随速度相关 k_i 改变。

    真实调用链:谁在用哪一套

    Toyota 控制器为纵向 PID 传 k_f=1、速度相关 k_i 和加速度上下限,每三帧更新加速度滤波器,并使用 HighPassFilter 的坡度动态分量。这里的采样频率是车型控制代码给出的名义频率,不是滤波器自动测得。Nissan 控制器按车速重设转角低通 rc,非横向激活时直接覆写 angle_filter.x,再经过车型角度限制;滤波参数与状态重置策略由车型层负责。

    body 控制器有车速和差速两个 PID,差速积分冻结判断读取 error_integral;当 CC.enabled 为假时输出目标转矩为零,但 PID 内部状态未在该分支调用 reset(),该状态延续的动态影响需要回放或台架核对。主机侧方面,横向 PID 与风扇控制导入的是 openpilot.common.pid,把车型 PID 的经验直接搬到这些调用方会改变前馈和积分语义。

    限幅探针揭示的积分状态差异

    三项原 PID 探针可用固定命令复现。取 k_i=0.004、rate=2、误差 5、前馈 50、输出范围 [30,100],主仓库 PID 把名义积分增量 0.01 夹为 i=30,输出 80;车型 PID 按前馈剩余裕度保留 i=0.01,输出 50.01。改成对称范围 [-100,100] 后两者此步均输出 50.01。这与风扇目标中的 30% 点火下限直接相关,但不等于车辆控制器也使用正下限。

    另一探针先在宽范围积累 i=20,再把输出上限从 100 收到 10 且误差置零:两者当前输出都被夹为 10,但主仓库 PID 保留 i=20,车型 PID 把 i 夹为 10。主仓库的横向与纵向控制器都会变更 PID 限幅,但探针只验证通用原语的构造案例,没有证明实际车型的参数变化会触发同一数值状态。插件迁移必须比较积分状态和后续恢复轨迹,不能只比较饱和帧输出。

    探针设置 主仓库 PID 车型 PID
    范围 [30,100]、误差 5、前馈 50 积分夹为 i=30,输出 80 保留 i=0.01,输出 50.01
    范围 [-100,100]、同参数 输出 50.01 输出 50.01
    先积累 i=20,上限收到 10,误差置零 输出 10,保留 i=20 输出 10,把 i 夹为 10

    面向插件化的约束

    对同一个采样周期,两套实现对「限幅是只夹输出还是也夹积分」的回答并不相同,这正是插件必须显式声明契约、而不能继承默认行为的原因。对滤波/PID 插件应显式记录实现来源、参数单位、采样周期、速度插值表、限幅、前馈倍率、接管与重置策略,不要以类名作为兼容性判断。适配层应使用关键字参数构造和更新,并用离线阶跃、饱和、接管、掉帧、失活重启用例比较输出与内部状态——静态等式一致不能证明车型在新硬件时序下等价。

    最后需要重申边界:这两套 Python 控制原语都只生成候选控制量;车型构帧、card 活动门和 panda MCU safety 仍是独立边界,不能由 PID 插件自行绕过。

    常见问题

    为什么两个同名的 FirstOrderFilter 不能直接互相替换?

    因为在当前相同参数下更新公式虽然一致,但 opendbc 版有 update_dt() 可重算系数、dt 是公开字段;主仓库版只有 update_alpha(rc)。依赖 update_dt() 的插件换成主仓库类会直接失败,两者也都不自动读取真实采样间隔。

    PIDController 的第三个位置参数为什么危险?

    opendbc 侧参数顺序是 k_p,k_i,k_f,k_d,...,主仓库侧是 k_p,k_i,k_d,...。第三个位置参数在两套实现中分别代表 k_f 和 k_d,位置传参迁移会静默改变含义,必须改用关键字参数并显式适配。

    相同错误与前馈下,两套 PID 的输出会一样吗?

    不保证。opendbc 侧计算 f = feedforward * k_f 并有 override,主仓库侧直接 f = feedforward。限幅探针也显示:范围 [30,100] 时两者输出分别为 80 与 50.01,仅当范围对称后才都输出 50.01。

    输出暂时相同是否代表两套 PID 等价?

    不代表。先积累 i=20 再把上限收到 10 时,两者当前输出都被夹为 10,但主仓库 PID 保留 i=20、车型 PID 把 i 夹为 10,后续恢复轨迹不同。迁移必须比较积分状态而非单帧输出。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/OPENDBC_FILTER_PID_CROSS_REPO_CONTRACT_DEEP_DIVE.md(取用了两套滤波/PID 的函数级对照、真实调用链与限幅探针数值)。
    • 项目 02 —— openpilot-research/COMMON_RUNTIME_PRIMITIVES_DEEP_DIVE.md(仅用于确认主仓库运行时算法原语的边界)。

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

    更新记录

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

  • deleter 空间回收:书签只延后删除与五名额占用

    结论:user.preserve 书签只把目录往后排、并不使其成为不可删除状态;get_preserved_segments() 在解析段号之前先取「倒序前五个带标记目录」,若带标记的 boot/crash 或非段目录占了名额,较早的真实书签段就不会进入延后集合。容量阈值用严格小于:可用字节 < 5 GiB 或可用比例 < 10%;两次 statvfs 读取失败时各自返回高于阈值的默认值,因此缺测本身不触发删除。以下为固定快照 c8fb906 的静态阅读加假目录/临时文件系统探针,未删除真实路段、未填盘、未做车端测试。

    函数与数据依赖

    回收逻辑集中在 system/loggerd/deleter.py,它与容量配置、目录枚举和上传器存在共享依赖。下表的语义以源码事实为准。

    函数 源码语义
    has_preserve_xattr() 查询某目录 user.preserve == b'1';走进程内 xattr_cache,跨进程变更可能不可见
    get_preserved_segments() 从名称排序列表尾部向前找最多五个带标记目录,解析末个 -- 后的段号,将该段及前两段加入延后集合
    get_available_bytes() / get_available_percent() 各自独立调用 statvfs();OSError 时按调用方默认值返回,两个读数不是原子快照
    listdir_by_creation() 只列根目录下被判定为目录的项目,再按路段名称排序;函数名不表示按 inode 创建时间
    deleter_step() 空间低于任一阈值后枚举、分组排序、检查 .lock,尝试 shutil.rmtree();成功即返回,单次最多删一个目录
    deleter_thread() / main() 空间不足时每 0.1 秒重试,正常每 30 秒;主入口不另行处理步骤异常

    优先级与保留语义

    deleter_step() 使用 sorted(dirs, key=lambda d: (d in DELETE_LAST, d in preserved_dirs))。由于 False < True 且排序稳定,实际分组为「普通未保留、普通保留、boot/crash 未保留、boot/crash 保留」,同组内维持 listdir_by_creation() 的名称顺序。

    关键边界是:user.preserve 只延后删除。当普通路段删完而空间仍不足时,书签段、boot 与 crash 仍可被删。这里也不检查 user.upload 或远端对象存储回执,因此「待上传」与「已书签」都不是不可删除状态。

    另一个易被忽略的点:get_preserved_segments() 的 enumerate(filter(has_preserve_xattr, reversed(dirs))) 在解析路段格式之前就计算五个名额。假目录探针用两个非段目录占位,确认较早的真实书签段不进入集合——这不是实测删除事故。

    每个候选先用 os.listdir(delete_path) 检查任意 .lock,有锁即跳过。但 uploader.main() 启动时又会清理日志根下所有锁文件,因此锁的存在并非跨进程可靠 lease。若 loggerd 仍在写而锁被误清,静态看 deleter 可能把该目录视为可删;实际触发取决于进程调度和空间状态。shutil.rmtree() 仅在无锁分支执行,成功后返回 (True, path),不能证明释放字节数达到目标,也没有按本轮删除内容重新测量容量。

    缺测、异常与循环

    阈值分别使用严格小于:可用字节 < 5 GiB,或可用比例 < 10%。两个 statvfs 读取失败时各自默认高于阈值,所以缺测本身不触发删除;若仅一次失败,另一次仍可能触发。由 f_blocks == 0 引起的除零没有被 config.py 的 OSError 捕获。

    场景 实际行为与边界
    空间不足但无目录可删 返回 (True, None),外层每 0.1 秒再尝试,不转入较长间隔
    rmtree() 抛 OSError 在循环内捕获并转向下一目录;但书签 xattr 过滤与每项 os.listdir(delete_path) 位于该 try 之外
    listdir() 抛 FileNotFoundError 直接穿透 deleter_step();枚举后目录被并发删除或 xattr 读取异常时,错误可穿透 deleter_thread()
    目录符号链接 listdir_by_creation() 会把它列为候选,但 shutil.rmtree() 拒绝直接删除该链接,异常被记录后继续删普通段;链接及目标内容未动

    本机临时目录探针只证明函数级行为,未复现真实跨进程调度、Pipe 或断电。目标车端的 Python、文件系统与并发行为仍需另测,不能仅据路径字符串认定候选为受信任普通目录。

    现有测试与插件化验收

    test_deleter.py 用替换的 statvfs 和临时路段覆盖阈值、普通/书签/boot/crash 删除顺序及锁跳过;未覆盖缺测、f_blocks=0、异常目录、跨进程 xattr 陈旧、被其他进程并发删除、活动锁被清理或删除失败后的持续重试。

    四项原函数假目录探针可用 python3 -m unittest probes.test_deleter_preserve_failure_contract -q 复现书签名额、锁跳过、删除失败续试与列目录异常;两项临时文件系统探针执行原 listdir_by_creation()、书签分组与 deleter_step(),使用真实 os.listdir()/shutil.rmtree() 仅操作自动清理的临时目录:书签段及 boot 确实排在普通段之后,目录符号链接本机删不掉但不会阻断后续普通段删除。容量与 xattr 来源仍是假依赖,不覆盖运行中的 loggerd、真实日志根、断电或目标板文件系统。

    面向插件化,应由存储服务发放段级写入 lease,并把「段已关闭、远端已确认、书签保留期限、空间紧急覆盖」做成明确状态;删除前重新验证目录身份与 lease,异常统一记录并维持监督健康。容量插件须报告挂载身份、可用字节/比例、采样时间与缺测,不把默认值伪装成真实容量。

    常见问题

    带 user.preserve 的书签段会被删除吗?

    会。user.preserve 只延后删除;当普通路段删完、空间仍不足时,书签段、boot 与 crash 仍可被删。系统也不检查 user.upload 或远端回执。

    为什么较早的书签段可能没被保护?

    因为 get_preserved_segments() 在解析段号之前先取倒序前五个带标记目录;若 boot、crash 或其他非段目录占用了名额,它们随后因无合法段号被跳过,而较早的真实书签段不再进入延后集合。

    删除阈值是多少,缺测会怎样?

    阈值是严格小于:可用字节 < 5 GiB 或可用比例 < 10%。两次 statvfs 失败时各自返回高于阈值的默认值,缺测本身不触发删除;仅一次失败时另一次仍可能触发。

    锁能防止删到正在写入的段吗?

    不能保证。deleter 只检查目录内是否出现 .lock,而 uploader.main() 启动会清理日志根下所有锁文件,因此锁不是跨进程可靠 lease。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DELETER_PRESERVE_ORDER_FAILURE_CONTRACT_DEEP_DIVE.md(目录级回收的优先级、书签名额占用与失败边界)
    • 项目 02 —— openpilot-research/LOG_STORAGE_CAPACITY_FAILOPEN_CONTRACT_DEEP_DIVE.md(容量测量与缺测默认值,作为本文的补充阅读)

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

    更新记录

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

  • tinygrad DSP 后端:ION 视图偏移与 FastRPC 反向服务边界

    结论:tinygrad 的 Hexagon DSP 后端(ops_dsp.py)在固定归档里只是一条可选、且未被车端选用的推理路径:openpilot 的 load_oob() 当前只选 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。它把主机缓冲以「size + offset + ION share fd」三元组编入 FastRPC 参数,并在 DSP 侧生成 entry 包装读取 pra 数组;同时用一条常驻 daemon 线程向 DSP 提供 open/close/seek/read/stat/mmap 反向文件服务。这两处——非零 offset 视图的解映射地址,以及反向 RPC 的请求字段——都是跨处理器契约,不能当作无权限的数据通道。

    DSP 不是当前车端推理硬件

    源文档开篇即划定边界:openpilot 的 load_oob() 只选择 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。因此 ops_dsp.py 是 tinygrad 侧的 Hexagon 后端实现,不能被当作现有车端推理硬件。归档源码不修改,本篇为静态分析与无设备契约检查。

    真实路径的初始化是多阶段过程:DSPDevice.__init__() 打开 /dev/ion、读取 /dsp/cdsp/fastrpc_shell_3 并复制到 ION 缓冲、初始化 /dev/adsprpc-smd,然后启动反向服务线程;mock 路径则只注册模拟 renderer/program。渲染器禁用了 fp8/bfloat16 与 SQRT 原生映射,global_size、local_size 与 wait 未参与启动参数。

    阶段 静态行为 源文档标注未验证
    编译 目标 hexagonv65 / HVX v65 128b;真实路径生成 4 KiB 节对齐共享库链接脚本 Hexagon 工具链与编译产物
    主机内存 ION 分配并 share fd,再 mmap 为 CPU 可访问缓冲;视图继承同一 fd 并累加 offset /dev/ion 与驱动行为
    FastRPC 装载 shell、初始化 CDSP、动态装载 tinylib,失败重连一次 /dev/adsprpc-smd 与真实调用
    反向服务 daemon 线程响应 DSP 发起的文件操作 请求长度与权限边界

    主机缓冲与 DSP ABI 的三元组

    DSPAllocator 返回 DSPBuffer(va_addr,size,share_info,offset);DSPProgram 从缓冲提取 size、offset、shared fd,rpc_prep_args() 把 size/标量、offset 数组、8 字节计时输出和 fd/attrs 打包进 FastRPC 调用。DSP 侧生成的 entry 按固定下标读这些参数,这个数组顺序本身就是后端 ABI,不能只改 allocator 或 renderer 一侧。

    DSP 侧参数 含义
    pra[0] 读 buffer size 或标量值
    pra[1] 读偏移 offi
    pra[2] 写计时输出
    pra[i+3] 取得该输入的 DMA fd

    DSPProgram.__call__() 至多 15 个缓冲,按 8 字节槽写入 buffer size 与标量,另传 32 位偏移和 ION shared fd;调用 exec_lib() 后返回 DSP 侧微秒计时换算值。这意味着调用期必须持有 memoryview 引用以覆盖 ioctl,否则缓冲可能在传输中被回收。

    非零 offset 视图:映射与解映射地址不一致

    源文档把「非零 offset 的视图」列为需要单独验收项。DSPAllocator._offset() 给视图保留 size 并累计 offset;而 DSPRenderer._render_entry() 生成 HAP_mmap(..., sz_or_val_i, ..., fd, 0)+offi,最后却用已加偏移的 buf_i 调 HAP_munmap(buf_i, sz_or_val_i)。无设备调用 _render_entry() 的最小样例实际输出了这两行。

    后果是条件性的:当 offi 大于 0 时,映射基址、传给 kernel 的指针及解映射地址三者不一致;若长度只等于视图大小,指针后方可用范围也小于视图大小。源文档把它定性为「源码/生成文本可复核的条件性视图契约问题」,并明确尚未在目标 DSP 上确认驱动行为。对插件 ABI 的直接含义是:应传原始基址和映射总长,分别持有映射租约与视图偏移。

    反向 RPC 是一条跨处理器信任边界

    RPCListener.run() 是响应 DSP 发起请求的 daemon 线程,处理 open/close/seek/read/stat/mmap;tinylib 由当前 binded_lib 提供,其他路径经宿主 os.open/os.stat 等访问。源文档明确:请求字段是跨处理器信任边界,循环中对输入/输出对象个数、长度及指针推进没有完整容量验证。即使本代码只期望可信 DSP shell 与编译产物,硬件插件设计也应把路径白名单、请求大小上限、fd 生命周期和异常隔离前置。

    生命周期还有两处需要故障注入:exec_lib() 在 invoke 成功后关闭库,异常时先重连再重试;若 invoke 已打开句柄而后续失败,关闭/连接恢复的边界需验证。正常路径关闭库句柄,失败路径没有 finally 保证配对关闭。此外 RPCListener 是无限 daemon 循环,没有显式停止/join,同一设备上的 binded_lib/offset 为共享可变字段,并发装载需单独验证;未知 sc 抛异常会终止线程。

    自制硬件的验收顺序

    源文档给出的顺序是:先在没有执行器的隔离台架验证参数布局、视图偏移、fd/映射回收、RPC 文件权限、超时与并发,再对参考模型做逐节点数值和完整模型尾延迟比较。测试层面归档包含 DSP 专属量化用例,但需要对应设备与工具链;本轮只执行了 _render_entry() 的无设备生成探针,未运行 /dev/ion、FastRPC、Hexagon 编译、qemu 或真实模型。

    最后一条边界是权限:DSP 插件不应拥有 CAN 发送或修改 panda 安全策略的权限。真实初始化中的 ION 分配、fd share、CPU mmap、shell 装载和 FastRPC 连接是多阶段过程,源文档指出未见统一逐阶段回滚,这也是台架要覆盖的失败路径。

    常见问题

    DSP 后端现在被 openpilot 车端推理使用吗?

    没有。源文档明确 openpilot 的 load_oob() 当前只选择 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。ops_dsp.py 是 tinygrad 侧的 Hexagon 后端实现,不能当作现有车端推理硬件。

    非零 offset 的缓冲视图有什么风险?

    源文档探针显示,_render_entry() 生成映射时基址加了偏移,但解映射却用了已加偏移的缓冲指针。当 offi 大于 0 时,映射基址、传给 kernel 的指针与解映射地址不一致;若长度只等于视图大小,可用范围也小于视图大小。插件 ABI 应分别持有映射租约与视图偏移。

    反向 RPC 文件服务需要做哪些限制?

    该 daemon 线程把 DSP 请求的路径、大小与 fd 交给宿主文件操作,源文档指出循环中没有完整的容量验证,非 tinylib 名称可传到 os.open。即使只期望可信 DSP,也应前置路径白名单、请求大小上限、fd 生命周期和异常隔离。

    qemu 模拟出来的执行时间能当作 DSP 实时性数据吗?

    不能。mock 路径用 qemu-hexagon-static 执行静态程序,返回值按 1 GHz 假设把指令数伪装成秒数,源文档明确它不能作为 DSP 或车端实时性证据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_DSP_FASTRPC_ION_VIEW_RPC_SECURITY_CONTRACT_DEEP_DIVE.md(取用了 DSP 后端组件表、pra 参数 ABI、非零 offset 视图探针结果与反向 RPC 安全边界)。

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

    更新记录

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

  • tinygrad 超越函数降解:SIN 阈值 30 与双归约路径

    结论:transcendental.py 中的 SIN、EXP2、LOG2、SQRT 近似改写,只在该算子未被 renderer 声明为原生支持、或 force_transcendental=True 时才注册;TRANSCENDENTAL 默认值为 1,需 >= 2 才会在 dtype 降解之后并入后期降解,所以「源码里有近似函数」并不等于车端总走软件路径。xsin() 默认在 abs(d) >= 30 就从 Cody–Waite 切到 Payne–Hanek,但函数注释自述其适用范围从 39800 起,中间区间并无全域精度证明。以下为对固定源码归档的静态分析,未运行目标后端、未加载模型实体、未做实车验证。

    编译入口与注册条件

    get_transcendental_patterns() 只在 SIN、EXP2、LOG2、SQRT 未列入 renderer 支持算子,或 force_transcendental=True 时注册相应改写。float16/32/64 直接走本文件的公式,其他浮点 dtype 先转 float32 执行、再转回原 dtype。因此「后端是否声明原生超越指令」是决定走原生还是走降级的第一道开关。

    full_rewrite_to_sink() 在 dtype 降级之后把该模式集并入后期降级,并传入 TRANSCENDENTAL >= 2。TRANSCENDENTAL 默认值为 1,仅凭源码存在近似函数不能推断车端总走软件路径,需要结合该环境变量与实际后端来判定。

    另需区分入口:普通 POW 由 symbolic.py 的规则降为 xpow(),不受「后端是否支持 SIN/LOG2/EXP2」这同一入口控制。也就是说,即使后端声明了这些超越算子,幂运算仍可能落到本文件的 xpow() 上。

    位操作与结构辅助

    整个文件依赖目标后端在 BITCAST、移位和浮点格式上与公式假设完全一致。下表按函数族列出实现与限制,其中多处只对有限输入成立,不应泛化为标准库语义。

    函数族 实现与限制
    _lazy_map_numbers() 用条件选择分别处理 +Inf、-Inf、NaN 与普通数;NaN 判断用 x != x;最终特殊值由各顶层函数按自身语义决定
    mantissa_bits() / exponent_bias() / exponent_mask() 从 dtype 浮点格式取尾数与指数结构;依赖后端 BITCAST 与浮点格式精确一致
    pow2if() / ilogb2k() 通过整数位型构造 2^q、从位型提取指数
    rintk() 先按符号加减 0.5 再转整数,实现「中点远离零」取整;转换溢出与非有限输入不由该函数统一处理
    ldexp3k() / ldexp2k() 前者直接调浮点指数位,后者把整数指数拆两段构造二次幂再相乘;后者注释要求正、非次正规数
    frexp() 拆位得尾数与指数,假定输入非零;float64 掩码还会清符号;此处主要用于 SIN 对绝对值的范围归约

    这些函数的共同前提是「目标后端的整数宽度、移位与浮点转整数语义符合假设」。若设备缺 uint64 或其乘移语义不同,下面的 Payne–Hanek 路径需要重点复核。

    SIN 的两条归约路径

    SIN 先把幅值归约到小角,再用按 dtype 选择的多项式逼近,最后按象限恢复移相与符号。cody_waite_reduction() 用 1/pi 估算象限并做分段常数减法;float64 分拆多段 pi,float16 的减法临时升到 float32。payne_hanek_reduction() 则从 190 位 2/pi 常量提取片段,以 uint64 乘法求大角余量和象限。

    xsin() 默认同时构造 Cody–Waite 与 Payne–Hanek 两条路径,以绝对值 30 为选择阈值,并恢复原符号;fast=True 时只使用 Cody–Waite,调用者需自行约束幅值。Inf/NaN 最后统一映射为 NaN。

    路径 / 参数 机制 阈值与边界
    Cody–Waite 1/pi 估象限 + 分段常数减法 小角与中等角;float64 多段 pi,float16 临时升 float32
    Payne–Hanek 190 位 2/pi 片段 + uint64 乘法 注释声称适用范围从 39800 起;对整数宽度、移位/溢出与浮点转整数语义敏感
    xsin() 选择 默认在 abs(d) >= 30 选择 Payne–Hanek 有限样本在 31、100、1000、39800 及 1e6 未显示明显误差,但不构成整个 30–39800 区间或全部浮点值的精度证明
    fast=True 只用 Cody–Waite 调用者应自行约束幅值,否则大角误差无保证

    需要注意的一处不一致:Payne–Hanek 函数注释写的是 39800 <= d,而 xsin() 默认在 abs(d) >= 30 就选择其结果。文中不把函数注释里的「1 ULP」当作已验证保证。

    EXP2、LOG2、POW 与插件验收

    xexp2() 用 rintk 把输入分解为整数指数与小数部分,小数用多项式逼近、整数用 ldexp2k 恢复,随后按 dtype 上下界置 +Inf/0 并单独处理 NaN。xlog2() 对小幅值输入先乘二次幂以避开次正规区,再抽指数、正规化尾数并用多项式计算,末尾依次处理 +Inf、零、负数和 NaN,负零另用倒数检测。xpow() 从 exp2(exponent * log2(abs(base))) 构造幂;负底数时用转 int32 后回转识别非整数及奇偶,指数为零时强制返回 1。

    函数 关键分界 需另行验证的边界
    xexp2() 按 dtype 上下界置 +Inf/0,NaN 单独处理 上溢/下溢边界的实际取值
    xlog2() 小幅值先乘二次幂避开次正规区;负零用倒数检测 负值、零、次正规数
    xpow() 负底数转 int32 判奇偶;指数为 0 强制返回 1 超出整数转换可靠范围的指数、非有限值组合
    验收样本 正常数、次正规数、正负零、Inf/NaN SIN 切换边界与大幅值、EXP2 上下溢、LOG2 负值、POW 负底数/非整数指数

    面向自制计算后端,应明确选择「原生超越指令」还是「编译器降级」,并把该选择与模型工件、dtype、编译器版本及浮点模式绑定。验收要记录真实后端与参考实现的绝对/相对/ULP 误差和最坏时延。模型输出的变化必须在离线回放与受控台架中评估,计算插件不得获得 CAN 发送权。

    验证方面,在 PYTHONPATH=. DEV=PYTHON 下上游 test_transcendental_helpers.py 五项通过;TRANSCENDENTAL=2 小样例中,float32 的 SIN/EXP2/LOG2 对给定有限样本与 Python math 的最大绝对差分别约 2.8e-8、2.5e-8、0,POW 的负底数整数/非整数、0^0 及 LOG2 特殊值样例符合预期。更广的图重写边界测试因缺 hypothesis 未导入;大角样本只做有限抽查,未运行全域误差证明、目标 QCOM 后端、LFS 模型或实车验证,归档源码未编辑。

    常见问题

    后端不支持 SIN,就一定会走软件近似吗?

    不一定等同于此结论的细节是:只有在 renderer 未声明支持这些算子、或 force_transcendental=True 时才注册改写,且还需 TRANSCENDENTAL >= 2 才会并入后期降级;TRANSCENDENTAL 默认值为 1。

    这里的 SQRT 是独立的牛顿迭代平方根吗?

    不是。在无原生路径时,SQRT 被改写成 xpow(x, 0.5),因此它复用幂函数的构造,而不是另一套牛顿迭代实现。

    xsin 在 30 到 39800 之间可靠吗?

    不能这样断言。Payne–Hanek 的函数注释自述适用范围从 39800 起,而 xsin() 默认在 abs(d) >= 30 就选择它;有限样本在若干点上未显示明显误差,但这不构成该区间的全域精度证明,注释中的「1 ULP」亦未经验证。

    自制计算后端应重点验收哪些点?

    至少覆盖 float16/32/64 的正常数、次正规数、正负零、Inf/NaN,SIN 切换边界与大幅值,EXP2 上下溢,LOG2 负值,POW 负底数与非整数指数;若设备缺 uint64 或乘移语义不同,还需重点验证 Payne–Hanek 路径与 dtype 模拟。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_TRANSCENDENTAL_REDUCTION_NUMERICS_PLUGIN_CONTRACT_DEEP_DIVE.md(逐函数实现、SIN 两条归约路径与 TRANSCENDENTAL 入口条件)
    • 项目 02 —— openpilot-research/TINYGRAD_OP_DECOMPOSITION_DIVMOD_THREEFRY_PLUGIN_CONTRACT_DEEP_DIVE.md(基础算子降级专题,作为本文的补充阅读)

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

    更新记录

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

  • tinygrad C 风格渲染器:指针重解释、门条件与调用可信边界

    结论:tinygrad 的 C 风格渲染层(CStyleLanguage)不是「读任意计算图再生成代码」的入口,而是一道下游文本生成器:它只消费已完成范围展开、类型与算子降解、门搬移和线性化的 UOp 图,把模式表映射成 C 风格控制流、指针与类型声明。两个前提必须同时成立——门条件不能在这一层丢失,CALL 与 CUSTOM 必须当成可信编译输入边界。渲染成功既不证明指针可安全解引用,也不授予 CAN 发送权。

    渲染调用链:这一层消费的是「已完成」的图

    入口是 do_render():它对已线性化的 UOp 调用 renderer 的 render()。CStyleLanguage.render() 先经 _render() 生成内核体、名字与参数表,再由 render_kernel() 拼出源码;随后 do_compile() 把源码交给 compiler.compile_cached()。这是一条单向链路:上游做结构变换,这一层只做文本落地。

    关键在于它不能直接接收任意前端 UOp。范围展开、类型/算子降解、门搬移和线性化都必须在进入这一层之前完成;如果未匹配的模式漏了进来,_render() 会断言失败,而不是静默忽略。也就是说,本层对「输入是否已合法」是有断言的,但对「生成的指针是否真的可访问」没有保证。

    Clang、OpenCL、Metal、CUDA、HIP 与 QCOM 等子类都基于这一通用基类提供目标语法或能力。基类的 code_for_workitem 和 barrier 是空配置,正是留给具体后端填空的位置。

    模式表与关键函数

    下表列出这一层的核心组件及其行为前提。未匹配会在 _render() 断言失败,这是它与「静默降级」实现最本质的区别。

    函数或组件 行为与前提
    base_rewrite 用模式表把 RANGE/END/IF/ENDIF 渲染为 C 风格控制流,BARRIER 渲染为子类提供的 barrier 文本;CONST、CAST/BITCAST、INDEX/SHRINK、LOAD/STORE、ALU、WMMA、CALL 与 CUSTOM 也在此映射。
    CStyleLanguage 声明 kernel_typedef、地址空间前缀、code_for_workitem、code_for_op、向量语法与图像常量等可覆盖项。
    render_kernel() 用 SPECIAL 的局部维度上界计算 launch_bounds 并构造函数签名;有图像形状参数时附加固定的未归一化、边界钳制与最近邻采样器声明。只输出源码,不验证真实设备工作组资源。
    render_buffer() 为 BUFFER 生成局部/寄存器数组声明,LOCAL 可加对齐或共享内存前缀;数组长度来自 max_numel(),设备端资源上限仍需实际编译与启动验证。
    _render() 先统计子节点使用次数与被 STORE 指向的 PARAM;按 UOp 线性顺序命名临时变量,单消费者表达式多数直接内联,多消费者或控制节点发出语句并维护缩进。AFTER 仅取其值源名称。

    还有一组负责非原生浮点的函数:create_non_native_float_pats() 对不原生支持的浮点 dtype 在 ALU 前提交 weak 常量,通过 float 中间类型运算/比较;pm_manual_bf16_cast 与 pm_bf16_ushort_const 处理 BF16 位型转换及以 ushort 存储的常量文本。uops_to_dtypes() 与 wmma_args() 收集向量 dtype/宽度和 WMMA 形状,供子类生成 typedef、矩阵内建函数或额外声明。

    指针重解释、门条件与向量 ABI

    指针与访问类型的生成集中在 render_index()、_render_dtype()、render_type()、render_ptr() 和 render_access()。ALU 地址空间的索引按 C 数组或向量 lane 访问,其他地址空间返回缓冲指针加偏移;常量向量 lane 在小向量时可能变为 .xyzwabcd,阈值由 gep_arr_threshold 决定。

    环节 生成的文本行为 它不保证什么
    访问类型生成 按 lane 数选择向量访问类型。 不保证访问宽度与硬件对齐一致。
    向量指针重解释 向量访问宽于标量指针或访问 dtype 与缓冲 dtype 不同时,先重解释为目标指针再解引用。 不证明对齐、缓冲大小、别名或可访问性。
    图像类型选择 依据是否被写入选择 write_only/read_only image2d_t。 不校验真实采样规则与图像尺寸上限。
    门条件 LOAD base_rewrite 对有 gate 的 LOAD 生成条件表达式,避免 gate 假时实际求值访存。 普通 STORE 只接受无独立 gate 的形式,安全性同时依赖上游门搬移。

    这里有一条容易被忽略的两阶段依赖:更早的 pm_linearize_cleanups 把 gated STORE 拆为 IF-STORE-ENDIF,因此门的安全性同时依赖上游线性化和本层文本渲染。code_for_op 中的 CDIV/CMOD 是 C 的截断语义,FLOORDIV/FLOORMOD 的负数补偿已由前序降解处理,不能在后端直接合并为不等价指令。

    CALL 与 CUSTOM 是可信编译输入边界

    CALL 通过函数指针强制转换生成调用文本,CUSTOM/CUSTOMI 用 UOp 参数字符串格式化源码。源码文档明确把它们定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据。新后端应限制可用模板、绑定函数 ABI,并保留生成源码及二进制工件供审计。

    同样地,render_ptr() 的强制类型转换要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式就认定可安全读取。若后端仍保留未处理的 Invalid、门条件或不支持的 ALU,编译失败或运行时越界都必须使模型不可用,而不是继续向控制链输出「有效」观测。

    自制 C 风格后端的验收清单

    综合上述,自制 C 风格后端至少须定义线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler。验收时可对照下表逐项确认边界来源。

    验收项 必须显式声明 来源边界
    坐标与工作组 线程/工作组坐标、launch_bounds 依据 基类默认空配置,须目标语言补齐。
    同步 真实屏障文本 基类 barrier 为空。
    指针与向量 GLOBAL/LOCAL 前缀、向量 ABI、对齐要求 render_ptr() 只做文本转换。
    图像 读写方向、采样规则、尺寸上限 render_kernel() 只发声明。
    可审计性 生成源码与二进制工件留存 CALL/CUSTOM 为可信输入边界。

    渲染成功不授予 CAN 发送权——车型控制与 panda 固件 safety 仍是独立边界。验证层面,源码作者用 PYTHONPATH=. DEV=PYTHON 的最小 UOp 编译探针确认:CStyleLanguage(Target()) 对普通 float LOAD/ADD/STORE 输出参数指针和赋值源码;以输入值比较作为门的 STORE 输出 if (...) { ... }。首次尝试用裸 UOp.variable() 构造 gate 被 spec_program 拒绝,改用真实 LOAD 比较后才通过——失败样例不能计为有效通过。

    常见问题

    能直接用 tinygrad 的 C 风格渲染器写自己的 GPU 后端吗?

    可以作为起点,但基类只提供模式表与可覆盖项,code_for_workitem 和 barrier 默认是空配置。自制后端必须补齐线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler,否则生成的源码不具备可执行前提。

    为什么渲染成功不代表能发出 CAN?

    因为渲染层只输出源码文本,render_kernel() 也只发声明、不验证真实设备工作组资源。源码文档明确:渲染成功不授予 CAN 发送权,车型控制与 panda 固件 safety 是独立边界。编译通过只是计算路径的前一步,不影响车辆控制与安全许可。

    指针强制类型转换后读内存安全吗?

    不安全。render_ptr() 的向量指针重解释只是文本转换,源码文档说明它不证明对齐、缓冲大小、别名或可访问性,并要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式认定可安全读取。

    CALL 与 CUSTOM 能不能给热插拔插件或远程数据使用?

    不建议。源码把 CALL(函数指针强制转换)与 CUSTOM/CUSTOMI(按 UOp 参数字符串格式化源码)定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据;新后端应限制模板、绑定函数 ABI,并保留生成源码与二进制工件供审计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CSTYLE_RENDERER_POINTER_GATE_CALL_TRUST_CONTRACT_DEEP_DIVE.md(取用了 base_rewrite 模式表、指针/图像类型生成、门条件两阶段依赖、CALL/CUSTOM 可信边界与最小 UOp 编译探针结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_RENDERER_CAPABILITY_ESTIMATES_LAUNCH_METADATA_DEEP_DIVE.md(仅用于确认渲染能力专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • eSIM 异步操作:busy 布尔值与乐观切卡都不等于成功

    结论:CellularManager 用一个 busy 布尔值代表 eSIM 异步操作状态,但它不是互斥锁,也没有拒绝重入;switch_profile() 会先把本地缓存里目标 ICCID 标成 enabled 并立刻刷新页面,属于乐观切换,不等于运营商注册成功。轮询失败只把状态从「真」降为「未知」并保留旧列表,必须连续两次为假才清空——因此「界面上还列着 profile」不证明本次探测健康。

    本文基于固定源码快照对 CellularManager 的并发与 UI 回执做静态分析,不执行 APDU、下载、切卡或网络注册。eUICC 协议本身的下载流程、LPA 通道归属,以及页面入口属于相邻专题;这里只关注「请求提交后,主机到底知道什么」这一层。

    并发:一个 busy 布尔值管不了两个操作

    _run_operation() 被调用时立刻把 busy 置为真,然后启动一个 daemon worker;worker 持有 _lock 执行 LPA 调用,默认随后调用 list_profiles(),无论成功还是异常,都只把结果排队交给 UI 线程的 _finish() 处理。_lock 保证 LPA 调用被串行化,但不保证请求唯一、不保证 deadline、不保证可取消,也不校验调用方身份。方法自身没有 if busy 分支去拒绝第二个操作。

    _finish() 在 UI 回调里把 busy 置假,并把下一次周期轮询延后约 5 秒。问题在于两个操作的完成回调会依次清同一个布尔量:第一个回调执行时,第二个操作可能仍在运行。mici 页面多数按钮在调用前检查 busy,但管理器公共方法不设这道门,所以「UI 看起来只有一个操作在跑」是页面层的行为,不是管理器的并发契约。

    状态字段 维护位置 它真正代表什么 它不保证什么
    busy _run_operation() 置位,_finish() 清位 「至少有一个操作处于已提交或执行中」 不是互斥锁;不计数、不拒绝重入、不代表唯一请求
    polling _poll_profiles() 置位,_finish_poll() 清位 「本轮 profile 轮询正在进行」 轮询异常只清标记并记日志,不触发操作错误回调
    profiles UI 线程按结果回调更新 eUICC/LPA 视图下的 profile 缓存列表 不代表 modem 已注册蜂窝网络;乐观切换会先改此缓存
    is_euicc 每轮 _poll_profiles() 重新探测 本轮探测的 SIM 是否 eUICC,取值含未知 连续两次为假才清列表;单次为假只降为未知
    modem_state process_callbacks() 空闲时读取 硬件快照 读取无异常捕获、无独立 deadline;抛错会传播到 UI 调用方

    _ensure_lpa() 与 _enqueue() 负责首次按平台取得 LPABase 并缓存实例,后台线程用短锁保护回调队列。SIM 是否为 eUICC 每轮重新探测,但 LPA 对象本身不会每轮重建。这一点决定了锁的粒度:锁保护的是「队列与单次 LPA 调用」,而不是「一次业务事务」。

    乐观切卡与结果回执:两个不同的层面

    switch_profile() 先用 replace() 把目标 ICCID 在缓存列表中标记为 enabled,并立即触发页面更新;worker 随后执行切换与通知处理,但显式指定 relist=False,成功后不马上读取新的 profile 列表。真正的校正要等下一次周期轮询。EsimUI._on_profile_clicked() 还会把按钮移到前端,进一步强化「已经切好了」的观感。

    即使随后的 LPA 调用成功,运营商注册、数据可用性和路由切换仍是其他系统的状态,不在这个布尔量或这个列表里。delete_profile()、download_profile()、nickname_profile() 都通过同一 worker 入口运行,默认在操作后重列;这份列表代表 eUICC/LPA 视图,不证明 modem 已注册蜂窝网络。失败会展示错误并安排刷新,但错误与回滚之间存在异步窗口。

    操作 成功路径的即时行为 回执边界
    switch_profile() 先改本地 enabled 位并刷新页面;relist=False,成功不立即重列 乐观状态要等下次周期轮询校正;不等于激活确认
    delete_profile() 经同一 worker 运行,默认操作后重列 列表刷新不等于运营商注册或数据连通
    download_profile() 经同一 worker 运行,默认操作后重列 下载完成仍须核对新实体是否恰好新增一个
    nickname_profile() 经同一 worker 运行,默认操作后重列 仅改标签,不改变注册状态

    通知处理这一层也独立:execute_and_process_notifications() 在操作后等待 1 秒,并在 finally 中尝试处理通知;通知处理异常只打印到 stderr,不重新抛出。管理器可能把核心操作视为成功,但通知投递单独失败,二者不能合并成同一个布尔值。

    轮询与缺测:失败不等于无卡

    _poll_profiles() 在后台线程锁内先调用 is_euicc(),为真才列 profile,结果回 UI。_finish_poll() 在忙碌时丢弃本轮结果;若轮询抛异常,只清 polling 并记日志,没有操作错误回调。更关键的是状态迁移:先前为真而本轮为假时,会把状态设为 None 且保留旧列表,下一次再为假才清空。所以 UI 上仍有 profile,并不证明本轮探测健康。

    process_callbacks() 在空闲且距上次轮询至少 5 秒时,于 UI 调用线程同步读取 HARDWARE.get_modem_state(),这里没有捕获硬件状态读取异常,也没有该读取的独立 deadline。离线探针确认:空闲轮询会在读取硬件状态之前刷新 _last_profile_poll,因此 get_modem_state() 抛错会传播到 UI 调用方,并在同一时间点的下次调用中抑制重试。

    观测现象 源码行为 不能推出的结论
    本轮 is_euicc 为假,前次为真 状态降为未知,保留旧列表;第二次为假才清空 不能判定「现在无卡」
    轮询抛异常 仅清 polling 并记日志,无错误回调 不能判定当前列表是新鲜结果
    操作失败并展示错误 安排重新轮询,但不回滚乐观列表 错误与回滚之间仍有余量窗口
    安装弹窗存在时列表更新 只有恰好新增一个 ICCID 才走普通激活,其他情况也关弹窗 不能把「刷新成功」当「安装成功且可用」

    下载完成后的核对同样独立:EsimUI._on_profiles_updated() 在安装弹窗存在时比较新旧 ICCID,只有恰好新增一个才进入普通激活流程,其他情况也会关闭安装弹窗。列表刷新、安装成功、新 profile 可用不能压缩成同一布尔。

    面向硬件与插件的契约建议

    把 eUICC 能力视为受信任设备服务时,每个请求应带唯一 ID、SIM/卡槽身份、操作种类、deadline 与可恢复阶段,LPA 通道锁只由该服务持有。服务要分别回报 accepted、APDU/HTTPS 操作完成、通知处理、profile 重列、modem 注册与数据连通性这几种状态。

    1. 多请求状态按操作 ID 计数或排队,不应由一个 busy 布尔值代表。
    2. 硬件状态读取失败要保留错误并约定重试,而不是吞掉或抑制下一次调用。
    3. 禁止第三方 UI 插件直接操作 AT/电源,或以 busy 和乐观 enabled 位作为切卡完成证明。
    4. 台架应注入连续请求、SIM 热插拔、AT 忙/超时、通知失败、列表短暂陈旧、网络失联及 UI 离页后回调。

    需要保留的边界:本归档没有实卡,也没有上述故障注入结果,探针只复现 Python 回调顺序,不构成原生线程抢占、UI 帧调度或实卡状态的验证。

    常见问题

    busy 为真时再提交一个操作会发生什么?

    管理器公共方法没有 if busy 拒绝分支,第二个操作仍会进入 worker。worker 的 LPA 段由 _lock 串行,但两个完成回调会依次把同一个 busy 清零——第一个回调执行时第二个操作可能还在运行。这是条件性源码推导,源文档明确未做并发交错实测。

    点了切卡、界面已经高亮,是不是就切好了?

    不是。switch_profile() 先用 replace() 把目标 ICCID 标为 enabled 并立即刷新页面,且设置 relist=False,成功不立即重列,属于乐观切换。运营商注册、数据可用性与路由切换是其他系统状态,本地 enabled 位不等于激活确认。

    轮询失败会不会把 profile 列表清掉?

    不会一次清空。先前为真而本轮为假时,状态先转成未知并保留旧列表,第二次再为假才清空;轮询异常只清 polling 并记日志,没有操作错误回调。所以列表还在,并不表示本轮探测健康。

    下载完成就等于新卡可用吗?

    不等于。源码在安装弹窗存在时比较新旧 ICCID,只有恰好新增一个才进入普通激活流程;列表刷新、安装成功、新 profile 可用是三种不同状态。通知处理还可能单独失败,只打印到 stderr 不重抛。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CELLULAR_MANAGER_ESIM_ASYNC_RESULT_CONTRACT_DEEP_DIVE.md(取用了 CellularManager 的函数级时序、五项结果竞态边界与无设备回调/轮询探针结论)。
    • 项目 02 —— openpilot-research/ESIM_LPA_TLV_APDU_DOWNLOAD_DEEP_DIVE.md(仅用于区分主机回执与 eUICC 下载协议,未取用其实卡验证内容)。

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

    更新记录

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

  • 丰田 RAV4 TSS2 雷达 ARS510:0x80 对象列表与 0x85 元数据

    结论:在一辆车主自有的 2022 款丰田 RAV4(TSS2,前向雷达固件 8821F0R03100,雷达在 0x750/0x0f)上,ARS510 前向毫米波雷达的对象列表是可以被动解码的广播数据:它走雷达总线,以 0x80 分 106 帧拼成一条 742 字节记录,每个对象槽 36 字节,直接给出纵向距离、横向偏移、对地速度、车道权重和类别。这个结论只覆盖这一辆车这一次固件记录——文档明确把「字段比例与零点」标为名义值、多处置信度存疑,不能外推到 RAV4 其他年款或其他雷达固件。

    本文整理自一份针对这台车雷达的开源解码与集成文档。该文档把「原始解码」「物理量校准」「实车结论」分层标注,本文保留这套分级,不将其拔高为「RAV4 全系通用」。

    车辆与适用范围

    数据来自一位车主的一辆 2022 RAV4,用 openpilot 记录。文档给出的证据结构是:3 段参考行程(开发、城市、高速,共 88 分钟)带摄像头与里程计参照;24 段行程中的 399 个一分钟片段用于字段统计;20 段留出行程(4.6 小时)做回放对照;6 段新行程(1.9 小时)与多套分支上的闭环路测。

    项 内容
    车型 丰田 RAV4(TSS2,对应 openpilot 的 2022 / 2023 平台标识)
    年款 / 市场版本 2022(源文档未给出销售市场,也未确认具体配置)
    ECU / 改装状态 前向雷达固件 8821F0R03100;由 openpilot 采集,非量产改装
    采集跨度 3 段参考 88 分钟 + 399 个一分钟片段(来自 24 段行程)+ 20 段留出 4.6 小时
    适用范围 仅这一辆车的雷达与这一次固件;比例与零点多为名义值

    总线与寻址

    文档按 openpilot 的总线编号描述器件:雷达本身与其 ACC 目标帧在 bus 1,车速来自 bus 0 的车体帧 0xB4。对象列表在 bus 1 的 0x80,以 106 帧拼成一条 742 字节记录:头部、20 个 36 字节对象槽、一个 CRC32。雷达周期约 60 毫秒,解码后以约 16.7 Hz 输出对象点。

    文档没有描述这台车的 OBD 口之后是否存在网关,也没有给出 11 位 / 29 位寻址或 CAN FD 的结论——这几项属于源文档未覆盖的范围,本文不补。

    项 内容
    物理层 车用 CAN,分为雷达总线(bus 1)与车体总线(bus 0)
    链路层 对象列表 0x80 分帧传输(106 帧 → 742 字节记录)
    寻址 源文档未给出 11 位 / 29 位结论
    CAN FD 源文档未提及
    记录完整性 CRC32/ISO-HDLC(小端)覆盖记录第 1 至 140 字节

    已解码信号

    下表从源文档的对象槽与 0x85 元数据记录中抽取代表性字段。置信度沿用文档符号:● 已确认、◐ 可能、○ 候选。位宽写法如 32|12 表示起始位 32、宽 12 位。

    信号 位置(ID / 位域) 解码 证据
    dRel(纵向距) 0x80 槽 32|12 (code − 160) / 16 米 ● 字段布局确认;零点与比例见准确度文档
    yRel(左为正横向偏移) 0x80 槽 44|12 (code − 2048) / 64 米 ● 同上,比例标称 ±10%
    对地速度 0x80 槽 64|10 (code − 510.5) × 0.15 米/秒;相对速度 = 该值 − 自车速度 ● 运动解释确认
    age(航迹年龄) 0x80 槽 24|7 雷达周期数;重启即视为新航迹 ●
    车道权重(右 / 左 / 自车) 0x80 槽 148|4、152|4、156|4 0–15,三者之和为 15 或 16 ●
    类别 class 0x80 槽 163|3 1 新建、2 车、3 大型车、4 行人、5 暂定、6 两轮车 ●;5 为 ○ 候选
    ACC 目标 0x235 / 0x237,50 Hz 原始字段 ● 原始字段;◐ 名义单位换算
    目标摘要 0x191–0x194 原始结构 ● 原始结构;◐ 解释,需度量与类别校准
    事件对 0x195 / 0x196 原始载荷;10 位短时距碰撞码领先本车减速 ● 原始载荷;◐ 状态解释
    元数据 c0 偏移 0x85 单元 32|12 (code − 2000) × 0.01 米 ◐ 比例约 ±10%
    元数据 c1 航向 0x85 单元 48|16 −1.8e-5 × (code − 31200) 弧度/码,零点 31100–31300 ◐ 符号与零点;单位有界 1.6–2.2e-5
    元数据 c2 曲率 0x85 单元 64|15 +2.5e-6 × (code − 16020) 1/米/码 ◐ 符号与零点;单位有界 2.0–2.7e-6
    元数据 c3 曲率变化率 0x85 单元 10|10 +4e-6 × (code − 500) 1/米²/码 ◐ 符号、零点;单位仅到量级

    读取方法与注意事项

    拼接与校验。0x80 记录按 106 帧重组后,用 CRC32/ISO-HDLC(小端)校验;0x85 记录的 147 字节同样按 21 帧重组成,CRC32 覆盖第 1 至 140 字节。文档报告自带样本与七份完整日志的全部记录均通过校验。

    配对要按计数器,不要按到达时间。0x85 前缀与 0x80 头部共享一个时钟和记录计数器;用「最近的到达时间」配对,约有三分之一的机会会配到相邻周期,因此文档要求按计数器配对,并报告在 28 段行程、446371 组配对中覆盖了超过 99.96% 的 0x80 记录。

    速度外溢是这套解码目前最大的问题。文档记录了一个缺陷:出现 1 至 10 秒的假接近,距离超过 40 米。默认的 anchor 配置用雷达自车 ACC 目标给前车速度加界,并叠加 steady 的护栏与平滑;两者合起来在 20 段留出行程上把硬假刹车从 93 个 tick 降到 48 个 tick,响应时间不变。文档另给出 steady(不含 ACC 目标)与 stock(纯解码,外溢直达规划器)两个配置。

    证据等级与边界

    文档用 ●/◐/○ 三级标注:● 已确认、◐ 可能、○ 候选。所有比例与零点都被写明是名义值。也就是说,「(code − 160) / 16」这类公式是文档给的标称关系,并非经独立测量标定的绝对定标。

    另一层边界在来源:全部统计都出自这位车主这一辆车、这一次雷达固件与这批日志。车道单元的分配、航向与曲率的相关性、以及与摄像头车道线的中位误差,都只在记录范围内成立;据此,任何字段含义与比例都不能外推到其他 RAV4、其他年款或其他固件版本。

    常见问题

    这辆 RAV4 的雷达对象列表,能从 OBD 口直接读吗?

    源文档没有涉及 OBD 口这一层,它只按 openpilot 的总线编号给出「雷达在 bus 1、车速在 bus 0」。OBD 口之后是否存在网关、能否直达雷达总线,文档未记录,本文不据此下结论。

    为什么对象列表和 0x85 要按计数器配对,而不是按到达时间?

    因为两条记录的时间戳邻近,用「最近到达时间」配对约有三分之一的机会会配到相邻周期,把不同周期的对象和车道曲线混在一起。文档改用共享的时钟与记录计数器配对,并在 28 段行程、446371 组配对中把覆盖提高到 99.96% 以上。

    文档里说的「速度外溢」是什么,怎么缓解?

    它是这套解码目前最大的已知缺陷:在 1 至 10 秒窗口内出现距离超过 40 米的假接近。默认 anchor 配置用雷达自车 ACC 目标给前车速度加界,叠加 steady 的护栏与平滑,在 20 段留出上把硬假刹车从 93 降到 48 个 tick,响应不变;stock 配置不做过滤,外溢会直达规划器。

    这些字段比例与零点,能套到别的 RAV4 或其他年款吗?

    不能。比例与零点在源文档里都是名义值,全部结论只覆盖这一辆车、这一次固件与这批日志。其他年款、其他雷达固件是否一致,文档未验证。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/toyota-ars510-radar-48b9508/README.md(本项目车型档案主来源,取用了对象列表与总线的总体结论)。
    • 项目 01 —— public-can-dbc-datasets/additional/toyota-ars510-radar-48b9508/docs/04_metadata_record_0x85.md(取用了 0x85 记录的布局、配对规则与车道边界单元字段)。

    说明:本文整理自第三方开源项目的公开文档,仅代表该文档所记录的单个车辆样本,不代表同型号全系车型。CAN 与诊断操作存在风险,任何实车操作前请自行复核并遵守当地法规。

    更新记录

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