标签: DFU 恢复

  • 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

  • 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