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)
- Motors NEVER spin on power-on — 3 s arm hold required
- Tilt cutoff ±25° — immediate motor zero, manual re-arm
- Hardware watchdog 50 ms — firmware hang → motors cut
- RC ESTOP channel checked every loop iteration
- Orin CAN timeout 500 ms — revert to RC-only mode
- Speed hard cap in firmware — start at 10%, increase in 10% steps
- 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 <Kp> <Ki> <Kd> — set PID gains
arm — arm (if upright)
disarm — disarm immediately
estop — emergency stop
tilt_limit <deg> — set tilt cutoff (default 25)
speed_limit <pct> — set speed cap (default 10)
can_stats — CAN bus stats (tx/rx/errors)
reboot — soft reboot