Archive STM32 firmware to legacy/stm32/: - src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py - test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c - USB_CDC_BUG.md Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol, stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params, stm32_cmd.launch.py → esp32_cmd.launch.py, test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node Content cleanup across all files: - Mamba F722S → ESP32-S3 BALANCE - BlackPill → ESP32-S3 IO - STM32F722/F7xx → ESP32-S3 - stm32Mode/Version/Port → esp32Mode/Version/Port - STM32 State/Mode labels → ESP32 State/Mode - Jetson Nano → Jetson Orin Nano Super - /dev/stm32 → /dev/esp32 - stm32_bridge → esp32_bridge - STM32 HAL → ESP-IDF docs/SALTYLAB.md: - Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28) - Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF No new functionality — cleanup only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
3.7 KiB
C
119 lines
3.7 KiB
C
/*
|
||
* 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;
|
||
}
|