结论:Ford 的 fordcan.py 只负责生成候选帧:横向、纵向、界面与按钮四类构帧函数把 CarControl 变成带计算器/校验的 CAN 载荷,是否真正上总线由独立固件白名单与继电器门裁决。CAN FD 校验 calculate_lat_ctl2_checksum() 是「原始位求和取反」的专用算法,不是通用 CRC;而函数注释里的 33/20/50/5/1 Hz 只是名义频率,不能当作总线最大负载预算。
本文基于固定源码快照,从 fordcan.py 的公开构帧函数出发,追踪 CarController.update() 与 panda 安全钩子,属于静态结构分析,未连接车辆或发送 CAN。完整固件侧规则见 Ford panda safety 专题,本文只解释主机侧「构帧了什么、依赖了什么」。
构帧函数与报文契约
四类输出分工明确:横向有 create_lka_msg()(空 Lane Assist)、create_lat_ctl_msg()(经典 CAN 曲率与路径)、create_lat_ctl2_msg()(CAN FD 曲率、计数、校验);纵向是 create_acc_msg();界面与按钮是 create_acc_ui_msg()、create_lkas_ui_msg()、create_button_msg()。它们只产出候选帧,不提供安全许可。
| 函数 | 控制器调用 | 输出与关键限制 |
|---|---|---|
create_lka_msg() |
33 Hz 名义节拍 | 对 Lane_Assist_Data1 传空信号字典;panda 要求其动作字段必须为零,不能当额外转向通道 |
create_lat_ctl_msg() |
经典 CAN 分支 | 构造 LateralMotionControl,LatCtl_D_Rq 随横向活动位切换;曲率取限幅后符号反转值 |
create_lat_ctl2_msg() |
CAN FD 分支 | 先以校验字段零值打包同名帧,算完校验再以实际 CAN.main 构帧;计数器按节拍 modulo 16,当前只用 mode=0/1 |
create_acc_msg() |
纵向控制分支 | ACCDATA 带加速度、油门、预充/减速、停车与目标速度;仅自控纵向配置下按名义 50 Hz 发送 |
create_acc_ui_msg() |
5 Hz 或 UI 变化 | 从相机 ACCDATA_3 复制多项字段,再覆盖 TJA 状态、自控纵向显示与 FCW 告警 |
create_lkas_ui_msg() |
1 Hz 或 UI 变化 | 从相机 IPMA_Data 复制配置/告警字段,再改写车道线状态与握盘提示 |
create_button_msg() |
按钮分支 | 从主总线 Steering_Data_FD1 复制开关状态,只覆盖 cancel/resume/TJA 三个位;取消和恢复同时送 camera/main,TJA toggle 只送 camera |
CanBus 定下总线映射:main=offset、radar=offset+1、camera=offset+2,基类以 len(CP.safetyConfigs) 推导 4 路一组的偏移。不能硬编码所有车辆为 bus 0,偏移取决于安全配置数量。
CAN FD 校验:专用算法,不是通用 CRC
calculate_lat_ctl2_checksum() 从已编码的 8 字节中抽取曲率、曲率率、路径角、路径偏移的原始位值,加模式和计数器,再取 0xFF - (sum & 0xFF)。它与固件侧从相同位置提取字段的做法对应,所以构帧器依赖所选 DBC 的位布局与物理总线映射——把该函数当成跨平台字节数组插件会失效。
两步打包的原因也在这里:先打包零校验占位帧是为了让校验字段取值稳定,计算后再用实际 CAN.main 构帧。计数器和校验只保证字节自洽,不保证 ECU 接受,也不替代安全门。
| 环节 | 做法 | 边界 |
|---|---|---|
| 校验算法 | 原始位求和后取 0xFF - (sum & 0xFF) |
专用算法,非通用 CRC;依赖 DBC 位布局 |
| 计数器 | 按发送节拍 modulo 16 | 节拍来自 100 Hz 控制帧计数与状态变化时的额外发送 |
| 模式字段 | 当前只用 mode=0/1 |
不代表其余模式在实车被验证 |
横向与纵向:控制器负责限幅,构帧器不查许可
CarController 在横向发送前计算当前曲率、按车速施加曲率误差/速率限制,然后对 CAN FD 与经典 CAN 分别构帧。两条路径的 path_offset、path_angle、curvature_rate 在当前调用中都传零——源码中对多项式完整操控的描述不是当前已启用能力。
纵向方面,自控纵向的 gas 和 accel 分开限幅;若非 longActive 或油门低于门槛,油门设为中性原始值。制动预充/减速请求根据坡度补偿后的加速度在 >0.3 与 <0 之间迟滞,位状态由控制器记忆。构帧器不自行检查这些状态是否与 panda 许可一致:限幅是主机策略,许可在固件。
两种 UI 构帧和按钮构帧从 CarState.update() 缓存的原车解析字典复制字段。字典存在不等于所依赖 CAN 帧足够新鲜;插件接口应保留总线、时间戳和健康信息,并在台架验证失联后的透传行为。panda 对 ACCDATA_3/IPMA_Data 有白名单但无逐字段专属限制,对按钮仅专门约束 cancel/resume 权限。
名义频率不能当负载预算
fordcan.py 的 docstring 所列 33/20/50/5/1 Hz 是名义频率,实际来自 100 Hz 控制帧计数和状态变化时的额外发送。create_button_msg() 自身写 10 Hz,但 cancel 可以随每次控制更新发送。不能拿函数注释当作总线最大负载预算。
新硬件或插件化验收至少要分别验证:车型 flags、CAN FD/经典 DBC、总线偏移、帧长度、计数器/校验、原车 UI 字段的新鲜度,以及 panda TX 拒帧。Ford 构帧插件只能交付候选帧;白名单、曲率/纵向限值及原车 AEB 禁止位必须继续由独立安全层执行。
固定快照附带两份离线探针用于核对契约:原版 DBC 构帧探针把经典 CAN 与 CAN FD 两种格式各验证一次,界内帧通过、主机速度突变候选被拒;此前的手工帧探针用正的曲率 CAN 单位测试对称安全限值,但控制器实际在调用 fordcan 前将曲率取负,原版探针按该符号链送入 CANPacker,并包含 DBC 精度位与 CAN FD 校验。需要记住边界:它未覆盖 CarController.update() 的前置测量裁剪、状态迁移或车辆 ECU 响应。
常见问题
calculate_lat_ctl2_checksum 是 CRC 吗?
不是。它从已编码的 8 字节中抽取曲率、曲率率、路径角、路径偏移的原始位值,加模式和计数器,再取 0xFF - (sum & 0xFF),是与固件同位置取值对应的专用算法,不是通用 CRC,也不能当作跨平台字节数组插件。
Ford 的 CAN 总线一定是 bus 0 吗?
不是固定值。CanBus 用 main=offset、radar=offset+1、camera=offset+2 映射,偏移由基类按 len(CP.safetyConfigs) 以 4 路一组推导,所以不能硬编码所有车辆为 bus 0。
控制器已经限幅,为什么还要固件安全门?
因为构帧器不检查状态是否与 panda 许可一致。主机侧只负责曲率/纵向限幅与状态记忆,白名单、曲率与纵向限值、原车 AEB 禁止位仍由独立固件层裁决;构帧器只交付候选帧。
函数注释里的发送频率能当负载预算吗?
不能。33/20/50/5/1 Hz 是名义频率,实际来自 100 Hz 控制帧计数和状态变化时的额外发送;create_button_msg() 虽写 10 Hz,cancel 仍可随每次控制更新发送。
延伸阅读
- Ford panda safety 模式:白名单、曲率与纵向限值 —— 补齐本文只字未提的固件侧 TX 规则。
- CAN 报文解析的「健康位」在说什么:校验、计数器与总线空窗 —— 解释 UI 构帧依赖的解析字典为何还要看新鲜度。
- 同一个「转向灯」在三家车厂是三种实现:车辆状态归一化的坑 —— 说明原车字段复制为何必须按品牌区分。
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 承接「主机编码之后仍由固件独立许可」。
参考资料
- 项目 02 ——
openpilot-research/FORD_CAN_FRAME_CONSTRUCTION_CONTRACT_DEEP_DIVE.md(取用了fordcan.py构帧函数契约、CAN FD 校验算法、总线映射与名义频率边界)。 - 项目 02 ——
openpilot-research/FORD_PANDA_SAFETY_MODE_DEEP_DIVE.md(仅用于确认固件侧白名单与限值边界,未取用其未公开内容)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复