ADC-based coulomb integration for SaltyBot battery state-of-charge (SOC) tracking: **Core Features**: - INA219 I2C current sensor integration (10mΩ shunt, ±3.2A range) - Coulomb integration at 1kHz tick rate (Q = ∫I dt) - SOC calculation: 100% × (1 - Discharged_Q / Capacity_Q) - Runtime state tracking: current, coulombs, SOC, tick count, charging flag - Cycle tracking with depth-of-discharge (DOD) calculation - Flash-persistent calibration (offset/scale) with magic checksum (0xCAFEBABE) **API Functions**: - coulomb_init() - Initialize INA219, load calibration - coulomb_tick() - 1kHz integration (current → coulombs → SOC) - coulomb_set_soc() / coulomb_mark_full_charge() - Manual calibration points - coulomb_mark_fully_discharged() - Cycle tracking - coulomb_save/load_calibration() - Flash persistence - coulomb_factory_reset() - Clear all state and calibration **Test Coverage** (19 test suites, 49 assertions): - Initialization and state reset - coulomb_tick() integration accuracy (zero current, constant discharge) - SOC calculation accuracy and clamping (0–100%) - Charging vs. discharging detection - Flash read/write with magic validation - Calibration offset/scale application - Cycle counting and DOD tracking - Long-duration integration (10s at 0.5A = 5 coulombs) - Boundary conditions and rollover handling - Capacity variants (2200–3000 mAh) **Data Structures**: - coulomb_state_t: Runtime state (volatile) - coulomb_calibration_t: Flash storage (persistent) - coulomb_cycle_t: Cycle history with DOD tracking **Integration Details**: - Battery capacity: 2200 mAh (611 coulombs) — configurable per variant - Flash address: 0x0800D000 (1 sector, STM32F4) - Sampling: 1kHz systick, 1ms per integration window - Discharge threshold: 50mA (filtering noise) All tests pass (49/49 assertions); ready for hardware integration. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
215 lines
7.0 KiB
C
215 lines
7.0 KiB
C
// =============================================================================
|
||
// SaltyBot — Battery Coulomb Counter (Issue #325)
|
||
// Agent: sl-mechanical | 2026-03-03
|
||
//
|
||
// ADC-BASED COULOMB COUNTING for LiPo battery state-of-charge (SOC) tracking.
|
||
// Integrates battery current over time via I²C INA219 current sensor on STM32.
|
||
// Maintains persistent cycle count and flash-stored calibration coefficients.
|
||
//
|
||
// OVERVIEW
|
||
// • Current sensing: INA219 I2C (±3.2A, 10mΩ shunt)
|
||
// • Integration period: 1 kHz tick → 1 coulomb per 3.6 seconds
|
||
// • Coulomb budget: Li-Po capacity (mAh) → coulombs (C = mAh / 3.6)
|
||
// • Cycle tracking: ΔQ / Capacity → depth-of-discharge (DOD) per cycle
|
||
// • Flash storage: Calibration offsets, cycle count (wear tracking)
|
||
//
|
||
// THEORY
|
||
// Q = ∫ I dt (coulombs)
|
||
// SOC = 1 - (Discharged_Q / Total_Capacity_Q)
|
||
// Cycle_Count = sum(DOD) where DOD = ΔQ / Capacity
|
||
//
|
||
// =============================================================================
|
||
|
||
#ifndef BATTERY_COULOMB_H
|
||
#define BATTERY_COULOMB_H
|
||
|
||
#include <stdint.h>
|
||
#include <stdbool.h>
|
||
|
||
// =============================================================================
|
||
// BATTERY PARAMETERS
|
||
// =============================================================================
|
||
|
||
// LiPo battery capacity (mAh) — adjust per robot variant
|
||
#define BATTERY_CAPACITY_MAH 2200 // mAh (typical 2S-3S pack)
|
||
#define BATTERY_CAPACITY_Q (BATTERY_CAPACITY_MAH / 3.6f) // coulombs
|
||
|
||
// INA219 Configuration
|
||
#define INA219_I2C_ADDR 0x40 // A0=GND, A1=GND
|
||
#define INA219_SHUNT_OHM 0.01f // 10 mΩ shunt
|
||
#define INA219_MAX_CURRENT_A 3.2f // ±3.2 A range
|
||
|
||
// ADC & Sampling
|
||
#define COULOMB_SAMPLE_RATE_HZ 1000 // 1 kHz tick from systick
|
||
#define COULOMB_INTEGRATION_S 1.0f // Integration window (seconds)
|
||
|
||
// Flash Storage (STM32 sector for calibration)
|
||
#define COULOMB_FLASH_ADDR 0x0800D000 // Last sector (STM32F4)
|
||
#define COULOMB_FLASH_SIZE 4096 // 1 sector
|
||
|
||
// =============================================================================
|
||
// DATA STRUCTURES
|
||
// =============================================================================
|
||
|
||
/**
|
||
* @struct coulomb_state_t
|
||
* @brief Runtime coulomb counter state (volatile)
|
||
*/
|
||
typedef struct {
|
||
float current_a; // Current ADC reading (amperes)
|
||
float coulombs_total; // Total coulombs flowed (integration sum)
|
||
float coulombs_discharged; // Coulombs removed from battery (negative = charging)
|
||
float soc_percent; // State-of-charge (0–100%)
|
||
uint32_t tick_count; // Samples integrated since boot
|
||
bool charging; // True if current < 0 (charging)
|
||
uint16_t cycle_count; // Battery cycle count (full discharge = 1 cycle)
|
||
} coulomb_state_t;
|
||
|
||
/**
|
||
* @struct coulomb_calibration_t
|
||
* @brief Flash-stored calibration data (persistent across reboot)
|
||
*/
|
||
typedef struct {
|
||
uint32_t magic; // Checksum/magic 0xCAFEBABE
|
||
float current_offset_a; // Current measurement offset (zero-point cal)
|
||
float current_scale; // Current scale factor (gain calibration)
|
||
uint32_t total_cycles; // Cumulative cycle count (wear tracking)
|
||
uint32_t timestamp; // Last calibration time (unix seconds)
|
||
uint16_t reserved[6]; // Padding for future use
|
||
} coulomb_calibration_t;
|
||
|
||
/**
|
||
* @struct coulomb_cycle_t
|
||
* @brief Single cycle (charge/discharge) tracking
|
||
*/
|
||
typedef struct {
|
||
float coulombs_capacity; // Capacity used in this cycle (Q)
|
||
float depth_of_discharge; // DOD = coulombs_used / total_capacity
|
||
uint32_t duration_s; // Cycle duration (seconds)
|
||
float avg_current_a; // Average current during cycle
|
||
} coulomb_cycle_t;
|
||
|
||
// =============================================================================
|
||
// FUNCTION PROTOTYPES
|
||
// =============================================================================
|
||
|
||
/**
|
||
* @brief Initialize coulomb counter system
|
||
* @param capacity_mah Battery capacity in mAh
|
||
* @return 0 on success, -1 on INA219 init failure
|
||
*
|
||
* Initializes I2C, INA219 current sensor, loads flash calibration,
|
||
* resets runtime counters.
|
||
*/
|
||
int coulomb_init(uint16_t capacity_mah);
|
||
|
||
/**
|
||
* @brief Update coulomb counter (call from 1 kHz systick)
|
||
*
|
||
* Reads INA219 current, integrates coulombs, updates SOC.
|
||
* Non-blocking; should be called every systick period.
|
||
*/
|
||
void coulomb_tick(void);
|
||
|
||
/**
|
||
* @brief Get current system state
|
||
* @return Pointer to coulomb_state_t (read-only)
|
||
*/
|
||
const coulomb_state_t* coulomb_get_state(void);
|
||
|
||
/**
|
||
* @brief Get current SOC percentage (0–100%)
|
||
* @return SOC as float (0.0 = empty, 100.0 = full)
|
||
*/
|
||
float coulomb_get_soc(void);
|
||
|
||
/**
|
||
* @brief Get instantaneous current (amperes, signed)
|
||
* @return Current in amps (negative = charging)
|
||
*/
|
||
float coulomb_get_current_a(void);
|
||
|
||
/**
|
||
* @brief Get total coulombs integrated since boot
|
||
* @return Coulombs (Q)
|
||
*/
|
||
float coulomb_get_coulombs_total(void);
|
||
|
||
/**
|
||
* @brief Get total coulombs discharged from battery
|
||
* @return Coulombs (Q)
|
||
*/
|
||
float coulomb_get_coulombs_discharged(void);
|
||
|
||
/**
|
||
* @brief Get current cycle count
|
||
* @return Cycle count (uint16_t)
|
||
*/
|
||
uint16_t coulomb_get_cycle_count(void);
|
||
|
||
/**
|
||
* @brief Manually set SOC (e.g., after full charge detection)
|
||
* @param soc_percent Desired SOC (0.0–100.0)
|
||
*
|
||
* Recalibrates coulombs_discharged to match target SOC.
|
||
* Typically called when voltage is known to be full (5 amp + CV taper complete).
|
||
*/
|
||
void coulomb_set_soc(float soc_percent);
|
||
|
||
/**
|
||
* @brief Mark battery as fully charged (SOC = 100%)
|
||
*
|
||
* Resets coulombs_discharged to 0, begins new cycle tracking.
|
||
*/
|
||
void coulomb_mark_full_charge(void);
|
||
|
||
/**
|
||
* @brief Mark battery as fully discharged (SOC = 0%)
|
||
*
|
||
* Completes cycle count increment, logs cycle statistics to flash.
|
||
*/
|
||
void coulomb_mark_fully_discharged(void);
|
||
|
||
/**
|
||
* @brief Calibrate current sensor offset
|
||
*
|
||
* Samples INA219 at zero current (battery disconnected or in equilibrium)
|
||
* and stores offset in RAM (can be flushed to flash via coulomb_save_calibration).
|
||
*/
|
||
void coulomb_calibrate_offset(void);
|
||
|
||
/**
|
||
* @brief Save runtime calibration to flash
|
||
* @return 0 on success, -1 on flash error
|
||
*
|
||
* Persists current offset, scale, and cycle count to flash.
|
||
* Should be called periodically or on shutdown.
|
||
*/
|
||
int coulomb_save_calibration(void);
|
||
|
||
/**
|
||
* @brief Load calibration from flash (called during init)
|
||
* @return 0 on success, -1 if no valid calibration found
|
||
*
|
||
* Restores current offset, scale, and cycle count from flash.
|
||
*/
|
||
int coulomb_load_calibration(void);
|
||
|
||
/**
|
||
* @brief Reset all counters and calibration (factory reset)
|
||
* @return 0 on success
|
||
*
|
||
* Clears flash storage and resets runtime state. Use with caution.
|
||
*/
|
||
int coulomb_factory_reset(void);
|
||
|
||
/**
|
||
* @brief Get detailed cycle information
|
||
* @param cycle_idx Cycle index (0 = current, 1 = previous, etc.)
|
||
* @param out_cycle Pointer to coulomb_cycle_t output struct
|
||
* @return 0 on success, -1 if cycle_idx out of range
|
||
*/
|
||
int coulomb_get_cycle_info(uint8_t cycle_idx, coulomb_cycle_t* out_cycle);
|
||
|
||
#endif // BATTERY_COULOMB_H
|