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}