UDSLib separates core diagnostic logic from the input/output layer and the operating system.
Three rules define the library's structure:
- Strict Isolation: Logic runs independently of I/O and OS specifics.
- No Resource Ownership: The library does not allocate memory or create threads.
- Dependency Injection: Platform-specific functions are injected at runtime via function pointers.
The library divides into four main areas:
- Service Registry: A table-driven dispatcher that routes requests.
- Core SDU Engine: Parses and validates protocol requirements (Session, Security, Length).
- State Manager: Tracks sessions, security levels, and timers (S3, P2, P2*). It uses optional hooks for NVM persistence. An opt-in built-in session policy (
restrict_sessions) can confine privileged services to ISO-sensible sessions. - Transport Abstraction: A layer that connects to OS sockets (Zephyr/Linux) or uses the internal ISO-TP fallback.
graph TD
App[User Application] -->|uds_client_request| Core
App -->|uds_init/process| Core
Core -->|Lookup| Registry[Service Registry]
Registry -->|Handler| Ext[External/User Service]
Registry -->|Handler| Builtin[Built-in Service]
Core -->|Condition Check| Gate{Safety Gate}
Gate -->|Passed| Registry
Gate -->|Rejected| NRC[Send NRC 0x22]
Core[UDSLib Core] -->|SDU Send| TP[Transport Layer]
TP -->|CAN Frame| HW[Hardware/HAL]
HW -->|CAN Frame| TP
TP -->|SDU Input| Core
Core -->|Callback| App
A table-driven dispatcher manages UDS services (all 27 of 27 ISO 14229-1 application services are implemented; see SERVICE_COMPLIANCE.md for the authoritative matrix).
- Scalability: Adding a service (like SID 0x29) requires adding an entry to the
core_servicestable. - Extensibility: Applications register
user_servicesinuds_config_tto override or extend standard functionality. - Validation: The core engine enforces ISO 14229-1 NRC priorities (Session → Subfunction → Length → Security → Safety) before calling the handler.
For industrial and automotive safety, UDSLib implements Safety Gates.
Every potentially destructive service (Reset, Write, Download) passes through an application-provided fn_is_safe callback.
- If the callback returns
false, the stack rejects the request with NRC 0x22 (ConditionsNotCorrect). - This allows engineers to block diagnostics when the machine is in an unsafe state.
The protocol state (Active Session and Security Level) persists across power cycles using NVM Persistence Hooks.
fn_nvm_save: Runs when the session or security level changes.fn_nvm_load: Runs duringuds_initto restore the last known valid state.
The Transport Layer (ISO 15765-2) operates as a pluggable module.
- SDU (Service Data Unit): A complete UDS message (e.g.,
[0x10, 0x03]). - PDU (Protocol Data Unit): A single CAN frame (e.g.,
[0x02, 0x10, 0x03, 0x00...]).
The udslib core logic strictly consumes and produces SDUs.
If the underlying OS (like Zephyr or Linux) has a native ISO-TP stack, UDSLib communicates directly at the SDU level. This removes redundant reassembly logic.
If the OS is "dumb" (Bare Metal), UDSLib uses the uds_tp_isotp.c fallback to handle reassembly, converting SDUs into raw CAN PDUs.
To ensure MISRA-C compliance and reliability:
- The library never calls
malloc()orfree(). - The caller provides buffers for RX and TX operations.
- Message structures use fixed sizes.
The server has two entry points, and the scheduling model is defined entirely by where each one runs:
uds_input_sdu()/uds_input_sdu_addr()— the receive path. Called for each inbound SDU, from a transport task or an RX interrupt.uds_process()— the periodic tick. It is non-blocking: it usesget_time_ms()to check the S3/P2/P2* timers, advances the responsePending state machine, runs the 0x2A periodic and 0x86 ResponseOnEvent schedulers, and drains any deferred post-TX action. It must be called regularly.
uds_process() fits any substrate:
- Super loop: call once per loop.
- RTOS task: call periodically (
vTaskDelay/k_sleep). - Timer interrupt: call when a hardware timer fires.
The two entry points may run in different contexts at the same time (RX task vs.
process task, or RX interrupt vs. main loop) only when the OSAL mutex callbacks
are supplied (docs/OSAL.md). The mutex makes the two critical sections
mutually exclusive over the shared state (session, security, timers, the
post-TX action, the periodic/ROE schedule). The cross-context fields are
volatile so the compiler cannot cache a stale copy across the critical section
when the lock is a bare interrupt-disable.
Key rule: fn_tp_send is invoked outside the lock. Each entry point builds
its response under the lock, latches the length, releases the lock, then
transmits. A slow or blocking transport therefore never stalls the other context
(or, under an interrupt-disable lock, never extends interrupt latency). When RX
runs in an interrupt, the lock must be an ISR-safe critical section
(disable-IRQ / BASEPRI), not an RTOS mutex — see docs/OSAL.md. Two threads both
calling uds_input_sdu() on one context is not a supported configuration; UDS
is one-request-at-a-time.
Service state — the active session, the security level, and the transfer
(download/upload) state — commits when the handler returns and is not rolled
back if the subsequent emit fails. A handler that, say, unlocks security or
enters the programming session has already mutated the context by the time the
framework tries to put the positive response on the wire; if fn_tp_send then
rejects the frame, the state change stands.
This is deliberate. On CAN, fn_tp_send is queue-not-wire: a successful return
means the frame was accepted into a transmit mailbox, not that the tester
received it. There is no point at which the library could know a response was
truly delivered, so unwinding committed state on a queue rejection would trade a
well-defined state for a guess — and the tester resynchronises on its next
request regardless.
The only emit-gated operations are the deferred post-TX actions: ECUReset
(0x11) and the LinkControl (0x87) transition. These are disruptive (a
reboot or a bus-parameter switch) and must not happen if the tester never got its
confirmation, so the post-TX engine runs them only after the response has been
handed to the transport (emit_ok) and, when fn_tx_complete is supplied, only
after the frame has physically drained. A failed emit cancels them. Ordinary
session/security/transfer state has no such gate.