Skip to main content

m5stack_core/driver/
ip5306.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! IP5306 power-management / battery-gauge driver (Fire27 & classic Core).
3//!
4//! The **I2C-enabled IP5306** at address `0x75` is the battery charger/gauge on
5//! the PMIC-less classic ESP32 cores: the M5Stack **Fire** carries one onboard,
6//! and the **M5GO Battery Bottom (A014)** carries one to give the Basic Core
7//! battery management. It sits on the M-Bus internal I2C bus (`SharedI2cBus`),
8//! which on the Fire is `G21/G22`. The chip exposes only a coarse 4-step fuel
9//! gauge plus charge / charge-done flags — no per-mV ADC.
10//!
11//! **Not used on CoreS3.** The CoreS3 has its own PMIC (AXP2101 @ `0x34`) that
12//! manages the battery — including the M5GO bottom's cell on the BAT pin — so
13//! read the battery there via [`crate::driver::axp2101`], not here. An A014
14//! bottom's IP5306 does not appear on the CoreS3 I2C scan.
15//!
16//! Register map (matches the M5Stack `Power`/UIFlow IP5306 usage):
17//!   0x70  READ0   bit 3 = charging in progress (CHARGE_ENABLE)
18//!   0x71  READ1   bit 3 = charge complete (battery full)
19//!   0x78  READ4   bits[7:4] = remaining-gauge code (see [`battery_level`])
20//!
21//! The `0x78` high nibble is the chip's LED-gauge encoding, **not** a linear
22//! percentage — it counts how many of the four gauge LEDs are *unlit*, so the
23//! value runs "backwards". [`Ip5306Driver::battery_level`] reproduces the exact
24//! mapping M5Stack uses (`0xF0→0%, 0xE0→25%, 0xC0→50%, 0x80→75%, 0x00→100%`).
25//!
26//! Refs: <https://docs.m5stack.com/en/core/fire> (IP5306 @ 0x75),
27//! <https://zenn.dev/tomorrow56/articles/43f64daa279510?locale=en> (register map)
28use thiserror_no_std::Error;
29
30use crate::io::shared_i2c::SharedI2cBus;
31
32/// IP5306 I2C address on the M-Bus (7-bit; the chip's raw 8-bit slave addr is 0xEA).
33pub const IP5306_ADDR: u8 = 0x75;
34
35const REG_READ0: u8 = 0x70; // charge status
36const REG_READ1: u8 = 0x71; // charge-full status
37const REG_READ4: u8 = 0x78; // battery gauge
38
39const CHARGE_BIT: u8 = 1 << 3; // READ0 bit 3
40const CHARGE_FULL_BIT: u8 = 1 << 3; // READ1 bit 3
41const GAUGE_MASK: u8 = 0xF0; // READ4 high nibble
42
43#[derive(Debug, Error)]
44pub enum Ip5306Error {
45    #[error("I2C error: {0:?}")]
46    I2cError(#[from] esp_hal::i2c::master::Error),
47}
48
49pub struct Ip5306Driver {
50    i2c: &'static SharedI2cBus,
51    address: u8,
52}
53
54impl Ip5306Driver {
55    /// Construct a driver on the shared bus. Use [`IP5306_ADDR`] for the address.
56    pub fn new(i2c: &'static SharedI2cBus, address: u8) -> Self {
57        Self { i2c, address }
58    }
59
60    async fn read_reg(&mut self, reg: u8) -> Result<u8, Ip5306Error> {
61        let mut buf = [0u8; 1];
62        self.i2c
63            .lock()
64            .await
65            .write_read_async(self.address, &[reg], &mut buf)
66            .await?;
67        debug!("IP5306 rd 0x{:02x} = 0x{:02x}", reg, buf[0]);
68        Ok(buf[0])
69    }
70
71    /// Probe whether an IP5306 actually answers at this address (the bottom may
72    /// not be attached). Returns `Ok(true)` if register 0x70 reads back.
73    pub async fn present(&mut self) -> bool {
74        self.read_reg(REG_READ0).await.is_ok()
75    }
76
77    /// Coarse battery level in percent: one of `0, 25, 50, 75, 100`.
78    ///
79    /// The IP5306 only reports a 4-LED gauge; the high nibble of register 0x78
80    /// is its "LEDs remaining off" code, mapped here exactly as M5Stack does.
81    /// Any unexpected nibble (e.g. `0xF0`) reads as `0`.
82    pub async fn battery_level(&mut self) -> Result<u8, Ip5306Error> {
83        let gauge = self.read_reg(REG_READ4).await? & GAUGE_MASK;
84        Ok(match gauge {
85            0xE0 => 25,
86            0xC0 => 50,
87            0x80 => 75,
88            0x00 => 100,
89            _ => 0, // 0xF0 and any other pattern
90        })
91    }
92
93    /// True while the battery is charging (USB/charge-base power present).
94    pub async fn is_charging(&mut self) -> Result<bool, Ip5306Error> {
95        Ok(self.read_reg(REG_READ0).await? & CHARGE_BIT != 0)
96    }
97
98    /// True once charging has completed (battery full).
99    pub async fn is_charge_full(&mut self) -> Result<bool, Ip5306Error> {
100        Ok(self.read_reg(REG_READ1).await? & CHARGE_FULL_BIT != 0)
101    }
102}