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}