Library dir: lib/AutoWire/ -> lib/KLine/ Site title, docs, CLAUDE.md, platformio.ini all updated. All 4 firmware environments build clean.
259 lines
11 KiB
Plaintext
259 lines
11 KiB
Plaintext
---
|
|
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
|
|
}
|
|
```
|