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

67 lines
2.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.