LabWired The easy way to build hardware

Running a full ECU diagnostic check with no hardware

Phase 1 — session & security 0/27 services
STM32H5
UDS TESTER
udslib client
virtual FDCAN
STM32H5
UDS ECU
udslib server
0x10 DiagnosticSessionControl
g_service_results @ 0x20010000 0x00000000
SERVICES 27/27 PASS · exit=0
Every exchange in the real gate, in the order the tester firmware actually runs them — request bytes, response SID and results bit are taken from main.c. Note the bits fill out of order: they're indexed by SID, not by execution order, and 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.

labwired · one deterministic process · no hardware in the loop STM32H563 UDS TESTER udslib client firmware.elf STM32H563 UDS ECU udslib server · unmodified firmware.elf 0x7E0 · request virtual FDCAN ISO-TP · CAN-FD 0x7E8 · response g_service_results @ 0x20010000 one bit per service — set only when the response crossed the bus and matched == 0x07FFFFFF
Both nodes are real 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 0x07FFFFFF - all 27 services answered correctly, across the bus. 27/27, 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:

$ labwired test --script examples/h5_uds_tester/allservices-gate.yaml

H5-UDS-TESTER   H5-UDS-ECU-FULL          # two virtual STM32H5s boot
TESTER_REQ_10   ECU_READY   RESP_50_OK   # DiagnosticSessionControl
TESTER_REQ_27_SEED          RESP_67_OK   # SecurityAccess (seed)
TESTER_REQ_27_KEY           RESP_67_OK   # SecurityAccess (key)
PHASE1 4/4 PASS
TESTER_REQ_22               RESP_62_OK   # ReadDataByIdentifier (VIN)
TESTER_REQ_2E               RESP_6E_OK   # WriteDataByIdentifier
PHASE2 8/8 PASS
TESTER_REQ_85               RESP_C5_OK   # ControlDTCSetting
TESTER_REQ_86               RESP_C6_OK   # ResponseOnEvent
PHASE3 4/4 PASS
TESTER_REQ_34               RESP_74_OK   # RequestDownload
TESTER_REQ_38               RESP_78_OK   # RequestFileTransfer
PHASE4 6/6 PASS
TESTER_REQ_84               RESP_C4_OK   # SecuredDataTransmission
TESTER_REQ_11   ECU_RESET   RESP_51_OK   # EcuReset (tested last)
PHASE5 5/5 PASS

SERVICES 27/27 PASS
exit=0
Session & security
4 of 27
10DiagnosticSessionControl 29Authentication 27SecurityAccess 3ETesterPresent
Data services
8 of 27
22ReadDataByIdentifier 2EWriteDataByIdentifier 3DWriteMemoryByAddress 23ReadMemoryByAddress 24ReadScalingDataByIdentifier 2CDynamicallyDefineDataIdentifier 2AReadDataByPeriodicIdentifier 2FInputOutputControlByIdentifier
DTC services
4 of 27
19ReadDTCInformation 85ControlDTCSetting 14ClearDiagnosticInformation 86ResponseOnEvent
Programming / transfer
6 of 27
31RoutineControl 34RequestDownload 36TransferData 37RequestTransferExit 35RequestUpload 38RequestFileTransfer
Remaining + reset
5 of 27
28CommunicationControl 83AccessTimingParameter 87LinkControl 84SecuredDataTransmission 11EcuReset — last
All 27, in the order the tester actually walks them. The grouping isn't cosmetic: writes run before the reads that check them, and EcuReset is held to the very end so the reboot can't wipe the other 26 results.

And it's not just on my machine: the same thing runs on a clean GitHub Actions runner. Install the toolchain, build both firmwares, build the emulator, run the gate, green end to end. Here's the CI run.

Here's the udslib ECU running on a virtual STM32H5:

Press Run and watch the H5 answer a diagnostic 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.

That's the version of “digital twin”, your actual firmware running against a CPU model honest enough to surface real bugs - before anything touches a bench.

Try it / the receipts

Andrii Shylenko
Andrii Shylenko

Founder, LabWired.