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

Conversation

avsaase
Copy link

@avsaase avsaase commented Apr 10, 2025

Closes #184.

This only adds a blocking trait. Once #176 is merged I can rebase and add the async version. Or this can be merged first and the other PR can add the async version. Either is fine with me.

@thejpster
Copy link
Member

thejpster commented Apr 11, 2025

Seems reasonable enough, but I'd like to see some current users of embedded-sdmmc-rs port over to this style to check it's usable in practice.

@thejpster
Copy link
Member

There's also some overlap with #170, which is about SD Cards in general (i.e. using SD Card Host Controllers with the 4-bit bus) rather than this which is about SPI bus based SD Cards specifically.

avsaase and others added 2 commits April 11, 2025 19:44
Co-authored-by: Jonathan 'theJPster' Pallant <github@thejpster.org.uk>
@avsaase
Copy link
Author

avsaase commented Apr 11, 2025

Seems reasonable enough, but I'd like to see some current users of embedded-sdmmc-rs port over to this style to check it's usable in practice.

I'll also do some dogfooding by using it in my project. I'm mostly interested in the async API but I think I can do some tests with the block API.

There's also some overlap with #170, which is about SD Cards in general (i.e. using SD Card Host Controllers with the 4-bit bus) rather than this which is about SPI bus based SD Cards specifically.

How do you want to move forward with this? There hasn't been any recent activity on that PR. I think it makes sense to first fix the dummy byte issue.

@thejpster
Copy link
Member

If we're going to abstract the SD card transport it makes sense to do it in a way that both fixes the SPI issue and allows the use of SD Host Controllers.

I also have a mild preference for custom types over implementing traits on tuples. I think it makes it clearer what's going on.

Maybe something like:

  • struct VolumeManager uses a BlockDevice
  • SdCard implements BlockDevice using an SdCardTransport
  • RefCellSpiSdCardTransport implements SdCardTransport using a &RefCell
  • Stm32SdHostTransport also implements SdCardTransport
  • etc

@avsaase
Copy link
Author

avsaase commented Apr 12, 2025

Personally I don't have the need for a generic Transport trait. I understand why it would be useful to have but I think that's a separate thing from what this PR tries to a do. If @luojia65 is still interested in their PR I'm happy to discuss how we can combine efforts but I would like to limit the scope of this PR to just fixing the dummy byte issue.


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

@thejpster
Copy link
Member

Ok, fair enough.

@ValouBambou
Copy link
Contributor

ValouBambou commented Apr 30, 2025

Is this still planned? I wrote a patch to fix the build test checks and open a PR in @avsaase fork. I'm interested in trying to have the async API merged, so if you need help I will be happy to contribute!

Valentin Trophime and others added 2 commits April 30, 2025 17:26
@avsaase
Copy link
Author

avsaase commented Apr 30, 2025

Some (positive) life events happened so I couldn't work on this for a few weeks but I'm still planning on getting this merged. The last thing I did was to quickly whip up an async version of this PR to see how that would work. Unfortunately it did not work at all and consistently gave CRC errors. I haven't had the chance yet to see if this was a problem with this PR or my quick async version of it (it doesn't look like I pushed that code to github). In any case it's probably best to test the code from this PR first to see if the basic approach is correct. If you want to help with that that would be great.

@ValouBambou
Copy link
Contributor

I could test your 2 versions async and blocking next week if you want I would have access to real hardware (rp2040 + some SD cards). I will just need the async code. I'm not sure what is the best way to include the async code since the other PR is still waiting.

@avsaase avsaase changed the title Add SdCardDevice trait Add SdCardSpiDevice trait Apr 30, 2025
@avsaase
Copy link
Author

avsaase commented Apr 30, 2025

I pushed my async compatible version of this PR here: https://github.com/avsaase/embedded-sdmmc-rs/tree/bisync-sd-card-device.

@ValouBambou
Copy link
Contributor

When writing a simple example of just raw reads and writes, I struggle a bit to see how to set the frequency to a high value after the initialization of 400 kHz. We could do this in the previous API :

let mut config = embassy_rp::spi::Config::default();
config.frequency = 16_000_000;
sdcard.spi(|dev| dev.bus_mut().set_config(&config));

But with the new one I don't see a way of doing that, maybe we should add a bus_mut method to the SdCardSpiDevice trait in order to have a mutable borrow to the underlying bus since it's private.

@ValouBambou
Copy link
Contributor

ValouBambou commented May 5, 2025

I also tried with an ExclusiveDevice but I realized that it doesn't implement the trait in async. In order to implement it, we need to add the async feature from embedded-hal-bus (since ExclusiveDevice implement both async and blocking traits) and some await point should be enough.

@avsaase
Copy link
Author

avsaase commented May 5, 2025

I struggle a bit to see how to set the frequency to a high value after the initialization of 400 kHz

Yeah that's a tricky one. Providing mutable access to the bus does not allow using different SPI speeds for different SPI devices. In my own project that uses embassy I created a newtype and implemented SdCardSpiDevice for it. This implementation can then set the SPI speed to the correct value for the SD card device.

pub struct SdCardSpiDeviceWithConfig<'a, BUS: SetConfig, CS, M: RawMutex> {
    bus: &'a Mutex<M, BUS>,
    cs: CS,
    config: BUS::Config,
}

impl<'a, BUS: SetConfig, CS: OutputPin, M: RawMutex> SdCardSpiDeviceWithConfig<'a, BUS, CS, M> {
    pub fn new(bus: &'a Mutex<M, BUS>, cs: CS, config: BUS::Config) -> Self {
        Self { bus, cs, config }
    }
}

impl<BUS: SpiBus + SetConfig, CS: OutputPin, M: RawMutex> SdCardSpiDevice
    for SdCardSpiDeviceWithConfig<'_, BUS, CS, M>
{
    async fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), SdCardDeviceError> {
        let mut bus = self.bus.lock().await;
        bus.set_config(&self.config).map_err(|_| SdCardDeviceError::Spi)?;
        bus_transaction(&mut *bus, &mut self.cs, operations).await
    }

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

I just pushed a commit to this branch that makes bus_transaction and send_clock_pulses public to make this possible. The embedded-hal trait does not expose functions to set SPI speeds so I think this is the only way to do it.

@marcfir
Copy link

marcfir commented May 5, 2025

I pushed my async compatible version of this PR here: https://github.com/avsaase/embedded-sdmmc-rs/tree/bisync-sd-card-device.

I tested this version on a ESP32S3 with embassy

    let sclk = peripherals.GPIO12;
    let miso = peripherals.GPIO11;
    let mosi = peripherals.GPIO13;
    let cs = peripherals.GPIO10;

    let mut sd_cs = Output::new(cs, Level::High);
    let dma_channel = peripherals.DMA_CH0;

    let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(32000);
    let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap();
    let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap();

    let mut spi_bus = Spi::new(
        peripherals.SPI2,
        spi::master::Config::default()
            .with_frequency(400.kHz())
            .with_mode(spi::Mode::_0),
    )
    .unwrap()
    .with_sck(sclk)
    .with_mosi(mosi)
    .with_miso(miso)
    .with_dma(dma_channel)
    .with_buffers(dma_rx_buf, dma_tx_buf)
    .into_async();
    let spi_dev = RefCellSdCardDevice::new(spi_bus, sd_cs);
    let sdcard = SdCard::new(spi_dev, Delay);
    sdcard.init_card().await;
    let mut volume_mgr = VolumeManager::new(sdcard, DummyTimesource::default());
    static VOL_MGR: StaticCell<
        VolumeManager<
            SdCard<RefCellSdCardDevice<'_, SpiDmaBus<'_, esp_hal::Async>, Output<'_>>, Delay>,
            DummyTimesource,
        >,
    > = StaticCell::new();
    let volume_mgr = VOL_MGR.init(volume_mgr);
    let volume0 = volume_mgr.open_volume(VolumeIdx(0)).await.unwrap();

It crashes on this let volume0 = volume_mgr.open_volume(VolumeIdx(0)).await.unwrap(); line with

DEBUG - Creating new embedded-sdmmc::VolumeManager
TRACE - Reading partition table
DEBUG - Read 1 blocks @ 0
DEBUG - acquiring card with opts: AcquireOpts { use_crc: true, acquire_retries: 50 }
TRACE - Reset card..
TRACE - Enter SPI mode, attempt: 1..
WARN - Got response: 3f, trying again..
TRACE - Enter SPI mode, attempt: 2..
DEBUG - Enable CRC: true
DEBUG - Card version: SDHC
ERROR - read: ReadError


====================== PANIC ======================
panicked at soundcube\src\main.rs:229:62:
called `Result::unwrap()` on an `Err` value: DeviceError(ReadError)

When analyzing the bus I see CMD17 as the last command. Is the CS line correct?
image

I think https://github.com/avsaase/embedded-sdmmc-rs/blob/08fc93c092da0c030e9618264c805d0fedea669b/src/inner/sdcard/mod.rs#L371-L373 is where the error is thrown?

@ValouBambou
Copy link
Contributor

ValouBambou commented May 9, 2025

Maybe I get something wrong, but I can't see why just exposing a method to get a mutable reference to the underlying SpiBus in RefCellSdCardDevice or in the trait SpiCardSpiDevicedirectly is not an option. It's less boilerplate than your custom type and just depend on if the underlying type supports to change the frequency or not. I tried to add such method in RefCellSdCardDevice and it worked well on my clone.

impl<'a, BUS, CS> RefCellSdCardDevice<'a, BUS, CS> {
    /// Try stuff
    pub fn foo(&self) -> RefMut<'_, BUS> {
        self.bus.borrow_mut()
    }
}

However, I also confirmed running read errors on a different target (rp2040).
I didn't have this issue with an older commit (I will try to bisect by hand since API are changed with different commits). Here is the main code for reproduction :

#![no_std]
#![no_main]
#![feature(impl_trait_in_assoc_type)]

use core::cell::RefCell;

use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::gpio;
use embassy_rp::spi::Spi;
use embedded_sdmmc::asynchronous::{SdCard, sdcard::device::RefCellSdCardDevice};
use gpio::{Level, Output};
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    // BEGIN stuff with real peripherals
    let p = embassy_rp::init(Default::default());
    let mut config = embassy_rp::spi::Config::default();
    config.frequency = 400_000;
    let spi_bus = Spi::new(
        p.SPI1, p.PIN_10, p.PIN_11, p.PIN_12, p.DMA_CH0, p.DMA_CH1, config,
    );
    let cs = Output::new(p.PIN_13, Level::High);
    // END stuff with real peripherals
    let delay = embassy_time::Delay;
    let bus = RefCell::new(spi_bus);
    let spi_device = RefCellSdCardDevice::new(&bus, cs);
    let sdcard = SdCard::new(spi_device, delay);
    sdcard.init_card().await.unwrap();
    info!("init is ok");
    let mut config = embassy_rp::spi::Config::default();
    config.frequency = 16_000_000;
    sdcard.spi(|dev| dev.foo().set_config(&config));
    info!("freq is ok");
    info!("Card size is {} bytes", sdcard.num_bytes().await.unwrap()); // it panics here!
}

EDIT: I found the bad commit is this one 835b2e4 (i.e the last one pushed on the async fork)
EDIT 2: I got the same error as @marcfir it's thrown at the same line (status check in read_data). My card was of type SDHC if it can help.

@avsaase
Copy link
Author

avsaase commented May 9, 2025

Maybe I get something wrong, but I can't see why just exposing a method to get a mutable reference to the underlying SpiBus in RefCellSdCardDevice or in the trait SpiCardSpiDevicedirectly is not an option. It's less boilerplate than your custom type and just depend on if the underlying type supports to change the frequency or not. I tried to add such method in RefCellSdCardDevice and it worked well on my clone.

How would you use two devices with differett speeds on the same bus? You need a way to change the speed for every bus transaction and I think the only way to do that properly is in the SdCardSpiDevice trait impl. This is similar to what embassy does with their SpiDeviceWithConfig type.

EDIT: I found the bad commit is this one 835b2e4 (i.e the last one pushed on the async fork)

That's strange. I and several of my users are using that exact commit without any problem for weeks now.

@ValouBambou
Copy link
Contributor

How would you use two devices with differett speeds on the same bus? You need a way to change the speed for every bus transaction and I think the only way to do that properly is in the SdCardSpiDevice trait impl. This is similar to what embassy does with their SpiDeviceWithConfig type.

Yes, and you can call the set_config method directly, like in my example, by just adding a tiny get_inner method. I don't see why you need to reimplement the trait "only for that". If you need to set the frequency before each transaction you will reimplement it for your custom type to avoid calling get_inner every time, but for using it once it's easier with just exposing the reference to the concrete type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sending 0xFF byte with CS deasserted when used on shared bus
4 participants