Skip to main content

core_s3/
sd.rs

1//! TF/microSD card metadata and helpers for CoreS3.
2//!
3//! CoreS3 routes the TF card over the same SPI signal group used by the LCD:
4//! SCLK GPIO36, MOSI/COPI GPIO37, MISO/CIPO GPIO35, and CS GPIO4. Applications
5//! that need both display and SD access should coordinate ownership of this
6//! shared bus at the HAL layer. The card-detect switch is exposed through
7//! AW9523B port 0 bit 4 and is active-low, matching the official M5Stack demo.
8
9use embedded_hal::{delay::DelayNs, spi::SpiDevice};
10
11use crate::pins::SpiSdPins;
12
13/// GPIO35's logical role while CoreS3 LCD and TF-card share SPI2.
14#[cfg_attr(feature = "defmt", derive(defmt::Format))]
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum SharedGpio35Role {
17    /// GPIO35 is released as the TF-card MISO input / safe idle role.
18    SdMisoInput,
19    /// GPIO35 is driven as LCD D/C while LCD CS is active.
20    LcdDcOutput,
21}
22
23/// Pure CoreS3 shared-SPI invariant model used by host tests and docs.
24#[cfg_attr(feature = "defmt", derive(defmt::Format))]
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct SharedSpiLogicState {
27    pub lcd_cs_low: bool,
28    pub sd_cs_low: bool,
29    pub gpio35: SharedGpio35Role,
30}
31
32impl SharedSpiLogicState {
33    /// Safe CoreS3 shared-SPI idle state: both CS lines high and GPIO35 MISO-safe.
34    pub const SAFE_IDLE: Self = Self {
35        lcd_cs_low: false,
36        sd_cs_low: false,
37        gpio35: SharedGpio35Role::SdMisoInput,
38    };
39
40    /// Begin an LCD transaction: SD CS high, GPIO35 D/C output, LCD CS low.
41    pub const fn begin_lcd(self) -> Self {
42        Self {
43            lcd_cs_low: true,
44            sd_cs_low: false,
45            gpio35: SharedGpio35Role::LcdDcOutput,
46        }
47    }
48
49    /// Begin an SD transaction: LCD CS high, GPIO35 MISO-safe, SD CS low.
50    pub const fn begin_sd(self) -> Self {
51        Self {
52            lcd_cs_low: false,
53            sd_cs_low: true,
54            gpio35: SharedGpio35Role::SdMisoInput,
55        }
56    }
57
58    /// End or clean up either transaction into the BSP's safe idle state.
59    pub const fn safe_idle(self) -> Self {
60        Self::SAFE_IDLE
61    }
62
63    /// Whether the model satisfies CoreS3's LCD/SD shared-bus invariants.
64    pub const fn invariants_hold(self) -> bool {
65        !(self.lcd_cs_low && self.sd_cs_low)
66            && if self.lcd_cs_low {
67                matches!(self.gpio35, SharedGpio35Role::LcdDcOutput)
68            } else {
69                matches!(self.gpio35, SharedGpio35Role::SdMisoInput)
70            }
71    }
72}
73
74/// Pure state for CoreS3 SD command CS-framing over `embedded-sdmmc` calls.
75#[cfg_attr(feature = "defmt", derive(defmt::Format))]
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub struct SdCommandFramingState {
78    pub selected_command: Option<u8>,
79    pub trailing_single_response_bytes: u8,
80    pub data_token_seen: bool,
81    pub data_payload_seen: bool,
82}
83
84impl SdCommandFramingState {
85    pub const IDLE: Self = Self {
86        selected_command: None,
87        trailing_single_response_bytes: 0,
88        data_token_seen: false,
89        data_payload_seen: false,
90    };
91
92    pub const fn command_write(self, command: u8) -> Self {
93        Self {
94            selected_command: Some(command),
95            trailing_single_response_bytes: 0,
96            data_token_seen: false,
97            data_payload_seen: false,
98        }
99    }
100
101    pub const fn one_byte_poll(self, byte: u8) -> Self {
102        match self.selected_command {
103            Some(_) if self.trailing_single_response_bytes > 0 => {
104                if self.trailing_single_response_bytes == 1 {
105                    Self::IDLE
106                } else {
107                    Self {
108                        trailing_single_response_bytes: self.trailing_single_response_bytes - 1,
109                        ..self
110                    }
111                }
112            }
113            Some(command) if command_has_data_block_const(command) && byte == 0xFE => Self {
114                data_token_seen: true,
115                ..self
116            },
117            Some(command) if (byte & 0x80) == 0 => {
118                if command_has_single_byte_after_r1_const(command) {
119                    Self {
120                        trailing_single_response_bytes: 1,
121                        ..self
122                    }
123                } else if !command_has_trailing_response_const(command)
124                    && !command_has_data_block_const(command)
125                {
126                    Self::IDLE
127                } else {
128                    self
129                }
130            }
131            _ => self,
132        }
133    }
134
135    pub const fn transfer_in_place(self, len: usize) -> Self {
136        match self.selected_command {
137            Some(command) if command_has_trailing_response_const(command) => Self::IDLE,
138            Some(command) if command_has_data_block_const(command) && self.data_token_seen => {
139                if self.data_payload_seen && len == 2 {
140                    Self::IDLE
141                } else {
142                    Self {
143                        data_payload_seen: true,
144                        ..self
145                    }
146                }
147            }
148            _ => self,
149        }
150    }
151}
152
153const fn command_has_trailing_response_const(command: u8) -> bool {
154    matches!(command, 8 | 58)
155}
156
157const fn command_has_single_byte_after_r1_const(command: u8) -> bool {
158    matches!(command, 13)
159}
160
161const fn command_has_data_block_const(command: u8) -> bool {
162    matches!(command, 9 | 10 | 17 | 18 | 24 | 25)
163}
164
165/// Default SPI clock used by M5Stack's CoreS3 SD demo.
166pub const DEFAULT_SPI_HZ: u32 = 25_000_000;
167/// AW9523B port-0 bit used for TF card detect.
168pub const CARD_DETECT_P0_BIT: u8 = 4;
169
170/// Static CoreS3 TF-card wiring and bus settings.
171#[cfg_attr(feature = "defmt", derive(defmt::Format))]
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub struct SdCardSlot {
174    /// SPI pins for the TF card socket.
175    pub spi: SpiSdPins,
176    /// Recommended maximum SPI clock for initialization/use.
177    pub spi_hz: u32,
178    /// Card-detect signal exposed through AW9523B input port 0.
179    pub detect: SdCardDetect,
180}
181
182impl SdCardSlot {
183    /// CoreS3 onboard TF-card slot.
184    pub const CORE_S3: Self = Self {
185        spi: SpiSdPins::TF_CARD,
186        spi_hz: DEFAULT_SPI_HZ,
187        detect: SdCardDetect::AW9523B_P0_4_ACTIVE_LOW,
188    };
189}
190
191/// Card-detect wiring description.
192#[cfg_attr(feature = "defmt", derive(defmt::Format))]
193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
194pub struct SdCardDetect {
195    /// AW9523B input port number.
196    pub port: u8,
197    /// Bit in the input port.
198    pub bit: u8,
199    /// Whether a low level means card-present.
200    pub active_low: bool,
201}
202
203impl SdCardDetect {
204    /// CoreS3 TF card detect: AW9523B port 0 bit 4, active-low.
205    pub const AW9523B_P0_4_ACTIVE_LOW: Self = Self {
206        port: 0,
207        bit: CARD_DETECT_P0_BIT,
208        active_low: true,
209    };
210
211    /// Interpret a raw AW9523B input-port value.
212    pub const fn present_from_port_value(self, value: u8) -> bool {
213        let high = (value & (1 << self.bit)) != 0;
214        if self.active_low { !high } else { high }
215    }
216}
217
218/// Runtime SD-slot metadata exposed to downstream storage stacks.
219#[cfg_attr(feature = "defmt", derive(defmt::Format))]
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub struct CoreS3SdSlot {
222    /// SPI SCLK GPIO number.
223    pub sclk_gpio: u8,
224    /// SPI MOSI/COPI/CMD GPIO number.
225    pub mosi_gpio: u8,
226    /// SPI MISO/CIPO/D0 GPIO number. On CoreS3 this is also the LCD D/C pad.
227    pub miso_gpio: u8,
228    /// TF-card chip-select GPIO number.
229    pub cs_gpio: u8,
230    /// Maximum SPI frequency used by the official M5Stack SD example.
231    pub max_frequency_hz: u32,
232    /// Optional direct card-detect GPIO. CoreS3 uses AW9523B instead, so this is `None`.
233    pub card_detect_gpio: Option<u8>,
234    /// Optional direct power-enable GPIO. CoreS3 SD power is board-managed, so this is `None`.
235    pub power_enable_gpio: Option<u8>,
236}
237
238impl CoreS3SdSlot {
239    /// Onboard CoreS3 TF-card slot metadata.
240    pub const CORE_S3: Self = Self {
241        sclk_gpio: 36,
242        mosi_gpio: 37,
243        miso_gpio: 35,
244        cs_gpio: 4,
245        max_frequency_hz: DEFAULT_SPI_HZ,
246        card_detect_gpio: None,
247        power_enable_gpio: None,
248    };
249}
250
251impl From<SdCardSlot> for CoreS3SdSlot {
252    fn from(slot: SdCardSlot) -> Self {
253        Self {
254            sclk_gpio: slot.spi.sclk.0,
255            mosi_gpio: slot.spi.mosi.0,
256            miso_gpio: slot.spi.miso.0,
257            cs_gpio: slot.spi.cs.0,
258            max_frequency_hz: slot.spi_hz,
259            card_detect_gpio: None,
260            power_enable_gpio: None,
261        }
262    }
263}
264
265/// Low-level SD resources returned by ESP-HAL BSP helpers.
266///
267/// `spi_device` implements [`embedded_hal::spi::SpiDevice`] and can be passed to
268/// `embedded_sdmmc::SdCard::new(spi_device, delay)` by downstream firmware. The
269/// BSP intentionally does not add Wi-Fi credential, token, or application-secret
270/// abstractions; applications should encrypt sensitive bytes before writing them.
271pub struct CoreS3SdParts<SPI, DELAY> {
272    /// Chip-select scoped SPI device for the TF-card socket.
273    pub spi_device: SPI,
274    /// Delay provider suitable for SD-card initialization.
275    pub delay: DELAY,
276    /// Static CoreS3 TF-card slot metadata.
277    pub slot: CoreS3SdSlot,
278}
279
280impl<SPI, DELAY> CoreS3SdParts<SPI, DELAY>
281where
282    SPI: SpiDevice,
283    DELAY: DelayNs,
284{
285    /// Convert these parts into an `embedded-sdmmc` SD-card block device.
286    #[cfg(feature = "sdmmc")]
287    pub fn into_sdmmc(self) -> embedded_sdmmc::SdCard<SPI, DELAY> {
288        embedded_sdmmc::SdCard::new(self.spi_device, self.delay)
289    }
290}
291
292/// Interpret CoreS3's raw AW9523B P0 input byte as TF-card presence.
293pub const fn core_s3_card_present_from_aw9523_p0(value: u8) -> bool {
294    SdCardDetect::AW9523B_P0_4_ACTIVE_LOW.present_from_port_value(value)
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn card_detect_is_active_low_on_p0_bit4() {
303        assert!(core_s3_card_present_from_aw9523_p0(0b1110_1111));
304        assert!(!core_s3_card_present_from_aw9523_p0(0b0001_0000));
305    }
306
307    #[test]
308    fn shared_spi_logic_starts_sd_miso_safe() {
309        let state = SharedSpiLogicState::SAFE_IDLE;
310        assert!(!state.lcd_cs_low);
311        assert!(!state.sd_cs_low);
312        assert_eq!(state.gpio35, SharedGpio35Role::SdMisoInput);
313        assert!(state.invariants_hold());
314    }
315
316    #[test]
317    fn lcd_transaction_forces_sd_deselected_and_restores_idle() {
318        let active = SharedSpiLogicState::SAFE_IDLE.begin_lcd();
319        assert_eq!(
320            active,
321            SharedSpiLogicState {
322                lcd_cs_low: true,
323                sd_cs_low: false,
324                gpio35: SharedGpio35Role::LcdDcOutput,
325            }
326        );
327        assert!(active.invariants_hold());
328        assert_eq!(active.safe_idle(), SharedSpiLogicState::SAFE_IDLE);
329    }
330
331    #[test]
332    fn sd_transaction_forces_lcd_deselected_and_restores_idle() {
333        let active = SharedSpiLogicState::SAFE_IDLE.begin_sd();
334        assert_eq!(
335            active,
336            SharedSpiLogicState {
337                lcd_cs_low: false,
338                sd_cs_low: true,
339                gpio35: SharedGpio35Role::SdMisoInput,
340            }
341        );
342        assert!(active.invariants_hold());
343        assert_eq!(active.safe_idle(), SharedSpiLogicState::SAFE_IDLE);
344    }
345
346    #[test]
347    fn repeated_lcd_sd_lcd_transitions_preserve_invariants() {
348        let mut state = SharedSpiLogicState::SAFE_IDLE;
349        for _ in 0..8 {
350            state = state.begin_lcd();
351            assert!(state.invariants_hold());
352            state = state.safe_idle().begin_sd();
353            assert!(state.invariants_hold());
354            state = state.safe_idle();
355            assert_eq!(state, SharedSpiLogicState::SAFE_IDLE);
356        }
357    }
358
359    #[test]
360    fn invalid_simultaneous_cs_state_is_rejected_by_model() {
361        let invalid = SharedSpiLogicState {
362            lcd_cs_low: true,
363            sd_cs_low: true,
364            gpio35: SharedGpio35Role::LcdDcOutput,
365        };
366        assert!(!invalid.invariants_hold());
367    }
368
369    #[test]
370    fn cmd0_stays_selected_until_r1_response() {
371        let state = SdCommandFramingState::IDLE.command_write(0);
372        assert_eq!(state.selected_command, Some(0));
373        let state = state.one_byte_poll(0xFF);
374        assert_eq!(state.selected_command, Some(0));
375        let state = state.one_byte_poll(0x01);
376        assert_eq!(state, SdCommandFramingState::IDLE);
377    }
378
379    #[test]
380    fn cmd17_closes_after_data_payload_and_crc() {
381        let state = SdCommandFramingState::IDLE
382            .command_write(17)
383            .one_byte_poll(0x00)
384            .one_byte_poll(0xFF)
385            .one_byte_poll(0xFE);
386        assert_eq!(state.selected_command, Some(17));
387        assert!(state.data_token_seen);
388        let state = state.transfer_in_place(512);
389        assert_eq!(state.selected_command, Some(17));
390        let state = state.transfer_in_place(2);
391        assert_eq!(state, SdCommandFramingState::IDLE);
392    }
393
394    #[test]
395    fn cmd8_and_cmd58_close_after_trailing_response() {
396        for command in [8, 58] {
397            let state = SdCommandFramingState::IDLE
398                .command_write(command)
399                .one_byte_poll(0x01)
400                .transfer_in_place(4);
401            assert_eq!(state, SdCommandFramingState::IDLE);
402        }
403    }
404
405    #[test]
406    fn cmd13_keeps_cs_for_second_status_byte() {
407        let state = SdCommandFramingState::IDLE
408            .command_write(13)
409            .one_byte_poll(0x00);
410        assert_eq!(state.selected_command, Some(13));
411        assert_eq!(state.trailing_single_response_bytes, 1);
412        let state = state.one_byte_poll(0x00);
413        assert_eq!(state, SdCommandFramingState::IDLE);
414    }
415
416    #[test]
417    fn cmd24_and_cmd25_remain_selected_for_data_phase() {
418        for command in [24, 25] {
419            let state = SdCommandFramingState::IDLE
420                .command_write(command)
421                .one_byte_poll(0x00);
422            assert_eq!(state.selected_command, Some(command));
423        }
424    }
425}