Repository navigation
can-full: the DC-DC as an AUTOSAR ECU, described by an ARXML - #18
Merged
Merged
Conversation
tkeairns
force-pushed
the
feat/full-image
branch
from
October 14, 2026 18:21
664f489 to
68b3a41
Compare
tkeairns
force-pushed
the
feat/autosar-ecu
branch
4 times, most recently
from
October 19, 2026 04:23
634aa7d to
634291b
Compare
tkeairns
marked this pull request as ready for review
October 19, 2026 04:40
…m it arxml/can-full.arxml describes the can-full bench as an AUTOSAR R19-11 System Template: bench.dbc's frames unchanged, six new DC-DC frames on 0x310-0x315 (E2E profiles 1, 2 and 5, a container I-PDU with short headers, a multiplexed I-PDU, a SecOC profile 1 secured I-PDU), UDS as DCM I-PDUs over CanTp, XCP as general-purpose I-PDUs. It loads strictly, without warnings or dangling references, in autosar-data and cantools. tools/arxml2c.py generates the DC-DC's C tables from it: ids, timing, signal layouts and scaling, E2E and SecOC parameters. `just arxml` runs it and the firmware workflow fails when the tables and the ARXML disagree. A description the core cannot send (big-endian signals, long or little-endian container headers, unaligned SecOC fields, other E2E profiles, raw ranges beyond float precision) fails generation rather than the node.
lib/autosar is a sans-IO core (signal packing, E2E profiles 1, 2 and 5 per the PRS, the container short header, multiplexed segments, the SecOC trailer) and its Zephyr side: one thread sends a schedule of PDUs on their ARXML timing through lib/can, earliest deadline first and below the application threads, and AES-128 CMAC comes from PSA Crypto (mbedtls and tf-psa-crypto join the west allowlist). The DC-DC maps each model sample onto the generated tables, one build function per PDU; a PDU the ARXML adds without one does not compile. CommunicationControl stops them with its other normal messages. nodes/full takes the snippet, adding 200 frames/s (about 5 % of 500 kbit/s). The SecOC key is RFC 4493's example key: public on purpose, a demo key. full.elf grows by 10.8 KB of text and 13.3 KB of RAM: the 3 KB thread stack, 8 KB of AES tables, PSA state.
suites/autosar tests the DC-DC as a bench tests an AUTOSAR ECU, with cantools reading arxml/can-full.arxml: ids, lengths and cycles as described; E2E profile 1 by hand (cantools has none), 2 and 5 by cantools; the container's PDUs in turn; the multiplexed PDU's alternatives; the SecOC MAC verified with the key (cryptography); every value equal to DCDC_Status's of the same sample; UDS on the CanTp addressing and flow control the ARXML gives; 0x28 silencing the PDUs. It records the bus with candump, out of process, and fails when the host dropped frames rather than blaming the node. can-full runs it in CI and on hardware. ztests check the core against the PRS E2E example tables, RFC 4493's CMAC examples and signal scaling at its edges. The UDS suite's self-test test now reads its socket during the 2 s routine: at the added 200 frames/s the idle socket overflowed and dropped the final response, which udsoncan had received.
On hardware the 600 s coexistence soak failed on one missing DAQ sample while the node's counters were clean. With candump -d the record shows why: the suite's candump, writing to a file, dropped 501 frames in four bursts, while a second candump on the same run writing to tmpfs dropped none of 1.86 M frames and saw every DAQ sample. A write waiting on the disk stalls candump until its socket overflows. frames.recording() now has candump write into a pipe the test process drains, records every frame when given no ids, and fails as an incomplete record when the host drops frames instead of as a lost sample. The soak records through it; its assertions are unchanged.
P0A94-00 DC/DC converter performance (overtemperature), P0A09-00 status circuit low (input undervoltage), P0A08-00 status circuit (output overcurrent), U0111-00 lost communication with the battery energy control module (BMS_Limits missing). A scan tool shows the generic text instead of manufacturer codes. Before: 0x1A0098, 0x1A0116, 0x1A0219, 0xC11187.
SAE J1979 services 0x01 (support bitmaps, MIL and DTC count, OBD standard, the node's PIDs), 0x02 (freeze frame 0 from snapshot record 0x01 of the first confirmed DTC), 0x03/0x07/0x0A (confirmed, pending, permanent DTCs as 2-byte codes), 0x04 (clear) and 0x09 (VIN from DID 0xF190, CALID, CVN, ECU name) on the UDS identifiers. Unsupported requests get nothing, physically or functionally. SAE J1979-2 on the same core: DIDs 0xF400 + PID, 0xF501, 0xF8xx; ReadDTCInformation 0x42, 0x55, 0x56; ClearDTC group 0xFFFF33. CONFIG_ZELOS_UDS_OBD, on with the UDS server. The DC-DC's PIDs are a four-row table, each a formula over one of its DIDs, so the freeze frame reports them from the snapshot: 0x1F run time, 0x42 module voltage, 0x46 ambient temperature, 0x5B pack life. New for every build: a DTC confirmed with its indicator requested is permanent. ClearDTC and 0x04 leave it; it goes once its test has passed in three consecutive operation cycles.
A scan tool's view of the DC-DC: single frames on 0x7DF, an ISO-TP stack per responder for the flow control a multi-frame response needs. PIDs walked from the bitmaps and checked against DCDC_Status; MIL and count; a lost BMS pending, confirmed and in the freeze frame; a permanent DTC surviving 0x04 and 0x14; the VIN by multi-frame; J1979-2 DIDs and 0x19 reports equal to the legacy services; silence to unsupported requests. Its fault tests play the BMS, so beside the bench's BMS they skip; CI also runs the suite on the image alone, as it runs can_raw.
DTCs follow 13 CCR 1968.2 (d)(2.2)-(2.5) on the one DTC memory UDS and OBD share. An operation cycle is still one boot (any ECUReset ends it). - Two-cycle confirmation: a failure stores pendingDTC and the freeze frame; confirmedDTC, warningIndicatorRequested and permanent follow when it fails in a second tested cycle (CONFIG_ZELOS_UDS_DTC_CONFIRM_CYCLES, 2). - The MIL is warningIndicatorRequested: it stays on while the test passes and goes off after three passed cycles (..._HEALING_CYCLES), taking the permanent DTC with it. confirmedDTC then ages out after 40 cycles without a failure (..._AGING_CYCLES), one cycle being one warm-up cycle here. - A permanent DTC whose MIL is off, after a clear or a power loss, goes after one cycle in which its test ran and never failed. core_init() takes the permanent set to restore after a power loss. - Freeze frames: the first with the pending code (records 0x00 and 0x01, OBD freeze frame 0), the second (0xF0) at the first failure of each later cycle; erased with an unconfirmed pending code or the aged confirmed one. OBD freeze frame 0 belongs to the DTC stored first, not the lowest index. - PID 0x01: MIL from warningIndicatorRequested; comprehensive components supported and complete. CONFIG_ZELOS_UDS_OBD_VIN: only one ECU reports the VIN. J1979-2: DID 0xF810 = 0x01; ReadDataByIdentifier answers the DIDs it supports and 0x31 only if none is (every DID, ISO 14229-1); 0x19 0x42 reports severity class 0x02 and applies the severity mask; 0x19 0x55 records are DTC and status, 4 bytes; 0x19 0x56 with no DTC in the group is 0x31; 0x19 0x04 records 0x00 and 0xF0 carry DIDs 0xF400 + PID; OBD requests are never answered 0x13 or 0x21. Legacy: requests mixing availability and data PIDs, more than 6 PIDs or 3 freeze frame pairs get nothing; 0x09 takes up to 6 availability infotypes. The uds suite's DTC test follows: pending before confirmed, the MIL kept until three passed cycles, two failed cycles in extended data 0x01, three snapshot records in 0x19 0x03, and a multi-DID read keeps the supported DIDs. Renode zeroes RAM on a reset (its reset reloads the ELF), so the test needing the memory to survive one skips there.
13 CCR 1968.2 (g)(4.4.6)(C): no power loss erases a permanent DTC. With CONFIG_ZELOS_UDS_NVS (default with OBD-II and a storage partition) the permanent DTCs are one Zephyr Memory Storage (ZMS) entry, their 3-byte numbers, read at start and handed to the core, which restores them when the RAM memory was lost. ZMS, not NVS: NVS takes sectors of 64 KB at most, and the H753's erase sector is 128 KB. A work queue at the lowest application priority rewrites the entry when the set changes: the flash driver busy-waits, and nothing of the application, XCP or the server may wait on it. The uds snippet moves the H753's storage partition to flash bank 2: a sector erase stalls fetches from its own bank, and the code runs from bank 1.
Each new test names the clause it checks: - a fault is pending in its first failing cycle, confirmed with the MIL and permanent in its second (13 CCR 1968.2 (d)(2.2.1)-(2.2.2), (d)(2.2.5)); - the MIL goes off after three passing cycles, the code stays confirmed and the permanent code goes with the MIL ((d)(2.3.2), (d)(2.4), (d)(2.5.1)); - a permanent code survives 0x04 and 0x14 0xFFFF33 and goes after one passing cycle ((g)(4.4.6)(C), (d)(2.5.2)(A)); - the freeze frame is stored with the pending code, and J1979-2 record 0x00 carries it as DIDs 0xF400 + PID ((d)(2.2.7), (g)(4.3.1)); - 22 F810 = 0x01 (ISO 15765-4:2021 6.1.1), a multi-DID read answers the supported subset, malformed OBD requests get no 0x13 ((g)(3.4.2)(A)), 19 56 of an empty group is 0x31, mixed availability and data PIDs get nothing (AUTOSAR SWS_Dcm_00943/00972). 0x19 0x42 and 0x55 are read with the suite's own byte layout (severity, DTC, status; DTC, status), no longer through udsoncan, whose 0x55 parser expects a severity byte. Operation cycles end with ECUReset; the fault tests play or silence BMS_Limits, so they run against any DC-DC responder that watches it, not only the node, and skip under Renode where a reset loses RAM.
The reset macro reloaded the ELF on every reset, including the firmware's own reboot, and LoadELF zero-fills the RW segment that holds .bss and .noinit. Every ECUReset under Renode was therefore a power loss: the DTC memory kept in .noinit was erased, and the lifecycle tests that end operation cycles with 0x11 0x01 had to skip there. The ELF now loads once, when the machine is created. A machine reset keeps memory but puts the vector table offset back to 0, where Renode's part has an empty ITCM, so the CPU halts with SP and PC 0; the macro sets it to the part's boot address (flash, 0x08000000) beside the SysTick frequency. The obd suite's three cycle tests and the uds suite's operation-cycle test no longer skip under Renode. Renode bench, full.elf alone: obd 15 passed (was 12 + 3 skipped), uds 60 passed + 4 timing skips (was 59 + 5). The can-full bench is unchanged at 100 passed, 15 skipped: these tests silence the BMS, and skip beside the bench's.
ISO 15765-4:2005 Table 7: the external test equipment sends one flow control with block size 0 and STmin 0. The suite's scan tool sent can-isotp's default block size, 8. Padding is the tester's choice; the scan tool now pads with 0xCC, as the UDS client does.
ReadDTCInformation 0x1A of record 0x91 (in-use ratio) or 0x92 (test results) answers 0x31: no DTC of the DC-DC has a ratio or test-result monitor (comprehensive components only, 13 CCR 1968.2 (g)(4.5.1), (d)(5.1); SWS_Dcm_01609), nothing functionally. 0x06 and 0x1A of those records are OBD requests, so a malformed one gets no 0x13 and none gets 0x21. The obd suite asserts that a J1979-2 scan tool reads what a legacy one does: readiness (01 01 = F501 = F401), the support bitmaps, the DTC lists, and no test results, ratios or PID 41 either way.
PID 42 is the live rail voltage; two reads at different times differed by more than the bound under a loaded bench. F442 must lie in the span of the PID 42 reads before and after it.
09 06 and 22 F806 computed the CRC-32 of the whole download window (64 KB on the DC-DC) on every request: in any session, without security, functionally, while a routine was pending, and under core_lock. The control loop takes that lock on every DTC report, so each such request could hold it off for the CRC's duration, with the UDS thread raised to its priority meanwhile. The CVN is now computed at start, and again on the first read after an erase or a TransferData write. Both need the programming session with its security level unlocked, so outside one no request computes a CRC; inside, a read computes at most one per such change.
seal() took core_lock and returned, so an ECUReset right after a DTC confirmation could reboot mid zms_write: a half-programmed flash word, possibly an ECC error that fails zms_mount on every later boot, after which permanent DTCs are never stored again. seal() now finishes the queued or running write before taking core_lock, which the write takes too, and then takes a lock the write holds through the flash access, so a write begun in between is waited out and none begins after. The reboot waits only for writes queued or begun before it.
… span Frame 0's PID 5B was compared with itself whenever the pack voltage was known; it is now checked against the snapshot's pack voltage, encoded as the node does. F446 in a multi-DID read is live temperature: it must lie within a degree of two PID 46 reads around it, not equal one taken later. Dropped asserts other tests already make: F400 and F440 against PID 00 and 40, F40C's 0x31, and the silence of 06 00 and 09 08.
The memory window holds the last control cycle's state, so a read right after the previous test restored the setpoint could still see 12.34 V.
The old check subtracted one second from PID 1F and failed on a responder up for less than a second.
With CONFIG_ZELOS_UDS_NVS, a storage partition that does not mount, or a write of the permanent DTCs that fails, sets P062F-00 (SAE J2012 internal control module EEPROM error; 0x062F00, legacy 0x062F) through the normal DTC path: it reports on 0x03, 0x0A and 0x19 0x42 and lights the MIL. AUTOSAR NvM/Dem report a failed non-volatile block the same way, as an ECU-internal DTC. The node runs on with its permanent DTCs in RAM, and the existing log line stays. CCR 1968.2 (g)(4.4.6)(C) requires permanent DTCs to survive a power loss; this is the ECU saying it cannot. - Confirmed at its first failure: its pending state would live only in RAM, which a power loss takes with the store broken, so a two-cycle rule would never confirm it. - Never written to flash: the mask the store holds and compares is the node's DTCs only, so its own permanent bit cannot retrigger the write that failed. - In the core only from its first failure until it ages out or is cleared, as the node's last DTC (the glue appends it with zelos_uds_core_extend()), and kept across a reset (zelos_uds_core_kept()): while the store works, the supported DTC list and every report are unchanged. - A mount, or a write that succeeds, passes it; it heals as any other. ztest test_store_failure_dtc: P062F appended cleared, the node's DTC memory kept, confirmed + MIL at one failure (0x03, 0x07, PID 0x01, 0x19 0x42), its permanent bit, kept over a reset and passing with the MIL kept, and gone after a power loss with a working store.
The same helper, now shared by the uds suites.
zms_mount() ran in zelos_uds_start(), on the main thread before the node's threads: a blank storage partition (first boot, or a garbage collection cut short) erases a 128 KB sector there, so CAN started 1-2 s late. The mount is now the first job of the store's work queue, at the lowest application priority, submitted once the UDS thread runs: the node starts at once. - Until the mount, the permanent DTCs are those in RAM and requests are answered from them as always (no 0x21 on OBD); writes queue behind the mount, and a reboot waits out a mount in progress as it does a write. - After a power loss, flash's permanent DTCs join when it has mounted (zelos_uds_core_restore(): applied only if the memory started cleared, as zelos_uds_core_init()'s argument was). - A mount that fails sets P062F then, as before. ztest test_permanent_dtcs_restored_after_the_mount: none before the mount, flash's after it on a power-on, RAM's kept over a reset.
A second tester on 0x7E0/0x7E8 flow-controls the node's responses too. Zephyr's ISO-TP (v4.4.2) took its FC, arriving while the FF or a block's last CF was still pending, as the one due: it released the next CF with one in flight, and the M_CAN priority queue sends equal identifiers by buffer index, so CFs left out of order. An FC reaching the receiver aborted the request being received, or discarded one just received. Either way the tester timed out. A west patch, applied when just updates the workspace, makes both sides ignore an FC they are not waiting for (ISO 15765-2) and keeps one frame in flight. Two HIL tests drive the node by hand with a second tester's FC after every frame of ours.
python-can marks every frame from this host as not received, and on the Renode bench the node is on this host: the hand reader saw nothing.
main added SIG's Waveform (0x7F0, 2 ms) to bench.dbc; the description carries the bench DBC unchanged, so it describes it too. The DC-DC's generated tables do not change.
can_send()'s callback can run before it returns. Counted after, a first frame or CF completed that early left tx_backlog at 1 with nothing in flight: every flow control after it was ignored, or the CF loop waited for good. The send context also starts at 0.
seal() flushed the write only. A reboot before the mount finished sealed memory without flash's permanent DTCs; the next boot kept that memory, skipped the restore, and wrote the reduced set back to flash.
Both held a non-default session through a wait timed on the host with no request in it: bound(0.5), which is 2.5 s under Renode (time scale 5). A Renode bench behind the host runs flat out until it catches up, and on a fast runner that reached about 2x: the wait passed S3 (5 s) on the node's clock, the session ended, and the DC-DC sent again or 28 00 met the default session. A functional, suppressed TesterPresent every 0.1 s holds the session through any wait, as a tester does.
tkeairns
marked this pull request as draft
October 19, 2026 18:16
tkeairns
force-pushed
the
feat/autosar-ecu
branch
from
October 19, 2026 18:16
634291b to
d0fd7d6
Compare
0x0104 is the DC-DC model's sample, updated every 50 ms, so two reads 1 s apart differ by 1000 or 1050 ms. On hardware 1050 came up in 2-6 of 15 runs on every image, before this branch's firmware too.
tkeairns
marked this pull request as ready for review
October 19, 2026 19:00
The west patch on subsys/canbus/isotp/isotp.c goes: the image builds against Zephyr v4.4.2 as released, as most Zephyr projects use it. M_CAN's TX buffers are a priority queue, which sends equal identifiers by buffer index. Zephyr's ISO-TP can queue a CF while the one before it is still pending (a flow control it takes early, a completion that comes late), and the two then left out of order. FDCAN1 now has one TX buffer: one frame in flight, so frames leave in the order they are queued. lib/can's queue already orders frames as the bus arbitrates. Two TX events, as Renode's model reads a full event FIFO as empty. A second tester's flow control is taken as the node's own again, as upstream does: the two tests asserting it is ignored go.
With nothing on the bus to acknowledge a frame (the tester unplugged mid-response, the node alone), M_CAN retransmits it for ever and Zephyr's ISO-TP never ends the send: the UDS thread waited on it for good, so S3 never ended the session and a 2F override stayed in force. send() now bounds its wait: above the slowest send ISO 15765-2 allows (4095 bytes, a CF per 127 ms, N_WFTmax waits, N_Bs and N_As). It checks ISO-TP's error each N_As, as its CF path records one and then stops without ending the send. A send with no end, or one ending in N_As or N_Bs, restarts the controller through zelos_can_restart(), which drops the frame it held; a send left behind gives back its flow control filter. The thread then goes on, and S3 ends the session. The driver checks it is started before it waits for a free buffer, not after: lib/can's thread, woken by can_stop(), would fill the freed buffer while stopped, and that frame never completes. A restart therefore waits for the thread to leave can_send(). ztests drive the three ends against a fake controller (unacknowledged first frame, CF with no buffer, unacknowledged CF to the backstop); the uds suite takes the node off Renode's CAN hub mid-response.
ISO 14229-1's NRC order checks the length before the record's value: a request with no DTCExtDataRecordNumber is NRC 0x13, not 0x12.
tkeairns
marked this pull request as draft
October 19, 2026 21:38
zelos_testing/uds.py is vendored by the UDS and OBD extensions: the test closes and reopens its client instead of adding to it.
A request on the physical identifier during a response is N_UNEXP_PDU, which ISO-TP ends itself once its CF loop is done. Taken for a send left behind, it cut the response short and restarted the controller (CI's coexistence soak). Only N_As and N_ERROR, which ISO-TP's CF path records when a CF's can_send() fails, leave a send without an end.
Zephyr's ISO-TP sends straight to the driver. With one TX buffer its CFs took turns with lib/can's frames instead of yielding to lower identifiers in M_CAN's priority queue. In the HIL coexistence soak, DCDC_Status (50 ms) then had an 81 ms gap, and lib/can's queue peaked at 16 (was 3). CF order under a second tester's flow control is again upstream behaviour. The no-ACK test now allows for the backstop: with free buffers, a CF can sit in one unacknowledged, and only the backstop ends the send.
Renode's M_CAN model reads a full TX event FIFO as empty, so its frames never complete: 16 at once, when the no-ACK test puts the node back on the bus, and every response after that met N_As. 15 TX buffers and 16 events; the message RAM shrinks and FDCAN2 stays where it was.
tkeairns
marked this pull request as ready for review
October 20, 2026 00:05
A response that ended in N_Bs restarted the controller even on a healthy bus, where it only means the tester sent no flow control, and the restart failed every frame pending in the controller. A frame nothing acknowledges drives M_CAN error-passive within a few retransmissions, so the restart now waits for that (or bus-off). A send with no end still restarts as before.
A model cycle passing the DTCs between the count (19 01 FF) and the list read changed what the two saw, and the test failed about 4% of the time. Counting with TF | TFSLC | CDTC | PDTC, which a passed test never sets, matches the empty list read just before.
The client gave a 0x78 the larger of 5 s and its response timeout, 2 s of node time on Renode, as much as the self-test routine itself takes, and the busy test waited the routine's 2 s for its result. When the emulation fell below a fifth of real time the tester gave up before the result, and the routine, still pending, answered the next tests' session changes with 0x21. Both now wait the server's P2* (5 s of node time).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebased on main b63ff97 (adds #20, #25: zelos-can 0.0.11a12 and the full image's
Waveformframe). #26 (ISO-TP stray flow control) was folded in and closed; its Zephyr patch has since been removed (2edc8d2): the image builds against Zephyr v4.4.2 as released. Conflicts:nodes/full/src/main.c's header comment and can-full's README node table, resolved keeping both sides (AUTOSAR DC-DC row and the signal generator row).Finalize: what changed on top of the rebase
seal()also flushes the DTC store's mount. A reboot before the mount finished sealed RAM without flash's permanent DTCs; the next boot kept that memory, skipped the restore, and wrote the reduced set back to flasharxml/can-full.arxmldescribes main'sWaveform(0x7F0, SIG, 2 ms) as bench.dbc has it. The DC-DC's generated tables are byte-identicalkeep_session()) through their host-timed wait; see belowtest_live_values_are_typed: the uptime DID is the model's 50 ms sample, so two reads 1 s apart differ by 1000 or 1050 ms; the tolerance allows one stepWhy #26 broke the CommunicationControl tests under Renode
It did not, in the firmware. The two tests had a latent dependency on Renode's pace, and the runs that happened to be fast were the patched ones.
test_communication_control_stops_the_broadcastandtest_communication_control_silences_the_pdusenter the extended session, send28 01 01, then wait with no request:collect(bus, bound(0.5))andtime.sleep(bound(0.5)). Under Renodebound()scales by the default time scale 5, so the wait is 2.5 s of host time.bound()is meant as a safety bound on a wait that ends on the node's frames, not as a wait length.TimeSourceBase.ElapsedVirtualHostTimeDifference,BaseCPUsleeps only while it is positive). A bench that fell behind (CI: 0.4-0.55x on average, a deficit of ~100 s by the uds suite) runs flat out until it catches up. On a fast runner that reached about 2x over a 2.5 s window: more than S3 of node time.28 00met the default session (the autosar test's NRC 0x7F). In the failing CI run with request logging,28 01was handled at node time 75.538 s, S3 expired at 80.539 s and28 00arrived at 80.862 s.--time-scale 12(a 6 s wait): both tests fail with the CI signatures (19 DCDC_Status frames; NRC 0x7F). The fixed tests on the patched firmware, same run: 2 passed.Stock Zephyr, and a response nothing acknowledges (on top of the finalize)
send()'s wait on ISO-TP gets a backstop above the slowest send ISO 15765-2 allows, and every N_As it checks for the error ISO-TP records when a CF'scan_send()fails. On a send with no end, or one ending in N_As / N_Bs, the controller restarts (zelos_can_restart()), dropping the frame M_CAN would retransmit for ever. A send left behind gives back its flow control filter. The UDS thread then goes on, and S3 ends the session and the2Foverride.19 1Awith no record byte answers NRC 0x13 (was 0x12): ISO 14229-1's NRC order puts the length check first.zelos_testing/uds.pyunchanged.can_get_state()). A frame nothing acknowledges drives it there within a few retransmissions; a tester that only withholds flow control leaves it error-active, so the restart no longer fails the other protocols' pending frames on a healthy bus. A send with no end still restarts unconditionally.test_dtc_reportscounts with the list's mask (TF | TFSLC | CDTC | PDTC): a model cycle passing the DTCs between the19 01 FFcount and the list read made it fail about 4% of the time.P2_STAR_S): the client's P2* was the larger of 5 s and its response timeout, 2 s of node time on Renode, as long as the self-test routine, andtest_busy_while_pendingwaited the routine's 2 s for its result. With the emulation below a fifth of real time the tester gave up before the result, and the still-pending routine answered the next tests' session changes with 0x21. Test-only:testing/src/zelos_testing/uds.py,testing/suites/uds/test_uds_conformance.py.No ACK on the bus: what the node does now
SEND_BACKSTOP_MS= 585 CFs (4095 bytes) x 127 ms + (N_WFTmax + 1) x N_Bs + N_As = 86.3 s, derived in code fromCONFIG_ISOTP_*.send()restarts the controller if it is error-passive or bus-off.send()sees this at its next N_As check, about 3-4 s in all.can_stop()fails every frame the controller holds with-ENETDOWN.can_start().can_send()and the restart takes it first. Stockcan_mcan_send()checksstartedbefore it waits for a buffer, not after, so a sender woken bycan_stop()fills the freed buffer while stopped. That frame never completes. With one buffer the node then stayed mute after reconnecting (seen on Renode once the test re-requested only what TXBRP showed pending); with more, each restart would lose a buffer.No-ACK tests
test_unacknowledged_first_frametest_flow_control_never_senttest_cf_without_a_buffercan_send()times out: stranded, seen at the next N_Astest_unacknowledged_cftest_request_mid_responsetest_a_tester_gone_mid_response_releases_the_output2Fto 15 V, FF + 7 CFs at STmin 127 ms. After CF1 the bench takesfulloff its CAN hub (monitor:connector Disconnect). After S3 + backstop + 3.6 s of node time it reconnects and re-requests whatever TXBRP shows pending, as a controller retransmits. Then the session must be default at once, and the output at the setpoint.Renode 1.16.1's M_CAN model never sets PSR.EP or PSR.BO (tagged, unimplemented), so on Renode the node always reads error-active and the gated restart never runs. The Renode no-ACK test still covers the restart: its CF finds no buffer, the send is stranded, and the unconditional path restarts (
response 0x62: no end (-1), restarting CANin every run). The gated path is covered by the two ztests above and on HIL (below).Gates (head 452db85, firmware 480fa7be)
cd can-full && just build(stock Zephyr v4.4.2)just twisterjust up && just test, 3 runsjust up && just test, 5 runstest_xcp_and_uds_share_the_ecuwith 0x37 (below)test_dtc_reportsx300, can-full on Renodejust hil full can0uv run pytest suites/uds --channel can0uv run pytest suites/uds suites/obd --channel can022 F190with no FC, 5 times (and 5 with FC)response 0x62: -2(N_Bs) 5 times, no restart; 0 frames missing on 0x300 / 0x310-0x315, largest gap 20.1 ms either way; can0 ERROR-ACTIVE, berr 0/0The board is left on 480fa7be.
The 8-failure Renode run, root-caused. Not the no-ACK path: the failures came about 50 tests after the reconnect, and the node logged no N_As / N_Bs end. The self-test got its 0x78, then the client's P2* (10 s host, 2 s of node time) ran out before the routine's result (2 s of node time); the routine, still pending, answered the next five tests' session changes with 0x21, and its late result was read by the OBD suite as the answer to
01 00. The bench host (zelosnuc, i7-1260P, powersave) runs this three-node bench at 0.25-0.30 of real time, dipping under 0.2, whichbound()'s factor 5 assumes; in a capture of a passing run the result came 6.7-7.3 s after the 0x78. Reproduced on demand with--time-scale 3: the same 6 failures on c388415 (d09e5221) and on 6c006c1, so it predates this PR's firmware; with 452db85 the self-test and busy tests pass there. GitHub's runners run the bench at 0.4-0.55.The 0x37 run.
test_xcp_and_uds_share_the_ecuwaits out the 10 s power-up security delay after the previous test's reset withinbound(11)= 55 s; the node ran at 0.18 of real time for that minute (10.5 s of node time in 58 s), so the delay outlasted the bound. Same cause, a slow host, and no protocol timer shorter than the node's: left as is.Gates (head c388415, firmware d09e5221)
cd can-full && just build(stock Zephyr v4.4.2 workspace, no west patch)just twister19 1Aassertion added)just up && just test(bench host)can-full/build/full.elftest_request_mid_responsewithwait_sent()'s old conditionno end (-6), restarting CAN); passes nowsuites/full -s, 2 runs eachcan-fullpush and PR,firmware,can-bench,serial-benchcan-benchfailed in setup (the runner kernel had nolinux-modules-extrapackage) and passed on rerunjust hil full can0(xcp, uds, obd, j1939, canopen, autosar, full, can_raw)uv run pytest suites/uds suites/obd suites/autosar --channel can0 --a2l ../can-full/build/full.a2luv run pytest suites/full -s --channel can0Known limits (stock ISO-TP)
test_programming_sequenceon HIL, with the RevPi answering the node's first frames with7E0 [3] 30 00 00. The patch had fixed it.just hil.2Foverride stays. Stock ISO-TP waits on that CF withK_FOREVERin the system work queue, and nothing above it can see the wait.Open from the finalize review
19 1Awith no record (0x13).What
arxml/can-full.arxml: the can-full bench as an AUTOSAR R19-11 System Template. bench.dbc's frames unchanged, six new DC-DC frames, UDS as DCM I-PDUs over CanTp, XCP as general-purpose I-PDUs.0x310DcdcOutputP10x311DcdcThermalP20x312DcdcPowerP50x313DcdcContainer0x314DcdcMux0x315DcdcSecuredtools/arxml2c.py,just arxml: the DC-DC's C tables (nodes/dcdc/src/autosar_pdus.{c,h}) generated from the ARXML. The firmware workflow fails when they differ. Descriptions the core cannot send fail generation: big-endian signals, long or little-endian container headers, unaligned SecOC fields, other E2E profiles, raw ranges beyond float precision.lib/autosar: a sans-IO core (signal packing, E2E profiles 1/2/5 per the PRS, container short header, multiplexed segments, SecOC trailer) and its Zephyr side: one thread sends the schedule through lib/can, earliest deadline first, below the application threads; AES-128 CMAC through PSA Crypto (mbedtls,tf-psa-cryptoadded to the west allowlist).-S autosaron the DC-DC;nodes/fulltakes it. UDS CommunicationControl stops these PDUs together with the node's other normal messages.testing/suites/autosar: tests the node from the ARXML alone, using cantools pluscryptographyfor the MAC. can-full runs it in CI and injust hil full.frames.recording(): candump runs as its own process with drop monitoring (-d) and writes into a pipe the test process drains. The autosar suite and the coexistence soak both record through it, and a host drop fails the test as an incomplete record.UDS suite: the self-test test reads its socket during the 2 s routine. At the added 200 frames/s its idle socket overflowed and dropped the final response.
Why
A production-style AUTOSAR ECU on the bench, with one description for the firmware, the tests and host tools. A tester switching from the DBC to the ARXML decodes the bench the same way and also sees E2E, containers, multiplexing, SecOC and the diagnostic transport.
How tested
just arxml+git diff --exit-code(CI); independent raw-XML comparison of every id, length, cycle, offset, compu factor/offset/limit, texttable value, E2E and SecOC parameter, header id, selector and segmentjust twister(nodes + tests, nucleo_h753zi + native_sim)cd can-full && just build && just up && just testfull(this branch's firmware) on NUCLEO-H753ZI, can0 500 kuv run pytest suites/<s> --channel can0 --a2l build/full.a2luv run pytest suites/full --channel can0 --a2l build/full.a2lcandump -d -L can0fullfull.elfvs #16'sip -details -statistics link show can0, before and after every hardware runThe coexistence soak's recorder
The 600 s soak had failed twice on this image, each time on one missing DAQ sample while the node's counters were clean. With
candump -dthe record shows why:A write that waits on the disk stalls candump until its ~300-frame socket buffer overflows. With the pipe, the same soak passed, and a second tmpfs candump again dropped nothing. pyxcp's in-process socket still loses samples under this load (103 gaps); the suite uses that record for information only.
Known limits
Follow-ups
OBD-II
What
-S udsbuild (CONFIG_ZELOS_UDS_OBD, default on), so infull. Legacy: $01 PIDs 00 / 20 / 40, 01, 1C, 1F, 42, 46, 5B; $02 frame 0; $03 / $07 / $0A; $04; $09 VIN (multi-frame), CALID, CVN, ECU name. J1979-2 on the same core:22 F810, F400 + PID, F501, F802 / F804 / F806 / F80A,19 42(severity 0x02),19 55(4-byte records),19 56,19 04records 0x00 / 0xF0,14 FFFF33.lib/uds/core/obd.c; the node's PIDs are a table of formulas over its DIDs.ZELOS_UDS_DTC_CONFIRM_CYCLES2,_HEALING_CYCLES3,_AGING_CYCLES40. An operation cycle is one boot.ZELOS_UDS_NVS, Zephyr ZMS), written by a lowest-priority work queue; the uds snippet movesstorage_partitionto flash bank 2 so an erase never stalls code fetch from bank 1. An orderly reboot (ECUReset) waits out a queued or running write.09 06/22 F806need no security and are served functionally, and a per-request CRC held the lock the control loop takes.testing/suites/obd(17 tests): a scan tool's view of all of the above, each test citing its clause; in can-full'sjust testand alone on the image in CI..noinit, so every ECUReset was a power loss); the reset macro sets the vector table offset to the boot address and the SysTick frequency.Why
A legislated-ECU counterpart for the OBD-II scan tool and the passive decoder: the same node, over the same bus, answers what a generic scan tool and an inspection read, with the DTC lifecycle they judge.
UDS behaviour changes
22answers the supported subset for every DID (22 F186 1234->62 F186 01, was 7F 22 31).19 03lists records 0x00 / 0x01 / 0xF0;19 04 FFreturns all three.14 FFFFFFkeeps permanent DTCs.19 42 / 55 / 56 / 1Aand14 FFFF33are served (were 0x12 / 0x31). A22whose first DID is F4xx-F8xx / E0xx is an OBD request: no 0x13, no 0x21.How tested
On the bench host's mirror, twister and Renode in the repo's containers; HIL on NUCLEO-H753ZI, can0 500 k.
just twister(tests/lib+nodes, native_sim run + H753 build-only)just arxmljust test-bench obd/udsoncan-full/build/full.elftest_dtc_reports, a model cycle passing the DTCs between its count and list reads; the rerun 60)cd can-full && just build && just up && just test, 2 runs at the head and 2 before the last two firmware commitsjust hil full can0, 2 runsbus_health), 37 and 87 of them, from a disturbance elsewhere on the bench bus: the node's run time (PID 1F) shows no reboot, and with no test running the adapter's error-warning / error-passive counts kept rising. The ECUReset tests plus the next OBD test, 2 runs each on this image, the P062F image without the async mount, then this image again: 4/4 every run, ERROR-ACTIVE before and afterjust hil full can0test_programming_sequence(UDS upload, P2 timeout) andtest_every_protocol_at_once(10 s soak on hardware since #24, UDS P2 timeout); both failed again on 2 reruns. A capture shows another tester on the bench bus answering the node's first frames with its own flow control (7E0 [3] 30 00 00) beside ours: a bench condition, not the node. Before the rebase on the previous image: 119 passed, 1 failed (the soak),test_programming_sequencepasseduv run pytest suites/obd --channel can0 --a2l ../can-full/build/full.a2l09 06,22 F806,01 42candump -L can0, checked with zelos-can's independent ISO-TP reassembler, J1979 formulas and ISO 15765-4 rulesfull.elfvs the AUTOSAR imageFlash store failure: P062F
CONFIG_ZELOS_UDS_NVS, a storage partition that does not mount, or a write of the permanent DTCs that fails, sets P062F-00 (SAE J2012 internal control module EEPROM error;0x062F00, legacy0x062F) through the normal DTC path: it reports on03,0Aand19 42and lights the MIL; the node runs on with its permanent DTCs in RAM; the existing log line stays. AUTOSAR NvM / Dem report a failed NV block the same way. CCR 1968.2 (g)(4.4.6)(C).test_store_failure_dtc; on Renode, an image whose storage partition is one sector (ZMS needs two, so the mount fails):03and0Alist 062F, PID 01 MIL on with a count,19 42status confirmed + MIL,19 0Alists five DTCs with P062F last; the same after an ECUReset (the mount fails again).The flash store mounts after start
zms_mount()ran inzelos_uds_start()on the main thread, before the node's threads: a blank partition (first boot, or a garbage collection cut short) erases a 128 KB sector there. It is now the first job of the store's lowest-priority work queue, submitted once the UDS thread runs. Until it completes, the permanent DTCs are those in RAM and requests are answered from them (no 0x21 on OBD); writes queue behind it; a reboot waits out a mount in progress as it does a write. After a power loss, flash's permanent DTCs join when it has mounted (zelos_uds_core_restore()). A failed mount sets P062F then.test_permanent_dtcs_restored_after_the_mount.Known limits
19 04record bodies, readiness group numbers (19 56always 0x31).19 1A/19 06of 0x91 / 0x92 answer 0x31.test_programming_sequenceabove): stock Zephyr ISO-TP, see Known limits at the top.Follow-ups
testing/suites/obd/test_dcdc_obd.py: re-vendor it for the19 1A0x13 assertion, and its demo vehicle must answer the same. zelos-extension-uds pdu: representative CANopen node, with SYNC, TIME and a short example suite #5 also vendors an olderzelos_testing/uds.pyandtest_uds_conformance.py: re-vendor from this head.just hil.storage_partitionis outside the code bank on other boards.OBD load on the control loop (HIL)
DCDC_Status period with and without an OBD request flood at about 10x the J1699 burst rate. Accepted as is: the mean does not move and the worst case stays within normal CAN arbitration variance.
CI before finalize
On 634291b every check passed:
can-full(firmware build, then the Renode bench: every suite asjust testruns them 104 passed, 15 skipped, including the 120 s coexistence soak;can_rawalone 6 passed;obdalone 17 passed),firmware,twister,can-bench,serial-bench.Release order
This PR is in step 5 of the AUTOSAR / OBD-II train (zeloscloud/src #1707 has the full list): merge it with or after zelos-extension-can #90. zelos-extension-uds #5 and zelos-extension-obd #1 vendor its UDS and OBD-II suites (both re-vendor from this head after it merges), so merge it in the same step as those. Nothing here is released or published.