Skip to content

Add SdCardSpiDevice trait #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ rust-version = "1.76"
[dependencies]
byteorder = {version = "1", default-features = false}
defmt = {version = "0.3", optional = true}
embassy-sync-06 = { package = "embassy-sync", version = "0.6.2", optional = true }
embedded-hal = "1.0.0"
embedded-hal-bus-03 = { package = "embedded-hal-bus", version = "0.3.0", optional = true }
embedded-io = "0.6.1"
heapless = "^0.8"
log = {version = "0.4", default-features = false, optional = true}
Expand All @@ -32,4 +34,6 @@ sha2 = "0.10"
[features]
default = ["log"]
defmt-log = ["dep:defmt"]
embassy-sync-06 = ["dep:embassy-sync-06"]
embedded-hal-bus-03 = ["dep:embedded-hal-bus-03"]
log = ["dep:log"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ designed for readability and simplicity over performance.

You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI.

To accommodate specific requirements when using SD cards on a shared SPI bus, this crate uses a bespoke `SdCardDevice` trait instead of the more commmon `embedded_hal::spi::SpiDevice` trait. Implementations for several types that wrap a `embedded-hal::spi::SpiBus` implementation are provided in the `sd_card` module. Some are guarded behind cargo features:

- `embedded-hal-bus-03`: adds support for `embedded-hal-bus::spi::ExclusiveDevice`. This is probably the easiest way to use this crate when the SD card is the only device on the bus.
- `embassy-sync-06`: adds support for using a blocking mutex from the `embassy-sync` crate. This allows sharing the bus between multiple tasks.

```rust
use embedded_sdmmc::{SdCard, VolumeManager, Mode, VolumeIdx};
// Build an SD Card interface out of an SPI device, a chip-select pin and the delay object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example needs to be kept in sync with the copy in readme-test.

Expand Down
3 changes: 1 addition & 2 deletions examples/readme_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,12 @@ fn main() -> Result<(), MyError> {
// BEGIN Fake stuff that will be replaced with real peripherals
let spi_bus = RefCell::new(FakeSpiBus());
let delay = FakeDelayer();
let sdmmc_spi = embedded_hal_bus::spi::RefCellDevice::new(&spi_bus, DummyCsPin, delay).unwrap();
let time_source = FakeTimesource();
// END Fake stuff that will be replaced with real peripherals

use embedded_sdmmc::{Mode, SdCard, VolumeIdx, VolumeManager};
// Build an SD Card interface out of an SPI device, a chip-select pin and the delay object
let sdcard = SdCard::new(sdmmc_spi, delay);
let sdcard = SdCard::new((&spi_bus, DummyCsPin), delay);
// Get the card size (this also triggers card initialisation because it's not been done yet)
println!("Card size is {} bytes", sdcard.num_bytes()?);
// Now let's look for volumes (also known as partitions) on our block device.
Expand Down
10 changes: 8 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
//! suitable for reading SD and SDHC cards over SPI.
//!
//! ```rust
//! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager};
//! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager, SdCardDevice};
//!
//! fn example<S, D, T>(spi: S, delay: D, ts: T) -> Result<(), Error<SdCardError>>
//! where
//! S: embedded_hal::spi::SpiDevice,
//! S: SdCardDevice,
//! D: embedded_hal::delay::DelayNs,
//! T: TimeSource,
//! {
Expand Down Expand Up @@ -116,6 +116,12 @@ pub use crate::sdcard::Error as SdCardError;
#[doc(inline)]
pub use crate::sdcard::SdCard;

#[doc(inline)]
pub use crate::sdcard::SdCardDevice;

#[doc(inline)]
pub use crate::sdcard::SdCardDeviceError;

mod volume_mgr;
#[doc(inline)]
pub use volume_mgr::VolumeManager;
Expand Down
31 changes: 26 additions & 5 deletions src/sdcard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! performance.

pub mod proto;
mod sd_card_device;

use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx};
use core::cell::RefCell;
Expand All @@ -14,6 +15,7 @@ use proto::*;
// ****************************************************************************

use crate::{debug, warn};
pub use sd_card_device::*;

// ****************************************************************************
// Types and Implementations
Expand All @@ -37,15 +39,15 @@ use crate::{debug, warn};
/// [`SpiDevice`]: embedded_hal::spi::SpiDevice
pub struct SdCard<SPI, DELAYER>
where
SPI: embedded_hal::spi::SpiDevice<u8>,
SPI: SdCardDevice,
DELAYER: embedded_hal::delay::DelayNs,
{
inner: RefCell<SdCardInner<SPI, DELAYER>>,
}

impl<SPI, DELAYER> SdCard<SPI, DELAYER>
where
SPI: embedded_hal::spi::SpiDevice<u8>,
SPI: SdCardDevice,
DELAYER: embedded_hal::delay::DelayNs,
{
/// Create a new SD/MMC Card driver using a raw SPI interface.
Expand Down Expand Up @@ -130,6 +132,18 @@ where
inner.card_type
}

/// Initialize the SD card.
///
/// This must be called before performing any operations on the card, with
/// SPI frequency of 100 to 400 KHz. After this method returns
/// successfully, the SPI frequency can be increased to the maximum
/// supported by the card.
pub fn init_card(&self) -> Result<(), Error> {
let mut inner = self.inner.borrow_mut();
inner.init()?;
Ok(())
}

/// Tell the driver the card has been initialised.
///
/// This is here in case you were previously using the SD Card, and then a
Expand All @@ -154,7 +168,7 @@ where

impl<SPI, DELAYER> BlockDevice for SdCard<SPI, DELAYER>
where
SPI: embedded_hal::spi::SpiDevice<u8>,
SPI: SdCardDevice,
DELAYER: embedded_hal::delay::DelayNs,
{
type Error = Error;
Expand Down Expand Up @@ -194,7 +208,7 @@ where
/// All the APIs required `&mut self`.
struct SdCardInner<SPI, DELAYER>
where
SPI: embedded_hal::spi::SpiDevice<u8>,
SPI: SdCardDevice,
DELAYER: embedded_hal::delay::DelayNs,
{
spi: SPI,
Expand All @@ -205,7 +219,7 @@ where

impl<SPI, DELAYER> SdCardInner<SPI, DELAYER>
where
SPI: embedded_hal::spi::SpiDevice<u8>,
SPI: SdCardDevice,
DELAYER: embedded_hal::delay::DelayNs,
{
/// Read one or more blocks, starting at the given block index.
Expand Down Expand Up @@ -561,6 +575,13 @@ where
}
Ok(())
}

fn init(&mut self) -> Result<(), Error> {
self.spi
.send_clock_pulses()
.map_err(|_e| Error::Transport)?;
Ok(())
}
}

/// Options for acquiring the card.
Expand Down
213 changes: 213 additions & 0 deletions src/sdcard/sd_card_device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! SD card device trait and provided implementations.

use core::cell::RefCell;

use embedded_hal::{
digital::OutputPin,
spi::{Operation, SpiBus},
};

/// Trait for SD cards connected via SPI.
pub trait SdCardDevice {
/// Perform a transaction against the device.
///
/// This is similar to [`embedded_hal::spi::SpiDevice::transaction`], except that this sends
/// a dummy `0xFF` byte to the device after deasserting the CS pin but before unlocking the
/// bus.
fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError>;

/// Send 80 clock pulses to the device with CS deasserted.
fn send_clock_pulses(&mut self) -> Result<(), SdCardDeviceError>;

/// Do a read within a transaction.
///
/// This is a convenience method equivalent to `device.transaction(&mut [Operation::Read(buf)])`.
///
/// See also: [`SdCardDevice::transaction`], [`embedded_hal::spi::SpiBus::read`]
#[inline]
fn read(&mut self, buf: &mut [u8]) -> Result<(), SdCardDeviceError> {
self.transaction(&mut [Operation::Read(buf)])
}

/// Do a write within a transaction.
///
/// This is a convenience method equivalent to `device.transaction(&mut [Operation::Write(buf)])`.
///
/// See also: [`SdCardDevice::transaction`], [`embedded_hal::spi::SpiBus::write`]
#[inline]
fn write(&mut self, buf: &[u8]) -> Result<(), SdCardDeviceError> {
self.transaction(&mut [Operation::Write(buf)])
}

/// Do a transfer within a transaction.
///
/// This is a convenience method equivalent to `device.transaction(&mut [Operation::Transfer(read, write)]`.
///
/// See also: [`SdCardDevice::transaction`], [`embedded_hal::spi::SpiBus::transfer`]
#[inline]
fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SdCardDeviceError> {
self.transaction(&mut [Operation::Transfer(read, write)])
}

/// Do an in-place transfer within a transaction.
///
/// This is a convenience method equivalent to `device.transaction(&mut [Operation::TransferInPlace(buf)]`.
///
/// See also: [`SdCardDevice::transaction`], [`embedded_hal::spi::SpiBus::transfer_in_place`]
#[inline]
fn transfer_in_place(&mut self, buf: &mut [u8]) -> Result<(), SdCardDeviceError> {
self.transaction(&mut [Operation::TransferInPlace(buf)])
}
}

/// Errors that can occur when using the [`SdCardDevice`].
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
#[non_exhaustive]
pub enum SdCardDeviceError {
/// An operation on the inner SPI bus failed.
Spi,
/// Setting the value of the Chip Select (CS) pin failed.
Cs,
}

impl<BUS, CS> SdCardDevice for (&RefCell<BUS>, CS)
where
BUS: SpiBus,
CS: OutputPin,
{
fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError> {
let (bus, cs) = self;
let mut bus = bus.borrow_mut();
bus_transaction(&mut *bus, cs, operations)
}

fn send_clock_pulses(&mut self) -> Result<(), SdCardDeviceError> {
let (bus, cs) = self;
let mut bus = bus.borrow_mut();
send_clock_pulses(&mut *bus, cs)
}
}

#[cfg(feature = "embassy-sync-06")]
impl<CS, BUS, M> SdCardDevice for (&embassy_sync_06::blocking_mutex::Mutex<M, RefCell<BUS>>, CS)
where
CS: OutputPin,
BUS: SpiBus,
M: embassy_sync_06::blocking_mutex::raw::RawMutex,
{
fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError> {
let (bus, cs) = self;
bus.lock(|bus| {
let mut bus = bus.borrow_mut();
bus_transaction(&mut *bus, cs, operations)
})
}

fn send_clock_pulses(&mut self) -> Result<(), SdCardDeviceError> {
let (bus, cs) = self;
bus.lock(|bus| {
let mut bus = bus.borrow_mut();
send_clock_pulses(&mut *bus, cs)
})
}
}

// `ExclusiveDevice` represents exclusive access to the bus so there's no need to send the dummy
// byte after deasserting the CS pin. We can delegate the implementation to the `embedded_hal` trait.
#[cfg(feature = "embedded-hal-bus-03")]
impl<CS, BUS, D> SdCardDevice for embedded_hal_bus_03::spi::ExclusiveDevice<BUS, CS, D>
where
BUS: SpiBus,
CS: OutputPin,
D: embedded_hal::delay::DelayNs,
{
fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError> {
<Self as embedded_hal::spi::SpiDevice>::transaction(self, operations)
.map_err(|_| SdCardDeviceError::Spi)
}

fn send_clock_pulses(&mut self) -> Result<(), SdCardDeviceError> {
let bus = self.bus_mut();

// There's no way to access the CS pin here so we can't set it high. Most like it already high so this is probbaly fine(?)

let send_res = bus.write(&[0xFF; 10]);

// On failure, it's important to still flush.
let flush_res = bus.flush().map_err(|_| SdCardDeviceError::Spi);

send_res.map_err(|_| SdCardDeviceError::Spi)?;
flush_res.map_err(|_| SdCardDeviceError::Spi)?;
Ok(())
}
}

fn bus_transaction<BUS, CS>(
bus: &mut BUS,
cs: &mut CS,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError>
where
BUS: SpiBus,
CS: OutputPin,
{
cs.set_low().map_err(|_| SdCardDeviceError::Cs)?;

let op_res = operations.iter_mut().try_for_each(|op| match op {
Operation::Read(buf) => bus.read(buf),
Operation::Write(buf) => bus.write(buf),
Operation::Transfer(read, write) => bus.transfer(read, write),
Operation::TransferInPlace(buf) => bus.transfer_in_place(buf),
Operation::DelayNs(_) => {
// We don't use delays in SPI transations in this crate so it fine to panic here.
panic!("Tried to use a delay in a SPI transaction. This is a bug in embedded-sdmmc.")
}
});

// On failure, it's important to still flush and deassert CS.
let flush_res = bus.flush();
let cs_res = cs.set_high();

op_res.map_err(|_| SdCardDeviceError::Spi)?;
flush_res.map_err(|_| SdCardDeviceError::Spi)?;
cs_res.map_err(|_| SdCardDeviceError::Cs)?;

// Write the dummy byte
let dummy_res = bus.write(&[0xFF]);
let flush_res = bus.flush();

dummy_res.map_err(|_| SdCardDeviceError::Spi)?;
flush_res.map_err(|_| SdCardDeviceError::Spi)?;

Ok(())
}

fn send_clock_pulses<BUS, CS>(bus: &mut BUS, cs: &mut CS) -> Result<(), SdCardDeviceError>
where
BUS: SpiBus,
CS: OutputPin,
{
cs.set_high().map_err(|_| SdCardDeviceError::Cs)?;
let send_res = bus.write(&[0xFF; 10]);

// On failure, it's important to still flush.
let flush_res = bus.flush().map_err(|_| SdCardDeviceError::Spi);

send_res.map_err(|_| SdCardDeviceError::Spi)?;
flush_res.map_err(|_| SdCardDeviceError::Spi)?;

Ok(())
}