诊断配错,没有栈回溯
ECU 的其它层出错时都会说一声。指针错了会 trap,模块漏了链接不过,配置写错启动就抛异常。诊断层不会。ECU 正常启动,诊断仪正常连上,会话正常进入 —— 然后某个请求回来一个 0x31 requestOutOfRange;或者某个 DID 回来的数据比规范写的短三个字节,而工具会带着标签把垃圾数据好好显示给你。
原因在结构上。诊断这件事由两份文件描述,而且是两批人在两个工具里做的:
| 产物 | 是什么 | 谁维护 |
|---|---|---|
| CDD | 诊断规范。诊断仪照着它实现 | 诊断工程师,CANdelaStudio |
| ARXML | ECU 配置。ECU 实际运行的就是它 | 基础软件工程师,DaVinci / EB tresos |
它们之间没有任何东西做比对。没有构建步骤,没有一个能覆盖两者的 schema,也没有任何 CI 会注意到。CDD 里改了一个诊断地址,ECU 会永远监听着旧的那个 —— 因为流水线上没有任何一环把这两份文档联系起来。
崩溃会告诉你去看哪里。诊断地址错了什么都不告诉你:诊断仪说「无响应」,而这话在 ECU 没上电、线束有问题、收发器睡死、网关把帧滤掉时也一模一样。于是集成调试从物理层开始查起,真正的原因是一个文本文件里的数字。
同一个数字,两处来源
讲 bug 之前先讲结构。CDD 里的诊断地址有两处来源,这个区别比看起来重要得多:
<!-- 1. <ECU> 下的属性实例 —— 这是本 ECU 的配置 -->
<ECU id='_ECU'>
<QUAL>Some_ECU</QUAL>
<UNS attrref='_ATTR_REQ' v='1848'/> <!-- 0x738 -->
</ECU>
<!-- 2. DEFATTS/ECUATTS 下的属性定义 —— 这是模板默认值 -->
<DEFATTS><ECUATTS>
<UNSDEF id='_ATTR_REQ' v='1792'> <!-- 0x700,占位值 -->
<QUAL>CAN.ReqCanId</QUAL>
</UNSDEF>
</ECUATTS></DEFATTS>
第 1 类是本 ECU 的配置。第 2 类是 CANdelaStudio 建模板时写进去的,无论有没有人填过 CAN 页它都在。解析器读的是第 1 类。所以当一个项目是「模板另存」而来、CAN 页从没填过时,属性表里一个 CAN 项都没有 —— 而文件本身解析得好好的,53 个 DID 在、129 个 DTC 在、例程一个不少。
这个组合就是 bug 能活过一整个版本的原因:文档结构完全合法,唯一缺的那部分,恰好是在 DID 列表全中时没人会去看的部分。
八个静默跳过的检查
比对代码原本长这样。请再读一遍中间那两行 —— 那就是整个 bug:
for code, sev, attr, role, label in id_map:
cdd_val = cdd.attrs.get(attr)
if not isinstance(cdd_val, int):
continue # <-- 读不到值?跳过这个检查。什么都不说。
arx_val = arx.canif_ids.get(role)
...
三个寻址检查(CDX-010/011/012)和五个时序检查(CDX-020 到 CDX-024)用的是同一个守护条件。没有 CAN 属性时,八个全部命中 continue。报告回来时有 DID 的结论、DTC 的结论、例程的结论,外加一个小巧整洁的汇总 —— 通篇没有一处提到 CAN ID。
| 用户看到的 | 实际含义 |
|---|---|
| 报告里没有 CAN ID 相关的结论 | 要么地址一致,要么根本没查 |
这两种状态在终端上是同一个字符串。53 个 DID、129 个 DTC、4 个例程全中的一次运行,读起来就是一份完整的评审。而实际上那份 CDD 里三个必需的 CAN 地址一个都没配 —— 躺在那儿的是模板占位值,那条总线上没有任何 ECU 会监听它。
更糟的是那些占位值不是随机的:0x700 / 0x600 / 0x750,看起来完全像个正经地址,没人会去质疑。而拿这份 CDD 导出 ODX,得到的诊断仪会把每个请求都发往 0x700。
一个不能选的修法
最顺手的修法是「读不到实例就用模板默认值,照常比」。不要这么做。那会对一个并非本 ECU 地址的数字给出自信的结论 —— 而一个自信的错误结论比一句「没查」更糟,因为读的人会照着去改。
所以缺口本身变成了一条结论。两个新检查码:
| 码 | 级别 | 触发条件 |
|---|---|---|
CDX-013 |
error | 三个诊断 CAN 地址有任一未配置 |
CDX-025 |
warn | P2 / P2* / S3 / STmin / Blocksize 有任一未配置 |
两条都点名说清是哪几个检查没跑。同时模板默认值被挪到单独的字段 —— attrs_template 而不是 attrs —— 这样它在下游永远不可能被当成配置。这个分离才是真正的修复,两个检查码只是让它可见。

哪一边错了
这部分才是这个工具值得写的原因。一个 diff 能告诉你 CDD 写的是 0x738、CanIf 写的是 0x72F。它不能告诉你该改哪个,而读这份报告的人通常只拥有这两个文件中的一个。
裁决证据在 ARXML 里:硬件验收滤波器。CAN 控制器不是把所有帧都交给软件,它按 CanHwFilterCode 与 CanHwFilterMask 过滤,一帧能通过的充要条件是:
(frame_id & mask) == (code & mask)
把两个候选 ID 分别对配置里所有滤波器求值。三种结果,其中只有一种允许下结论:
| 滤波器状态 | 结论 |
|---|---|
| 接受 CDD 的、不接受 CanIf 的 | CanIf 错了 |
| 接受 CanIf 的、不接受 CDD 的 | CDD 错了 |
| 两个都接受(mask 有无关位)/ 都不接受 / 没有滤波器 | 无法裁决 —— 明说,不猜 |
在一份真实工程里,51 个滤波器中只有一个能过,mask 是 0x7FE。有一位无关位,所以 0x738 和 0x739 都会落进那个邮箱 —— 而 0x72F 不会:
0x738 & 0x7FE = 0x738 == code 0x738 -> 接受
0x72F & 0x7FE = 0x72E != code 0x738 -> 拒绝
所以控制器只会把 0x738 的帧交上来。那条配成 0x72F 的 CanIf PDU 描述的是一个根本到不了软件的地址 —— 结论不是「两个文件不一致」,而是「把 CanIfRxPduCfg_Phys_Req.CanIfRxPduCanId 改成 0x738」。
CanTp 自己也带地址:N_SA 与 N_TA。那个工程里解出来是 56,也就是 0x38 —— 正是 0x738 的低字节。0x72F 的低字节是 0x2F(47),全文件里找不到。两份由不同人生成的产物都与 CDD 一致、与 CanIf 不一致。
一个诚实的补充,因为工具最初在这里说过头了。那个工程的 CanIf 软硬件滤波是关掉的(CanIfHrhSoftwareFilter = false),所以 CanIf 不在软件层二次校验 ID。因此那个错值是潜伏缺陷而不是立即失效:今天靠侥幸能跑,等哪天有人打开软件滤波、换了 HRH 路由方式、或者在集成测试里开了断言,就断了。
Phy 不是 phys
CanIf 的 PDU 命名一家一个样,所以按关键词识别角色,不按期望的名字。第一版找的是 phys:
if 'func' in name.lower(): role = 'func_req'
elif 'phys' in name.lower(): role = 'phys_req'
elif 'resp' in name.lower(): role = 'resp'
然后来了一份工程,PDU 叫 RX_PDU_UDS_DiagPhyReq_XXX。'phys' in 'diagphyreq' 是 False。于是物理请求 PDU 从来没被找到,工具报「CDD 定义了物理请求 CAN ID,但 CanIf 里找不到对应的诊断 PDU」—— 而那个 PDU 就躺在文件里,名字短了三个字母。
这个误报的方向最坏。漏报只是漏掉一个真缺陷;误报会让人去补一个并不缺的 PDU,补完就有两个了。
改成同时匹配 phy / phys / physical。一行改动,但这类 bug 只在第三个供应商的命名习惯出现时才暴露 —— 而那时候工具已经被信任了好几个月。
怎么证明「0 error」是真的
一个什么都不报的检查器,和一个坏掉的检查器,在输出上无法区分。唯一的分辨方法是埋已知缺陷,确认还能抓到 —— 更关键的是确认「真的通过了」和「跳过了」产生不同的输出。
所以自检有两组样例:
| 样例 | 断言 |
|---|---|
| 配齐全 + 埋 11 类缺陷 | 6 error / 4 warn / 3 info,13 个预期码全部触发 |
| 模板另存、CAN 页清空 | 必须触发 CDX-013 / CDX-025;不许触发 CDX-010/011/012/020/021/022(没有可比对的属性时不该报不一致) |
第二组还断言 attrs 必须为空、attrs_template 必须有值。没有这条,修法本身会退化回「把模板值读进 attrs」—— 那样前两条断言照样通过,而「自信的错误结论」问题原样回来。
样例之外再做注入测试:手工把 8 个正确属性写进那份未配置的 CDD,确认 CDX-013/025 消失;再把物理请求改成 0x700,确认 CDX-010 出现且裁决方向是「CDD 错了」。两个方向、一份文件、三十秒 —— 同时验证「检查没死」和「裁决逻辑没反」。
两份真实工程
这个 bug 不是读代码读出来的。是在第二份工程上跑的时候发现的 —— 那份 CDD 恰好是模板另存、CAN 页空着 —— 而触发点是我注意到报告有点不对劲。
发现问题的不是某个检查,而是 probe 的输出和上一次不一样:本该有 诊断参数 CAN.ReqCanId=… 的那一行整个消失了。而「这行不见了」和「这行是空的」在屏幕上没有区别 —— 这正是 probe 现在会显式打印缺口的原因。
| 工程 | 修复前 | 修复后 |
|---|---|---|
| CAN 属性基本齐全,缺 Blocksize | 1 error / 1 warn | 1 error / 2 warn —— 新的 CDX-025 如实说 CDX-024 没跑 |
| 模板另存、CAN 页全空 | 2 error / 2 warn | 3 error / 3 warn —— CDX-013 + CDX-025 出现 |
第一行值得盯着看。修复前它报 1 warn,看起来没事。修复后它报 2 warn,其中一条说某个检查没跑。配置没变,变的只是报告的诚实度。
第二份工程里到底有什么
除了地址没配,还有三条普通的结论,每一条都能省掉一次台架调试:
| 码 | 结论 | 台架上的表现 |
|---|---|---|
CDX-006 |
3 个例程在 CDD 有、Dcm 没有 | 31 01 FF00 擦除内存失败 |
CDX-031 |
安全等级 2 只在 CDD 有 | 受该等级保护的服务永远解不开 |
CDX-002 |
3 个 DID 在 Dcm 有、CDD 没有 | 不是 bug —— 要判断它们是否该在售后可读 |
最后一行才是重点。那三个 DID 的名字是 SystemSupplierInternalECUHardware、ReadOnlyTest 这种。两个是 OEM 保留标识,本来就不该出现在售后规范里;一个是配置工具自带示例工程的遗留。**正确动作很可能是从 Dcm 删掉,而不是补进 CDD。**
「Dcm 有、CDD 没有」听起来像是 CDD 落后。有时恰恰相反:ECU 暴露了不该暴露的东西。工具报的是不对称,归属问题留给工程师 —— 只有他知道哪份文档是权威。
查不出来的
25 项检查通过不等于诊断没问题,而检查类工具最典型的失效方式就是用户不再读注意事项。所以注意事项是每份报告的一部分,不是埋进文档里。
| 未覆盖 | 为什么重要 |
|---|---|
| DID 的数据长度与结构 | 两侧都有也能错:回来的是错的长度 |
| 服务对会话与安全的依赖 | 服务配了,但从它该在的会话里进不去 |
| 否定响应码的使用范围 | 只统计两侧清单,不校验谁该回哪个 |
| 安全访问算法 | 只比 Seed/Key 长度,不比算法 |
| 快照与扩展记录字段 | 只比 DTC 是否存在,不比记录内容 |
| 配了的值对不对 | 只能判「配了没配」,判不了「配得对不对」 |
最后一行是这套方法的诚实边界。如果 CDD 写 0x738、滤波器也写 0x738,而车上实际用的是 0x739,那么在这两份文件之间怎么交叉校验都发现不了。交叉校验发现的是「不一致」,不是「错」。
成果与可复用的方法论
python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py probe ecu.cdd ecuconfig.xml
python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py check ecu.cdd ecuconfig.xml
python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py matrix ecu.cdd ecuconfig.xml -o inv.csv
python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py selftest
退出码 0 / 1 / 2 —— 无错误、有错误、参数或文件错误 —— 所以能直接进 CI,不用包一层。25 个检查码,10 error / 12 warn / 3 info。纯标准库、只读、Python 3.8+。
这里有四条能推广到本工具之外:
| 原则 | 为什么 |
|---|---|
| 跳过必须和通过长得不一样 | 否则解析器坏了和配置干净会产生同一份报告 |
| 绝不静默回落到默认值 | 默认值会对一个并非真实的数字给出自信的结论 |
| 先找到裁决证据,再写结论 | 没有裁决的 diff 会让读的人猜该打开哪个文件 |
| 给「不该报的」也写断言 | 空 CAN 页那组样例,抓的是过度报错和修复本身的退化 |
源码与 issue:github.com/autosar-skills/autosar-agent-skills · MIT · 全部实现就是一个标准库 Python 文件加两组样例。以上示例全部为合成数据,不含任何真实车型、供应商或客户标识。
常见问题
CDD 和 ARXML 为什么会不一致?
它们是两批人在两个工具里维护的:CDD 是诊断规范,由诊断工程师在 CANdelaStudio 里写,用来生成诊断仪;ARXML 是 ECU 配置,由基础软件工程师在 DaVinci 或 EB tresos 里配,编译进 ECU。两者之间没有任何构建步骤做比对,一边改了另一边不会跟着变。表现就是编译不报错、诊断仪能连上,然后 ECU 回 0x31 requestOutOfRange。
CAN ID 不一致时,怎么判断该改 CDD 还是改 ARXML?
用硬件验收滤波器做裁决。读 CanHwFilterCode 与 CanHwFilterMask,按 (frame_id & mask) == (code & mask) 求值。滤波器接受 CDD 的 ID 而不接受 CanIf 的,说明芯片只会把 CDD 那个 ID 的帧交给上层,错的是 CanIf;反之错的是 CDD。两个都接受或都不接受时,明确说无法裁决,不猜。
报告 0 error 是不是就说明诊断配置没问题?
不是。它只说明这 25 项检查没查出问题。DID 的数据长度与结构、服务与会话和安全的依赖矩阵、否定响应码的使用范围、安全访问算法、快照记录字段内容都不在覆盖范围内。更重要的是:工具必须能区分「检查通过」和「检查没跑」—— 如果一个 CDD 连 CAN 地址都没配,那 CAN ID 相关的三项根本没执行,不能算通过。
CDD 里明明有值,工具为什么说没配置?
CDD 里的诊断地址有两处来源:<ECU> 节点下的属性实例是本 ECU 的配置,DEFATTS/ECUATTS 下的属性定义是 CANdelaStudio 的模板默认值。从模板另存新项目后如果没填 CAN 页,就只有模板定义存在。你看到的 0x700 / 0x600 / 0x750 是占位值,不是这个 ECU 的地址;拿这些值导出 ODX,诊断仪就会把每个请求都发往 0x700。
哪些诊断配置错误会在台架上直接失败?
三类最直接。第一,物理请求 CAN ID 不一致,诊断仪发的地址没有 ECU 监听,表现为完全连不上。第二,擦除内存或检查完整性的例程(0xFF00 eraseMemory、0xFF01 CheckProgrammingDependencies)在规范里有而 Dcm 里没配,刷写序列走到这一步会收到 0x31。第三,安全访问等级只有 CDD 定义而 Dcm 里没有对应的 DcmDspSecurityRow,所有受该等级保护的服务永远解不开锁。