Skip to main content
Diagram of UDS diagnostic sessions: default to extended via service 0x10, security access 0x27 and tester present 0x3E, from GSAS Micro Systems India

UDS protocol explained: ISO 14229 services, security access

GSAS Engineering · · 14 min read

The UDS protocol, or Unified Diagnostic Services, is the request and response diagnostic service layer named by ISO 14229. It defines service identifiers, diagnostic sessions and security access for reading, writing and testing an ECU. It is transport agnostic, so the same service rides CAN, CAN FD, LIN or automotive Ethernet unchanged.

If you have just been handed a bench, a diagnostic interface and an instruction to “read the fault codes over UDS”, this article is the mental model of the UDS protocol that makes the rest of the work obvious. It covers what Unified Diagnostic Services is, the grammar every request and response follows, the sessions that gate almost everything, the seed and key exchange, and how the same service rides a CAN wire or an Ethernet link.

Every value below comes from a public source you can open without a licence: the udsoncan client documentation, the py-uds knowledge base, the can-isotp and python-doipclient projects, and the Wireshark display filter reference. The ISO text itself is paywalled, and we do not quote clauses from it here. If you are new to the physical layer underneath, start with our complete guide to automotive Ethernet, and see our automotive Ethernet capabilities page for how we work with teams on bench bring up.

What UDS actually is

A service layer, not a bus

UDS is a set of services. A service is a question you can ask an ECU, or an instruction you can give it: read this value, write that one, run this routine, reset yourself, tell me your fault memory. The standard named ISO 14229 fixes the numbering and the shape of those questions and answers. It does not fix the wire.

The same ReadDataByIdentifier request is byte for byte identical whether it arrives inside a CAN frame on a two wire bus, inside a CAN FD frame, or inside a TCP segment on an Ethernet link. Only the packaging below it changes. When an engineer says “UDS on CAN” or “UDS on IP”, they are naming the transport, not a different protocol.

Client and server, tester and ECU

UDS is strictly request and response, and it is always the client that speaks first. In the vocabulary of the standard the diagnostic tester is the client and each ECU is a server. The py-uds knowledge base describes the routing case that follows: the client is often not directly connected to the ECU it wants, so an intermediate ECU acts as a gateway and forwards the request into the correct sub network, then forwards the response back.

What UDS is not

It is not OBD2. It is not a transport. It is not a database of what your ECU supports.

That last point causes the most wasted time. The standard defines the grammar and reserves large ranges for the vehicle manufacturer and the system supplier to fill in. Which data identifiers exist, which routines run, which security levels guard what: all of that is decided per ECU and arrives with you as ODX, CDD or ARXML data, or as a supplier specification. A service being defined in the standard tells you nothing about whether the ECU in front of you implements it.

The grammar of a UDS message

Learn one shape and you can read almost any trace.

The request

A request begins with a one byte service identifier, the SID. Many services then take a sub function byte, and after that come the parameters the service needs: a data identifier, a routine identifier, a memory address, a mask.

The sub function byte is not a plain number. The py-uds knowledge base documents the split: bit 7 is the suppressPosRspMsgIndicationBit, and bits 6 to 0 carry the actual sub function value. That is why sub function values are documented in the range 0x00 to 0x7F, and why the same request appears in a trace as either 0x02 or 0x82.

The positive response

The rule is arithmetic and has no exceptions: the response service identifier equals the request service identifier plus 0x40. The py-uds knowledge base states it as an equation that is always true for positive responses.

So a request that begins 0x22 comes back beginning 0x62. A 0x10 comes back as 0x50, a 0x27 as 0x67, a 0x3E as 0x7E. Once you have internalised the offset you can read the direction of every line in a log without a decoder.

The suppress positive response bit

Set bit 7 of the sub function and you are telling the ECU not to bother answering when it succeeds. On a physically addressed request a negative response is still sent if something goes wrong.

This is the reason a perfectly good request sometimes produces silence in your capture. It is also why the classic keep alive is written as 0x3E 0x80 rather than 0x3E 0x00: the tester wants the session held open and does not want an acknowledgement for every one.

The negative response

A refusal always takes the same three byte shape, documented in the py-uds knowledge base:

ByteMeaningValue
1Negative response service identifier0x7F
2Echo of the request service identifierSID
3Negative response codeNRC

So 0x7F 0x27 0x35 reads as: your SecurityAccess request was refused, code 0x35, which the same source names invalidKey. The code is where the diagnosis lives. We have pulled the full set into a companion reference, the complete UDS negative response code table.

One code deserves early mention because it is not a fault. The py-uds knowledge base describes 0x78, requestCorrectlyReceived-ResponsePending, as meaning the request was received correctly and all parameters were valid, but the action is not yet complete, and that the server will send a proper positive or negative response once it is. Test scripts that treat any 0x7F as a failure abort here and report bugs that do not exist.

Diagnostic sessions, and why they gate everything

The four defined session types

DiagnosticSessionControl, service 0x10, changes the ECU’s operating mode. The py-uds knowledge base documents the sub function layout for this service, with these values in bits 6 to 0:

ValueSession nameWhat it is for
0x01defaultSessionThe state after power on. Limited service set
0x02programmingSessionSoftware update work
0x03extendedDiagnosticSessionMost engineering work: routines, writes, actuator control
0x04safetySystemDiagnosticSessionSafety system related diagnostics
0x40 to 0x5FVehicle manufacturer specificDefined by the OEM
0x60 to 0x7ESystem supplier specificDefined by the supplier

Only one session is active at a time, and the same source notes that the ECU enters the default session after startup.

The positive response to service 0x10 is worth a note: it carries the server’s own timing parameters back to you. That means a well written client does not need to guess how long to wait, because the ECU has just told it. Read those parameters from the response rather than hard-coding a timeout.

The S3 timer and TesterPresent

Sessions expire. The py-uds knowledge base describes TesterPresent, service 0x3E, as preventing the server’s S3Server timer from expiring and thereby extending the active session, and adds a note worth reading twice: TesterPresent performs no diagnostic action, and reception of any diagnostic request, TesterPresent or otherwise, resets that timer.

Two consequences follow. First, a busy sequence with requests flowing continuously often needs no keep alive at all. Second, a sequence with a human pause in it, or a long routine, needs one, and the moment it stops arriving the ECU drops back to the default session on its own schedule rather than yours.

The py-uds knowledge base also states that the unlocked security level is cleared whenever the diagnostic session changes or the ECU is reset. A silent session timeout therefore costs you both the session and the unlock.

Why it works in the tool and fails in your script

This question reaches us repeatedly from validation teams, and the answer is almost never syntax. A vendor tool typically opens an extended session, unlocks security and starts a keep alive before you have finished clicking. Your script sends the bare request into a default session and gets 0x7F with 0x7F serviceNotSupportedInActiveSession, or 0x7E for the sub function equivalent, or 0x33 securityAccessDenied.

Read that as a state problem, not a formatting problem. Establish the session, unlock if required, keep the session alive, then send the interesting request.

Security access 0x27, without the mystique

The py-uds knowledge base sets out the unlocking sequence plainly: the client requests a seed, the server responds with a randomly generated seed, both sides compute a key from that seed using a secret algorithm, the client sends its key, and the server compares it against its own calculation. Match, and the corresponding security level is unlocked.

The sub function carries the level and the step at once. The udsoncan source documentation states the rule for its own API: for RequestSeed the level must be an odd value, and for SendKey it must be even, with the library normalising the least significant bit if you get it wrong. In practice you see pairs: request seed at 0x01, send key at 0x02; request seed at 0x11, send key at 0x12.

Four negative response codes belong to this exchange, all named in the py-uds knowledge base and all decoded in the complete UDS negative response code table:

NRCName
0x33securityAccessDenied
0x35invalidKey
0x36exceedNumberOfAttempts
0x37requiredTimeDelayNotExpired

That last pair is why a bench goes quiet after a few bad keys and stays quiet for a while. It is designed behaviour, not a broken ECU, and the fix is to wait rather than to keep hammering.

The key algorithm itself is never in the standard. It comes from the vehicle manufacturer or the supplier, as a library, a specification or a signed service. Where those secrets are placed into the ECU in the first place is a production question rather than a diagnostic one, and it is worth understanding separately if your work touches the line.

The newer Authentication service, 0x29, is listed in the udsoncan service documentation alongside the rest, and the py-uds negative response code list carries a block of certificate related codes from 0x50 to 0x5D covering invalid validity periods, signatures, chains of trust and scopes. That is the direction of travel: certificate based authentication in place of a shared secret computation.

Fault memory: DTCs, 0x19 and 0x14

A Diagnostic Trouble Code is a stored fault record. The py-uds knowledge base documents the UDS presentation as a 24 bit value, normally written as six hexadecimal digits, distinct from the familiar letter and digit OBD presentation.

The interesting part is not the code, it is the status byte that travels with it. The same source lists eight bits: testFailed, testFailedThisOperationCycle, pendingDTC, confirmedDTC, testNotCompletedSinceLastClear, testFailedSinceLastClear, testNotCompletedThisOperationCycle and warningIndicatorRequested. A code with confirmedDTC set is a different engineering problem from one with only pendingDTC set, and reporting “fault present” without the status byte throws away most of the information.

ReadDTCInformation, service 0x19, has many sub functions; the one you will use constantly is reportDTCByStatusMask, 0x02, which asks for every stored code matching a status mask you supply. ClearDiagnosticInformation, service 0x14, takes a group of DTC parameter, and the py-uds knowledge base documents 0xFFFFFF in that field as meaning all groups.

The services you will actually use

The udsoncan documentation lists the full service set. These are the ones that appear in nearly every session:

SIDServiceTypical use
0x10DiagnosticSessionControlMove out of the default session
0x11ECUResethardReset 0x01, keyOffOnReset 0x02, softReset 0x03
0x14ClearDiagnosticInformationClear fault memory
0x19ReadDTCInformationRead fault memory and status
0x22ReadDataByIdentifierRead a value by its DID
0x27SecurityAccessSeed and key unlock
0x2EWriteDataByIdentifierWrite a value by its DID
0x31RoutineControlStart, stop and request results of a routine
0x3ETesterPresentKeep the session alive

The full identifier list, including the transfer services and the less common ones, is in our companion article, every UDS service ID with examples.

One service, three transports

Physical and functional addressing

The py-uds knowledge base draws the distinction cleanly. Physical addressing is point to point between one client and one server, and the server responds unless the request suppresses the response. Functional addressing is one to many, aimed at several ECUs at once, and servers generally do not respond to it.

That asymmetry catches people. A functionally addressed request that produces no answer may have been received and executed perfectly.

ISO-TP in plain terms

A classic CAN frame carries at most eight bytes, and while most requests fit, the responses often do not. The transport layer under UDS on CAN splits them. The py-uds knowledge base names the four packet types: 0x0 single frame, 0x1 first frame, 0x2 consecutive frame, 0x3 flow control. A single frame carries a whole message that fits in one CAN packet. Anything longer opens with a first frame, and the receiver answers with a flow control frame that sets the terms for the consecutive frames that follow.

Two of those terms cause most transport level trouble. The can-isotp documentation describes blocksize as the number of consecutive frames a sender should send before waiting for another flow control message, and the py-uds knowledge base notes that 0x00 means unlimited. It describes stmin, the minimum separation time, as 1 to 127 representing milliseconds, with the range 0xF1 to 0xF9 representing hundreds of microseconds.

CAN FD relaxes the size constraint. The py-uds knowledge base lists the CAN FD data lengths as 0 to 8 linearly, then the discrete steps 12, 16, 20, 24, 32, 48 and 64 bytes. Fewer segments, less flow control, same UDS services on top.

The same request over Ethernet

Diagnostics over IP replaces that segmentation with ordinary TCP and UDP. The python-doipclient documentation gives the practical defaults: port 13400 without TLS and 3496 with it, a default client logical address of 0x0E00, routing activation types of 0 for default and 1 for regulatory diagnostics, and a vehicle announcement a node broadcasts when it powers on. Once routing activation succeeds, the diagnostic payload you send is the same UDS request you would have sent over CAN, which is why that same project documents a connector that hands its socket straight to udsoncan.

On a modern zonal architecture that link is likely shared with service oriented traffic, so a capture will contain both diagnostic sessions and service calls; our article on SOME/IP message format and service discovery covers the other half of what you will see. For the physical and switching layer underneath, our automotive Ethernet capabilities page is the starting point, and if your interest is production line software update over that same link rather than diagnostics, we cover the XDM-ETH automotive Ethernet unit separately.

UDS and OBD2 side by side

They are not competitors and they are not the same thing. OBD2 is a legislated emissions interface: a fixed, publicly documented set of modes and parameters that any generic scan tool can read from any compliant vehicle. UDS is manufacturer territory, where the identifiers, routines and security levels are defined per ECU and documented only to those who need them. Both frequently ride the same connector and the same bus, and an ECU can answer both. If a generic tool sees your ECU but your UDS request gets nothing, you are probably talking to two different service sets on one wire.

Working with UDS in practice

Open tooling gets you a long way before any purchase decision. On Linux, SocketCAN plus the can-isotp layer gives you a segmented transport, udsoncan gives you a client that speaks the services, and python-doipclient gives you the Ethernet path. Wireshark dissects UDS directly: its display filter reference documents fields including uds.sid, uds.err.code, uds.dsc.subfunction, uds.sa.seed, uds.sa.key and uds.rdtci.dtc_id, so uds.err.code alone will pull every refusal out of a long capture.

When you log, log enough that a failure is reproducible without you: timestamps, the current session, the raw request and response bytes, the NRC on every refusal, and the transport frames underneath. A UDS log that records only decoded service names will not tell you whether the problem was in the service or in the segmentation below it.

Five mistakes that cost days

Assuming a service exists because the standard defines it. Implementation is per ECU. Get the supplier data before you write the sequence.

Treating 0x78 as an error. It means the ECU is still working, and the real answer is on its way.

Confusing manufacturer specific ranges with defined values. The session and security tables both reserve large OEM and supplier ranges. A value in those ranges means what the OEM says it means.

Blaming UDS for a transport problem. A flow control mismatch, a wrong blocksize or an stmin the sender ignores looks exactly like an unreliable ECU until you look one layer down.

Testing without the gateway. A bench with the ECU directly attached behaves differently from a vehicle where a gateway forwards and possibly filters your request, especially for functionally addressed messages.

Where GSAS fits

Much of the UDS trouble we see is not protocol trouble. It is a session that expired quietly, a security level that never unlocked, a functionally addressed request that was never going to answer, or a transport parameter mismatch wearing a diagnostic costume. Knowing which of those you have is a question of instrumenting the right layer.

GSAS Micro Systems is an engineering partner to ECU, HIL and validation teams in India. We consult on the diagnostic access path for a given bench: which transport your architecture actually needs, what you should be capturing when a session fails, and how to get a first session running against real hardware rather than a simulator. Support is in IST hours, and commercial engagement is invoiced in INR.

If you are planning a diagnostic or automotive Ethernet bench, start with our automotive Ethernet capabilities page, or send us the architecture and the problem through a request for quote and we will come back with what we would use and why.

References

Building for Automotive & Mobility?

Talk to our application engineers for personalized tool recommendations.

Frequently asked questions

What is the UDS protocol in automotive?
UDS stands for Unified Diagnostic Services. It is the request and response language a diagnostic tester uses to talk to an ECU: every request opens with a one byte service identifier, and the ECU answers with that identifier plus 0x40 on success, or with 0x7F followed by the echoed identifier and a reason code on refusal. It defines what you ask, not the bus that carries it.
Is UDS the same as OBD2?
No. OBD2 is a legislated emissions interface with a fixed, publicly defined set of modes and parameter identifiers that any scan tool can read on any compliant vehicle. UDS is a manufacturer controlled diagnostic service layer whose data identifiers, routines and security levels are defined per ECU by the maker. The two often share the same physical connector and can coexist on the same bus.
What is a DTC in the UDS protocol?
A Diagnostic Trouble Code is a stored fault record. The py-uds knowledge base documents the UDS presentation as a 24 bit value, usually written as six hexadecimal digits, accompanied by an eight bit status byte whose bits include testFailed, pendingDTC, confirmedDTC and warningIndicatorRequested. You read them with ReadDTCInformation, service 0x19, and clear them with ClearDiagnosticInformation, service 0x14.
What is session control in the UDS protocol?
DiagnosticSessionControl, service 0x10, moves the ECU between operating modes. The py-uds knowledge base documents four defined types: defaultSession 0x01, programmingSession 0x02, extendedDiagnosticSession 0x03 and safetySystemDiagnosticSession 0x04, plus manufacturer and supplier specific ranges. Most services beyond simple reads are only available outside the default session, which is why session control is almost always the first request a tester sends.
What is TesterPresent in the UDS protocol?
TesterPresent, service 0x3E, tells the ECU the tester is still connected. The py-uds knowledge base states that receiving it prevents the S3Server timer from expiring, which keeps the non default session alive. It performs no diagnostic action of its own, and any diagnostic request extends the same timer, so you only need it when your sequence would otherwise sit idle.
What does security access 0x27 do, and where does the key come from?
SecurityAccess runs a two step exchange. The client asks for a seed with an odd sub function value, the ECU returns a random seed, the client computes a key and sends it back with the next even value. The py-uds knowledge base notes the algorithm is secret, so the key routine comes from the OEM or the supplier as a library or specification, never from the standard text.
Can UDS run over automotive Ethernet?
Yes. Diagnostics over IP carries the same UDS payloads over TCP and UDP. The python-doipclient documentation lists default ports of 13400 without TLS and 3496 with it, a default client logical address of 0x0E00, and routing activation types 0 for default and 1 for regulatory diagnostics. The services and response codes above the transport are unchanged.
Where do I get the list of DIDs for a specific ECU?
Not from the standard. Data identifiers, routine identifiers and security levels are defined per ECU by the vehicle manufacturer or the supplier, and reach you as ODX, CDD or ARXML data, or as a supplier specification. The py-uds knowledge base shows large manufacturer and supplier specific ranges precisely because those allocations are made outside the standard text.

Stay in the Loop

Get monthly compliance updates, product insights, and engineering best practices delivered to your inbox.