Skip to main content

m5stack_core/board/
spi2.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Shared SPI2 bus: on-board ILI9342C display + SD-card slot.
3//!
4//! Both boards route the display and the SD slot over the same SPI2 bus, so
5//! their bring-up is coupled and ordering-sensitive. This module owns that
6//! hardware knowledge; the SD-card *driver* stays with the application (the
7//! `sdspi` crate is not yet on crates.io) and connects through the generic
8//! chip-select + `SpiDevice` returned by [`Spi2Parts::finish`].
9//!
10//! # Bring-up sequence (application side)
11//!
12//! The recommended path is [`Spi2Parts::finish_sd`]: the BSP owns the ≥74-clock
13//! SD power-up idle and hands back a pre-initialised, presence-resolved
14//! [`PreparedCard`]. The app supplies only its SD driver plus retry/degrade
15//! policy — no board detail, no pre-init loop:
16//!
17//! ```ignore
18//! let (mut parts, card_cs) = board.spi2.into_parts(dma_rx_buf, dma_tx_buf)?;
19//! // Display comes up UNCONDITIONALLY (works even with a dead/absent card).
20//! // `CardPresence::ForceAbsent` reaches the SD-absent path with a card in slot.
21//! let (display, prepared) = parts.finish_sd(card_cs, CardPresence::Detect).await?;
22//! let mut sd = SdSpi::new(prepared.into_inner());
23//! // bounded init retries; on failure, degrade (SD absent). Both real-absent
24//! // and ForceAbsent fail here, on the same single degrade path.
25//! ```
26//!
27//! The lower-level [`Spi2Parts::finish`] primitive (no pre-init, plain generic
28//! `card_cs`) remains for callers that drive the exclusive `bus` themselves
29//! before sharing it.
30//!
31//! # CoreS3: GPIO35 is shared between SPI2 MISO and display DC
32//!
33//! The SPI peripheral owns GPIO35 as MISO input (via `with_miso`); the display
34//! DC is driven on the same pad through direct GPIO register writes
35//! ([`Gpio35Dc`](crate::board::cores3::Gpio35Dc)). [`Spi2Parts::finish`] calls
36//! [`gpio35_disable_output`](crate::board::cores3::gpio35_disable_output)
37//! after the display init so the pad returns to high-impedance MISO input —
38//! without it `sd_card.init()` reads garbage on MISO and never completes. At
39//! runtime, every SD operation must re-assert that mux (pass the same function
40//! as the block-device handler's pre-op hook). This is safe only while the
41//! display and the SD card share one task/bus mutex with no `.await` between a
42//! DC write and the SPI transfer.
43//!
44//! # Display/SD task join order differs per chip — do not unify
45//!
46//! When the app joins the display flush loop and the SD block-device handler
47//! in one task: on fire27 (ESP32/PDMA) the SD handler must be polled FIRST so
48//! it grabs the bus mutex preferentially — the mirror order reliably wedges
49//! the SD path. cores3 (ESP32-S3/GDMA) needs the opposite order. The two
50//! chips' interrupt/DMA timing has opposite contention characteristics; don't
51//! change either without re-validating both targets on hardware.
52
53use esp_hal::{
54    Async,
55    dma::{DmaRxBuf, DmaTxBuf},
56    gpio::{AnyPin, Level, Output, OutputConfig},
57    spi::{
58        Mode,
59        master::{AnySpi, Config as SpiConfig, ConfigError, Spi, SpiDmaBus},
60    },
61    time::Rate,
62};
63
64/// The shared bus: descriptor-backed DMA SPI. A plain `Spi::into_async()`
65/// flush goes "usr-stuck" after the first frame on the ESP32 PDMA path, so a
66/// `SpiDmaBus` is required (the CoreS3 uses GDMA for the same reason).
67pub type SpiBusType = SpiDmaBus<'static, Async>;
68
69/// Bus base config: 400 kHz Mode 0 — the clock SD cards require during init.
70/// The app raises the card device's clock (via `SetConfig`) after `init()`.
71pub fn sd_init_config() -> SpiConfig {
72    SpiConfig::default()
73        .with_frequency(Rate::from_khz(400))
74        .with_mode(Mode::_0)
75}
76
77/// Display device config: 40 MHz Mode 0.
78pub fn display_config() -> SpiConfig {
79    sd_init_config().with_frequency(Rate::from_khz(40_000))
80}
81
82/// SPI2 pins + units (CoreS3): SCK=GPIO36, MOSI=GPIO37, MISO/DC=GPIO35,
83/// display CS=GPIO3, card CS=GPIO4, GDMA channel 0.
84#[cfg(feature = "cores3")]
85pub struct Spi2Resources<'a> {
86    pub spi2: AnySpi<'a>,
87    pub spi2_dma: esp_hal::dma::AnyGdmaChannel<'a>,
88    pub sck: AnyPin<'a>,
89    pub mosi: AnyPin<'a>,
90    /// GPIO35 — SPI2 MISO (SD reads) **and** display DC via the register-level
91    /// mux ([`crate::board::cores3::Gpio35Dc`]); see the module docs.
92    pub miso_dc: AnyPin<'a>,
93    pub display_cs: AnyPin<'a>,
94    pub card_cs: AnyPin<'a>,
95}
96
97/// SPI2 pins + units (Fire27): SCK=GPIO18, MOSI=GPIO23, MISO=GPIO19,
98/// display CS=GPIO14 / DC=GPIO27 / RST=GPIO33 / BL=GPIO32, card CS=GPIO4,
99/// PDMA SPI2 channel.
100#[cfg(feature = "fire27")]
101pub struct Spi2Resources<'a> {
102    pub spi2: AnySpi<'a>,
103    pub spi2_dma: esp_hal::dma::AnySpiDmaChannel<'a>,
104    pub sck: AnyPin<'a>,
105    pub mosi: AnyPin<'a>,
106    pub miso: AnyPin<'a>,
107    pub display_cs: AnyPin<'a>,
108    pub display_dc: AnyPin<'a>,
109    pub display_rst: AnyPin<'a>,
110    pub display_bl: AnyPin<'a>,
111    pub card_cs: AnyPin<'a>,
112}
113
114/// The constructed bus + display pins, between [`Spi2Resources::into_parts`]
115/// and [`Spi2Parts::finish`]. `bus` is still exclusive here — the window for
116/// the app's SD pre-init (see the module docs).
117pub struct Spi2Parts {
118    /// Exclusive DMA bus (not yet shared) for `sdspi::sd_init`.
119    pub bus: SpiBusType,
120    display_cs: Output<'static>,
121    #[cfg(feature = "fire27")]
122    display_dc: Output<'static>,
123    #[cfg(feature = "fire27")]
124    display_rst: Output<'static>,
125    #[cfg(feature = "fire27")]
126    display_bl: Output<'static>,
127}
128
129#[cfg(feature = "cores3")]
130impl Spi2Resources<'static> {
131    /// Build the DMA bus and the CS outputs. The DMA buffers are supplied by
132    /// the app — their sizing is application policy (SD block size for RX,
133    /// display strip size for TX). Returns the card CS separately so the app
134    /// can wrap it (e.g. a HIL "no SD card" kill-switch) before
135    /// [`Spi2Parts::finish`].
136    pub fn into_parts(
137        self,
138        dma_rx_buf: DmaRxBuf,
139        dma_tx_buf: DmaTxBuf,
140    ) -> Result<(Spi2Parts, Output<'static>), ConfigError> {
141        let bus = Spi::new(self.spi2, sd_init_config())?
142            .with_sck(self.sck)
143            .with_mosi(self.mosi)
144            .with_miso(self.miso_dc)
145            .with_dma(self.spi2_dma)
146            .with_buffers(dma_rx_buf, dma_tx_buf)
147            .into_async();
148        let display_cs = Output::new(self.display_cs, Level::High, OutputConfig::default());
149        let card_cs = Output::new(self.card_cs, Level::High, OutputConfig::default());
150        Ok((Spi2Parts { bus, display_cs }, card_cs))
151    }
152}
153
154#[cfg(feature = "fire27")]
155impl Spi2Resources<'static> {
156    /// Build the DMA bus and the CS/DC/RST/BL outputs. The DMA buffers are
157    /// supplied by the app — their sizing is application policy (SD block size
158    /// for RX, display strip size for TX). Returns the card CS separately so
159    /// the app can wrap it (e.g. a HIL "no SD card" kill-switch) before
160    /// [`Spi2Parts::finish`]. The backlight starts LOW (no flicker during
161    /// panel init); `finish` turns it on.
162    pub fn into_parts(
163        self,
164        dma_rx_buf: DmaRxBuf,
165        dma_tx_buf: DmaTxBuf,
166    ) -> Result<(Spi2Parts, Output<'static>), ConfigError> {
167        let bus = Spi::new(self.spi2, sd_init_config())?
168            .with_sck(self.sck)
169            .with_mosi(self.mosi)
170            .with_miso(self.miso)
171            .with_dma(self.spi2_dma)
172            .with_buffers(dma_rx_buf, dma_tx_buf)
173            .into_async();
174        let display_cs = Output::new(self.display_cs, Level::High, OutputConfig::default());
175        let card_cs = Output::new(self.card_cs, Level::High, OutputConfig::default());
176        let display_dc = Output::new(self.display_dc, Level::Low, OutputConfig::default());
177        let display_rst = Output::new(self.display_rst, Level::Low, OutputConfig::default());
178        let display_bl = Output::new(self.display_bl, Level::Low, OutputConfig::default());
179        Ok((
180            Spi2Parts { bus, display_cs, display_dc, display_rst, display_bl },
181            card_cs,
182        ))
183    }
184}
185
186// --- Device construction + display init (feature `display`) ----------------
187
188#[cfg(feature = "display")]
189pub use devices::*;
190
191#[cfg(feature = "display")]
192mod devices {
193    use embassy_embedded_hal::shared_bus::asynch::spi::SpiDeviceWithConfig;
194    use embassy_sync::mutex::Mutex;
195    use embedded_hal::digital::{ErrorType, OutputPin};
196    use esp_hal::{
197        dma::{DmaRxBuf, DmaTxBuf},
198        gpio::{Level, Output, OutputConfig},
199        spi::master::Spi,
200    };
201    use esp_sync::RawMutex;
202    use lcd_async::interface::{Interface, SpiInterface};
203    use static_cell::StaticCell;
204
205    use super::{Spi2Parts, Spi2Resources, SpiBusType, display_config, sd_init_config};
206    use crate::board::display::{self, Ili9342c};
207
208    /// A device on the shared bus with a plain GPIO chip-select (the display).
209    pub type SpiDeviceType<'a> = SpiDeviceWithConfig<'a, RawMutex, SpiBusType, Output<'a>>;
210    /// The SD-card device: CS is generic so the app can wrap it.
211    pub type CardSpiDevice<CS> = SpiDeviceWithConfig<'static, RawMutex, SpiBusType, CS>;
212
213    /// Whether the SD slot should behave as populated or be forced to degrade.
214    ///
215    /// `ForceAbsent` is a general force-degrade capability, **not** a HIL word:
216    /// it makes the card device behave as an empty slot (chip-select never
217    /// asserts), so the app's real `SdSpi::init()` runs and fails *authentically*
218    /// — reaching the same graceful-degrade path as a physically empty slot,
219    /// with a card inserted. Any HIL arming (an RTC one-shot, a `:nosd` verb)
220    /// stays consumer-side; nothing HIL leaks into this surface.
221    #[non_exhaustive]
222    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
223    pub enum CardPresence {
224        /// Drive the chip-select normally: a real card initialises; an empty
225        /// slot degrades on its own.
226        Detect,
227        /// Freeze the chip-select deasserted so the card is never selected —
228        /// `SdSpi::init()` then fails exactly like an empty slot.
229        ForceAbsent,
230    }
231
232    /// Chip-select wrapper that can freeze the card *deasserted* to force the
233    /// absent-card path (see [`CardPresence::ForceAbsent`]).
234    ///
235    /// SD chip-select is active-low, so `set_low` selects: when `frozen` it is
236    /// suppressed (the card is never selected → MISO idles `0xFF` → authentic
237    /// init failure), while `set_high` (deselect) is always honoured. It is
238    /// carried *inside* [`PreparedCard`] because the freeze only bites where the
239    /// CS is first **asserted** — downstream in the app's `SdSpi::init()` CMD0,
240    /// not the CS-deasserted 74-clock pre-init. A single runtime-flag type (not
241    /// a type-level marker) keeps [`Spi2Parts::finish_sd`] monomorphic across a
242    /// runtime [`CardPresence`].
243    pub struct PresenceCs<CS> {
244        pin: CS,
245        frozen: bool,
246    }
247
248    impl<CS: ErrorType> ErrorType for PresenceCs<CS> {
249        type Error = CS::Error;
250    }
251
252    impl<CS: OutputPin> OutputPin for PresenceCs<CS> {
253        fn set_low(&mut self) -> Result<(), Self::Error> {
254            if self.frozen {
255                Ok(()) // suppress SELECT while forced-absent
256            } else {
257                self.pin.set_low()
258            }
259        }
260
261        fn set_high(&mut self) -> Result<(), Self::Error> {
262            self.pin.set_high() // deselect is always honoured
263        }
264    }
265
266    /// A card device on the shared SPI2 bus, pre-initialised by
267    /// [`Spi2Parts::finish_sd`] (the ≥74-clock power-up idle has run) and
268    /// presence-resolved. The BSP owns everything up to here with no SD-driver
269    /// type in its graph; the app supplies only its SD driver:
270    /// `SdSpi::new(prepared.into_inner())`.
271    pub struct PreparedCard<CS>(CardSpiDevice<PresenceCs<CS>>);
272
273    impl<CS> PreparedCard<CS> {
274        /// The presence-resolved card `SpiDevice`, ready for the app's SD driver.
275        pub fn into_inner(self) -> CardSpiDevice<PresenceCs<CS>> {
276            self.0
277        }
278    }
279
280    #[cfg(feature = "cores3")]
281    pub type DisplayInterface =
282        SpiInterface<SpiDeviceType<'static>, crate::board::cores3::Gpio35Dc>;
283    #[cfg(feature = "fire27")]
284    pub type DisplayInterface = SpiInterface<SpiDeviceType<'static>, Output<'static>>;
285
286    /// CoreS3 panel: no GPIO reset (AW9523B pulses it; SPI SoftReset fallback).
287    #[cfg(feature = "cores3")]
288    pub type DisplayType = Ili9342c<DisplayInterface>;
289    /// Fire27 panel: hardware reset pin.
290    #[cfg(feature = "fire27")]
291    pub type DisplayType = Ili9342c<DisplayInterface, Output<'static>>;
292
293    pub type DisplayInitError =
294        lcd_async::InitError<<DisplayInterface as Interface>::Error, core::convert::Infallible>;
295
296    /// Display-only DC pin: a plain [`Output`] on **both** boards. On CoreS3
297    /// this is GPIO35 configured as a real output (which routes the pad) — NOT
298    /// [`Gpio35Dc`](crate::board::cores3::Gpio35Dc), whose register-level mux
299    /// relies on `with_miso()` having routed the pad and would otherwise leave
300    /// it unrouted (black screen). Used by [`Spi2Resources::into_display_only`].
301    pub type DisplayOnlyInterface = SpiInterface<SpiDeviceType<'static>, Output<'static>>;
302
303    /// CoreS3 display-only panel: no GPIO reset (AW9523B pulses it).
304    #[cfg(feature = "cores3")]
305    pub type DisplayOnlyType = Ili9342c<DisplayOnlyInterface>;
306    /// Fire27 display-only panel: hardware reset pin.
307    #[cfg(feature = "fire27")]
308    pub type DisplayOnlyType = Ili9342c<DisplayOnlyInterface, Output<'static>>;
309
310    // Both boards' reset pins are infallible (`NoResetPin` / esp-hal `Output`).
311    pub type DisplayOnlyInitError =
312        lcd_async::InitError<<DisplayOnlyInterface as Interface>::Error, core::convert::Infallible>;
313
314    /// A display brought up on the shared SPI2 DMA bus with **no** SD-card path
315    /// ([`Spi2Resources::into_display_only`]). For LVGL and any DMA display-only
316    /// app that does not touch the SD slot.
317    pub struct DisplayBus {
318        pub display: DisplayOnlyType,
319        /// Backlight pin (GPIO32) — driven high by `into_display_only` once the
320        /// panel init succeeds; the caller keeps it alive.
321        #[cfg(feature = "fire27")]
322        pub backlight: Output<'static>,
323    }
324
325    /// The initialised on-board display (plus, on Fire27, its backlight pin).
326    pub struct DisplayDriver {
327        pub display: DisplayType,
328        #[cfg(feature = "fire27")]
329        bl: Output<'static>,
330    }
331
332    #[cfg(feature = "fire27")]
333    impl DisplayDriver {
334        pub fn bl_on(&mut self) {
335            self.bl.set_high();
336        }
337
338        pub fn bl_off(&mut self) {
339            self.bl.set_low();
340        }
341    }
342
343    static SPI_BUS: StaticCell<Mutex<RawMutex, SpiBusType>> = StaticCell::new();
344
345    impl Spi2Parts {
346        /// Share the bus, initialise the display, and build the SD-card
347        /// device.
348        ///
349        /// The display comes up first and unconditionally — it must work even
350        /// when the SD card is dead or absent, so the system keeps a UI. On
351        /// CoreS3 this also restores GPIO35 to high-impedance MISO input
352        /// afterwards (see the module docs); on Fire27 it turns the backlight
353        /// on after a successful init.
354        ///
355        /// On CoreS3 the panel must be out of reset before this is called:
356        /// wait for [`power_display_reset`](crate::board::cores3::power_display_reset)
357        /// (AW9523B `LCD_RST` pulse) — bounded, so a wedged I2C init can't
358        /// freeze the display task forever.
359        pub async fn finish<CS>(
360            self,
361            card_cs: CS,
362        ) -> Result<(DisplayDriver, CardSpiDevice<CS>), DisplayInitError> {
363            let bus = SPI_BUS.init(Mutex::new(self.bus));
364            let display_device =
365                SpiDeviceWithConfig::new(bus, self.display_cs, display_config());
366
367            #[cfg(feature = "cores3")]
368            let driver = {
369                let di = SpiInterface::new(display_device, crate::board::cores3::Gpio35Dc);
370                let display = display::init_ili9342c(di).await?;
371                // CRITICAL: the display init drove GPIO35 as DC (output).
372                // Restore it to high-impedance MISO input now — otherwise the
373                // app's `sd_card.init()` reads garbage on MISO and never
374                // completes. Runtime SD ops must re-assert this per-op.
375                crate::board::cores3::gpio35_disable_output();
376                DisplayDriver { display }
377            };
378
379            #[cfg(feature = "fire27")]
380            let driver = {
381                let di = SpiInterface::new(display_device, self.display_dc);
382                let display =
383                    display::init_ili9342c_with_reset(di, self.display_rst).await?;
384                let mut driver = DisplayDriver { display, bl: self.display_bl };
385                driver.bl_on();
386                driver
387            };
388
389            let card_device = SpiDeviceWithConfig::new(bus, card_cs, sd_init_config());
390            Ok((driver, card_device))
391        }
392
393        /// Publish-safe full SD bring-up, layered on [`finish`].
394        ///
395        /// Runs the mandatory ≥74-clock power-up idle on the still-exclusive
396        /// bus (chip-select deasserted), brings the display up unconditionally
397        /// (see [`finish`]), and hands back a presence-resolved
398        /// [`PreparedCard`]. The app owns only the final
399        /// `SdSpi::new(prepared.into_inner()).init()` plus its retry/degrade
400        /// policy — no SD-driver type enters the BSP graph.
401        ///
402        /// [`CardPresence::ForceAbsent`] reaches the app's normal SD-absent
403        /// degrade path with a card inserted: the freeze takes effect at the
404        /// first CS assert inside `SdSpi::init()`, so real-absent and
405        /// forced-absent share one degrade path.
406        pub async fn finish_sd<CS>(
407            mut self,
408            card_cs: CS,
409            presence: CardPresence,
410        ) -> Result<(DisplayDriver, PreparedCard<CS>), DisplayInitError>
411        where
412            CS: OutputPin,
413        {
414            // ≥74 clock cycles with CS deasserted (card_cs starts High) and DI
415            // high — the SD power-up idle, per the SD-SPI spec. Done on the
416            // still-exclusive bus before `finish` shares it; presence-independent
417            // (the freeze only bites at the first CS assert, downstream). Not
418            // via `sdspi::sd_init` — that fork must not enter the BSP graph.
419            //
420            // `SpiDmaBus::write` here is the inherent BLOCKING method: it returns
421            // `Result`, not a `Future`, and drives the DMA transfer to completion
422            // (~200 µs at 400 kHz for this one-time idle). Load-bearing: if `bus`
423            // ever becomes a type whose `write` yields a future, this must be
424            // `.await`ed or the idle would silently never run. HIL-validated on
425            // both GDMA (CoreS3) and PDMA (Fire27) — the 10-byte / 80-clock write
426            // does not wedge the ESP32 PDMA TX path.
427            if let Err(e) = self.bus.write(&[0xFF; 10]) {
428                warn!("SD power-up idle clock failed: {e:?}");
429            }
430            let card_cs = PresenceCs {
431                pin: card_cs,
432                frozen: matches!(presence, CardPresence::ForceAbsent),
433            };
434            let (driver, card_device) = self.finish(card_cs).await?;
435            Ok((driver, PreparedCard(card_device)))
436        }
437    }
438
439    #[cfg(feature = "cores3")]
440    impl Spi2Resources<'static> {
441        /// Bring up the display **only** on a descriptor-backed `SpiDmaBus`, with
442        /// no SD-card path. The DMA buffers are supplied by the app (TX sized to
443        /// the display stripe; RX unused by a write-only panel). DC is a plain
444        /// `Output` on GPIO35 — a configured output routes the pad, unlike
445        /// [`Gpio35Dc`](crate::board::cores3::Gpio35Dc) which needs `with_miso`.
446        ///
447        /// The panel must already be out of reset: call
448        /// [`power_display_reset`](crate::board::cores3::power_display_reset)
449        /// first (AW9523B `LCD_RST` pulse + AXP2101 backlight).
450        pub async fn into_display_only(
451            self,
452            dma_rx_buf: DmaRxBuf,
453            dma_tx_buf: DmaTxBuf,
454        ) -> Result<DisplayBus, DisplayOnlyInitError> {
455            // No `.with_miso()`: this path never reads the SD card, so GPIO35 is
456            // free to be a plain DC output (below).
457            let spi = Spi::new(self.spi2, display_config())
458                .expect("SPI2 display config")
459                .with_sck(self.sck)
460                .with_mosi(self.mosi)
461                .with_dma(self.spi2_dma)
462                .with_buffers(dma_rx_buf, dma_tx_buf)
463                .into_async();
464            let bus = SPI_BUS.init(Mutex::new(spi));
465            let display_cs = Output::new(self.display_cs, Level::High, OutputConfig::default());
466            let dc = Output::new(self.miso_dc, Level::Low, OutputConfig::default());
467            let device = SpiDeviceWithConfig::new(bus, display_cs, display_config());
468            let di = SpiInterface::new(device, dc);
469            let display = display::init_ili9342c(di).await?;
470            Ok(DisplayBus { display })
471        }
472    }
473
474    #[cfg(feature = "fire27")]
475    impl Spi2Resources<'static> {
476        /// Bring up the display **only** on a descriptor-backed `SpiDmaBus`, with
477        /// no SD-card path. The DMA buffers are supplied by the app (TX sized to
478        /// the display stripe; RX unused by a write-only panel). The panel is
479        /// reset via its GPIO RST pin; the backlight (GPIO32) is driven high on
480        /// success and returned in [`DisplayBus`] for the caller to keep alive.
481        pub async fn into_display_only(
482            self,
483            dma_rx_buf: DmaRxBuf,
484            dma_tx_buf: DmaTxBuf,
485        ) -> Result<DisplayBus, DisplayOnlyInitError> {
486            let spi = Spi::new(self.spi2, display_config())
487                .expect("SPI2 display config")
488                .with_sck(self.sck)
489                .with_mosi(self.mosi)
490                .with_miso(self.miso)
491                .with_dma(self.spi2_dma)
492                .with_buffers(dma_rx_buf, dma_tx_buf)
493                .into_async();
494            let bus = SPI_BUS.init(Mutex::new(spi));
495            let display_cs = Output::new(self.display_cs, Level::High, OutputConfig::default());
496            let dc = Output::new(self.display_dc, Level::Low, OutputConfig::default());
497            let rst = Output::new(self.display_rst, Level::Low, OutputConfig::default());
498            let mut backlight = Output::new(self.display_bl, Level::Low, OutputConfig::default());
499            let device = SpiDeviceWithConfig::new(bus, display_cs, display_config());
500            let di = SpiInterface::new(device, dc);
501            let display = display::init_ili9342c_with_reset(di, rst).await?;
502            backlight.set_high();
503            Ok(DisplayBus { display, backlight })
504        }
505    }
506}