结论:原生 pandad 用 100 Hz 主节拍派发约 20/10/2 Hz 的低频任务,但它暴露的「存活证据」必须分层看:进程存活、句柄 connected、CAN 更新、控制读成功、MCU 安全模式读回是五个不同层级。PandaSpiHandle::connected 在构造时为真,而在此 SPI 实现中未见出错后置假,因此 check_connected() 不是可靠的链路探针;comms_healthy 则在 bulk 负返回时锁存为假,即使随后成功也不自动恢复。本文为对本地 source/ 快照 c8fb906 的静态追踪,未启动进程、未接入实板、未发送车辆控制帧。
构建、进程所有权与运行开关
SConscript 在非 Darwin 平台构建原生程序;但 SCons 构建出二进制不等于 manager 直接管理它——进程表把原生 _pandad 设为 enabled=False,常驻的是 Python pandad 包装器。包装器通过签名门后 Popen(["./pandad"]),等待其退出再恢复,原生进程的退出码不作为包装器的成功判据。macOS 不构建此目录的原生程序,但进程表仍定义 Python 包装器,两者是独立的构建/运行条件。
| 开关 | 源码入口 | 对运行态的影响 |
|---|---|---|
BOARDD_LOOPBACK |
connect() |
只要变量存在就请求 panda CAN 回环,不是只读诊断 |
BOARDD_SKIP_FW_CHECK |
connect() |
跳过原生 up_to_date() 签名门;不跳过外层 Python 包装器的刷写/最终签名检查 |
FAKESEND |
pandad_run() |
阻止发送线程调 Panda::can_send(),但主循环仍可下发安全模式、心跳、风扇与 IR 命令 |
STARTED |
send_panda_states() |
在本地 health 副本中强制点火线位,影响点火/省电/NO_OUTPUT 分支与发布值,不改变真实引脚 |
NO_FAN_CONTROL |
process_peripheral_state() |
跳过风扇目标下发,仍处理 IR 与其他设备命令 |
PANDAD_MAXOUT |
can_receive() |
收 CAN 后追加一次向 0xab 端点的丢弃缓冲读取,返回值不单独作为健康判据 |
SPI_ERR_PROB |
lltransfer() |
正值时随机改 SPI 长度、TX 或 RX 数据,属实传输注错开关,不可在行车部署 |
这些开关不是可分配给普通插件的能力:FAKESEND 只约束候选 CAN 发送,STARTED 与回环/注错会改变网关控制流或真实总线行为。设备配置应由受信任启动器使用固定清单生成,并在上车前核对实际进程环境。
调度和观测语义
主循环先收 CAN,然后每 5 帧处理风扇/IR,每 10 帧更新 selfdriveState、deviceState 并处理 panda 健康/心跳/安全模式,每 50 帧发布外设状态;每轮还持续消费 MCU 串口日志。RateKeeper 名义基频是 100 Hz,首次 frame=0 即运行低频分支。
| 节拍 | 帧模数 | 任务 |
|---|---|---|
| 100 Hz | 每帧 | 先接收 CAN 并发布,逐轮读取串口日志;RateKeeper 调整帧计数与睡眠 |
| 约 20 Hz | 每 5 帧 | 处理风扇目标与 cabinCameraState IR 目标 |
| 约 10 Hz | 每 10 帧 | 更新 selfdriveState、deviceState,处理 MCU/CAN 健康、心跳与 PandaSafety 配置 |
| 约 2 Hz | 每 50 帧 | 发布 peripheralState(电压、电流、风扇) |
若 SPI 交易、串口读取或消息处理超期,实际频率由运行时决定,RateKeeper 不能保证硬实时或补发错过的状态帧。engaged 每个 10 Hz 周期由 selfdriveState 的 alive/valid 与 enabled 联合计算;is_onroad 仅在 deviceState 本次 updated 时更新,否则沿用上次布尔值。因此「设备消息未更新」与「确认仍在行车」不能等同。
send_panda_states() 对 health 或任一路 CAN health 返回空值时直接跳过发布,process_panda_state() 随之返回,本轮不发心跳,也不执行它的通信故障退出门;主循环随后仍调用 configureSafetyMode(is_onroad)。若读到了结构体但 comms_healthy=false,事件标无效;只在 ignition_local=false 时设置 do_exit,点火真时继续运行并依赖其他安全层。
失联、退出与故障路径
PandaSpiHandle::connected 构造时为真,在此 SPI 实现中未见出错后置假,因此 check_connected() 不是可靠的链路探针——进程存活、句柄 connected、CAN 更新、控制读成功、MCU 安全模式读回是不同层级的证据。默认 SPI 交易重试还可能没有总时限。退出主循环后,只有 is_onroad && !engaged && connected 才尝试发 NO_OUTPUT,且控制写结果没有回执;接着 join() 发送线程——若线程正阻塞于无总截止期的 SPI 交易,退出也可能被推迟。
Python 包装器会在原生进程退出后进入自己的恢复循环,但不能代替 MCU 固件的独立心跳/安全钩子。固件过期异常在 connect() 的构造 try 之外抛出,原生内部 100 ms 重试只覆盖构造失败,外层包装器才处理进程退场。
自制硬件和插件验收
应把网关状态拆成 process_alive、transport_available、can_rx_fresh、health_snapshot_complete、mcu_safety_mode_confirmed 和 heartbeat_age,每项带设备 UID、采样时间、错误原因与独立阈值。插件只能订阅这些观测或提交受约束候选帧,不能自行改变心跳/安全模式;退出和断电路径应由 MCU 侧默认安全态兜底,而非依赖主机退出时的一次控制写。
相邻的 test_pandad_spi.py 在实板注入小概率 SPI 错误,检查 CAN、pandaStates、peripheralState 的数据与平均/最大间隔;它没有覆盖永久 SPI 失败、health 只有短读、点火真假两种退出分支或 join() 被无限交易阻塞。本轮未运行该硬件测试,新平台应先在无车台架做这些故障注入。
常见问题
check_connected() 能证明 SPI 链路正常吗?
不能。PandaSpiHandle::connected 在构造时为真,而在此 SPI 实现中未见出错后置假,所以 check_connected() 不是可靠的链路探针;进程存活、句柄状态、CAN 更新、控制读成功与 MCU 安全模式读回是不同层级的证据。
comms_healthy 变假后会自动恢复吗?
不会。comms_healthy 只在 bulk 返回负值时被置假,本句柄内未见恢复为真,即使随后调用成功也不自动恢复;控制读写本身也不更新该位。
health 读失败时会发心跳吗?
不会。send_panda_states() 对 health 或任一路 CAN health 返回空值时直接跳过发布,process_panda_state() 随之返回,本轮不发心跳,也不执行它的通信故障退出门。
pandad 退出时一定会发 NO_OUTPUT 吗?
不一定。退出主循环后只有 is_onroad && !engaged && connected 同时成立才尝试发 NO_OUTPUT,且控制写结果没有回执;join() 若阻塞在无总截止期的 SPI 交易,退出也可能被推迟。
延伸阅读
- CAN 报文解析的「健康位」在说什么:校验、计数器与总线空窗 —— 健康位语义与本文的
comms_healthy分层相呼应 - 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 网关状态与执行安全的层间分离
- openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么 —— 100 Hz 节拍与运行时频率的背景
- panda 故障寄存器与配置契约 —— 板端故障与安全配置的另一面
参考资料
- 项目 02 ——
openpilot-research/PANDAD_NATIVE_MAIN_LOOP_LIVENESS_TELEMETRY_DEEP_DIVE.md(原生 pandad 入口、主/发送线程、调度分频与失联降级) - 项目 02 ——
openpilot-research/PANDAD_NATIVE_SPI_RESPONSE_BOUNDS_RETRY_CONTRACT_DEEP_DIVE.md(源文档指向的原生 SPI 响应边界与重试专题) - 项目 02 ——
openpilot-research/PROCESS_SUPERVISION_DEEP_DIVE.md(源文档指向的进程监督与设备恢复包装器专题)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复