Reverse Engineering
Reverse Engineering

Reading BMW E87 Steering Wheel Buttons over K-CAN with Arduino

An experiment on my BMW E87 116i: listening to the K-CAN bus with an Arduino and MCP2515 and reading the steering wheel buttons (ID 0x1D6) in the Serial Monitor, with the filtering/decoding code and real output I used.

BMW E87 K-CAN Arduino kapak görseli

In this post I walk through a small experiment on my BMW E87 2011 116i: figuring out how the steering wheel multimedia buttons appear on the K-CAN bus. The goal was not a big modification to the car; just to listen to the CAN bus and read the messages that arrive when the buttons are pressed, using an Arduino and an MCP2515, and watch them in the Serial Monitor.

The question I started with was simple: when I press the steering wheel buttons, which messages do I see on K-CAN? I had the idea of later using these buttons to control an external Bluetooth audio module, but I did not get there in this stage. My first goal was just to read the buttons and print the incoming CAN messages to the Serial Monitor.

Vehicle and Test Setup

text
Vehicle: BMW 1 Series
Body: E87
Model: 2011 BMW 116i
Bus: K-CAN (100 kbit/s)
Focus: Steering wheel multimedia buttons
Goal: Read button presses and observe them in the Serial Monitor
Tools Used
  • Arduinoreads CAN messages and prints to Serial
  • MCP2515 + TJA1050 CAN modulebridge between K-CAN and Arduino
  • Jumper wirestemporary test connection
  • Arduino IDE + Serial Monitorobservation

Why I Listened to the Bus Instead of Wiring the Buttons

On the BMW E87 many modules talk to each other, and the steering wheel buttons also send their info to the relevant modules over these buses. My goal was to see how that info is represented on the CAN bus when I press a button. So instead of running physical wires to the buttons, it made more sense to listen to the in-car communication bus.

The advantage of this approach: the original button logic is preserved, there is no need to intervene inside the steering wheel, and you can analyze how each button looks through the CAN frames. The bus I focused on was K-CAN.

Temporary Connection and Listening Logic

To observe the K-CAN bus I used a temporary test connection on the AC/IHKA connector side. My aim was not to write data to the bus, only to listen to the traffic; I did no permanent mounting, soldered connection or irreversible change.

Listening flow
  1. Temporarily listen to K-CAN from the IHKA connector side.
  2. Pass the messages to the Arduino through the MCP2515 module.
  3. Read the incoming CAN frames with the Arduino.
  4. Print them to the Serial Monitor.

First All Traffic, Then a Single ID

There can be a lot of messages on the CAN bus. So while it is possible to see all the traffic, what really mattered was telling apart the messages that change when the steering buttons are pressed. The logic I followed was:

The logic I followed
  1. Listen to K-CAN with Arduino + MCP2515.
  2. Watch the incoming messages in the Serial Monitor.
  3. Press the steering wheel buttons.
  4. Mark the CAN IDs that repeat or change during a press.
  5. Filter out the unneeded traffic and focus on the relevant ID.
Problem

The ID that looked meaningful for the steering buttons was 0x1D6. Important note: I do not claim this ID is the same for every car, year and hardware; it is a result based on observing my own vehicle.

Code: Filtering 0x1D6 and Decoding the Buttons

To make the traffic readable I used the MCP2515 hardware filter to pass only the 0x1D6 ID, then decoded the button from the first byte of the incoming frame. The full sketch below contains the filtering lines exactly as seen in the video; the setup/loop and button decoding are a reference implementation that reproduces the output I observed (first byte C0 idle, E0 for UP, D0 for DOWN). You need to adapt the pins and threshold values to your own wiring and vehicle.

cpp
#include <SPI.h>
#include <mcp_can.h>          // coryjfowler/MCP_CAN_lib

// --- pinler: kendi bağlantına göre ayarla ---
const int CAN0_CS = 10;        // MCP2515 CS pini
#define MODE_PIN  3
#define PLAY_PIN  4
#define NEXT_PIN  5
#define PREV_PIN  6
#define EQ_PIN    7

MCP_CAN CAN0(CAN0_CS);

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
bool pressed = false;          // şu an bir tuş basılı mı

void setup() {
  Serial.begin(115200);

  pinMode(MODE_PIN, OUTPUT);
  pinMode(PLAY_PIN, OUTPUT);
  pinMode(NEXT_PIN, OUTPUT);
  pinMode(PREV_PIN, OUTPUT);
  pinMode(EQ_PIN,   OUTPUT);
  digitalWrite(MODE_PIN, LOW);
  digitalWrite(PLAY_PIN, LOW);
  digitalWrite(NEXT_PIN, LOW);
  digitalWrite(PREV_PIN, LOW);
  digitalWrite(EQ_PIN,   LOW);

  // K-CAN 100 kbit/s; mavi MCP2515 modüllerinde kristal genelde 8 MHz
  if (CAN0.begin(MCP_STDEXT, CAN_100KBPS, MCP_8MHZ) == CAN_OK)
    Serial.println("MCP2515 init OK");
  else
    Serial.println("MCP2515 init FAIL");

  // sadece 0x1D6 (direksiyon tuşları) ID'sini geçir
  CAN0.init_Mask(0, 0, 0x07FF0000);
  CAN0.init_Filt(0, 0, 0x01D60000);
  //CAN0.init_Filt(1, 0, 0x01D60000);
  CAN0.init_Mask(1, 0, 0x07FF0000);
  CAN0.init_Filt(2, 0, 0x01D60000);

  // yalnızca dinleme: hatta ACK/veri yazmaz
  CAN0.setMode(MCP_LISTENONLY);
}

void loop() {
  if (CAN0.checkReceive() != CAN_MSGAVAIL) return;

  CAN0.readMsgBuf(&rxId, &len, rxBuf);
  if ((rxId & 0x7FF) != 0x1D6) return;

  Serial.print("ID: 1D6 Data: ");
  for (int i = 0; i < len; i++) {
    if (rxBuf[i] < 0x10) Serial.print('0');
    Serial.print(rxBuf[i], HEX);
    Serial.print(' ');
  }

  // gözlemlenen ilk byte: boşta 0xC0, UP 0xE0, DOWN 0xD0
  byte b = rxBuf[0];
  if (b == 0xE0)      { Serial.println("- UP Button Pressed down");   pressed = true; }
  else if (b == 0xD0) { Serial.println("- DOWN Button Pressed down"); pressed = true; }
  else if (b == 0xC0 && pressed) { Serial.println("- Buttons Released"); pressed = false; }
  else Serial.println();
}

The filter logic: using 0x07FF0000 as the mask (the full 11-bit ID) and 0x01D60000 in the filter lets only frames with ID 0x1D6 through (the value is the ID shifted left by 16 bits, 0x1D6 << 16). Since K-CAN runs at 100 kbit/s I started the module at that speed and put it in listen-only mode, so it writes nothing to the bus. The digitalWrite lines above pull the output pins I will later use to drive an external audio module to LOW at startup; I did not use that side yet at this stage.

What Did I See in the Serial Monitor?

This was the most fun part: when I pressed the steering buttons, the messages streamed live in the Serial Monitor. The output I observed was:

text
ID: 1D6 Data: C0 0C
ID: 1D6 Data: C0 0C - Vol + Button Pressed
ID: 1D6 Data: E0 0C - UP Button Pressed down
- Buttons Released
ID: 1D6 Data: D0 0C - DOWN Button Pressed down
- Buttons Released

Here the first data byte changes with the button: C0 when idle, E0 for UP, D0 for DOWN. On the sketch side I interpreted these values into readable events like "UP Button Pressed down", "DOWN Button Pressed down" and "Buttons Released" on release. This is where the project really "worked" for me, because I could now see, on my own screen, a communication that is invisible inside the car.

What Did I Achieve at This Stage?

Results
  1. I could read the K-CAN bus on the BMW E87.
  2. I observed the steering button messages with Arduino + MCP2515.
  3. I focused on the 0x1D6 ID and got meaningful button events in the Serial Monitor.
  4. I verified the reading layer before moving to an action layer like Bluetooth control.

This mattered to me because, before moving to the next stage, I had answered the question "can I actually read the buttons?"

Why Is Bluetooth Control the Next Stage?

One of the early ideas in this project was to control an external Bluetooth audio module with the buttons. But I wanted to see the reading side working solidly first. For electronics and automotive projects I think the healthy order is: read the signal, then make sense of it, then filter, and only at the end control another system. In this work I focused on the first two or three stages.

What I Learned

This small project reminded me of a few things. You should not rush with car electronics; on buses like CAN, listening, understanding and taking notes first is the safest approach. Not every incoming message looks meaningful; what matters is catching the right change under the right condition. With a simple, accessible module like the MCP2515 you can learn a lot about in-car communication. And for me this was not a permanent modification but a reading and learning experiment.

Next Step

Next steps: map the byte values for each button more precisely, label volume up/down and next/previous separately, build a clean event system on the Arduino side, and run a long stability test before controlling an external module. Bluetooth control only comes after these checks, as a separate project step.

Conclusion

This project was a small but, for me, very instructive experiment in reading the K-CAN bus on a BMW E87 with an Arduino and an MCP2515. The most important result was that, when I pressed the steering buttons, the in-car communication became visible in my Serial Monitor.

The steering wheel buttons can be read over K-CAN, and these messages can be made processable on the Arduino side.

This stage became a foundational learning step for more advanced automotive electronics and hardware-side reverse engineering projects. The reason I share these experiments is that the real value is not just in the result, but in how I broke the problem down, how I tested it, and what I learned.

Reading BMW E87 Steering Buttons over K-CAN with Arduino · Yaşar Kahramaner