Running a full ECU diagnostic check with no hardware
Category: Whitepaper
EcuReset is deliberately last because the ECU reboots after it.
There’s a friction in automotive software work. You want to test your ECU’s diagnostics: the UDS services a workshop tool or a flashing rig talks to; and to do that you need an ECU on a bench, a CAN interface (PCAN, a Vector box, something), wiring, power, and a tester driving the other end.
None of that fits in a CI pipeline. Diagnostics get tested by hand, on hardware, and the regression coverage is thin.
We wanted to see how far we could push the other direction: run the whole thing - a real ECU, a real tester, a real CAN bus. Entirely in software, headless, on a free CI runner.
What we built
Two virtual STM32H5 microcontrollers inside LabWired, wired together by a virtual FDCAN bus. One runs a UDS tester (the client side); the other runs a UDS ECU (the server) that registers all 27 ISO-14229 services. Both run real udslib firmware - the same open-source ISO-14229 stack you’d compile for a part. Nothing is mocked.
The tester sends a genuine request for every service, the request crosses the virtual bus, the ECU’s real handler runs, the response comes back, and the tester checks the bytes.
firmware.elf builds running on modelled H563 silicon —
the ECU's is stock udslib. The only assertion the gate makes is the tester's own
results word: memory_value @ 0x20010000 == 0x07FFFFFF.
At the end it sets one bit per service in a results word. A CI job reads that word and asserts it equals - all 27 services answered correctly, across the bus. , exit 0, no hardware in the loop.
Here’s an abridged trace from a real run. Every line is one UDS exchange crossing the virtual CAN bus and getting checked:
10DiagnosticSessionControl
29Authentication
27SecurityAccess
3ETesterPresent
22ReadDataByIdentifier
2EWriteDataByIdentifier
3DWriteMemoryByAddress
23ReadMemoryByAddress
24ReadScalingDataByIdentifier
2CDynamicallyDefineDataIdentifier
2AReadDataByPeriodicIdentifier
2FInputOutputControlByIdentifier
19ReadDTCInformation
85ControlDTCSetting
14ClearDiagnosticInformation
86ResponseOnEvent
31RoutineControl
34RequestDownload
36TransferData
37RequestTransferExit
35RequestUpload
38RequestFileTransfer
28CommunicationControl
83AccessTimingParameter
87LinkControl
84SecuredDataTransmission
11EcuReset — last
EcuReset is held to the
very end so the reboot can't wipe the other 26 results.
The same gate runs on a clean GitHub Actions runner: install the toolchain, build both firmwares, build the emulator, run the gate, green end to end. CI run. Also see LabWired for CI.
The udslib ECU on a virtual STM32H5:
ReadDataByIdentifier request over CAN-FD — real udslib firmware, no hardware. Open full-screen ↗It works, it runs nightly on free GitHub runners, and it catches real bugs. The interesting part is which bugs it caught. Every one turned out to be in the emulator, not udslib, and they’re all fixed and released.
What you get
A full 27-service UDS diagnostic check - session control, security access, read/write by identifier, DTCs, routine control, the whole programming-and-transfer sequence, right down to ECU reset. Running across two virtual MCUs on a simulated CAN network, headless, in CI, for the cost of a GitHub runner minute. The ECU firmware uses udslib completely unmodified.
Your actual firmware on a CPU model honest enough to surface real bugs, before anything touches a bench.
Try it / the receipts
- The gate & nightly CI: merged and live in udslib at examples/h5_uds_tester; it closed issue #58.
- The green CI run: full build + 27/27 on a clean runner: actions/runs/29379477389.
- udslib: the open ISO-14229 stack under test, used 100% unmodified: w1ne/udslib.
- Run the ECU yourself: STM32H5 UDS ECU in the LabWired playground.