Skip to main content

m5stack_core/driver/
aw9523b.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! AW9523B I2C GPIO expander driver for M5Stack CoreS3 (I2C 0x58).
3//!
4//! Register map (AW9523B English datasheet V1.5, §5 "Register Description"):
5//!   0x02  Output P0   output latch for port 0 (default 0x00)
6//!   0x03  Output P1   output latch for port 1 (default 0x00)
7//!   0x04  Config P0   direction: 0=output, 1=input (default 0xFF = all input)
8//!   0x05  Config P1   direction: 0=output, 1=input (default 0xFF = all input)
9//!   0x12  LEDMODE P0  0=LED current mode, 1=GPIO push-pull (default 0x00)
10//!   0x13  LEDMODE P1  0=LED current mode, 1=GPIO push-pull (default 0x00)
11//!
12//! Pin assignment (CoreS3 schematic v1.0 + M5Unified `Power_Class.cpp`):
13//!   P0_0  TOUCH_RST   active-LOW reset for FT6336U
14//!   P0_1  BUS_OUT_EN  active-HIGH — enables the M-Bus/Grove 5V output (load switch)
15//!   P0_2  AW_RST      self-reset (active-LOW)
16//!   P0_5  USB_OTG_EN  active-HIGH — enables USB OTG (kills USB-JTAG if asserted)
17//!   P1_0  CAM_RST     camera reset (active-LOW)
18//!   P1_1  LCD_RST     ILI9342C reset (active-LOW)
19//!   P1_2  TOUCH_INT   FT6336U interrupt (input)
20//!   P1_3  AW_INT      AW9523B interrupt output (active-LOW, open-drain)
21//!   P1_7  BOOST_EN    active-HIGH — enables the SY7088 5V boost converter
22//!
23//! NOTE: the BUS_OUT_EN polarity was verified active-HIGH against M5Unified
24//! (`setExtOutput(true)` *sets* the P0_1 bit) and the ME1502 load-switch topology
25//! — an earlier revision of this file wrongly documented it active-LOW.
26//!
27//! **SAFETY**: `init()` modifies P0 via read-modify-write touching ONLY P0_0
28//! (TOUCH_RST), preserving all other P0 bits at their power-on defaults so
29//! BUS_OUT_EN / USB_OTG_EN stay deasserted. The 5V output is brought up only by
30//! an explicit [`enable_bus_5v`](Aw9523bDriver::enable_bus_5v) call.
31//!
32//! Datasheet: <https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/products/core/CoreS3/AW9523B-EN.pdf>
33//! CoreS3 schematic: <https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/490/Sch_M5_CoreS3_v1.0.pdf>
34//! M5Unified: <https://github.com/m5stack/M5Unified/blob/master/src/utility/Power_Class.cpp>
35use embassy_time::{Duration, Timer};
36use thiserror_no_std::Error;
37
38use crate::io::shared_i2c::SharedI2cBus;
39
40pub const ADDR: u8 = 0x58;
41
42const REG_OUTPUT_P0: u8 = 0x02;
43const REG_OUTPUT_P1: u8 = 0x03;
44const REG_CONFIG_P0: u8 = 0x04;
45const REG_CONFIG_P1: u8 = 0x05;
46const REG_GCR: u8 = 0x11; // Global Control: bit4 = P0 push-pull (1) vs open-drain (0)
47const REG_LEDMODE_P0: u8 = 0x12;
48const REG_LEDMODE_P1: u8 = 0x13;
49
50const GCR_P0_PUSH_PULL: u8 = 0x10; // bit4
51
52// P0 pin masks
53pub const P0_TOUCH_RST: u8 = 0x01; // P0_0
54pub const P0_BUS_OUT_EN: u8 = 0x02; // P0_1 — active-HIGH: enables the M-Bus/Grove 5V output
55#[allow(dead_code)]
56pub const P0_AW_RST: u8 = 0x04; // P0_2
57
58// P1 pin masks
59pub const P1_CAM_RST: u8 = 0x01; // P1_0 (output)
60pub const P1_LCD_RST: u8 = 0x02; // P1_1 (output)
61pub const P1_BOOST_EN: u8 = 0x80; // P1_7 — active-HIGH: SY7088 5V boost converter
62#[allow(dead_code)]
63pub const P1_TOUCH_INT: u8 = 0x04; // P1_2 (input)
64#[allow(dead_code)]
65pub const P1_AW_INT: u8 = 0x08; // P1_3 (input)
66
67// P1: P1_0 (CAM_RST), P1_1 (LCD_RST) → outputs; rest inputs
68const CONFIG_P1_VAL: u8 = !(P1_CAM_RST | P1_LCD_RST);
69
70#[derive(Debug, Error)]
71pub enum Aw9523bError {
72    #[error("I2C error: {0:?}")]
73    I2cError(#[from] esp_hal::i2c::master::Error),
74}
75
76pub struct Aw9523bDriver {
77    i2c: &'static SharedI2cBus,
78    address: u8,
79}
80
81pub struct Aw9523bResources {
82    pub i2c: &'static SharedI2cBus,
83}
84
85impl Aw9523bDriver {
86    pub fn new(res: Aw9523bResources) -> Self {
87        Self {
88            i2c: res.i2c,
89            address: ADDR,
90        }
91    }
92
93    async fn read_reg(&mut self, reg: u8) -> Result<u8, Aw9523bError> {
94        let mut buf = [0u8];
95        self.i2c
96            .lock()
97            .await
98            .write_read_async(self.address, &[reg], &mut buf)
99            .await?;
100        Ok(buf[0])
101    }
102
103    async fn write_reg(&mut self, reg: u8, value: u8) -> Result<(), Aw9523bError> {
104        self.i2c
105            .lock()
106            .await
107            .write_async(self.address, &[reg, value])
108            .await?;
109        Ok(())
110    }
111
112    /// Read-modify-write: set bit(s) in register.
113    async fn set_bits(&mut self, reg: u8, mask: u8) -> Result<(), Aw9523bError> {
114        let val = self.read_reg(reg).await?;
115        self.write_reg(reg, val | mask).await
116    }
117
118    /// Read-modify-write: clear bit(s) in register.
119    async fn clear_bits(&mut self, reg: u8, mask: u8) -> Result<(), Aw9523bError> {
120        let val = self.read_reg(reg).await?;
121        self.write_reg(reg, val & !mask).await
122    }
123
124    /// Set LEDMODE to GPIO, configure P1 outputs, set up P0_0 (TOUCH_RST) via RMW.
125    pub async fn init(&mut self) -> Result<(), Aw9523bError> {
126        // P0: RMW — set TOUCH_RST latch HIGH before making it an output
127        self.set_bits(REG_OUTPUT_P0, P0_TOUCH_RST).await?;
128        self.set_bits(REG_LEDMODE_P0, P0_TOUCH_RST).await?; // GPIO mode for P0_0 only
129        self.clear_bits(REG_CONFIG_P0, P0_TOUCH_RST).await?; // P0_0 = output (0)
130        // P1: full writes (we own all P1 pins)
131        self.write_reg(REG_LEDMODE_P1, 0xFF).await?;
132        self.write_reg(REG_OUTPUT_P1, P1_CAM_RST | P1_LCD_RST)
133            .await?;
134        self.write_reg(REG_CONFIG_P1, CONFIG_P1_VAL).await?;
135        Ok(())
136    }
137
138    /// Pulse LCD_RST (P1_1) low for ≥10 µs, then wait 120 ms for ILI9342C stabilisation.
139    pub async fn lcd_rst_pulse(&mut self) -> Result<(), Aw9523bError> {
140        self.write_reg(REG_OUTPUT_P1, P1_CAM_RST).await?; // LCD_RST=0, CAM_RST=1
141        Timer::after(Duration::from_micros(10)).await;
142        self.write_reg(REG_OUTPUT_P1, P1_CAM_RST | P1_LCD_RST)
143            .await?;
144        Timer::after(Duration::from_millis(120)).await;
145        Ok(())
146    }
147
148    /// Enable the M-Bus / Grove **5 V output** that powers an attached M5GO
149    /// bottom's RGB LEDs on CoreS3 (their supply is the M-Bus 5 V rail, pin 28).
150    ///
151    /// Asserts both `BOOST_EN` (P1_7, the SY7088 boost) and `BUS_OUT_EN` (P0_1,
152    /// the load switch) **HIGH** — matching M5Unified's `setExtOutput(true)`.
153    ///
154    /// **CALLER GUARD:** M5Unified only enables this when a battery is present or
155    /// USB is absent — enabling the bus output with **no battery while on USB**
156    /// contends the shared VBUS rail. The caller must enforce that (see the
157    /// CoreS3 example). Drives the two pins as push-pull GPIO outputs, latched
158    /// high (RMW — other bits preserved).
159    pub async fn enable_bus_5v(&mut self) -> Result<(), Aw9523bError> {
160        // SY7088 boost (P1_7) high
161        self.set_bits(REG_LEDMODE_P1, P1_BOOST_EN).await?; // GPIO push-pull mode
162        self.set_bits(REG_OUTPUT_P1, P1_BOOST_EN).await?; // latch HIGH first
163        self.clear_bits(REG_CONFIG_P1, P1_BOOST_EN).await?; // then P1_7 = output
164        // Bus-output load switch (P0_1) high. Port 0 is OPEN-DRAIN by default and
165        // cannot drive high — put it in push-pull mode (GCR bit4) first, or P0_1
166        // latches high but floats and BUS_OUT_EN never asserts.
167        self.set_bits(REG_GCR, GCR_P0_PUSH_PULL).await?;
168        self.set_bits(REG_LEDMODE_P0, P0_BUS_OUT_EN).await?;
169        self.set_bits(REG_OUTPUT_P0, P0_BUS_OUT_EN).await?;
170        self.clear_bits(REG_CONFIG_P0, P0_BUS_OUT_EN).await?;
171        Ok(())
172    }
173
174    /// Pulse TOUCH_RST (P0_0) low for 5 ms, then wait 300 ms for FT6336U boot.
175    pub async fn touch_rst_pulse(&mut self) -> Result<(), Aw9523bError> {
176        self.clear_bits(REG_OUTPUT_P0, P0_TOUCH_RST).await?;
177        Timer::after(Duration::from_millis(5)).await;
178        self.set_bits(REG_OUTPUT_P0, P0_TOUCH_RST).await?;
179        Timer::after(Duration::from_millis(300)).await;
180        Ok(())
181    }
182}