Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Code to Write a File to an SD Card Using the ESP32

In this exercise, we will create (or overwrite) a file on an SD card and write "Hello, Ferris!" into it.

Generate project using esp-generate

We will enable async (Embassy) support for this project. To create the project, use the esp-generate command. Run the following:

esp-generate --chip esp32 sdcard-write

This will open a screen asking you to select options.

  • Select the option "Enable unstable HAL features"
  • Then, select the option "Adds embassy framework support".

Just save it by pressing "s" in the keyboard.

Additional Crates required

Update your Cargo.toml to add these additional crate along with the existing dependencies.

# sd card driver
embedded-sdmmc = "0.9.0"

# To convert Spi bus to SpiDevice
embedded-hal-bus = "0.3.0"

## For time parsing
jiff = { version = "0.2.16", default-features = false, features = ["static"] }

TimeSource with RTC

Before diving into the writing process, let's create a TimeSource for the sdmmc crate. In the previous exercise, we only read from a file and used a dummy time source. However, in this exercise, we want to create a file and ensure its time metadata is updated accordingly. To do this, we'll create a time source that's close enough to real-time for our needs.

We'll use the onboard RTC for this purpose. While this isn't a perfect solution; since the RTC requires an initial time to be set (which we'll pass through an environment variable). It will also reset whenever the ESP32 is restarted. Alternatively, you can get the current time from NTP servers using a Wi-Fi connection.

We will create a struct SdTimeSource that implements the TimeSource trait and uses the Rtc to get the current time. We have to specify how many years have passed since the year 1970 (the Unix epoch). We will get this by subtracting 1970 from the current year. We will also need to specify the month and date information in zero-indexed format, and the time as it is.

#![allow(unused)]
fn main() {
static TZ: jiff::tz::TimeZone = jiff::tz::get!("America/New_York");

struct SdTimeSource {
    timer: Rtc<'static>,
}

impl SdTimeSource {
    fn new(timer: Rtc<'static>) -> Self {
        Self { timer }
    }

    fn current_time(&self) -> u64 {
        self.timer.current_time_us()
    }
}

static TZ: jiff::tz::TimeZone = jiff::tz::get!("America/New_York");

impl TimeSource for SdTimeSource {
    fn get_timestamp(&self) -> Timestamp {
        let now_us = self.current_time();

        // Convert to jiff Time
        let now = jiff::Timestamp::from_microsecond(now_us as i64).unwrap();
        let now = now.to_zoned(TZ.clone());

        Timestamp {
            year_since_1970: (now.year() - 1970).unsigned_abs() as u8,
            zero_indexed_month: now.month().wrapping_sub(1) as u8,
            zero_indexed_day: now.day().wrapping_sub(1) as u8,
            hours: now.hour() as u8,
            minutes: now.minute() as u8,
            seconds: now.second() as u8,
        }
    }
}
}

Next, we'll update the SD card initialization code to use SdTimeSource as the time source. We'll pass the current time through an environment variable, parse it using jiff, and set it as the current time for the RTC.

#![allow(unused)]
fn main() {
// Timer for sdcard
let rtc = Rtc::new(peripherals.LPWR);
let current_time_us: u64 = env!("CURRENT_TIME_US")
    .parse()
    .expect("Invalid microseconds");
rtc.set_current_time_us(current_time_us);

let sd_timer = SdTimeSource::new(rtc);

println!("Init SD card controller and retrieve card size...");
let sd_size = sdcard.num_bytes().unwrap();
println!("card size is {} bytes\r\n", sd_size);

// Now let's look for volumes (also known as partitions) on our block device.
// To do this we need a Volume Manager. It will take ownership of the block device.
let volume_mgr = VolumeManager::new(sdcard, sd_timer);

// Try and access Volume 0 (i.e. the first partition).
// The volume object holds information about the filesystem on that volume.
let volume0 = volume_mgr.open_volume(VolumeIdx(0)).unwrap();

let root_dir = volume0.open_root_dir().unwrap();
}

Write to file

Let's open the file in ReadWriteCreateOrTruncate mode. If the file doesn't exist, this will create it. If it does exist, it will truncate the file, clearing any existing content.

#![allow(unused)]
fn main() {
let mut my_file = root_dir
.open_file_in_dir(
    "FERRIS.TXT",
    embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
)
.unwrap();
}

Once the file is open, we can write the message into it and then flush it to ensure the data is saved.

#![allow(unused)]
fn main() {
let line = "Hello, Ferris!";
if let Ok(()) = my_file.write(line.as_bytes()) {
    my_file.flush().unwrap();
    println!("Written Data");
} else {
    println!("Not wrote");
}
}

To verify, you can either connect the SD card to your computer or re-run the SD card reading program we used earlier.

Clone the existing project

You can also clone (or refer) project I created and navigate to the sdcard-write folder.

git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/sdcard-write

How to run?

We will pass the current date and time as an environment variable. To do this, we will add a shell command before cargo run that gets the current date and time and passes it to cargo run. This might not work in shells like Fish or on Windows.

CURRENT_DATETIME="$(date '+%Y-%m-%d %H:%M:%S')" cargo run --release

Full code

#![no_std]
#![no_main]
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]

use defmt::info;
use embassy_executor::Spawner;
use embassy_time::{Delay, Duration, Timer};
use esp_hal::clock::CpuClock;
use esp_hal::timer::timg::TimerGroup;
use esp_println::{self as _, println};

// SPI
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::gpio::{Level, Output, OutputConfig};
use esp_hal::spi::{self, master::Spi};
use esp_hal::time::Rate;

// SD card reader
use embedded_sdmmc::{SdCard, TimeSource, Timestamp, VolumeIdx, VolumeManager};

// For time
use esp_hal::rtc_cntl::Rtc;

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();

struct SdTimeSource {
    timer: Rtc<'static>,
}

impl SdTimeSource {
    fn new(timer: Rtc<'static>) -> Self {
        Self { timer }
    }

    fn current_time(&self) -> u64 {
        self.timer.current_time_us()
    }
}

static TZ: jiff::tz::TimeZone = jiff::tz::get!("America/New_York");

impl TimeSource for SdTimeSource {
    fn get_timestamp(&self) -> Timestamp {
        let now_us = self.current_time();

        // Convert to jiff Time
        let now = jiff::Timestamp::from_microsecond(now_us as i64).unwrap();
        let now = now.to_zoned(TZ.clone());

        Timestamp {
            year_since_1970: (now.year() - 1970).unsigned_abs() as u8,
            zero_indexed_month: now.month().wrapping_sub(1) as u8,
            zero_indexed_day: now.day().wrapping_sub(1) as u8,
            hours: now.hour() as u8,
            minutes: now.minute() as u8,
            seconds: now.second() as u8,
        }
    }
}

#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
    // generator version: 1.0.0

    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    let timg0 = TimerGroup::new(peripherals.TIMG0);
    esp_rtos::start(timg0.timer0);

    info!("Embassy initialized!");

    let _ = spawner;

    let spi_bus = Spi::new(
        peripherals.SPI2,
        spi::master::Config::default()
            .with_frequency(Rate::from_khz(400))
            .with_mode(spi::Mode::_0),
    )
    .unwrap()
    .with_sck(peripherals.GPIO18)
    .with_mosi(peripherals.GPIO23)
    .with_miso(peripherals.GPIO19)
    .into_async();

    let sd_cs = Output::new(peripherals.GPIO5, Level::High, OutputConfig::default());
    let spi_dev = ExclusiveDevice::new(spi_bus, sd_cs, Delay).unwrap();

    // Timer for sdcard
    let rtc = Rtc::new(peripherals.LPWR);
    let current_time_us: u64 = env!("CURRENT_TIME_US")
        .parse()
        .expect("Invalid microseconds");
    rtc.set_current_time_us(current_time_us);

    let sd_timer = SdTimeSource::new(rtc);

    let sdcard = SdCard::new(spi_dev, Delay);

    println!("Init SD card controller and retrieve card size...");
    let sd_size = sdcard.num_bytes().unwrap();
    println!("card size is {} bytes\r\n", sd_size);

    // Now let's look for volumes (also known as partitions) on our block device.
    // To do this we need a Volume Manager. It will take ownership of the block device.
    let volume_mgr = VolumeManager::new(sdcard, sd_timer);

    // Try and access Volume 0 (i.e. the first partition).
    // The volume object holds information about the filesystem on that volume.
    let volume0 = volume_mgr.open_volume(VolumeIdx(0)).unwrap();

    let root_dir = volume0.open_root_dir().unwrap();

    let my_file = root_dir
        .open_file_in_dir(
            "FERRIS.TXT",
            embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
        )
        .unwrap();

    let line = "Hello, Ferris!";
    if let Ok(()) = my_file.write(line.as_bytes()) {
        my_file.flush().unwrap();
        println!("Written Data");
    } else {
        println!("Not wrote");
    }

    loop {
        Timer::after(Duration::from_secs(30)).await;
    }
}