The problem
Every modern vehicle ECU, including EV BMS and motor controllers, is serviced through UDS. Engineers who understand services, sessions, negative responses and the transport layer can build and debug diagnostic stacks, a core skill in automotive firmware.
Objectives
- Implement ISO-TP (ISO 15765-2) single and multi-frame transport with flow control.
- Implement UDS services 0x10, 0x3E, 0x22, 0x2E, 0x19, 0x14, 0x31 and 0x11.
- Expose live kit data such as cell voltage and temperature through data identifiers.
- Test with a PC tester, including error cases and timing.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- CAN bus interface
- 1S battery with connector and T-sense input
- Analog potentiometer input
- General output relay (NO / COM / NC)
- SPI flash memory
- 16×2 LCD
- Serial UART interface
Software
- STM32CubeIDE
- Embedded C with HAL drivers
- Python with python-can (and optionally the udsoncan and can-isotp packages for cross-checking)
Extra parts
- A USB-CAN adapter — To act as the diagnostic tester node on the bus, since the kit provides one CAN node.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- UDS service and sub-function
- Each request starts with a service ID, such as 0x22 ReadDataByIdentifier. A positive response adds 0x40 to the service ID; a negative response is 0x7F, the service ID and a negative response code.
- ISO-TP
- A transport layer that splits messages longer than 7 bytes into a first frame and consecutive frames, with the receiver sending flow-control frames to set block size and separation time.
- Diagnostic session
- The default session allows basic services; an extended session unlocks more. A session times out back to default unless the tester sends TesterPresent (0x3E).
- Negative response codes
- Codes such as 0x11 service not supported, 0x13 incorrect message length, 0x31 request out of range and 0x7F service not supported in active session tell the tester why a request failed.
Step-by-step guide
- 1
Plan the service set and identifiers
Choose services: 0x10 DiagnosticSessionControl, 0x3E TesterPresent, 0x22 ReadDataByIdentifier, 0x2E WriteDataByIdentifier, 0x19 ReadDTCInformation (sub-functions 0x01 and 0x02), 0x14 ClearDiagnosticInformation, 0x31 RoutineControl and 0x11 ECUReset. Define data identifiers such as 0xF190 VIN-style serial (writable), and project DIDs for cell voltage, temperature and potentiometer value. Define one routine, such as a relay self-test. Keep this specification as the single reference for both the firmware and the Python tester, and update it whenever a byte layout changes during development.
Checkpoint: A service and DID specification with request and response byte layouts and session rules.
- 2
Configure CAN
Turn on the DIP switches for CAN, battery, potentiometer, relay, LCD and UART blocks. Configure bxCAN at 500 kbit/s and set a filter so the controller receives the physical request ID (for example 0x7E0) and functional ID 0x7DF. Use 0x7E8 for responses. Connect the USB-CAN adapter with proper 120 ohm termination at both ends. Check that the functional ID reaches the same dispatcher as physical requests.
Checkpoint: Frames sent from the PC to 0x7E0 are received and echoed back on 0x7E8.
- 3
Implement the ISO-TP layer
Write receive and transmit state machines for single frames, first frames, consecutive frames and flow control, with sequence numbers and timeouts (N_Bs, N_Cr). Use a buffer large enough for your longest message. Respect the tester's separation time when transmitting multi-frame responses, using a timer rather than blocking delays. Test with different block sizes and separation times requested by the tester, including block size zero, which means the sender may send all frames without waiting.
Checkpoint: A 50-byte message transfers correctly in both directions, and a missing consecutive frame causes a timeout and a clean reset.
- 4
Build the UDS dispatcher
Write a dispatcher that reads the service ID and calls a handler, checking message length and session permission first. Return negative responses with the correct codes for unsupported services, wrong length or wrong session. Implement suppress-positive-response for sub-functions where the bit is set. Keep a table of services with their allowed sessions and minimum lengths, so permission checks are data rather than scattered if-statements. This makes adding services later much safer.
Checkpoint: Unsupported and malformed requests receive the correct negative response codes.
- 5
Implement session and timing services
Implement 0x10 with default and extended sessions, returning P2 and P2* timing values in the response. Start an S3 timer in the extended session that returns to default after about five seconds without requests, and reset it on any request including 0x3E. Show the current session on the LCD. Also handle a session change request while a multi-frame transfer is in progress, and document the behaviour you chose, since the standard leaves some choices to the implementer.
Checkpoint: The session falls back to default after the timeout unless TesterPresent is sent.
- 6
Implement data and DTC services
Implement 0x22 for one or several DIDs per request, reading live ADC values in scaled units. Implement 0x2E for the writable serial DID only in the extended session, storing it in SPI flash. Implement 0x19 and 0x14 with a small DTC table such as over-temperature and throttle out of range, using UDS status bits. Return 0x31 for unknown DIDs and 0x7F for writes attempted in the default session.
Checkpoint: Live values read correctly, the written serial persists through power cycling, and DTCs read and clear.
- 7
Implement routine control and reset
Implement 0x31 start and request-results for the relay self-test: toggle the general output relay and report success. Implement 0x11 hard reset by sending the positive response and then calling NVIC_SystemReset after the frame is transmitted. For the routine, check that a request-results call before the routine has started returns the negative response 0x24, request sequence error. For reset, confirm that session state, security state and any partly received ISO-TP message are cleared after the restart.
Checkpoint: The routine runs and reports results, and the ECU resets and comes back in the default session.
- 8
Build and run the PC tester
Write a Python tester using python-can, and cross-check with the udsoncan and can-isotp packages if available. Script a test suite covering every service, every negative response code you support and the session timeouts. Log all frames with timestamps. Make every test check the exact response bytes, not just whether a reply arrived. Run the full suite after each firmware change so regressions are caught at once, and save a summary that lists each service with a pass or fail result.
Checkpoint: The full test suite passes, with logs saved.
- 9
Measure timing
Measure response times from request to response on the PC log and check they are within the P2 value you declared. Measure multi-frame throughput at different block sizes and separation times. Check that diagnostics do not disturb the kit's other tasks. Run a background load on the kit, such as LCD updates and ADC sampling, during timing tests so the figures represent a busy ECU. Repeat each measurement many times and report the worst case as well as the average.
Checkpoint: A timing table showing all responses within P2.
- 10
Document the stack
Write up the architecture of transport, dispatcher and services, the service specification, the test suite and timing results. State which parts of ISO 14229 you implemented and which you did not, such as security access and flash programming. Include message sequence charts for a single-frame read, a multi-frame read and a session timeout. Add the full test suite results and a table listing each supported service, its sub-functions, the sessions where it is allowed and its negative responses.
Checkpoint: A report and a live demo with the Python tester.
Testing & validation
- Multi-frame ISO-TP transfers with flow control in both directions.
- Correct negative response codes for each error case.
- Session timeout and TesterPresent behaviour verified.
- Response times within the declared P2 limit.
Expected results
A working UDS server subset with ISO-TP on the STM32, a scripted Python tester and logged evidence that services, sessions, negative responses and timing behave as specified.
Take it further
- Add 0x27 SecurityAccess with a seed and key.
- Add 0x34, 0x36 and 0x37 for firmware download to SPI flash.
- Add 0x2F InputOutputControl for the RGB LEDs.
- Support extended 29-bit addressing.
Viva and review questions
What is the positive response to 0x22?
0x62, which is the service ID plus 0x40, followed by the DID and its data.
Why does ISO-TP need flow control?
A receiver may have limited buffers or processing time. The flow-control frame tells the sender how many frames to send in a block and the minimum gap between them.
What happens if the tester stops sending requests in the extended session?
The S3 timer expires after about five seconds and the ECU returns to the default session, which locks the extended-session services again.