# SAUL-TEE System Reference — SaltyLab ESP32 Architecture *Source of truth for all hardware, pin, protocol, and CAN assignments.* *Generated 2026-04-04 from hal@Orin spec.* --- ## Overview | Board | Role | MCU | USB | |-------|------|-----|-----| | **ESP32-S3 BALANCE** | PID balance loop, CAN to VESCs, LCD UI | ESP32-S3 | CH343 USB-serial | | **ESP32-S3 IO** | RC input, motor drivers, sensors, LEDs, peripherals | ESP32-S3 | JTAG USB | **Robot form factor:** 4-wheel wagon — 870 × 510 × 550 mm, ~23 kg **Power:** 36 V battery, DC-DC to 5 V / 12 V **Orin connection:** CANable2 USB → CAN bus (same bus as VESCs) --- ## ESP32-S3 BALANCE ### Board Waveshare ESP32-S3 Touch LCD 1.28 (GC9A01 round 240×240 LCD, CST816 touch, QMI8658 IMU) ### Pin Assignments | Function | GPIO | Notes | |----------|------|-------| | **QMI8658 IMU** | | | | SPI SCK | IO39 | | | SPI MOSI | IO38 | | | SPI MISO | IO40 | | | IMU CS | IO41 | | | IMU INT1 | IO42 | data-ready interrupt | | **GC9A01 LCD** | | | | SPI (shared) | IO39/38 | same SPI bus | | LCD CS | IO12 | | | LCD DC | IO11 | | | LCD RST | IO10 | | | LCD BL | IO9 | PWM backlight | | **CST816 Touch** | | | | I2C SDA | IO4 | | | I2C SCL | IO5 | | | Touch INT | IO6 | | | Touch RST | IO7 | | | **CAN (SN65HVD230)** | | 500 kbps | | CAN TX | IO43 | to SN65HVD230 TXD | | CAN RX | IO44 | from SN65HVD230 RXD | | **Inter-board UART** | | 460800 baud | | UART TX → IO | IO17 | | | UART RX ← IO | IO18 | | ### Software Responsibilities - Read QMI8658 @ 1 kHz (SPI) - Run PID balance loop (configurable Kp/Ki/Kd, target pitch) - Send VESC speed commands via CAN (IDs 68 left, 56 right) - Receive Orin velocity commands via CAN (0x300–0x303) - Receive IO board status (arming, RC, faults) via UART - Drive LCD status display (pitch, speed, battery, state) - Enforce tilt cutoff at ±25°, hardware watchdog 50 ms - Send telemetry CAN frames 0x400–0x401 at 10 Hz --- ## ESP32-S3 IO ### Board Bare ESP32-S3 devkit (JTAG USB) ### Pin Assignments | Function | GPIO | Notes | |----------|------|-------| | **TBS Crossfire RC** | | UART0, primary RC | | CRSF RX | IO44 | UART0 RX | | CRSF TX | IO43 | UART0 TX | | **ELRS RC failover** | | UART2 | | ELRS RX | IO16 | UART2 RX | | ELRS TX | IO17 | UART2 TX | | **BTS7960 Motor Drivers** | | 2× drivers (left/right) | | Left RPWM | IO1 | forward PWM | | Left LPWM | IO2 | reverse PWM | | Left R_EN | IO3 | enable | | Left L_EN | IO4 | enable | | Right RPWM | IO5 | forward PWM | | Right LPWM | IO6 | reverse PWM | | Right R_EN | IO7 | enable | | Right L_EN | IO8 | enable | | **I2C bus** | | sensors | | SDA | IO11 | | | SCL | IO12 | | | NFC reader | I2C | PN532 or similar | | Barometer | I2C | BMP280 / BMP388 | | ToF sensor | I2C | VL53L0X / VL53L1X | | **WS2812B LEDs** | | | | LED data | IO13 | | | **Outputs** | | | | Horn / buzzer | IO14 | PWM | | Headlight | IO15 | PWM or digital | | Fan | IO16 | (shared if ELRS not fitted) | | Arming button | IO9 | active-low, hold 3 s | | Kill switch in | IO10 | hardware estop sense | | **Inter-board UART** | | 460800 baud | | UART TX → BAL | IO18 | | | UART RX ← BAL | IO21 | | ### Software Responsibilities - Parse CRSF frames (TBS Crossfire primary) - Parse ELRS frames (failover if CRSF absent) - Drive BTS7960 motor drivers (PWM + enable) - Read NFC, barometer, ToF via I2C - Drive WS2812B LEDs (status patterns) - Control horn, headlight, fan, buzzer - Manage arming sequence (hold button 3 s while upright) - Monitor hardware kill switch - Forward RC + sensor data to BALANCE board via UART - Report faults, RC loss, sensor health upstream --- ## Inter-Board Protocol (UART 460800 baud) ``` Frame: [0xAA][LEN][TYPE][PAYLOAD...][CRC8] 0xAA — start byte LEN — payload length (1 byte) TYPE — message type (1 byte) PAYLOAD — LEN bytes CRC8 — CRC-8/MAXIM over TYPE+PAYLOAD ``` ### Message Types (IO → BALANCE) | TYPE | Name | Payload | |------|------|---------| | 0x01 | RC_CMD | int16 throttle, int16 steer, uint8 flags (armed, kill) | | 0x02 | SENSOR | uint16 tof_mm, int16 baro_pa_delta, uint8 nfc_present | | 0x03 | FAULT | uint8 fault_flags (rc_loss, motor_fault, estop) | ### Message Types (BALANCE → IO) | TYPE | Name | Payload | |------|------|---------| | 0x10 | STATE | int16 pitch_x100, int16 pid_out, uint8 error_state | | 0x11 | LED_CMD | uint8 pattern, uint8 r, uint8 g, uint8 b | | 0x12 | BUZZER | uint8 tone_hz, uint16 duration_ms | --- ## CAN Bus — 500 kbps ### Node IDs | Node | CAN ID | Description | |------|--------|-------------| | VESC Left motor | 68 | Speed/duty commands via VESC protocol | | VESC Right motor | 56 | Speed/duty commands via VESC protocol | | ESP32 BALANCE | — | Initiates VESC commands; sends telemetry | | Jetson Orin | — | Via CANable2 USB adapter | ### CAN Frame Assignments | ID | Direction | Description | Rate | |----|-----------|-------------|------| | 0x300 | Orin → BALANCE | Velocity cmd: int16 speed_mmps, int16 steer_mrad | 20 Hz | | 0x301 | Orin → BALANCE | PID override: float Kp, Ki, Kd (IEEE 754, 4B each) | on demand | | 0x302 | Orin → BALANCE | Mode: uint8 (0=off,1=balance,2=manual,3=estop) | on demand | | 0x303 | Orin → BALANCE | Config: uint16 tilt_limit_x100, uint16 max_speed | on demand | | 0x400 | BALANCE → Orin | Telemetry A: int16 pitch_x100, int16 pid_out, int16 speed_mmps, uint8 state | 10 Hz | | 0x401 | BALANCE → Orin | Telemetry B: int16 vesc_l_rpm, int16 vesc_r_rpm, uint16 battery_mv, uint8 faults | 10 Hz | --- ## RC Channel Mapping (CRSF / ELRS) | Channel | Function | Range | Notes | |---------|----------|-------|-------| | CH1 | Roll (steer) | 988–2012 µs | ±100% = ±max steer | | CH2 | Pitch (throttle) | 988–2012 µs | forward/back speed | | CH3 | Throttle | 988–2012 µs | unused / spare | | CH4 | Yaw | 988–2012 µs | unused | | CH5 | ARM switch | <1500=disarm, >1500=arm | SB on TX | | CH6 | ESTOP | <1500=normal, >1500=kill | SC on TX, always checked | | CH7 | Speed limit | 988–2012 µs | 10–100% speed cap | | CH8 | Spare | | | RC loss (CRSF failsafe or no frame >100 ms): immediate motor cutoff, fault 0x03. --- ## Safety Invariants (unchanged from STM32 era) 1. Motors NEVER spin on power-on — 3 s arm hold required 2. Tilt cutoff ±25° — immediate motor zero, manual re-arm 3. Hardware watchdog 50 ms — firmware hang → motors cut 4. RC ESTOP channel checked every loop iteration 5. Orin CAN timeout 500 ms — revert to RC-only mode 6. Speed hard cap in firmware — start at 10%, increase in 10% steps 7. Never test untethered until stable for 5+ min tethered --- ## Serial Commands (USB debug, both boards) ``` help — list commands status — print state, pitch, PID, CAN stats pid — set PID gains arm — arm (if upright) disarm — disarm immediately estop — emergency stop tilt_limit — set tilt cutoff (default 25) speed_limit — set speed cap (default 10) can_stats — CAN bus stats (tx/rx/errors) reboot — soft reboot ```