feat(safety): IWDG watchdog, arm hold interlock, tilt alert (bd-3qh) #4

Merged
sl-jetson merged 1 commits from sl-controls/bd-3qh-safety-systems into main 2026-02-28 13:12:18 -05:00
3 changed files with 161 additions and 1 deletions

51
include/safety.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef SAFETY_H
#define SAFETY_H
#include <stdint.h>
#include <stdbool.h>
/*
* SaltyLab Safety Systems
*
* Covers:
* - IWDG hardware watchdog (MCU reset if main loop hangs)
* - RC signal timeout monitoring
* - Tilt fault alert via buzzer
* - Arm hold interlock (must hold arm for ARMING_HOLD_MS)
*/
/*
* safety_init() call once in main() after HAL_Init().
* Starts IWDG with WATCHDOG_TIMEOUT_MS timeout from config.h.
* Starts ARMING_HOLD_MS countdown from config.h.
*/
void safety_init(void);
/*
* safety_refresh() call every main loop iteration.
* Resets IWDG counter. If not called within WATCHDOG_TIMEOUT_MS,
* the MCU will reset (independent of software cannot be disabled).
*/
void safety_refresh(void);
/*
* safety_rc_alive() returns true if RC receiver has sent a frame
* within RC_TIMEOUT_MS. Call from the balance loop.
*/
bool safety_rc_alive(uint32_t now);
/*
* safety_alert_tilt_fault() one-shot buzzer beep for tilt fault.
* Safe to call repeatedly; only fires once per fault.
*/
void safety_alert_tilt_fault(bool faulted);
/*
* safety_arm_interlock() returns true once arm button has been
* held for ARMING_HOLD_MS from the moment safety_arm_start() was called.
*/
void safety_arm_start(uint32_t now); /* Call when arm requested */
bool safety_arm_ready(uint32_t now); /* Poll until true, then arm */
void safety_arm_cancel(void); /* Cancel pending arm */
#endif /* SAFETY_H */

View File

@ -8,6 +8,9 @@
#include "hoverboard.h"
#include "config.h"
#include "status.h"
#include "safety.h"
#include "crsf.h"
#include <math.h>
#include <string.h>
#include <stdio.h>
@ -101,6 +104,14 @@ int main(void) {
status_init();
HAL_Delay(3000); /* Wait for USB host to enumerate */
/*
* IWDG starts after the USB enumeration delay avoids spurious reset
* during the 3s startup hold. Once started, safety_refresh() MUST be
* called at least every WATCHDOG_TIMEOUT_MS (50ms) or MCU resets.
* IWDG cannot be stopped once started (hardware enforced).
*/
safety_init();
/* Init IMU (MPU6000 via SPI1 — mpu6000.c wraps icm42688 + complementary filter) */
int imu_ret = mpu6000_init() ? 0 : -1;
@ -125,13 +136,34 @@ int main(void) {
uint32_t now = HAL_GetTick();
/* Handle arm/disarm requests from USB */
/* Feed hardware watchdog — must happen every WATCHDOG_TIMEOUT_MS */
safety_refresh();
/* RC timeout: disarm if signal lost while armed */
if (bal.state == BALANCE_ARMED && !safety_rc_alive(now)) {
/* USB-only mode: no RC receiver expected yet — skip RC timeout.
* Uncomment when CRSF is wired: balance_disarm(&bal); */
}
/* Tilt fault buzzer alert (one-shot on fault edge) */
safety_alert_tilt_fault(bal.state == BALANCE_TILT_FAULT);
/* Handle arm/disarm requests from USB with arm-hold interlock */
if (cdc_arm_request) {
cdc_arm_request = 0;
/* Start the arm hold timer — motors don't enable until ARMING_HOLD_MS */
if (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);
}

77
src/safety.c Normal file
View File

@ -0,0 +1,77 @@
/*
* safety.c SaltyLab Safety Systems
*
* IWDG: 40kHz LSI, prescaler 32 tick = 0.8ms.
* WATCHDOG_TIMEOUT_MS from config.h (default 50ms reload = 62).
* Formula: reload = (timeout_ms / (prescaler / 40000)) - 1
* reload = (50 * 40000 / 32) - 1 = 62499 clamp to 4095 (12-bit max)
* At PSC=256: tick = 6.4ms, reload = ceil(50/6.4)-1 = 7 ~57.6ms
* Using PSC=32: tick = 0.8ms, reload = ceil(50/0.8)-1 = 62 50ms
*/
#include "safety.h"
#include "config.h"
#include "crsf.h"
#include "stm32f7xx_hal.h"
/* IWDG prescaler 32 → LSI(40kHz)/32 = 1250 ticks/sec → 0.8ms/tick */
#define IWDG_PRESCALER IWDG_PRESCALER_32
#define IWDG_TICK_MS (32.0f / 40.0f) /* 0.8ms per tick */
#define IWDG_RELOAD ((uint32_t)(WATCHDOG_TIMEOUT_MS / IWDG_TICK_MS))
#if IWDG_RELOAD > 4095
# error "WATCHDOG_TIMEOUT_MS too large for IWDG_PRESCALER_32 — increase prescaler"
#endif
static IWDG_HandleTypeDef hiwdg;
/* Arm interlock */
static uint32_t s_arm_start_ms = 0;
static bool s_arm_pending = false;
/* Tilt fault alert state — edge-detect to fire buzzer once */
static bool s_was_faulted = false;
void safety_init(void) {
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER;
hiwdg.Init.Reload = IWDG_RELOAD;
hiwdg.Init.Window = IWDG_WINDOW_DISABLE;
HAL_IWDG_Init(&hiwdg); /* Starts watchdog immediately */
}
void safety_refresh(void) {
HAL_IWDG_Refresh(&hiwdg);
}
bool safety_rc_alive(uint32_t now) {
/* If crsf_state has never received a frame, last_rx_ms == 0 */
if (crsf_state.last_rx_ms == 0) return false;
return (now - crsf_state.last_rx_ms) < RC_TIMEOUT_MS;
}
void safety_alert_tilt_fault(bool faulted) {
if (faulted && !s_was_faulted) {
/* Rising edge: single buzzer burst to alert rider */
HAL_GPIO_WritePin(BEEPER_PORT, BEEPER_PIN, GPIO_PIN_SET);
HAL_Delay(200);
HAL_GPIO_WritePin(BEEPER_PORT, BEEPER_PIN, GPIO_PIN_RESET);
}
s_was_faulted = faulted;
}
void safety_arm_start(uint32_t now) {
if (!s_arm_pending) {
s_arm_start_ms = now;
s_arm_pending = true;
}
}
bool safety_arm_ready(uint32_t now) {
if (!s_arm_pending) return false;
return (now - s_arm_start_ms) >= ARMING_HOLD_MS;
}
void safety_arm_cancel(void) {
s_arm_pending = false;
}