标签: 固件更新

  • rusEFI 开源 ECU 的 CAN 接口:GPL-3.0 三条附加条款与取证路径

    结论:把 rusEFI 当作 CAN 报文约定的来源时,第一件事不是找 ID,而是读许可证附加条款:它在 GPL-3.0 之上另加三条用途限制(仅限非道路的陆上/水上车辆、不符合汽车环保法规、未批准用于载人航空)。本次受检快照只保留 README.md 与 license.txt 两个文件,不含任何 DBC 或 CAN ID 表,因此报文定义必须回到固件源码、simulator 与 rusEFI console 三条路径上取证。

    三条附加条款先划出使用边界

    GPL-3.0 本身处理的是「改了要不要开源」,而 rusEFI 在许可证正文之前单独加了三条附加条款,处理的是「能不能用在这个场景」。对任何打算把它的报文约定或移植代码接入产品的团队,这三条比许可证传染性更早构成否决条件。

    条款 原文含义 对接入方的影响
    rusEFI#1 面向非道路的陆上与水上车辆 道路车辆项目须自行评估合规责任,不能把「开源」当作上路依据
    rusEFI#2 不符合汽车环保法规 排放相关标定与后处理功能不能引用其实现作为合规证据
    rusEFI#3 未批准用于载人航空应用 航空场景直接排除,无需再做技术评估
    GPL-3.0 主文 传染性,修改后分发须同许可证开源 闭源产品宜以运行期数据/独立进程隔离,不要静态链接固件代码

    这三条的表述方式值得注意:它们写在许可证正文之前的「附加条款」段,而不是放在 README 的免责声明里。也就是说,它们是许可证的组成部分,不是项目方的使用建议。

    快照里有什么、没有什么

    受检快照只含两个文件:README.md 与 license.txt。README 是一份目录索引,描述了仓库顶层各子目录的用途,但源码、硬件文件与 DBC 都没有随快照保留。这意味着本文不能给出任何报文 ID、波特率或信号缩放——任何这类数字都不在受检范围内。

    仓库子目录 官方说明中的定位 快照是否含该文件 能拿来做什么
    firmware 面向 STM32 芯片的开源发动机控制单元源码,含实现概述 否 CAN 报文定义的最终出处,需回上游仓库核对
    simulator 固件的 win32 或 posix 版本,无需任何硬件即可探索 否 不接 ECU 硬件时验证解析逻辑的主要手段
    java_console rusEFI 自有的原生标定软件 否 观察运行中变量与报文交互的参考实现
    hardware 较旧 PCB 的 KiCAD 文件 否 确认 CAN 收发器与引脚连接
    unit_tests 固件各部分的单元测试,公开覆盖率报告 否 判断哪些报文处理路径有回归保护
    java_tools 杂项开发工具集 否 格式转换与辅助脚本
    misc 软件开发流程说明 否 了解上游的变更与评审节奏
    firmware/CHANGELOG.md 发布变更日志 否 确认报文约定在哪一版发生变更

    把这张表理解成取证顺序而不是功能列表:firmware 是定义的唯一权威来源,simulator 是不接硬件时的验证手段,java_console 是观察交互行为的参考实现。三者都缺失时,任何关于报文的说法都只能是推断。

    不接硬件的三步取证路径

    CAN 接口类项目的常见错误是先写解析器再找真值。rusEFI 的目录结构给出了一条成本更低的顺序:先用 simulator 的固件版本在没有 ECU 的情况下跑通解析,再用 console 观察变量与报文的对应关系,最后回固件源码确认定义与校验逻辑。

    步骤 使用对象 能得到的结论 前置条件
    1. 跑通解析 simulator 解析器能否处理固件实际发出的帧序列 需要按 README 完成构建,win32 与 posix 二选一
    2. 对齐语义 java_console 报文字段与标定变量名的对应关系 需要与 simulator 或真实硬件建立连接
    3. 确认定义 firmware 报文 ID、字节布局、缩放与校验的真实出处 需要能阅读 C/C++ 源码并定位到对应版本
    4. 锁定版本 firmware/CHANGELOG.md 报文约定在哪次发布发生变更 需要把结论固定到具体发布版本号

    四份外部资料的分工

    README 同时指向四类外部资源,取证的可靠性差别很大:Doxygen 生成的源码文档在 rusefi.com/docs/html,反映代码现状,适合定位具体实现;wiki 承担使用说明,适合理解配置项含义;论坛承担问答,适合确认某条约定是否已有结论但不构成证据;变更日志随固件发布更新,是判断「某个行为从哪一版开始变化」的唯一顺序依据。做报文取证时应当以 Doxygen 与变更日志为准,把论坛结论当作待验证的线索。

    构建环节有三个必须确认的契约点

    README 对构建只给了三条硬性提示,但每一条都会直接影响报文取证的可复现性:仓库使用了 submodule,克隆后必须执行 git submodule update --init,否则依赖缺失会表现为编译失败而非功能缺失;推荐 GCC 版本写在 firmware/gcc_version_check.c 里,版本不符可能只在高优化级别下暴露问题;构建产物从 build server 取,说明本地构建与官方二进制可能存在差异,比对报文行为时须注明用的是哪一个。

    契约点 要求 跳过会怎样
    子模块 克隆后执行 git submodule update --init 依赖缺失,编译在早期阶段失败
    编译器版本 以 firmware/gcc_version_check.c 为准 可能构建成功但运行行为与官方二进制不一致
    产物来源 官方二进制来自 build server,需注明版本 不同来源的报文行为差异被误判为协议问题
    文档口径 Doxygen 文档与 wiki 分开维护 只看一处会漏掉近期的接口变更

    常见问题

    这份快照里能直接拿到 rusEFI 的 CAN 报文 ID 吗?

    不能。受检快照只有 README.md 与 license.txt 两个文件,README 仅描述顶层目录用途,源码、硬件文件与 DBC 均未随快照保留。报文 ID、波特率与信号缩放必须回到上游仓库的 firmware 目录,并配合 simulator 与 java_console 交叉确认。

    GPL-3.0 的三条附加条款是不是意味着完全不能上车?

    不等于禁止,但它把合规责任明确留给了使用方。三条条款分别声明:项目面向非道路的陆上与水上车辆、不符合汽车环保法规、未批准用于载人航空应用。道路车辆项目不能把「开源」本身当作上路或排放合规的依据,需要自行完成评估与认证。

    没有 ECU 硬件能不能先验证报文解析逻辑?

    可以。README 列出 simulator 目录提供固件的 win32 或 posix 版本,允许在没有任何硬件的情况下探索。配合 unit_tests 目录公开的覆盖率报告,可以判断哪些报文处理路径已有回归保护。但前提是先把构建跑通,包括子模块初始化与 GCC 版本对齐。

    克隆下来就能直接编译吗?

    不能只做一次克隆。README 明确说明仓库使用 submodule,必须在克隆后执行 git submodule update –init;同时推荐 GCC 版本写在 firmware/gcc_version_check.c 中,需按该文件核对。若只需二进制,官方构建产物从 build server 获取。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/rusefi-can/license.txt(GPL-3.0 主文与 rusEFI#1 至 rusEFI#3 三条附加条款)
    • 项目 01 —— public-can-dbc-datasets/additional/rusefi-can/README.md(顶层目录用途、子模块构建要求、GCC 版本检查与 build server 产物说明)

    免责声明

    本文仅记录受检快照中 README 与许可证文件所载内容,未包含任何未经材料支持的报文 ID、波特率或信号定义。rusEFI 为社区开源项目,文中引用的目录用途与许可证条款以材料原文为准;快照未包含源码与许可证以外的第三方组件信息。涉及道路车辆、排放合规与安全相关用途时,请以官方资料与实际测量为准,并自行完成合规评估。

    更新记录

    • 2026-10-15 初稿

  • DBC 到固件的版本校验:T26 用 64 位 SHA-256 心跳

    结论:T26 的 DBC 工作流把版本一致性做成了一条 CAN 心跳:cantools 生成 C 代码的同时,脚本把每个 DBC 的 SHA-256 截断为前 8 字节(64 bit)写成宏,各 ECU 用专用 ID 0x055、DLC 8 周期广播并互相比对。截断到 64 bit 不是为了省事,而是为了塞进一帧经典 CAN 的 8 字节载荷——代价是碰撞概率从 2⁻²⁵⁶ 升到约 2⁻⁶⁴,对这个用途完全够用。

    DBC 字段与固件解码的一一对应

    DBC 的价值在于把十六进制报文翻译成带名字和单位的信号。T26 的流程从 Kvaser Database Editor 建库开始:先建节点(如 ECU_Principal),再建报文(Name、ID、DLC、Transmitter),最后逐条加信号。每个字段的填写错误都会直接变成固件里的读数错误。

    DBC 字段 要填什么 填错后的固件表现
    ID 与 DLC CAN 标识符与字节长度(如 0x101、8) 收不到该帧,或按错误长度解包直接越界
    Start bit 与 Length 起始位与位长度(如 0、16) 取到相邻信号的位,数值整体偏移
    Byte order Intel(小端)或 Motorola(大端) 高低字节互换,数值随量级剧烈跳变
    Value type Signed / Unsigned 负值变成极大正数,温度、转矩类信号尤其明显
    Factor 与 Offset 缩放因子与偏移(如 factor = 0.1,则 100 表示 10.0 km/h) 量程整体缩放错误,趋势对但绝对值全错
    Unit 单位(如 km/h) 不影响解码,但跨团队协作时是主要误读源

    这张表也是版本校验为何必要的直接解释:上述任何一格被改动,固件都不会报错,只会安静地输出错误数据。

    自动生成:DBC 到 C 代码与 Excel

    仓库配了一个 GitHub Actions 工作流,每当 DBC 变更被推送,就用 python3 -m cantools generate_c_source Autonomous.dbc --output autonomous_c_output 重新生成 C 代码,产物落在 generated/。同一个工作流还会导出 xls,每个 DBC 一个页面、每个 ID 一张表,方便非固件成员审阅。

    把生成环节放进 CI 的意义在于消除「本地改了 DBC、忘了重新生成」这类偏差。手工维护的解码头文件几乎必然与 DBC 漂移,而漂移本身不可见。

    版本校验怎么做

    工作流同时执行 embed_dbc_sha256.py,计算每个 .dbc 的 SHA-256,并把结果作为宏写进生成的头文件:

    #define DATA_T26_DBC_SHA256 0x3ee5339f1dc658c6ULL

    为塞进经典 CAN 单帧最大 8 字节的限制,哈希被截断为 SHA-256 的前 8 字节(64 bit)。文档给出的取舍依据是:两个不同版本碰撞的概率约 2⁻⁶⁴;要累积到约 43 亿个版本才有 50% 的碰撞概率;而加密哈希的前 64 位仍保留完整的雪崩效应,改一个字节就会改变该值。

    方案 承载报文 载荷长度 碰撞概率 适用网络
    截断 64 bit 经典 CAN 2.0,DLC 8 8 字节 约 2⁻⁶⁴,约 43 亿版本才有 50% 概率 CAN 2.0 网络
    完整 256 bit CAN FD 单帧 32 字节 2⁻²⁵⁶ 需 CAN FD(单帧至多 64 字节)

    文档给的实现示例是 STM32 HAL + CAN 2.0:每个 ECU 用专用 ID 0x055(文中示例为每 1 秒一次)发送 8 字节版本帧,其余 ECU 收到后与自身宏比对,不一致即判定为版本落后并告警或拒绝该节点。拷贝时所有 ECU 必须统一使用 Intel 小端字节序,否则比对本身会失效。

    不做校验会发生什么

    文档把风险写得很直白:运行旧版 DBC 的 ECU 会按错误的 bit order、factor、offset 或信号位置去解释报文,可能导致传感器读数错误、发动机与制动等系统误动作、以及难以诊断的安全故障——而这些症状在运行时没有任何可见错误。加上版本常量比对后,版本分歧在第一帧收到时就能被发现。

    在 Teensy 4.1 上的落地形态

    仓库给出的 MCU 示例用 FlexCAN_T4,CAN.setBaudRate(500000),接收队列 RX_SIZE_256、发送队列 TX_SIZE_16。解码时直接包含生成的头文件,例如 autonomous_dv_driving_dynamics_1.h,调用 autonomous_dv_driving_dynamics_1_unpack 读入 speed_actual、speed_target、steering_angle_actual;回包时用 _pack 打包,示例把 speed_target 改为 150 后写回同一 ID。

    这条链路把「DBC 是唯一事实源」落到了实处:固件里不应再出现手写的位运算,所有位定义都来自生成产物,而生成产物与 DBC 哈希绑定。

    常见问题

    为什么 SHA-256 要截断到 64 位?

    为了塞进一帧经典 CAN。CAN 2.0 单帧最多 8 字节,因此只取 SHA-256 的前 8 字节(64 bit)作为宏。文档给出的依据是碰撞概率约 2⁻⁶⁴、需约 43 亿个版本才有 50% 碰撞概率,且前 64 位保留完整雪崩效应,改一个字节即变化。

    DBC 版本不一致在实车上会表现出什么症状?

    不会报错,只会输出错误数据。运行旧版 DBC 的 ECU 会按错误的 bit order、factor、offset 或信号位置解包,导致传感器读数错误、执行系统误动作以及难以诊断的安全故障,运行期没有可见错误提示。因此需要版本心跳在第一帧就比对出来。

    版本心跳必须用 0x055 吗?

    不必。0x055 只是文档示例里为版本心跳专门分配的 ID,任何未被占用的标准 ID 都可以,关键是全网络约定一致、DLC 固定为 8,且所有 ECU 拷贝常量时使用相同的 Intel 小端字节序。

    没有 CAN FD 的网络还能做版本校验吗?

    能。经典 CAN 用截断到 64 bit 的方案即可,把 8 字节常量放进 DLC 8 的帧里周期广播。若网络支持 CAN FD(单帧至多 64 字节),则可以一帧发送完整的 32 字节 SHA-256,无需截断。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/fslart-t26-dbc/README.md(DBC 字段定义、cantools 自动生成、SHA-256 截断与 0x055 心跳示例、Teensy 4.1 用例)

    免责声明

    本文内容整理自公开开源仓库的自述文档,其中的宏值、ID、波特率与概率估算均为该仓库给出的示例或说明,未经独立实测验证。不同工具链、编译器与网络拓扑下的实现细节可能不同,工程落地请以实际测试为准。

    更新记录

    • 2026-10-14 初稿

  • IFS08 uDV 节点:6 报文 DBC 与预编译 libmicroros 子模块

    结论:IFS08 的 uDV 节点是一套跑在 STM32H7 上的 micro-ROS 固件,随仓库发布的 IFS08_FSG26_datalogger.dbc(标注为 IFS08_FSG26_DL_v1.0)只有 6 条报文、45 条信号,用途单一——把行驶动力学、自动驾驶系统状态与安全遥测发给数采。它是一份部分节点地图,不是整车总线定义;真正决定这套固件能否顺利构建的,是那个预编译了 libmicroros.a 的子模块。

    DBC 的规模与覆盖范围

    这份 DBC 位于 docs/IFS08_FSG26_datalogger.dbc,是六报文、45 信号的规模。内容按三类组织:行驶动力学、自动驾驶系统状态、安全遥测,目标接收方是数采设备。

    维度 内容 边界
    规模 6 条报文、45 条信号 仅覆盖发给数采的部分
    版本标识 IFS08_FSG26_DL_v1.0 对应 FSG26 赛季数采配置
    内容分类 行驶动力学、自动驾驶系统状态、安全遥测 不含其他节点间通信
    验证状态 未独立核查哪些报文实际在车上有过流量 需实车核对后使用

    「未核查哪些报文实际在车上有过流量」这一条很关键。DBC 里写了不等于车上发了,尤其在赛季前配置频繁变动的阶段。用这份文件做离线解码是安全的,但拿它推断整车的总线负载就会出错。

    micro-ROS 子模块为什么是构建的关键

    固件依赖一个名为 micro_ros_stm32cubemx_utils 的子模块,固定在 isc-fs 自己的 humble-isc 分支上,而非上游 micro-ROS 组织的主线。这个 fork 相对上游有两处差异,其中第一处直接决定了开发体验。

    差异项 上游 micro-ROS 做法 本 fork 做法 对开发的影响
    预编译库 libmicroros.a 与 microros_include/ 被 gitignore 在工作树中跟踪这两个产物 克隆带 –recurse-submodules 即可构建,无需 Docker 步骤
    自定义接口 仅标准接口 把自定义 ROS 接口包编译进 libmicroros 当前固件只用 std_srvs/std_msgs,用标准重建也可以

    把预编译库纳入版本跟踪,本质上是把「构建环境」变成「仓库内容」。代价是仓库体积,收益是任何装了 ARM 工具链的机器都能直接 make。对一支成员轮换频繁的学生车队来说,这个取舍是合理的——它消除了最常见的「在我机器上能编」类问题。

    当前固件的 ROS 接口面有多窄

    仓库注明固件目前只用到 std_srvs 与 std_msgs,因此即使用上游标准方式重建 libmicroros 也能工作。这一点写在 Core/Src/ros_task.c 里。反过来说,一旦引入自定义消息类型,就必须按 firststeps.md 里记录的流程重建库,而不能沿用现成的预编译产物。

    持续集成:双平台门控

    仓库的 .github/workflows/build.yml 在每次推送到 main、dev、feat/**、fix/** 以及每个 PR 时运行一次固件构建。

    检查项 配置 作用
    工具链 arm-none-eabi-gcc 14.3.Rel1 锁定编译器版本,避免工具链漂移
    运行平台 ubuntu-latest 与 macos-latest 双跑 排除只在单一平台能编的偶发通过
    构建方式 make -j 并行构建,缩短反馈时间
    产物断言 检查 uDV.elf / uDV.bin / uDV.hex 存在 防止「编译无错但没出产物」
    产出 上传为 uDV-firmware 构件 每次提交都有可下载固件

    dev 分支设有保护规则:两个 runner 都必须为绿才允许合并 PR,因此 CI 红了合并按钮直接变灰。这条规则把「CI 通过」从建议变成了硬门槛。

    分支模型:把分支历史挪进 Issue

    这套流程的设计目标很明确:让仓库保持干净,同时不丢失任何一次改动的历史。仓库只有 main 与 dev 两条常驻分支,且都不允许直接提交。

    分支 定位 写入方式
    main 生产分支,只含可刷车的已验证代码 由 dev 经完整固件验证(HIL/台架)后 PR 合入
    dev 集成分支 只接受来自 feature 分支的 PR
    feat/编号-短标题 新功能 从 dev 创建,合并后删除
    fix/编号-短标题 缺陷修复 从 dev 创建,合并后删除

    feature 分支合并后即被删除,历史靠 GitHub Issue 保留:每个分支对应一个带 feat 或 fix 标签的 Issue,标题里写完整分支名。要查当前在做什么就按标签过滤「open」,要查全部历史就过滤「closed」;下一个分支编号取同类最后一个已关闭 Issue 的编号加一。

    三个自动化细节

    推送分支时工作流会自动开 Issue,标题镜像分支名并打上正确标签,还带一个填写说明的模板与创建者名字。如果编号不是预期的下一个,Issue 里会显示警告并提示删除重建。开发者第一次提交并推送后,Issue 的描述区会被该提交信息自动填充一次——若开发者事先手动编辑过则不会被覆盖,且后续提交不再修改。这几处设计把「编号靠自觉」变成了「编号错了会被提示」。

    常见问题

    这份 DBC 能代表 IFS08 整车的 CAN 总线吗?

    不能。它是六报文、45 信号的数采专用定义,只覆盖行驶动力学、自动驾驶系统状态与安全遥测这三类发给数采的内容,属于部分节点地图而非整车总线定义。上游也未独立核查哪些报文实际在车上有过流量。

    为什么克隆时必须带 –recurse-submodules?

    因为 micro-ROS 支持库在子模块里,且该 fork 把预编译的 libmicroros.a 与 microros_include/ 直接跟踪在工作树中。带上这个参数后,任何装了 ARM 工具链的机器都能直接构建,不需要额外的 Docker 步骤。若忘了带,可在仓库内执行 git submodule update –init –recursive 补救。

    CI 为什么要同时在 Ubuntu 和 macOS 上跑?

    为了排除只在单一平台能编译通过的偶发情况。dev 分支保护规则要求两个 runner 都为绿才允许合并 PR,因此任一平台失败都会让合并按钮变灰,把 CI 通过从建议变成硬门槛。

    固件现在用了自定义 ROS 消息类型吗?

    没有。仓库注明固件目前只使用 std_srvs 与 std_msgs,因此用上游标准方式重建 libmicroros 也能工作,相关信息写在 Core/Src/ros_task.c 里。一旦引入自定义消息类型,就必须按 firststeps.md 记录的流程重建库。

    如何知道下一个 feature 分支该用几号?

    取同类最后一个已关闭 Issue 的编号加一。在 GitHub Issues 里按 feat 或 fix 标签过滤并按最新排序,读出最后一个编号即可;feat 与 fix 两个计数器互相独立。编号不对时自动创建的 Issue 会显示警告并提示删除重建。

    延伸阅读

    参考资料

    • 项目 06 —— public-can-dbc-datasets/additional/isc-ifs08-udv/SOURCE.md(DBC 路径、六报文 45 信号规模、版本标识与权利状态)
    • 项目 06 —— public-can-dbc-datasets/additional/isc-ifs08-udv/README.md(子模块 fork 差异、CI 配置、分支模型与自动化流程)

    免责声明

    本文内容来自公开仓库的说明文件,描述的是一支学生方程式车队在特定赛季的开发配置,其工具链版本、分支策略与 CI 配置随时间可能变动。DBC 规模与覆盖范围均依据该仓库说明,未做实车核查。文中方法可直接参考,但具体参数请以实际项目状态为准。

    更新记录

    • 2026-10-14 初稿

  • 风扇控制:30% 点火下限直接夹积分,15 秒永久告警的边界

    结论:风扇链是一条 命令加故障告警,而不是以 RPM 调速的闭环:hardwared 按温区与点火状态算目标百分比,经 deviceState → pandad → 0xb1 → panda 固件 执行,再由 peripheralState 回报约 2 Hz 转速,selfdrived 只做告警。两个反直觉细节必须记住:点火的 30% 下限同时被用作积分项下界,因此首次点火可能直接把积分项夹到 30,而不是缓慢爬升;selfdrived 的代码注释写「5 seconds」,实际判断是严格超过 15.0 秒,且事件只含 ET.PERMANENT,不等于自动降额、软禁用或关机。

    命令链:从复合温度到 8 Hz PWM

    下表把整条链的关键环节与职责列出。要注意主机侧目标与固件侧执行是两段判断,deviceState 里的百分比并不等于最终 PWM 占空。

    环节 源码 实现职责
    FanController.update() fan_controller.py 输入当前复合温度和点火状态,更新 PID 上下限;点火状态变化时重置积分;返回整数百分比。
    PIDController.update() pid.py k_p=0、k_i=0.004、无微分,积分时间步长由传入 rate 固定;输出等于积分项与前馈之和再限幅。
    hardware_thread() hardwared.py 用内存/CPU/GPU/PMIC 的滤波温度生成目标;过热告警时在 PID 结果之后覆盖为 100%,随后发布 deviceState。
    process_peripheral_state() pandad.cc 在 deviceState 更新且未设置 NO_FAN_CONTROL 时下发改变的目标;还在满足 frame 模条件时重发同一目标。
    Panda::set_fan_speed() / get_fan_speed() panda.cc 通过控制请求 0xb1 写目标、0xb2 读 RPM。
    fan_set_power() / fan_tick() fan.h 非零目标先夹到 20–100%,8 Hz tick 更新 PWM、测速滤波及共享供电门。
    send_peripheral_state() pandad.cc 读取 panda 状态与转速,约 2 Hz 发布 peripheralState;外层 valid 来自通信健康。
    selfdrived 风扇检查 selfdrived.py、events.py 已知 panda、RPM 小于 500 且期望大于 50 时,自最后健康处理帧起严格超过 15 秒对应帧数后添加永久 fanMalfunction;本分支不校验反馈新鲜度。

    主机控制不是 RPM 闭环:30% 下限直接夹积分

    风扇控制器在导入模块时按设备类型设定 OFFSET:mici 为 0,其他为 5 °C。因此设定点分别为 75 / 80 °C,前馈从 60 / 65 °C 的 0% 线性增加到 100 / 105 °C 的 100%。FanController 只读温度和点火,不读实际 RPM;点火时输出限在 30–100%,驻车限在 0–30%;点火边沿重置 PID 状态。积分步长固定为 1/rate,hardwared 传 int(1/DT_HW),调度不均匀时仍按名义步长积分。

    探针揭示了上下界实现的一个关键组合:PIDController.update() 的限幅不是单纯「最终输出至少 30%」——点火下限 neg_limit=30 同时被用作积分项的下界。mici 在 80 °C 时前馈为 50%、误差为 5 °C;首次点火更新的名义积分增量仅 0.004 × 0.5 × 5 = 0.01,但被直接夹到 i=30,最终输出为 80%。即使把 rate 从 2 改为 10,此样例首步仍为 i=30;只有把 PID 下限单独改成 0 后,才显出 1/rate 的 0.01 / 0.002 增量。不应把它误读为缓慢积分爬升。

    另一个容易误判的地方是高温覆盖:hardwared 对正常目标计算之后把 deviceState 里的期望值改为 100%,PID 内部积分并不知道这次覆盖。因此告警解除时目标可能回到此前积分/前馈决定的值,而不是从 100% 受控斜坡下降。此外 panda 固件有非零最小 20% 的夹限,主机驻车期望 1–19% 与实际 PWM 目标并不线性一致,期望 0% 才是真正的停机请求。

    量 取值 / 边界 含义
    平台偏移 OFFSET mici 0 °C,其他 5 °C 设定点 75 / 80 °C,前馈 60–100 / 65–105 °C
    点火输出限 30–100% 下限同时作为积分项下界
    驻车输出限 0–30% 期望 0% 才是真停机请求
    固件非零最小 20% 主机期望 1–19% 与实际 PWM 不线性一致
    告警阈值 期望 > 50 且 RPM < 500 配合 15 秒条件触发永久告警

    故障语义:15 秒永久告警不等于停机

    固件 tach 每个 8 Hz tick 根据四个边沿/转计算瞬时 RPM,再以新值 1/4、旧值 3/4 滤波。send_peripheral_state() 即使从 panda 读到 0 RPM,也由通信健康而非风扇健康设置外层 valid;selfdrived 的事件条件检查 pandaType、目标和 RPM,没有直接对 RPM 字段的采样年龄或质量做独立门控。代码注释写「5 seconds」,实际判断是 > 15.0 秒,事件定义只含 ET.PERMANENT——这不等于自动降额、软禁用或关机。整条链是命令加故障告警,不是以 RPM 调速的闭环。

    指令新鲜度:frame 模条件不是看门狗

    pandad 以 100 Hz 主循环运行、约 20 Hz 调用 process_peripheral_state();只有 sm.updated("deviceState") 为真才进入风扇下发分支。目标变化会立即发送,未变化时的 sm.frame % 100 == 0 只是周期性重发尝试,但仍受新 deviceState 到达及其与 frame 余数的相位约束,不是独立、可靠的五秒看门狗。NO_FAN_CONTROL 环境变量只要存在就跳过主机风扇命令;源码也没有把 deviceState 过期、0xb1 写失败或固件实际占空确认转换为主机可观察的执行状态。

    新硬件验收

    自制硬件应把 has_fan、PWM 可用、tach 可用、电源门共享关系和转速量程作为能力声明;指令含序号、期望值与有效期,设备返回接受/执行状态。RPM 观测需带采样时间与有效位,堵转或断线要由可信热安全策略处理,不能靠 UI 永久告警替代降额。台架需测 0→20% 门槛、点火边沿、温度源缺失、100% 强制覆盖、主机或 USB 失联、tach 开短路、供电门与 IR 干扰,以及最坏情况下的 SoC 温升和恢复时间。探针不覆盖 panda 指令丢失、RPM 零值、无风扇板型、NO_FAN_CONTROL、状态消息中断、15 秒事件时序或高温覆盖后的恢复。

    常见问题

    为什么点火后风扇输出会直接从 30% 起跳?

    因为点火下限 neg_limit=30 同时被用作积分项的下界。探针显示,mici 在 80 °C 时名义积分增量只有 0.01,但被直接夹到 i=30,最终输出 80%;即使把 rate 从 2 改为 10,首步仍是 i=30。这不是缓慢积分爬升。

    高温告警触发后,温度下降会平滑降速吗?

    不一定。hardwared 是在正常目标计算之后把 deviceState 期望值覆盖为 100%,PID 内部积分并不知道这次覆盖,因此告警解除时目标可能直接回到此前积分/前馈决定的值,而不是从 100% 做受控斜坡下降。

    停机时把期望设成 1% 到 19% 有用吗?

    不等于停机。panda 固件对非零目标有最小 20% 的夹限,所以主机驻车期望 1–19% 与实际 PWM 目标并不线性一致;期望 0% 才是真正的停机请求。

    出现 fanMalfunction 就代表风扇停了吗?

    不代表。触发条件是已知 panda、期望大于 50 且 RPM 小于 500,并自最后健康处理帧起严格超过 15 秒后添加永久事件;事件只含 ET.PERMANENT,不等于自动降额、软禁用或关机。而且 peripheralState 的外层 valid 来自通信健康而非风扇健康。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/FAN_HOST_PANDA_FEEDBACK_CONTRACT_DEEP_DIVE.md(取用了风扇命令链的组件索引、主机 PID 与 30% 下限夹积分、15 秒永久告警边界、指令新鲜度与新硬件验收要求)。

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

    更新记录

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

  • pandad 主机复位循环:reset/BOOT0 交替与无退避重试

    结论:pandad 的 Python 包装器是一台「复位—恢复—刷写—启动」的循环机:偶数轮常规复位、奇数轮带 BOOT0 的 ROM 恢复,恢复所有 DFU 设备后要求运行态 panda 恰好一块。它的失效模式不是崩溃而是「沉默继续」——GPIO 写异常被吞掉、recover() 的布尔返回值未检查、无设备时无退避地快速重试,而包装器存活并不等于 CAN 已就绪。

    包装器时序:心跳丢失与每轮枚举

    启动时,包装器遍历已发现的 panda,读取内部板 heartbeat_lost;若为真,写持久键并记录日志。此处构造的 Panda() 默认 disable_checks=True,连接会禁用心跳检查、退出省电并重置部分 CAN 配置,因此这一步也不是纯只读探测;异常被记录后仍进入恢复循环。

    每轮 count 偶数执行正常复位,奇数执行带 BOOT0 的 ROM 恢复。之后 PandaDFU.list() 合并 USB/SPI DFU 发现结果,包装器对所有 DFU 序列号顺序执行 recover();只有运行态 Panda.list() 最终恰好一块时才进入应用签名验收和原生进程。设备数大于一会抛 AssertionError,被宽泛异常分支记录后下一轮重试;无设备也直接下一轮。

    入口 实现语义
    reset_internal_panda() 初始化 reset 与 BOOT0;高电平保持 reset、BOOT0 低,10 ms 后释放 reset。
    recover_internal_panda() reset 高、BOOT0 高,10 ms 后释放 reset,再过 10 ms 拉低 BOOT0,使 MCU 进入 ROM 路径。
    gpio_init()、gpio_set() sysfs 写失败仅打印错误,不向上抛异常,也无强制读回。
    get_expected_signature()、flash_panda() 从 H7 应用镜像提预期签名,条件性刷应用固件、恢复 bootstub,最后比对运行态签名。
    pandad.main() 内部板 reset/recover 交替、恢复 DFU、限制运行态 panda 数量、启动并等待 C++ pandad。
    PandaDFU.list()、recover()、Panda.recover() USB/SPI DFU 枚举后逐个恢复;恢复属于有固件写入副作用的维护操作,等待 DFU 超时返回 False。

    复位极性:STM_RST_N 是高电平复位

    源码引脚注释特别说明 STM_RST_N 实际为高电平复位,不能按后缀 _N 推断极性。这一点直接决定了 reset_internal_panda() 与 recover_internal_panda() 的差异:前者保持 BOOT0 低,后者在释放 reset 后再拉低 BOOT0,从而把 MCU 送进 ROM 路径。

    更隐蔽的是 GPIO helper 会吞掉写异常并返回 None,因此包装器不会因为 reset/BOOT0 写失败而直接知道硬件没有进入预期模式。仅仅「写 sysfs 成功」不能证明 MCU 已启动。

    固件验收:bootstub 与签名门

    flash_panda() 在 bootstub 或运行签名不等于本地 H7 镜像签名时调用 flash()。若刷后仍在 bootstub,内部板先走主机 GPIO ROM 恢复,外部板则用 panda 自身 reset 进入 ROM,再调用 panda.recover();最后仍在 bootstub 或签名不匹配就抛错。Panda.recover() 的 False 返回值在此处没有单独检查,最终由 bootstub/签名门决定是否继续。

    清理层面也值得注意:panda.close() 只在正常完成的末尾显式调用;异常路径未见同一函数内的清理块。假依赖探针验证了控制流:运行签名匹配时跳过刷写并显式关闭;bootstub 内部板按 flash()、主机 GPIO 恢复、recover(reset=False) 的顺序调用,外部板则调用 recover(reset=True);recover() 返回 False 但假板卡最终状态和签名合格时仍可完成;最终签名不符时抛错且本函数未显式 close()。这些结果只说明包装器如何使用依赖,不说明真实恢复失败后仍可安全继续。

    无退避循环与 SIGINT 竞态

    签名验收通过后,包装器启动 ./pandad 并以 process.wait() 阻塞。原生子进程结束后回到外层恢复循环,没有固定退避;无设备、多个设备或持续协议错误同样可能快速反复切换内部板模式,具体速率取决于枚举和日志成本。

    包装器接收 SIGINT 时设置退出标记,并在子进程存在时转发 SIGINT;但退出标记只在下一轮循环入口检查。假依赖探针在运行态 Panda.list() 返回前注入 SIGINT,确认当前轮仍会调用 flash_panda() 并启动 ./pandad,且信号到达时子进程尚不存在,因此没有转发给新进程。这是需在真实进程/设备台架上处理和验收的退出竞态。

    失效点 静态表现 为何不能静默
    GPIO 写失败 helper 吞异常返回 None,无强制读回 包装器不知道 MCU 未进入预期模式。
    recover() 返回 False 调用点未检查布尔值 假 DFU 的返回不阻断后续流程。
    无设备/多设备 直接进入下一轮,无固定退避 可能快速反复切换内部板模式。
    SIGINT 退出标记只在下一轮入口检查 新子进程可能未收到转发信号。

    维护权限与插件边界

    主机平台插件可以提供板卡发现与复位请求,但 ROM 切换、固件写入和安全模式配置应由受信任维护服务独占,限制在驻车、供电稳定且目标设备身份已绑定的事务。主机 GPIO 应回报操作是否成功、复位线和 BOOT0 实测电平、设备重枚举代号以及截止时间。外置看门狗和 CAN 收发器默认无输出仍需在自制板上独立实现并验证。台架需分别注入 GPIO 拒写、BOOT0 卡高/卡低、无 panda/多 panda、USB/SPI DFU 同时出现、错误固件签名、刷写中断、原生子进程崩溃和 wrapper SIGINT;不能以包装器 PID 存活作为车辆控制授权。

    常见问题

    为什么 STM_RST_N 这个命名容易误导?

    因为源码引脚注释特别说明 STM_RST_N 实际为高电平复位,不能按后缀 _N 推断极性。复位极性直接决定 reset_internal_panda() 与带 BOOT0 的 recover_internal_panda() 行为差异。

    GPIO 写失败后包装器会知道吗?

    不会。GPIO helper 在 sysfs 写失败时仅打印错误、返回 None,不向上抛异常也无强制读回,因此包装器不会因为 reset/BOOT0 写失败而直接知道硬件没有进入预期模式。

    recover() 返回 False 会阻断流程吗?

    在当前调用点不会。Panda.recover() 的 False 返回值未被单独检查,最终由 bootstub/签名门决定是否继续;假依赖探针也确认返回 False 但假板卡最终状态和签名合格时仍可完成。

    为什么包装器存活不代表 CAN 就绪?

    manager 跟踪的是 Python 包装器的存活,而非独立证明固件签名、安全模式或 CAN 流已就绪。签名通过后包装器只是启动并等待 C++ pandad,因此不能以包装器 PID 存活作为车辆控制授权。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDAD_HOST_RESET_DFU_RECOVERY_LOOP_DEEP_DIVE.md(取用了包装器时序、复位极性、固件验收、无退避循环与 SIGINT 竞态)。
    • 项目 02 —— openpilot-research/PANDA_BOOTSTUB_SIGNATURE_FLASH_BOUNDARY_DEEP_DIVE.md(仅用于确认固件签名与刷写门的边界)。

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

    更新记录

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

  • Athena 日志转发:4096 字符分帧、100 秒等待与 1 小时重发门槛

    结论:Athena 诊断日志转发是在 swaglog 已落盘之后的独立一段:非 PC 会话线程每约 10 秒扫描 swaglog_root,把候选文件整份读入、组装 RPC、按 4096 字符拆成 WebSocket 帧发送,并等待最多约 100 秒的成功响应。三个数字决定重发行为:4096 字符分帧、100 秒确认等待、3600 秒重发门。要注意两个非持久化事实:写入 user.upload 的发送时间只是「已尝试」标记,不是云端交付证明;确认只依据响应 JSON 的 result.success,不核对文件哈希或服务端对象键。而响应里的 id 会被直接拼进日志根路径,log_handler() 未校验它是否为本进程已发送的普通文件名。

    转发链:从轮转文件到 WebSocket 分帧

    本地来源是 SwaglogRotatingFileHandler 按 INFO 以上轮转落盘的文件,转发线程读的是完整文件而非逐条 logMessage。下表是这一段的关键函数与职责。

    函数 / 组件 实现职责
    SwaglogRotatingFileHandler 把 INFO 以上日志轮转落盘;转发线程读取完整文件,而非逐条 logMessage。
    get_logs_to_send_sorted() 枚举 Paths.swaglog_root(),检查 user.upload,未标记或标记早于一小时的文件进入候选;排序后排除候选集末项。
    xattr_cache.getxattr()/setxattr() 按路径与属性名缓存读取;本模块 setxattr() 先清缓存再写属性。来自其他进程的属性改动不主动失效。
    log_handler() 非 PC 会话线程每约 10 秒重新扫描;每轮弹出一个候选,写发送时间、读全文件、组装 RPC,并消费确认队列。
    send_queue_push()、ws_send() 日志请求采用低优先级,队列按 (priority, seq) 排序;发送线程把消息按 4096 字符拆成 WebSocket 帧。入队不等于已写到网络。
    jsonrpc_handler()、is_response() 带 id 且含 result/error 的消息进入模块级 log_recv_queue;无专门的请求来源或连接代号关联。

    三个数字决定重发行为:4096 / 100 / 3600

    第一是分帧长度:ws_send() 把消息按 4096 字符拆成 WebSocket 帧,发送中失败时该消息已经从队列取走,源码没有针对这条发送失败即时重排,重发只能依赖一小时扫描。第二是确认等待:当前文件对应的响应最多等待约 100 次、每次 1 秒。第三是重发门:get_logs_to_send_sorted() 用当前墙钟秒数减去 xattr 里的发送时间,无标记或超过 3600 秒即视作可重发。

    阈值 / 取值 含义 边界
    4096 字符 ws_send() 的 WebSocket 分帧长度 发送中失败后消息已出队,无即时重排
    约 100 秒 等待当前文件成功响应的最长时间(约 100 次 × 1 秒) 期间收到其他 ID 的响应仍会处理其成功标记
    3600 秒 无标记或标记早于一小时才重新列为候选 失败后不会立刻重试
    2147483647 本地「收到成功响应」的哨兵值 正常当前时间下不会进入候选,但不是对象存储的签名回执

    还有一个排序细节:返回值是 sorted(logs)[:-1],排除的是当次可发送候选中排序最大的文件,而不是先在全部目录项中识别活动文件。如果最新活动文件已有哨兵值而不在候选集,另一个较新候选也会被排除——这是源码排序条件的推导,文件名与实际轮转状态的一致性仍需验证。

    「已尝试发送」标记先写,打开可能失败

    log_handler() 从候选尾部取文件,先写当前 Unix 秒数到 user.upload,后打开并完整读取文件、调用 send_queue_push()。这个顺序带来一个明确后果:文件打开失败、进程中断、发送线程尚未发送、网络中断,都可能留下「已尝试」标记;而没有成功确认时要等一小时条件满足才能重新列为候选。探针也复现了这一点——打开日志失败前已经写入了「尝试发送」时间,且没有排入发送队列。

    此外,int.to_bytes(curr_time, 4, ...) 与 open(...).read() 之间没有事务,单个轮转文件会全部进入内存及 JSON 消息,这条路径没有专属的文件大小上限。对读属性时的 ValueError/TypeError 会被当成「未标记」,其他 OSError 交由外层循环记录。

    确认只信响应 JSON,不校验文件身份

    result.success 为真时,才把响应 id 对应的文件标成哨兵值;错误响应、超时或解析异常不会直接清理原发送时间。确认只基于响应 JSON 字段,不核对文件哈希、服务端对象键或持久化状态。由于每个请求的 id 就是文件名,而消费响应时未核对当前已发送的 ID 集合,这里存在可被误触的风险:响应 id 被直接传给 os.path.join(Paths.swaglog_root(), log_entry) 并用于 setxattr(),is_response() 只检查形状。

    探针显示:在没有当前发送任务时,形如 {"id":"../outside","result":{"success":true}} 的假响应仍会使 log_handler() 尝试对拼出的 /logs/../outside 写成功属性。这只证明函数没有已发送 ID 校验,不证明生产 WebSocket 可收到该响应、外部路径可写或服务器已持久化日志。插件化时正确的做法是把关联 ID 映射为宿主持有的文件句柄或受限路径,而不是用远端字符串拼路径。

    硬件与插件化验收

    源文档给出一条清晰的验收方向:诊断转发只能在非实时服务域运行,限制单文件与单次 RPC 大小,并定义网络费用、电量与存储写入预算,不影响控制周期与录制带宽;以本地生成的不可猜任务 ID 绑定文件身份、内容摘要和连接代号,独立记录 pending/sending/accepted/retry_wait,只有服务端可验证确认才推进终态;日志收集与远端转发分别授予能力,默认脱敏设备标识、位置、令牌和用户数据。测试上应在离线服务模拟器中注入发送前崩溃、分帧中断、延迟/重复/伪造响应、墙钟跳变、xattr 不可用、活动文件轮转及超大文件。

    常见问题

    写入 user.upload 的发送时间能证明日志已上传成功吗?

    不能。它只是「已尝试」标记,源码先写这个时间再打开并读取文件,因此文件打开失败、进程中断或网络中断都可能留下该标记。真正的成功标记是收到 result.success 后写入的 2147483647 哨兵值,而它也只是本地的成功响应标记,不是对象存储的签名回执。

    发送失败后日志会立刻重试吗?

    不会立刻。get_logs_to_send_sorted() 用当前墙钟秒数减 xattr 发送时间,无标记或超过 3600 秒即视作可重发;ws_send() 发送中失败时消息已经从队列取走,源码没有即时重排,重发依赖一小时扫描。

    为什么确认只等 100 秒,而不是一直等?

    源码对当前文件对应的响应最多等待约 100 次、每次 1 秒。期间即使收到其他 id 的响应,仍会处理其成功标记,直到收到当前 id 或等待结束;log_recv_queue 位于模块级,重连时不会清空。

    响应里的 id 被直接当路径用,有风险吗?

    有校验缺口。响应 id 会被直接拼进日志根路径并用于 setxattr(),而 is_response() 只检查形状、log_handler() 未验证这个 ID 是本进程已发送的普通文件名。探针证明函数没有已发送 ID 校验,但不证明生产环境可达或被利用;插件化时应改用受限路径或文件句柄映射。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ATHENA_DIAGNOSTIC_LOG_FORWARD_ACK_DEEP_DIVE.md(取用了转发函数的组件索引、4096/100/3600 三个阈值、发送标记先写、确认字段与路径拼接风险、插件化验收要求)。

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

    更新记录

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

  • pandad 硬件测试:DFU 恢复、SPI 注错与 loopback 边界

    结论:pandad 的三个设备测试给出三种不同强度的证据:恢复测试只证明设备可见性,CAN loopback 证明期望集合被逐条消耗,SPI 注错证明服务存活与粗时序。它们都会复位/刷写 panda、切换 allOutput safety 并发送随机 CAN,绝不可在连接车辆的环境运行。源文档明确,三者都不能证明永久 NACK、非法长度、短健康包、热插拔、多 Panda、心跳失效后的 MCU 静默或实际车型 RX/TX safety 钩子。本文为静态核对,未启动测试、未刷写、未发送 CAN、未连接任何设备。

    恢复测试:只证明设备可被看到

    TestPandad 要求 comma 硬件;每个测试的 setUp() 先执行测试基类的硬件初始化,再停止已有 pandad,如果当前枚举不到 Panda 就尝试启动包装器恢复。核心 _run_test() 启动 pandad,在 30 秒内订阅 pandaStates,看到至少一块且首块 pandaType != unknown 即停止并返回耗时。

    源文档强调它没有断言 CAN 能收发、固件签名值、心跳健康或目标安全模式。因此「看到 pandaStates」不等于整条安全链通过。

    场景 实际操作 静态覆盖范围
    test_in_dfu() 主机 GPIO 进入 ROM DFU,再让包装器恢复 验证可恢复到非 unknown 状态,不测刷写中断
    test_in_bootstub() 经 Panda API 进入 bootstub,确认标志,再让包装器恢复 验证 bootstub 起点的正常恢复
    test_in_reset() 操作 STM_RST_N,确认暂时枚举不到 Panda,再启动包装器 属于物理复位线测试,不是纯软件 mock
    test_release_to_devel_bootstub() _flash_bootstub() 选择 MCU 配套 bootstub 文件并通过 PandaDFU.program_bootstub() 写入 有真实固件修改;名称里的 release/devel 不能替代证书/签名核验
    test_recover_from_bad_bootstub() 进入 DFU 后向 bootstub 起始地址写入 100 个零字节,复位并确认普通/DFU 枚举为空,再靠包装器恢复 主动制造启动损坏,失败时可能需要外部救援

    公共台架夹具与 allOutput 的放开范围

    setup_pandad() 有三次重试:清除当前测试 Prefix 下的 Params,反复发布 deviceState.started=False 等待非 unknown pandaStates,再写 FirmwareQueryDone、ControlsReady 和仅含 allOutput 的 CarParams.safetyConfigs,发布 started=True。

    两处边界需要注意:测试基类的 OpenpilotPrefix 隔离的是软件路径,不隔离接在 panda 上的物理 CAN;allOutput 的 tx_hook 无帧内容限制,通用发送门也对该模式放开白名单,这不是车型量产 safety 策略。台架必须与车辆 CAN 物理断开,不能仅依靠软件设置「loopback」。

    loopback 与 SPI 注错的断言边界

    send_random_can_messages() 对 0~2 号总线生成随机 29 位以内地址和 1~8 字节载荷,用 sendcan 发布;没有 CAN FD 长帧。TestBoarddLoopback.test_loopback() 运行 200 轮,每轮除原总线集合外还期待 src+128 的发送回执集合;收到消息就从集合移除,最后要求所有期望集合清空。源文档指出这比只看「有 CAN 流量」强,但不测负载极限、重复帧保序、超时恢复或车辆控制的安全限幅;每轮接收循环最多 500 次 wait_for_one,没有外层总时间上限。

    TestBoarddSpi.setup_class() 设置 STARTED=1、SPI_ERR_PROB=0.001,默认还开启 BOARDD_LOOPBACK;SPI 层在配置概率下扰动传输长度、TX/RX 缓冲,然后继续真实 ioctl,故它是传输层随机注错而非确定性故障脚本。test_spi_corruption() 在默认约 20 秒循环中发送随机 CAN,检查状态消息有效与设备类型/点火/电压/风扇范围,并统计三种主题的平均/最大消息间隔和数量。

    测试 断言边界 不能证明
    test_loopback() 200 轮,期望集合(含 src+128 回执)全部清空 负载极限、重复帧保序、超时恢复、安全限幅
    test_spi_corruption() CAN Event 即使 valid=False 仍进入测试;只断言收到且 src<=4 的(地址,载荷)曾在对应发送集合中;最后只要求 total_recv_count > 20(集合移除被注释) 接收数等于发送数、每条只到达一次或全部发送帧均回传
    JUNGLE_SPAM 模式 校验地址、源总线和 0xff 载荷模式 不产生本测试自己的发送集合

    三种证据各自不证明什么

    源文档收束得很清楚:恢复测试的设备可见性、loopback 的期望集合消耗、SPI 注错时的服务存活/粗时序,三者都不能证明永久 NACK、非法长度、短健康包、热插拔、多 Panda、心跳失效后的 MCU 静默或实际车型 RX/TX safety 钩子。

    硬件插件验证的推荐顺序是:先用离线消息/协议测试,再在断开车辆的台架做可重复错误矩阵与固件恢复,最后另由独立 safety 层和受监督实车流程验收。已静态核对三个测试文件、公共 helper、SPI 注错点与 allOutput 安全钩子;未运行设备测试或执行任何刷写/CAN 操作。

    源文档也把恢复包装器列为待验证对象:真实包装器的复位、签名和重试边界需要另做专题核对,本节列出的是测试证据。分支和计数结论是源码可见的断言范围,目标硬件时序和异常恢复结果仍需专用台架验证;全项目其他组件仍待逐项审查。

    常见问题

    pandad 恢复测试通过是否代表固件与安全链正常?

    不代表。源文档明确 _run_test() 只要求在 30 秒内看到至少一块非 unknown 的 pandaStates,没有断言 CAN 能收发、固件签名值、心跳健康或目标安全模式。

    为什么 SPI 注错测试通过不能说明接收完整?

    因为它的断言很窄。源文档指出,test_spi_corruption() 只断言收到且 src<=4 的帧曾在对应发送集合中,集合移除被注释,最后只要求 total_recv_count > 20,并不要求接收数等于发送数或全部发送帧均回传。

    台架上把 safety 设成 allOutput 就安全吗?

    不安全。allOutput 的 tx_hook 无帧内容限制,通用发送门也放开白名单,它不是车型量产 safety 策略;且 OpenpilotPrefix 只隔离软件路径,不隔离物理 CAN,台架必须与车辆 CAN 物理断开。

    这些测试能在连接车辆的环境下跑吗?

    绝对不能。源文档在开头即警示:它们会复位/刷写 panda、切换 allOutput 并发送随机 CAN,绝不可在连接车辆的环境运行。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDAD_HARDWARE_TEST_DFU_SPI_LOOPBACK_SAFETY_BOUNDARY_DEEP_DIVE.md(取用了恢复测试场景表、公共台架夹具、loopback 与 SPI 注错的断言范围、三类证据边界)。

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

    更新记录

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

  • 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

  • Toyota SecOC 边界缺口:密钥在内存、同步不匹配只记日志、计数先于确认

    结论:Toyota 的 SecOC 链路在源码里是三段彼此独立的机制:密钥装载、同步计数器校验与 panda 安全钩子。它们各自都有明确的边界缺口——密钥只以 16 字节形式留在内存中,Params 的 DONT_LOG 标记并不等于加密;同步 MAC 不匹配时源码只写一条 carlog.error(),不停止组帧;三个消息计数在调用 add_mac() 后、返回候选帧前就递增,不等待 sendcan、SPI、panda TX 或 ECU 的接受回执。发布版对这类车型设 dashcamOnly,MCU 侧也不持有车辆认证密钥。以下为固定快照的静态阅读,未获取车辆密钥、未连接 ECU、未发送 CAN。

    能力门与密钥生命周期

    Toyota SecOC 平台把认证要求写进 CarParams,并把安全参数传给 panda。ToyotaSecOCPlatformConfig.init() 设置 TSS2/NO_DSU/SECOC 标志和专用 DBC;CarInterface._get_params() 置 secOcRequired 与 Toyota safety 参数,发布版把这类车型设成 dashcamOnly。Car.__init__() 从缓存文件或 Params 读取键,做长度与十六进制解析后交给 CarState/CarController;其中的 secOcKeyAvailable 仅是本地装载标志。

    具体地,card 可以从设备本地缓存文件中的 SecOCKey 导入 32 个字符到 Params,再用 bytes.fromhex() 解成 16 字节。正确长度只能证明数据格式,secOcKeyAvailable=True 并不证明它匹配车辆;若 Params 里是非十六进制文本,bytes.fromhex() 在该分支没有局部异常处理,初始化可能直接失败。密钥在 Params 里注册为 DONT_LOG,这只约束常规日志取值,不是存储加密、进程隔离或密钥来源认证。

    层 载体 能证明什么 / 不能证明什么
    能力门 CarParams.secOcRequired + dashcamOnly 声明该车型需要 SecOC;发布版因此进入被动模式,安全配置被改为 NO_OUTPUT。
    密钥装载 设备本地缓存文件中的 SecOCKey → Params → 16 字节内存 长度与格式可校验;不证明匹配车辆,也不是加密存储。
    日志标记 SecOCKey 注册为 DONT_LOG 只约束常规日志取值,不等于进程隔离或来源认证。

    同步与认证:MAC 不匹配只记日志

    SECOC_SYNCHRONIZATION 在 DBC 中是 8 字节消息,含 Trip/Reset 计数和 Authenticator。CarState 每轮复制解析器缓存的当前值,未在此处对同步 MAC 或样本年龄作校验。控制器看到 Reset 与上次不同时,把 LKA、LTA2、ACC2 三个计数清零,再计算期望的同步 MAC。

    关键边界在于:不匹配时只有 carlog.error(),没有在该分支停止组帧或撤销控制资格。另有一个初始化细节——首次本地 Reset 初值为零,若同步消息也为零,本轮不会触发这次校验。解析器总线健康属于另一层,控制器在这里没有逐同步帧的年龄门。

    签名输出:计数先于链路确认递增

    add_mac() 用地址、控制载荷前四字节以及 Trip/Reset/消息计数构造 CMAC 输入,截断后与两组低位标记一起放进 8 字节 CAN 数据;不同命令流各有递增计数。CarController.update() 在 Reset 变化时归零三种本地计数,并分别签 LKA、LTA2、ACC2。

    递增时机是核心问题:计数在调用 add_mac() 之后、返回候选 CAN 帧之前增加,不等待 sendcan、SPI、panda TX hook 或 ECU 的接受回执。因此应用层的计数是「本地尝试序号」,不是「车辆已执行序号」;断流、重启、Reset 切换及报文丢失时的同步策略需要另行验证。

    环节 源码可见做法 缺口
    同步帧解析 CarState.update() 复制解析器缓存 未在此处校验 MAC 或样本年龄。
    同步 MAC 校验 Reset 变化时清零三计数并算期望值 不匹配只 carlog.error(),不停组帧。
    Reset 初值 首次本地 Reset 为 0 同步消息也为 0 时不触发本轮校验。
    命令计数 add_mac() 后立即递增 不等待 sendcan/SPI/panda/ECU 回执。

    panda safety 与覆盖范围

    Toyota safety 的 SecOC TX 白名单包含相应的 LKA/LTA2/ACC2 报文,继续约束消息地址、总线、长度、力矩/角度及纵向输出;例如 SecOC LTA2 的非零执行字段在当前钩子中被禁止。toyota_secoc 参数只有在 ALLOW_DEBUG 编译时才从安全参数开启,不能假定所有固件构建都允许相同模式。

    更重要的是责任边界:MCU 侧没有车辆 SecOC 密钥,也不在此代码中校验 AES-CMAC;认证通过与否由车辆 ECU 决定,安全钩子只负责本地发送约束。相邻测试里,通用车型 TX 回放会跳过 SecOC(需要真实键),panda safety 单测会检查 SecOC 地址与限幅,但不验证 ECU 的密码学接受——白名单与限幅通过并不证明车辆会接受签名帧。

    自制硬件与插件重构

    把车辆认证能力放在受信任、受限的签名服务中,而不是通用车型插件或日志进程:签名服务只对已授权的车型、命令与当前有效同步状态处理候选帧,记录键版本而不泄露键值;CAN 执行仍需独立的 ECU 安全限幅网关。输入契约至少应包含同步帧来源、解析器有效位、接收时间、Trip/Reset 代号和认证结果;MAC 失败、缺帧、过期或密钥不匹配都应进入明确的不可执行状态,不能只写日志。

    离线验收要覆盖:未配置键、非法十六进制键、错误键、同步 MAC 不匹配、Reset 为零或跳变、丢帧、重复或过期同步,以及三条命令流计数的重启行为;再在无车台架验证安全钩子与总线发送结果。在没有车辆密钥或 ECU 反馈时,只能确认本地编码与限幅,不应宣称 SecOC 车型已受支持或已可安全上路。

    常见问题

    SecOCKey 注册为 DONT_LOG 就等于密钥被加密了吗?

    不是。DONT_LOG 只约束常规日志取值,它不代表存储加密、进程隔离或密钥来源认证。密钥最终以 16 字节形式留在 CarState/CarController 内存中,正确长度也只证明数据格式,不证明匹配车辆。

    同步 MAC 不匹配会阻止发送控制帧吗?

    不会。源码在不匹配分支里只有一条 carlog.error(),没有停止组帧或撤销控制资格。此外首次本地 Reset 初值为零,若同步消息也为零,本轮根本不会触发这次校验。

    LKA/LTA2/ACC2 的计数是「车辆已执行」的序号吗?

    不是。计数在调用 add_mac() 后、返回候选 CAN 帧前就递增,不等待 sendcan、SPI、panda TX hook 或 ECU 回执,因此它只是本地尝试序号,不是车辆已执行的序号。

    panda safety 通过就能说明 SecOC 车型可用吗?

    不能。panda 只约束消息地址、总线、长度与力矩/角度/纵向限幅;MCU 不持有车辆密钥、也不校验 AES-CMAC。车型回放测试因无密钥会跳过 SecOC TX 用例,白名单与限幅通过并不证明车辆会接受签名帧。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_SECOC_KEY_FRESHNESS_SAFETY_BOUNDARY_DEEP_DIVE.md(取用了能力门与密钥生命周期、同步与认证边界、add_mac() 计数时机、panda safety 覆盖范围与插件重构建议)。

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

    更新记录

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

  • opendbc 车型校验算法族:同名 CRC8 不可替换,RX/TX 共用函数

    结论:opendbc 的车型校验不是「一种 CRC8 走天下」,而是按品牌/平台硬绑定的一族函数:Honda 用半字节补码、Toyota 把帧长本身算进和、Chrysler 用 CRC8 J1850(初值与末异或均为 0xFF)、Hyundai CAN FD 用 CRC16 XMODEM、VW MQB/MEB 用 CRC8 H2F 再追加地址常量。同名「CRC8」只代表多项式查找表相同,地址、帧长、字节顺序与最终 XOR 各不相同,不构成车型间可替换的插件接口。更关键的是,RX 校验与 TX 生成共用同一个函数,任何「只改发送端」的假设都必须重新核对。

    每个校验函数都写死了覆盖字节与校验位位置

    在固定源码快照里,get_checksum_state() 按 DBC 名称与信号名把每帧绑定到一个具体函数。所谓「算法」其实由四件事共同决定:哪些字节进入计算、校验字段在哪个字节或半字节、初始值与末异或是什么、帧长是否参与。源文档明确指出,帧长度和字段位置本身就是协议的一部分,不能当作可省略的装载细节。

    下面的函数目录来自源文档的逐函数静态核对。注意「参与计算的内容」一列——同一种「CRC8」在 Chrysler、VW、body 三处覆盖的字节范围完全不同。

    家族 / 函数 实际参与计算的内容 重要前提
    honda_checksum() 地址各半字节、每个载荷字节的高低半字节;末字节仅取高半字节,结果为 8 - sum,扩展 ID 再加 3,截 4 位。 假定校验位在最后字节低半字节;扩展地址判据是 address > 0x7FF。
    toyota_checksum() len(d)、地址各字节和除最后一字节外的载荷字节,相加截 8 位。 假定校验在末字节,帧长本身参与计算。
    subaru_checksum() 地址各字节与除第一字节外的载荷,相加截 8 位。 Global 版本校验位默认在首字节;和 pre-global 手动辅助函数不是同一算法。
    chrysler_checksum() 对末字节以外数据查 CRC8 J1850 表,初值和末异或均为 0xFF。 不把地址计入此函数;checksum 按末字节处理。
    fca_giorgio_checksum() 同表、初值 0,末异或按地址 0xDE/0x106/0x122/其他 分别为 0x10/0xF6/0xF1/0x0A。 未识别地址落入通用 0x0A 分支;不能把四组样本扩大为所有 FCA 报文。
    hkg_can_fd_checksum() 从第 2 字节到末尾做 CRC16 XMODEM,再加入地址低、高字节;仅 8/16/24/32 字节追加特定末异或常量。 前两字节视作校验字段;其它长度不加表中常量,但函数自身不会拒绝。
    volkswagen_mqb_meb_checksum() 初值 0xFF,对第 1 字节起载荷做 CRC8 H2F;已知地址再按第二字节低半字节选 16 项常量追加一步,最终异或 0xFF。 首字节为校验位;未在常量表中的地址跳过附加常量步骤。
    tesla_checksum() 地址低、高字节加上除 checksum 字节外的所有载荷,截 8 位。 校验位置由 sig.start_bit 决定,不限固定末字节。
    body_checksum() 从倒数第 2 字节向首字节反向迭代做 CRC8 BODY,初值 0xFF。 仅最后一字节不进入计算,函数不检查其字段含义。
    byd_checksum() 前七字节之和按位取反再截 8 位。 固定覆盖前七字节;不是通用「跳过任意校验字段」的算法。
    psa_checksum() 地址选择 4 位初值,先在传入 bytearray 中清除校验半字节,再把全帧高低半字节求和,从初值中减去并截 4 位。 函数就地修改传入缓冲;RX 解析器传副本,TX packer 传当前构帧缓冲。

    CRC8H2F、CRC8J1850、CRC8BODY、CRC16_XMODEM 只是按多项式生成的查找表。查找表可以共享,但函数外层的地址、帧长、字节顺序和最终 XOR 无法共享,这正是「同名不同义」的根源。

    RX 校验与 TX 生成共用同一个函数

    源文档反复强调一个契约:RX 验证和 TX 生成走的是同一份实现,而不是「接收一套、发送另一套」。这带来两个直接后果。第一,校验位的位置往往由信号元数据决定,例如 tesla_checksum() 的校验位置由 sig.start_bit 决定,并非固定在末字节。第二,某些函数带有副作用,例如 psa_checksum() 会就地清除并回写传入缓冲里的校验半字节。

    副作用的差别决定了移植风险:接收解析器传入的是 bytearray(dat) 副本,因此就地修改不影响原始帧;而发送打包器传入的是当前工作缓冲。把这类函数当成无副作用纯函数重用,会直接改变构帧结果。同时,VW MLB 的 XOR 变体以 sig.start_bit // 8 定位整字节,说明「校验在哪个字节」不是全局常量,而是逐信号给定的。

    按「接收帧已有校验字节」选择变体,而不是按算法优劣

    VW MEB 提供了一个容易被误读的变体函数 volkswagen_meb_alt_crc_checksum():它对备用表内六个地址先按指定长度算另一组 CRC,只有候选值等于输入 d[0] 时才返回,否则退回标准 MQB/MEB CRC。这个语义是按接收帧里已经带着的校验字节来「认领」变体,并不代表发送端会主动优先采用备用值。

    源文档用三个隔离探针把边界固定下来,结果如下表。它们都使用原版函数与真实查找表,但只在纯函数层面运行,未加载完整车型 DBC、未发 CAN、未测 ECU。

    探针 输入 观察结果 结论边界
    MEB 双 CRC 选择 地址 0x0DB、64 字节零缓冲 备用长度 42,备用结果 0xEB、标准结果 0xED;d[0]=0 时选 0xED,改成 0xEB 时才选 0xEB CANPacker.pack() 默认从零缓冲开始,故不能凭接收端可识别备用 CRC 就推断发送端会按备用算法生成
    Hyundai 长度分支 长度 8/12/16/24/32 的全零缓冲 各长度均给出结果;12 字节路径不会被拒绝,只是不使用四种已列长度常量 仅验证函数分支,不代表 12 字节是该平台合法帧长
    PSA 就地修改 0x452、start_bit=0、输入 AB CD 返回校验半字节 1,同时把首字节原地改为 A0 移植时不能把该函数当无副作用纯函数

    把这几条合起来看:一个「接收端能认」的校验并不自动意味着「发送端会这么写」。当前实际发送函数是否涉及 MEB 那六个地址,需要按车型另做核对,本篇探针不说明已有车型发生过错误发送。

    「跳过校验字节」不是通用能力

    Subaru 有两个函数常被混为一谈:subaru_checksum() 是 Global 版本,校验位默认在首字节;subaru_preglobal_checksum() 则先用 packer 生成字节,再对除 checksum_byte 指定字节(默认第 7 字节)外的字节求和。后者由 pre-global 构帧函数显式填写 Checksum,不由 DBC.get_checksum_state() 自动绑定。同理,byd_checksum() 固定覆盖前七字节,也是一种特定布局,而不是「跳过任意校验字段」的通用规则。

    这些差异说明:校验算法必须和 DBC 名称、报文地址、DLC、校验字段位位置、覆盖字节范围、counter 位置一起版本化。只声明「用了 CRC8」在车型之间没有任何可迁移性。

    给协议插件的验收底线

    新车型的协议插件应把下列字段作为一个整体来管理和验收,缺一项都可能造成接收能过、发送写错,或反之。

    必须版本化的字段 为什么不能省
    DBC 名称、报文地址、DLC 决定帧长与字段位置,帧长本身参与部分算法(如 Toyota)。
    校验字段位位置、覆盖字节范围 Tesla 由 sig.start_bit 决定、Honda 在低半字节,不能固定假定末字节。
    counter 位置 与 checksum 同级参与健康判定,不能只看数值。
    CRC 多项式 / 初值 / 末异或 Chrysler 与 FCA Giorgio 同表不同初值与末异或。0x10/0xF6/0xF1/0x0A
    长度变体与 RX/TX 选择规则 MEB 备用 CRC 只在匹配接收字节时启用;Hyundai 只在 8/16/24/32 追加常量。

    测试层面,应在参考载荷上直接比对计算值与输出字节,并确认解析器确实接受本帧;还要覆盖坏 checksum、截短/超长帧、counter 回绕与未知地址。需要提醒的是,checksum 只检测某类传输错误,不提供发送身份认证,也不构成执行器安全约束——这些仍由可信的车型逻辑、panda safety 和物理台架验收承担。

    常见问题

    为什么同为「CRC8」,不同车型的校验函数不能互相替换?

    因为查找表相同不代表算法相同。Chrysler 用 CRC8 J1850 且初值与末异或均为 0xFF,VW MQB/MEB 用 CRC8 H2F、初值 0xFF 并追加地址常量,body 则从倒数第二字节反向迭代。地址、帧长、字节顺序和最终 XOR 各不相同,源文档明确指出相同「CRC8」名称不构成车型间可替换的插件接口。

    接收端能校验通过的备用 CRC,发送端也会照此生成吗?

    不会自动如此。MEB 备用函数只有候选值等于输入 d[0] 时才返回,否则退回标准 CRC,这是「按接收帧已有校验字节选择变体」的语义;而 CANPacker.pack() 默认从零缓冲开始,不会凭接收可识别就优先采用备用值。当前发送函数是否涉及那六个地址需按车型另核。

    PSA 校验函数可以直接当纯函数复用吗?

    不可以。源文档探针显示,psa_checksum() 对 0x452、start_bit=0、输入 AB CD 返回校验半字节 1 的同时,把首字节原地改成了 A0。接收解析器传的是副本,发送打包器传的是原始工作缓冲,移植时必须区分。

    checksum 通过是否等于报文安全可信?

    不等于。checksum 只检测某类传输错误,不提供发送身份认证或执行器安全约束。源文档明确这些仍由可信的车型逻辑、panda safety 和物理台架验收承担,并且本专题没有实板证据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/OPENDBC_CHECKSUM_ALGORITHM_FAMILY_RX_TX_CONTRACT_DEEP_DIVE.md(取用了校验函数目录、RX/TX 共用函数的语义、三个隔离探针结果与插件验收字段清单)。

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

    更新记录

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