Skip to main content

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}