lilkaoxide/
sd.rs

1use crate::spi::{InnerDelay, SpiBusWrapper, SpiInitError};
2use esp_hal::gpio::{Level, Output, OutputConfig};
3use esp_hal::peripherals::GPIO16;
4
5use embedded_timers::{clock::Clock, instant::TimespecInstant};
6use sdmmc;
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>, SdChipSelect<'a>, SdClock>;
66
67/// Keep the card deselected when an initialization attempt fails or it is dropped.
68pub struct SdChipSelect<'a>(Output<'a>);
69impl<'a> SdChipSelect<'a> {
70    pub fn new(pin: GPIO16<'a>) -> Self {
71        Self(Output::new(pin, Level::High, OutputConfig::default()))
72    }
73}
74impl embedded_hal::digital::ErrorType for SdChipSelect<'_> {
75    type Error = core::convert::Infallible;
76}
77impl embedded_hal::digital::OutputPin for SdChipSelect<'_> {
78    fn set_low(&mut self) -> Result<(), Self::Error> {
79        self.0.set_low();
80        Ok(())
81    }
82    fn set_high(&mut self) -> Result<(), Self::Error> {
83        self.0.set_high();
84        Ok(())
85    }
86}
87impl Drop for SdChipSelect<'_> {
88    fn drop(&mut self) {
89        self.0.set_high();
90    }
91}
92
93/// Raw SD card handle.
94///
95/// An alias for [`sdmmc::SD`], wrapping the initialised SD card and its SPI bus.
96/// Available as `Lilka::sd` after a successful [`crate::Lilka::init`].
97///
98/// # Usage
99///
100/// ```rust,no_run
101/// use lilkaoxide::prelude::*;
102///
103/// # async fn example(mut lilka: Lilka) {
104/// if let Some(sd) = &mut lilka.sd {
105///     // Read raw sectors here, or attach a separate FAT filesystem adapter.
106///     defmt::info!("SD card ready");
107/// } else {
108///     defmt::warn!("No SD card detected");
109/// }
110/// # }
111/// ```
112pub type SDC<'a> = sdmmc::SD<SdBus<'a>>;
113/// Historical alias for the raw SD card handle. Does not mount a FAT filesystem.
114pub type SdVolMgr<'a> = SDC<'a>;
115
116/// Error returned when the SD card fails to initialise.
117///
118/// Propagated **non-fatally** from [`crate::Lilka::init`]: a failure sets
119/// `Lilka::sd` to `None` rather than returning an error, so the console can
120/// still run without an SD card.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum SdInitError {
123    /// Creating the `SdSpi` device adapter from the shared bus failed.
124    SpiDevice(SpiInitError),
125    /// SD card initialisation sequence (CMD0, ACMD41, etc.) failed.
126    CardInitFailed,
127}
128
129#[cfg(feature = "AtomicBus")]
130compile_error!("AtomicBus is not supported with async-sdmmc");
131
132impl SdInitError {
133    /// Returns a short, `'static` error description suitable for `defmt` logging.
134    pub const fn as_str(self) -> &'static str {
135        match self {
136            Self::SpiDevice(err) => err.as_str(),
137            Self::CardInitFailed => "SD card init failed",
138        }
139    }
140}
141
142// SD requires MOSI high (0xFF), including while clocking response bytes.
143#[cfg(feature = "blocking")]
144fn transfer_blocking(
145    bus: &mut crate::spi::SpiBusInner,
146    tx: &[u8],
147    rx: &mut [u8],
148) -> Result<(), SpiInitError> {
149    let tail = crate::sd_wire::prepare_read(tx, rx);
150    if !rx.is_empty() {
151        embedded_hal::spi::SpiBus::transfer_in_place(bus, rx)
152            .map_err(|_| SpiInitError::DeviceInitFailed)?;
153    }
154    if !tail.is_empty() {
155        embedded_hal::spi::SpiBus::write(bus, tail).map_err(|_| SpiInitError::DeviceInitFailed)?;
156    }
157    Ok(())
158}
159
160#[cfg(feature = "blocking")]
161impl<'a> sdmmc::bus::spi::Transfer for SdSpi<'a> {
162    type Error = SpiInitError;
163    fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
164        #[cfg(feature = "RefCellBus")]
165        {
166            transfer_blocking(&mut self.0.borrow_mut(), tx, rx)
167        }
168        #[cfg(feature = "CriticalSectionBus")]
169        {
170            critical_section::with(|cs| {
171                transfer_blocking(&mut self.0.borrow(cs).borrow_mut(), tx, rx)
172            })
173        }
174    }
175}
176
177#[cfg(feature = "async")]
178impl<'a> sdmmc::bus::spi::Transfer for SdSpi<'a> {
179    type Error = SpiInitError;
180    async fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
181        let mut bus = self.0.lock().await;
182        let tail = crate::sd_wire::prepare_read(tx, rx);
183        if !rx.is_empty() {
184            embedded_hal_async::spi::SpiBus::transfer_in_place(&mut *bus, rx)
185                .await
186                .map_err(|_| SpiInitError::DeviceInitFailed)?;
187        }
188        if !tail.is_empty() {
189            embedded_hal_async::spi::SpiBus::write(&mut *bus, tail)
190                .await
191                .map_err(|_| SpiInitError::DeviceInitFailed)?;
192        }
193        Ok(())
194    }
195}
196
197#[cfg_attr(not(feature = "async"), deasync::deasync)]
198async fn set_frequency(
199    bus: &SpiBusWrapper,
200    frequency: esp_hal::time::Rate,
201) -> Result<(), SdInitError> {
202    let config = esp_hal::spi::master::Config::default().with_frequency(frequency);
203    #[cfg(all(feature = "blocking", feature = "RefCellBus"))]
204    let result = bus.borrow_mut().apply_config(&config);
205    #[cfg(all(feature = "blocking", feature = "CriticalSectionBus"))]
206    let result = critical_section::with(|cs| bus.borrow(cs).borrow_mut().apply_config(&config));
207    #[cfg(feature = "async")]
208    let result = bus.lock().await.apply_config(&config);
209    result.map_err(|_| SdInitError::SpiDevice(SpiInitError::BusInitFailed))
210}
211
212/// Initialises the SD card over the shared SPI bus and returns an [`SDC`] handle.
213///
214/// Called internally by [`crate::Lilka::init`].  The SD card uses GPIO16 as its
215/// chip-select and shares SPI2 with the display.
216///
217/// # Card detection behaviour
218///
219/// SD card failure is **non-fatal** in [`crate::Lilka::init`]: a failure here causes
220/// `Lilka::sd` to be set to `None` rather than aborting the entire init sequence.
221/// This allows the console to boot normally when no SD card is inserted.
222///
223/// # Parameters
224///
225/// * `spi_bus` — `'static` reference to the shared [`SpiBusWrapper`].
226/// * `sd_cs` — GPIO16 output, already deselected before display initialization.
227/// * `delay` — Delay for SD SPI timing; obtained from [`SdDelay`].
228/// * `frequency` — Shared-bus operating rate, restored after 400 kHz identification.
229///
230/// # Returns
231///
232/// `Ok(`[`SDC`]`)` on success, or `Err(`[`SdInitError`]`)` if the card does not respond.
233///
234/// # Note on `async` vs `blocking`
235///
236/// Like [`crate::Lilka::init`], the `deasync` proc-macro generates a synchronous
237/// version of this function when the `blocking` feature is active.
238#[cfg_attr(not(feature = "async"), deasync::deasync)]
239pub async fn init_sd<'a>(
240    spi_bus: &'a SpiBusWrapper,
241    sd_cs: SdChipSelect<'a>,
242    delay: SdDelay,
243    frequency: fugit::HertzU32,
244) -> Result<SDC<'a>, SdInitError> {
245    use esp_hal::time::Rate;
246    // The card's identification phase requires 100-400 kHz, even when the
247    // display and initialized card normally share a much faster bus.
248    set_frequency(spi_bus, Rate::from_khz(400)).await?;
249    let mut bus = SdBus::new(SdSpi(spi_bus), sd_cs, SdClock);
250    let card = match bus.init(delay).await {
251        Ok(card) => card,
252        Err(error) => {
253            esp_println::println!("SD identification failed at 400 kHz: {:?}", error);
254            set_frequency(spi_bus, Rate::from_hz(frequency.raw())).await?;
255            return Err(SdInitError::CardInitFailed);
256        }
257    };
258    let result = sdmmc::SD::init(bus, card).await.map_err(|error| {
259        esp_println::println!("SD card registers could not be read: {:?}", error);
260        SdInitError::CardInitFailed
261    });
262    // Always restore the shared bus, including when no card was found.
263    set_frequency(spi_bus, Rate::from_hz(frequency.raw())).await?;
264    result
265}