m5stack_core/board/cores3.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! M5Stack CoreS3 board-level bring-up sequences and pin wiring.
3//!
4//! # Internal I²C bus (`I2C_SYS`)
5//!
6//! One shared bus on **GPIO12 (SDA) / GPIO11 (SCL)** carries seven devices; the
7//! BSP drives the first three, the rest are listed for completeness:
8//!
9//! | Device | Addr | Role | BSP driver |
10//! |----------|------|-----------------------------|---------------------|
11//! | AXP2101 | 0x34 | PMIC / power rails / battery | [`Axp2101Driver`] |
12//! | AW9523B | 0x58 | IO expander (LCD/touch reset)| [`Aw9523bDriver`] |
13//! | FT6336U | 0x38 | capacitive touch | `driver::ft6336u` |
14//! | BMI270 | 0x69 | 6-axis IMU | — |
15//! | BM8563 | 0x51 | RTC | — |
16//! | ES7210 | 0x40 | audio ADC (mic) | — |
17//! | AW88298 | 0x36 | speaker amp | — |
18//!
19//! # AXP2101 power rails (from `Sch_M5_CoreS3_v1.0`)
20//!
21//! | AXP2101 out | Net | Spec | Powers |
22//! |-------------|---------------|-------------|-----------------------------------------|
23//! | DCDC1 | `VDD_3V3` | 3.3 V / 2 A | **Core VDD** — ESP32-S3, **AW9523B**, **FT6336U touch**, LCD logic |
24//! | DCDC3 | `VCC_3V3` | 3.3 V / 2 A | Peripherals VDD |
25//! | DLDO1 | `VCC_BL` | 3.3 V | LCD **backlight** (see [`BACKLIGHT_MV`]) |
26//! | ALDO1 | `VDD_1V8` | 1.8 V/300 mA| AW88298 PA DVDD |
27//! | ALDO2 | `VDDA_3V3` | 3.3 V/300 mA| ES7210 codec VDDP |
28//! | ALDO3 | `VDDCAM_3V3` | 3.3 V/300 mA| camera / codec VDDA |
29//! | ALDO4 | `VDD_3V3_SD` | 3.3 V/300 mA| microSD card VDD |
30//! | BLDO1 | `AVDD` | 2.8 V/300 mA| sensor VDDA |
31//! | BLDO2 | `DVDD` | 1.2 V/300 mA| sensor VDD |
32//!
33//! **Reset gating (important):** the **AW9523B `RSTN` is wired to `AXP_PG`**
34//! (AXP2101 power-good) and has an internal ~100 kΩ pull-*low* — so the expander
35//! is held in reset until the AXP asserts power-good. It in turn drives
36//! `LCD_RST` / `TOUCH_RST`, so panel + touch bring-up all chain off it. See
37//! [`power_display_reset`] for the cold-boot readiness handling.
38
39use embedded_hal::digital::{ErrorType, OutputPin};
40use esp_hal::{
41 Blocking,
42 dma::{AnyGdmaChannel, DmaChannelConvert},
43 gpio::AnyPin,
44 i2c::master::{BusTimeout, Config as I2cConfig, I2c, SoftwareTimeout},
45 interrupt::software::SoftwareInterruptControl,
46 peripherals::{BT, PCNT, Peripherals, RMT, USB_DEVICE, WIFI},
47 spi::master::AnySpi,
48 time::Rate,
49 timer::{AnyTimer, timg::TimerGroup},
50};
51
52use embassy_time::{Duration, Timer};
53
54use crate::board::SystemResources;
55use crate::board::spi2::Spi2Resources;
56use crate::driver::aw9523b::{Aw9523bDriver, Aw9523bResources};
57use crate::driver::axp2101::Axp2101Driver;
58use crate::io::shared_i2c::SharedI2cBus;
59
60/// AXP2101 PMIC I2C address on CoreS3.
61const AXP2101_ADDR: u8 = 0x34;
62/// Display backlight rail: AXP2101 DLDO1 @ 3.3 V.
63const BACKLIGHT_MV: u16 = 3300;
64
65/// AW9523B post-power-on I²C-ready time — the datasheet's "minimal wait time for
66/// I2C communication is 5 ms".
67const AW9523B_POR_MS: u64 = 5;
68/// Deterministic settle before the first AW9523B access: 200% of the POR time.
69/// On CoreS3 the expander's `RSTN` has an internal ~100 kΩ pull-*low* and is
70/// wired to `AXP_PG` (AXP2101 power-good), so it is held in reset until the AXP
71/// asserts power-good; at cold power-on `VDD_3V3` is already up (it powers the
72/// running ESP32) but `AXP_PG` may lag, leaving the expander briefly in reset.
73/// Waiting first — rather than poking the bus and retrying — avoids the premature
74/// `AcknowledgeCheckFailed(Address)`, each of which fires one chip-wide I2C clock
75/// reset.
76const AW9523B_SETTLE_MS: u64 = 2 * AW9523B_POR_MS;
77/// Fallback if the expander is still not ready after the settle. This should not
78/// happen in normal operation, so each attempt logs a warning; bounded, with a
79/// final failure logged as an error (best-effort — bring-up continues).
80const AW9523B_FALLBACK_TRIES: u32 = 5;
81const AW9523B_FALLBACK_DELAY_MS: u64 = 5;
82
83/// Fault-injection predicate for the `test-fault-inject` feature: reports the
84/// AW9523B "not ready" for the first `M5_TEST_AW_NACK` post-settle attempts (a
85/// build-time env var), so the cold-boot fallback retry can be regression-tested
86/// on the bench without a real power cycle. Absent from production builds.
87#[cfg(feature = "test-fault-inject")]
88fn aw_fault_inject_active(tries: u32) -> bool {
89 match option_env!("M5_TEST_AW_NACK") {
90 Some(s) => tries < s.parse::<u32>().unwrap_or(0),
91 None => false,
92 }
93}
94
95/// Power + display bring-up over the shared I2C bus: AW9523B IO-expander
96/// (`LCD_RST` + touch reset) then AXP2101 PMIC backlight (DLDO1 @ 3.3 V).
97///
98/// A [`AW9523B_SETTLE_MS`] wait precedes the first AW9523B access so the expander
99/// is out of reset (its `RSTN` follows `AXP_PG`) before we touch the bus — this
100/// removes the intermittent cold-boot `AcknowledgeCheckFailed(Address)` that
101/// otherwise fired one chip-wide I2C clock reset at every power-on. A bounded
102/// warn-on-failure retry backs it up in case the settle was not enough.
103///
104/// Best-effort — each sub-step logs and continues on error, so a flaky chip
105/// can't block the display/control loop from coming up. Caller-driven so the
106/// caller owns the *when* and the *which core*: this MUST run on the core that
107/// owns the I2C IRQ (APP core on the regulator — see its `irq-core-binding`
108/// doc), and the caller signals display-readiness after it returns
109/// (`LCD_RST` must precede display init).
110pub async fn power_display_reset(i2c: &'static SharedI2cBus) {
111 let mut aw = Aw9523bDriver::new(Aw9523bResources { i2c });
112 // Deterministic settle for the cold-boot reset-release window (RSTN = AXP_PG)
113 // before the first bus access — see AW9523B_SETTLE_MS.
114 Timer::after(Duration::from_millis(AW9523B_SETTLE_MS)).await;
115 let mut tries = 0u32;
116 loop {
117 // Test hook (feature `test-fault-inject`, absent in production builds):
118 // simulate the AW9523B still being not-ready for the first
119 // `M5_TEST_AW_NACK` post-settle attempts, so the fallback retry is
120 // exercisable on the bench without a power cycle (which the rig can't do).
121 #[cfg(feature = "test-fault-inject")]
122 if aw_fault_inject_active(tries) {
123 tries += 1;
124 if tries > AW9523B_FALLBACK_TRIES {
125 error!("AW9523B not ready (fault-inject) after {} retries", AW9523B_FALLBACK_TRIES);
126 break;
127 }
128 warn!(
129 "[test-fault-inject] AW9523B not-ready simulated; retry {}/{}",
130 tries, AW9523B_FALLBACK_TRIES
131 );
132 Timer::after(Duration::from_millis(AW9523B_FALLBACK_DELAY_MS)).await;
133 continue;
134 }
135 match aw.init().await {
136 Ok(()) => {
137 if tries > 0 {
138 info!("AW9523B ready after settle + {} retries", tries);
139 }
140 break;
141 }
142 Err(e) => {
143 tries += 1;
144 if tries > AW9523B_FALLBACK_TRIES {
145 error!(
146 "AW9523B not ready after {} ms settle + {} retries: {:?}",
147 AW9523B_SETTLE_MS, AW9523B_FALLBACK_TRIES, e
148 );
149 break;
150 }
151 warn!(
152 "AW9523B not ready after settle; retry {}/{}: {:?}",
153 tries, AW9523B_FALLBACK_TRIES, e
154 );
155 Timer::after(Duration::from_millis(AW9523B_FALLBACK_DELAY_MS)).await;
156 }
157 }
158 }
159 if let Err(e) = aw.lcd_rst_pulse().await {
160 error!("AW9523B LCD RST failed: {:?}", e);
161 }
162 if let Err(e) = aw.touch_rst_pulse().await {
163 error!("AW9523B TOUCH RST failed: {:?}", e);
164 }
165 let mut axp = Axp2101Driver::new(i2c, AXP2101_ADDR);
166 if let Err(e) = axp.set_dldo1(true, BACKLIGHT_MV).await {
167 error!("AXP2101 backlight enable failed: {:?}", e);
168 }
169}
170
171// --- GPIO35 MISO/DC mux -----------------------------------------------------
172
173/// GPIO35 bit position in the GPIO1 (pins 32–) register bank.
174const GPIO35_BIT: u32 = 1 << (35 - 32);
175
176/// Zero-sized display-DC "pin" that drives GPIO35 via direct register writes.
177///
178/// On the CoreS3, GPIO35 is hardware-shared between SPI2 MISO (SD-card reads)
179/// and display DC (command/data select). This type does NOT own the pin — the
180/// SPI peripheral owns it for MISO input (`with_miso`), which also routes the
181/// pad through the GPIO matrix. Each `set_low`/`set_high` re-enables the
182/// output driver, so it composes with [`gpio35_disable_output`] (called after
183/// every SD access window).
184///
185/// Two valid configurations for GPIO35 as DC:
186/// 1. **No SD use** (simple display apps): configure GPIO35 as a plain
187/// [`Output`](esp_hal::gpio::Output) instead — a bare register hack without
188/// any pad routing leaves the pad unrouted and DC never toggles.
189/// 2. **Display + SD shared** ([`super::spi2`]): SPI owns the pad as MISO and
190/// DC writes go through this type.
191///
192/// Safety of the mux: the display and the SD card must share one SPI-bus
193/// mutex within a single task, with no `.await` between a DC write and the
194/// SPI transfer — otherwise an SD operation could preempt mid-command.
195pub struct Gpio35Dc;
196
197impl ErrorType for Gpio35Dc {
198 type Error = core::convert::Infallible;
199}
200
201impl OutputPin for Gpio35Dc {
202 fn set_low(&mut self) -> Result<(), Self::Error> {
203 unsafe {
204 let gpio = &*esp_hal::peripherals::GPIO::PTR;
205 gpio.out1_w1tc().write(|w| w.bits(GPIO35_BIT));
206 gpio.enable1_w1ts().write(|w| w.bits(GPIO35_BIT));
207 }
208 Ok(())
209 }
210
211 fn set_high(&mut self) -> Result<(), Self::Error> {
212 unsafe {
213 let gpio = &*esp_hal::peripherals::GPIO::PTR;
214 gpio.out1_w1ts().write(|w| w.bits(GPIO35_BIT));
215 gpio.enable1_w1ts().write(|w| w.bits(GPIO35_BIT));
216 }
217 Ok(())
218 }
219}
220
221/// Disable the GPIO35 output driver so the pad returns to high-impedance
222/// input (SPI2 MISO). Must be called before any SD-card SPI operation — both
223/// at bring-up (after display init; [`super::spi2`] does it) and per-op at
224/// runtime (pass this as the block-device handler's pre-op hook).
225pub fn gpio35_disable_output() {
226 unsafe {
227 let gpio = &*esp_hal::peripherals::GPIO::PTR;
228 gpio.enable1_w1tc().write(|w| w.bits(GPIO35_BIT));
229 }
230}
231
232// --- Pin wiring ---------------------------------------------------------------
233
234/// External M5-Bus pins the BSP does not claim for on-board functions. What
235/// hangs off them (1-Wire sensors, RPM pickups, ...) is application wiring.
236/// Not exhaustive — extend as needed.
237pub struct M5Bus<'a> {
238 /// M5-Bus pin 20 (`BUS_PA_SCL`) → GPIO1.
239 pub gpio1: AnyPin<'a>,
240 /// M5-Bus pin 10 (`BUS_PB_OUT`) → GPIO9.
241 pub gpio9: AnyPin<'a>,
242 /// M5-Bus pin 2 (`BUS_ADC1`) → GPIO10.
243 pub gpio10: AnyPin<'a>,
244 /// M5-Bus pin 8 (`BUS_G5`) → GPIO5.
245 pub gpio5: AnyPin<'a>,
246 /// M5-Bus pin 21 (`BUS_G6`) → GPIO6.
247 pub gpio6: AnyPin<'a>,
248 /// M5-Bus pin 22 (`BUS_G7`) → GPIO7.
249 pub gpio7: AnyPin<'a>,
250 /// M5-Bus pin 13 (`BUS_U0RXD`) → GPIO44 — the ROM console's RX pin,
251 /// unused on CoreS3 (console is USB-Serial-JTAG). Assign as an input:
252 /// a ROM boot-log burst on the TX side (`gpio43`) can then never
253 /// contend with a driven line here.
254 pub gpio44: AnyPin<'a>,
255 /// M5-Bus pin 14 (`BUS_U0TXD`) → GPIO43 — the ROM console's TX pin,
256 /// unused on CoreS3. Assign as an output; see [`Self::gpio44`].
257 pub gpio43: AnyPin<'a>,
258}
259
260/// The CoreS3's peripherals, split into board-fact groups. See
261/// [`Board::split`].
262pub struct Board {
263 /// Display + SD-card shared SPI2 bus (see [`super::spi2`]).
264 pub spi2: Spi2Resources<'static>,
265 /// Internal I2C bus (SDA=GPIO12, SCL=GPIO11) shared by the AXP2101 PMIC
266 /// (0x34), AW9523B expander (0x58) and FT6336U touch (0x38).
267 ///
268 /// Returned BLOCKING: defer `into_async()` to the core that runs the I2C
269 /// tasks — `into_async()` binds the I2C peripheral IRQ to the calling
270 /// core, and on a BLE app it must stay off the core running RWBLE's
271 /// level-1 IRQ (cumulative IRQ blocking otherwise desyncs the BLE
272 /// controller blob and wedges that core's executor).
273 pub i2c0: I2c<'static, Blocking>,
274 pub bt: BT<'static>,
275 pub wifi: WIFI<'static>,
276 /// USB-Serial-JTAG — console + serial-cmd
277 /// ([`crate::io::serial_cmd::SerialCmdResources`]).
278 pub usb_device: USB_DEVICE<'static>,
279 /// RMT — e.g. the 1-Wire driver ([`crate::io::ow_temp::OnewireResources`]).
280 pub rmt: RMT<'static>,
281 /// PCNT — e.g. pulse counting ([`crate::io::rpm::RpmResources`]).
282 pub pcnt: PCNT<'static>,
283 /// Spare SPI3 unit — the BSP does not drive it, listed for the app to
284 /// claim (same idea as [`Self::rmt`]/[`Self::pcnt`]). Wiring (which
285 /// [`Self::m5bus`] pin is SCK/MOSI/MISO/CS) and the interrupt priority /
286 /// core allocation are application knowledge. Attach the CS pin as the
287 /// peripheral's **hardware** chip-select (`with_cs`), not a
288 /// software-toggled [`esp_hal::gpio::Output`] — some designs depend on a
289 /// sub-microsecond CS gap that software toggling cannot hold. See #27.
290 pub spi3: AnySpi<'static>,
291 /// GDMA channel for [`Self::spi3`] — any free channel works, this is just
292 /// not the one `spi2` already owns (`DMA_CH0`).
293 pub spi3_dma: AnyGdmaChannel<'static>,
294 /// SK6812 LED bars data pin (M-Bus pin 23 → GPIO13 on CoreS3), driven over
295 /// RMT ([`crate::driver::sk6812::Sk6812Driver`]). The M5GO bottom's 5 V LED
296 /// rail is gated by the AW9523B — see `Aw9523bDriver::enable_bus_5v`.
297 pub sk6812: AnyPin<'static>,
298 /// External SPI PSRAM (~8 MB) — feed to `mem::psram_map` / `mem::psram_split`
299 /// (sizing/usage is application policy). [feature `psram`]
300 pub psram: esp_hal::peripherals::PSRAM<'static>,
301 pub m5bus: M5Bus<'static>,
302 pub system: SystemResources<'static>,
303}
304
305impl Board {
306 /// Wire the CoreS3 pin map. Call once with the output of
307 /// [`crate::board::init`] (or `esp_hal::init`); heap setup stays with the
308 /// app (sizing is application policy).
309 pub fn split(peripherals: Peripherals) -> Self {
310 let spi2 = Spi2Resources {
311 spi2: AnySpi::from(peripherals.SPI2),
312 spi2_dma: peripherals.DMA_CH0.degrade(),
313 sck: AnyPin::from(peripherals.GPIO36),
314 mosi: AnyPin::from(peripherals.GPIO37),
315 miso_dc: AnyPin::from(peripherals.GPIO35),
316 display_cs: AnyPin::from(peripherals.GPIO3),
317 card_cs: AnyPin::from(peripherals.GPIO4),
318 };
319
320 let i2c0 = I2c::new(
321 peripherals.I2C0,
322 I2cConfig::default()
323 .with_frequency(Rate::from_khz(400))
324 .with_timeout(BusTimeout::BusCycles(20))
325 // Bound async transactions: without a software timeout the
326 // deadline is None, so a stuck/NACKing transaction loops
327 // forever in esp-hal's `check_all_commands_done` yield_now
328 // spin. On an InterruptExecutor that spin pins the core,
329 // masking the very completion IRQ that would end it — the
330 // whole executor wedges (HIL-proven on S3, 2026-06-02). A
331 // finite deadline makes a stuck op return Err instead.
332 .with_software_timeout(SoftwareTimeout::Transaction(
333 esp_hal::time::Duration::from_millis(25),
334 )),
335 )
336 .expect("I2C0 init failed")
337 .with_sda(AnyPin::from(peripherals.GPIO12))
338 .with_scl(AnyPin::from(peripherals.GPIO11));
339
340 let tg0 = TimerGroup::new(peripherals.TIMG0);
341 let tg1 = TimerGroup::new(peripherals.TIMG1);
342 let system = SystemResources {
343 sw_int: SoftwareInterruptControl::new(peripherals.SW_INTERRUPT),
344 timer0_0: AnyTimer::from(tg0.timer0),
345 timer0_1: AnyTimer::from(tg0.timer1),
346 timer1_0: AnyTimer::from(tg1.timer0),
347 timer1_1: AnyTimer::from(tg1.timer1),
348 cpu_ctrl: peripherals.CPU_CTRL,
349 lpwr: peripherals.LPWR,
350 };
351
352 Board {
353 spi2,
354 i2c0,
355 bt: peripherals.BT,
356 wifi: peripherals.WIFI,
357 usb_device: peripherals.USB_DEVICE,
358 rmt: peripherals.RMT,
359 pcnt: peripherals.PCNT,
360 spi3: AnySpi::from(peripherals.SPI3),
361 spi3_dma: peripherals.DMA_CH1.degrade(),
362 sk6812: AnyPin::from(peripherals.GPIO13),
363 psram: peripherals.PSRAM,
364 m5bus: M5Bus {
365 gpio1: AnyPin::from(peripherals.GPIO1),
366 gpio9: AnyPin::from(peripherals.GPIO9),
367 gpio10: AnyPin::from(peripherals.GPIO10),
368 gpio5: AnyPin::from(peripherals.GPIO5),
369 gpio6: AnyPin::from(peripherals.GPIO6),
370 gpio7: AnyPin::from(peripherals.GPIO7),
371 gpio44: AnyPin::from(peripherals.GPIO44),
372 gpio43: AnyPin::from(peripherals.GPIO43),
373 },
374 system,
375 }
376 }
377}