BMW_E8x_EPS/can-io/PROTOCOL.md
Luca c32c4645b5 Reverse-engineer BMW E8x EPS for standalone operation
Tooling and findings for running an E8x/E9x electric power steering unit
outside its donor car, e.g. in an EV conversion.

Headline result: the EPS needs only two CAN messages plus a 12V enable
wire, not the 69-message set the car puts on the bus:
  0x130 CAS terminal status (100ms) brings the unit up
  0x1A0 DSC road speed (20ms) sets the assist level
Total required rate is 60 frames/s. Protocol write-up, including what is
proven vs. inferred and the open questions, is in eps-comms/.

Contents:
  adapters/   CANdapter (its SLCAN dialect differs) and generic SLCAN
  gateway/    car<->EPS relay, replay, message bench, EPS controller,
              4-tab Streamlit UI
  decoder/    PT-CAN frame decoding and live/replay sources
  can-io/     XIAO ESP32-S3 firmware: CAN IO board + USB-CAN bridge with
              a CAN-independent digital IO channel
  tools/      capture, bitrate scan, startup-order and session analysis,
              checksum solver
  captures/   reference working session + the replay set eps_control reads
2026-08-29 19:34:43 +02:00

2.6 KiB
Raw Permalink Blame History

CAN Protocol — CAN IO Board

Bus: 500 kbit/s, 11-bit (standard) identifiers, classic CAN 2.0. Base ID: 0x100 (configurable in firmware/include/config.h).

ID Direction Purpose
base + 0 = 0x100 board → bus Status frame
base + 1 = 0x101 bus → board Command frame

Bit numbering in all bitmaps: bit0 = IN1/OUT1, bit1 = IN2/OUT2, … 1 = active.

Status frame — ID 0x100, DLC 8

Sent every second (heartbeat), immediately on any input/output change, in response to GET_STATUS, and once at boot.

Byte Meaning
0 Input bitmap (bit0=IN1 … bit3=IN4)
1 Output bitmap (bit0=OUT1, bit1=OUT2)
2 Reason: 0=periodic, 1=change, 2=request, 3=boot
3 Reserved (0)
4–7 Uptime in seconds, uint32 little-endian

Example: 05 02 01 00 3C 00 00 00 → IN1+IN3 active, OUT2 on, reason=change, uptime 60 s.

Command frame — ID 0x101

Byte 0 selects the command. Malformed/unknown frames are ignored.

Cmd Name DLC Layout
0x00 GET_STATUS ≥1 [00] → board replies with a status frame
0x01 SET_OUTPUT ≥3 [01, index, value] index 0-based, value 0=off / ≠0=on
0x02 SET_ALL ≥3 [02, mask, values] outputs where mask bit=1 get the corresponding values bit
0x03 TOGGLE ≥2 [03, index]

Examples (send to 0x101):

Bytes Effect
00 request status
01 00 01 OUT1 on
01 01 00 OUT2 off
02 03 03 OUT1 + OUT2 both on
02 03 00 OUT1 + OUT2 both off
03 00 toggle OUT1

Every accepted output change triggers an immediate status frame (reason = change), so the bus always confirms what actually happened — there is no separate ACK.

Local rules (run on the board itself)

Evaluated on the activation edge of an input; releasing does nothing (outputs latch):

Input Action
IN1 OUT1 → ON
IN2 OUT1 → OFF
IN3 OUT2 → ON
IN4 OUT2 → OFF

Inputs already active at power-up are reported but do not fire rules.