Skip to main content

m5stack_core/driver/
axp2101.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Minimal AXP2101 PMIC driver for M5Stack CoreS3 (I2C 0x34).
3//!
4//! Only DLDO1 (backlight), battery voltage ADC, and VBUS detection are used.
5//!
6//! Register map (verified against XPowersLib / CircuitPython_AXP2101):
7//!   0x00  Power status    bit 3 = VBUS present
8//!   0x34  VBAT ADC high   bits[5:0] = high 6 bits of 14-bit reading (1 mV/LSB)
9//!   0x35  VBAT ADC low    bits[7:0] = low 8 bits
10//!   0x90  LDO enable      bit 7 = DLDO1 enable
11//!   0x99  DLDO1 voltage   bits[4:0] = (mV − 500) / 100, range 500–3400 mV
12//!
13//! Datasheet: <https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/products/core/CoreS3/AXP2101_Datasheet_V1.1_en.pdf>
14//! Also: <https://github.com/lewisxhe/XPowersLib> (register reference)
15use thiserror_no_std::Error;
16
17use crate::io::shared_i2c::SharedI2cBus;
18
19const REG_PWR_STATUS: u8 = 0x00;
20const REG_ADC_EN: u8 = 0x30; // ADC channel enable (XPowersLib: ADC_CHANNEL_CTRL)
21const REG_LDO_EN: u8 = 0x90;
22const REG_DLDO1_VOL: u8 = 0x99;
23const REG_VBAT_H: u8 = 0x34;
24
25const DLDO1_VOL_MIN_MV: u16 = 500;
26const DLDO1_VOL_STEP_MV: u16 = 100;
27const DLDO1_EN_BIT: u8 = 0x80; // bit 7
28const VBUS_PRESENT_BIT: u8 = 0x08; // bit 3
29const VBAT_ADC_EN_BIT: u8 = 0x01; // REG 0x30 bit 0 = battery-voltage ADC
30
31#[derive(Debug, Error)]
32pub enum Axp2101Error {
33    #[error("I2C error: {0:?}")]
34    I2cError(#[from] esp_hal::i2c::master::Error),
35
36    #[error("Voltage out of range")]
37    VoltageOutOfRange,
38}
39
40pub struct Axp2101Driver {
41    i2c: &'static SharedI2cBus,
42    address: u8,
43}
44
45impl Axp2101Driver {
46    pub fn new(i2c: &'static SharedI2cBus, address: u8) -> Self {
47        Self { i2c, address }
48    }
49
50    async fn read_reg(&mut self, reg: u8) -> Result<u8, Axp2101Error> {
51        let mut buf = [0u8; 1];
52        self.i2c
53            .lock()
54            .await
55            .write_read_async(self.address, &[reg], &mut buf)
56            .await?;
57        debug!("AXP2101 rd 0x{:02x} = 0x{:02x}", reg, buf[0]);
58        Ok(buf[0])
59    }
60
61    async fn write_reg(&mut self, reg: u8, value: u8) -> Result<(), Axp2101Error> {
62        debug!("AXP2101 wr 0x{:02x} = 0x{:02x}", reg, value);
63        self.i2c
64            .lock()
65            .await
66            .write_async(self.address, &[reg, value])
67            .await?;
68        Ok(())
69    }
70
71    /// Enable or disable DLDO1 and set output voltage (mV). Range: 500–3400 mV, 100 mV steps.
72    pub async fn set_dldo1(&mut self, enabled: bool, mv: u16) -> Result<(), Axp2101Error> {
73        if mv < DLDO1_VOL_MIN_MV || mv > 3400 || (mv - DLDO1_VOL_MIN_MV) % DLDO1_VOL_STEP_MV != 0 {
74            return Err(Axp2101Error::VoltageOutOfRange);
75        }
76        let vol_val = ((mv - DLDO1_VOL_MIN_MV) / DLDO1_VOL_STEP_MV) as u8;
77        debug!(
78            "AXP2101 DLDO1 enabled={} {}mV (reg_val={})",
79            enabled, mv, vol_val
80        );
81        self.write_reg(REG_DLDO1_VOL, vol_val).await?;
82
83        let en_reg = self.read_reg(REG_LDO_EN).await?;
84        let en_val = if enabled {
85            en_reg | DLDO1_EN_BIT
86        } else {
87            en_reg & !DLDO1_EN_BIT
88        };
89        self.write_reg(REG_LDO_EN, en_val).await?;
90        Ok(())
91    }
92
93    /// Enable the battery-voltage ADC channel (REG 0x30 bit 0). Must be called
94    /// once before [`battery_voltage_mv`](Self::battery_voltage_mv) returns a
95    /// live reading — the ADC is off at reset, so the VBAT registers read 0.
96    pub async fn enable_battery_adc(&mut self) -> Result<(), Axp2101Error> {
97        let en = self.read_reg(REG_ADC_EN).await?;
98        self.write_reg(REG_ADC_EN, en | VBAT_ADC_EN_BIT).await?;
99        Ok(())
100    }
101
102    /// Read battery voltage in mV (14-bit ADC, 1 mV/LSB).
103    pub async fn battery_voltage_mv(&mut self) -> Result<u16, Axp2101Error> {
104        let hi = self.read_reg(REG_VBAT_H).await?;
105        let mut lo_buf = [0u8; 1];
106        self.i2c
107            .lock()
108            .await
109            .write_read_async(self.address, &[REG_VBAT_H + 1], &mut lo_buf)
110            .await?;
111        debug!("AXP2101 rd 0x{:02x} = 0x{:02x}", REG_VBAT_H + 1, lo_buf[0]);
112        let raw = ((hi as u16 & 0x3F) << 8) | lo_buf[0] as u16;
113        Ok(raw)
114    }
115
116    /// Returns true if VBUS (USB power) is present.
117    pub async fn vbus_present(&mut self) -> Result<bool, Axp2101Error> {
118        let status = self.read_reg(REG_PWR_STATUS).await?;
119        Ok(status & VBUS_PRESENT_BIT != 0)
120    }
121}