Skip to main content

core_s3/
power.rs

1//! Power and battery helpers for the CoreS3 AXP2101 PMIC.
2//!
3//! Battery percentage is estimated from voltage only. This is useful for UI hints
4//! but is not a precise state-of-charge measurement because LiPo voltage depends
5//! on load, age, temperature, and charge/discharge history.
6
7use embedded_hal::i2c::I2c;
8
9use crate::devices;
10
11const REG_STATUS1: u8 = 0x00;
12const REG_STATUS2: u8 = 0x01;
13const REG_DATA_BUFFER0: u8 = 0x04;
14const REG_POWER_OFF: u8 = 0x10;
15const REG_LDOS_ON_OFF: u8 = 0x90;
16const REG_ALDO3_VOLTAGE: u8 = 0x94;
17const REG_ALDO4_VOLTAGE: u8 = 0x95;
18const REG_DLDO1_VOLTAGE: u8 = 0x99;
19const REG_BATTERY_VOLTAGE_H: u8 = 0x34;
20const REG_BATTERY_VOLTAGE_L: u8 = 0x35;
21const LDO_3V3_CODE: u8 = 33 - 5;
22
23/// Battery charging state reported or inferred from the PMIC.
24#[cfg_attr(feature = "defmt", derive(defmt::Format))]
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum ChargeState {
27    Unknown,
28    Discharging,
29    Charging,
30    Full,
31}
32
33/// External input power state.
34#[cfg_attr(feature = "defmt", derive(defmt::Format))]
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ExternalPower {
37    Unknown,
38    Disconnected,
39    Connected,
40}
41
42/// High-level battery/power status for UI and application policy.
43#[cfg_attr(feature = "defmt", derive(defmt::Format))]
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct BatteryStatus {
46    pub millivolts: u16,
47    pub percentage: u8,
48    pub charge_state: ChargeState,
49    pub external_power: ExternalPower,
50    pub low_battery: bool,
51}
52
53impl BatteryStatus {
54    pub const fn new(millivolts: u16, charge_state: ChargeState) -> Self {
55        Self {
56            millivolts,
57            percentage: estimate_lipo_percentage(millivolts),
58            charge_state,
59            external_power: ExternalPower::Unknown,
60            low_battery: millivolts <= LowBatteryThreshold::DEFAULT.millivolts,
61        }
62    }
63
64    pub const fn with_power(
65        millivolts: u16,
66        charge_state: ChargeState,
67        external_power: ExternalPower,
68        threshold: LowBatteryThreshold,
69    ) -> Self {
70        Self {
71            millivolts,
72            percentage: estimate_lipo_percentage(millivolts),
73            charge_state,
74            external_power,
75            low_battery: millivolts <= threshold.millivolts,
76        }
77    }
78}
79
80/// Low-battery threshold in millivolts.
81#[cfg_attr(feature = "defmt", derive(defmt::Format))]
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct LowBatteryThreshold {
84    pub millivolts: u16,
85}
86
87impl LowBatteryThreshold {
88    pub const DEFAULT: Self = Self { millivolts: 3_500 };
89}
90
91/// Integer exponential moving average for battery voltage readings.
92#[cfg_attr(feature = "defmt", derive(defmt::Format))]
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct VoltageSmoother {
95    value_mv: Option<u16>,
96    weight_new: u8,
97}
98
99impl VoltageSmoother {
100    pub const fn new(weight_new: u8) -> Self {
101        Self {
102            value_mv: None,
103            weight_new,
104        }
105    }
106
107    pub fn update(&mut self, sample_mv: u16) -> u16 {
108        let weight = self.weight_new.clamp(1, 100) as u32;
109        let next = match self.value_mv {
110            None => sample_mv,
111            Some(current) => {
112                let current = u32::from(current);
113                let sample = u32::from(sample_mv);
114                (((current * (100 - weight)) + (sample * weight)) / 100) as u16
115            }
116        };
117        self.value_mv = Some(next);
118        next
119    }
120
121    pub const fn value(&self) -> Option<u16> {
122        self.value_mv
123    }
124}
125
126/// Voltage-only LiPo percentage estimate.
127pub const fn estimate_lipo_percentage(millivolts: u16) -> u8 {
128    match millivolts {
129        4200..=u16::MAX => 100,
130        4100..=4199 => 90,
131        4000..=4099 => 80,
132        3920..=3999 => 70,
133        3850..=3919 => 60,
134        3790..=3849 => 50,
135        3740..=3789 => 40,
136        3700..=3739 => 30,
137        3610..=3699 => 20,
138        3500..=3609 => 10,
139        3300..=3499 => 5,
140        _ => 0,
141    }
142}
143
144/// Generic blocking AXP2101 driver.
145pub struct Axp2101<I2C> {
146    i2c: I2C,
147    address: u8,
148    low_threshold: LowBatteryThreshold,
149}
150
151impl<I2C> Axp2101<I2C> {
152    pub const fn new(i2c: I2C) -> Self {
153        Self {
154            i2c,
155            address: devices::i2c::AXP2101_PMU,
156            low_threshold: LowBatteryThreshold::DEFAULT,
157        }
158    }
159
160    pub fn release(self) -> I2C {
161        self.i2c
162    }
163
164    pub fn set_low_battery_threshold(&mut self, threshold: LowBatteryThreshold) {
165        self.low_threshold = threshold;
166    }
167}
168
169impl<I2C, Error> Axp2101<I2C>
170where
171    I2C: I2c<Error = Error>,
172{
173    pub fn init_core_s3_defaults(&mut self) -> Result<(), Error> {
174        self.write_register(REG_LDOS_ON_OFF, 0xBF)?;
175        self.write_register(REG_ALDO3_VOLTAGE, LDO_3V3_CODE)?;
176        self.write_register(REG_ALDO4_VOLTAGE, LDO_3V3_CODE)
177    }
178
179    pub fn set_display_backlight(&mut self, brightness: u8) -> Result<(), Error> {
180        if brightness == 0 {
181            self.write_bit(REG_LDOS_ON_OFF, 7, false)
182        } else {
183            let voltage = ((u16::from(brightness) + 641) >> 5) as u8;
184            self.write_bit(REG_LDOS_ON_OFF, 7, true)?;
185            self.write_register(REG_DLDO1_VOLTAGE, voltage)
186        }
187    }
188
189    pub fn battery_voltage_mv(&mut self) -> Result<u16, Error> {
190        let high = u16::from(self.read_register(REG_BATTERY_VOLTAGE_H)?);
191        let low = u16::from(self.read_register(REG_BATTERY_VOLTAGE_L)?);
192        Ok(((high & 0x3F) << 8) | low)
193    }
194
195    pub fn status(&mut self) -> Result<BatteryStatus, Error> {
196        let status1 = self.read_register(REG_STATUS1)?;
197        let status2 = self.read_register(REG_STATUS2)?;
198        let voltage = self.battery_voltage_mv().unwrap_or(0);
199        let external = if status1 & 0x20 != 0 {
200            ExternalPower::Connected
201        } else {
202            ExternalPower::Disconnected
203        };
204        let charge_state = if status2 & 0x04 != 0 {
205            ChargeState::Charging
206        } else if external == ExternalPower::Connected {
207            ChargeState::Full
208        } else {
209            ChargeState::Discharging
210        };
211        Ok(BatteryStatus::with_power(
212            voltage,
213            charge_state,
214            external,
215            self.low_threshold,
216        ))
217    }
218
219    pub fn prepare_sleep(&mut self, wake_marker: u8) -> Result<(), Error> {
220        self.write_register(REG_DATA_BUFFER0, wake_marker)
221    }
222
223    pub fn shutdown(&mut self) -> Result<(), Error> {
224        self.write_register(REG_POWER_OFF, 0x01)
225    }
226
227    pub fn read_register(&mut self, register: u8) -> Result<u8, Error> {
228        let mut value = [0u8];
229        self.i2c.write_read(self.address, &[register], &mut value)?;
230        Ok(value[0])
231    }
232
233    pub fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error> {
234        self.i2c.write(self.address, &[register, value])
235    }
236
237    pub fn write_bit(&mut self, register: u8, bit: u8, value: bool) -> Result<(), Error> {
238        let current = self.read_register(register)?;
239        let mask = 1u8 << bit;
240        let next = if value {
241            current | mask
242        } else {
243            current & !mask
244        };
245        self.write_register(register, next)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn estimates_voltage_percentage() {
255        assert_eq!(estimate_lipo_percentage(4200), 100);
256        assert_eq!(estimate_lipo_percentage(3750), 40);
257        assert_eq!(estimate_lipo_percentage(3400), 5);
258    }
259
260    #[test]
261    fn smooths_voltage() {
262        let mut smoother = VoltageSmoother::new(25);
263        assert_eq!(smoother.update(4000), 4000);
264        assert_eq!(smoother.update(3800), 3950);
265    }
266}