sl-firmware 4a46fad002 feat(rc): CRSF/ELRS RC integration — telemetry uplink + channel fix (Issue #103)
## Summary
- config.h: CH1[0]=steer, CH2[1]=throttle (was CH4/CH3); CRSF_FAILSAFE_MS→500ms
- include/battery.h + src/battery.c: ADC3 Vbat reading on PC1 (11:1 divider)
  battery_read_mv(), battery_estimate_pct() for 3S/4S auto-detection
- include/crsf.h + src/crsf.c: CRSF telemetry TX uplink
  crsf_send_battery() — type 0x08, voltage/current/SoC to ELRS TX module
  crsf_send_flight_mode() — type 0x21, "ARMED\0"/"DISARM\0" for handset OSD
- src/main.c: battery_init() after crsf_init(); 1Hz telemetry tick calls
  crsf_send_battery(vbat_mv, 0, soc_pct) + crsf_send_flight_mode(armed)
- test/test_crsf_frames.py: 28 pytest tests — CRC8-DVB-S2, battery frame
  layout/encoding, flight-mode frame, battery_estimate_pct SoC math

Existing (already complete from crsf-elrs branch):
  CRSF frame decoder UART4 420000 baud DMA circular + IDLE interrupt
  Mode manager: RC↔autonomous blend, CH6 3-pos switch, 500ms smooth transition
  Failsafe in main.c: disarm if crsf_state.last_rx_ms stale > CRSF_FAILSAFE_MS
  CH5 arm switch with ARMING_HOLD_MS interlock + edge detection
  RC override: mode_manager blends steer/speed per mode (CH6)

Closes #103

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:35:48 -05:00

413 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "stm32f7xx_hal.h"
#include "usbd_core.h"
#include "usbd_cdc.h"
#include "usbd_cdc_if.h"
#include "usbd_desc.h"
#include "mpu6000.h"
#include "balance.h"
#include "hoverboard.h"
#include "motor_driver.h"
#include "mode_manager.h"
#include "config.h"
#include "status.h"
#include "safety.h"
#include "crsf.h"
#include "i2c1.h"
#include "bmp280.h"
#include "mag.h"
#include "jetson_cmd.h"
#include "battery.h"
#include <math.h>
#include <string.h>
#include <stdio.h>
extern volatile uint8_t cdc_streaming; /* set by S command in CDC RX */
extern volatile uint8_t cdc_arm_request; /* set by A command */
extern volatile uint8_t cdc_disarm_request; /* set by D command */
extern volatile uint8_t cdc_recal_request; /* set by G command */
extern volatile uint32_t cdc_rx_count; /* total CDC packets received */
extern volatile uint8_t cdc_estop_request;
extern volatile uint8_t cdc_estop_clear_request;
/*
* Apply a PID tuning command string from the USB terminal.
* Format: P<kp> I<ki> D<kd> T<setpoint_deg> M<max_speed> ?
* Returns a short ack string written into `reply` (max reply_sz bytes).
*/
static void apply_pid_cmd(balance_t *b, const char *cmd, char *reply, int reply_sz) {
float val = 0.0f;
/* Simple ASCII float parse — avoids sscanf in hot path */
if (cmd[0] == '?') {
snprintf(reply, reply_sz,
"{\"kp\":%d,\"ki\":%d,\"kd\":%d,\"sp\":%d,\"ms\":%d}\n",
(int)(b->kp * 1000), (int)(b->ki * 1000), (int)(b->kd * 1000),
(int)(b->setpoint * 10), (int)b->max_speed);
return;
}
/* Parse float after the command letter */
if (sscanf(cmd + 1, "%f", &val) != 1) {
snprintf(reply, reply_sz, "{\"err\":\"bad_fmt\"}\n");
return;
}
switch (cmd[0]) {
case 'P': if (val >= 0.0f && val <= 500.0f) b->kp = val; break;
case 'I': if (val >= 0.0f && val <= 50.0f) b->ki = val; break;
case 'K': /* alias for D */ /* fall through */
case 'D': if (val >= 0.0f && val <= 50.0f) b->kd = val; break;
case 'T': if (val >= -20.0f && val <= 20.0f) b->setpoint = val; break;
case 'M': if (val >= 0.0f && val <= 1000.0f) b->max_speed = (int16_t)val; break;
}
snprintf(reply, reply_sz,
"{\"kp\":%d,\"ki\":%d,\"kd\":%d,\"sp\":%d,\"ms\":%d}\n",
(int)(b->kp * 1000), (int)(b->ki * 1000), (int)(b->kd * 1000),
(int)(b->setpoint * 10), (int)b->max_speed);
}
USBD_HandleTypeDef hUsbDevice;
static void SystemClock_Config(void) {
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};
RCC_PeriphCLKInitTypeDef pclk = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLM = 8; osc.PLL.PLLN = 432; osc.PLL.PLLP = 2; osc.PLL.PLLQ = 9;
if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
osc.OscillatorType = RCC_OSCILLATORTYPE_HSI;
osc.HSIState = RCC_HSI_ON; osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
osc.HSEState = RCC_HSE_OFF; osc.PLL.PLLSource = RCC_PLLSOURCE_HSI; osc.PLL.PLLM = 16;
if (HAL_RCC_OscConfig(&osc) != HAL_OK) while (1);
}
HAL_PWREx_EnableOverDrive();
pclk.PeriphClockSelection = RCC_PERIPHCLK_CLK48 | RCC_PERIPHCLK_I2C1;
pclk.Clk48ClockSelection = RCC_CLK48SOURCE_PLLSAIP;
pclk.PLLSAI.PLLSAIN = 384; pclk.PLLSAI.PLLSAIQ = 7; pclk.PLLSAI.PLLSAIP = RCC_PLLSAIP_DIV8;
pclk.I2c1ClockSelection = RCC_I2C1CLKSOURCE_PCLK1;
if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) while (1);
clk.ClockType = RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV4; clk.APB2CLKDivider = RCC_HCLK_DIV2;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_7);
}
extern PCD_HandleTypeDef hpcd;
void OTG_FS_IRQHandler(void) { HAL_PCD_IRQHandler(&hpcd); }
void SysTick_Handler(void) { HAL_IncTick(); }
int main(void) {
SCB_EnableICache();
/* DCache stays ON — MPU Region 0 in usbd_conf.c marks USB buffers non-cacheable. */
checkForBootloader(); /* Check RTC magic BEFORE HAL_Init — must be first thing */
HAL_Init();
SystemClock_Config();
/* USB CDC */
USBD_Init(&hUsbDevice, &SaltyLab_Desc, 0);
USBD_RegisterClass(&hUsbDevice, &USBD_CDC);
USBD_CDC_RegisterInterface(&hUsbDevice, &USBD_CDC_fops);
USBD_Start(&hUsbDevice);
status_init();
HAL_Delay(3000); /* Wait for USB host to enumerate */
/* Init IMU (MPU6000 via SPI1 — mpu6000.c wraps icm42688 + complementary filter) */
int imu_ret = mpu6000_init() ? 0 : -1;
/*
* Gyro bias calibration — hold board still for ~1s.
* Must run before safety_init() (IWDG not yet started).
* Blocks arming and streaming until complete.
*/
if (imu_ret == 0) mpu6000_calibrate();
/* Init hoverboard ESC UART */
hoverboard_init();
/* Init balance controller */
balance_t bal;
balance_init(&bal);
/* Init motor driver */
motor_driver_t motors;
motor_driver_init(&motors);
/* Init CRSF/ELRS receiver on UART4 (PA0/PA1) with DMA */
crsf_init();
/* Init mode manager (RC/autonomous blend; CH6 mode switch) */
mode_manager_t mode;
mode_manager_init(&mode);
/* Init battery ADC (PC1/ADC3 — Vbat divider 11:1) for CRSF telemetry */
battery_init();
/* Probe I2C1 for optional sensors — skip gracefully if not found */
int baro_chip = 0; /* chip_id: 0x58=BMP280, 0x60=BME280, 0=absent */
mag_type_t mag_type = MAG_NONE;
if (i2c1_init() == 0) {
int chip = bmp280_init();
baro_chip = (chip > 0) ? chip : 0;
mag_type = mag_init();
}
/*
* IWDG starts AFTER all peripheral inits — avoids reset during mpu6000_init()
* which takes ~510ms (well above the 50ms WATCHDOG_TIMEOUT_MS).
* Once started, safety_refresh() MUST be called every WATCHDOG_TIMEOUT_MS
* or MCU resets. IWDG cannot be stopped once started (hardware enforced).
*/
safety_init();
char buf[320];
int len;
uint32_t tx_count = 0; /* telemetry frames sent to host */
IMUData imu;
uint32_t send_tick = 0;
uint32_t balance_tick = 0;
uint32_t esc_tick = 0;
uint32_t crsf_telem_tick = 0; /* CRSF uplink telemetry TX timer */
const float dt = 1.0f / PID_LOOP_HZ; /* 1ms at 1kHz */
while (1) {
if (imu_ret == 0) mpu6000_read(&imu);
uint32_t now = HAL_GetTick();
/* Feed hardware watchdog — must happen every WATCHDOG_TIMEOUT_MS */
safety_refresh();
/* Mode manager: update RC liveness, CH6 mode selection, blend ramp */
mode_manager_update(&mode, now);
if (cdc_estop_request) {
EstopSource src = (cdc_estop_request == 1) ? ESTOP_REMOTE : ESTOP_CELLULAR_TIMEOUT;
cdc_estop_request = 0;
safety_remote_estop(src);
safety_arm_cancel();
balance_disarm(&bal);
motor_driver_estop(&motors);
}
if (cdc_estop_clear_request) {
cdc_estop_clear_request = 0;
if (safety_remote_estop_active() && bal.state == BALANCE_DISARMED)
safety_remote_estop_clear();
}
/* RC CH5 kill switch: disarm immediately if RC is alive and CH5 off.
* Applies regardless of active mode (CH5 always has kill authority). */
if (mode.rc_alive && !crsf_state.armed && bal.state == BALANCE_ARMED) {
safety_arm_cancel();
balance_disarm(&bal);
}
/* RC failsafe: disarm if signal lost AFTER RC was previously alive.
* Prevents auto-disarm in USB-only mode (crsf_state.last_rx_ms == 0). */
if (bal.state == BALANCE_ARMED &&
crsf_state.last_rx_ms != 0 &&
(now - crsf_state.last_rx_ms) > CRSF_FAILSAFE_MS) {
safety_arm_cancel();
balance_disarm(&bal);
}
/* Tilt fault buzzer alert (one-shot on fault edge) */
safety_alert_tilt_fault(bal.state == BALANCE_TILT_FAULT);
/* RC arm/disarm via CH5 switch (CRSF) — edge detect, same hold interlock */
{
static bool s_rc_armed_prev = false;
bool rc_armed_now = safety_rc_alive(now) && crsf_state.armed;
if (rc_armed_now && !s_rc_armed_prev) {
/* Rising edge: start arm hold (motors enable after ARMING_HOLD_MS) */
if (!safety_remote_estop_active() &&
mpu6000_is_calibrated() &&
bal.state == BALANCE_DISARMED && fabsf(bal.pitch_deg) < 10.0f) {
safety_arm_start(now);
}
}
if (!rc_armed_now && s_rc_armed_prev) {
/* Falling edge: cancel pending arm or disarm if already armed */
safety_arm_cancel();
if (bal.state == BALANCE_ARMED) balance_disarm(&bal);
}
s_rc_armed_prev = rc_armed_now;
}
/* Handle arm/disarm requests from USB with arm-hold interlock */
if (cdc_arm_request) {
cdc_arm_request = 0;
if (!safety_remote_estop_active() &&
mpu6000_is_calibrated() &&
bal.state == BALANCE_DISARMED && fabsf(bal.pitch_deg) < 10.0f) {
safety_arm_start(now);
}
}
/* Complete arming once hold timer expires */
if (safety_arm_ready(now) && bal.state == BALANCE_DISARMED) {
safety_arm_cancel();
balance_arm(&bal);
}
if (cdc_disarm_request) {
cdc_disarm_request = 0;
safety_arm_cancel();
balance_disarm(&bal);
}
/* Gyro recalibration — disarm first, then re-sample bias (~1s blocked) */
if (cdc_recal_request) {
cdc_recal_request = 0;
safety_arm_cancel();
balance_disarm(&bal);
if (imu_ret == 0) mpu6000_calibrate();
}
/* Handle PID tuning commands from USB (P/I/D/T/M/?) */
if (cdc_cmd_ready) {
cdc_cmd_ready = 0;
char reply[96];
apply_pid_cmd(&bal, (const char *)cdc_cmd_buf, reply, sizeof(reply));
CDC_Transmit((uint8_t *)reply, strlen(reply));
}
/* Handle Jetson C<speed>,<steer> command (parsed here, not in ISR) */
if (jetson_cmd_ready) {
jetson_cmd_ready = 0;
jetson_cmd_process();
}
/* Balance PID at 1kHz — apply Jetson speed offset when active */
if (imu_ret == 0 && now - balance_tick >= 1) {
balance_tick = now;
float base_sp = bal.setpoint;
if (jetson_cmd_is_active(now)) bal.setpoint += jetson_cmd_sp_offset();
balance_update(&bal, &imu, dt);
bal.setpoint = base_sp;
}
/* Latch estop on tilt fault or disarm */
if (bal.state == BALANCE_TILT_FAULT) {
motor_driver_estop(&motors);
} else if (bal.state == BALANCE_DISARMED && motors.estop &&
!safety_remote_estop_active()) {
motor_driver_estop_clear(&motors);
}
/* Feed autonomous steer from Jetson into mode manager.
* mode_manager_get_steer() blends it with RC steer per active mode. */
if (jetson_cmd_is_active(now))
mode_manager_set_auto_cmd(&mode, jetson_cmd_steer(), 0);
/* Send to hoverboard ESC at 50Hz (every 20ms) */
if (now - esc_tick >= 20) {
esc_tick = now;
if (bal.state == BALANCE_ARMED) {
/* Blended steer (RC ↔ auto per mode) + RC speed bias */
int16_t steer = mode_manager_get_steer(&mode);
int16_t spd_bias = mode_manager_get_speed_bias(&mode);
int32_t speed = (int32_t)bal.motor_cmd + spd_bias;
if (speed > 1000) speed = 1000;
if (speed < -1000) speed = -1000;
motor_driver_update(&motors, (int16_t)speed, steer, now);
} else {
/* Always send zero while disarmed to prevent ESC timeout */
motor_driver_update(&motors, 0, 0, now);
}
}
/* CRSF telemetry uplink — battery voltage + arm state at 1 Hz.
* Sends battery sensor frame (0x08) and flight mode frame (0x21)
* back to ELRS TX module so the pilot's handset OSD shows Vbat + state. */
if (now - crsf_telem_tick >= (1000u / CRSF_TELEMETRY_HZ)) {
crsf_telem_tick = now;
uint32_t vbat_mv = battery_read_mv();
uint8_t soc_pct = battery_estimate_pct(vbat_mv);
crsf_send_battery(vbat_mv, 0u, soc_pct);
crsf_send_flight_mode(bal.state == BALANCE_ARMED);
}
/* USB telemetry at 50Hz (only when streaming enabled and calibration done) */
if (cdc_streaming && mpu6000_is_calibrated() && now - send_tick >= 20) {
send_tick = now;
if (imu_ret == 0) {
float err = bal.setpoint - bal.pitch_deg;
/* Build JSON incrementally; append optional sensor fields */
char *p = buf;
int rem = (int)sizeof(buf);
int n;
n = snprintf(p, rem,
"{\"p\":%d,\"r\":%d,\"e\":%d,\"ig\":%d,\"m\":%d,\"s\":%d,\"y\":%d",
(int)(bal.pitch_deg * 10), /* pitch degrees x10 */
(int)(imu.roll * 10), /* roll degrees x10 */
(int)(err * 10), /* PID error x10 */
(int)(bal.integral * 10), /* integral x10 (windup monitor) */
(int)bal.motor_cmd, /* ESC command -1000..+1000 */
(int)bal.state,
(int)(imu.yaw * 10)); /* yaw degrees x10 (gyro-integrated) */
p += n; rem -= n;
n = snprintf(p, rem, ",\"md\":%d",
(int)mode_manager_active(&mode)); /* 0=MANUAL,1=ASSISTED,2=AUTO */
p += n; rem -= n;
if (mag_type != MAG_NONE) {
int16_t hd = mag_read_heading();
if (hd >= 0)
n = snprintf(p, rem, ",\"hd\":%d", hd); /* heading deg×10 */
else
n = snprintf(p, rem, ",\"hd\":-1"); /* not ready */
p += n; rem -= n;
}
if (baro_chip) {
int32_t pres_pa; int16_t temp_x10;
bmp280_read(&pres_pa, &temp_x10);
int32_t alt_cm = bmp280_pressure_to_alt_cm(pres_pa);
/* alt cm, temp °C×10, pressure hPa×10 (Pa÷10 = hPa×10) */
n = snprintf(p, rem, ",\"alt\":%ld,\"t\":%d,\"pa\":%ld",
(long)alt_cm, (int)temp_x10, (long)(pres_pa / 10));
p += n; rem -= n;
if (baro_chip == 0x60) { /* BME280: add humidity %RH×10 */
int16_t hum_x10 = bmp280_read_humidity();
if (hum_x10 >= 0) {
n = snprintf(p, rem, ",\"h\":%d", (int)hum_x10);
p += n; rem -= n;
}
}
}
if (safety_rc_alive(now)) {
/* RSSI, link quality, and CH1CH4 mapped to µs (10002000) */
n = snprintf(p, rem, ",\"rssi\":%d,\"lq\":%d"
",\"ch1\":%d,\"ch2\":%d,\"ch3\":%d,\"ch4\":%d",
(int)crsf_state.rssi_dbm, (int)crsf_state.link_quality,
(int)crsf_to_range(crsf_state.channels[0], 1000, 2000),
(int)crsf_to_range(crsf_state.channels[1], 1000, 2000),
(int)crsf_to_range(crsf_state.channels[2], 1000, 2000),
(int)crsf_to_range(crsf_state.channels[3], 1000, 2000));
p += n; rem -= n;
}
/* Jetson active flag, USB TX/RX packet counters */
int es; EstopSource _rs = safety_get_estop();
if (_rs >= ESTOP_REMOTE) es = (int)_rs;
else if (bal.state == BALANCE_TILT_FAULT) es = ESTOP_TILT;
else es = ESTOP_CLEAR;
n = snprintf(p, rem, ",\"ja\":%d,\"txc\":%u,\"rxc\":%u,\"es\":%d}\n",
jetson_cmd_is_active(now) ? 1 : 0,
(unsigned)tx_count,
(unsigned)cdc_rx_count, es);
len = (int)(p + n - buf);
} else {
len = snprintf(buf, sizeof(buf), "{\"err\":%d}\n", imu_ret);
}
CDC_Transmit((uint8_t *)buf, len);
tx_count++;
}
status_update(now, (imu_ret == 0),
(bal.state == BALANCE_ARMED),
(bal.state == BALANCE_TILT_FAULT),
safety_remote_estop_active());
HAL_Delay(1);
}
}