--- title: OBD-II K-line Protocol description: "ISO 9141/14230 initialization, PID requests, and session management" --- 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 | 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 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) 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:** 1. Hold TX HIGH for 300ms (W0 idle period) 2. Bit-bang target address (default `0x33`) at 5 baud: start bit + 8 data bits LSB-first + stop bit. Total: 2 seconds for one byte 3. Re-attach UART at 10400 baud 4. Flush RX buffer (UART received garbage during the 2-second bit-bang) 5. Read sync byte (`0x55`) from ECU -- confirms baud rate agreement (W1: 20-300ms timeout) 6. Read keyword byte 1 (KW1) from ECU (W2: 5-20ms between bytes) 7. Read keyword byte 2 (KW2) from ECU 8. Wait 25-50ms (W3) 9. Send inverted KW2 (`~KW2`) back to ECU -- tester confirms handshake 10. Verify echo of inverted KW2 (half-duplex bus reflection) 11. 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) The faster method. Sends a timed pulse followed by a StartCommunication service request. **Full sequence:** 1. Hold TX HIGH for 300ms (idle) 2. TiniPulse: TX LOW for 25ms, then TX HIGH for 25ms 3. Re-attach UART, flush RX 4. Send StartCommunication request: `[0xC1, 0x33, 0xF1, 0x81]` + checksum - `0xC1`: format byte (functional addressing, 1 data byte) - `0x33`: target (functional address) - `0xF1`: source (tester) - `0x81`: StartCommunication service ID 5. Verify echo (5 bytes on half-duplex bus) 6. Read positive response (`0xC1` = service ID `0x81` + `0x40`) 7. 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 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. ```cpp 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 Both requests and responses use the ISO 14230 frame structure: ``` [Format] [Target] [Source] [Data...] [Checksum] or [Format] [Target] [Source] [Length] [Data...] [Checksum] ``` ### 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 Additive sum modulo 256 of all bytes except the checksum itself: ``` checksum = (byte[0] + byte[1] + ... + byte[N-1]) & 0xFF ``` The library validates the checksum on every received frame. Frames with invalid checksums are rejected. ## Request/Response Cycle ### Sending a PID Request ```cpp 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 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_B ``` The library verifies the response service ID and PID match before returning data bytes. ### 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 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 | 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 | 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 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 | 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 ### 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. ```cpp // P3_KEEPALIVE_MS = 4000 if (millis() - _lastRequestMs > P3_KEEPALIVE_MS) { sendTesterPresent(); } ``` ### 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: ```cpp if (consecutiveFailures >= MAX_FAILURES_BEFORE_REINIT) { scanner->reset(); tryInit(); // slow init, then fast init fallback } ``` ## 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 Defined in `Obd2Pids.h`: ```cpp 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 } ```