m5stack-core 0.6.1

Board support crate for M5Stack Fire27 and CoreS3 (ESP32/ESP32-S3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// SPDX-License-Identifier: MIT OR Apache-2.0
//! M5Stack CoreS3 board-level bring-up sequences and pin wiring.
//!
//! # Internal I²C bus (`I2C_SYS`)
//!
//! One shared bus on **GPIO12 (SDA) / GPIO11 (SCL)** carries seven devices; the
//! BSP drives the first three, the rest are listed for completeness:
//!
//! | Device   | Addr | Role                        | BSP driver          |
//! |----------|------|-----------------------------|---------------------|
//! | AXP2101  | 0x34 | PMIC / power rails / battery | [`Axp2101Driver`]   |
//! | AW9523B  | 0x58 | IO expander (LCD/touch reset)| [`Aw9523bDriver`]   |
//! | FT6336U  | 0x38 | capacitive touch            | `driver::ft6336u`   |
//! | BMI270   | 0x69 | 6-axis IMU                  | —                   |
//! | BM8563   | 0x51 | RTC                         | —                   |
//! | ES7210   | 0x40 | audio ADC (mic)             | —                   |
//! | AW88298  | 0x36 | speaker amp                 | —                   |
//!
//! # AXP2101 power rails (from `Sch_M5_CoreS3_v1.0`)
//!
//! | AXP2101 out | Net           | Spec        | Powers                                  |
//! |-------------|---------------|-------------|-----------------------------------------|
//! | DCDC1       | `VDD_3V3`     | 3.3 V / 2 A | **Core VDD** — ESP32-S3, **AW9523B**, **FT6336U touch**, LCD logic |
//! | DCDC3       | `VCC_3V3`     | 3.3 V / 2 A | Peripherals VDD                         |
//! | DLDO1       | `VCC_BL`      | 3.3 V       | LCD **backlight** (see [`BACKLIGHT_MV`]) |
//! | ALDO1       | `VDD_1V8`     | 1.8 V/300 mA| AW88298 PA DVDD                         |
//! | ALDO2       | `VDDA_3V3`    | 3.3 V/300 mA| ES7210 codec VDDP                       |
//! | ALDO3       | `VDDCAM_3V3`  | 3.3 V/300 mA| camera / codec VDDA                     |
//! | ALDO4       | `VDD_3V3_SD`  | 3.3 V/300 mA| microSD card VDD                        |
//! | BLDO1       | `AVDD`        | 2.8 V/300 mA| sensor VDDA                             |
//! | BLDO2       | `DVDD`        | 1.2 V/300 mA| sensor VDD                              |
//!
//! **Reset gating (important):** the **AW9523B `RSTN` is wired to `AXP_PG`**
//! (AXP2101 power-good) and has an internal ~100 kΩ pull-*low* — so the expander
//! is held in reset until the AXP asserts power-good. It in turn drives
//! `LCD_RST` / `TOUCH_RST`, so panel + touch bring-up all chain off it. See
//! [`power_display_reset`] for the cold-boot readiness handling.

use embedded_hal::digital::{ErrorType, OutputPin};
use esp_hal::{
    Blocking,
    dma::{AnyGdmaChannel, DmaChannelConvert},
    gpio::AnyPin,
    i2c::master::{BusTimeout, Config as I2cConfig, I2c, SoftwareTimeout},
    interrupt::software::SoftwareInterruptControl,
    peripherals::{BT, PCNT, Peripherals, RMT, USB_DEVICE, WIFI},
    spi::master::AnySpi,
    time::Rate,
    timer::{AnyTimer, timg::TimerGroup},
};

use embassy_time::{Duration, Timer};

use crate::board::SystemResources;
use crate::board::spi2::Spi2Resources;
use crate::driver::aw9523b::{Aw9523bDriver, Aw9523bResources};
use crate::driver::axp2101::Axp2101Driver;
use crate::io::shared_i2c::SharedI2cBus;

/// AXP2101 PMIC I2C address on CoreS3.
const AXP2101_ADDR: u8 = 0x34;
/// Display backlight rail: AXP2101 DLDO1 @ 3.3 V.
const BACKLIGHT_MV: u16 = 3300;

/// AW9523B post-power-on I²C-ready time — the datasheet's "minimal wait time for
/// I2C communication is 5 ms".
const AW9523B_POR_MS: u64 = 5;
/// Deterministic settle before the first AW9523B access: 200% of the POR time.
/// On CoreS3 the expander's `RSTN` has an internal ~100 kΩ pull-*low* and is
/// wired to `AXP_PG` (AXP2101 power-good), so it is held in reset until the AXP
/// asserts power-good; at cold power-on `VDD_3V3` is already up (it powers the
/// running ESP32) but `AXP_PG` may lag, leaving the expander briefly in reset.
/// Waiting first — rather than poking the bus and retrying — avoids the premature
/// `AcknowledgeCheckFailed(Address)`, each of which fires one chip-wide I2C clock
/// reset.
const AW9523B_SETTLE_MS: u64 = 2 * AW9523B_POR_MS;
/// Fallback if the expander is still not ready after the settle. This should not
/// happen in normal operation, so each attempt logs a warning; bounded, with a
/// final failure logged as an error (best-effort — bring-up continues).
const AW9523B_FALLBACK_TRIES: u32 = 5;
const AW9523B_FALLBACK_DELAY_MS: u64 = 5;

/// Fault-injection predicate for the `test-fault-inject` feature: reports the
/// AW9523B "not ready" for the first `M5_TEST_AW_NACK` post-settle attempts (a
/// build-time env var), so the cold-boot fallback retry can be regression-tested
/// on the bench without a real power cycle. Absent from production builds.
#[cfg(feature = "test-fault-inject")]
fn aw_fault_inject_active(tries: u32) -> bool {
    match option_env!("M5_TEST_AW_NACK") {
        Some(s) => tries < s.parse::<u32>().unwrap_or(0),
        None => false,
    }
}

/// Power + display bring-up over the shared I2C bus: AW9523B IO-expander
/// (`LCD_RST` + touch reset) then AXP2101 PMIC backlight (DLDO1 @ 3.3 V).
///
/// A [`AW9523B_SETTLE_MS`] wait precedes the first AW9523B access so the expander
/// is out of reset (its `RSTN` follows `AXP_PG`) before we touch the bus — this
/// removes the intermittent cold-boot `AcknowledgeCheckFailed(Address)` that
/// otherwise fired one chip-wide I2C clock reset at every power-on. A bounded
/// warn-on-failure retry backs it up in case the settle was not enough.
///
/// Best-effort — each sub-step logs and continues on error, so a flaky chip
/// can't block the display/control loop from coming up. Caller-driven so the
/// caller owns the *when* and the *which core*: this MUST run on the core that
/// owns the I2C IRQ (APP core on the regulator — see its `irq-core-binding`
/// doc), and the caller signals display-readiness after it returns
/// (`LCD_RST` must precede display init).
pub async fn power_display_reset(i2c: &'static SharedI2cBus) {
    let mut aw = Aw9523bDriver::new(Aw9523bResources { i2c });
    // Deterministic settle for the cold-boot reset-release window (RSTN = AXP_PG)
    // before the first bus access — see AW9523B_SETTLE_MS.
    Timer::after(Duration::from_millis(AW9523B_SETTLE_MS)).await;
    let mut tries = 0u32;
    loop {
        // Test hook (feature `test-fault-inject`, absent in production builds):
        // simulate the AW9523B still being not-ready for the first
        // `M5_TEST_AW_NACK` post-settle attempts, so the fallback retry is
        // exercisable on the bench without a power cycle (which the rig can't do).
        #[cfg(feature = "test-fault-inject")]
        if aw_fault_inject_active(tries) {
            tries += 1;
            if tries > AW9523B_FALLBACK_TRIES {
                error!(
                    "AW9523B not ready (fault-inject) after {} retries",
                    AW9523B_FALLBACK_TRIES
                );
                break;
            }
            warn!(
                "[test-fault-inject] AW9523B not-ready simulated; retry {}/{}",
                tries, AW9523B_FALLBACK_TRIES
            );
            Timer::after(Duration::from_millis(AW9523B_FALLBACK_DELAY_MS)).await;
            continue;
        }
        match aw.init().await {
            Ok(()) => {
                if tries > 0 {
                    info!("AW9523B ready after settle + {} retries", tries);
                }
                break;
            }
            Err(e) => {
                tries += 1;
                if tries > AW9523B_FALLBACK_TRIES {
                    error!(
                        "AW9523B not ready after {} ms settle + {} retries: {:?}",
                        AW9523B_SETTLE_MS, AW9523B_FALLBACK_TRIES, e
                    );
                    break;
                }
                warn!(
                    "AW9523B not ready after settle; retry {}/{}: {:?}",
                    tries, AW9523B_FALLBACK_TRIES, e
                );
                Timer::after(Duration::from_millis(AW9523B_FALLBACK_DELAY_MS)).await;
            }
        }
    }
    if let Err(e) = aw.lcd_rst_pulse().await {
        error!("AW9523B LCD RST failed: {:?}", e);
    }
    if let Err(e) = aw.touch_rst_pulse().await {
        error!("AW9523B TOUCH RST failed: {:?}", e);
    }
    let mut axp = Axp2101Driver::new(i2c, AXP2101_ADDR);
    if let Err(e) = axp.set_dldo1(true, BACKLIGHT_MV).await {
        error!("AXP2101 backlight enable failed: {:?}", e);
    }
}

// --- GPIO35 MISO/DC mux -----------------------------------------------------

/// GPIO35 bit position in the GPIO1 (pins 32–) register bank.
const GPIO35_BIT: u32 = 1 << (35 - 32);

/// Zero-sized display-DC "pin" that drives GPIO35 via direct register writes.
///
/// On the CoreS3, GPIO35 is hardware-shared between SPI2 MISO (SD-card reads)
/// and display DC (command/data select). This type does NOT own the pin — the
/// SPI peripheral owns it for MISO input (`with_miso`), which also routes the
/// pad through the GPIO matrix. Each `set_low`/`set_high` re-enables the
/// output driver, so it composes with [`gpio35_disable_output`] (called after
/// every SD access window).
///
/// Two valid configurations for GPIO35 as DC:
/// 1. **No SD use** (simple display apps): configure GPIO35 as a plain
///    [`Output`](esp_hal::gpio::Output) instead — a bare register hack without
///    any pad routing leaves the pad unrouted and DC never toggles.
/// 2. **Display + SD shared** ([`super::spi2`]): SPI owns the pad as MISO and
///    DC writes go through this type.
///
/// Safety of the mux: the display and the SD card must share one SPI-bus
/// mutex within a single task, with no `.await` between a DC write and the
/// SPI transfer — otherwise an SD operation could preempt mid-command.
pub struct Gpio35Dc;

impl ErrorType for Gpio35Dc {
    type Error = core::convert::Infallible;
}

/// Pending DC level, applied by [`apply_pending_dc`] when the display actually
/// owns the bus.
static DC_LOW: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);

impl OutputPin for Gpio35Dc {
    // RECORD, do not drive.
    //
    // GPIO35 is MISO as well as DC. `SpiInterface` sets DC and *then* awaits the
    // SPI transfer, and that await now blocks on the shared-bus permit — so a
    // DC write that took effect immediately would hold GPIO35 as an output
    // while the SD card is mid-command, and every card response would read back
    // as 0x00. (Observed exactly so: "unexpected token 0x00",
    // "CrcMismatch(0, ..)", "UnknownDataResponse(0)".)
    //
    // The module docs used to require "no `.await` between a DC write and the
    // SPI transfer". The fair arbiter introduced one, so the requirement is
    // enforced here instead of being documented and hoped for: the level is
    // recorded now and applied in [`apply_pending_dc`] once the permit is held.
    fn set_low(&mut self) -> Result<(), Self::Error> {
        DC_LOW.store(true, core::sync::atomic::Ordering::Release);
        Ok(())
    }

    fn set_high(&mut self) -> Result<(), Self::Error> {
        DC_LOW.store(false, core::sync::atomic::Ordering::Release);
        Ok(())
    }
}

/// Drive GPIO35 to the DC level recorded by [`Gpio35Dc`].
///
/// Call ONLY while holding the SPI2 permit: it takes the pad away from MISO.
pub fn apply_pending_dc() {
    unsafe {
        let gpio = &*esp_hal::peripherals::GPIO::PTR;
        if DC_LOW.load(core::sync::atomic::Ordering::Acquire) {
            gpio.out1_w1tc().write(|w| w.bits(GPIO35_BIT));
        } else {
            gpio.out1_w1ts().write(|w| w.bits(GPIO35_BIT));
        }
        gpio.enable1_w1ts().write(|w| w.bits(GPIO35_BIT));
    }
}

/// Disable the GPIO35 output driver so the pad returns to high-impedance
/// input (SPI2 MISO). Must be called before any SD-card SPI operation — both
/// at bring-up (after display init; [`super::spi2`] does it) and per-op at
/// runtime (pass this as the block-device handler's pre-op hook).
pub fn gpio35_disable_output() {
    unsafe {
        let gpio = &*esp_hal::peripherals::GPIO::PTR;
        gpio.enable1_w1tc().write(|w| w.bits(GPIO35_BIT));
    }
}

// --- Pin wiring ---------------------------------------------------------------

/// External M5-Bus pins the BSP does not claim for on-board functions. What
/// hangs off them (1-Wire sensors, RPM pickups, ...) is application wiring.
/// Not exhaustive — extend as needed.
pub struct M5Bus<'a> {
    /// M5-Bus pin 20 (`BUS_PA_SCL`) → GPIO1.
    pub gpio1: AnyPin<'a>,
    /// M5-Bus pin 10 (`BUS_PB_OUT`) → GPIO9.
    pub gpio9: AnyPin<'a>,
    /// M5-Bus pin 2 (`BUS_ADC1`) → GPIO10.
    pub gpio10: AnyPin<'a>,
    /// M5-Bus pin 8 (`BUS_G5`) → GPIO5.
    pub gpio5: AnyPin<'a>,
    /// M5-Bus pin 21 (`BUS_G6`) → GPIO6.
    pub gpio6: AnyPin<'a>,
    /// M5-Bus pin 22 (`BUS_G7`) → GPIO7.
    pub gpio7: AnyPin<'a>,
    /// M5-Bus pin 13 (`BUS_U0RXD`) → GPIO44 — the ROM console's RX pin,
    /// unused on CoreS3 (console is USB-Serial-JTAG). Assign as an input:
    /// a ROM boot-log burst on the TX side (`gpio43`) can then never
    /// contend with a driven line here.
    pub gpio44: AnyPin<'a>,
    /// M5-Bus pin 14 (`BUS_U0TXD`) → GPIO43 — the ROM console's TX pin,
    /// unused on CoreS3. Assign as an output; see [`Self::gpio44`].
    pub gpio43: AnyPin<'a>,
}

/// The CoreS3's peripherals, split into board-fact groups. See
/// [`Board::split`].
pub struct Board {
    /// Display + SD-card shared SPI2 bus (see [`super::spi2`]).
    pub spi2: Spi2Resources<'static>,
    /// Internal I2C bus (SDA=GPIO12, SCL=GPIO11) shared by the AXP2101 PMIC
    /// (0x34), AW9523B expander (0x58) and FT6336U touch (0x38).
    ///
    /// Returned BLOCKING: defer `into_async()` to the core that runs the I2C
    /// tasks — `into_async()` binds the I2C peripheral IRQ to the calling
    /// core, and on a BLE app it must stay off the core running RWBLE's
    /// level-1 IRQ (cumulative IRQ blocking otherwise desyncs the BLE
    /// controller blob and wedges that core's executor).
    pub i2c0: I2c<'static, Blocking>,
    pub bt: BT<'static>,
    pub wifi: WIFI<'static>,
    /// USB-Serial-JTAG — console + serial-cmd
    /// ([`crate::io::serial_cmd::SerialCmdResources`]).
    pub usb_device: USB_DEVICE<'static>,
    /// RMT — e.g. the 1-Wire driver ([`crate::io::ow_temp::OnewireResources`]).
    pub rmt: RMT<'static>,
    /// PCNT — e.g. pulse counting ([`crate::io::rpm::RpmResources`]).
    pub pcnt: PCNT<'static>,
    /// Spare SPI3 unit — the BSP does not drive it, listed for the app to
    /// claim (same idea as [`Self::rmt`]/[`Self::pcnt`]). Wiring (which
    /// [`Self::m5bus`] pin is SCK/MOSI/MISO/CS) and the interrupt priority /
    /// core allocation are application knowledge. Attach the CS pin as the
    /// peripheral's **hardware** chip-select (`with_cs`), not a
    /// software-toggled [`esp_hal::gpio::Output`] — some designs depend on a
    /// sub-microsecond CS gap that software toggling cannot hold. See #27.
    pub spi3: AnySpi<'static>,
    /// GDMA channel for [`Self::spi3`] — any free channel works, this is just
    /// not the one `spi2` already owns (`DMA_CH0`).
    pub spi3_dma: AnyGdmaChannel<'static>,
    /// SK6812 LED bars data pin (M-Bus pin 23 → GPIO13 on CoreS3), driven over
    /// RMT ([`crate::driver::sk6812::Sk6812Driver`]). The M5GO bottom's 5 V LED
    /// rail is gated by the AW9523B — see `Aw9523bDriver::enable_bus_5v`.
    pub sk6812: AnyPin<'static>,
    /// External SPI PSRAM (~8 MB) — feed to `mem::psram_map` / `mem::psram_split`
    /// (sizing/usage is application policy). [feature `psram`]
    pub psram: esp_hal::peripherals::PSRAM<'static>,
    pub m5bus: M5Bus<'static>,
    pub system: SystemResources<'static>,
}

impl Board {
    /// Wire the CoreS3 pin map. Call once with the output of
    /// [`crate::board::init`] (or `esp_hal::init`); heap setup stays with the
    /// app (sizing is application policy).
    pub fn split(peripherals: Peripherals) -> Self {
        let spi2 = Spi2Resources {
            spi2: AnySpi::from(peripherals.SPI2),
            spi2_dma: peripherals.DMA_CH0.degrade(),
            sck: AnyPin::from(peripherals.GPIO36),
            mosi: AnyPin::from(peripherals.GPIO37),
            miso_dc: AnyPin::from(peripherals.GPIO35),
            display_cs: AnyPin::from(peripherals.GPIO3),
            card_cs: AnyPin::from(peripherals.GPIO4),
        };

        let i2c0 = I2c::new(
            peripherals.I2C0,
            I2cConfig::default()
                .with_frequency(Rate::from_khz(400))
                .with_timeout(BusTimeout::BusCycles(20))
                // Bound async transactions: without a software timeout the
                // deadline is None, so a stuck/NACKing transaction loops
                // forever in esp-hal's `check_all_commands_done` yield_now
                // spin. On an InterruptExecutor that spin pins the core,
                // masking the very completion IRQ that would end it — the
                // whole executor wedges (HIL-proven on S3, 2026-06-02). A
                // finite deadline makes a stuck op return Err instead.
                .with_software_timeout(SoftwareTimeout::Transaction(
                    esp_hal::time::Duration::from_millis(25),
                )),
        )
        .expect("I2C0 init failed")
        .with_sda(AnyPin::from(peripherals.GPIO12))
        .with_scl(AnyPin::from(peripherals.GPIO11));

        let tg0 = TimerGroup::new(peripherals.TIMG0);
        let tg1 = TimerGroup::new(peripherals.TIMG1);
        let system = SystemResources {
            sw_int: SoftwareInterruptControl::new(peripherals.SW_INTERRUPT),
            timer0_0: AnyTimer::from(tg0.timer0),
            timer0_1: AnyTimer::from(tg0.timer1),
            timer1_0: AnyTimer::from(tg1.timer0),
            timer1_1: AnyTimer::from(tg1.timer1),
            cpu_ctrl: peripherals.CPU_CTRL,
            lpwr: peripherals.LPWR,
        };

        Board {
            spi2,
            i2c0,
            bt: peripherals.BT,
            wifi: peripherals.WIFI,
            usb_device: peripherals.USB_DEVICE,
            rmt: peripherals.RMT,
            pcnt: peripherals.PCNT,
            spi3: AnySpi::from(peripherals.SPI3),
            spi3_dma: peripherals.DMA_CH1.degrade(),
            sk6812: AnyPin::from(peripherals.GPIO13),
            psram: peripherals.PSRAM,
            m5bus: M5Bus {
                gpio1: AnyPin::from(peripherals.GPIO1),
                gpio9: AnyPin::from(peripherals.GPIO9),
                gpio10: AnyPin::from(peripherals.GPIO10),
                gpio5: AnyPin::from(peripherals.GPIO5),
                gpio6: AnyPin::from(peripherals.GPIO6),
                gpio7: AnyPin::from(peripherals.GPIO7),
                gpio44: AnyPin::from(peripherals.GPIO44),
                gpio43: AnyPin::from(peripherals.GPIO43),
            },
            system,
        }
    }
}