use embedded_hal::{delay::DelayNs, spi::SpiDevice};
use crate::pins::SpiSdPins;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SharedGpio35Role {
SdMisoInput,
LcdDcOutput,
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SharedSpiLogicState {
pub lcd_cs_low: bool,
pub sd_cs_low: bool,
pub gpio35: SharedGpio35Role,
}
impl SharedSpiLogicState {
pub const SAFE_IDLE: Self = Self {
lcd_cs_low: false,
sd_cs_low: false,
gpio35: SharedGpio35Role::SdMisoInput,
};
pub const fn begin_lcd(self) -> Self {
Self {
lcd_cs_low: true,
sd_cs_low: false,
gpio35: SharedGpio35Role::LcdDcOutput,
}
}
pub const fn begin_sd(self) -> Self {
Self {
lcd_cs_low: false,
sd_cs_low: true,
gpio35: SharedGpio35Role::SdMisoInput,
}
}
pub const fn safe_idle(self) -> Self {
Self::SAFE_IDLE
}
pub const fn invariants_hold(self) -> bool {
!(self.lcd_cs_low && self.sd_cs_low)
&& if self.lcd_cs_low {
matches!(self.gpio35, SharedGpio35Role::LcdDcOutput)
} else {
matches!(self.gpio35, SharedGpio35Role::SdMisoInput)
}
}
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SdCommandFramingState {
pub selected_command: Option<u8>,
pub trailing_single_response_bytes: u8,
pub data_token_seen: bool,
pub data_payload_seen: bool,
}
impl SdCommandFramingState {
pub const IDLE: Self = Self {
selected_command: None,
trailing_single_response_bytes: 0,
data_token_seen: false,
data_payload_seen: false,
};
pub const fn command_write(self, command: u8) -> Self {
Self {
selected_command: Some(command),
trailing_single_response_bytes: 0,
data_token_seen: false,
data_payload_seen: false,
}
}
pub const fn one_byte_poll(self, byte: u8) -> Self {
match self.selected_command {
Some(_) if self.trailing_single_response_bytes > 0 => {
if self.trailing_single_response_bytes == 1 {
Self::IDLE
} else {
Self {
trailing_single_response_bytes: self.trailing_single_response_bytes - 1,
..self
}
}
}
Some(command) if command_has_data_block_const(command) && byte == 0xFE => Self {
data_token_seen: true,
..self
},
Some(command) if (byte & 0x80) == 0 => {
if command_has_single_byte_after_r1_const(command) {
Self {
trailing_single_response_bytes: 1,
..self
}
} else if !command_has_trailing_response_const(command)
&& !command_has_data_block_const(command)
{
Self::IDLE
} else {
self
}
}
_ => self,
}
}
pub const fn transfer_in_place(self, len: usize) -> Self {
match self.selected_command {
Some(command) if command_has_trailing_response_const(command) => Self::IDLE,
Some(command) if command_has_data_block_const(command) && self.data_token_seen => {
if self.data_payload_seen && len == 2 {
Self::IDLE
} else {
Self {
data_payload_seen: true,
..self
}
}
}
_ => self,
}
}
}
const fn command_has_trailing_response_const(command: u8) -> bool {
matches!(command, 8 | 58)
}
const fn command_has_single_byte_after_r1_const(command: u8) -> bool {
matches!(command, 13)
}
const fn command_has_data_block_const(command: u8) -> bool {
matches!(command, 9 | 10 | 17 | 18 | 24 | 25)
}
pub const DEFAULT_SPI_HZ: u32 = 25_000_000;
pub const CARD_DETECT_P0_BIT: u8 = 4;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SdCardSlot {
pub spi: SpiSdPins,
pub spi_hz: u32,
pub detect: SdCardDetect,
}
impl SdCardSlot {
pub const CORE_S3: Self = Self {
spi: SpiSdPins::TF_CARD,
spi_hz: DEFAULT_SPI_HZ,
detect: SdCardDetect::AW9523B_P0_4_ACTIVE_LOW,
};
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SdCardDetect {
pub port: u8,
pub bit: u8,
pub active_low: bool,
}
impl SdCardDetect {
pub const AW9523B_P0_4_ACTIVE_LOW: Self = Self {
port: 0,
bit: CARD_DETECT_P0_BIT,
active_low: true,
};
pub const fn present_from_port_value(self, value: u8) -> bool {
let high = (value & (1 << self.bit)) != 0;
if self.active_low { !high } else { high }
}
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CoreS3SdSlot {
pub sclk_gpio: u8,
pub mosi_gpio: u8,
pub miso_gpio: u8,
pub cs_gpio: u8,
pub max_frequency_hz: u32,
pub card_detect_gpio: Option<u8>,
pub power_enable_gpio: Option<u8>,
}
impl CoreS3SdSlot {
pub const CORE_S3: Self = Self {
sclk_gpio: 36,
mosi_gpio: 37,
miso_gpio: 35,
cs_gpio: 4,
max_frequency_hz: DEFAULT_SPI_HZ,
card_detect_gpio: None,
power_enable_gpio: None,
};
}
impl From<SdCardSlot> for CoreS3SdSlot {
fn from(slot: SdCardSlot) -> Self {
Self {
sclk_gpio: slot.spi.sclk.0,
mosi_gpio: slot.spi.mosi.0,
miso_gpio: slot.spi.miso.0,
cs_gpio: slot.spi.cs.0,
max_frequency_hz: slot.spi_hz,
card_detect_gpio: None,
power_enable_gpio: None,
}
}
}
pub struct CoreS3SdParts<SPI, DELAY> {
pub spi_device: SPI,
pub delay: DELAY,
pub slot: CoreS3SdSlot,
}
impl<SPI, DELAY> CoreS3SdParts<SPI, DELAY>
where
SPI: SpiDevice,
DELAY: DelayNs,
{
#[cfg(feature = "sdmmc")]
pub fn into_sdmmc(self) -> embedded_sdmmc::SdCard<SPI, DELAY> {
embedded_sdmmc::SdCard::new(self.spi_device, self.delay)
}
}
pub const fn core_s3_card_present_from_aw9523_p0(value: u8) -> bool {
SdCardDetect::AW9523B_P0_4_ACTIVE_LOW.present_from_port_value(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn card_detect_is_active_low_on_p0_bit4() {
assert!(core_s3_card_present_from_aw9523_p0(0b1110_1111));
assert!(!core_s3_card_present_from_aw9523_p0(0b0001_0000));
}
#[test]
fn shared_spi_logic_starts_sd_miso_safe() {
let state = SharedSpiLogicState::SAFE_IDLE;
assert!(!state.lcd_cs_low);
assert!(!state.sd_cs_low);
assert_eq!(state.gpio35, SharedGpio35Role::SdMisoInput);
assert!(state.invariants_hold());
}
#[test]
fn lcd_transaction_forces_sd_deselected_and_restores_idle() {
let active = SharedSpiLogicState::SAFE_IDLE.begin_lcd();
assert_eq!(
active,
SharedSpiLogicState {
lcd_cs_low: true,
sd_cs_low: false,
gpio35: SharedGpio35Role::LcdDcOutput,
}
);
assert!(active.invariants_hold());
assert_eq!(active.safe_idle(), SharedSpiLogicState::SAFE_IDLE);
}
#[test]
fn sd_transaction_forces_lcd_deselected_and_restores_idle() {
let active = SharedSpiLogicState::SAFE_IDLE.begin_sd();
assert_eq!(
active,
SharedSpiLogicState {
lcd_cs_low: false,
sd_cs_low: true,
gpio35: SharedGpio35Role::SdMisoInput,
}
);
assert!(active.invariants_hold());
assert_eq!(active.safe_idle(), SharedSpiLogicState::SAFE_IDLE);
}
#[test]
fn repeated_lcd_sd_lcd_transitions_preserve_invariants() {
let mut state = SharedSpiLogicState::SAFE_IDLE;
for _ in 0..8 {
state = state.begin_lcd();
assert!(state.invariants_hold());
state = state.safe_idle().begin_sd();
assert!(state.invariants_hold());
state = state.safe_idle();
assert_eq!(state, SharedSpiLogicState::SAFE_IDLE);
}
}
#[test]
fn invalid_simultaneous_cs_state_is_rejected_by_model() {
let invalid = SharedSpiLogicState {
lcd_cs_low: true,
sd_cs_low: true,
gpio35: SharedGpio35Role::LcdDcOutput,
};
assert!(!invalid.invariants_hold());
}
#[test]
fn cmd0_stays_selected_until_r1_response() {
let state = SdCommandFramingState::IDLE.command_write(0);
assert_eq!(state.selected_command, Some(0));
let state = state.one_byte_poll(0xFF);
assert_eq!(state.selected_command, Some(0));
let state = state.one_byte_poll(0x01);
assert_eq!(state, SdCommandFramingState::IDLE);
}
#[test]
fn cmd17_closes_after_data_payload_and_crc() {
let state = SdCommandFramingState::IDLE
.command_write(17)
.one_byte_poll(0x00)
.one_byte_poll(0xFF)
.one_byte_poll(0xFE);
assert_eq!(state.selected_command, Some(17));
assert!(state.data_token_seen);
let state = state.transfer_in_place(512);
assert_eq!(state.selected_command, Some(17));
let state = state.transfer_in_place(2);
assert_eq!(state, SdCommandFramingState::IDLE);
}
#[test]
fn cmd8_and_cmd58_close_after_trailing_response() {
for command in [8, 58] {
let state = SdCommandFramingState::IDLE
.command_write(command)
.one_byte_poll(0x01)
.transfer_in_place(4);
assert_eq!(state, SdCommandFramingState::IDLE);
}
}
#[test]
fn cmd13_keeps_cs_for_second_status_byte() {
let state = SdCommandFramingState::IDLE
.command_write(13)
.one_byte_poll(0x00);
assert_eq!(state.selected_command, Some(13));
assert_eq!(state.trailing_single_response_bytes, 1);
let state = state.one_byte_poll(0x00);
assert_eq!(state, SdCommandFramingState::IDLE);
}
#[test]
fn cmd24_and_cmd25_remain_selected_for_data_phase() {
for command in [24, 25] {
let state = SdCommandFramingState::IDLE
.command_write(command)
.one_byte_poll(0x00);
assert_eq!(state.selected_command, Some(command));
}
}
}