lilkaoxide/
sd.rs

1use crate::spi::{InnerDelay, SpiBusWrapper, SpiInitError};
2use esp_hal::gpio::{Level, Output, OutputConfig};
3use esp_hal::peripherals::GPIO16;
4
5use sdmmc;
6use embedded_timers::{clock::Clock, instant::TimespecInstant};
7
8/// Thin wrapper around [`InnerDelay`] that implements the correct delay trait
9/// for the `sdmmc` crate depending on the active feature.
10///
11/// * **`blocking`** — implements `embedded_hal::delay::DelayNs`.
12/// * **`async`** — implements `sdmmc::delay::Delay` via `embassy_time::Timer`.
13///
14/// This type is an internal implementation detail; you do not need to construct
15/// it directly.
16pub struct SdDelay(pub InnerDelay);
17
18#[cfg(feature = "blocking")]
19impl embedded_hal::delay::DelayNs for SdDelay {
20    fn delay_ns(&mut self, ns: u32) {
21        self.0.delay_ns(ns)
22    }
23}
24
25#[cfg(feature = "async")]
26impl sdmmc::delay::Delay for SdDelay {
27    type Future = embassy_time::Timer;
28
29    fn delay_ms(&mut self, ms: u32) -> Self::Future {
30        let _ = &mut self.0;
31        embassy_time::Timer::after_millis(ms as u64)
32    }
33}
34
35/// Monotonic clock source for the `sdmmc` crate.
36///
37/// Internally uses:
38/// * `embassy_time::Instant` in `async` builds.
39/// * `esp_hal::time::Instant` in `blocking` builds.
40///
41/// Resolution: 1 ms.  This is an internal implementation detail.
42pub struct SdClock;
43
44impl Clock for SdClock {
45    type Instant = TimespecInstant;
46
47    fn now(&self) -> Self::Instant {
48        #[cfg(feature = "async")]
49        let now = embassy_time::Instant::now().as_millis();
50        #[cfg(feature = "blocking")]
51        let now = esp_hal::time::Instant::now()
52            .duration_since_epoch()
53            .as_millis();
54        TimespecInstant::new((now / 1000) as u32, ((now % 1000) * 1_000_000) as u32)
55    }
56}
57
58/// Newtype wrapping a `&SpiBusWrapper` for the `sdmmc` SPI bus trait.
59///
60/// This is an internal adapter between the SDK's shared-bus abstraction and
61/// the `sdmmc` crate's transfer interface.
62pub struct SdSpi<'a>(pub &'a SpiBusWrapper);
63
64/// SD card SPI bus type (internal).
65pub type SdBus<'a> = sdmmc::bus::spi::Bus<SdSpi<'a>, Output<'a>, SdClock>;
66
67/// SD card volume manager handle.
68///
69/// An alias for [`sdmmc::SD`], wrapping the initialised SD card and its SPI bus.
70/// Available as `Lilka::sd` after a successful [`crate::Lilka::init`].
71///
72/// # Usage
73///
74/// ```rust,no_run
75/// use lilkaoxide::prelude::*;
76///
77/// # async fn example(mut lilka: Lilka) {
78/// if let Some(sd) = &mut lilka.sd {
79///     // Use `sdmmc` API to open volumes, directories, and files
80///     defmt::info!("SD card ready");
81/// } else {
82///     defmt::warn!("No SD card detected");
83/// }
84/// # }
85/// ```
86pub type SDC<'a> = sdmmc::SD<SdBus<'a>>;
87/// Type alias for the SD volume manager (same as [`SDC`]).
88pub type SdVolMgr<'a> = SDC<'a>;
89
90/// Error returned when the SD card fails to initialise.
91///
92/// Propagated **non-fatally** from [`crate::Lilka::init`]: a failure sets
93/// `Lilka::sd` to `None` rather than returning an error, so the console can
94/// still run without an SD card.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum SdInitError {
97    /// Creating the `SdSpi` device adapter from the shared bus failed.
98    SpiDevice(SpiInitError),
99    /// SD card initialisation sequence (CMD0, ACMD41, etc.) failed.
100    CardInitFailed,
101}
102
103#[cfg(feature = "AtomicBus")]
104compile_error!("AtomicBus is not supported with async-sdmmc");
105
106impl SdInitError {
107    /// Returns a short, `'static` error description suitable for `defmt` logging.
108    pub const fn as_str(self) -> &'static str {
109        match self {
110            Self::SpiDevice(err) => err.as_str(),
111            Self::CardInitFailed => "SD card init failed",
112        }
113    }
114}
115
116#[cfg(feature = "blocking")]
117impl<'a> sdmmc::bus::spi::Transfer for SdSpi<'a> {
118    type Error = SpiInitError;
119
120    fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
121        #[cfg(feature = "RefCellBus")]
122        {
123            let bus = &mut *self.0.borrow_mut();
124            match (!tx.is_empty(), !rx.is_empty()) {
125                (true, true) => {
126                    let n = core::cmp::min(tx.len(), rx.len());
127                    rx[..n].copy_from_slice(&tx[..n]);
128                    embedded_hal::spi::SpiBus::transfer_in_place(bus, &mut rx[..n])
129                        .map_err(|_| SpiInitError::DeviceInitFailed)?;
130                    Ok(())
131                }
132                (true, false) => embedded_hal::spi::SpiBus::write(bus, tx)
133                    .map_err(|_| SpiInitError::DeviceInitFailed),
134                (false, true) => embedded_hal::spi::SpiBus::read(bus, rx)
135                    .map_err(|_| SpiInitError::DeviceInitFailed),
136                _ => Ok(()),
137            }
138        }
139        #[cfg(feature = "CriticalSectionBus")]
140        {
141            self.0.lock(|bus| {
142                let mut bus = bus.borrow_mut();
143                match (!tx.is_empty(), !rx.is_empty()) {
144                    (true, true) => {
145                        let n = core::cmp::min(tx.len(), rx.len());
146                        rx[..n].copy_from_slice(&tx[..n]);
147                        embedded_hal::spi::SpiBus::transfer_in_place(&mut *bus, &mut rx[..n])
148                            .map_err(|_| SpiInitError::DeviceInitFailed)?;
149                        Ok(())
150                    }
151                    (true, false) => embedded_hal::spi::SpiBus::write(&mut *bus, tx)
152                        .map_err(|_| SpiInitError::DeviceInitFailed),
153                    (false, true) => embedded_hal::spi::SpiBus::read(&mut *bus, rx)
154                        .map_err(|_| SpiInitError::DeviceInitFailed),
155                    _ => Ok(()),
156                }
157            })
158        }
159    }
160}
161
162#[cfg(feature = "async")]
163impl<'a> sdmmc::bus::spi::Transfer for SdSpi<'a> {
164    type Error = SpiInitError;
165
166    async fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
167        let mut guard = self.0.lock().await;
168        match (!tx.is_empty(), !rx.is_empty()) {
169            (true, true) => {
170                let n = core::cmp::min(tx.len(), rx.len());
171                rx[..n].copy_from_slice(&tx[..n]);
172                embedded_hal_async::spi::SpiBus::transfer_in_place(&mut *guard, &mut rx[..n])
173                    .await
174                    .map_err(|_| SpiInitError::DeviceInitFailed)?;
175                Ok(())
176            }
177            (true, false) => embedded_hal_async::spi::SpiBus::write(&mut *guard, tx)
178                .await
179                .map_err(|_| SpiInitError::DeviceInitFailed),
180            (false, true) => embedded_hal_async::spi::SpiBus::read(&mut *guard, rx)
181                .await
182                .map_err(|_| SpiInitError::DeviceInitFailed),
183            _ => Ok(()),
184        }
185    }
186}
187
188/// Initialises the SD card over the shared SPI bus and returns an [`SDC`] handle.
189///
190/// Called internally by [`crate::Lilka::init`].  The SD card uses GPIO16 as its
191/// chip-select and shares SPI2 with the display.
192///
193/// # Card detection behaviour
194///
195/// SD card failure is **non-fatal** in [`crate::Lilka::init`]: a failure here causes
196/// `Lilka::sd` to be set to `None` rather than aborting the entire init sequence.
197/// This allows the console to boot normally when no SD card is inserted.
198///
199/// # Parameters
200///
201/// * `spi_bus` — `'static` reference to the shared [`SpiBusWrapper`].
202/// * `sd_cs` — GPIO16 peripheral token (moved permanently into an output pin).
203/// * `delay` — Delay for SD SPI timing; obtained from [`SdDelay`].
204///
205/// # Returns
206///
207/// `Ok(`[`SDC`]`)` on success, or `Err(`[`SdInitError`]`)` if the card does not respond.
208///
209/// # Note on `async` vs `blocking`
210///
211/// Like [`crate::Lilka::init`], the `deasync` proc-macro generates a synchronous
212/// version of this function when the `blocking` feature is active.
213#[cfg_attr(not(feature = "async"), deasync::deasync)]
214pub async fn init_sd<'a>(
215    spi_bus: &'a SpiBusWrapper,
216    sd_cs: GPIO16<'a>,
217    delay: SdDelay,
218) -> Result<SDC<'a>, SdInitError> {
219    let sd_cs = Output::new(sd_cs, Level::Low, OutputConfig::default());
220    let mut bus = SdBus::new(SdSpi(spi_bus), sd_cs, SdClock);
221    let card = bus.init(delay).await.map_err(|_| SdInitError::CardInitFailed)?;
222    let sd = sdmmc::SD::init(bus, card)
223        .await
224        .map_err(|_| SdInitError::CardInitFailed)?;
225
226    Ok(sd)
227}