Skip to main content

oxstack_cardputer/
battery.rs

1//! Battery voltage on GPIO10 (ADC1) through a 100 kΩ / 100 kΩ divider.
2//!
3//! The board routes no charge-status or USB-presence signal to a GPIO, so
4//! voltage is the only battery information available. While USB power is
5//! connected, the reading is the charger's output, not the cell's resting
6//! voltage.
7
8use esp_hal::Async;
9use esp_hal::analog::adc::{Adc, AdcCalCurve, AdcConfig, AdcPin, Attenuation};
10use esp_hal::peripherals::{ADC1, GPIO10};
11
12/// The divider halves the battery voltage.
13const DIVIDER_RATIO: u32 = 2;
14/// Samples averaged per reading.
15const SAMPLES: u32 = 16;
16
17type Cal<'d> = AdcCalCurve<ADC1<'d>>;
18
19/// ADC1 and the sense pin.
20#[allow(missing_docs)]
21pub struct BatteryResources<'d> {
22    pub adc: ADC1<'d>,
23    pub pin: GPIO10<'d>,
24}
25reborrow_group!(BatteryResources { adc, pin });
26
27impl<'d> BatteryResources<'d> {
28    /// Configures ADC1 with eFuse curve calibration on the sense pin.
29    pub fn init(&mut self) -> Battery<'_> {
30        let mut config = AdcConfig::new();
31        // 11 dB covers ~0–3.1 V at the pin, i.e. a 0–6.2 V battery.
32        let pin = config.enable_pin_with_cal(self.pin.reborrow(), Attenuation::_11dB);
33        let adc = Adc::new(self.adc.reborrow(), config).into_async();
34        Battery { adc, pin }
35    }
36}
37
38/// Battery voltage monitor.
39pub struct Battery<'d> {
40    adc: Adc<'d, ADC1<'d>, Async>,
41    pin: AdcPin<GPIO10<'d>, ADC1<'d>, Cal<'d>>,
42}
43
44/// Rough state of charge for a single Li-ion cell from its voltage, in %.
45///
46/// Piecewise-linear over a typical resting-voltage curve. It is only an
47/// estimate: it reads high while charging (the charger holds ~4.2 V) and
48/// low under heavy load (Wi-Fi bursts, speaker).
49pub const fn estimate_percent(millivolts: u16) -> u8 {
50    const CURVE: [(u16, u8); 10] = [
51        (3300, 0),
52        (3400, 5),
53        (3500, 10),
54        (3600, 20),
55        (3700, 35),
56        (3800, 50),
57        (3900, 65),
58        (4000, 80),
59        (4100, 90),
60        (4200, 100),
61    ];
62    if millivolts <= CURVE[0].0 {
63        return 0;
64    }
65    let mut i = 1;
66    while i < CURVE.len() {
67        let (v1, p1) = CURVE[i];
68        if millivolts <= v1 {
69            let (v0, p0) = CURVE[i - 1];
70            let span = (millivolts - v0) as u32 * (p1 - p0) as u32 / (v1 - v0) as u32;
71            return p0 + span as u8;
72        }
73        i += 1;
74    }
75    100
76}
77
78const _: () = {
79    assert!(estimate_percent(3000) == 0);
80    assert!(estimate_percent(3300) == 0);
81    assert!(estimate_percent(3750) == 42); // halfway between 35 % and 50 %
82    assert!(estimate_percent(4200) == 100);
83    assert!(estimate_percent(4400) == 100);
84};
85
86impl Battery<'_> {
87    /// Battery voltage in millivolts, averaged over 16 calibrated samples.
88    pub async fn millivolts(&mut self) -> u16 {
89        let mut sum = 0u32;
90        for _ in 0..SAMPLES {
91            // With a calibration scheme, readings are already in mV.
92            sum += u32::from(self.adc.read_oneshot(&mut self.pin).await);
93        }
94        (sum / SAMPLES * DIVIDER_RATIO) as u16
95    }
96
97    /// [`estimate_percent`] of a fresh reading.
98    pub async fn percent(&mut self) -> u8 {
99        estimate_percent(self.millivolts().await)
100    }
101}