Archive STM32 firmware to legacy/stm32/: - src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py - test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c - USB_CDC_BUG.md Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol, stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params, stm32_cmd.launch.py → esp32_cmd.launch.py, test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node Content cleanup across all files: - Mamba F722S → ESP32-S3 BALANCE - BlackPill → ESP32-S3 IO - STM32F722/F7xx → ESP32-S3 - stm32Mode/Version/Port → esp32Mode/Version/Port - STM32 State/Mode labels → ESP32 State/Mode - Jetson Nano → Jetson Orin Nano Super - /dev/stm32 → /dev/esp32 - stm32_bridge → esp32_bridge - STM32 HAL → ESP-IDF docs/SALTYLAB.md: - Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28) - Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF No new functionality — cleanup only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
AGENTS.md — SaltyLab Agent Onboarding
You're working on SaltyLab, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything.
⚠️ ARCHITECTURE — SAUL-TEE (finalised 2026-04-04)
<<<<<<< HEAD
Full hardware spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md — read it before writing firmware.
| Board | Role |
|---|---|
| ESP32-S3 BALANCE | Waveshare Touch LCD 1.28 (CH343 USB). QMI8658 IMU, PID loop, CAN→VESC L(68)/R(56), GC9A01 LCD |
| ESP32-S3 IO | Bare devkit (JTAG USB). TBS Crossfire RC (UART0), ELRS failover (UART2), BTS7960 motors, NFC/baro/ToF, WS2812, buzzer/horn/headlight/fan |
| Jetson Orin | CANable2 USB→CAN. Cmds on 0x300–0x303, telemetry on 0x400–0x401 |
Jetson Orin ──CANable2──► CAN 500kbps ◄───────────────────────┐
│ │
ESP32-S3 BALANCE ←─UART 460800─► ESP32-S3 IO
(QMI8658, PID loop) (BTS7960, RC, sensors)
│ CAN 500kbps
┌─────────┴──────────┐
VESC Left (ID 68) VESC Right (ID 56)
=======
A hoverboard-based balancing robot with two compute layers:
1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer.
2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch) │ ▼ ESP32-S3 BALANCE (MPU6000 IMU, PID balance) │ ▼ UART2 Hoverboard ESC (FOC) → 2× 8" hub motors
291dd68(feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Frame: `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
Legacy `src/` STM32 HAL code is **archived — do not extend.**
## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT
This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants:
1. **Motors NEVER spin on power-on.** Requires deliberate arming: hold button 3s while upright.
2. **Tilt cutoff at ±25°** — motors to zero, require manual re-arm. No retry, no recovery.
3. **Hardware watchdog (50ms)** — if firmware hangs, motors cut.
4. **RC kill switch** — dedicated ELRS channel, checked every loop iteration. Always overrides.
5. **Jetson UART timeout (200ms)** — if Jetson disconnects, motors cut.
6. **Speed hard cap** — firmware limit, start at 10%. Increase only after proven stable.
7. **Never test untethered** until PID is stable for 5+ minutes on a tether.
**If you break any of these, you are removed from the project.**
## Repository Layout
<<<<<<< HEAD firmware/ # Legacy ESP32/STM32 HAL firmware (PlatformIO, archived)
firmware/ # ESP-IDF firmware (PlatformIO)
291dd68(feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only) ├── src/ │ ├── main.c # Entry point, clock config, main loop │ ├── icm42688.c # QMI8658-P SPI driver (backup IMU — currently broken) │ ├── bmp280.c # Barometer driver (disabled) │ └── status.c # LED + buzzer status patterns ├── include/ │ ├── config.h # Pin definitions, constants │ ├── icm42688.h │ ├── mpu6000.h # MPU6000 driver header (primary IMU) │ ├── hoverboard.h # Hoverboard ESC UART protocol │ ├── crsf.h # ELRS CRSF protocol │ ├── bmp280.h │ └── status.h ├── lib/USB_CDC/ # USB Serial (CH343) stack (serial over USB) │ ├── src/ # CDC implementation, USB descriptors, PCD config │ └── include/ └── platformio.ini # Build config
cad/ # OpenSCAD parametric parts (16 files) ├── dimensions.scad # ALL measurements live here — single source of truth ├── assembly.scad # Full robot assembly visualization ├── motor_mount_plate.scad ├── battery_shelf.scad ├── fc_mount.scad # Vibration-isolated FC mount ├── jetson_shelf.scad ├── esc_mount.scad ├── sensor_tower_top.scad ├── lidar_standoff.scad ├── realsense_bracket.scad ├── bumper.scad # TPU bumpers (front + rear) ├── handle.scad ├── kill_switch_mount.scad ├── tether_anchor.scad ├── led_diffuser_ring.scad └── esp32c3_mount.scad
ui/ # Web UI (Three.js + WebSerial) └── index.html # 3D board visualization, real-time IMU data
SALTYLAB.md # Master design doc — architecture, wiring, build phases SALTYLAB-DETAILED.md # Power budget, weight budget, detailed schematics PLATFORM.md # Hardware platform reference
## Hardware Quick Reference
<<<<<<< HEAD
### ESP32 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
=======
### ESP32-S3 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Primary IMU | MPU6000 (WHO_AM_I = 0x68) |
| IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 |
| IMU EXTI | PC4 (data ready interrupt) |
| IMU Orientation | CW270 (Betaflight convention) |
| Secondary IMU | QMI8658-P (on same SPI1, CS unknown — currently non-functional) |
| Betaflight Target | DIAT-MAMBAF722_2022B |
| USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 |
| VID/PID | 0x0483/0x5740 |
| LEDs | PC15 (LED1), PC14 (LED2), active low |
| Buzzer | PB2 (inverted push-pull) |
| Battery ADC | PC1=VBAT, PC3=CURR (ADC3) |
| DFU | Hold yellow BOOT button + plug USB (or send 'R' over CDC) |
### UART Assignments
| UART | Pins | Connected To | Baud |
|------|------|-------------|------|
| USART1 | PA9/PA10 | Jetson Orin Nano Super | 115200 |
| USART2 | PA2/PA3 | Hoverboard ESC | 115200 |
| USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) |
| UART4 | — | Spare | — |
| UART5 | — | Spare | — |
### Motor/ESC
- 2× 8" pneumatic hub motors (36V, hoverboard type)
- Hoverboard ESC with FOC firmware
- UART protocol: `{0xABCD, int16 speed, int16 steer, uint16 checksum}` at 115200
- Speed range: -1000 to +1000
### Physical Dimensions (from `cad/dimensions.scad`)
| Part | Key Measurement |
|------|----------------|
| FC mounting holes | 25.5mm spacing (NOT standard 30.5mm!) |
| FC board size | ~36mm square |
| Hub motor body | Ø200mm (~8") |
| Motor axle | Ø12mm, 45mm long |
| Jetson Orin Nano Super | 100×80×29mm, M2.5 holes at 86×58mm |
| RealSense D435i | 90×25×25mm, 1/4-20 tripod mount |
| RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle |
| Kill switch hole | Ø22mm panel mount |
| Battery pack | ~180×80×40mm |
| Hoverboard ESC | ~80×50×15mm |
| 2020 extrusion | 20mm square, M5 center bore |
| Frame width | ~350mm (axle to axle) |
| Frame height | ~500-550mm total |
| Target weight | <8kg (current estimate: 7.4kg) |
### 3D Printed Parts (16 files in `cad/`)
| Part | Material | Infill |
|------|----------|--------|
| motor_mount_plate (350×150×6mm) | PETG | 80% |
| battery_shelf | PETG | 60% |
| esc_mount | PETG | 40% |
| jetson_shelf | PETG | 40% |
| sensor_tower_top | ASA | 80% |
| lidar_standoff (Ø80×80mm) | ASA | 40% |
| realsense_bracket | PETG | 60% |
| fc_mount (vibration isolated) | TPU+PETG | — |
| bumper front + rear (350×50×30mm) | TPU | 30% |
| handle | PETG | 80% |
| kill_switch_mount | PETG | 80% |
| tether_anchor | PETG | 100% |
| led_diffuser_ring (Ø120×15mm) | Clear PETG | 30% |
| esp32c3_mount | PETG | 40% |
## Firmware Architecture
### Critical Lessons Learned (DON'T REPEAT THESE)
1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times.
<<<<<<< HEAD
2. **DCache breaks SPI on ESP32** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
=======
2. **DCache breaks SPI on ESP32-S3** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes.
4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first.
5. **USB Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception.
### DFU Reboot (Betaflight Method)
The firmware supports reboot-to-DFU via USB command:
1. Send `R` byte over USB Serial (CH343)
2. Firmware writes `0xDEADBEEF` to RTC backup register 0
3. `NVIC_SystemReset()` — clean hardware reset
4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic
<<<<<<< HEAD
5. If magic found: clears it, remaps system memory, jumps to ESP32 BALANCE bootloader at `0x1FF00000`
=======
5. If magic found: clears it, remaps system memory, jumps to ESP32-S3 bootloader at `0x1FF00000`
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
6. Board appears as DFU device, ready for `dfu-util` flash
### Build & Flash
```bash
cd firmware/
python3 -m platformio run # Build
dfu-util -a 0 -s 0x08000000:leave -D .pio/build/f722/firmware.bin # Flash
Dev machine: mbpm4 (seb@192.168.87.40), PlatformIO project at ~/Projects/saltylab-firmware/
Clock Configuration
HSE 8MHz → PLL (M=8, N=432, P=2, Q=9) → SYSCLK 216MHz
PLLSAI (N=384, P=8) → CLK48 48MHz (USB)
APB1 = HCLK/4 = 54MHz
APB2 = HCLK/2 = 108MHz
Fallback: HSI 16MHz if HSE fails (PLL M=16)
Current Status & Known Issues
Working
- USB Serial (CH343) serial streaming (50Hz JSON:
{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}) - Clock config with HSE + HSI fallback
- Reboot-to-DFU via USB 'R' command
- LED status patterns (status.c)
- Web UI with WebSerial + Three.js 3D visualization
Broken / In Progress
- QMI8658-P SPI reads return all zeros — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary.
- MPU6000 driver — header exists but implementation needs completion
- PID balance loop — not yet implemented
- Hoverboard ESC UART — protocol defined, driver not written
- ELRS CRSF receiver — protocol defined, driver not written
- Barometer (BMP280) — I2C init hangs, disabled
TODO (Priority Order)
- Get MPU6000 streaming accel+gyro data
- Implement complementary filter (pitch angle)
- Write hoverboard ESC UART driver
- Write PID balance loop with safety checks
- Wire ELRS receiver, implement CRSF parser
- Bench test (ESC disconnected, verify PID output)
- First tethered balance test at 10% speed
- Jetson UART integration
- LED subsystem (ESP32-C3)
Communication Protocols
Jetson → FC (UART1, 50Hz)
struct { uint8_t header=0xAA; int16_t speed; int16_t steer; uint8_t mode; uint8_t checksum; };
// mode: 0=idle, 1=balance, 2=follow, 3=RC
FC → Hoverboard ESC (UART2, loop rate)
struct { uint16_t start=0xABCD; int16_t speed; int16_t steer; uint16_t checksum; };
// speed/steer: -1000 to +1000
FC → Jetson Telemetry (UART1 TX, 50Hz)
T:12.3,P:45,L:100,R:-80,S:3\n
// T=tilt°, P=PID output, L/R=motor commands, S=state (0-3)
FC → USB Serial (CH343) (50Hz JSON)
{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0}
// Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp
LED Subsystem (ESP32-C3)
ESP32-C3 eavesdrops on FC→Jetson telemetry (listen-only tap on UART1 TX). No extra FC UART needed.
| State | Pattern | Color |
|---|---|---|
| Disarmed | Slow breathe | White |
| Arming | Fast blink | Yellow |
| Armed idle | Solid | Green |
| Turning | Sweep direction | Orange |
| Braking | Flash rear | Red |
| Fault | Triple flash | Red |
| RC lost | Alternating flash | Red/Blue |
Printing (Bambu Lab)
- X1C (192.168.87.190) — for structural PETG/ASA parts
- A1 (192.168.86.161) — for TPU bumpers and prototypes
- LAN access codes and MQTT details in main workspace MEMORY.md
- STL export from OpenSCAD, slice in Bambu Studio
Rules for Agents
- Read SALTYLAB.md fully before making any design decisions
- Never remove safety checks from firmware — add more if needed
- All measurements go in
cad/dimensions.scad— single source of truth - Test firmware on bench before any motor test — ESC disconnected, verify outputs on serial
- One variable at a time — don't change PID and speed limit in the same test
- Document what you change — update this file if you add pins, change protocols, or discover hardware quirks
- Ask before wiring changes — wrong connections can fry the FC ($50+ board)