From 4dd52b47dc9a056e70d7d0f95ad30a49ac7a13d7 Mon Sep 17 00:00:00 2001 From: sl-controls Date: Sat, 28 Feb 2026 12:33:15 -0500 Subject: [PATCH] feat(safety): IWDG watchdog, arm hold interlock, tilt alert (bd-3qh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safety systems implementation: IWDG Hardware Watchdog (50ms timeout, config.h WATCHDOG_TIMEOUT_MS): - safety_init() configures IWDG at PSC/32 (0.8ms tick), reload=62 - safety_refresh() must be called every loop iteration - Cannot be disabled once started — MCU resets if loop hangs - Started after 3s USB init delay (avoids spurious startup reset) Arm Hold Interlock (3s, config.h ARMING_HOLD_MS): - Arm command starts a hold timer, not immediate motor enable - Motors only enable after ARMING_HOLD_MS consecutive hold - Disarm or tilt > 10° cancels pending arm - Prevents accidental arm from single keypress Tilt Fault Alert: - safety_alert_tilt_fault() fires one-shot buzzer on TILT_FAULT edge - Rider hears alarm when tilt cutoff triggers - Edge-detected (buzzer only fires once per fault event) RC Timeout (infrastructure): - safety_rc_alive() checks crsf_state.last_rx_ms vs RC_TIMEOUT_MS - RC disarm wired but guarded (no CRSF yet) — remove guard when wired - Compatible with future CRSF implementation Co-Authored-By: Claude Sonnet 4.6 --- include/safety.h | 51 ++++++++++++++++++++++++++++++++ src/main.c | 34 ++++++++++++++++++++- src/safety.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 include/safety.h create mode 100644 src/safety.c diff --git a/include/safety.h b/include/safety.h new file mode 100644 index 0000000..8086fb6 --- /dev/null +++ b/include/safety.h @@ -0,0 +1,51 @@ +#ifndef SAFETY_H +#define SAFETY_H + +#include +#include + +/* + * 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 */ diff --git a/src/main.c b/src/main.c index 40c6441..ba031fc 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,9 @@ #include "hoverboard.h" #include "config.h" #include "status.h" +#include "safety.h" +#include "crsf.h" +#include #include #include @@ -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); } diff --git a/src/safety.c b/src/safety.c new file mode 100644 index 0000000..5408c1b --- /dev/null +++ b/src/safety.c @@ -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; +} -- 2.47.2