/* * coulomb_counter.c — Battery coulomb counter (Issue #325) * * Tracks Ah consumed from current readings, provides SoC independent of load. * Time integration: consumed_mah += current_ma * dt_ms / 3600000 */ #include "coulomb_counter.h" #include "stm32f7xx_hal.h" /* State structure */ static struct { bool initialized; bool valid; /* At least one measurement taken */ uint16_t capacity_mah; /* Battery capacity in mAh */ uint32_t accumulated_mah_x100; /* Accumulated coulombs in mAh×100 (fixed-point) */ uint32_t last_tick_ms; /* Last update timestamp (ms) */ } s_state = {0}; void coulomb_counter_init(uint16_t capacity_mah) { if (capacity_mah == 0 || capacity_mah > 20000) { /* Sanity check: reasonable battery is 100–20000 mAh */ return; } s_state.capacity_mah = capacity_mah; s_state.accumulated_mah_x100 = 0; s_state.last_tick_ms = HAL_GetTick(); s_state.initialized = true; s_state.valid = false; } void coulomb_counter_accumulate(int16_t current_ma) { if (!s_state.initialized) return; uint32_t now_ms = HAL_GetTick(); uint32_t dt_ms = now_ms - s_state.last_tick_ms; /* Handle tick wraparound (~49.7 days at 32-bit ms) */ if (dt_ms > 86400000UL) { /* If jump > 1 day, likely wraparound; skip this sample */ s_state.last_tick_ms = now_ms; return; } /* Prevent negative dt or dt=0 */ if (dt_ms == 0) return; if (dt_ms > 1000) { /* Cap to 1 second max per call to prevent overflow */ dt_ms = 1000; } /* Accumulate: mAh += mA × dt_ms / 3600000 * Using fixed-point (×100): accumulated_mah_x100 += mA × dt_ms / 36000 */ int32_t coulomb_x100 = (int32_t)current_ma * (int32_t)dt_ms / 36000; /* Only accumulate if discharging (positive current) or realistic charging */ if (coulomb_x100 > 0) { s_state.accumulated_mah_x100 += (uint32_t)coulomb_x100; } else if (coulomb_x100 < 0 && s_state.accumulated_mah_x100 > 0) { /* Allow charging (negative current) to reduce accumulated coulombs */ int32_t new_val = (int32_t)s_state.accumulated_mah_x100 + coulomb_x100; if (new_val < 0) { s_state.accumulated_mah_x100 = 0; } else { s_state.accumulated_mah_x100 = (uint32_t)new_val; } } /* Clamp to capacity */ if (s_state.accumulated_mah_x100 > (uint32_t)s_state.capacity_mah * 100) { s_state.accumulated_mah_x100 = (uint32_t)s_state.capacity_mah * 100; } s_state.last_tick_ms = now_ms; s_state.valid = true; } uint8_t coulomb_counter_get_soc_pct(void) { if (!s_state.valid) return 255; /* 255 = invalid/not measured */ /* SoC = 100 - (consumed_mah / capacity_mah) * 100 */ uint32_t consumed_mah = s_state.accumulated_mah_x100 / 100; if (consumed_mah >= s_state.capacity_mah) { return 0; /* Fully discharged */ } uint32_t remaining_mah = s_state.capacity_mah - consumed_mah; uint8_t soc = (uint8_t)((remaining_mah * 100u) / s_state.capacity_mah); return soc; } uint16_t coulomb_counter_get_consumed_mah(void) { return (uint16_t)(s_state.accumulated_mah_x100 / 100); } uint16_t coulomb_counter_get_remaining_mah(void) { if (!s_state.valid) return s_state.capacity_mah; uint32_t consumed = s_state.accumulated_mah_x100 / 100; if (consumed >= s_state.capacity_mah) { return 0; } return (uint16_t)(s_state.capacity_mah - consumed); } void coulomb_counter_reset(void) { if (!s_state.initialized) return; s_state.accumulated_mah_x100 = 0; s_state.last_tick_ms = HAL_GetTick(); } bool coulomb_counter_is_valid(void) { return s_state.valid; }