diff --git a/Cargo.toml b/Cargo.toml index 49b1317..b3db3ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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} @@ -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"] diff --git a/README.md b/README.md index 9bd06b1..5748100 100644 --- a/README.md +++ b/README.md @@ -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 `SdCardSpiDevice` 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 diff --git a/examples/readme_test.rs b/examples/readme_test.rs index 0d63d80..a4aeb69 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -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; @@ -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. diff --git a/src/lib.rs b/src/lib.rs index c6af4e9..6343783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, SdCardSpiDevice}; //! //! fn example(spi: S, delay: D, ts: T) -> Result<(), Error> //! where -//! S: embedded_hal::spi::SpiDevice, +//! S: SdCardSpiDevice, //! D: embedded_hal::delay::DelayNs, //! T: TimeSource, //! { @@ -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; diff --git a/src/sdcard/device.rs b/src/sdcard/device.rs new file mode 100644 index 0000000..ddc0980 --- /dev/null +++ b/src/sdcard/device.rs @@ -0,0 +1,265 @@ +//! 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: [`SdCardSpiDevice::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: [`SdCardSpiDevice::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: [`SdCardSpiDevice::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: [`SdCardSpiDevice::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, + cs: CS, +} + +impl<'a, BUS, CS> RefCellSdCardDevice<'a, BUS, CS> { + /// Create a new `RefCellSdCardDevice`. + pub fn new(bus: &'a RefCell, cs: CS) -> Self { + Self { bus, cs } + } +} + +impl 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>, + cs: CS, + } + + impl<'a, BUS, CS, M> EmbassyMutexSdCardDevice<'a, BUS, CS, M> { + /// Create a new `EmbassyMutexSdCardDevice`. + pub fn new(bus: &'a blocking_mutex::Mutex>, cs: CS) -> Self { + Self { bus, cs } + } + } + + impl 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 SdCardSpiDevice for ExclusiveDevice + where + BUS: SpiBus, + CS: OutputPin, + D: embedded_hal::delay::DelayNs, + { + fn transaction( + &mut self, + operations: &mut [Operation<'_, u8>], + ) -> Result<(), SdCardDeviceError> { + ::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. This function can be used to implement [`SdCardSpiDevice`] for your own device types. +pub fn bus_transaction( + 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. +pub fn send_clock_pulses(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(()) +} diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 553791f..10beb75 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -3,6 +3,7 @@ //! This is currently optimised for readability and debugability, not //! performance. +mod device; pub mod proto; use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx}; @@ -14,6 +15,7 @@ use proto::*; // **************************************************************************** use crate::{debug, warn}; +pub use device::*; // **************************************************************************** // Types and Implementations @@ -37,7 +39,7 @@ use crate::{debug, warn}; /// [`SpiDevice`]: embedded_hal::spi::SpiDevice pub struct SdCard where - SPI: embedded_hal::spi::SpiDevice, + SPI: SdCardSpiDevice, DELAYER: embedded_hal::delay::DelayNs, { inner: RefCell>, @@ -45,7 +47,7 @@ where impl SdCard where - SPI: embedded_hal::spi::SpiDevice, + SPI: SdCardSpiDevice, DELAYER: embedded_hal::delay::DelayNs, { /// Create a new SD/MMC Card driver using a raw SPI interface. @@ -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 @@ -154,7 +168,7 @@ where impl BlockDevice for SdCard where - SPI: embedded_hal::spi::SpiDevice, + SPI: SdCardSpiDevice, DELAYER: embedded_hal::delay::DelayNs, { type Error = Error; @@ -194,7 +208,7 @@ where /// All the APIs required `&mut self`. struct SdCardInner where - SPI: embedded_hal::spi::SpiDevice, + SPI: SdCardSpiDevice, DELAYER: embedded_hal::delay::DelayNs, { spi: SPI, @@ -205,7 +219,7 @@ where impl SdCardInner where - SPI: embedded_hal::spi::SpiDevice, + SPI: SdCardSpiDevice, DELAYER: embedded_hal::delay::DelayNs, { /// Read one or more blocks, starting at the given block index. @@ -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.