OBD-II K-line Protocol
OBD-II over K-line (ISO 9141-2 and ISO 14230 / KWP2000) is a standard vehicle diagnostic protocol. Unlike BMW I/K-Bus which is multi-master and always-on, K-line is master/slave — the tester (ESP32) initiates all communication, and the ECU only responds when asked.
The K-Line library implements K-line support through KLineObd2, which handles initialization, request/response framing, echo clearing, checksum validation, and session keepalive.
Protocol Parameters
Section titled “Protocol Parameters”| Parameter | Value |
|---|---|
| Baud rate | 10400 |
| Framing | 8N1 (8 data bits, no parity, 1 stop bit) |
| Checksum | Additive sum mod 256 |
| Bus topology | Master/slave (tester initiates) |
| Bus type | Single-wire, half-duplex |
| Idle state | HIGH |
| Session timeout (P3max) | ~5 seconds |
Initialization
Section titled “Initialization”K-line ECUs require an initialization sequence before they accept diagnostic requests. Two methods are supported — the ECU determines which one it responds to.
5-Baud Slow Init (ISO 9141-2)
Section titled “5-Baud Slow Init (ISO 9141-2)”The older, more widely supported method. Sends the target ECU address at 5 baud (200ms per bit), then negotiates baud rate and protocol capabilities.
Full sequence:
- Hold TX HIGH for 300ms (W0 idle period)
- Bit-bang target address (default
0x33) at 5 baud: start bit + 8 data bits LSB-first + stop bit. Total: 2 seconds for one byte - Re-attach UART at 10400 baud
- Flush RX buffer (UART received garbage during the 2-second bit-bang)
- Read sync byte (
0x55) from ECU — confirms baud rate agreement (W1: 20-300ms timeout) - Read keyword byte 1 (KW1) from ECU (W2: 5-20ms between bytes)
- Read keyword byte 2 (KW2) from ECU
- Wait 25-50ms (W3)
- Send inverted KW2 (
~KW2) back to ECU — tester confirms handshake - Verify echo of inverted KW2 (half-duplex bus reflection)
- Read inverted target address from ECU — final acknowledgment
The target address 0x33 is the ISO 9141 “all ECUs” broadcast. The keyword bytes declare the ECU’s protocol capabilities.
Implementation detail: The ESP32 UART peripheral cannot generate 5 baud. The library detaches the UART from the TX pin using pinMatrixOutDetach(), bit-bangs using digitalWrite() with 200ms delays, then re-attaches the UART for normal communication.
Fast Init (ISO 14230 / KWP2000)
Section titled “Fast Init (ISO 14230 / KWP2000)”The faster method. Sends a timed pulse followed by a StartCommunication service request.
Full sequence:
- Hold TX HIGH for 300ms (idle)
- TiniPulse: TX LOW for 25ms, then TX HIGH for 25ms
- Re-attach UART, flush RX
- Send StartCommunication request:
[0xC1, 0x33, 0xF1, 0x81]+ checksum0xC1: format byte (functional addressing, 1 data byte)0x33: target (functional address)0xF1: source (tester)0x81: StartCommunication service ID
- Verify echo (5 bytes on half-duplex bus)
- Read positive response (
0xC1= service ID0x81+0x40) - Extract keyword bytes from response
The scanner sketch tries slow init first, then falls back to fast init if slow init fails.
Half-Duplex Echo
Section titled “Half-Duplex Echo”K-line is a single wire. Everything the tester transmits appears on the RX line as well. After every transmission, the library reads back and verifies each echo byte against what was sent.
bool clearEcho(const uint8_t* expected, uint8_t count);A mismatch indicates bus contention — another device was transmitting at the same time. Init sequences abort on echo mismatch rather than trying to parse a corrupted response.
ISO 14230 Frame Format
Section titled “ISO 14230 Frame Format”Both requests and responses use the ISO 14230 frame structure:
[Format] [Target] [Source] [Data...] [Checksum] or[Format] [Target] [Source] [Length] [Data...] [Checksum]Format Byte
Section titled “Format Byte”The first byte encodes the addressing mode and data length:
| Bits | Field | Values |
|---|---|---|
| 7:6 | Address mode | 0=none, 1=CARB (unsupported), 2=physical, 3=functional |
| 5:0 | Data length | 0=separate length byte follows, 1-63=inline length |
When address mode is 2 or 3, two address bytes follow (target + source). When the data length field is 0, a separate length byte appears after the address bytes.
The library’s parseFrameData() decodes this structure and returns the offset of the first data byte (service ID), regardless of which header format the ECU uses.
Checksum
Section titled “Checksum”Additive sum modulo 256 of all bytes except the checksum itself:
checksum = (byte[0] + byte[1] + ... + byte[N-1]) & 0xFFThe library validates the checksum on every received frame. Frames with invalid checksums are rejected.
Request/Response Cycle
Section titled “Request/Response Cycle”Sending a PID Request
Section titled “Sending a PID Request”uint8_t requestPid(uint8_t mode, uint8_t pid, uint8_t* response, uint8_t maxLen);The library constructs an ISO 14230 frame with physical addressing:
[0x82] [ECU_ADDR] [TESTER_ADDR] [MODE] [PID] [CHECKSUM]0x82: physical addressing, 2 data bytes- ECU address: from init handshake
- Tester address:
0xF1
The request is transmitted, echo bytes are cleared and verified, and the response is read with a 2-second timeout.
Positive Response
Section titled “Positive Response”A positive response has the service ID = request mode + 0x40. For a Mode 01 request:
Request: mode=0x01, pid=0x0C (RPM)Response: service_id=0x41, pid=0x0C, data_A, data_BThe library verifies the response service ID and PID match before returning data bytes.
Negative Response
Section titled “Negative Response”If the ECU returns service 0x7F, it’s a negative response. The third data byte is the Negative Response Code (NRC):
| NRC | Meaning |
|---|---|
0x10 | General reject |
0x11 | Service not supported |
0x12 | Sub-function not supported |
0x22 | Conditions not correct |
0x78 | Response pending (ECU needs more time) |
requestPid() returns 0 on negative response. In debug mode (-DKLINE_DEBUG), the NRC code is printed to serial.
Common PIDs and Decode Formulas
Section titled “Common PIDs and Decode Formulas”These are Mode 01 (current data) PIDs from SAE J1979. The Obd2Pids.h header provides constants and inline decode functions for each.
Single-Byte PIDs
Section titled “Single-Byte PIDs”| PID | Hex | Description | Formula | Unit | Range |
|---|---|---|---|---|---|
| Engine Load | 0x04 | Calculated engine load | A * 100 / 255 | % | 0-100 |
| Coolant Temp | 0x05 | Engine coolant temperature | A - 40 | C | -40 to 215 |
| Short Fuel Trim | 0x06 | Short term fuel trim, bank 1 | A / 1.28 - 100 | % | -100 to 99.2 |
| Long Fuel Trim | 0x07 | Long term fuel trim, bank 1 | A / 1.28 - 100 | % | -100 to 99.2 |
| Intake Pressure | 0x0B | Intake manifold pressure | A | kPa | 0-255 |
| Speed | 0x0D | Vehicle speed | A | km/h | 0-255 |
| Timing Advance | 0x0E | Timing advance | A / 2 - 64 | deg BTDC | -64 to 63.5 |
| Intake Temp | 0x0F | Intake air temperature | A - 40 | C | -40 to 215 |
| Throttle | 0x11 | Throttle position | A * 100 / 255 | % | 0-100 |
| Baro Pressure | 0x33 | Barometric pressure | A | kPa | 0-255 |
| Fuel Level | 0x2F | Fuel tank level | A * 100 / 255 | % | 0-100 |
| Oil Temp | 0x5C | Engine oil temperature | A - 40 | C | -40 to 210 |
Two-Byte PIDs
Section titled “Two-Byte PIDs”| PID | Hex | Description | Formula | Unit | Range |
|---|---|---|---|---|---|
| RPM | 0x0C | Engine RPM | (A * 256 + B) / 4 | RPM | 0-16383.75 |
| MAF Rate | 0x10 | MAF air flow rate | (A * 256 + B) / 100 | g/s | 0-655.35 |
| Run Time | 0x1F | Time since engine start | A * 256 + B | s | 0-65535 |
| Module Voltage | 0x42 | Control module voltage | (A * 256 + B) / 1000 | V | 0-65.535 |
| Fuel Rate | 0x5E | Engine fuel rate | (A * 256 + B) / 20 | L/h | 0-3212.75 |
Supported PID Discovery
Section titled “Supported PID Discovery”PID 0x00 returns a 4-byte bitmask indicating which PIDs 0x01 through 0x20 the ECU supports. PID 0x20 returns the bitmask for 0x21-0x40, and PID 0x40 for 0x41-0x60. Query these first to avoid requesting unsupported PIDs.
Diagnostic Modes
Section titled “Diagnostic Modes”| Mode | Hex | Description |
|---|---|---|
| Current Data | 0x01 | Read current sensor values (most common) |
| Freeze Frame | 0x02 | Read data snapshot from last DTC |
| Stored DTCs | 0x03 | Read stored diagnostic trouble codes |
| Clear DTCs | 0x04 | Clear stored DTCs and freeze frame |
| O2 Monitoring | 0x05 | O2 sensor monitoring test results |
| Test Results | 0x06 | On-board monitoring test results |
| Pending DTCs | 0x07 | Read pending DTCs (current drive cycle) |
| Control | 0x08 | Control of on-board systems |
| Vehicle Info | 0x09 | VIN, calibration IDs, etc. |
| Permanent DTCs | 0x0A | Permanent DTCs (cannot be cleared) |
The current K-Line implementation handles Mode 01 (current data) PID requests. Modes 03 and 04 (DTC read/clear) are planned.
Session Management
Section titled “Session Management”TesterPresent Keepalive
Section titled “TesterPresent Keepalive”K-line diagnostic sessions expire after the P3 timeout (typically 5 seconds of inactivity). The library sends TesterPresent (service 0x3E) automatically when more than 4 seconds have elapsed since the last request. This is transparent to the caller — requestPid() checks the timer internally.
// P3_KEEPALIVE_MS = 4000if (millis() - _lastRequestMs > P3_KEEPALIVE_MS) { sendTesterPresent();}Session Recovery
Section titled “Session Recovery”If the session is lost (ECU stops responding), call reset() to clear the initialized state, then re-run slowInit() or fastInit(). The scanner sketch tracks consecutive failures and re-initializes automatically after 5 total failures:
if (consecutiveFailures >= MAX_FAILURES_BEFORE_REINIT) { scanner->reset(); tryInit(); // slow init, then fast init fallback}Error Handling
Section titled “Error Handling”| Condition | Behavior |
|---|---|
| Echo mismatch | clearEcho() returns false (bus contention detected) |
| Checksum invalid | readResponse() returns 0, frame discarded |
| Timeout | readByteTimeout() returns -1 after deadline; uses remainingMs() to prevent unsigned underflow |
| Negative response | requestPid() returns 0, NRC logged in debug mode |
| Buffer overflow | Response buffer is 32 bytes; messages exceeding this are truncated (checksum cannot be validated) |
| Session expired | After 5 consecutive all-zero poll cycles, scanner calls reset() and re-initializes |
K-line Protocol Constants
Section titled “K-line Protocol Constants”Defined in Obd2Pids.h:
namespace obd2 { constexpr uint8_t SYNC_BYTE = 0x55; // ECU sync response constexpr uint8_t DEFAULT_TARGET = 0x33; // ISO 9141 default ECU address constexpr uint8_t TESTER_ADDR = 0xF1; // external test equipment constexpr uint8_t FUNC_ADDR = 0x33; // functional addressing constexpr uint8_t KWP_FMT_NO_ADDR = 0x00; // no address info constexpr uint8_t KWP_FMT_PHYS_ADDR = 0x80; // physical addressing constexpr uint8_t KWP_FMT_FUNC_ADDR = 0xC0; // functional addressing}