Skip to content

Add SdCardDevice 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 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion 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 @@ -30,6 +32,8 @@ hex-literal = "0.4.1"
sha2 = "0.10"

[features]
default = ["log"]
default = ["log", "embassy-sync-06", "embedded-hal-bus-03"]
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
6 changes: 3 additions & 3 deletions examples/readme_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use core::cell::RefCell;

use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp};
use embedded_sdmmc::{sdcard::RefCellSdCardDevice, Error, SdCardError, TimeSource, Timestamp};

pub struct DummyCsPin;

Expand Down Expand Up @@ -117,13 +117,13 @@ 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 spi_device = RefCellSdCardDevice::new(&spi_bus, DummyCsPin);
let sdcard = SdCard::new(spi_device, 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::SdCardSpiDevice;

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

mod volume_mgr;
#[doc(inline)]
pub use volume_mgr::VolumeManager;
Expand Down
262 changes: 262 additions & 0 deletions src/sdcard/device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//! SD card SPI 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 SdCardSpiDevice {
/// 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,
}

/// A wrapper around a SPI bus and a CS pin, using a `RefCell`.
///
/// This allows sharing the bus within the same thread.
pub struct RefCellSdCardDevice<'a, BUS, CS> {
bus: &'a RefCell<BUS>,
cs: CS,
}

impl<'a, BUS, CS> RefCellSdCardDevice<'a, BUS, CS> {
/// Create a new `RefCellSdCardDevice`.
pub fn new(bus: &'a RefCell<BUS>, cs: CS) -> Self {
Self { bus, cs }
}
}

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

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

#[cfg(feature = "embassy-sync-06")]
mod embassy_sync_06 {
use core::cell::RefCell;

use ::embassy_sync_06::blocking_mutex;

use super::*;

/// A wrapper around a SPI bus and a CS pin, using an `embassy-sync` blocking mutex.
///
/// This allows sharing the bus with according to the `embassy-sync` mutex model.
/// See [`blocking_mutex::Mutex`] for more details.

pub struct EmbassyMutexSdCardDevice<'a, BUS, CS, M> {
bus: &'a blocking_mutex::Mutex<M, RefCell<BUS>>,
cs: CS,
}

impl<'a, BUS, CS, M> EmbassyMutexSdCardDevice<'a, BUS, CS, M> {
/// Create a new `EmbassyMutexSdCardDevice`.
pub fn new(bus: &'a blocking_mutex::Mutex<M, RefCell<BUS>>, cs: CS) -> Self {
Self { bus, cs }
}
}

impl<CS, BUS, M> SdCardSpiDevice for EmbassyMutexSdCardDevice<'_, BUS, CS, M>
where
CS: OutputPin,
BUS: SpiBus,
M: blocking_mutex::raw::RawMutex,
{
fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), SdCardDeviceError> {
self.bus.lock(|bus| {
let mut bus = bus.borrow_mut();
bus_transaction(&mut *bus, &mut self.cs, operations)
})
}

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

#[cfg(feature = "embassy-sync-06")]
pub use embassy_sync_06::*;

#[cfg(feature = "embedded-hal-bus-03")]
mod embedded_hal_bus_03 {
use ::embedded_hal_bus_03::spi::ExclusiveDevice;
use embedded_hal::spi::SpiDevice;

use super::*;

// `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.

impl<CS, BUS, D> SdCardSpiDevice for 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 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 likely it is already high so this is probably 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(())
}
}
}

/// Perform a transaction against the device. This sends a dummy `0xFF` byte to the device after
/// deasserting the CS pin but before unlocking the bus.
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(())
}

/// Send 80 clock pulses to the device with CS deasserted. This is needed to initialize the SD card.
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();

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

Ok(())
}
Loading
Loading