/** * @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 #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