标签: panda

  • tinygrad AMD PSP:固件加载成功不等于 GPU 能跑模型

    结论:AMD GPU 的固件链里,「固件加载成功」「GPU 队列可工作」「模型输出有效」是三个不同状态:PSP 把经过选择的镜像交给 sOS 后,后续 SMU/GFX/SDMA 才依次使用。固定 SHA-256 只保证字节与清单一致,不等于签名、可信发布者或运行时兼容性证明;而 _ring_submit() 的写指针未见软件取模,属待验证项而非已定性缺陷。

    本文依据固定归档的 AMFirmware 与 AM_PSP 做静态源码与纯内存探针分析。这是 AMD GPU 计算设备的启动链,不是车辆控制或 panda 安全固件链;没有下载固件、访问 GPU/车辆、发送 CAN 或执行刷写。系统总图与 AM IP 启动顺序属于相邻专题。

    主调用与关键函数

    AMDev.init_sw() 先构造 AMFirmware,再创建七个 IP 对象并依次调用 init_sw();GPU 硬件启动期间,AMDev.init_hw() 按传入 IP 顺序调用 init_hw()。数据流依次是:IP 版本 → load_fw() → 按头部拆 SOS 与 IP 描述符 → PSP msg1 缓冲 → bootloader 装 sOS → 创建 ring → TOC 与 TMR → SMU 与其他 IP 固件命令 → 后续 GPU IP 初始化。

    入口 具体职责 需要验证的边界
    AMFirmware.__init__() 按 MP0/MP1/GC/SDMA IP 版本选文件;拆 SOS;形成 SMU、SDMA、PFP/ME/MEC、IMU、RLC 等描述符 固件集合必须与芯片/IP 版本、头部版本和硬件启动模式一致
    load_fw() / fetch_fw() 从本地 amdgpu 固件目录解压并比对固定 SHA-256;本地无匹配文件时取固定修订再校验哈希 哈希保证字节与清单一致,不等于签名或运行时兼容性证明
    desc() 以头部给出的 offset/size 切 memoryview,挂接 PSP 固件类型 普通切片会截短越界长度,未显式确认 len(slice)==size
    AM_PSP.init_sw() USB 路径用 SYS 映射 msg1;非 USB 路径用 VRAM;分配命令、fence、64 KiB ring,并设置 boot-time/autoload TMR 标志 物理/MC/虚拟地址单位、映射寿命和 DMA 可见性不能交给通用算法插件
    init_hw() 缺 sOS 时逐项经 bootloader 加载;重建 ring;可选 TOC/TMR;先 SMU,再设 TMR,后载各 IP 固件;最后处理 RLC 启动依赖顺序是状态机,不宜平铺为可任意重排的插件钩子
    _prep_msg1() / _bootloader_load_component() 复制固件到 msg1,补 4 字节并按 16 字节填充,flush HDP;bootloader 经消息寄存器读取地址及组件 ID 长度断言只检查原始数据,未覆盖追加和对齐后的长度
    _ring_create() / _ring_submit() 销毁旧 ring 后创建;写命令与 frame,推进写指针,等 fence,再查响应 status ring 写指针没有显式按 ring_size 取模,硬件语义尚未确定

    固件来源与信任边界

    load_fw() 用 ctypes.from_address() 读取版本头和具体头部,desc() 与 SOS 拆分依赖固件给出的数量、偏移、长度。固定哈希减小意外字节变更风险,但不足以替代解析边界检查。缓存文件、镜像哈希、芯片版本、驱动版本应一起进入部署清单与启动审计。

    信任环节 当前做法 它能证明什么 / 不能证明什么
    镜像完整性 解压后比对固定 SHA-256;必要时取固定修订再校验 证明字节与清单一致;不证明签名或可信发布者
    头部解析 按头部 offset/size 切 memoryview 依赖固件自报数量/偏移/长度;需补结构/长度校验
    离线部署 车端需预置并锁定镜像 无网络时无法即时取固定修订,必须提前锁定
    启动结果 命令返回 status==0 不能扩大解释成整个 GPU 或模型健康

    已确认边界与未验证问题

    纯内存探针直接调用 _prep_msg1(),提供 32 字节 memoryview 与假的 flush_hdp(),未触碰设备:输入 16、28 字节成功;29、32 字节通过现有断言,但因追加 4 字节后填充到 16 字节导致结构不匹配的 ValueError;33 字节由断言拒绝。因此检查对象应是填充后的长度。该结果只证明函数缓冲边界,不代表现实固件恰好接近 msg1 容量。

    问题 证据 当前定性
    msg1 长度检查对象 探针:28 字节通过、29 与 32 字节在填充后报结构不匹配、33 字节被拒 应检查填充后的长度,而非仅原始长度
    ring 写指针回卷 用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模;模拟实现也线性取 frame 待验证项,不将其定性为实机越界缺陷
    desc 切片越界 普通切片会截短越界长度,未显式确认长度相等 后续加载前需做结构/长度校验

    _ring_submit() 用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模;MockPSP._ring_submit() 也按旧写指针线性取 frame,不会证明真实硬件自动回卷。需要先获得 PSP 对写指针单位、回卷规则和 ring 最大积压的文档或设备跟踪,再用小 ring 故障注入验证。

    TMR 与命令编码各有边界:_tmr_init() 由 TOC 响应给出 TMR 大小,部分启动从 scratch5 恢复大小,再按平台模式申请并提交 TMR——scratch 恢复值及内存保留关系须经冷/暖启动台架确认。_load_ip_fw_cmd()、_load_toc_cmd() 与 _rlc_autoload_cmd() 把镜像、TOC 或自动装载请求编码为 PSP 命令,但不能把命令返回 status==0 扩大解释成整个 GPU 或模型健康。

    启动顺序本身也是契约的一部分:init_hw() 先经 bootloader 逐项加载 sOS,重建 ring,可选 TOC/TMR,然后先 SMU、再设置 TMR、后载入各 IP 固件,最后处理 RLC。把这条依赖顺序平铺成可任意重排的插件钩子,会丢掉「前一步未就绪则后一步无意义」的状态约束。

    对自制硬件与插件化的结论

    将 PSP/MMIO/DMA/固件装载封在受信任设备驱动,只向推理插件暴露带版本的「设备发现、固件校验、内存就绪、队列就绪、任务完成」能力及健康状态。

    1. 插件不得提供任意固件路径、物理地址、PSP 命令或重置入口。
    2. 新车载算力平台可替换计算驱动,但必须独立通过固件/ABI、内存一致性、冷暖启动、异常清理和模型数值/时限验证。
    3. 最终车辆输出仍受车型适配和 panda 板端 safety 约束,不能因「固件加载成功」就授权执行。
    4. 启动依赖顺序是状态机,不应把步骤平铺为可任意重排的插件钩子。

    边界说明:本专题没有 GPU、实车或 LFS 模型验证证据;探针只证明函数缓冲边界。

    常见问题

    固件加载成功就等于 GPU 能跑模型吗?

    不等于。PSP 把经过选择的镜像交给 sOS 后,后续 SMU/GFX/SDMA 才依次使用;「固件加载成功」「GPU 队列可工作」「模型输出有效」是三个不同状态,命令返回 status==0 不能扩大解释成整个 GPU 或模型健康。

    固定 SHA-256 能证明固件可信吗?

    不能。哈希只保证字节与清单一致,不等于签名、可信发布者或运行时兼容性证明。离线车端还需要预置并锁定镜像,并把缓存文件、镜像哈希、芯片版本、驱动版本一起纳入部署清单与启动审计。

    _ring_submit 的写指针没有取模一定是缺陷吗?

    不能下这个结论。源码用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模,模拟实现也按旧写指针线性取 frame,不会证明真实硬件自动回卷。这是待验证项,需要文档或设备跟踪,再用小 ring 故障注入验证。

    为什么消息缓冲的长度断言要按填充后的长度?

    因为探针显示:输入 29 或 32 字节虽通过现有断言,但因追加 4 字节后再填充到 16 字节而对齐结构不匹配并报错;28 字节可以成功,33 字节被断言拒绝。所以检查对象应是填充后的长度,而不是仅原始长度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_AM_PSP_FIRMWARE_BOOT_RING_TRUST_CONTRACT_DEEP_DIVE.md(取用了 AMFirmware/AM_PSP 函数索引、固件信任边界、msg1 与 ring 的探针结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_AM_DEVICE_IP_BOOT_PARTIAL_RESET_LIFECYCLE_DEEP_DIVE.md(仅用于确认 AM IP 启动顺序,未取用其未公开内容)。

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

    更新记录

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

  • Nissan panda safety:总线变体锁定、转角与取消载荷

    结论:Nissan safety 的核心是「变体先见即锁定」:get_addr_check_index() 在健康检查组内第一次看到某地址/总线/长度变体后就锁定其下标,之后只匹配该变体。对 Altima,如果错误接线或额外转发让 0x285 总线 0 先于所需的总线 1 到达,该组会锁到总线 0,而车型 RX 钩子只处理总线 1,后续车速不再被识别。另一条关键边界是:不在当前锁定变体内的帧,rx_msg_safety_check() 返回 true,但 safety_rx_hook() 的 whitelisted 为假,所以「返回有效不等于已消费」。TX 侧 0x169 受共享角度限制,而 0x280 Leaf 取消帧与 HUD 帧没有逐位载荷门。

    初始化与总线变体锁定

    nissan_init() 的 ALT_EPS_BUS 参数位只切换 RX 钩子预期的车速、踏板和巡航总线,构建同一 TX 白名单。get_addr_check_index() 在组内首次匹配时设置 msg_seen 与 status.index,之后只匹配该变体;safety_rx_hook() 要求变体有效且当前仍匹配才调用车型 RX 钩子。

    nissan_rx_hook() 采样 0x185 转角、0x285 后轮速度、0x15C/0x239 油门、0x454/0x239 制动和 0x30F 原车巡航。在 ALT_EPS_BUS=1 时它从总线 1 读取 0x285、0x15C、0x454、0x30F;常规模式从总线 0 读取前三类、总线 2 读取巡航;0x185 转角始终要求总线 0。

    RX:五组健康检查与「有效不等于被消费」

    nissan_rx_checks 有五个健康检查组,各项均显式忽略校验和、计数器、质量位,仅以地址、长度、总线与名义频率/存活约束健康。

    检查组 地址 允许总线
    转角 0x185 0
    后轮速度 0x285 0/1
    巡航 0x30F 2/1
    油门 0x15C 或 Leaf 0x239 0/1 或 0
    制动/状态 0x454 或 0x1CC 0/1 或 0

    源文档强调,这不是对每个 CAN 字段做完整性认证。Leaf 0x239 总线 0 的同一帧可更新油门与制动;它被放在油门健康检查组,制动组另有 0x1CC 变体,后者未被车型 RX 钩子直接解析为 brake_pressed。因此「健康表监视的消息」与「车型钩子消费的信号」不是完全一对一。

    转角以 -0.01 度/位及偏置 1310 度解码到每度 100 单位;后轮双速取平均更新速度,任一原始后轮速度非零便视为在移动。原车巡航接合由 0x30F 输入巡航检查,其上升沿可使能、断开时退出。源文档提醒,新硬件不能将 Altima 总线、Leaf 踏板和 X-Trail 踏板归一成没有来源的旧值。

    TX:转角、按钮与 Leaf 取消载荷

    nissan_tx_hook() 对 0x169 从 CAN 位域解码目标角与 LKA_ACTIVE,用共享的转角命令检查:600 度绝对界限、随车速变化的起/退转角速率、测量角度及未使能中性状态。源文档强调这里是每度 100 原始单位,不是通用扭矩接口。对 0x20B 的附加判据只有 (data[1] & 0x3D) == 0:它禁止掩码内非取消按钮位,但不会逐位检查整帧或要求取消位必须为 1。

    TX 帧 检查强度 备注
    0x169 解码目标角与 LKA_ACTIVE,运行共享角度限制 每度 100 原始单位,不是通用扭矩接口
    0x20B 仅检查 (data[1] & 0x3D) == 0 可在总线 2 或 Altima 总线 1 发送;check_relay=false
    0x2B1、0x4CC HUD 无逐字段检查 总线 0
    0x280 Leaf 取消 无逐字段检查 总线 2;探针中非零任意测试载荷仍通过 safety TX

    软件 create_acc_cancel_cmd() 会复制原车字段并覆盖按钮;Leaf create_cancel_msg() 会把 CANCEL_SEATBELT 置位,利用原车安全带告警退出巡航。源文档指出后者是驾驶员可感知的安全/HMI 行为,不能包装成普通、静默的取消插件。软件构帧约定也不能替代 MCU 逐位门:隔离探针中 0x280 的非零任意测试载荷仍通过 safety TX。

    验证证据与插件边界

    已有测试覆盖常规与 Altima 的转角/按钮基础行为,但源文档指出 Leaf 的按钮测试被明确跳过,不能证明 0x280 的 CANCEL_SEATBELT 载荷被硬件安全规则约束。本机新增探针以严格编译和 UBSan 通过按钮掩码、0x280 无载荷门及 Altima 错误总线先到锁定序列;未运行完整车型测试、目标编译器、实板或车辆 ECU 行为。

    插件化时必须把 Nissan 平台、ALT_EPS_BUS、DBC、总线拓扑与取消方式绑定;0x280 及 HUD 原始帧应列为高权限输出,不能由任意插件直接构造。Nissan 未定义专属转发钩子,共享转发按当前 TX 表中 check_relay=true 的地址/目标总线阻断主机自身控制帧;0x20B 设置 check_relay=false,不应把它的白名单资格混同于静态阻断或原车报文真实性。

    常见问题

    为什么 Altima 的车速有时候识别不到?

    因为健康检查组先见即锁定。源文档指出,若错误接线或额外转发让 0x285 总线 0 先于 Altima 所需的总线 1 到达,该组会锁到总线 0,而车型 RX 钩子仅处理总线 1,后续总线 1 车速不会被该组识别;本机隔离探针复现了这一序列。

    健康检查通过是否说明车型已经消费了该信号?

    不说明。源文档明确:不在当前锁定变体内的帧会让 rx_msg_safety_check() 返回 true,但 safety_rx_hook() 的 whitelisted 为假,车型状态不更新,因此「返回有效不等于已消费」。

    0x280 Leaf 取消帧有逐位载荷校验吗?

    没有。源码的 TX 白名单允许总线 2 的 0x280,但 nissan_tx_hook() 对它没有逐字段检查;源文档的隔离探针显示非零任意测试载荷仍通过 safety TX,因此它应列为高权限输出。

    0x20B 的 check_relay=false 是否等于原车报文可信?

    不等于。0x20B 设置 check_relay=false,源文档提示不应把它的白名单资格混同于静态转发阻断或原车报文真实性;共享转发只按 check_relay=true 的地址与目标总线阻断主机自身控制帧。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/NISSAN_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了 RX 五组健康检查表、变体锁定语义、TX 帧检查强度表、Leaf 取消载荷与探针结果)。

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

    更新记录

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

  • 风扇控制: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

  • joystick 输入生产者:100 Hz 发布与 0.2 秒失联门的缺口

    结论:joystick_control 是一个输入生产者:它要么读终端键盘、要么读游戏手柄,然后由一个独立的守护线程以 100 Hz 目标节拍发布 testJoystick。三个事实决定它的边界:轴值是运行期估计(极值动态扩展,未标定);发布线程不检查输入年龄与 cancel,采集阻塞时旧轴值仍能被封装成新的 valid=True 消息;下游 joystickd 的约 0.2 秒帧差失联门只感知消息停发,识别不了「重复旧输入的新消息」。要注意这描述的是软件路径,不是实车会保持执行器动作的结论——selfdriveState、card 和 panda 安全策略仍独立约束输出。

    两个实现:终端键盘与游戏手柄

    入口带一个容易被误读的开关:直接执行脚本时会检查 IsOffroad,但设置 ZMQ 环境变量会跳过该入口检查,而 main() 本身直接构造游戏手柄。这是开发工具的启动约束,不是强制的进程级安全策略,ZMQ 的存在也不能视为网络身份认证。

    组件 / 函数 具体行为 边界与含义
    Keyboard.update() 阻塞读取一个字符;W/S 与 A/D 分别按 0.05 累积,R 归零,C 将本地 cancel 置真,下一次输入又先置假。 轴值不自动归中;输入停止不触发归零,长期保持的值由发布线程继续发送。
    KBHit.__init__()、set_kbhit_terminal()、set_normal_term() 从 sys.stdin 取 fd,保存 termios,清除 ICANON/ECHO 并以 TCSAFLUSH 应用;注册进程正常退出时的恢复回调。 非 TTY 的 tcgetattr() 会失败;异常/信号强杀不保证 atexit 执行,终端状态不是按对象生命周期恢复。
    KBHit.getch()、getarrow()、kbhit() 单字符阻塞读、三字符方向键读取、select(..., 0) 非阻塞就绪查询分别作为静态方法暴露。 Keyboard.update() 只调用 getch(),未先查询 kbhit();getarrow() 假设读满三字符,畸形序列可抛异常。
    Joystick.update() get_gamepad()[0] 处理首个事件,按 PC/车载不同映射转换纵横轴;采集到的极值动态扩展区间,再插值、死区与三次曲线。 极值是运行期估计,不是经标定的设备物理量;只按首个事件更新。已处理的拔出异常会归零,其他异常或采集线程停止不进入该分支。
    send_thread() 独立守护线程以 100 Hz 目标节拍发布 testJoystick,把外层 valid 置真,只复制 axes_order 对应的两轴值。 不检查采集线程是否阻塞、上次真实输入时间或 cancel。
    Joystick schema 结构有 axes 和 buttons;服务表频率为 0。 当前生产者只填 axes;cancel 不是该生产者消息的一部分,下游也只取轴值。

    发布线程不看输入:100 Hz 与约 0.2 秒失联门

    send_thread() 周期性复制 axes_values。采集线程没有为每次输入维护时戳,发布线程也不检查 joystick.cancel。因此在主采集循环阻塞、设备静默但不抛已处理异常时,生产端仍会产生新的 valid=True 两轴消息。joystick_control_thread() 先同步写入 JoystickDebugMode=True,启动发布线程,主线程无限调用 update(),没有显式 stop/join/模式撤销协议。

    下游 joystickd 的约 0.2 秒帧差失联门只感知消息停发,不会识别这类「重复旧输入的新消息」。探针也印证了这一点:send_thread() 在轴值不变且 cancel=True 时仍连续发送相同的两轴,每条 valid=True 且不填 buttons。这里要重复一次边界——这描述的是软件路径,selfdriveState、card 和 panda 安全策略仍独立约束输出,不能据此断言实车会保持执行器动作。

    生命周期缺口 源码事实 后果
    输入年龄门 采集线程无每次输入时戳,发布线程不查上次输入时间 采集阻塞仍发布旧轴为「新消息」
    取消传播 C 只改本地 cancel,且下次按键即清除;消息不含取消字段 取消不会作为消息传给下游
    发布时钟 发布按 100 Hz 目标节拍,与采样时钟无关 发布时钟不能代表采样新鲜度
    关闭握手 无显式 stop/join/模式撤销协议 进程退出才停;不能推断采集卡住时会停止发布

    模式参数与下游 joystickd:两种进程切换方式

    JoystickDebugMode 在 manager 启动/下线时切换清理:车载调试模式用它把控制权在 controlsd 与 joystickd 之间切换。这里要区分两件事——参数清理不等于 testJoystick 发布生命周期监测,模式选择与输入有效性需要分开验证。下游 joystickd 只取轴值并设置帧数超时门,而 card 与 panda 各自还有独立的安全门。

    游戏手柄一侧的映射也不是只有一套:Joystick.update() 按 PC 与车载两套映射转换纵横轴,采集到的极值动态扩展区间后,再经插值、3 百分比死区与三次曲线。探针确认 PC 扳机翻转后会再归一化,已处理的拔出异常会清零轴值。源文档还列出若干待验证项:不同游戏手柄 inputs 事件顺序与极值、真实拔出/静默行为、GIL 调度下两个线程的读写时序、消息停发后的实际时间,以及车辆安全钩子的响应;当前归档目录没有针对这些边界的专用原仓库单元测试,本篇的隔离探针不能替代真实设备验证。

    输入契约:采样时钟不能由发布时钟代替

    源文档对插件化的要求很明确:输入插件输出应包含设备身份、采样单调时戳、序号、校准版本、轴语义/单位以及明确的取消事件,发布时钟不能代替采样时钟;宿主按真实输入年龄与设备健康授予短期租约,过期、拔出、采集线程失活或异常时撤销租约,并且发布无效/零值不能伪装成新鲜的真实操纵;测试控制与生产控制分属隔离命名空间,即使测试输入有效,也只形成受监督的候选指令,必须继续通过唯一控制仲裁和独立硬件安全门。

    还要留意终端资源这块的实测边界:Keyboard() 构造时才创建 KBHit,但 joystick_control_thread() 没有 finally 或显式调用 set_normal_term();正常 Python 退出时 atexit 尝试恢复,被强制终止则不能保证。此外该类注释里「Windows no-op」的说法与模块顶层 import termios 不一致,不能把它当跨平台终端插件接口。

    常见问题

    为什么「消息还在发」不代表输入还新鲜?

    因为 send_thread() 按 100 Hz 目标节拍复制 axes_values,而采集线程没有为每次输入维护时戳,发布线程也不检查上次真实输入时间或 cancel。采集阻塞但进程仍在时,旧轴值仍会被封装成新的 valid=True 消息。

    终端里按 C「取消」会传给下游吗?

    不会。C 只是把本地 cancel 置真,且下一次输入又会先置假;cancel 并不是该生产者消息的一部分,下游 joystickd 也只取轴值。因此不能把终端说明中的取消键理解为已传播的取消命令。

    游戏手柄拔出后轴值会归零吗?

    对已处理的 OSError/UnpluggedError 会归零并返回假;但源文档指出其他异常或采集线程停止并不进入该归零分支,而且极值是运行期估计,不是经标定的设备物理量。

    设置 ZMQ 环境变量是否等于更安全?

    不是。设置 ZMQ 只是跳过 CLI 入口的 IsOffroad 检查,改变的是本地检查,不能被当作网络身份认证或进程级安全策略。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOYSTICK_INPUT_PRODUCER_DEEP_DIVE.md(取用了键盘与游戏手柄两种输入实现、100 Hz 发布与约 0.2 秒失联门缺口、六项隔离探针结果与插件化输入契约)。

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

    更新记录

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

  • Honda 车型接口:能力参数、雷达静默与恢复边界

    结论:Honda 车型接口把「能力参数」和「能力兑现」分得很清楚:openpilotLongitudinalControl、ControlsReady 与 tester-present 候选都只是软件侧的请求或标记,不是 ECU 行为的回执。雷达静默依赖 disable_ecu(),而它对通信控制请求传入空预期响应前缀、随后无条件打印「ecu disabled」并返回 True,因此至多证明会话得到匹配应答且后续查询没抛异常,不能证明 ECU 已停止发送。没有闭环证据时应保持 noOutput 或只读。

    关键函数与组件

    基线是本地 source/ 归档提交 c8fb906,只分析 Honda interface.py、通用诊断函数及 card/pandad 交接;不代表任何具体年款的线束、ECU 响应或原车 AEB 行为已验证。

    入口 职责
    CarInterfaceBase.get_params() 从平台规格建立基础 CarParams,再交给 Honda _get_params() 补齐能力与参数。
    CarInterface._get_params() 选择 Bosch/Nidec 安全模式、纵向策略、指纹能力、调校和安全参数。
    get_pid_accel_limits() Bosch 给定静态加速度范围;Nidec 的最大值靠近巡航设定速度时下降。
    init() / deinit() 有雷达 Bosch 自控纵向的 UDS 通信控制请求与恢复请求。
    disable_ecu() 扩展会话重试、通信控制发送及布尔结果;注释明确警告禁用原车 AEB。
    IsoTpParallelQuery.get_data() 按预期响应前缀收集结果;空前缀不会验证通信控制的具体正响应码。

    能力与安全配置

    get_params() 先装载车型平台的质量、轴距、转向比和静态 flags,再调用 Honda _get_params()。Bosch 设置 hondaBosch、radarUnavailable=True;alphaLongitudinalAvailable 对 Bosch CAN FD 为假,openpilotLongitudinalControl 还取决于启动时传入的 alpha_long。它们是软件能力/选择位,不等于雷达实际静默。Bosch CAN FD 且 PT 总线编号至少 4 时,安全配置列表前插 noOutput,但并非所有 CAN FD 都无输出。

    Nidec 选择 hondaNidec,同时设置 openpilotLongitudinalControl=True 和 pcmCruise=True,因此不能用 pcmCruise 单独推断执行权由原车 ACC 完全负责。指纹分支检测特定 CR-V BSM 标志、扩展 HUD、混动标志、自动/CVT/手动变速箱以及备用制动帧;EPS 固件版本含逗号时设 dashcamOnly。若 card 判定控制器不可用、用户关闭开关或 dashcamOnly,它会把整组安全配置替换为 noOutput 并标记 passive。

    能力位/标记 含义与边界
    openpilotLongitudinalControl 取决于启动时 alpha_long,是软件选择位,不等于雷达已静默。
    pcmCruise Nidec 同时置真,不能单独推断执行权归属。
    safetyParam 对备用制动、Nidec SCM 变体、Bosch 自控纵向、radarless 和 CAN FD 按位编码;固件还需依自身编译门与白名单解释这些位。
    ControlsReady card 在初始化门满足时立即写,不验证雷达静默结果。

    特别地,Bosch 自控纵向在固件里受 ALLOW_DEBUG 条件编译约束,Python 参数置位本身不足以证明固件允许该模式。此外,多数车型设置 PID 横向参数,部分车型走通用扭矩调校;三处分支直接改写 CarControllerParams.BOSCH_GAS_LOOKUP_V 类属性,不是写入本次 ret 或控制器实例,且函数内没有恢复默认值——同一进程先构建某一车型参数、再构建另一车型时,后者可能继承前者的查表值。

    禁用、保活与恢复链

    只有 BOSCH、非 BOSCH_RADARLESS 且 openpilotLongitudinalControl 时,CarInterface.init() 才调用 disable_ecu(),目标是固定的诊断目标地址与 PT 总线。它先尝试 UDS 扩展会话并要求匹配应答;随后发通信控制请求,子功能带 suppress-response 位。disable_ecu() 对后一步传入空的预期响应前缀并调用 get_data(0),然后无条件打印「ecu disabled」并返回 True。所以它至多证明前一步会话得到匹配应答且后一步查询没有抛出异常,不能证明 ECU 已停止发送;Honda init() 又没有返回该布尔值。

    card 首次满足初始化门时调用 CI.init(),紧接着写 ControlsReady=True,没有验证雷达静默结果。pandad 在 FirmwareQueryDone 和 ControlsReady 都为真后读取参数、设置安全模式;这两项参数并非 ECU 行为回执。保持发送候选、MCU 接受、ECU 收到以及 ECU 持续静默是四个不同事实。

    CarInterface.deinit() 用通信控制「启用 RX/TX」请求复用 init() 路径,仍不检查恢复应答。当前主 card 运行链只见 init() 调用,未见对 Honda deinit() 的调用;进程退出或异常时不能由这个函数的存在推断原车雷达/AEB 已恢复。恢复行为是否由 ECU 会话超时、断电或其他机制完成,必须在目标车型台架和受控场地验证。

    插件与硬件验收

    将「雷达静默」设计成受信任诊断服务持有的事务能力:记录目标 ECU、物理总线、请求、正/负响应、发送持续性、实际雷达报文消失与恢复证据,并把原车 AEB/FCW 影响作为显式安全决策。没有闭环证据时保持 noOutput 或只读,不能让车型插件以 openpilotLongitudinalControl、ControlsReady 或 tester-present 候选代替验收。多车型参数生成必须隔离/冻结 BOSCH_GAS_LOOKUP_V 等调校状态;至少测试 Civic Bosch、Odyssey MMR、RDX MMR 与普通 Bosch 的不同构建顺序。

    常见问题

    disable_ecu() 返回 True 就代表雷达被禁用了吗?

    不代表。disable_ecu() 对通信控制请求传入空的预期响应前缀并调用 get_data(0),然后无条件打印「ecu disabled」并返回 True。源码说明它至多证明前一步扩展会话得到匹配应答且后一步查询没有抛出异常,不能证明 ECU 已停止发送;Honda init() 也没有返回该布尔值。

    pcmCruise 为真表示减速完全由原车负责吗?

    不能这样推断。Nidec 选择 hondaNidec 时同时设置 openpilotLongitudinalControl=True 和 pcmCruise=True,源码因此明确指出不能用 pcmCruise 单独推断执行权由原车 ACC 完全负责。

    BOSCH_GAS_LOOKUP_V 为什么会有跨车型风险?

    因为部分调校分支直接改写 CarControllerParams.BOSCH_GAS_LOOKUP_V 类属性,不是写入本次 ret 或控制器实例,且函数内没有恢复默认值。源码可推断:同一 Python 进程先构建某一车型参数、再构建另一车型时,后者可能继承前者的查表值。这需用 Civic Bosch、Odyssey MMR、RDX MMR 等不同构建顺序验证。

    进程退出后原车雷达/AEB 会自动恢复吗?

    不能由源码推断。deinit() 用通信控制「启用 RX/TX」请求复用 init() 路径、仍不检查恢复应答,而当前主 card 运行链只见 init() 调用、未见 Honda deinit() 调用。恢复行为是否由 ECU 会话超时、断电或其他机制完成,必须在目标车型台架和受控场地验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HONDA_INTERFACE_CAPABILITY_RADAR_LIFECYCLE_DEEP_DIVE.md(取用了 Honda 接口函数表、能力与安全配置、禁用/保活/恢复链、插件与硬件验收要求)。
    • 项目 02 —— openpilot-research/HONDA_CARSTATE_SPEED_BRAKE_CRUISE_INPUT_DEEP_DIVE.md(仅用于确认 Honda 输入状态专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • 四个开发工具的信任边界:回放导出与 CarParams 注入

    结论:四个开发工具的信任等级并不相同:print_flags.py 只读枚举,test_fw_query_on_routes.py 只读已记录固件,run_process_on_route.py 会把新旧消息合并导出,而 set_car_params.py 会写入当前参数库。前两者可分析,后两者带副作用,其中最危险的是把 CarParams 写入下放给任意插件。

    工具清单与副作用等级

    工具 源码链路 静态结论
    test_fw_query_on_routes.py 参数可为单一路段或文件路径;文件分支取每行再交 SegmentRange,未指定段则拼 /0,随后按 QLOG 读 carParams 并分别做精确/模糊固件匹配。 原主入口探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败也在 try 前传播。脚本只读已记录固件,不向 ECU 发查询。
    批量统计与不匹配输出 以 dongle_id 去重,在读路段前就把它加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确/未识别/模糊计数。 假消息探针确认首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。输出含 VIN 与固件字节,需视为敏感诊断数据。
    run_process_on_route.py 默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程;读完整路段调用回放,只按实际产生的消息类型删除旧输入,再将新旧消息按时间合并并保存。 探针确认零回放输出会保留并重排全部旧消息,未知白名单会选出空配置但仍读取和导出原日志。文件名进程部分来自集合迭代顺序,不是稳定标识。导出不调用车型日志净化器,副本可能携带原始 CAN/VIN。
    set_car_params.py 有路段时只读第一条 qlog 中第一条 carParams;无参数时新建空白消息,只写两个纵向标志;随后为每个键新建 Params(),按三个键顺序阻塞写入。 探针确认缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常。本层无回滚或读回,不校验平台、VIN、safety 或当前车辆身份。
    print_flags.py 遍历 BRANDS 与各平台配置标志,按标志列出车型名称。 Platforms 与归档的 CAR 枚举均未定义脚本调用的 with_flags();存在非零标志配置,非零假枚举时原主体抛 AttributeError,零标志枚举则无输出。

    run_process_on_route:替换与导出的边界

    该脚本默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程,读完整路段后调用 replay_process(),只按本轮实际产生的消息类型删除旧输入,再把新旧消息按 logMonoTime 合并并 save_log()。

    假依赖主入口探针确认:零回放输出会保留并重排全部旧消息;未知白名单会选出空配置但仍读取和导出原日志。另外,文件名中进程部分来自集合迭代顺序,不是稳定标识;save_log() 不调用车型日志净化器,副本可能携带原始 CAN/VIN。因此不能以脚本名称替代对进程副作用的审查。

    回放导出还有一个隐含前提:所选进程必须在当前代码版本下可复现,否则混合出的日志既不等于原始输入,也不等于一次真实回放;这正是需要把代码版本与被替换主题一并写入导出元数据的原因。

    test_fw_query_on_routes:批量审计的失败面

    该脚本的参数可为单一路段或文件路径;文件分支用 list(open(...)) 直接取每行,再交 SegmentRange;未指定段则拼 /0,随后按 QLOG 读取 carParams 并分别做精确/模糊固件匹配。探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败同样在 try 前传播。

    统计侧以 dongle_id 去重,且在读路段之前就把该键加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确、未识别与模糊匹配计数。需要提醒的是:首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。这些输出包含 VIN 与固件字节,应视为敏感诊断数据。

    set_car_params:三键写入无补偿

    有路段时,脚本只读取第一条 qlog 中第一条 carParams;无参数时新建空白消息,只把 openpilotLongitudinalControl=True、alphaLongitudinalAvailable=False 写入。随后为每个键新建 Params(),按 CarParams、CarParamsCache、CarParamsPersistent 的顺序阻塞写入。

    假 Params 探针确认:缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。但原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常,脚本仍继续写第三键。本层没有回滚或读回,也不校验平台、VIN、safety 或当前车辆身份。另据键属性,前两者会被 manager 启动清理,CarParamsPersistent 持久化。

    键 写入时机 生命周期
    CarParams 三键顺序写入的第一键 被 manager 启动清理。
    CarParamsCache 第二键 被 manager 启动清理。
    CarParamsPersistent 第三键 持久化。

    插件化:写入权归谁

    print_flags.py 的只读枚举也提示了一件事:即便修复了缺少 with_flags() 的问题,它仍只枚举本地声明,不证明硬件或 safety 已启用。实际车型能力仍须从车型接口、DBC 与 panda safety 交叉验证。这也说明开发工具的结论天然是局部的:它们验证的是脚本自身的控制流,而不是目标车辆的参数正确性。

    探针 覆盖的契约
    标志枚举探针 核对 Platforms 与各品牌方法清单,确认缺少 with_flags() 的异常与零输出分支。
    参数注入主入口探针 验证默认两个标志、三键写入顺序、只取首个 carParams,以及第二键异常后不触发第三键。
    固件审计主入口探针 验证行尾换行、读取器构造异常、同设备去重及不支持 panda 后的状态报告。
    回放导出主入口探针 确认剔除被排除的模型进程、只替换实际产生的类型、零输出仍导出全部旧消息。

    批量固件审计应先把路段列表解析、去重与错误计数做成显式结果;精确/模糊候选只能用于平台审核,不自动修改固件库。回放导出的日志需携带输入路段、所选进程、代码版本、被替换主题、生成主题和隐私标记,避免把混合旧/新消息误当统一来源。CarParams 的写入应归受控车辆身份服务所有,要求平台/线束/DBC/safety 绑定、三键一致性门和读回验证,不能下放给任意插件。

    常见问题

    为什么 set_car_params.py 不能在车端随意试跑?

    因为它会写入当前参数库:按 CarParams、CarParamsCache、CarParamsPersistent 三键顺序阻塞写入,且本层没有回滚或读回、不校验平台/VIN/safety/车辆身份。底层写盘失败还可能因负错误码被丢弃而不抛异常。

    回放导出会混入旧消息吗?

    会。探针确认零回放输出时,全部旧消息仍会进入导出并按时间排序;脚本只按本轮实际产生的消息类型删除旧输入。导出还不调用车型日志净化器,副本可能携带原始 CAN/VIN。

    为什么固件审计脚本可能整批中断?

    因为文件分支取每行时行尾换行未剥离,SegmentRange 的正则在内部 try 之前就抛断言并中断整批;读取器构造失败也在 try 前传播,同样会中断。

    print_flags.py 能证明某车型能力已启用吗?

    不能。它只遍历本地品牌与平台配置来列出车型名称,且归档中并未定义它调用的 with_flags();即使修复,它仍只枚举本地声明,不证明硬件或 safety 已启用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_FW_REPLAY_CARPARAMS_TOOLS_TRUST_BOUNDARY_DEEP_DIVE.md(取用了四个开发工具的入口、数据流与失败边界及探针结论)。
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(仅用于确认回放进程与验证的边界)。

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

    更新记录

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

  • VehicleModel 的 0.1 与 1e-6:低速回退与横滚补偿奇异点

    结论:VehicleModel 是主机上的纯计算模型,不是独立的安全许可,它的两个数字最容易被误用。第一是速度下限 0.1:steady_state_sol() 在 u > 0.1 时选动态解,否则回退到运动学解,而动力学矩阵 A 含 1/u,不能把零速送入动态分支。第二是 1e-6:roll_compensation() 在 abs(sf) < 1e-6 时直接给零,否则按 g*roll/(1/sf - u²) 计算。此外 panda 固件的曲率因子不包含 Python 侧的 chi 或横滚补偿,两端不能宣称逐点数学等价。以下基于固定快照 c8fb906 的静态源码分析,未做实车动力学辨识或 CAN 发送验证。

    输入、状态与公式

    VehicleModel.__init__() 从 CarParams 保存质量 m、转动惯量 j、轴距 l、质心到前后轴距离 aF/aR、后轮转向比 chi 以及前后原始侧偏刚度。update_params() 仅把原始刚度乘以在线 stiffness_factor,并替换方向盘转向比 sR,不会累乘上次结果。

    三条核心公式:calc_slip_factor() 算 sf = m(cF*aF − cR*aR) / (l²*cF*cR);curvature_factor() 给出 k = (1 − chi) / (l * (1 − sf*u²));calc_curvature() 返回 k*sa/sR + roll_compensation,其中 sa 为方向盘弧度、u 为 m/s,输出单位为 1/m。get_steer_from_curvature() 先扣除横滚补偿再除以 k,不是简单以轴距乘曲率。

    函数 作用 输入与边界
    steady_state_sol() 速度 u>0.1 时选动态解,否则选运动学解 返回 [侧向速度, 横摆角速度];低速分支不使用 roll。
    kin_ss_sol() 低速按轴距、后轴到质心距离、转向比得线性解 即使 u=0 也返回零;不显式使用 chi。
    create_dyn_state_matrices() 构造状态 [侧向速度, 横摆角速度]、输入 [方向盘角, 横滚角] 的 2×2 A/B A 含 1/u,不能把零速送入动态分支。
    dyn_ss_sol() 用 −solve(A, B) @ [sa, roll] 求稳态 solve 可能因参数退化或矩阵奇异失败,函数本身不做回退。
    get_steer_from_yaw_rate() 先以 yaw_rate/u 求目标曲率,再反算方向盘角 u=0 无保护;与低速运动学回退是两条不同 API。

    低速回退与奇异点约束

    模型里有两个需要调用方自己守住的数值边界。其一是低速回退:u=0 时动态分支不可用,必须走运动学近似,且该近似即使 u=0 也返回零。其二是横滚补偿的奇异点:roll_compensation() 在 abs(sf) < 1e-6 时直接给零,避开分母 1/sf − u² 的退化。

    边界量 取值 作用
    动态/运动学切换 u>0.1 决定用动态解还是低速运动学解。
    侧偏因子近零门 abs(sf)<1e-6 横滚补偿直接给零,避开奇异分母。
    在线更新下限 0.1 controlsd 更新模型时取的下限,仅防非正值。
    角度限幅最小速度 1 m/s apply_steer_angle_limits_vm() 里原始速度至少取 1 m/s。

    需要注意的是,controlsd 从 vehicleParameters 读取估计值并各自以 0.1 为下限更新模型,这个下限仅防止非正值,不能证明估计值物理可信。上游 paramsd 发布时另带有效性字段,但该调用点没有以这些字段阻止更新。

    运行调用链与安全边界

    paramsd 发布估计的转向比、刚度、横滚和转角偏移,controlsd 每周期更新模型,再用转角减偏移估算实际曲率(此处曲率加负号以匹配控制层符号约定)。LatControlCurvature.update() 用模型曲率构成跟踪误差;LatControlTorque.update() 将其乘 vEgo² 作为横向加速度测量,并用模型把方向盘死区换算为加速度死区。模型误差会传播到控制反馈,但模型本身不决定 latActive。

    apply_steer_angle_limits_vm() 用反算方向盘角把横向 jerk/加速度上限转换为主机角度限幅;Tesla 与 BYD 的控制器各用固定安全车型参数实例化独立模型,避免把 controlsd 在线估计参数直接当作固件许可条件。

    主机与固件的差异

    panda 的 steer_angle_cmd_checks_vm() 在固件内独立计算角度/加速度/jerk 门;其 get_curvature_factor() 与 Python 基式相近,但采用固件给定 slip_factor/轴距/转向比,不包含 Python 的 chi 或 roll_compensation,还使用固件车速与额外容差。因此不能声称两端逐点数学等价。

    项 Python 主机模型 panda 固件
    后轮转向比 chi 参与 curvature_factor() 不包含
    横滚补偿 roll_compensation() 参与 不包含
    侧偏因子/轴距/转向比 来自 CarParams 或在线估计 固件给定值
    车速来源与容差 主机估计 固件车速加额外容差

    joystickd_thread() 也会用模型估算测试转角幅度,这不代表绕过车端许可。车辆模型可作为纯计算插件提供曲率、横摆角速度及反算接口,但应在边界明确参数来源、单位、版本、速度有效域和非有限值处理;在线估计值应视为观测数据而非授权数据,主机控制器与 panda 固件必须绑定同一车辆身份和经校准的安全参数集,版本失配时禁用执行。

    现有验证与缺口

    现有 test_vehicle_model.py 以 Honda Civic 参数验证 1–30 m/s 下的横摆角速度往返、动态稳态解与直接公式一致,以及一秒离散仿真逼近稳态;它没有覆盖 u=0 反算、参数退化、在线更新突变、固件与 Python 跨实现一致性或实车传感器误差。本专题只做静态源码核查,未运行测试或实车验证。下一阶段宜先在隔离仿真中加入零速/奇异点、参数边界、符号一致性和主机-固件限幅差异矩阵,再讨论车端实验。

    常见问题

    为什么 0.1 这个速度下限会出现在模型里?

    因为动态状态矩阵 A 含 1/u,steady_state_sol() 在 u > 0.1 时才选动态解、否则回退运动学解;controlsd 更新模型时也取 0.1 为下限,但那只是防止非正值,不能证明估计值物理可信。

    roll_compensation() 在什么情况下直接返回零?

    当 abs(sf) < 1e-6 时直接给零,否则按 g*roll/(1/sf − u²) 计算。这个门槛是为了避开分母退化,属于奇异点约束。

    主机模型和 panda 固件算的曲率一样吗?

    不一样,也不能宣称逐点等价。固件的 get_curvature_factor() 与 Python 基式相近,但采用固件给定的 slip_factor/轴距/转向比,不包含 Python 的 chi 或 roll_compensation,还使用固件车速与额外容差。

    VehicleModel 是一个转向安全许可吗?

    不是。它是主机上的纯计算模型,不决定 latActive;候选转角经宿主限幅后仍须由不可被普通插件替换的独立固件安全门检查。在线估计值应视为观测数据而非授权数据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VEHICLE_MODEL_DYNAMICS_CONTROL_SAFETY_CONTRACT_DEEP_DIVE.md(取用了输入与公式、关键函数索引、低速回退与 1e-6 奇异点、运行调用链、主机与固件差异以及现有测试缺口)。

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

    更新记录

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

  • tinygrad 范围与归约化简:编译器插件契约与四个正确性门

    结论:codegen/simplify.py 处理 UOp 图中的循环范围、条件索引和归约,是编译器内核优化插件,不是可在线替换的控制插件。它的正确性靠几道静态门保证,最核心的是:合并范围只接受 FLOORDIV/FLOORMOD 总数不增加的结果;RANGE % 常量 拆分必须能静态证明整除、且范围长度节点本身为 CONST;遇到无门索引、STORE 或嵌套 REDUCE 时收缩或 collapse 会被阻止。源文档另指出 postopt symbolic 不受 if optimize 保护,插件输入必须是已通过类型、范围及缓冲 ABI 校验的 UOp 图。

    编译调用链:optimize 与非 optimize 的分界

    get_kernel_graph() 先 run_rangeify(),再用 symbolic + pm_reduce_simplify 等规则简化无负载归约,随后把缓冲图降到 STORE。full_rewrite_to_sink() 仅在 optimize=True 时依次执行 pm_load_collapse、pm_split_ranges + pm_flatten_range、初始符号化、pm_flatten_range + pm_simplify_ranges,再进入后范围优化;关闭优化并非仍会执行这些规则。

    源文档特别指出:同一函数的 postopt symbolic 不受上述 if optimize 保护,仍运行 pm_reduce_unparented;而 pm_reduce_load_collapse 是供 pm_load_collapse 内部使用的增强规则集,不能当成独立顶层 pass。

    范围规则与关键函数

    函数或规则 作用及必要条件
    flatten_range() 对 REDUCE/END 的普通范围经 SINK 重新收集并展平;void/bool 回边留在末尾,不作为普通循环范围结束。
    simplify_merge_adjacent() END 只试相邻范围,REDUCE 试排列组合;两轴必须同 AxisType、在所有内部 REDUCE 中归属一致;合并后的索引用商/余数还原,且只接受 FLOORDIV/FLOORMOD 总数不增加的结果;每次只合并一对。
    mark_gated() 从 INDEX 的 WHERE 有效条件中识别 RANGE < CONST,对同一范围取已见上界的最大值;若某次索引未受该范围的门保护,就记录原范围长度;REDUCE 也强制原长度。
    pm_simplify_ranges 汇集 END/REDUCE 合并、INDEX 门收集、REDUCE 禁止收缩;直到 SINK 才通过 do_substitute() 一次性替换范围并清空上下文。
    mark_range_mod() 只针对 RANGE % 常量;范围长度须为常量且 UOp.divides(c.val) 能证明被模数整除;WARP/DEVICE 轴排除;已有待拆轴不会重复登记。
    pm_split_ranges 在 SINK 用商轴与余数轴重建原索引:outer * c + inner,新增轴编号由原 arg 派生。
    reduce_unparented() 源表达式未引用的 REDUCE 轴可移除:ADD 乘以轴长,MUL 对轴长取幂,MAX 保持原值;其他归约运算不走此规则。

    源文档强调:这里的「整除」是静态证明,不是运行时猜测。UOp.divides() 对常量、STACK、ADD、MUL 和带 multiple_of 约束的 PARAM/BUFFER 做有限证明,无法证明即返回 None;mark_range_mod() 还要求范围长度节点本身是 CONST,所以动态长度即使数学上可能整除,也不会触发该拆分。

    四个正确性门

    正确性门 表现
    不增加 FLOORDIV/FLOORMOD 数量 合并相邻范围后,只接受商/余数总数不增加的结果。
    归约范围归属一致 两个轴必须在所有内部 REDUCE 中归属一致,否则不能合并。
    无门索引和归约阻止收缩 某次索引未受范围门保护时记录原长度,REDUCE 强制原长度。
    STORE 和嵌套 REDUCE 阻止 collapse reduce_collapse() 发现 STORE 或内层 REDUCE 即退出。

    pm_reduce_collapse 包含无父范围规则和几类代数重写:把与 RANGE 无关的加项或正整数乘项移出小于比较;把有上下界的布尔门求和化为截断区间长度;拆分加法求和;把布尔乘法改为 WHERE。源文档指出适用范围由模式和 no_range() 判断决定,不意味着所有求和都能闭式化简。

    pm_reduce_load_collapse 额外识别「用动态索引挑选某个 RANGE 元素」的 gated load 求和:替代 RANGE 前先检查索引在 [0, extent),并保留有效性条件;其最后一条规则在含加载索引的比较中逆转一种代数移项,以免对加载得到的索引做可能溢出的加法。源文档提醒,不能把这一优化等同于任意 gather 的安全实现。

    源文档还指出 reduce_collapse() 的处理方式:它逐归约轴提取依赖子图,外部输入先换成有范围界的临时 PARAM 再运行模式重写,结果仍含 RANGE 时放弃。这一设计避免把外部动态表达式误当成轴内常量,但只证明匹配规则覆盖的形式,不等于对所有求和都成立。

    面向硬件与插件化

    这层适合定义为编译器内核优化插件。插件输入应为已通过类型、范围及缓冲 ABI 校验的 UOp 图;输出须保留 RANGE 的轴类型与索引等价、门条件、归约 dtype/溢出语义以及 LOAD/STORE 副作用。ctx 是一次图重写的临时状态,不能跨模型或并发编译复用。

    车载推理部署应将编译器版本、模型工件、renderer/设备能力、缓冲布局及测试结果锁定在同一版本。关键回归包括非整除/动态轴长度、无门索引、归约嵌套、边界像素、整数溢出、不同后端浮点归约误差和最坏执行时延。源文档明确:编译通过不证明相机输入、模型输出或整车控制安全,执行许可仍由车型适配和 panda 固件安全门约束。

    验证与限制

    上游四项定向用例已通过:TestReduceCollapse.test_multi_range_reduce_add、test_reduce_shapeless_const_unroll、TestSchedule.test_arange_sum、test_arange_sum_alt(PYTHONPATH=. DEV=PYTHON)。test_simplify_valid_idx.py 的范围收缩用例因环境缺少 z3 而未运行。源文档明确未进行完整后端编译、LFS 模型数值、目标 GPU、实车或 CAN 验证,source/ 未修改。

    常见问题

    这层化简是可在线替换的控制插件吗?

    不是。源文档把它定义为编译器内核优化插件,不是可在线替换的控制插件:插件输入应为已通过类型、范围及缓冲 ABI 校验的 UOp 图,执行许可仍由车型适配和 panda 固件安全门约束。

    RANGE % 常量为什么不是总能拆分?

    因为它要求静态证明。mark_range_mod() 要求范围长度是常量,且 UOp.divides(c.val) 能证明被模数整除;UOp.divides() 只对常量、STACK、ADD、MUL 和带 multiple_of 约束的 PARAM/BUFFER 做有限证明,无法证明即返回 None,动态长度也不会触发拆分。

    为什么不把加载索引的求和都做闭式化简?

    因为需要保留边界条件。源文档指出 pm_reduce_load_collapse 只在替代 RANGE 前确认索引在 [0, extent) 并保留有效性条件,且最后一条规则刻意避免对加载索引做可能溢出的加法;不能把这一优化等同于任意 gather 的安全实现。

    编译器优化通过能否说明推理安全?

    不能。源文档明确编译通过不证明相机输入、模型输出或整车控制安全;车载部署应把编译器版本、模型工件、后端能力与测试结果锁定在同一版本,并覆盖溢出、动态轴长度与最坏执行时延等回归。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_RANGE_REDUCE_SIMPLIFY_PLUGIN_CONTRACT_DEEP_DIVE.md(取用了编译调用链分界、范围规则表、四个正确性门、插件化输入输出约束与定向用例结果)。

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

    更新记录

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

  • Chrysler panda safety:CUSW 双位按钮与两套模式矩阵

    结论:Chrysler panda safety 有两套互不通用的安全模式:常规模式按参数位选 Pacifica/Jeep、Ram DT 或调试构建中的 Ram HD,CUSW 是独立且仅调试注册的另一套钩子。最需要记住的边界是按钮载荷:常规模式以 data[0]==1 判取消、data[0]==0x10 判恢复,两个值不能同时匹配;而 CUSW 用两个独立位,表达式 is_cancel || (is_resume && controls_allowed) 使 0x11 同时置位在控制未许可时仍会通过。

    本文基于固定源码快照,分析常规 Chrysler 与独立 CUSW safety 模式,并用本机隔离探针核对若干分支。ALLOW_DEBUG 是编译条件,不等于目标 panda 固件已经启用;探针不代表刷写、台架或实车验证。

    模式入口与函数索引

    chrysler_init() 按参数位选择 RX/TX 表,Ram DT 分支优先于 Ram HD;非调试构建忽略 Ram HD 位,落入 Pacifica/Jeep 配置。CUSW 的安全模式注册受 ALLOW_DEBUG 限制,属于独立钩子。

    函数 作用与关键差异
    chrysler_init() 按参数位选 Pacifica/Jeep、Ram DT 或调试构建中的 Ram HD;Ram DT 优先于 Ram HD,非调试构建忽略 Ram HD 位
    chrysler_rx_hook() 按平台地址与总线更新 EPS 电机扭矩、ACC 接合、停车状态、油门及制动
    chrysler_tx_hook() 解码目标转向扭矩及请求位,调用共享限幅;按钮仅允许取消,或在 controls_allowed 时恢复
    chrysler_cusw_init() 建立独立的五组 RX 与三组 TX 白名单;安全模式注册受 ALLOW_DEBUG 限制
    chrysler_cusw_tx_hook() 扭矩限制为 250 原始单位;取消与恢复按钮分别取 bit 0、bit 4
    chrysler_compute_checksum() 对除末字节外的载荷按多项式 0x1D 做 CRC8,初始值 0xFF,末尾按位取反;末字节为接收校验值

    平台与信号矩阵

    四种模式的电机扭矩误差项均为 80。表内上限是 safety 内部原始单位,不是物理方向盘扭矩,也不是可直接移植的标定值。常规模式的 EPS 测量来自载荷字节 4/5,CUSW 来自字节 3/4;常规模式以 0x1F4/0x99 的 bit 21 判断 ACC,CUSW 以 0x2EC 的 bit 7。

    模式 关键 RX 来源 TX 地址、总线、长度 扭矩上限 / 每帧升降 / 实时变化
    Pacifica/Jeep EPS 0x220/bus 0;ACC 0x1F4/bus 0;车速 0x202/bus 0;油门 0x22F、制动 0x140/bus 0 按钮 0x23B/0/3;转向 0x292/0/6;HUD 0x2A6/0/8 261 / 3、3 / 112
    Ram DT EPS 0x31/bus 0;ACC 0x99/bus 2;车速 0x79/bus 0;油门 0x9D、制动 0x83/bus 0 按钮 0xB1/2/3;转向 0xA6/0/8;HUD 0xFA/0/8 350 / 6、6 / 112
    Ram HD,仅 ALLOW_DEBUG EPS 0x220/bus 0;ACC 0x1F4/bus 2;车速 0x11C/bus 0;油门 0x22F、制动 0x140/bus 0 按钮 0x23A/2/3;转向 0x276/0/8;HUD 0x275/0/8 361 / 14、14 / 182
    CUSW,仅 ALLOW_DEBUG 电机扭矩 0x1EC;ACC 0x2EC;车速 0x1E4;油门 0x1FE;制动 0x1E8,均 bus 0 按钮 0x2FA/0/3;转向 0x1F6/0/4;HUD 0x5DC/0/4 250 / 4、4 / 150

    移动判断也按平台分:Pacifica/Jeep 用左右轮速 0x202,Ram 用 ESP_8 双字节非零,CUSW 用 0x1E4 车速字段非零。健康检查方面,常规 RX 表监视五组;Pacifica/Jeep 的 0x202 特别忽略校验和与计数器,其余四组检查 CRC/计数器但忽略质量位;常规计数器为字节 6 高半字节,CUSW 计数器为倒数第二字节低半字节。CRC/计数健康不等于报文来源认证,总线身份、线束和原车 ECU 时序仍需单独验证。

    按钮载荷与转发边界

    常规按钮帧以 data[0] == 1 判断取消,以 data[0] == 0x10 判断恢复,因此两值不能同时匹配,但其它字节没有车型专属检查。CUSW 以两个独立位判断,表达式 is_cancel || (is_resume && controls_allowed) 使 0x11 同时置位在控制未许可时仍通过。

    项目 常规模式 CUSW
    取消判定 data[0] == 1 bit 0
    恢复判定 data[0] == 0x10 bit 4
    双位组合 两值互斥,组合无法同时匹配 0x11 在控制未许可时仍通过
    测试覆盖 明确测试组合被拒 测了中性、单独取消/恢复,未断言双位组合

    各模式的 HUD 地址在 TX 白名单,但两套车型 TX 钩子都没有 HUD 载荷检查。转向与 HUD 的 check_relay=true 会参与共享继电器故障检查和来自 bus 2 的静态转发阻断;按钮的 check_relay=false 不具备这两项效果。

    转发边界的判定要分三步:safety_tx_hook() 先做通用地址/总线/长度白名单与继电器故障门,再由车型钩子做载荷判定,而来自另一总线的静态转发阻断由 safety_fwd_hook() 处理。因此「能不能发」与「会不会阻断原车转发」是两个独立问题,不能只看白名单。

    两套测试的口径也不同:现有 test_chrysler_cusw.py 测了中性、单独取消或恢复,但没有断言双位组合;常规 test_chrysler.py 则明确测试组合被拒。这一差异是现有钩子行为,不应当视作车辆接受或设计允许组合键。

    软件层与固件层不是同一件事

    软件 CarInterface._get_params() 把 Ram HD 与 CUSW 标为 dashcamOnly,CUSW 另选 chryslerCusw 模式;其 CarControllerParams 还设置软件侧限幅与周期。尤其 Ram DT 软件上限为 261,而板端限值为 350,说明软件策略和固件许可不是同一层。

    自制硬件插件不能把 dashcamOnly、软件限幅或模式参数当作板端已经正确配置的回执。插件化时应把平台指纹、DBC、总线映射、safety 模式/参数、编译能力与线束版本作为不可分割的准入清单。HUD 与按钮原始帧均须列为受控输出,普通 UI/诊断插件不能绕过 card 和板端安全层直接发送。

    本机隔离探针在 release/debug 两种 ALLOW_DEBUG 编译条件下均以 -Wall -Wextra -Werror 与 UBSan 通过,覆盖平台按钮总线、Ram HD 非调试回退、CUSW 非调试不可注册、CUSW 双位组合、HUD 无逐字段门及静态转发阻断。它没有输入有效 RX 序列、验证 CRC 错误/计数回绕或执行完整车型测试;更不代表目标固件、台架或实车。

    常见问题

    CUSW 模式和常规 Chrysler 模式能混用吗?

    不能。CUSW 是独立钩子,拥有自己的五组 RX 与三组 TX 白名单、独立的扭矩编码与限值,且安全模式注册受 ALLOW_DEBUG 限制;常规模式的地址、总线、长度与按钮位都不能按品牌名复用。

    Ram HD 在普通构建里会怎样?

    非调试构建会忽略 Ram HD 位,落入 Pacifica/Jeep 配置;而且 Ram DT 分支优先于 Ram HD。所以不是「选了 Ram HD 位就一定按 Ram HD 配置」,还要看构建条件与优先级。

    为什么 CUSW 的 0x11 组合键仍能通过?

    因为 CUSW 用两个独立位判断,表达式为 is_cancel || (is_resume && controls_allowed),只要有取消位就通过,所以 0x11 在控制未许可时仍会通过。常规模式则因两值互斥无法同时匹配,且测试明确覆盖组合被拒。这是现有钩子行为,不应视作车辆接受或设计允许组合键。

    HUD 地址在白名单里,是不是就有逐字段校验?

    不是。各模式的 HUD 地址在 TX 白名单,但两套车型 TX 钩子都没有 HUD 载荷检查。白名单控制的是地址、总线、长度,并受继电器故障门约束,不等于载荷逐字段校验。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHRYSLER_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了常规与 CUSW 两套模式矩阵、按钮载荷差异、平台信号表与隔离探针结论)。
    • 项目 02 —— openpilot-research/ACTUATION_GATE_CHAIN_DEEP_DIVE.md(仅用于确认执行链分层语义,未取用其未公开内容)。

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

    更新记录

    首次发布 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