---
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
}
```
