ZZBX / NOTES ← All notes
AutoSAR · UDS · Agent Engineering

Not configured looks exactly
like no problem at all.

A diagnostic checking tool reported a clean-looking review over a CDD whose CAN addresses had never been filled in. Eight checks skipped themselves, and nothing in the output said so. This is the story of that bug, and of the one design decision that makes this tool different from a diff: it can tell you which of the two files is wrong.

2026-11-06 ◆ 14 min read ◆ Python / stdlib ◆ 25 check codes
25check codes
0dependencies
0.46son 5.6 MB
8checks that skipped

A diagnostic failure has no stack trace

Every other layer of an ECU tells you when it breaks. A pointer bug traps. A missing module fails the link. A wrong configuration throws at startup. The diagnostic layer does not do any of that: the ECU boots, the tester connects, the session opens, and then a request comes back 0x31 requestOutOfRange — or a DID answers with a payload three bytes shorter than the spec says, and the tool happily shows you garbage with a label on it.

The reason is structural. The two sides are written by different people in different tools:

Artefact What it is Written by
CDD The diagnostic specification. The tester is built from it. Diagnostic engineer, in CANdelaStudio
ARXML Dcm / Dem / CanIf / CanTp configuration. What the ECU actually runs. BSW engineer, in DaVinci / EB tresos

Nothing compares them. There is no build step, no schema that spans both, no CI that would notice. Change a CAN address in the CDD and the ECU keeps listening on the old one forever, because nothing in the pipeline connects the two documents.

Why this is worse than a crash

A crash tells you where to look. A wrong diagnostic address tells you nothing: the tester says "no response", which is also what it says when the ECU is off, the wiring is bad, the CAN transceiver is asleep, or the gateway filters it. Integration debugging then starts from the wire, and the actual fault is a number in a text file.

Two sources for the same number

Before the bug, the structure. A CDD stores diagnostic addresses in two different places, and the difference matters more than it looks:



  Some_ECU
       




          
    CAN.ReqCanId
  

Type 1 is configuration. Type 2 is what CANdelaStudio put there when the template was created, and it exists whether or not anyone ever filled in the CAN tab. The parser reads type 1. So when a project is created by saving a template and the CAN page is left empty, the attribute map comes back with no CAN entries at all — and the file still parses perfectly, still has 53 DIDs, still has 129 DTCs, still has every routine.

That combination is what made the bug invisible for a whole release: the document is structurally valid, and the only thing missing is the part nobody looks at when the DID list matches.

Eight checks that skipped in silence

The comparison block looked like this. Read the second and third lines again — they are the whole bug:

for code, sev, attr, role, label in id_map:
    cdd_val = cdd.attrs.get(attr)
    if not isinstance(cdd_val, int):
        continue          # <-- no value? skip the check. say nothing.
    arx_val = arx.canif_ids.get(role)
    ...

Three addressing checks (CDX-010/011/012) and five timing checks (CDX-020 through CDX-024) all guarded themselves the same way. With no CAN attributes, all eight hit continue. The report came back with DID findings, DTC findings, routine findings, and a small tidy summary — and no mention of CAN ids anywhere.

What the user sees What it means
Report has no CAN id findings Either the ids agree, or nobody checked

Those two states are the same string on a terminal. A run where all 53 DIDs, all 129 DTCs and all 4 routines matched reads like a complete review. In fact the CDD had 0 of the 3 required CAN addresses configured — and the template defaults that sat there instead were placeholder values that no ECU on that bus listens for.

Worse: the placeholder values are not random. They are 0x700 / 0x600 / 0x750, which looks plausible enough that nobody questions it, and exporting ODX from that CDD produces a tester that sends every request to 0x700.

The fix that is not a fix

The obvious repair is to fall back to the template value and compare anyway. Do not do that. It produces a confident verdict about a number that is not this ECU's address — and a confident wrong verdict is worse than a stated gap, because the reader will act on it.

So the gap itself became a finding. Two new codes:

Code Severity Fires when
CDX-013 error Any of the three diagnostic CAN addresses is unconfigured
CDX-025 warn Any of P2 / P2* / S3 / STmin / Blocksize is unconfigured

Both name the checks that did not run. And the template defaults are kept in a separate field — attrs_template rather than attrs — so they can never be silently mistaken for configuration downstream. That separation was the actual fix; the two codes only make it visible.

Check output when the CDD is fully configured
Fully configured CDD: the addressing checks now run and name the parameter to change

Which side is wrong

This is the part that makes the tool worth writing. A diff can tell you that the CDD says 0x738 and CanIf says 0x72F. It cannot tell you which one to change, and the person reading the report is usually the owner of exactly one of those two files.

There is a tie-breaker, and it is in the ARXML: the hardware acceptance filter. The CAN controller does not deliver every frame to the software — it filters on CanHwFilterCode and CanHwFilterMask, and a frame passes only when:

(frame_id & mask) == (code & mask)

Evaluate both candidate ids against every filter in the configuration. Three outcomes, and only one of them licenses a verdict:

Filter state Verdict
Accepts CDD's id, rejects CanIf's CanIf is wrong
Accepts CanIf's id, rejects CDD's The CDD is wrong
Accepts both (mask has don't-care bits) / accepts neither / no filter Cannot decide — say so, do not guess

On a real project this resolved to a single filter out of 51, with mask 0x7FE. The mask has one don't-care bit, which is why 0x738 and 0x739 both land in that mailbox — and why 0x72F does not:

0x738 & 0x7FE = 0x738   == code 0x738   -> accepted
0x72F & 0x7FE = 0x72E   != code 0x738   -> rejected

So the controller only hands 0x738 frames upward. The CanIf PDU configured for 0x72F therefore describes an address that cannot reach the software — and the answer is not "the two files disagree", it is "change CanIfRxPduCfg_Phys_Req.CanIfRxPduCanId to 0x738".

A second, independent confirmation

CanTp carries its own addressing: N_SA and N_TA. On that project they decoded to 56, which is 0x38 — the low byte of 0x738. The low byte of 0x72F is 0x2F, or 47, and appears nowhere. Two artefacts built by different people agree with the CDD and disagree with CanIf.

One honest caveat, because the tool used to overclaim it. On that project CanIf hardware-software filtering was disabled (CanIfHrhSoftwareFilter = false), so CanIf does not re-validate the id in software. The wrong value is therefore a latent defect rather than an immediate failure: it works today by accident, and breaks the day someone enables the software filter, changes the HRH routing, or turns on an integration-test assertion.

A Phy is not a phys

CanIf PDU naming differs per supplier, so roles are detected by keyword rather than by an expected name. The first version looked for phys:

if 'func' in name.lower():    role = 'func_req'
elif 'phys' in name.lower():  role = 'phys_req'
elif 'resp' in name.lower():  role = 'resp'

Then a project arrived with RX_PDU_UDS_DiagPhyReq_LDLP. 'phys' in 'diagphyreq' is False. So the physical request PDU was never found, and the tool reported "the CDD defines a physical request CAN id but CanIf has no corresponding diagnostic PDU" — while that PDU sat in the file under a name that was three letters shorter.

The failure direction is the bad one. A missed mismatch leaves a real defect unreported; a false mismatch sends someone to create a PDU that already exists, and then they have two.

Fixed by matching phy, phys and physical. Trivial line, but it is the class of bug that only appears when the third supplier's naming convention shows up — and by then the tool has been trusted for months.

Proving 0 error is real

A checker that reports nothing is indistinguishable from a checker that broke. The only way to tell them apart is to plant known defects and confirm they still get caught — and specifically, to confirm that a passing fixture and a skipping fixture produce different output.

The self-test therefore has two fixtures:

Fixture Asserts
Configured CDD with 11 planted defects 6 error / 4 warn / 3 info, and all 13 expected codes fire
Template save, CAN page emptied CDX-013 / CDX-025 must fire; CDX-010/011/012/020/021/022 must NOT (nothing to disagree with)

The second fixture also asserts that attrs stays empty while attrs_template is populated. Without that assertion, the fix itself could regress into "read the template values into attrs" — which passes the first two assertions and reintroduces the confident-wrong-verdict problem.

Injection test

Beyond the fixtures: write the correct eight attributes into the unconfigured CDD manually and confirm CDX-013/CDX-025 disappear; then write 0x700 for the physical request and confirm CDX-010 fires and blames the CDD. Two directions, one file, thirty seconds — and it catches both "the check is dead" and "the verdict logic is inverted".

Two real projects

The bug was not found by reading the code. It was found by running the tool on a second project whose CDD happened to be a template save with an empty CAN page — and noticing something was off about the report.

What noticed it was not a check. It was the probe output looking different from the previous run: the line that used to report CAN.ReqCanId under diagnostic parameters was simply gone. A missing line and an empty line look the same on a terminal, which is exactly why the probe now prints the gap explicitly.

Project Before After
CAN attributes mostly filled, Blocksize missing 1 error / 1 warn 1 error / 2 warn — the new CDX-025 honestly says CDX-024 never ran
Template save, CAN page empty 2 error / 2 warn 3 error / 3 warn — CDX-013 + CDX-025 appear

The first row is the one worth staring at. Before the fix it reported 1 warn and looked fine. After the fix it reports 2 warns and one of them says a check did not run. The configuration did not change — only the honesty of the report did.

What the second project actually contained

Beyond the missing addresses, three ordinary findings that would each have cost a bench session:

Code Finding Bench symptom
CDX-006 3 routines in the CDD, absent from Dcm Flashing calls 31 01 FF00 to erase and gets 0x31 back
CDX-031 Security level 2 defined in the CDD only Every service behind that level is locked forever
CDX-002 3 DIDs in Dcm, absent from the CDD Not a bug — decide whether they should be readable after sales at all

The last row is the direction that matters. The three DIDs were named like SystemSupplierInternalECUHardware and ReadOnlyTest. Two are OEM-reserved identifiers that should not appear in an after-sales spec at all, and one is a leftover from the configuration tool's demo project. The correct action may well be to delete them from Dcm, not to add them to the CDD.

A warn is not a verdict

"Present in Dcm, missing from the CDD" sounds like the CDD is behind. Sometimes it is the opposite: the ECU exposes something it should not. The tool reports the asymmetry and leaves the ownership question to the engineer, because only they know which document is authoritative.

What it cannot see

Twenty-five checks passing is not the same as correct diagnostics, and the failure mode of a checking tool is that people stop reading the caveats. So the caveats are printed as part of every report, not buried in documentation.

Not covered Why it matters
DID data length and structure A DID can exist on both sides and still return the wrong payload length
Session/security dependency per service A service can be configured and still be unreachable from the session it belongs to
NRC usage per service Only the two lists are counted, not which service must return which code
Security access algorithm Seed/key lengths compared; the algorithm itself is not
Snapshot / extended record fields DTC presence compared, record contents are not
Whether a configured value is correct Only "configured" vs "not configured" is decidable

The last row is the honest limit of the whole approach. If the CDD says 0x738 and the filter also says 0x738 but the vehicle actually uses 0x739, no amount of cross-checking between these two files will find it. Cross-checking finds disagreement, not wrongness.

Results and method

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

Exit codes are 0 / 1 / 2 — no error, errors found, bad arguments — so it drops into CI without a wrapper. 25 codes, 10 error / 12 warn / 3 info. Standard library only, read-only, Python 3.8+.

Four things here generalise past this tool:

Principle Why
A skipped check must look different from a passing one Otherwise a broken parser and a clean config produce the same report
Never fall back to defaults in silence A default compares confidently against a number that is not the real one
Find the tie-breaker before writing the verdict A diff without a verdict makes the reader guess which file to open
Assert the negative fixture The fixture with the empty page is what catches over-reporting and regression in the fix itself
Where it lives

Source and issue tracker: github.com/autosar-skills/autosar-agent-skills · MIT · the whole thing is one standard-library Python file plus two fixtures. All examples above are synthetic; no real vehicle, supplier or customer identifiers appear.

Common questions

Why do a CDD and an ARXML disagree in the first place?

They are two artefacts maintained by two groups in two tools: the CDD is the diagnostic spec, written by a diagnostic engineer in CANdelaStudio and used to build the tester; the ARXML is the ECU configuration, written by a BSW engineer in DaVinci or EB tresos and compiled into the ECU. Nothing in the pipeline compares them, so changing one never updates the other. The symptom is that it compiles, the tester connects, and then the ECU answers 0x31 requestOutOfRange.

When the CAN ids disagree, how do I know which file to change?

Use the hardware acceptance filter as the tie-breaker: read CanHwFilterCode and CanHwFilterMask and evaluate (frame_id & mask) == (code & mask). If the filter accepts the CDD's id and rejects CanIf's, the controller only hands the CDD's id upward, so CanIf is wrong. If it is the other way round, the CDD is wrong. When both or neither are accepted, say it cannot be decided rather than guessing.

Does 0 error mean the diagnostic configuration is fine?

No. It means those 25 checks found nothing. DID payload structure, the service-session-security dependency matrix, NRC usage ranges, the security algorithm and snapshot record contents are all outside the scope. More importantly, the tool has to distinguish a check that passed from a check that never ran: if a CDD has no CAN addresses at all, the three addressing checks did not execute and that is not a pass.

Why does it say the CDD has no CAN configuration when values are visible in the file?

A CDD carries diagnostic addresses in two places: attribute instances under (this ECU's configuration) and attribute definitions under DEFATTS/ECUATTS (the CANdelaStudio template defaults). Save a template as a new project without filling the CAN page and only the definitions exist. The 0x700 / 0x600 / 0x750 you see are placeholders, not this ECU's addresses; exporting ODX from them makes the tester send every request to 0x700.

Which diagnostic configuration errors fail immediately on the bench?

Three most directly. A wrong physical request CAN id means no ECU listens on the address the tester sends to, so it cannot connect at all. A missing erase or check-integrity routine (0xFF00 / 0xFF01) makes the flashing sequence fail with 0x31 at that step. A security level defined in the CDD but absent from Dcm locks every service it protects, permanently.

Harry Zhao
AUTOMOTIVE EMBEDDED · AUTOSAR BSW / DIAGNOSTIC TOOLING

12 years in automotive embedded software, from ECU low-level drivers up to AutoSAR CP platform architecture: build flows, test scripting, Git servers, Jenkins CI/CD, and chip-level functional safety on Infineon TC397 / NXP S32K314. The checkers described here come out of real supplier configuration reviews, and most of them exist because something in a review was missed first.

Read-only tool. All example data is synthetic; no real vehicle, supplier or customer identifiers appear.