Skip to content

Getting Started

K-Line is a multi-protocol automotive bus library for ESP32. It supports BMW I/K-Bus (9600 baud, 8E1, optocoupler isolated) and OBD-II K-line (10400 baud, 8N1, ISO 9141/14230). The firmware ships with two sketches: a BMW bus sniffer and an OBD-II scanner.

  • PlatformIO (CLI or VS Code extension)
  • An ESP32 development board (ESP32, ESP32-C3, or ESP32-S3)
  • USB cable for flashing and serial monitor

PlatformIO handles all toolchain and library dependencies automatically. No manual SDK installation needed.

Terminal window
git clone <repository-url>
cd i-k-bus-board/firmware

The default build target. Prints all bus traffic with module name lookup.

Terminal window
# Build for ESP32 (classic, dual-core)
pio run -e esp32dev
# Build for ESP32-C3 (single-core RISC-V)
pio run -e esp32-c3
# Build for ESP32-S3 (dual-core, USB native)
pio run -e esp32-s3

Polls RPM, speed, coolant temperature, throttle position, and battery voltage from an ECU via ISO 9141/14230 K-line.

Terminal window
pio run -e obd2-scanner
Terminal window
# Flash the default environment to a connected board
pio run -t upload
# Open serial monitor at 115200 baud
pio device monitor

Each ESP32 variant has different GPIO numbering. The build environments in platformio.ini set the correct pins for each board via -D build flags.

FunctionGPIOUARTNotes
Bus TXGPIO 17UART1 TXThrough R5 -> R3 -> Q1 -> U2
Bus RXGPIO 16UART1 RXFrom U1 emitter (pin 3)
Debug TXGPIO 1UART0 TXUSB serial monitor
Debug RXGPIO 3UART0 RXUSB serial monitor
Status LEDGPIO 2Activity indicator
FunctionGPIONotes
Bus TXGPIO 5UART1 TX
Bus RXGPIO 4UART1 RX
Status LEDGPIO 8Onboard LED on most C3 devkits
FunctionGPIONotes
Bus TXGPIO 16UART1 TX
Bus RXGPIO 15UART1 RX
Status LEDGPIO 48RGB LED

All pin assignments can be overridden in platformio.ini build flags without modifying source code:

build_flags =
-DIBUS_RX_PIN=4
-DIBUS_TX_PIN=5
-DIBUS_LED_PIN=8
-DIBUS_UART_NUM=1

For the OBD-II scanner, the same pins are used by default (they alias to KLINE_RX_PIN, etc.). Override with KLINE_* defines if needed:

build_flags =
-DKLINE_RX_PIN=16
-DKLINE_TX_PIN=17
-DKLINE_TX_INVERT=0

Set KLINE_TX_INVERT=1 for PC817 optocoupler circuits, 0 for direct transistor circuits.

firmware/
├── platformio.ini # 4 environments (BMW sniffer x3, OBD-II scanner)
├── include/config.h # Pin defaults + protocol constants
├── lib/KLine/
│ ├── library.json # PlatformIO lib metadata
│ ├── KLineTransport.h/.cpp # Shared hardware layer (UART, ISR, ring buffers)
│ ├── IbusHandler.h/.cpp # BMW I/K-Bus protocol FSM
│ ├── IbusEsp32.h/.cpp # Backward-compatible facade
│ ├── KLineObd2.h/.cpp # OBD-II K-line handler
│ ├── Obd2Pids.h # PID constants + SAE J1979 decode helpers
│ ├── RingBuffer.h/.cpp # Circular buffer
│ └── E46Codes.h # ~100 BMW E46 command arrays
└── src/
├── main.cpp # BMW I/K-Bus sniffer with module name lookup
└── obd2_scanner.cpp # OBD-II scanner (RPM, speed, coolant, throttle, voltage)

Listens to all I/K-Bus traffic and prints each message with human-readable module names:

50 MFL -> 68 RAD [32 11] chk=1F

That output means: MFL (steering wheel, address 0x50) sent a message to RAD (radio, address 0x68) with data bytes 32 11 and XOR checksum 1F. This is a volume-up command.

Uses the IbusEsp32 facade, so the code is minimal:

IbusEsp32 ibus;
ibus.begin(Serial1, RX_PIN, TX_PIN, LED_PIN);
ibus.onPacket(callback);
// in loop():
ibus.run();

Initializes a diagnostic session with an ECU (tries 5-baud slow init, falls back to fast init) and polls five PIDs in a loop:

  • Engine RPM
  • Vehicle speed (km/h)
  • Coolant temperature (C)
  • Throttle position (%)
  • Control module voltage (V)

Sends TesterPresent every 4 seconds to keep the session alive. After 5 consecutive failures, re-initializes the session automatically.

Uses KLineTransport and KLineObd2 directly (no facade needed):

KLineTransport transport;
KLineObd2* scanner = new KLineObd2(transport);
// Configure for OBD-II
KLineConfig config;
config.baud = 10400;
config.framing = SERIAL_8N1;
config.idleTimeoutUs = 0; // master/slave, no idle detect
config.txInvert = KLINE_TX_INVERT;
config.checksumType = CHECKSUM_MOD256;
transport.begin(Serial1, RX_PIN, TX_PIN, LED_PIN, UART_NUM, config);
scanner->slowInit(0x33);
// Poll a PID:
uint8_t data[4];
uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data));
if (len >= 2) {
float rpm = obd2::decodeRpm(data[0], data[1]);
}

After building the firmware, the recommended testing sequence is:

  1. Loopback test — wire TX to RX on the same board, verify messages round-trip through the library
  2. Bench test — 12V supply through a 1k ohm resistor to simulate the bus, inject bytes from a USB-UART adapter at 9600 baud 8E1
  3. Vehicle test — connect to the BMW K-Bus via the CD changer connector (E46) or junction block, key position 1 (accessories), sniff traffic before attempting any TX

See the circuit design reference for the hardware schematic, and the bill of materials for components.