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
165 lines
7.1 KiB
C
165 lines
7.1 KiB
C
/**
|
|
* @file config.h
|
|
* @brief Central configuration for the CAN IO board.
|
|
*
|
|
* Everything tweakable lives here: pin mapping, signal polarities, debounce
|
|
* timing, CAN identifiers/bitrate and the local input->output rules.
|
|
* The rest of the code should never contain magic numbers.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <stdint.h>
|
|
#include "driver/twai.h"
|
|
|
|
/* ------------------------------------------------------------------ pins --
|
|
* XIAO ESP32-S3 pin map. Numbers are raw ESP32-S3 GPIO numbers, NOT the
|
|
* "D" numbers on the silkscreen.
|
|
*
|
|
* GPIO 1 (D0) CAN TX -> SN65HVD230 D (driver input)
|
|
* GPIO 2 (D1) CAN RX <- SN65HVD230 R (receiver output)
|
|
* GPIO 3 (D2) IN1 <- optocoupler
|
|
* GPIO 4 (D3) IN2 <- optocoupler
|
|
* GPIO 5 (D4) IN3 <- optocoupler
|
|
* GPIO 6 (D5) IN4 <- optocoupler
|
|
* GPIO 43 (D6) OUT1 -> darlington -> relay 1
|
|
* GPIO 44 (D7) OUT2 -> darlington -> relay 2
|
|
*
|
|
* NOTE: GPIO43/44 double as UART0 TX/RX. The ROM bootloader chirps on
|
|
* GPIO43 for a moment at reset — see README "Boot behaviour" for details.
|
|
*/
|
|
#define PIN_CAN_TX GPIO_NUM_1
|
|
#define PIN_CAN_RX GPIO_NUM_2
|
|
|
|
#define NUM_INPUTS 4
|
|
#define NUM_OUTPUTS 2
|
|
|
|
static const uint8_t PIN_INPUTS[NUM_INPUTS] = { 3, 4, 5, 6 };
|
|
static const uint8_t PIN_OUTPUTS[NUM_OUTPUTS] = { 43, 44 };
|
|
|
|
#define PIN_STATUS_LED 21 /* XIAO on-board user LED, active LOW */
|
|
|
|
/* ------------------------------------------------------------ polarities --
|
|
* Inputs: the optocoupler transistor pulls the GPIO to GND when the external
|
|
* contact/button is closed. Pins use internal pull-ups, so LOW = active.
|
|
* Set to 0 if your optos drive the pin high instead.
|
|
*/
|
|
#define INPUTS_ACTIVE_LOW 1
|
|
|
|
/* Outputs: darlington driver — GPIO HIGH energises the relay.
|
|
* Set to 0 for an active-low driver stage.
|
|
*/
|
|
#define OUTPUTS_ACTIVE_HIGH 1
|
|
|
|
/* -------------------------------------------------------------- debounce --
|
|
* Inputs are sampled every INPUT_SCAN_PERIOD_MS. A new level is accepted
|
|
* only after INPUT_DEBOUNCE_SAMPLES consecutive identical samples:
|
|
* 8 x 5 ms = 40 ms — plenty for both relay contacts and push buttons.
|
|
*/
|
|
#define INPUT_SCAN_PERIOD_MS 5
|
|
#define INPUT_DEBOUNCE_SAMPLES 8
|
|
|
|
/* ------------------------------------------------------------------- CAN --
|
|
* 500 kbit/s, 11-bit (standard) identifiers.
|
|
* The full frame formats are documented in PROTOCOL.md.
|
|
*/
|
|
#define CAN_TIMING_CONFIG TWAI_TIMING_CONFIG_500KBITS()
|
|
|
|
#define CAN_BASE_ID 0x100
|
|
#define CAN_ID_STATUS (CAN_BASE_ID + 0) /* board -> bus (TX) */
|
|
#define CAN_ID_COMMAND (CAN_BASE_ID + 1) /* bus -> board (RX) */
|
|
|
|
/* Period of the unsolicited status ("heartbeat") frame. Changes are
|
|
* additionally reported immediately. */
|
|
#define STATUS_HEARTBEAT_MS 1000
|
|
|
|
/** Command codes: first data byte of a CAN_ID_COMMAND frame. */
|
|
enum can_command : uint8_t {
|
|
CMD_GET_STATUS = 0x00, /* reply with a status frame */
|
|
CMD_SET_OUTPUT = 0x01, /* data[1] = output index, data[2] = 0/1 */
|
|
CMD_SET_ALL = 0x02, /* data[1] = bit mask, data[2] = bit values */
|
|
CMD_TOGGLE = 0x03, /* data[1] = output index */
|
|
};
|
|
|
|
/** Reason codes: data[2] of a CAN_ID_STATUS frame — why it was sent. */
|
|
enum status_reason : uint8_t {
|
|
REASON_PERIODIC = 0x00, /* heartbeat */
|
|
REASON_CHANGE = 0x01, /* an input or output changed */
|
|
REASON_REQUEST = 0x02, /* answer to CMD_GET_STATUS */
|
|
REASON_BOOT = 0x03, /* first frame after power-up/reset */
|
|
};
|
|
|
|
/* ---------------------------------------------------- input -> output map --
|
|
* Local rules, evaluated on the ACTIVATION EDGE of an input (i.e. the moment
|
|
* a button is pressed / a contact closes). Releasing does nothing, so the
|
|
* outputs latch — classic set/reset stations:
|
|
*
|
|
* IN1 -> OUT1 ON IN2 -> OUT1 OFF
|
|
* IN3 -> OUT2 ON IN4 -> OUT2 OFF
|
|
*
|
|
* Inputs that are already active at boot do NOT fire rules (safe start);
|
|
* see io_input.cpp if you want different behaviour.
|
|
*/
|
|
typedef struct {
|
|
uint8_t input; /* 0-based input index (0 = IN1) */
|
|
uint8_t output; /* 0-based output index (0 = OUT1) */
|
|
bool turn_on; /* true: switch output ON, false: OFF */
|
|
} input_rule_t;
|
|
|
|
/* Empty: output control is fully handed to the PC app (gateway/gateway_app.py's
|
|
* "software passthrough" toggle + manual On/Off/Toggle), which reads IN1-4
|
|
* back from the board's status frames and drives OUT1/OUT2 via CAN commands.
|
|
* A firmware-side rule here would keep re-asserting on every debounced edge
|
|
* (including input noise) and fight manual/software control within ~40ms. */
|
|
static const input_rule_t INPUT_RULES[] = {};
|
|
#define NUM_INPUT_RULES (sizeof(INPUT_RULES) / sizeof(INPUT_RULES[0]))
|
|
|
|
/* --------------------------------------------------- direct passthrough --
|
|
* Unlike INPUT_RULES (edge-triggered latch), entries here make an output
|
|
* continuously follow an input's live level - e.g. mirroring the car's 12V
|
|
* wake/terminal signal straight through to the EPS unit. Evaluated on every
|
|
* debounced level change, in EITHER direction (not just the activation edge).
|
|
*
|
|
* Empty by default - add a mapping once you know which IN/OUT pair carries
|
|
* the 12V passthrough signal, e.g.:
|
|
* static const passthrough_map_t PASSTHROUGH_MAP[] = { { 0, 0 } }; // IN1 -> OUT1
|
|
*/
|
|
typedef struct {
|
|
uint8_t input; /* 0-based input index (0 = IN1) */
|
|
uint8_t output; /* 0-based output index (0 = OUT1) */
|
|
} passthrough_map_t;
|
|
|
|
/* Empty for the same reason as INPUT_RULES above - see gateway_app.py's
|
|
* software passthrough instead. */
|
|
static const passthrough_map_t PASSTHROUGH_MAP[] = {};
|
|
#define NUM_PASSTHROUGH (sizeof(PASSTHROUGH_MAP) / sizeof(PASSTHROUGH_MAP[0]))
|
|
|
|
/* ------------------------------------------------------------- USB bridge --
|
|
* When enabled, the board mirrors every CAN frame it receives out over USB
|
|
* serial as SLCAN ASCII ('t'/'T' + id + dlc + data), and accepts the same
|
|
* format back to inject frames onto the bus - turning the board into a
|
|
* transparent USB-CAN adapter for its bus, on top of its normal IO duties.
|
|
* See firmware/src/usb_bridge.cpp and adapters/slcan_adapter.py (PC side).
|
|
*
|
|
* IMPORTANT: while enabled, Serial is reserved for the SLCAN stream - all
|
|
* human-readable debug logging is compiled out via DEBUG_LOG() below, so it
|
|
* never corrupts the frame stream. Set to 0 and reflash to get plain-text
|
|
* logs back for local debugging (e.g. `pio device monitor`).
|
|
*/
|
|
#define USB_BRIDGE_ENABLE 1
|
|
|
|
#if USB_BRIDGE_ENABLE
|
|
#define DEBUG_LOG(...) do {} while (0)
|
|
#else
|
|
#define DEBUG_LOG(...) Serial.printf(__VA_ARGS__)
|
|
#endif
|
|
|
|
/* ------------------------------------------------------- FreeRTOS tuning -- */
|
|
#define TASK_STACK_SIZE 4096 /* bytes, generous for all tasks */
|
|
|
|
#define PRIO_CAN_RX 10 /* react to bus traffic first */
|
|
#define PRIO_CAN_TX 9
|
|
#define PRIO_CAN_ALERT 9
|
|
#define PRIO_USB_BRIDGE 9
|
|
#define PRIO_INPUT 8 /* keeps the 5 ms scan on time */
|
|
#define PRIO_OUTPUT 8
|
|
#define PRIO_STATUS 5
|