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
67 lines
2.6 KiB
Markdown
67 lines
2.6 KiB
Markdown
# 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.
|