OBD2 Transport Layer, Diagnostic Trouble Codes, and UDS
Document ID: OBD-02 Series: Telematics Tutorial Series Target audience: Advanced — you should understand On-Board Diagnostics (OBD2) service modes and Parameter Identifiers (PID) (OBD-01) and be comfortable with hexadecimal byte-level protocol analysis. For OBD-II fundamentals, see OBD-01. For diagnostic tools and python-OBD, see OBD-03.
Learning Objectives
By the end of this document, you will be able to:
- Describe ISO-TP (ISO 15765-2) frame types (Single Frame, First Frame, Consecutive Frame, Flow Control) and how they enable multi-frame diagnostic messages
- Trace a complete multi-frame ISO-TP transfer from request to reassembled response
- Decode the full Diagnostic Trouble Code (DTC) structure: system letter, category, subcategory, and fault code
- Read and interpret freeze frame data associated with DTCs
- Describe the key Unified Diagnostic Services (UDS) (ISO 14229) Service Identifiers (SID) and their relationship to OBD2
- Understand UDS Security Access and its role in protecting Electronic Control Unit (ECU) programming
1. ISO-TP: Transport Protocol for CAN Diagnostics
ISO-TP (International Organization for Standardization (ISO) Transport Protocol, defined in ISO 15765-2) is the transport layer protocol that enables messages larger than a single Controller Area Network (CAN) frame to be transmitted over CAN. A single CAN frame carries at most 8 bytes of payload (7 usable bytes after the Protocol Control Information (PCI) byte). Many diagnostic messages — VIN requests, DTC lists, firmware blocks — exceed this limit. For CAN physical layer fundamentals, see CAN-01.
ISO-TP segments large messages into multiple CAN frames with flow control, reassembly, and error handling.
1.1 ISO-TP Frame Types
ISO-TP defines four frame types, identified by the upper nibble of the first data byte (the PCI byte) (see Diagram D-OBD02-01 in the diagrams/ directory):
packet-beta 0-3: "0 (SF)" 4-7: "DL (1-7)" 8-63: "Data (1-7 bytes) + Padding"
Figure: OBD02 01 isotp frame types
| Frame type | PCI nibble | Abbreviation | Purpose |
|---|---|---|---|
| Single Frame | 0x0 | SF | Complete message in one CAN frame (1–7 bytes) |
| First Frame | 0x1 | FF | First segment of a multi-frame message |
| Consecutive Frame | 0x2 | CF | Subsequent segments of a multi-frame message |
| Flow Control | 0x3 | FC | Receiver controls the sender’s transmission rate |
ISO-TP Padding: ISO-TP frames are padded to the full CAN frame length (8 bytes for classical CAN, up to 64 bytes for CAN Flexible Data-rate (CAN FD)). The padding byte is typically
0xCCor0xAA(manufacturer-dependent). When decoding, ignore bytes beyond the declared data length.
1.2 Single Frame (SF)
Used when the entire message fits in a single CAN frame (1–7 payload bytes):
Byte 0: [0 | DL] PCI: upper nibble = 0, lower nibble = data length (1-7)
Bytes 1-7: [data...] Message payload
Example — OBD2 request for engine RPM:
[02, 01, 0C, 00, 00, 00, 00, 00]
│ │ │
│ │ └── PID 0x0C
│ └────── SID 0x01
└─────────── PCI: SF, DL=2 (2 payload bytes follow)
1.3 First Frame (FF)
Sent when the message exceeds 7 bytes. The FF contains the total message length and the first 6 bytes of data:
Byte 0: [1 | DL_high] PCI: upper nibble = 1, lower nibble = high 4 bits of total length
Byte 1: [DL_low] Low 8 bits of total length
Bytes 2-7: [data...] First 6 bytes of message data
Total length = (DL_high << 8) | DL_low (12 bits, max 4095 bytes)
For messages > 4095 bytes, the FF uses an extended format:
Bytes 0-1: [10, 00] PCI: FF with length = 0 (signals extended format)
Bytes 2-5: [32-bit length] Total message length (up to 4,294,967,295 bytes)
Bytes 6-7: [data...] First 2 bytes of message data
1.4 Consecutive Frame (CF)
Carries subsequent segments of a multi-frame message:
Byte 0: [2 | SN] PCI: upper nibble = 2, lower nibble = sequence number (0-F)
Bytes 1-7: [data...] Next 7 bytes of message data
The sequence number (SN) starts at 1 for the first CF (the FF is implicitly SN=0), increments to 0xF, then wraps to 0.
1.5 Flow Control (FC)
Sent by the receiver after receiving a FF, to tell the sender how to proceed:
Byte 0: [3 | FS] PCI: upper nibble = 3, lower nibble = flow status
Byte 1: [BS] Block Size (BS): number of CFs before next FC (0 = send all remaining)
Byte 2: [STmin] Separation Time minimum (STmin): minimum delay between consecutive frames
Flow Status (FS):
0 = Continue To Send (CTS) — sender may continue
1 = Wait — sender must wait for another FC
2 = Overflow — abort transfer (receiver buffer full)
STmin encoding:
| STmin value | Separation time |
|---|---|
| 0x00 | No delay |
| 0x01–0x7F | 1–127 ms |
| 0x80–0xF0 | Reserved |
| 0xF1–0xF9 | 100–900 µs (in 100 µs steps) |
| 0xFA–0xFF | Reserved |
1.6 Complete Multi-Frame Transfer Example
Scenario: Request the Vehicle Identification Number (VIN) (17 American Standard Code for Information Interchange (ASCII) characters, 20 bytes total response payload including service response bytes) (see Diagram D-OBD02-02 in the diagrams/ directory for the complete multi-frame sequence flow).
sequenceDiagram
participant T as Tester (0x7E0)
participant E as ECU (0x7E8)
T->>E: SF: [02 09 02 ...]
Request VIN
Note over T,E: N_Bs timeout starts
E->>T: FF: [10 14 49 02 01 57 44 42]
Total length=20, first 6 data bytes
T->>E: FC: [30 00 00 ...]
CTS, BS=0, STmin=0
Note over T,E: STmin gap (0ms)
E->>T: CF: [21 52 46 36 31 4A 37 36]
SN=1, 7 data bytes
E->>T: CF: [22 41 30 31 32 33 34 35]
SN=2, 7 data bytes (last)
Note over T: Reassemble: VIN = WDBRF61J76A012345
Figure: OBD02 02 multiframe sequence
Step 1: Tester sends Single Frame request
Tester → ECU: 0x7DF [02, 09, 02, 00, 00, 00, 00, 00]
SF, DL=2, SID=0x09, PID=0x02
Step 2: ECU sends First Frame with total length and first data
ECU → Tester: 0x7E8 [10, 14, 49, 02, 01, 57, 44, 42]
FF, total_len=20, SID=0x49, PID=0x02,
num_items=1, VIN starts: "WDB"
Step 3: Tester sends Flow Control
Tester → ECU: 0x7E0 [30, 00, 00, 00, 00, 00, 00, 00]
FC, FS=CTS, BS=0 (send all), STmin=0 (no delay)
Step 4: ECU sends Consecutive Frames
ECU → Tester: 0x7E8 [21, 52, 46, 36, 31, 4A, 37, 36]
CF, SN=1, "RF61J76"
ECU → Tester: 0x7E8 [22, 41, 30, 31, 32, 33, 34, 35]
CF, SN=2, "A012345"
Step 5: Reassemble
Payload: 49 02 01 57 44 42 52 46 36 31 4A 37 36 41 30 31 32 33 34 35
VIN bytes: 57 44 42 52 46 36 31 4A 37 36 41 30 31 32 33 34 35
VIN (ASCII): W D B R F 6 1 J 7 6 A 0 1 2 3 4 5
# Reassembling an ISO-TP multi-frame response
ff_data = bytes([0x49, 0x02, 0x01, 0x57, 0x44, 0x42]) # From FF (bytes 2-7)
cf1_data = bytes([0x52, 0x46, 0x36, 0x31, 0x4A, 0x37, 0x36]) # From CF SN=1
cf2_data = bytes([0x41, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35]) # From CF SN=2
total_payload = ff_data + cf1_data + cf2_data
total_length = 20 # From FF header
payload = total_payload[:total_length]
# Skip SID (0x49) and PID (0x02) and item count (0x01)
vin_bytes = payload[3:]
vin = vin_bytes.decode('ascii')
print(f"VIN: {vin}")
# Output
VIN: WDBRF61J76A012345
1.7 ISO-TP Timing Parameters
| Parameter | Symbol | Default | Range | Description |
|---|---|---|---|---|
| N_As | — | 1000 ms | 0–1000 ms (typical 25–70 ms) | Timeout for sender to transmit a frame |
| N_Ar | — | 1000 ms | 0–1000 ms (typical 25–70 ms) | Timeout for receiver to transmit FC |
| N_Bs | — | 1000 ms | 0–1000 ms (typical 10–150 ms) | Timeout between FF and FC |
| N_Cr | — | 1000 ms | 0–1000 ms, must honor STmin | Timeout between consecutive CFs |
| STmin | — | 0 ms | 0–127 ms | Minimum separation time |
| Block Size | BS | 0 | 0–255 | Frames per FC block (0 = unlimited) |
⚠️ Warning: If any timeout is exceeded during an ISO-TP transfer, the entire transfer is aborted. Both sender and receiver must handle timeout errors gracefully. The most common cause of ISO-TP timeout failures is: (1) the tester failing to send the FC after receiving the FF, or (2) bus congestion delaying CFs beyond the N_Cr timeout.
CAN FD and WWH-OBD: CAN FD transport for diagnostics is defined in ISO 15765-2:2016 and used by World-Wide Harmonized OBD (WWH-OBD) (ISO 27145). WWH-OBD uses UDS over CAN FD for emission diagnostics in Euro VI+ heavy-duty vehicles. CAN FD frames carry up to 64 data bytes, significantly reducing the number of consecutive frames needed for large transfers. For J1939 transport protocols (BAM, CMDT), see J19-02. For CAN FD implementation details, see J19-03.
2. DTC Structure and Encoding
2.1 DTC Format Recap
Each OBD2 DTC is a 5-character code (covered briefly in OBD-01). The encoding is defined by Society of Automotive Engineers (SAE) standard J2012. This section provides the complete encoding detail (see Diagram D-OBD02-03 in the diagrams/ directory for a visual breakdown of the DTC bit-field structure).
packet-beta 0-1: "System (P=00)" 2-3: "Cat (0=00)" 4-7: "3rd char (4=0100)" 8-11: "4th char (2=0010)" 12-15: "5th char (0=0000)"
Figure: OBD02 03 dtc structure
DTC binary encoding (2 bytes):
Byte 1: [S1 S0 | C1 C0 | D3 D2 D1 D0]
Byte 2: [D7 D6 D5 D4 | D3 D2 D1 D0]
Where:
S1 S0 = System (2 bits): 00=P, 01=C, 10=B, 11=U
C1 C0 = Category (2 bits): 00-11 → digit 0-3
D3-D0 (byte 1 lower nibble) = Third character (hex digit)
D7-D0 (byte 2) = Fourth and fifth characters
2.2 Complete DTC Decode Example
def decode_dtc_full(byte1, byte2):
"""Decode a 2-byte OBD2 DTC with full detail."""
# System letter (bits 7-6 of byte 1)
system_bits = (byte1 >> 6) & 0x03
system = ['P', 'C', 'B', 'U'][system_bits]
system_name = ['Powertrain', 'Chassis', 'Body', 'Network'][system_bits]
# Category digit (bits 5-4 of byte 1)
category = (byte1 >> 4) & 0x03
# Third character (bits 3-0 of byte 1)
third = byte1 & 0x0F
# Fourth and fifth characters (byte 2)
fourth = (byte2 >> 4) & 0x0F
fifth = byte2 & 0x0F
code = f"{system}{category}{third:X}{fourth:X}{fifth:X}"
category_desc = {
0: "SAE generic",
1: "Manufacturer specific",
2: "SAE generic",
3: "SAE/manufacturer joint"
}
return code, system_name, category_desc[category]
# Decode several common DTCs
examples = [(0x04, 0x20), (0x01, 0x33), (0x01, 0x71), (0x43, 0x00)]
for b1, b2 in examples:
code, system_name, cat = decode_dtc_full(b1, b2)
print(f"0x{b1:02X} 0x{b2:02X} → {code} ({system_name}, {cat})")
# Output
0x04 0x20 → P0420 (Powertrain, SAE generic)
0x01 0x33 → P0133 (Powertrain, SAE generic)
0x01 0x71 → P0171 (Powertrain, SAE generic)
0x43 0x00 → C0300 (Chassis, SAE generic)
2.3 DTC Status Byte
Each DTC has an associated status byte that indicates its current state (see Diagram D-OBD02-03 in the diagrams/ directory for a visual representation of the DTC status byte bit fields). The status byte is an 8-bit bitmask:
| Bit | Name | Meaning when set (1) |
|---|---|---|
| 0 | testFailed | DTC test failed this monitoring cycle |
| 1 | testFailedThisOperationCycle | DTC test failed at least once this operation cycle |
| 2 | pendingDTC | DTC is pending (not yet confirmed) |
| 3 | confirmedDTC | DTC is confirmed (stored, MIL may be on) |
| 4 | testNotCompletedSinceLastClear | Test has not run since DTCs were cleared |
| 5 | testFailedSinceLastClear | Test has failed at least once since last clear |
| 6 | testNotCompletedThisOperationCycle | Test has not completed this driving cycle |
| 7 | warningIndicatorRequested | Malfunction Indicator Lamp (MIL) (check engine light) is requested |
2.4 UDS 3-Byte DTC Format
UDS uses a 3-byte DTC format (ISO 14229-1) that extends the 2-byte OBD-II format with a Failure Type Byte (FTB):
| Byte | Content | Example |
|---|---|---|
| Byte 1 (High) | DTC category + high nibble | 0x04 → P04xx |
| Byte 2 (Low) | DTC low byte | 0x20 → P0420 |
| Byte 3 | FTB | 0x11 → short to ground |
Comparison with OBD-II 2-byte DTCs:
| Feature | OBD-II (2-byte) | UDS (3-byte) |
|---|---|---|
| Format | 0x0420 → P0420 |
0x04 0x20 0x11 → P0420-11 |
| Failure detail | None (just the code) | Failure Type Byte describes the nature of the failure |
| Total unique codes | ~65,536 | ~16.7 million (65,536 × 256 FTBs) |
Common Failure Type Byte values (ISO 14229-1, Annex D):
| FTB | Meaning |
|---|---|
| 0x00 | No failure type information |
| 0x01 | General electrical failure |
| 0x02 | General signal failure |
| 0x04 | System internal failures |
| 0x08 | System programming failures |
| 0x09 | Algorithm-based failures |
| 0x0A | Calibration/software failures |
| 0x11 | Short to ground |
| 0x12 | Short to battery |
| 0x13 | Open circuit |
| 0x14 | Short to ground or open |
| 0x15 | Short to battery or open |
| 0x16 | Circuit voltage below threshold |
| 0x17 | Circuit voltage above threshold |
| 0x1C | Component internal failure |
| 0x1F | No signal |
| 0x29 | Signal invalid |
| 0x40 | Signal stuck low |
| 0x41 | Signal stuck high |
| 0x42–0x4F | Signal stuck range (reserved subtypes) |
| 0x80–0xFE | ISO/SAE reserved or manufacturer-specific |
| 0xFF | No matching failure type |
The FTB allows a single DTC base code (e.g., P0420) to carry different failure modes (short to ground vs. open circuit vs. signal stuck), giving workshops more precise diagnostic information than the 2-byte OBD-II encoding alone.
def decode_uds_dtc(dtc_bytes: bytes) -> dict:
"""Decode a 3-byte UDS DTC."""
high = dtc_bytes[0]
low = dtc_bytes[1]
ftb = dtc_bytes[2]
categories = {0: 'P', 1: 'C', 2: 'B', 3: 'U'}
category = categories.get((high >> 6) & 0x03, '?')
code = f"{category}{(high >> 4) & 0x03}{high & 0x0F:01X}{low:02X}"
ftb_names = {
0x00: 'no sub-type', 0x11: 'short to ground',
0x12: 'short to battery', 0x13: 'open circuit',
0x1C: 'component internal failure'
}
ftb_name = ftb_names.get(ftb, f'0x{ftb:02X}')
return {'code': code, 'ftb': ftb, 'ftb_name': ftb_name,
'full': f"{code}-{ftb:02X}"}
# Example
dtc = decode_uds_dtc(bytes([0x04, 0x20, 0x11]))
print(f"DTC: {dtc['full']} ({dtc['ftb_name']})")
# Output: DTC: P0420-11 (short to ground)
3. Freeze Frame Data
When a DTC is stored, the ECU captures a snapshot of key sensor values at the moment the fault was detected. This snapshot is called a freeze frame and is accessible through OBD2 Service 0x02.
3.1 Freeze Frame Structure
A freeze frame contains the same PIDs as Service 0x01 (current data), but the values represent the state of the vehicle when the DTC was first triggered. Common freeze frame data includes:
- Engine RPM
- Vehicle speed
- Engine coolant temperature
- Calculated engine load
- Fuel system status
- Short/long-term fuel trims
- Intake manifold pressure
- DTC that triggered the freeze frame
3.2 Reading Freeze Frame Data
Request: 02 <PID> <Frame_Number>
Response: 42 <PID> <data bytes>
Example: Read engine RPM from freeze frame 0
Request: 02 0C 00
Response: 42 0C 1A F8 → RPM = ((0x1A × 256) + 0xF8) / 4 = 1726 RPM
The freeze frame number (third byte in the request) is typically 0x00 for the most recent freeze frame. Some vehicles store multiple freeze frames.
3.3 Freeze Frame Query and Display
# pip install obd
import obd
connection = obd.OBD()
# Query freeze frame DTCs first (Service 0x02, PID 0x02)
ff_dtc = connection.query(obd.commands.FREEZE_DTC)
if ff_dtc.is_null():
print("No freeze frame data available.")
else:
print(f"Freeze frame DTC: {ff_dtc.value}")
# Query freeze frame parameters (same PIDs as Service 0x01, but via Service 0x02)
# python-OBD handles the service mode internally
ff_pids = [
("RPM", obd.commands.RPM),
("SPEED", obd.commands.SPEED),
("COOLANT_TEMP", obd.commands.COOLANT_TEMP),
("ENGINE_LOAD", obd.commands.ENGINE_LOAD),
("SHORT_FUEL_TRIM_1", obd.commands.SHORT_FUEL_TRIM_1),
("LONG_FUEL_TRIM_1", obd.commands.LONG_FUEL_TRIM_1),
]
print("\nFreeze Frame Snapshot:")
print("-" * 40)
for name, cmd in ff_pids:
# Query with frame=0 for freeze frame
resp = connection.query(cmd)
if not resp.is_null():
print(f" {name:25s} {resp.value}")
else:
print(f" {name:25s} N/A")
print("-" * 40)
connection.close()
4. Unified Diagnostic Services (UDS)
Unified Diagnostic Services (UDS), defined in ISO 14229, is a comprehensive diagnostic protocol that extends far beyond OBD2’s emissions-focused scope. UDS provides services for ECU programming, security access, routine control, and manufacturer-specific diagnostics.
Historical context: UDS supersedes Keyword Protocol 2000 (KWP2000) (ISO 14230), which was the primary diagnostic protocol before CAN became dominant. KWP2000 ran over K-line serial buses and used a similar service-based model. Many UDS SIDs (e.g., 0x10, 0x22, 0x27) map directly to their KWP2000 predecessors. European On-Board Diagnostics (EOBD) (the European equivalent of OBD-II, mandated by EU Directive 98/69/EC) also uses UDS for extended diagnostics beyond the basic emission scope.
4.1 UDS vs OBD2
| Feature | OBD2 (SAE J1979) | UDS (ISO 14229) |
|---|---|---|
| Scope | Emissions-related diagnostics only | Full vehicle diagnostics |
| Mandatory | Yes (legal requirement) | No (manufacturer choice) |
| Access | Public (any scan tool) | Restricted (manufacturer tools, security access) |
| Service IDs | 0x01–0x0A (10 services) | 0x10–0x3E + manufacturer range (many services) |
| CAN IDs | 0x7DF/0x7E0–0x7EF (fixed) | Manufacturer-defined |
| Transport | ISO-TP (ISO 15765-2) | ISO-TP (ISO 15765-2) |
| DTCs | 5-character (P0xxx) | Extended 3-byte DTCs with status |
DTC Encoding Detail: OBD-II DTCs use the 2-byte format (e.g., P0420 encodes as
0x04 0x20). The first nibble of byte 1 encodes the category: 0=P0, 1=P1, 2=P2, 3=P3, 4=C0, etc. UDS extends this with a third byte for failure-type identification (per ISO 14229 DTC format:HighByte LowByte FailureType).
4.2 Key UDS Services
| SID | Name | Purpose |
|---|---|---|
| 0x10 | DiagnosticSessionControl | Switch between default, programming, and extended sessions |
| 0x11 | ECUReset | Reset the ECU (hard reset, key off/on, soft reset) |
| 0x14 | ClearDiagnosticInformation | Clear DTCs and related data |
| 0x19 | ReadDTCInformation | Read DTCs with full status and snapshot data |
| 0x22 | ReadDataByIdentifier | Read data using a Data Identifier (DID) (like PIDs but manufacturer-defined) |
| 0x23 | ReadMemoryByAddress | Read ECU memory at a specific address |
| 0x27 | SecurityAccess | Authenticate for protected operations (seed-key exchange) |
| 0x28 | CommunicationControl | Enable/disable ECU transmission or reception on specific channels |
| 0x29 | Authentication | UDS authentication service (ISO 14229-1:2020) for PKI-based ECU access control |
| 0x2E | WriteDataByIdentifier | Write data to a DID (e.g., set VIN, calibrate sensors) |
| 0x2F | InputOutputControlByIdentifier | Control actuators (e.g., activate fuel pump, move throttle) |
| 0x31 | RoutineControl | Start, stop, or query results of ECU routines |
| 0x34 | RequestDownload | Initiate a data transfer to the ECU (firmware update) |
| 0x35 | RequestUpload | Initiate a data transfer from the ECU (memory dump, calibration read-back) |
| 0x36 | TransferData | Transfer data blocks during download/upload |
| 0x37 | RequestTransferExit | Complete a data transfer |
| 0x3E | TesterPresent | Keep a diagnostic session alive (heartbeat) |
| 0x85 | ControlDTCSetting | Enable or disable DTC detection (used during programming to suppress spurious DTCs) |
4.3 UDS Diagnostic Sessions
UDS uses a session model to control which services are available (see Diagram D-OBD02-02 in the diagrams/ directory for the session transition sequence):
| Session | SID 0x10 sub-function | Available services |
|---|---|---|
| Default (0x01) | 0x01 | Basic diagnostics (read DTCs, read data) |
| Programming (0x02) | 0x02 | ECU flashing, firmware update (requires SecurityAccess) |
| Extended (0x03) | 0x03 | Advanced diagnostics, actuator control, memory access |
| Manufacturer-specific | 0x40–0x5F | Proprietary features |
| System-supplier-specific | 0x60–0x7E | Supplier-defined features |
Session Type Ranges: UDS session types 0x01–0x03 are standardized. Session types 0x40–0x5F are reserved for vehicle-manufacturer-specific use — OEMs define proprietary sessions for factory calibration, End-of-Line (EOL) programming, and internal diagnostics. Session types 0x60–0x7E are reserved for system-supplier-specific use (e.g., a Tier 1 supplier may define sessions for their own ECU development and test tooling). Some OEMs also define Hardware Breakout Module (HBM) diagnostic sessions for bench-testing ECUs outside the vehicle during development.
# Enter extended diagnostic session
Request: 10 03
Response: 50 03 [timing parameters]
4.4 Security Access (Seed-Key Authentication)
UDS Security Access (SID 0x27) protects sensitive operations (ECU programming, memory write, actuator control) behind a cryptographic challenge-response mechanism:
Step 1: Tester requests a seed
Request: 27 01 (security level 1, request seed)
Response: 67 01 AA BB CC (seed bytes: 0xAABBCC)
Step 2: Tester calculates the key from the seed
Key = manufacturer_specific_algorithm(seed)
(The algorithm is secret and manufacturer-specific)
Step 3: Tester sends the key
Request: 27 02 DD EE FF (security level 1, send key)
Response: 67 02 (success — security unlocked)
Security levels:
- Odd sub-functions (0x01, 0x03, 0x05, …) = request seed
- Even sub-functions (0x02, 0x04, 0x06, …) = send key
- The level number determines which services are unlocked
⚠️ Warning: Security Access is not intended to be bypassed. Attempting to brute-force the seed-key algorithm can trigger a lockout (the ECU refuses further attempts for a cooldown period, typically 10 seconds to 24 hours). In production vehicles, Security Access protects safety-critical ECU programming — unauthorized modification can compromise vehicle safety.
4.5 Negative Response Codes
When a UDS request fails, the ECU responds with a Negative Response (SID 0x7F) containing a Negative Response Code (NRC):
Response: 7F <SID> <NRC>
SID = the service that was requested
NRC = Negative Response Code
| NRC | Name | Meaning |
|---|---|---|
| 0x10 | generalReject | Request rejected, no specific reason |
| 0x11 | serviceNotSupported | The SID is not supported |
| 0x12 | subFunctionNotSupported | The sub-function is not supported |
| 0x13 | incorrectMessageLengthOrInvalidFormat | Wrong payload length |
| 0x22 | conditionsNotCorrect | Prerequisites not met (wrong session, engine running, etc.) |
| 0x24 | requestSequenceError | Requests sent in wrong order |
| 0x25 | noResponseFromSubnetComponent | Gateway cannot reach target ECU |
| 0x31 | requestOutOfRange | Parameter value is invalid |
| 0x33 | securityAccessDenied | SecurityAccess not unlocked |
| 0x35 | invalidKey | Wrong key in SecurityAccess |
| 0x36 | exceededNumberOfAttempts | Too many failed SecurityAccess attempts |
| 0x37 | requiredTimeDelayNotExpired | Must wait before retrying SecurityAccess |
| 0x72 | generalProgrammingFailure | ECU programming operation failed |
| 0x78 | requestCorrectlyReceivedResponsePending | ECU is processing, response will follow later |
📝 Note: NRC 0x78 (responsePending) is special — it means the ECU has accepted the request but needs more time to respond. The tester should continue waiting (reset the P2* timeout) until a positive response or a different NRC arrives. This is common during ECU flash operations that can take several seconds.
Worked example — Negative Response decoding:
Scenario: Tester requests ReadDataByIdentifier (0x22) for DID 0xF190
without first entering Extended Diagnostic Session.
Request: 22 F1 90
Response: 7F 22 22
Decode:
0x7F → Negative Response
0x22 → Rejected service: ReadDataByIdentifier
0x22 → NRC: conditionsNotCorrect (prerequisites not met)
The ECU rejected the request because the default session (0x01)
does not permit reading DID 0xF190. The tester must first send
DiagnosticSessionControl (10 03) to enter the extended session.
Scenario: Tester sends SecurityAccess send-key (27 02) with wrong key.
Request: 27 02 DE AD BE EF
Response: 7F 27 35
Decode:
0x7F → Negative Response
0x27 → Rejected service: SecurityAccess
0x35 → NRC: invalidKey (wrong key value)
After multiple failed attempts, the ECU may respond with NRC 0x36
(exceededNumberOfAttempts), followed by NRC 0x37
(requiredTimeDelayNotExpired) on subsequent retries until the
lockout timer expires.
4.6 UDS Implementation with python-udsoncan
# pip install udsoncan python-can can-isotp
import udsoncan
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.client import Client
from udsoncan.services import *
import can
import isotp
# === Connection Setup ===
# Create CAN bus connection
can_bus = can.Bus(channel='can0', interface='socketcan', bitrate=500000)
# Configure ISO-TP layer
tp_addr = isotp.Address(
addressing_mode=isotp.AddressingMode.Normal_11bits,
txid=0x7E0, # Tester → ECU
rxid=0x7E8 # ECU → Tester
)
tp_layer = isotp.NotifierBasedCanStack(bus=can_bus, address=tp_addr)
# Create UDS client with ISO-TP connection
conn = PythonIsoTpConnection(tp_layer)
client = Client(conn)
# Configure client behavior
client.config['data_identifiers'] = {
0xF190: udsoncan.AsciiCodec(17), # VIN (17 chars)
0xF187: udsoncan.AsciiCodec(20), # Spare part number
}
# === Example 1: DiagnosticSessionControl ===
client.open()
try:
# Switch to Extended Diagnostic Session
response = client.change_session(
DiagnosticSessionControl.Session.extendedDiagnosticSession
)
print(f"Session change: {response.service_data.session_type}")
# === Example 2: ReadDataByIdentifier ===
# Read VIN
response = client.read_data_by_identifier(0xF190)
vin = response.service_data.values[0xF190]
print(f"VIN: {vin}")
# Read ECU spare part number
response = client.read_data_by_identifier(0xF187)
part_num = response.service_data.values[0xF187]
print(f"Part number: {part_num}")
# === Example 3: SecurityAccess (Seed-Key Exchange) ===
# Step 1: Request seed (odd sub-function)
response = client.request_seed(access_level=0x01)
seed = response.service_data.seed
print(f"Seed received: {seed.hex()}")
# Step 2: Compute key from seed (algorithm is OEM-specific)
def compute_key(seed_bytes: bytes) -> bytes:
"""Example key computation — replace with actual OEM algorithm."""
# This is a placeholder! Real algorithms use XOR, AES, or proprietary logic
key = bytes([b ^ 0xAA for b in seed_bytes])
return key
key = compute_key(seed)
# Step 3: Send key (even sub-function = odd + 1)
response = client.send_key(access_level=0x02, key=key)
print(f"Security access: {'granted' if response.valid else 'denied'}")
# === TesterPresent Keepalive ===
# Send TesterPresent to prevent session timeout (typically every 2-4 seconds)
response = client.tester_present()
print("TesterPresent sent (session kept alive)")
finally:
client.close()
can_bus.shutdown()
Expected output:
Session change: extendedDiagnosticSession
VIN: WVWZZZ3CZWE123456
Part number: 5Q0 906 259 B
Seed received: a1b2c3d4
Security access: granted
TesterPresent sent (session kept alive)
5. OBD2 on CAN: Protocol Details (ISO 15765-4)
ISO 15765-4 defines the specific rules for using OBD2 over CAN, including addressing, timing, and protocol detection.
5.1 Protocol Detection Sequence
When a scan tool connects to a vehicle, it must determine the CAN configuration. ISO 15765-4 defines this detection sequence:
- Try 500 kbit/s, 11-bit IDs: Send request 0x7DF, look for response 0x7E8–0x7EF
- Try 250 kbit/s, 11-bit IDs: Same request/response IDs
- Try 500 kbit/s, 29-bit IDs: Send request 0x18DB33F1, look for response 0x18DAF1xx
- Try 250 kbit/s, 29-bit IDs: Same request/response IDs
The first configuration that produces a valid response is the correct one.
5.2 Response Timing
| Parameter | Value | Range | Description |
|---|---|---|---|
| P2 timeout | 50 ms | 0–50 ms default, up to 5000 ms after NRC 0x78 | Maximum time between request and response |
| P2* timeout | 5000 ms | 0–5000 ms | Extended timeout after NRC 0x78 (responsePending) |
Troubleshooting
| # | Symptom | Likely cause | Diagnostic step | Resolution |
|---|---|---|---|---|
| 1 | ISO-TP transfer aborts mid-way | FC timeout (tester didn’t send FC within N_Bs) or CF timeout (bus congestion) | Check timing between FF and FC, and between consecutive CFs | Ensure tester sends FC promptly after FF; reduce bus load if CFs are delayed |
| 2 | DTC decode produces invalid characters | Byte order swapped, or reading wrong bytes from the response | Verify the 2-byte DTC extraction starts at the correct offset | Check that bytes are read in the correct order (byte1 then byte2, not reversed) |
| 3 | UDS SecurityAccess returns NRC 0x35 (invalidKey) | Seed-key algorithm implementation is incorrect | Verify the algorithm against the OEM documentation | Use the correct manufacturer-specific seed-key algorithm |
| 4 | UDS SecurityAccess returns NRC 0x36 (exceededAttempts) | Too many failed key attempts triggered the lockout | Wait for the lockout timer to expire (10s to 24h depending on OEM) | Wait, then retry with the correct key; do not brute-force |
| 5 | Multi-frame response missing last bytes | ISO-TP reassembly error — not reading enough CFs, or padding not stripped | Count total CFs needed: ceil((total_length - 6) / 7); verify all CFs received | Fix the CF count calculation; handle last-CF padding correctly |
| 6 | NRC 0x78 keeps repeating with no final response | ECU is stuck in processing, or the operation failed silently | Count the number of 0x78 responses; most testers give up after 20-30 | Implement a maximum repeat count; if exceeded, abort and report ECU fault |
| 7 | Freeze frame data shows zeros for all PIDs | Vehicle does not store freeze frame data for the current DTC | Query Service 0x02 PID 0x00 to check supported freeze frame PIDs | Not all vehicles store all PIDs in freeze frames; this is normal for some manufacturers |
References
-
ISO 15765-2:2016 — Road vehicles — Diagnostic communication over Controller Area Network (DoCAN) — Part 2: Transport protocol and network layer services. Defines ISO-TP.
-
ISO 15765-4:2016 — Road vehicles — Diagnostic communication over Controller Area Network (DoCAN) — Part 4: Requirements for emissions-related systems. OBD2 on CAN specifics.
-
ISO 14229-1:2020 — Road vehicles — Unified diagnostic services (UDS) — Part 1: Application layer. Defines all UDS services.
-
ISO 14229-3:2019 — Road vehicles — Unified diagnostic services (UDS) — Part 3: UDS on CAN (UDSonCAN). CAN-specific UDS implementation.
-
SAE J1979:2014 — E/E Diagnostic Test Modes. OBD2 service modes and PIDs.
-
SAE J2012:2013 — Diagnostic Trouble Code Definitions. DTC format and standard codes.
-
ISO 15031-6:2015 — Diagnostic trouble code definitions. ISO equivalent of SAE J2012.
-
python-udsoncan — Python UDS library. Repository: github.com/pylessard/python-udsoncan.
-
isotp — Python ISO-TP library. Repository: github.com/pylessard/python-can-isotp.
-
ISO 27145:2012 — Road vehicles — Implementation of World-Wide Harmonized On-Board Diagnostics (WWH-OBD). Defines UDS-based emission diagnostics for heavy-duty vehicles over CAN FD.
Changelog
| Version | Date | Author | Summary of changes |
|---|---|---|---|
| 1.0 | 2026-03-16 | Telematics Tutorial Series | Initial publication |
| 1.1 | 2026-03-16 | Telematics Tutorial Series | Added ISO-TP padding note, CAN FD/WWH-OBD forward reference, DTC encoding detail note, timing parameter ranges, UDS session type ranges, expanded UDS services table, inline diagram references |
| 1.2 | 2026-03-16 | Telematics Tutorial Series | Final polish: expanded all acronyms at first use (ISO, NRC, CAN FD, FTB, STmin, BS, EOBD, KWP, EOL, HBM), added cross-references to OBD-01/OBD-03/CAN-01/J19-02, added language identifiers to all code blocks, added negative response worked examples, expanded FTB table with full ISO 14229-1 Annex D values |
```