结论:panda 的故障、寄存器巡检与制造身份是一条观测链,不是执行许可链。故障路径只把「记录过的 fault」送到 cereal.PandaState;「记录了 fault」不等于执行器已被独立切断。并且源码里至少存在四处静态契约不一致:寄存器哈希上界越界 1 项、故障位图与状态不是同一真值、位 27 的故障无法被正确表达、TIM1 更新中断的向量符号与 C 包装器不匹配。以下基于源码快照 c8fb906,未做实机故障注入、内存安全与制造写入验证。
故障路径:从位图到 PandaState
故障聚合由 fault_occurred() 与 fault_recovered() 完成,维护位图、首次打印与状态级别;本版本 PERMANENT_FAULTS=0。get_health_pkt() 把 fault_status 与 32 位 faults 作为两个独立字段返回,fill_panda_state() 再把健康字段映射进 PandaState,故障位转列表,枚举定义在 log.capnp。整条链的职责是观测:实际 CAN 许可仍须另追 safety hook、心跳与主机执行门。
| 组件 | 关键实现 | 作用与边界 |
|---|---|---|
| 临界区 | ENTER/EXIT_CRITICAL、enable/disable_interrupts |
嵌套深度与全局使能状态共同决定退出时是否恢复 IRQ;这不是跨核锁。 |
| IRQ 统计 | handle_interrupt()、interrupt_timer_handler() |
中断调用计数与忙闲时间;每秒换算负载,超出注册上限记故障。 |
| 寄存器跟踪 | register_set()、check_registers() |
写寄存器时记录期望值/掩码,主固件每秒检查漂移;仅覆盖经此 API 登记且掩码不含硬件自变位的寄存器。 |
| 故障聚合 | fault_occurred()/fault_recovered() |
位图聚合、首次打印与状态级别;本版本 PERMANENT_FAULTS=0。 |
| 身份读取 | get_provision_chunk()、控制请求 0xd0 |
读固定地址的 32 字节制造块或 16 字节序列号;全 0xff 时返回内置 unprovisioned 占位值。 |
中断速率与负载的真实语义
临界区实现值得单说:ENTER_CRITICAL() 直接执行 __disable_irq() 并把全局 uint8_t global_critical_depth 加一,EXIT_CRITICAL() 减一,仅在深度回到零且 interrupts_enabled 为真时才 __enable_irq()。这是单核全局 IRQ 屏蔽与嵌套约定,不是自动保存/恢复进入时的 PRIMASK,也不是按外设分别关中断;若绕过包装器直接改 PRIMASK,或进入/退出不配对,8 位深度可下溢或回绕;源码没有运行期断言阻止这两种误用。enter_stop_mode() 刻意使用原始 __disable_irq(),清理 NVIC、只留唤醒源,WFI 返回后立即请求系统复位——这段代码不能当作可返回的通用临界区。
本版本为 IRQ 分配 163 个槽位,init_interrupts(true) 先把每个外设槽设为「未使用」处理器,再启动 TIM6 每秒统计中断。每次中断先增 call_counter 再调用处理器,若当前秒内次数大于登记上限就 fault_occurred();interrupt_timer_handler() 每秒把计数复制到 call_rate 并清零。这里没有对速率故障调用 fault_recovered(),所以即使下一秒速率恢复,故障位仍可能保留,不能把这一位解释为「当前这一秒仍超限」。
| 字段/入口 | 取值与换算 | 不能代表什么 |
|---|---|---|
call_rate / EP0 0xc4 |
上一个完整统计窗的 IRQ 调用次数,Python 侧再解码为整数 | 不是当前秒的实时值 |
interrupt_load |
busy_time/(busy_time+idle_time),量化到 0..255,主机除以 255 |
是 ISR 占用比例,不是 MCU 总 CPU 使用率 |
| 速率故障位 | 超限即置位,不做自动恢复 | 不等于设备已停止 CAN 或复位 |
普通主循环或非 IRQ 的阻塞时间会计入「闲」,因此不能仅靠 interruptLoad 证明控制任务有足够计算余量。无效 IRQ 号在固件侧没有 4 字节响应。
寄存器巡检与哈希上界
register_set() 在写寄存器时记录期望值与掩码,主固件每秒调用 check_registers() 检查漂移。它的覆盖范围有明确前提:只包含经此 API 登记、且掩码不含硬件自变位的寄存器。更值得记录的是数组容量与索引的错位:容量是 0x3ff 个元素,合法索引为 0..0x3fe;但 hash_addr() 用 & 0x3ff 计算,冲突检查又先以该值索引。若某个寄存器地址或再探测值哈希到 0x3ff,就会越界,且 check_registers() 不会扫描该槽。触发地址及硬件后果需要针对实际地址清单验证。
四处静态契约差异
下面这些差异都属于「代码能编译、单看一侧也自洽,但两侧对不上」的类型:
| 差异 | 源码事实 | 含义 |
|---|---|---|
| 寄存器哈希上界 | 容量 0x3ff,但 & 0x3ff 可算出 0x3ff |
该槽越界且不被巡检 |
| 状态与位图 | fault_recovered() 仅清位图,不重算 fault_status |
位图可为 0 而状态仍为 TEMPORARY |
| 故障枚举范围 | 固件位 27 有定义,但 cereal 与 pandad 止于位 26 | 该故障不能被正确表达 |
| TIM1 更新向量 | 汇编指向 TIM1_UP_IRQHandler,C 包装器却是 TIM1_UP_TIM10_IRQHandler |
当前 NVIC 该通道被禁用,未触发 |
关于 TIM1:汇编向量槽与弱别名把它默认映射到死循环,而 C 包装器定义的是另一个符号名。静态比对 140 个非保留外设向量与 140 个 C 包装器后,除芯片头文件明确支持的 COMP_IRQHandler 别名外,只有这一组不匹配。当前时钟输出初始化虽置了 TIM1 更新中断使能位,却明确 NVIC_DisableIRQ(TIM1_UP_TIM10_IRQn),因此不能表述为已触发的运行期故障;若后续启用该通道,须先校正向量符号并在无负载台架验证。另外 body 的诊断能力不同:它用 tick 与中断计数,但没有普通 panda tick 中的寄存器巡检,其 EP0 请求分派也不是普通 get_health_pkt() 契约。
制造身份与插件界线
制造身份应被当作受控的只读能力:MCU UID、OTP 序列号与 provision chunk 分开建模;未知或全 0xff 必须显式标记「未灌录」,不能用测试占位值生成生产身份或信任根。诊断插件可以订阅、分类和呈现故障,但不得单凭 fault_status 清除控制禁用——安全控制权属于独立固件与经验证的主机门控。台架验证应覆盖寄存器哈希边界、故障出现/恢复的双字段序列、位 27 的序列化,以及 IRQ 过载下健康包的发布时延,且都在无车辆执行器的隔离环境完成。
常见问题
panda 记录了 fault,执行器就已被切断了吗?
没有。故障路径只是把故障位与状态送到 PandaState,属于观测链;实际 CAN 许可由 safety hook、心跳与主机执行门决定。
速率故障位会自己恢复吗?
不会自动恢复。超限时置位,但代码没有对速率故障调用 fault_recovered(),所以即使下一秒速率恢复,故障位仍可能保留,不能当作当前秒的实时读数。
interruptLoad 能代表 MCU 的总 CPU 占用吗?
不能。它只统计 ISR 的忙闲比例,普通主循环或非 IRQ 的阻塞时间会计入「闲」。因此它不能单独证明控制任务有足够计算余量。
为什么故障位图是 0,状态却仍显示临时故障?
因为 fault_recovered() 只清位图,不重算 fault_status。消费端应把位图与状态分别解释,不能只读状态来推断当前故障。
延伸阅读
- CAN 报文解析的「健康位」在说什么:校验、计数器与总线空窗 —— 同为「健康/故障标志不等于数据可用」的语义边界
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 为什么观测标志不能替代执行门
- panda 板卡与固件生态 —— 本文涉及的 IRQ 槽、寄存器巡检与制造数据的硬件载体
参考资料
- 项目 02 ——
openpilot-research/PANDA_FAULT_REGISTER_PROVISION_CONTRACT_DEEP_DIVE.md(panda 故障、寄存器巡检与制造身份契约,含源码行号索引) - 项目 02 ——
openpilot-research/PANDA_BOOTSTUB_SIGNATURE_FLASH_BOUNDARY_DEEP_DIVE.md(同目录 panda 启动与维护面专题,供制造身份边界的延伸阅读)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复