ZZBX / NOTES ← 全部笔记
AutoSAR · Agent Engineering · Python

105 个错误,
99 个是假的

给 AI 写一个 AUTOSAR 通信栈 ARXML × DBC 交叉校验工具。第一版在真实工程上报了 105 个错误,逐条核实后 99 个是假的 —— 那是一份描述 11 个节点的整网矩阵,绝大部分报文跟本 ECU 无关。假报比漏报更害人:没人会读第一百行之后的内容。

2026-11-05 ◆ 约 12 分钟 ◆ Python / stdlib ◆ 源码开放
26个检查码
0第三方依赖
3份真实工程
0.31s跑完 5.6 MB

为什么这层没人校验

AUTOSAR 的通信栈由两份文件描述,而且是两批人在维护:供应商交付的 ECU 配置 ARXML(5.6 MB、23 个模块、几万个 ECUC 容器),和整车通信矩阵 DBC。两者必须对得上:帧号、长度、每个信号的起始位、字节序、初始值、收发方向。

对不上会怎样?编译期一句话都不会说。我见过最典型的一类:起始位差了三位,信号被解到隔壁信号的位上。收发双方都运行得好好的,帧也能正常解析,但那个值永远是错的 —— 你在任何日志里都看不出来,因为它看起来是完全正常的数据。

格式 写错了会怎样
JSON解析失败,报行号
XML有 schema 可以校验
YAML缩进错会报
ARXML / DBC工具全都能打开,报文照发,数值全错

所以第一步是找现成的轮子。没有。canmatrix 只读系统描述 ARXML(CAN-CLUSTER/FRAME),从不碰 ECUC 配置;cogu/autosar 只是 parser;本地 AUTOSAR lint 有 Com 规则但不做 DBC 交叉校验。没人做这件事,因为 ECUC 结构一家一个样,写通用解析器是苦活。

真实工程上的校验输出
一行命令,帧数、ECU 节点、总线覆盖率、逐条缺陷一次给全
约束

单文件、纯标准库、只读。最后一条比看起来重要 —— 一个能改 ECU 配置的工具,迟早会真的改坏一次。

位几何:我一开始推错了方向

这是整件事里最值得写全的一段。AUTOSAR 里信号的起始位叫 ComBitPosition(或 START-POSITION),DBC 里是 SG_ 后面的第一个数字。看起来是同一个东西,直接比就行了 —— 不行。

我第一版按「两种约定都存在」的假设做:MSB 派和 LSB 派,两种都算,任一命中就通过。当时觉得这样稳健 —— 宽容一点,误报少。然后在真实文件上测了一下。

公式 命中
锯齿 S+L-1 / 平坦 [S, S+L)153 / 214(71.5%)
锯齿 S / 平坦 [S-L+1, S]33 / 214(15.4%)

两种都不高,说明假设本身有问题。于是按 PDU 分组统计,看是不是某些 PDU 用一派、另一些用另一派。Scene_Sync 100% 符合锯齿,Node_Info、Time_Info 100% 符合平坦 —— 看起来真的有两派。

然后我发现自己犯了个低级错误:那几个 PDU 里,一个是小端信号,一个是大端信号。我把大小端的差异,误读成了约定差异。

真相

AUTOSAR 的 ComBitPosition 存的始终是信号的 LSB,按平坦编号(bit0 = byte0 的 LSB),跟大端小端完全无关。DBC 对大端信号用锯齿编号,而且起始位写的是 MSB。所以换算只有一件事:先回退 len-1 步求出 MSB,再按锯齿走完整个信号。

改完之后:215 / 215,零失败。而之前那个「两派共存」的观察也消失了 —— 从来只有一套公式,只是方向写反了。

必须自我批评的决定

「两种都试、任一命中即通过」是错的,两条理由:它会把真错误洗白 —— 起始位真写错时,另一种解释偶尔也能巧合命中;它基于一个假前提 —— 两派根本不存在。宽容的校验器不是好校验器,它让你感觉良好,代价是放过真问题。

105 个错误里 99 个是假的

工具写完,第一份真实工程:0 error, 16 warn。第二份,换了一家、换了颗芯片:5 error,都是真问题。第三份是整张网络矩阵,91 帧、11 个节点 —— 跑出来 105 个 error。

我第一反应是「这份配置烂透了」。然后我把「CanIf 里找不到对应 PDU」的帧按发送方归了个类:

NodeA     22 帧
NodeB      7 帧
NodeC      6 帧
NodeD      4 帧
NodeE      2 帧
...
ECU_R      1 帧   ← 只有这个是本 ECU 发的

看明白了吗 —— 91 帧里绝大部分是「A 节点发给 B 节点」的报文,跟本 ECU 一点关系都没有。而我的代码假设:DBC 里每一帧都必须在本 ECU 的 CanIf/Com 里有配置。这个假设只对「单 ECU 视角」的 DBC 成立 —— 那种 DBC 只有十几帧,BU_: 里基本都是本 ECU。前两份恰好都是这样,所以才完全暴露不出来。对整网矩阵,这个假设本身就是错的:与本 ECU 无关的帧,缺配置是正确状态,不是缺陷。

修法是逐帧判断「本 ECU 到底参不参与」:发送节点是本 ECU,或该帧任一信号的接收方列表含本 ECU。都不是就跳过。结果 105 → 61,那 6 个真缺陷这才露出来。

按检查码归并的错误统计
修复后 61 error。修复前 105,其中 99 条是假报
修复背后的 bug

这个判断依赖信号的接收方列表,也就是 SG_ 行末尾那段:"" ECU_L,ECU_R。我的代码是 split(),按空格拆。而 DBC 规范用的是逗号。所以拆出来是 ['ECU_L,ECU_R'] —— 一个元素。那个判断从写下来那一刻起就是空转的:它永远返回「不是接收方」,于是每一帧都走进「无关」分支。

如果我没顺手核对这一行,整个修复等于没做 —— 只是把 105 改成了 61。任何「看起来生效了」的修复,都要单独验一次它的输入。

跳过,看起来和通过一模一样

这个坑最阴险,因为它藏在成功里。我拿一个空 DBC 去试健壮性:

summary: 0 error, 0 warn, 53 info

0 error。绿色的。原因链条:DBC 零帧 → 所有总线命中率 0% → 全部判为「不在本 DBC 覆盖范围」→ 整体跳过 → 什么都没查,但报告是干净的。拿错文件给供应商,会得到一份「全绿」报告。

错误输入必须失败
AXD-023:没查就得失败,不能报成功

新增 AXD-023,两种情况都强制失败:DBC 里没有任何帧;没有任何 PDU 落在覆盖范围内。「跳过」和「通过」必须在报告里长得完全不一样。这和第二节那个「两种都试」是同一个毛病的两种表现 —— 都在用「看起来没问题」冒充「检查过了」。

六个只在真实文件暴露的 bug

自造 demo 数据测了无数遍都是绿的,一上真实文件全崩。

Bug 后果
参数标签是 -PARAM-VALUE,我按 -PARAMETER-VALUE 找 所有 CAN ID / DLC / 长度读成 None,但 probe 仍显示容器数量,极易误判成「文件没配」
ComSignal 名带 _IPDU_<PDU名> 后缀没剥 226 条假「信号缺失」+ 252 条假「信号多余」
按名字直连匹配 Com ↔ CanIf 真实拓扑不成立
单节点方向判定 一个 MCU 管左右两路,对侧帧全被误判成方向错
位序公式方向写反 见第二节
TX PDU 的 DLC 从 CanIf 取 该厂商 TX 压根不配 CanIfTxPduDlc,必须回 EcuC 的 PduLength

按名字匹配这条特别值得说。真实配置里 Com 和 CanIf 不直连:DBC 帧 → CanIf PDU → EcuC Pdu → PduR 路由 → EcuC Pdu(COM) → Com IPdu。IPDU_COM_TP_Param 和 DBC 帧 Diag_CalData 名字毫无关系,但它们通过 COM_TP_Param → CanTp_TP_Param → 0x9C 是同一条链路。按名字匹配必然全错。

怎么证明「0 error」是真的

工具报 0 error,有两种可能:真的没问题;或者阈值放太宽,什么都抓不到。从报告上完全看不出区别。所以往真实文件里埋已知错误,看能不能抓到。

[E] AXD-003  TX_PDU_ECU_L_St DLC 与 DBC 不一致:CanIf=8B / DBC=32B
[E] AXD-008  IPDU_COM_Node_Info.CarMode 位几何不一致

两个都抓到了。这才叫「0 error 是有意义的」。同样的思路用在自检上:断言的不只是「哪些检查码被触发」,还有每个级别的条数 —— 因为我一开始只断言了码集合,示例数据漂了、条数变了,自检照样打印「自检通过」。

规律

这个项目里出现过三次「测试报成功,但检查的比我以为的少」:只断言码集合、按空格拆接收方、空 DBC。三个都是同一个形状。

让一个新会话自己去用

工具写完、文档也写了,但这不等于 agent 会用。我把 skill 装好,起一个全新的会话,只给两个文件路径和一句话,不给任何提示,看它会不会自己找到 skill、读 SKILL.md、按顺序执行、调已安装的副本(而不是我仓库里那份)、并报出版本 sha256。

它全做对了,输出和我的手跑一致。更有价值的是:它反过来抓出了我文档里的 4 个问题。

问题 性质
文档写「期望 8 error / 3 warn」,实际 13/2/1 实质:自检没断言条数
文档承诺汇报总线覆盖率,但代码根本不输出 实质:承诺了不存在的东西
示例写 --node LDM,是别的工程残留 误导
notes 为空没说明,容易被当成输出被吞 文档缺失

第二条最典型:我在 SKILL.md 里写了「汇报时必须原样转述总线覆盖率」,但代码根本没把这行打出来。任何照着文档做的 agent,都会被要求转述一个不存在的东西。文档承诺了而代码没做的事,比代码写错更隐蔽。

我自己的一个乌龙

文档里 AXD-016 的提示语我写的是「未配 FD 帧格式」。用户看了眼数据说:这几帧是 DBC 写错了,不是 ARXML 漏配。

确实 —— 四帧都是经典帧却要求 32 字节,而信号实际占满了 32 字节(最高 bit 255)。经典 CAN 上限 8 字节,所以「FD」这件事是事实,可疑的是这帧该不该在这个 ECU 的矩阵里。我那句提示语把责任直接指向了 ARXML,会让人改错方向。

现在提示语会现场从 Com IPdu 的信号实际 bit 占用算出 span,分两种情形说话:占满 >8 字节 → FD 是事实、错的是标记;这帧本就不该在 → 补标记没用,是 DBC 内容问题。同一个检查码,在不同证据下给不同指向,而不是固定甩锅。

还有个操作失误

同步那次改动时我把 cp 方向写反了,把代码改动覆盖回了旧版 —— 文档改了、代码没改。这种错误只有靠 diff -r 才能抓到。

成果与可复用的方法论

26 个检查码,覆盖 Com / PduR / CanIf / CanDrv 四层。单文件、纯标准库、约 2500 行。

工程 规模 结果
A21 帧,双 ECU 视角0 error, 17 warn, 47 info
B25 帧,另一颗 MCU5 error, 4 warn, 13 info
整网矩阵91 帧,11 节点61 error, 15 warn, 76 info

5.6 MB 的 ARXML,0.31 秒跑完,峰值内存 62 MB。验收:自检(码集合 + 条数)、契约测试(中英双语文档的检查码与实现一一对应)、注入已知错误、以及在清空环境变量且仓库不参与的情况下独立运行。

五条可复用的

1. 宽容的校验器不是好校验器,「两种都试」会让真错误被巧合命中洗白。
2. 「跳过」和「通过」必须在报告里长得不一样。
3. 自造 demo 必须用真实拓扑,否则自检验证的是一条不存在的路径。
4. 任何「看起来生效了」的修复,都要单独验一次输入。
5. 让一个新会话自己去用 —— 它能找出「文档承诺了但代码没做」的地方。

常见问题

为什么同一个项目里 ARXML 和 DBC 会对不上?

因为这是两批人在两个时间点分别维护的:供应商交付 ECU 配置 ARXML,主机厂发布整车 DBC 矩阵。 两者不是互相生成的,也没有任何构建步骤去比对 —— 一边改了,另一边不会跟着变。

AUTOSAR 的位位置和 DBC 的起始位怎么对应?

只有一个正确公式。AUTOSAR 的 ComBitPosition 存的始终是信号 LSB(平坦编号,与字节序无关); DBC 对大端用锯齿编号且写 MSB。从 LSB 回退 len-1 步就得到 DBC 起始位。 「两种约定都试、任一命中即通过」是错的 —— 真错误会被巧合命中洗白。

为什么第一版报了 105 个错误,真的只有 6 个?

因为那份 DBC 描述的是 11 个节点的整网,而检查器假设「每一帧本 ECU 都必须有配置」。 绝大多数帧属于别的节点,于是产生了一堆无从下手报错。改成逐帧判定 「发送方是不是本 ECU、或有信号的接收方含本 ECU」之后,99 条假报消失。

怎么证明「没报错」是真的,而不是根本没查?

往真实文件里注入已知错误,确认还能抓到;并且让「跳过」和「通过」在报告里长得不一样。 空 DBC 曾经报 0 error —— 零帧导致所有总线命中率为 0、全部判为「不在范围内」而跳过。 对一份根本没读过的文件出一份全绿报告,比报一堆噪音更糟。

Harry Zhao
汽车嵌入式 · AutoSAR BSW / 平台工具链

12 年汽车电子底层软件经验,从 ECU 底层驱动做到 AutoSAR CP 平台架构:编译流程、测试脚本、 Git 代码服务器、Jenkins CI/CD,以及 TC397 / S32K314 芯片级功能安全。 文中这些工具都来自真实的供应商配置评审。

工具只读。示例数据全部为合成,不含任何真实车型、供应商或客户标识。