Skip to main content

core_s3/
power.rs

1//! Power and battery helpers for the CoreS3 AXP2101 PMIC.
2//!
3//! Battery state-of-charge uses the AXP2101 battery level register when valid,
4//! matching M5Unified's CoreS3 behavior. Voltage-derived percentage remains a
5//! coarse fallback only because LiPo voltage depends on load, age, temperature,
6//! and charge/discharge history.
7
8use embedded_hal::i2c::I2c;
9
10use crate::devices;
11
12const REG_STATUS1: u8 = 0x00;
13const REG_STATUS2: u8 = 0x01;
14const REG_DATA_BUFFER0: u8 = 0x04;
15const REG_POWER_OFF: u8 = 0x10;
16const REG_LDOS_ON_OFF: u8 = 0x90;
17const REG_ALDO3_VOLTAGE: u8 = 0x94;
18const REG_ALDO4_VOLTAGE: u8 = 0x95;
19const REG_DLDO1_VOLTAGE: u8 = 0x99;
20const REG_BATTERY_VOLTAGE_H: u8 = 0x34;
21const REG_BATTERY_VOLTAGE_L: u8 = 0x35;
22const REG_BATTERY_LEVEL: u8 = 0xA4;
23const STATUS1_BATTERY_PRESENT: u8 = 0x08;
24const STATUS1_VBUS_GOOD: u8 = 0x20;
25const LDO_3V3_CODE: u8 = 33 - 5;
26
27/// Battery charging state reported or inferred from the PMIC.
28#[cfg_attr(feature = "defmt", derive(defmt::Format))]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum ChargeState {
31    Unknown,
32    Discharging,
33    Charging,
34    Full,
35}
36
37/// External input power state.
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum ExternalPower {
41    Unknown,
42    Disconnected,
43    Connected,
44}
45
46/// High-level battery/power status for UI and application policy.
47#[cfg_attr(feature = "defmt", derive(defmt::Format))]
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub struct BatteryStatus {
50    pub millivolts: u16,
51    /// Battery percentage for existing UI code.
52    ///
53    /// This is the AXP2101 gauge value when [`state_of_charge`] is `Some(_)` and
54    /// a coarse voltage estimate otherwise.
55    pub percentage: u8,
56    pub charge_state: ChargeState,
57    pub external_power: ExternalPower,
58    pub low_battery: bool,
59    /// True when [`percentage`] is derived from voltage instead of the AXP2101
60    /// gauge register.
61    pub percentage_estimated: bool,
62    /// Preferred battery state-of-charge from AXP2101 register `0xA4`.
63    pub state_of_charge: Option<u8>,
64    /// Battery presence decoded from AXP2101 status register `0x00` bit `0x08`.
65    pub battery_present: Option<bool>,
66}
67
68impl BatteryStatus {
69    pub const fn new(millivolts: u16, charge_state: ChargeState) -> Self {
70        Self {
71            millivolts,
72            percentage: estimate_lipo_percentage(millivolts),
73            charge_state,
74            external_power: ExternalPower::Unknown,
75            low_battery: millivolts <= LowBatteryThreshold::DEFAULT.millivolts,
76            percentage_estimated: true,
77            state_of_charge: None,
78            battery_present: None,
79        }
80    }
81
82    pub const fn with_power(
83        millivolts: u16,
84        charge_state: ChargeState,
85        external_power: ExternalPower,
86        threshold: LowBatteryThreshold,
87    ) -> Self {
88        Self::with_power_and_soc(
89            millivolts,
90            charge_state,
91            external_power,
92            threshold,
93            None,
94            None,
95        )
96    }
97
98    pub const fn with_power_and_soc(
99        millivolts: u16,
100        charge_state: ChargeState,
101        external_power: ExternalPower,
102        threshold: LowBatteryThreshold,
103        state_of_charge: Option<u8>,
104        battery_present: Option<bool>,
105    ) -> Self {
106        let voltage_estimate = estimate_lipo_percentage(millivolts);
107        let percentage = match state_of_charge {
108            Some(value) => value,
109            None => voltage_estimate,
110        };
111        Self {
112            millivolts,
113            percentage,
114            charge_state,
115            external_power,
116            low_battery: millivolts <= threshold.millivolts,
117            percentage_estimated: state_of_charge.is_none(),
118            state_of_charge,
119            battery_present,
120        }
121    }
122}
123
124/// Low-battery threshold in millivolts.
125#[cfg_attr(feature = "defmt", derive(defmt::Format))]
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct LowBatteryThreshold {
128    pub millivolts: u16,
129}
130
131impl LowBatteryThreshold {
132    pub const DEFAULT: Self = Self { millivolts: 3_500 };
133}
134
135/// Integer exponential moving average for battery voltage readings.
136#[cfg_attr(feature = "defmt", derive(defmt::Format))]
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct VoltageSmoother {
139    value_mv: Option<u16>,
140    weight_new: u8,
141}
142
143impl VoltageSmoother {
144    pub const fn new(weight_new: u8) -> Self {
145        Self {
146            value_mv: None,
147            weight_new,
148        }
149    }
150
151    pub fn update(&mut self, sample_mv: u16) -> u16 {
152        let weight = self.weight_new.clamp(1, 100) as u32;
153        let next = match self.value_mv {
154            None => sample_mv,
155            Some(current) => {
156                let current = u32::from(current);
157                let sample = u32::from(sample_mv);
158                (((current * (100 - weight)) + (sample * weight)) / 100) as u16
159            }
160        };
161        self.value_mv = Some(next);
162        next
163    }
164
165    pub const fn value(&self) -> Option<u16> {
166        self.value_mv
167    }
168}
169
170/// Decode AXP2101 register `0xA4` as battery state-of-charge percentage.
171pub const fn decode_axp2101_soc(raw: u8) -> Option<u8> {
172    if raw <= 100 { Some(raw) } else { None }
173}
174
175/// Decode AXP2101 register `0x00` bit `0x20` as external VBUS state.
176pub const fn decode_axp2101_external_power(status0: u8) -> ExternalPower {
177    if status0 & STATUS1_VBUS_GOOD != 0 {
178        ExternalPower::Connected
179    } else {
180        ExternalPower::Disconnected
181    }
182}
183
184/// Decode AXP2101 register `0x00` bit `0x08` as battery presence.
185pub const fn decode_axp2101_battery_present(status0: u8) -> Option<bool> {
186    Some(status0 & STATUS1_BATTERY_PRESENT != 0)
187}
188
189/// Decode AXP2101 register `0x01` bits 5:6 as charging state.
190pub const fn decode_axp2101_charge_state(status0: u8, status1: u8) -> ChargeState {
191    match (status1 >> 5) & 0b11 {
192        0b01 => ChargeState::Charging,
193        0b10 => ChargeState::Discharging,
194        0b00 => match decode_axp2101_external_power(status0) {
195            ExternalPower::Connected => ChargeState::Full,
196            _ => ChargeState::Unknown,
197        },
198        _ => ChargeState::Unknown,
199    }
200}
201
202/// Voltage-only LiPo percentage estimate.
203///
204/// This is a coarse fallback for UI hints, not a precise state-of-charge value.
205pub const fn estimate_lipo_percentage(millivolts: u16) -> u8 {
206    match millivolts {
207        4200..=u16::MAX => 100,
208        4100..=4199 => 90,
209        4000..=4099 => 80,
210        3920..=3999 => 70,
211        3850..=3919 => 60,
212        3790..=3849 => 50,
213        3740..=3789 => 40,
214        3700..=3739 => 30,
215        3610..=3699 => 20,
216        3500..=3609 => 10,
217        3300..=3499 => 5,
218        _ => 0,
219    }
220}
221
222/// Generic blocking AXP2101 driver.
223pub struct Axp2101<I2C> {
224    i2c: I2C,
225    address: u8,
226    low_threshold: LowBatteryThreshold,
227}
228
229impl<I2C> Axp2101<I2C> {
230    pub const fn new(i2c: I2C) -> Self {
231        Self {
232            i2c,
233            address: devices::i2c::AXP2101_PMU,
234            low_threshold: LowBatteryThreshold::DEFAULT,
235        }
236    }
237
238    pub fn release(self) -> I2C {
239        self.i2c
240    }
241
242    pub fn set_low_battery_threshold(&mut self, threshold: LowBatteryThreshold) {
243        self.low_threshold = threshold;
244    }
245}
246
247impl<I2C, Error> Axp2101<I2C>
248where
249    I2C: I2c<Error = Error>,
250{
251    pub fn init_core_s3_defaults(&mut self) -> Result<(), Error> {
252        self.write_register(REG_LDOS_ON_OFF, 0xBF)?;
253        self.write_register(REG_ALDO3_VOLTAGE, LDO_3V3_CODE)?;
254        self.write_register(REG_ALDO4_VOLTAGE, LDO_3V3_CODE)
255    }
256
257    pub fn set_display_backlight(&mut self, brightness: u8) -> Result<(), Error> {
258        if brightness == 0 {
259            self.write_bit(REG_LDOS_ON_OFF, 7, false)
260        } else {
261            let voltage = ((u16::from(brightness) + 641) >> 5) as u8;
262            self.write_bit(REG_LDOS_ON_OFF, 7, true)?;
263            self.write_register(REG_DLDO1_VOLTAGE, voltage)
264        }
265    }
266
267    pub fn battery_voltage_mv(&mut self) -> Result<u16, Error> {
268        let high = u16::from(self.read_register(REG_BATTERY_VOLTAGE_H)?);
269        let low = u16::from(self.read_register(REG_BATTERY_VOLTAGE_L)?);
270        Ok(((high & 0x3F) << 8) | low)
271    }
272
273    /// Read AXP2101 register `0xA4` as the preferred CoreS3 battery SOC.
274    ///
275    /// This matches M5Unified's CoreS3 `getBatteryLevel()` path. Values outside
276    /// `0..=100` are treated as unavailable rather than clamped.
277    pub fn battery_level_percent(&mut self) -> Result<Option<u8>, Error> {
278        self.read_register(REG_BATTERY_LEVEL)
279            .map(decode_axp2101_soc)
280    }
281
282    /// Read AXP2101 external VBUS-good state from register `0x00` bit `0x20`.
283    pub fn external_power(&mut self) -> Result<ExternalPower, Error> {
284        self.read_register(REG_STATUS1)
285            .map(decode_axp2101_external_power)
286    }
287
288    /// Read AXP2101 battery-present state from register `0x00` bit `0x08`.
289    pub fn battery_present(&mut self) -> Result<Option<bool>, Error> {
290        self.read_register(REG_STATUS1)
291            .map(decode_axp2101_battery_present)
292    }
293
294    /// Read AXP2101 charge state from register `0x01` bits 5:6.
295    pub fn charge_state(&mut self) -> Result<ChargeState, Error> {
296        let status0 = self.read_register(REG_STATUS1)?;
297        let status1 = self.read_register(REG_STATUS2)?;
298        Ok(decode_axp2101_charge_state(status0, status1))
299    }
300
301    pub fn status(&mut self) -> Result<BatteryStatus, Error> {
302        let status0 = self.read_register(REG_STATUS1)?;
303        let status1 = self.read_register(REG_STATUS2)?;
304        let voltage = self.battery_voltage_mv().unwrap_or(0);
305        let state_of_charge = self.battery_level_percent().unwrap_or(None);
306        let external = decode_axp2101_external_power(status0);
307        let battery_present = decode_axp2101_battery_present(status0);
308        let charge_state = decode_axp2101_charge_state(status0, status1);
309        Ok(BatteryStatus::with_power_and_soc(
310            voltage,
311            charge_state,
312            external,
313            self.low_threshold,
314            state_of_charge,
315            battery_present,
316        ))
317    }
318
319    pub fn prepare_sleep(&mut self, wake_marker: u8) -> Result<(), Error> {
320        self.write_register(REG_DATA_BUFFER0, wake_marker)
321    }
322
323    pub fn shutdown(&mut self) -> Result<(), Error> {
324        self.write_register(REG_POWER_OFF, 0x01)
325    }
326
327    pub fn read_register(&mut self, register: u8) -> Result<u8, Error> {
328        let mut value = [0u8];
329        self.i2c.write_read(self.address, &[register], &mut value)?;
330        Ok(value[0])
331    }
332
333    pub fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error> {
334        self.i2c.write(self.address, &[register, value])
335    }
336
337    pub fn write_bit(&mut self, register: u8, bit: u8, value: bool) -> Result<(), Error> {
338        let current = self.read_register(register)?;
339        let mask = 1u8 << bit;
340        let next = if value {
341            current | mask
342        } else {
343            current & !mask
344        };
345        self.write_register(register, next)
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn decodes_axp2101_soc() {
355        assert_eq!(decode_axp2101_soc(0), Some(0));
356        assert_eq!(decode_axp2101_soc(1), Some(1));
357        assert_eq!(decode_axp2101_soc(50), Some(50));
358        assert_eq!(decode_axp2101_soc(90), Some(90));
359        assert_eq!(decode_axp2101_soc(100), Some(100));
360        assert_eq!(decode_axp2101_soc(101), None);
361        assert_eq!(decode_axp2101_soc(0x7F), None);
362        assert_eq!(decode_axp2101_soc(0xFF), None);
363    }
364
365    #[test]
366    fn decodes_axp2101_charge_state() {
367        assert_eq!(
368            decode_axp2101_charge_state(0, 0b01 << 5),
369            ChargeState::Charging
370        );
371        assert_eq!(
372            decode_axp2101_charge_state(0, 0b10 << 5),
373            ChargeState::Discharging
374        );
375        assert_eq!(
376            decode_axp2101_charge_state(STATUS1_VBUS_GOOD, 0),
377            ChargeState::Full
378        );
379        assert_eq!(decode_axp2101_charge_state(0, 0), ChargeState::Unknown);
380        assert_eq!(
381            decode_axp2101_charge_state(0, 0b11 << 5),
382            ChargeState::Unknown
383        );
384    }
385
386    #[test]
387    fn decodes_axp2101_external_power() {
388        assert_eq!(
389            decode_axp2101_external_power(STATUS1_VBUS_GOOD),
390            ExternalPower::Connected
391        );
392        assert_eq!(
393            decode_axp2101_external_power(0),
394            ExternalPower::Disconnected
395        );
396    }
397
398    #[test]
399    fn decodes_axp2101_battery_present() {
400        assert_eq!(
401            decode_axp2101_battery_present(STATUS1_BATTERY_PRESENT),
402            Some(true)
403        );
404        assert_eq!(decode_axp2101_battery_present(0), Some(false));
405    }
406
407    #[test]
408    fn estimates_voltage_percentage() {
409        assert_eq!(estimate_lipo_percentage(4200), 100);
410        assert_eq!(estimate_lipo_percentage(3750), 40);
411        assert_eq!(estimate_lipo_percentage(3400), 5);
412    }
413
414    #[test]
415    fn battery_status_prefers_gauge_soc() {
416        let status = BatteryStatus::with_power_and_soc(
417            3750,
418            ChargeState::Discharging,
419            ExternalPower::Disconnected,
420            LowBatteryThreshold::DEFAULT,
421            Some(87),
422            Some(true),
423        );
424        assert_eq!(status.percentage, 87);
425        assert!(!status.percentage_estimated);
426        assert_eq!(status.state_of_charge, Some(87));
427        assert_eq!(status.battery_present, Some(true));
428    }
429
430    #[test]
431    fn battery_status_falls_back_to_voltage_estimate() {
432        let status = BatteryStatus::with_power_and_soc(
433            3750,
434            ChargeState::Discharging,
435            ExternalPower::Disconnected,
436            LowBatteryThreshold::DEFAULT,
437            None,
438            Some(true),
439        );
440        assert_eq!(status.percentage, 40);
441        assert!(status.percentage_estimated);
442        assert_eq!(status.state_of_charge, None);
443    }
444
445    #[test]
446    fn smooths_voltage() {
447        let mut smoother = VoltageSmoother::new(25);
448        assert_eq!(smoother.update(4000), 4000);
449        assert_eq!(smoother.update(3800), 3950);
450    }
451}