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
2.6 KiB
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.