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}