diff --git a/boards/rp-pico/Cargo.toml b/boards/rp-pico/Cargo.toml index 0294470..13e636d 100644 --- a/boards/rp-pico/Cargo.toml +++ b/boards/rp-pico/Cargo.toml @@ -28,6 +28,10 @@ cortex-m-rtic = "0.6.0-rc.4" nb = "1.0" i2c-pio = { git = "https://github.com/ithinuel/i2c-pio-rs", rev = "df06e4ac94a5b2c985d6a9426dc4cc9be0d535c0" } heapless = "0.7.9" +embedded-sdmmc = { git = "https://github.com/rust-embedded-community/embedded-sdmmc-rs.git" } + +defmt = "0.2.0" +defmt-rtt = "0.2.0" [features] default = ["boot2", "rt"] diff --git a/boards/rp-pico/README.md b/boards/rp-pico/README.md index c406ee6..de24f3d 100644 --- a/boards/rp-pico/README.md +++ b/boards/rp-pico/README.md @@ -94,6 +94,12 @@ interrupts when USB data arrives. Demonstrates emulating a USB Human Input Device (HID) Mouse. The mouse cursor will jiggle up and down. +### [pico_spi_sd_card](./examples/pico_spi_sd_card.rs) + +Example that shows how to use the +[embedded_sdmmc crate](https://github.com/rust-embedded-community/embedded-sdmmc-rs) +with the Raspberry Pi Pico. + ## Contributing Contributions are what make the open source community such an amazing place to diff --git a/boards/rp-pico/examples/pico_spi_sd_card.rs b/boards/rp-pico/examples/pico_spi_sd_card.rs new file mode 100644 index 0000000..3410dec --- /dev/null +++ b/boards/rp-pico/examples/pico_spi_sd_card.rs @@ -0,0 +1,381 @@ +//! # Pico SD Card Example +//! +//! Reads and writes a file from/to the SD Card that is formatted in FAT32. +//! This example uses the SPI0 device of the Raspberry Pi Pico on the +//! pins 4,5,6 and 7. If you don't use an external 3.3V power source, +//! you can connect the +3.3V output on pin 36 to the SD card. +//! +//! SD Cards up to 2TB are supported by the `embedded_sdmmc` crate. +//! I've tested this with a 64GB micro SD card. +//! +//! You need to format the card with an regular old FAT32 filesystem +//! and also make sure the first partition has the right type. This is how your +//! `fdisk` output should look like: +//! +//! ```text +//! fdisk /dev/sdj +//! +//! Welcome to fdisk (util-linux 2.34). +//! Changes will remain in memory only, until you decide to write them. +//! Be careful before using the write command. +//! +//! Command (m for help): Disk /dev/sdj: +//! 59,49 GiB, 63864569856 bytes, 124735488 sectors +//! Disk model: SD/MMC/MS/MSPRO +//! Units: sectors of 1 * 512 = 512 bytes +//! Sector size (logical/physical): 512 bytes / 512 bytes +//! I/O size (minimum/optimal): 512 bytes / 512 bytes +//! Disklabel type: dos +//! Disk identifier: 0x00000000 +//! +//! Device Boot Start End Sectors Size Id Type +//! /dev/sdj1 2048 124735487 124733440 59,5G c W95 FAT32 (LBA) +//! ``` +//! +//! The important bit here is the _Type_ with `W95 FAT32 (LBA)`, other types +//! are rejected by the `embedded_sdmmc` filesystem implementation. +//! +//! Formatting the partition can be done using `mkfs.fat`: +//! +//! $ mkfs.fat /dev/sdj1 +//! +//! In the following ASCII art the SD card is also connected to 5 strong pull up +//! resistors. I've found varying values for these, from 50kOhm, 10kOhm +//! down to 5kOhm. +//! Stronger pull up resistors will eat more amperes, but also allow faster +//! data rates. +//! +//! ```text +//! +3.3V +//! Pull Ups ->|||| +//! 4x[5kOhm] +//! ||| \ +//! _______________ ||| \ +//! | DAT2/NC 9\---o|| \ _|USB|_ +//! | S DAT3/CS 1|---o+----+------SS--\ |1 R 40| +//! | D CMD/DI 2|----o----+-----MOSI-+-\ |2 P 39| +//! | VSS1 3|-- GND | | | GND-|3 38|- GND +//! | C VDD 4|-- +3.3V | /--SCK--+-+----SPI0 SCK-|4 P 37| +//! | A CLK/SCK 5|---------+-/ | \----SPI0 TX--|5 I 36|- +3.3V +//! | R VSS2 6|-- GND | /--MISO-+------SPI0 RX--|6 C | +//! | D DAT0/DO 7|---------o-/ \------SPI0 CSn-|7 O | +//! | DAT1/IRQ 8|-[5k]- +3.3V | | +//! """""""""""""""" | | +//! | | +//! ......... +//! |20 21| +//! """"""" +//! Symbols: +//! - (+) crossing lines, not connected +//! - (o) connected lines +//! ``` +//! +//! The example can either be used with a probe to receive debug output +//! and also the LED is used as status output. There are different blinking +//! patterns. +//! +//! For every successful stage in the example the LED will blink long once. +//! If everything is successful (9 long blink signals), the example will go +//! into a loop and either blink in a _"short long"_ or _"short short long"_ pattern. +//! +//! If there are 5 different error patterns, all with short blinking pulses: +//! +//! - **2 short blink (in a loop)**: Block device could not be aquired, either +//! no SD card is present or some electrical problem. +//! - **3 short blink (in a loop)**: Card size could not be retrieved. +//! - **4 short blink (in a loop)**: Error getting volume/partition 0. +//! - **5 short blink (in a loop)**: Error opening root directory. +//! - **6 short blink (in a loop)**: Could not open file 'O.TST'. +//! +//! See the `Cargo.toml` file for Copyright and licence details. + +#![no_std] +#![no_main] + +// The macro for our start-up function +use cortex_m_rt::entry; + +// info!() and error!() macros for printing information to the debug output +use defmt::*; +use defmt_rtt as _; + +// Ensure we halt the program on panic (if we don't mention this crate it won't +// be linked) +use panic_halt as _; + +// Pull in any important traits +use rp_pico::hal::prelude::*; + +// Embed the `Hz` function/trait: +use embedded_time::rate::*; + +// A shorter alias for the Peripheral Access Crate, which provides low-level +// register access +use rp_pico::hal::pac; + +// Import the SPI abstraction: +use rp_pico::hal::spi; + +// Import the GPIO abstraction: +use rp_pico::hal::gpio; + +// A shorter alias for the Hardware Abstraction Layer, which provides +// higher-level drivers. +use rp_pico::hal; + +// Link in the embedded_sdmmc crate. +// The `SdMmcSpi` is used for block level access to the card. +// And the `Controller` gives access to the FAT filesystem functions. +use embedded_sdmmc::{Controller, SdMmcSpi, TimeSource, Timestamp, VolumeIdx}; + +// Get the file open mode enum: +use embedded_sdmmc::filesystem::Mode; + +/// A dummy timesource, which is mostly important for creating files. +#[derive(Default)] +pub struct DummyTimesource(); + +impl TimeSource for DummyTimesource { + // In theory you could use the RTC of the rp2040 here, if you had + // any external time synchronizing device. + fn get_timestamp(&self) -> Timestamp { + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } +} + +// Setup some blinking codes: +const BLINK_OK_LONG: [u8; 1] = [8u8]; +const BLINK_OK_SHORT_LONG: [u8; 4] = [1u8, 0u8, 6u8, 0u8]; +const BLINK_OK_SHORT_SHORT_LONG: [u8; 6] = [1u8, 0u8, 1u8, 0u8, 6u8, 0u8]; +const BLINK_ERR_2_SHORT: [u8; 4] = [1u8, 0u8, 1u8, 0u8]; +const BLINK_ERR_3_SHORT: [u8; 6] = [1u8, 0u8, 1u8, 0u8, 1u8, 0u8]; +const BLINK_ERR_4_SHORT: [u8; 8] = [1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8]; +const BLINK_ERR_5_SHORT: [u8; 10] = [1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8]; +const BLINK_ERR_6_SHORT: [u8; 12] = [1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8, 1u8, 0u8]; + +fn blink_signals( + pin: &mut dyn embedded_hal::digital::v2::OutputPin, + delay: &mut cortex_m::delay::Delay, + sig: &[u8], +) { + for bit in sig { + if *bit != 0 { + pin.set_high().unwrap(); + } else { + pin.set_low().unwrap(); + } + + let length = if *bit > 0 { *bit } else { 1 }; + + for _ in 0..length { + delay.delay_ms(100); + } + } + + pin.set_low().unwrap(); + + delay.delay_ms(500); +} + +fn blink_signals_loop( + pin: &mut dyn embedded_hal::digital::v2::OutputPin, + delay: &mut cortex_m::delay::Delay, + sig: &[u8], +) -> ! { + loop { + blink_signals(pin, delay, sig); + delay.delay_ms(1000); + } +} + +#[entry] +fn main() -> ! { + info!("Program start"); + + // Grab our singleton objects + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + // + // The default is to generate a 125 MHz system clock + let clocks = hal::clocks::init_clocks_and_plls( + rp_pico::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + + // Set the pins up according to their function on this particular board + let pins = rp_pico::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Set the LED to be an output + let mut led_pin = pins.led.into_push_pull_output(); + + // Setup a delay for the LED blink signals: + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer()); + + // These are implicitly used by the spi driver if they are in the correct mode + let _spi_sclk = pins.gpio2.into_mode::(); + let _spi_mosi = pins.gpio3.into_mode::(); + let _spi_miso = pins.gpio4.into_mode::(); + let spi_cs = pins.gpio5.into_push_pull_output(); + + // Create an SPI driver instance for the SPI0 device + let spi = spi::Spi::<_, _, 8>::new(pac.SPI0); + + // Exchange the uninitialised SPI driver for an initialised one + let spi = spi.init( + &mut pac.RESETS, + clocks.peripheral_clock.freq(), + 16_000_000u32.Hz(), + &embedded_hal::spi::MODE_0, + ); + + info!("Aquire SPI SD/MMC BlockDevice..."); + let mut sdspi = SdMmcSpi::new(spi, spi_cs); + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + // Next we need to aquire the block device and initialize the + // communication with the SD card. + let block = match sdspi.acquire() { + Ok(block) => block, + Err(e) => { + error!("Error retrieving card size: {}", defmt::Debug2Format(&e)); + blink_signals_loop(&mut led_pin, &mut delay, &BLINK_ERR_2_SHORT); + } + }; + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + info!("Init SD card controller..."); + let mut cont = Controller::new(block, DummyTimesource::default()); + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + info!("OK!\nCard size..."); + match cont.device().card_size_bytes() { + Ok(size) => info!("card size is {} bytes", size), + Err(e) => { + error!("Error retrieving card size: {}", defmt::Debug2Format(&e)); + blink_signals_loop(&mut led_pin, &mut delay, &BLINK_ERR_3_SHORT); + } + } + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + info!("Getting Volume 0..."); + let mut volume = match cont.get_volume(VolumeIdx(0)) { + Ok(v) => v, + Err(e) => { + error!("Error getting volume 0: {}", defmt::Debug2Format(&e)); + blink_signals_loop(&mut led_pin, &mut delay, &BLINK_ERR_4_SHORT); + } + }; + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + // After we have the volume (partition) of the drive we got to open the + // root directory: + let dir = match cont.open_root_dir(&volume) { + Ok(dir) => dir, + Err(e) => { + error!("Error opening root dir: {}", defmt::Debug2Format(&e)); + blink_signals_loop(&mut led_pin, &mut delay, &BLINK_ERR_5_SHORT); + } + }; + + info!("Root directory opened!"); + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + // This shows how to iterate through the directory and how + // to get the file names (and print them in hope they are UTF-8 compatible): + cont.iterate_dir(&volume, &dir, |ent| { + info!( + "/{}.{}", + core::str::from_utf8(ent.name.base_name()).unwrap(), + core::str::from_utf8(ent.name.extension()).unwrap() + ); + }) + .unwrap(); + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + let mut successful_read = false; + + // Next we going to read a file from the SD card: + if let Ok(mut file) = cont.open_file_in_dir(&mut volume, &dir, "O.TST", Mode::ReadOnly) { + let mut buf = [0u8; 32]; + let read_count = cont.read(&volume, &mut file, &mut buf).unwrap(); + cont.close_file(&volume, file).unwrap(); + + if read_count >= 2 { + info!("READ {} bytes: {}", read_count, buf); + + // If we read what we wrote before the last reset, + // we set a flag so that the success blinking at the end + // changes it's pattern. + if buf[0] == 0x42 && buf[1] == 0x1E { + successful_read = true; + } + } + } + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + match cont.open_file_in_dir(&mut volume, &dir, "O.TST", Mode::ReadWriteCreateOrTruncate) { + Ok(mut file) => { + cont.write(&mut volume, &mut file, b"\x42\x1E").unwrap(); + cont.close_file(&volume, file).unwrap(); + } + Err(e) => { + error!("Error opening file 'O.TST': {}", defmt::Debug2Format(&e)); + blink_signals_loop(&mut led_pin, &mut delay, &BLINK_ERR_6_SHORT); + } + } + + cont.free(); + + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_LONG); + + if successful_read { + info!("Successfully read previously written file 'O.TST'"); + } else { + info!("Could not read file, which is ok for the first run."); + info!("Reboot the pico!"); + } + + loop { + if successful_read { + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_SHORT_SHORT_LONG); + } else { + blink_signals(&mut led_pin, &mut delay, &BLINK_OK_SHORT_LONG); + } + + delay.delay_ms(1000); + } +}