sl-firmware 9ed678ca35 feat: IMU mount angle cal, CAN telemetry, LED override (Issues #680, #672, #685)
Issue #680 — IMU mount angle calibration:
- imu_cal_flash.h/.c: store pitch/roll offsets in flash sector 7
  (0x0807FF00, 64 bytes; preserves PID records across sector erase)
- mpu6000_set_mount_offset(): subtracts offsets from pitch/roll output
- mpu6000_has_mount_offset(): reports cal_status=2 to Orin
- 'O' CDC command: capture current pitch/roll → save to flash → ACK JSON
- Load offsets on boot; report in printf log

CAN telemetry correction (Tee: production has no USB to Orin):
- FC_IMU (0x402): pitch/roll/yaw/cal_status/balance_state at 50 Hz
- orin_can_broadcast_imu() rate-limited to ORIN_IMU_TLM_HZ (50 Hz)
- FC_BARO (0x403): pressure_pa/temp_x10/alt_cm at 1 Hz (Issue #672)
- orin_can_broadcast_baro() rate-limited to ORIN_BARO_TLM_HZ (1 Hz)

Issue #685 — LED CAN override:
- ORIN_CAN_ID_LED_CMD (0x304): pattern/brightness/duration_ms from Orin
- orin_can_led_override volatile state + orin_can_led_updated flag
- main.c: apply pattern to LED state machine on each LED_CMD received

Orin side:
- saltybot_can_node.py: production SocketCAN bridge — reads 0x400-0x403,
  publishes /saltybot/imu, /saltybot/balance_state, /saltybot/barometer;
  subscribes /cmd_vel → 0x301 DRIVE; /saltybot/leds → 0x304 LED_CMD;
  sends 0x300 HEARTBEAT at 5 Hz; sends 0x303 ESTOP on shutdown
- setup.py: register saltybot_can_node entry point + uart_bridge launch

Fix: re-apply --defsym __stack_end=_estack-0x1000 linker fix to branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:49:21 -04:00

212 lines
7.7 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 "usbd_cdc_if.h"
#include "stm32f7xx_hal.h"
#include "ota.h"
extern USBD_HandleTypeDef hUsbDevice;
volatile uint8_t cdc_streaming = 1; /* auto-stream */
static volatile uint8_t cdc_port_open = 0; /* set when host asserts DTR */
volatile uint8_t cdc_arm_request = 0; /* set by A command */
volatile uint8_t cdc_disarm_request = 0; /* set by D command */
volatile uint8_t cdc_recal_request = 0; /* set by G command — gyro recalibration */
volatile uint8_t cdc_imu_cal_request = 0; /* set by O command — mount offset calibration (Issue #680) */
volatile uint32_t cdc_rx_count = 0; /* total CDC packets received from host */
volatile uint8_t cdc_estop_request = 0;
volatile uint8_t cdc_estop_clear_request = 0;
/*
* PID tuning command buffer.
* CDC_Receive (USB IRQ) copies multi-char commands here.
* Main loop polls cdc_cmd_ready, parses, and clears.
* Commands: P<kp> I<ki> D<kd> T<setpoint> M<max_speed> ?
*/
volatile uint8_t cdc_cmd_ready = 0;
volatile char cdc_cmd_buf[32];
/*
* Jetson command buffer (bidirectional protocol).
* 'H'\n — heartbeat, ISR updates jetson_hb_tick only (no buf copy needed).
* 'C'<s>,<t>\n — drive command: ISR copies to buf, main loop parses with sscanf.
* jetson_hb_tick is also refreshed on every C command.
*/
volatile uint8_t jetson_cmd_ready = 0;
volatile char jetson_cmd_buf[32];
volatile uint32_t jetson_hb_tick = 0; /* HAL_GetTick() of last H or C */
/*
* USB TX/RX buffers grouped into a single 512-byte aligned struct so that
* one MPU region (configured in usbd_conf.c) can mark them non-cacheable.
* Size must be a power-of-2 >= total size for MPU RASR SIZE encoding.
*/
static struct {
uint8_t tx[256];
uint8_t rx[256];
} __attribute__((aligned(512))) usb_nc_buf;
#define UserTxBuffer usb_nc_buf.tx
#define UserRxBuffer usb_nc_buf.rx
/* Exported so usbd_conf.c USB_NC_MPU_Config() can set the region base */
void * const usb_nc_buf_base = &usb_nc_buf;
/*
* Betaflight-proven DFU reboot:
* 1. Write magic to RTC backup register (persists across soft reset)
* 2. NVIC_SystemReset() — clean hardware reset
* 3. Early startup checks magic, clears it, jumps to system bootloader
*
* Magic is written to BKP15R (OTA_DFU_BKP_IDX) — not BKP0R — so that
* BKP0RBKP6R are available for BNO055 calibration offsets (PR #150).
* The magic check in checkForBootloader() reads the same BKP15R register.
*/
static void request_bootloader(void) {
/* Betaflight-proven: write magic, disable IRQs, reset.
* checkForBootloader() runs on next boot before anything else. */
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_RTC_ENABLE();
/* Write magic to BKP15R via OTA module constants (avoids BNO055 BKP06) */
(&RTC->BKP0R)[OTA_DFU_BKP_IDX] = OTA_DFU_MAGIC;
__disable_irq();
NVIC_SystemReset();
}
/*
* Call this VERY early in main(), before HAL_Init().
* Checks RTC backup register for magic value left by request_bootloader().
* If found: clear magic, jump to STM32F7 system bootloader at 0x1FF00000.
*/
void checkForBootloader(void) {
/*
* Betaflight-proven bootloader jump for STM32F7.
* Called VERY early, before HAL_Init/caches/clocks.
* At this point only RCC PWR is needed to read RTC backup regs.
*/
/* Enable backup domain access to read RTC backup register */
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_RTC_ENABLE();
uint32_t magic = (&RTC->BKP0R)[OTA_DFU_BKP_IDX]; /* read BKP15R */
if (magic != OTA_DFU_MAGIC) {
return; /* Normal boot */
}
/* Clear magic so next boot is normal */
(&RTC->BKP0R)[OTA_DFU_BKP_IDX] = 0;
/* Jump to STM32F7 system bootloader at 0x1FF00000.
* Exactly as Betaflight does it — no cache/VTOR/MEMRMP games needed
* because we run before any of that is configured. */
__HAL_RCC_SYSCFG_CLK_ENABLE();
__set_MSP(*(uint32_t *)0x1FF00000);
((void (*)(void))(*(uint32_t *)0x1FF00004))();
while (1);
}
static int8_t CDC_Init(void) {
USBD_CDC_SetTxBuffer(&hUsbDevice, UserTxBuffer, 0);
USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
USBD_CDC_ReceivePacket(&hUsbDevice);
/* Reset TxState so CDC_Transmit works after host (re)connects.
* Without this, if transmits happen before host opens port,
* TxState stays BUSY forever since host never ACKs. */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDevice.pClassData;
if (hcdc) hcdc->TxState = 0;
return USBD_OK;
}
static int8_t CDC_DeInit(void) { return USBD_OK; }
static int8_t CDC_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length) {
(void)pbuf; (void)length;
if (cmd == 0x22) { /* CDC_SET_CONTROL_LINE_STATE — host opened port */
cdc_port_open = 1;
cdc_streaming = 1;
}
return USBD_OK;
}
static int8_t CDC_Receive(uint8_t *buf, uint32_t *len) {
if (*len < 1) goto done;
switch (buf[0]) {
case 'S': cdc_streaming = !cdc_streaming; break;
case 'A': cdc_arm_request = 1; break;
case 'D': cdc_disarm_request = 1; break;
case 'G': cdc_recal_request = 1; break; /* gyro recalibration */
case 'O': cdc_imu_cal_request = 1; break; /* mount offset cal (Issue #680) */
case 'R': request_bootloader(); break; /* never returns */
case 'E': cdc_estop_request = 1; break;
case 'F': cdc_estop_request = 2; break;
case 'Z': cdc_estop_clear_request = 1; break;
/*
* PID tuning: P<kp> I<ki> D<kd> T<setpoint> M<max_speed> ?
* Copy to cmd buffer; main loop parses float (avoids sscanf in IRQ).
*/
case 'P': case 'I': case 'K': case 'T': case 'M': case '?': {
uint32_t copy_len = *len < 31 ? *len : 31;
for (uint32_t i = 0; i < copy_len; i++) cdc_cmd_buf[i] = (char)buf[i];
cdc_cmd_buf[copy_len] = '\0';
cdc_cmd_ready = 1;
break;
}
/* Jetson heartbeat — just refresh the tick, no buffer copy needed */
case 'H':
jetson_hb_tick = HAL_GetTick();
break;
/* Jetson drive command: C<speed>,<steer>\n
* Copy to buffer; main loop parses ints (keeps sscanf out of ISR). */
case 'C': {
uint32_t copy_len = *len < 31 ? *len : 31;
for (uint32_t i = 0; i < copy_len; i++) jetson_cmd_buf[i] = (char)buf[i];
jetson_cmd_buf[copy_len] = '\0';
jetson_hb_tick = HAL_GetTick(); /* C command also refreshes heartbeat */
jetson_cmd_ready = 1;
break;
}
default: break;
}
done:
cdc_rx_count++;
USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
USBD_CDC_ReceivePacket(&hUsbDevice);
return USBD_OK;
}
USBD_CDC_ItfTypeDef USBD_CDC_fops = { CDC_Init, CDC_DeInit, CDC_Control, CDC_Receive };
uint8_t CDC_Transmit(uint8_t *buf, uint16_t len) {
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDevice.pClassData;
if (hcdc == NULL) return USBD_FAIL;
if (hcdc->TxState != 0) {
/* If stuck busy (no host ACK), force reset after a while */
static uint32_t busy_count = 0;
if (++busy_count > 100) { hcdc->TxState = 0; busy_count = 0; }
return USBD_BUSY;
}
/* Always copy into the static UserTxBuffer so the USB hardware reads
* from a known fixed SRAM address — never from the caller's stack.
* The USB TXFE IRQ fires asynchronously; a stack buffer could be
* overwritten by the time the FIFO is loaded. */
if (len > sizeof(UserTxBuffer)) len = sizeof(UserTxBuffer);
memcpy(UserTxBuffer, buf, len);
USBD_CDC_SetTxBuffer(&hUsbDevice, UserTxBuffer, len);
return USBD_CDC_TransmitPacket(&hUsbDevice);
}