panda 主机 CAN 组包:70 字节线格式与 XOR 校验为什么晚于安全钩子

作者:

在

结论:主机到 MCU 的 CAN 字节流是一台分片组包机:CANPacket_t 头部固定 6 字节,DLC 查表最长对应 70 字节线格式,packed, aligned(4) 结构在本机编译为 72 字节。校验顺序是「先安全钩子、后 XOR 校验」:can_send() 先运行 safety_tx_hook(),坏 XOR 帧虽然不会上总线,却可能先触达有状态的安全钩子。returned 回执只代表已提交硬件 FIFO,不等于车端已接收。

6 字节头与 72 字节结构:线格式到底传多少

报文的字节布局由 CANPacket_t 与 pack_can_buffer() 定义:6 字节头包含 DLC、bus、FD、地址、标志与 XOR;DLC 查表对应 0–64 字节载荷,因此最长有效线格式为 70 字节。C 结构带 packed, aligned(4),在本机编译为 72 字节,尾部的对齐字节不应传输。

Python 编码显式组出首字节 DLC:4 | bus:3 | fd:1、随后四字节小端地址/状态位和 XOR;原生 C++ can_header 则把首位命名为 reserved,发送时零初始化。两者的 6 字节头部形状相同,但发送 FD 的策略不同——这一点在下一节展开。

环节 关键值 含义
头部长度 6 字节 DLC、bus、FD、地址、标志与 XOR
载荷范围 0–64 字节 由 DLC 查表决定
最长线格式 70 字节 6 字节头 + 最大载荷
本机 C 结构 72 字节 尾部对齐字节不传输
MCU 写缓冲 72 字节数组 容纳一个最大协议帧

主机到车辆:一半包留在哪里、什么时候补齐

Python 把完整消息串接成字节流:can_send_many() 经 USB EP3,SPI 由 endpoint3 自身事务长度承载,两者进入同一 comms_can_write()。MCU 侧用 can_write_buffer 保存不完整帧及剩余字节数;补齐后构造 CANPacket_t 并调用 can_send(..., false),其余输入逐帧处理。

环节 实现 行为
主机分块写入 can_send_many() / USB EP3 / SPI endpoint3 USB 按客户端块切分,SPI 按自身事务长度;进入同一 comms_can_write()
MCU 写入组包 comms_can_write() 半包存于 72 字节缓冲;补齐后调用 can_send(..., false)
MCU 读出分片 comms_can_read() 先发上次帧尾,再取 RX 队列;按 max_len 返回实际字节数
主机接收重组 can_recv() / unpack_can_buffer() 拼接残包,逐帧验证 XOR 并保留下次所需残包
状态清空 comms_can_reset() 连接时主机发 0xc0,清 MCU 读/写半包指针;不清 CAN RX/TX 队列
容量流控 refresh_can_tx_slots_available() 三路 TX 队列同时满足 USB 51 帧或 SPI 170 帧余量时恢复接收

两个边界必须分清:读出侧不保证传输块与 CAN 帧边界重合,所以主机必须能拼接残包;主机缓存与 MCU 的两个缓存(写半包、读残包)是三份不同状态。另外,51 帧 / 170 帧是背压门槛,不代表安全钩子放行或总线已发送。

校验与许可的真实顺序

这是整篇文章最容易被写反的一点。对完整帧,comms_can_write() 直接调用 can_send();can_send() 先运行 safety_tx_hook(),再决定入 TX 软件队列或生成 rejected 回执。直到 process_can() 从队列取出时,才运行 can_check_checksum();校验失败只增加 total_tx_checksum_error_cnt。

顺序 环节 结果
1 USB/SPI 字节进入 comms_can_write 组出完整包
2 can_send 调用 safety_tx_hook 拒绝则 rejected 回执入 RX 队列
3 允许则入 TX 软件队列 尚未校验 XOR
4 process_can 校验 XOR 错误则增加 tx checksum error
5 正确则提交 FDCAN TX FIFO 产生 returned 回执

由此得到两个结论:坏 XOR 帧不会由这条发送路径上总线,但可能先触达有状态的安全钩子——例如 Toyota 的 toyota_tx_hook() 会调用 steer_torque_cmd_checks(),其中可能更新 desired_torque_last。源文档对此的措辞很克制:具体可触发条件及后续有效帧影响需要隔离测试,本文不宣称已复现控制偏差。另一个结论是:returned 仅代表提交硬件 FIFO,不能解释为车端已确认接收。

读出侧还有一个实现细节:comms_can_read() 用自定义 memcpy() 把残包向左搬移。当前实现是正向复制,因此该方向可工作;但换成标准库 memcpy 后,重叠内存没有可移植保证。

CAN FD 位:主机两条路径的差异

原生 pandad connect() 给三路总线打开 canfd_auto,而原生 pack_can_buffer() 不显式置 FD 位。板端 process_can() 据 canfd_auto ? canfd_enabled : to_send.fd 决定 FDF,其中 canfd_enabled 在收到 CAN FD 帧后才设置。因此在首次收到 FD 帧之前,原生主机候选帧的 FDF 取决于板端尚未观察到 FD 的状态;即使 DLC 表可编码 12–64 字节,也不能据此断言硬件已经按 CAN FD 发送。

Python 侧相反:Panda.connect() 关闭自动切换,pack_can_buffer(..., fd=...) 明确设置首位。此外固件 0xdd 暴露的 CAN 包版本由 can.h 原文字节 SHA-256 前四字节形成;Python 的 can_send_many()/can_recv() 用装饰器比对这个值,而原生 C++ pandad 不显式调用 0xdd,主要依赖固件签名一致性门。该哈希不是对主机 can_header 源文件或运行时硬件 FD 状态的证明。

插件化边界

源码 test_comms.py 有半包 reset、随机 64 字节块写入和 5 万条接收帧重组用例,但这些用例在 allOutput 模式下验证主要组包行为,没有直接证明坏 XOR 帧经过车型安全钩子前后状态不变,也不覆盖中断并发、物理 CAN 时序或全部车型许可分支。

对自制硬件或「万物即插件」重构,主机 CAN 流适配、分块与遥测可以插件化,但需要在受信任入口先验证包长、DLC、bus、XOR/传输完整性,再交给有状态的安全策略;最终发送仍须固件独立许可。插件不得通过原始 USB/SPI 端点绕过验证,也不能把 returned 回执解释为车端已确认接收。

常见问题

为什么说 XOR 校验晚于安全钩子?

因为完整帧进入 comms_can_write() 后直接调用 can_send(),而 can_send() 先运行 safety_tx_hook(),之后才入 TX 队列;can_check_checksum() 要等到 process_can() 从队列取出时才执行。所以坏 XOR 帧不会上总线,却可能先触达有状态的安全钩子。

6 字节头和 72 字节结构到底哪个上总线?

线格式只传 6 字节头加 DLC 对应的有效载荷,最长 70 字节;data 偏移为 6。带 packed, aligned(4) 的完整 C 结构在本机编译为 72 字节,但其尾部对齐字节不应传输。

returned 回执能说明车端收到了吗?

不能。returned 仅代表已提交硬件 TX FIFO,不等于车端 ECU 已确认接收。源文档明确要求不要把它解释为车端确认。

为什么主机两条路径的 CAN FD 行为不同?

原生 pandad 给三路总线打开 canfd_auto,且原生 pack_can_buffer() 不显式置 FD 位,FDF 由板端在收到 FD 帧后才置的 canfd_enabled 决定;Python 的 Panda.connect() 关闭自动切换,由 pack_can_buffer(..., fd=...) 明确设置首位。新硬件须分别验证这两条路径及冷启动。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/PANDA_CAN_COMMS_FRAGMENT_ASSEMBLY_DEEP_DIVE.md(取用了 CANPacket_t 字节布局、组包函数契约、安全钩子与 XOR 的真实顺序、CAN FD 位差异与插件边界)。
  • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 can_comms.h 调用 can_send(..., false) 与 safety_tx_hook 的衔接位置)。

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

更新记录

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注