6.2 KiB
Plan: SaltyRover STM32 Firmware Variant
Context
SaltyRover is the 4-wheel-drive variant of SaltyBot. It uses the same STM32F722 MCU, CRSF RC, USB CDC, safety systems, and IMU — but replaces the balance PID loop with direct 4-wheel independent motor commands. Two hoverboard ESCs are used: front axle on USART2 (same as balance bot), rear axle on UART5 (PC12/PD2).
saltyrover-dev is 16 commits behind main and lacks: mode_manager, remote e-stop (PR #69), jetson_cmd updates, and some CDC additions. We'll apply all relevant current main-equivalent features on the new branch.
Branch
- Source:
origin/saltyrover-dev - Branch name:
sl-firmware/rover-firmware - PR target:
saltyrover-dev
Files to Create
include/rover_driver.h
Public API for the 4-wheel rover motor layer:
typedef struct {
int16_t fl, fr, rl, rr; /* last wheel commands (-1000..+1000) */
bool armed;
bool estop;
uint32_t last_cmd_ms; /* HAL_GetTick() of last set_cmd() call */
} rover_driver_t;
void rover_driver_init(rover_driver_t *r);
void rover_driver_set_cmd(rover_driver_t *r, int16_t fl, int16_t fr, int16_t rl, int16_t rr, uint32_t now);
void rover_driver_update(rover_driver_t *r, uint32_t now); /* call at 50Hz */
void rover_driver_arm(rover_driver_t *r);
void rover_driver_disarm(rover_driver_t *r);
void rover_driver_estop(rover_driver_t *r);
void rover_driver_estop_clear(rover_driver_t *r);
src/rover_driver.c
Implementation — two hoverboard ESC control:
rover_driver_init(): calls existinghoverboard_init()(USART2 front), adds UART5 init for rear (HAL_UART_Init at 115200, PC12/PD2)rover_driver_update()at 50Hz:- estop or disarmed → send (0,0) to both ESCs
- armed → compute
front_spd=(fl+fr)/2,front_str=(fr-fl)/2,rear_spd=(rl+rr)/2,rear_str=(rr-rl)/2; clamp each to ±ROVER_SPEED_MAX - If
now - last_cmd_ms > ROVER_CMD_TIMEOUT_MS→ zero commands (safety fallback) - Calls
hoverboard_send(front_spd, front_str)for USART2 - Calls local
hoverboard2_send(rear_spd, rear_str)for UART5 (static function in rover_driver.c, same 8-byte protocol)
Files to Modify
include/config.h
Add rover-specific constants section:
/* --- SaltyRover 4WD --- */
#define ROVER_MAX_TILT_DEG 45.0f /* ESC estop angle (vs 25° balance cutoff) */
#define ROVER_CMD_TIMEOUT_MS 500 /* Zero wheels if no Jetson cmd for 500ms */
#define ROVER_SPEED_MAX 800 /* Max per-wheel command (ESC units) */
include/safety.h + src/safety.c
Apply PR #69 remote-estop additions (same as main): EstopSource enum, safety_remote_estop/clear/get/active(), s_estop_source static. These are already on main but not saltyrover-dev — include them here.
lib/USB_CDC/src/usbd_cdc_if.c + include/usbd_cdc_if.h
Apply PR #69 CDC additions: cdc_estop_request, cdc_estop_clear_request, cases 'E'/'F'/'Z'.
Add rover JSON command handling:
volatile uint8_t rover_json_ready = 0;
volatile char rover_json_buf[80];
// In CDC_Receive:
case '{': {
uint32_t n = *len < 79 ? *len : 79;
for (uint32_t i = 0; i < n; i++) rover_json_buf[i] = (char)buf[i];
rover_json_buf[n] = '\0';
rover_json_ready = 1;
jetson_hb_tick = HAL_GetTick(); /* JSON cmd refreshes heartbeat */
break;
}
Export in header: extern volatile uint8_t rover_json_ready; extern volatile char rover_json_buf[80];
src/main.c
Full replacement of the main loop. Keep all init (clock, USB, IMU, CRSF, I2C sensors, safety), strip balance_t, replace with rover_driver_t. Key loop logic:
Per-iteration (1ms):
safety_refresh()— IWDG feedmpu6000_read(&imu)— IMU readmode_manager_update(&mode, now)— RC liveness + CH6 mode- Remote e-stop block (from PR #69):
cdc_estop_request→rover_driver_estop() - Tilt watchdog:
if (fabsf(imu.pitch) > ROVER_MAX_TILT_DEG) rover_driver_estop() - RC CH5 arm/disarm (same hold interlock; pitch check relaxed to
< ROVER_MAX_TILT_DEG) - USB arm/disarm (A/D commands)
- Parse
rover_json_ready:sscanf(rover_json_buf, ...)fordrive4cmd →rover_driver_set_cmd() - RC direct drive fallback (MANUAL mode): CH3→speed all 4 wheels, CH4→differential steer
50Hz ESC block:
if (rover.armed && !rover.estop) {
// If Jetson active: use stored fl/fr/rl/rr
// If RC MANUAL: compute fl=fr=rl=rr=speed, add steer diff
rover_driver_update(&rover, now);
} else {
rover_driver_update(&rover, now); // sends zeros
}
50Hz telemetry:
{"p":<pitch×10>,"r":<roll×10>,"s":<armed>,"fl":<fl>,"fr":<fr>,"rl":<rl>,"rr":<rr>,"md":<mode>,"es":<es>,"txc":<N>,"rxc":<N>}
Optional fields: heading, altitude/temp/pressure (same as balance bot).
Status LEDs: Same status_update(now, imu_ok, rover.armed, |pitch|>ROVER_MAX_TILT_DEG, safety_remote_estop_active())
Arm guard: |pitch_deg| < ROVER_MAX_TILT_DEG (45°) instead of < 10.0f
Key Architectural Differences vs Balance Bot
| Balance Bot | SaltyRover | |
|---|---|---|
| PID loop | 1kHz balance PID | None |
| Motor cmd | bal.motor_cmd |
Individual fl/fr/rl/rr |
| ESCs | 1× USART2 | 2× USART2 + UART5 |
| Tilt cutoff | 25° (balance state) | 45° (simple estop only) |
| Drive cmd | C<spd>,<str>\n |
{"cmd":"drive4",...} JSON |
| Arm pitch guard | < 10° |
< 45° |
| State machine | DISARMED/ARMED/TILT_FAULT | armed bool + estop bool |
Test Plan
- Build:
pio run -e saltyrover(or equivalent) — confirms compile - Hardware: send
{"cmd":"drive4","fl":200,"fr":200,"rl":200,"rr":200}→ all 4 wheels spin forward - Estop: send
E\n→ wheels stop, LED fast-blinks 200ms;Z\n+ RC arm → resumes - Tilt: tilt rover >45° → wheels cut immediately; no cut at 30°
- RC manual: CH5 arm + CH3/CH4 → wheels respond; CH5 disarm → stop
- Telemetry: verify
"fl"/"fr"/"rl"/"rr"in JSON stream,"es"field changes on estop
Critical Files
src/main.c— rover main loop (full rewrite of loop body)include/rover_driver.h— new filesrc/rover_driver.c— new file (UART5 init + two-ESC update)include/config.h— rover constantsinclude/safety.h+src/safety.c— remote estop additionslib/USB_CDC/src/usbd_cdc_if.c— JSON '{' case + estop flagslib/USB_CDC/include/usbd_cdc_if.h— export new flags