sl-firmware 7785a16bff feat: Battery voltage telemetry and LVC (Issue #613)
- Add include/lvc.h + src/lvc.c: 3-stage low voltage cutoff state machine
  WARNING  21.0V: MELODY_LOW_BATTERY buzzer, full motor power
  CRITICAL 19.8V: double-beep every 10s, 50% motor power scaling
  CUTOFF   18.6V: MELODY_ERROR one-shot, motors disabled + latched
  200mV hysteresis on recovery; CUTOFF latched until reboot
- Add JLINK_TLM_LVC (0x8B, 4 bytes): voltage_mv, percent, protection_state
  jlink_send_lvc_tlm() frame encoder in jlink.c
- Wire into main.c:
  lvc_init() at startup; lvc_tick() each 1kHz loop tick
  lvc_is_cutoff() triggers safety_arm_cancel + balance_disarm + motor_driver_estop
  lvc_get_power_scale() applied to ESC speed command (100/50/0%)
  1Hz JLINK_TLM_LVC telemetry with fuel-gauge percent field
- Add LVC thresholds to config.h (LVC_WARNING/CRITICAL/CUTOFF/HYSTERESIS_MV)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:04:38 -04:00

145 lines
4.1 KiB
C

/*
* lvc.c -- Low Voltage Cutoff (LVC) protection (Issue #613)
*
* State machine:
* NORMAL -> WARNING when vbat < LVC_WARNING_MV
* WARNING -> CRITICAL when vbat < LVC_CRITICAL_MV
* CRITICAL -> CUTOFF when vbat < LVC_CUTOFF_MV
*
* Recovery: step down severity only when voltage exceeds the threshold
* by LVC_HYSTERESIS_MV. CUTOFF is latched for the remainder of the session.
*
* Buzzer alerts:
* WARNING -- MELODY_LOW_BATTERY once, then every 30 s
* CRITICAL -- MELODY_LOW_BATTERY x2, then every 10 s
* CUTOFF -- MELODY_ERROR (one-shot; motor disable handled by main.c)
*/
#include "lvc.h"
#include "buzzer.h"
#include "config.h"
/* Periodic buzzer reminder intervals */
#define LVC_WARN_INTERVAL_MS 30000u /* 30 s */
#define LVC_CRIT_INTERVAL_MS 10000u /* 10 s */
static LvcState s_state = LVC_NORMAL;
static bool s_cutoff_latched = false;
static uint32_t s_buzzer_tick = 0u;
/* ---- lvc_init() ---- */
void lvc_init(void)
{
s_state = LVC_NORMAL;
s_cutoff_latched = false;
s_buzzer_tick = 0u;
}
/* ---- lvc_tick() ---- */
void lvc_tick(uint32_t now_ms, uint32_t vbat_mv)
{
if (vbat_mv == 0u) {
return; /* ADC not ready; hold current state */
}
/* Determine new state from raw voltage */
LvcState new_state;
if (vbat_mv < LVC_CUTOFF_MV) {
new_state = LVC_CUTOFF;
} else if (vbat_mv < LVC_CRITICAL_MV) {
new_state = LVC_CRITICAL;
} else if (vbat_mv < LVC_WARNING_MV) {
new_state = LVC_WARNING;
} else {
new_state = LVC_NORMAL;
}
/* Hysteresis on recovery: only decrease severity when voltage exceeds
* the threshold by LVC_HYSTERESIS_MV to prevent rapid toggling. */
if (new_state < s_state) {
LvcState recovered;
if (vbat_mv >= LVC_CUTOFF_MV + LVC_HYSTERESIS_MV) {
recovered = LVC_CRITICAL;
} else if (vbat_mv >= LVC_CRITICAL_MV + LVC_HYSTERESIS_MV) {
recovered = LVC_WARNING;
} else if (vbat_mv >= LVC_WARNING_MV + LVC_HYSTERESIS_MV) {
recovered = LVC_NORMAL;
} else {
recovered = s_state; /* insufficient margin; stay at current level */
}
new_state = recovered;
}
/* CUTOFF latch: once triggered, only a reboot clears it */
if (s_cutoff_latched) {
new_state = LVC_CUTOFF;
}
if (new_state == LVC_CUTOFF) {
s_cutoff_latched = true;
}
/* Buzzer alerts */
bool state_changed = (new_state != s_state);
s_state = new_state;
switch (s_state) {
case LVC_WARNING:
if (state_changed ||
(now_ms - s_buzzer_tick) >= LVC_WARN_INTERVAL_MS) {
s_buzzer_tick = now_ms;
buzzer_play_melody(MELODY_LOW_BATTERY);
}
break;
case LVC_CRITICAL:
if (state_changed ||
(now_ms - s_buzzer_tick) >= LVC_CRIT_INTERVAL_MS) {
s_buzzer_tick = now_ms;
/* Double alert to distinguish critical from warning */
buzzer_play_melody(MELODY_LOW_BATTERY);
buzzer_play_melody(MELODY_LOW_BATTERY);
}
break;
case LVC_CUTOFF:
if (state_changed) {
/* One-shot alarm; motors disabled by main.c */
buzzer_play_melody(MELODY_ERROR);
}
break;
default:
break;
}
}
/* ---- lvc_get_state() ---- */
LvcState lvc_get_state(void)
{
return s_state;
}
/* ---- lvc_get_power_scale() ---- */
/*
* Returns the motor power scale factor (0-100):
* NORMAL / WARNING : 100% -- no reduction
* CRITICAL : 50% -- halve motor commands
* CUTOFF : 0% -- all commands zeroed
*/
uint8_t lvc_get_power_scale(void)
{
switch (s_state) {
case LVC_NORMAL: /* fall-through */
case LVC_WARNING: return 100u;
case LVC_CRITICAL: return 50u;
case LVC_CUTOFF: return 0u;
default: return 100u;
}
}
/* ---- lvc_is_cutoff() ---- */
bool lvc_is_cutoff(void)
{
return s_cutoff_latched;
}