battery.cpp (2600B)
1 // battery.cpp — LiPo fuel gauge implementation. See battery.h. 2 // 3 #include <Arduino.h> 4 #include "battery.h" 5 #include "config.h" 6 7 static uint16_t g_mv = 0; // smoothed battery millivolts (0 = not yet sampled) 8 static uint32_t g_last_sample_ms = 0; 9 10 // Burst-read VBAT → divider-corrected millivolts. One discarded priming read lets 11 // the ADC sample/hold cap settle (it can't fully charge through the ~500kΩ onboard 12 // divider in a single conversion), then average VBAT_BURST_SAMPLES reads to beat 13 // down the per-read noise + low-bias. Pin millivolts are averaged BEFORE the 14 // ×VBAT_DIVIDER scale so the rounding happens at full resolution. 15 static uint16_t read_vbat_mv() { 16 (void)analogReadMilliVolts(PIN_VBAT); // priming read (settle the S/H cap) 17 uint32_t sum = 0; 18 for (int i = 0; i < VBAT_BURST_SAMPLES; i++) 19 sum += analogReadMilliVolts(PIN_VBAT); 20 return (uint16_t)((sum / VBAT_BURST_SAMPLES) * VBAT_DIVIDER); 21 } 22 23 void battery_init() { 24 // GPIO34 is input-only; analogReadMilliVolts handles attenuation/calibration. 25 // 11dB attenuation lets the ~0..2.1V at the pin (half of 4.2V) read cleanly. 26 // 27 // Arduino-ESP32 core 3.x's ADC oneshot driver wants the pin attached as an 28 // analog channel BEFORE attenuation is configured, else it logs 29 // [E] __analogChannelConfig(): Pin is not configured as analog channel 30 // A throwaway analogRead() attaches the channel first; harmless ordering on 31 // core 2.x, which configures the pin lazily either way. 32 analogRead(PIN_VBAT); 33 analogSetPinAttenuation(PIN_VBAT, ADC_11db); 34 // Seed the EMA with one immediate (burst-averaged) reading so the first frame 35 // isn't 0% — and so the gauge opens near the true voltage, not a low single read. 36 g_mv = read_vbat_mv(); 37 g_last_sample_ms = 0; 38 } 39 40 void battery_sample(uint32_t now) { 41 if (g_last_sample_ms != 0 && now - g_last_sample_ms < VBAT_SAMPLE_MS) return; 42 g_last_sample_ms = now; 43 44 uint16_t raw = read_vbat_mv(); 45 if (g_mv == 0) { 46 g_mv = raw; // first real sample 47 } else { 48 // EMA: g = g + alpha*(raw - g), alpha = NUM/DEN, in integer math. 49 int32_t delta = (int32_t)raw - (int32_t)g_mv; 50 g_mv = (uint16_t)((int32_t)g_mv + (delta * VBAT_EMA_NUM) / VBAT_EMA_DEN); 51 } 52 } 53 54 uint16_t battery_mv() { 55 return g_mv; 56 } 57 58 uint8_t battery_pct() { 59 if (g_mv <= VBAT_EMPTY_MV) return 0; 60 if (g_mv >= VBAT_FULL_MV) return 100; 61 uint32_t span = VBAT_FULL_MV - VBAT_EMPTY_MV; 62 return (uint8_t)(((uint32_t)(g_mv - VBAT_EMPTY_MV) * 100) / span); 63 } 64 65 bool battery_is_low() { 66 return g_mv != 0 && g_mv <= VBAT_LOW_MV; 67 }