saltylab-firmware/legacy/stm32/src/coulomb_counter.c
sl-firmware fa75c442a7 feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only
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>
2026-04-04 09:00:38 -04:00

119 lines
3.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 10020000 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;
}