Persistent Storage in Zephyr: Saving Data to Files

 

Introduction

In this series of blog posts introducing The Zephyr Project RTOS, we have primarily concentrated on Zephyr internals and infrastructure. Recall that Zephyr wants to be a leading RTOS for devices with limited resources that are connected. To guarantee a platform that is secure, dependable, and vendor-neutral, Zephyr incorporates open-source and security best practices.



I'll demonstrate how to make an application in Zephyr to store data on a microSD card in this blog post. Even though the majority of embedded systems today can upload sensor data via the internet, the connection might be erratic. There are two benefits to having a microSD card. It can be expanded, to start. A micro-SD card of one size can be changed for a larger one, but onboard RAM and flash storage are fixed. Second, a desktop computer can be used to access data from a microSD card.

Hardware

This blog post will use the Nordic nRF52840 development kit (https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk). We will connect the nRF52840 development kit to the SparkFun microSD Transflash Breakout board (https://www.sparkfun.com/products/544). Any microSD card from a reputable vendor will suffice.

The following diagram shows the connections between the SparkFun microSD module and the nRF52840 development kit:



Embedded Software

We will go over the pertinent parts of the embedded software that interface with the SD card in this section. First, we can use West to get Zephyr v3.5 by running the following command:

$> west init -m <a href="https://github.com/zephyrproject-rtos/zephyr">https://github.com/zephyrproject-rtos/zephyr</a> --mr v3.5.0 zephyrproject
$> cd zephyrproject
$> west update

Second, we can clone the repository that contains our test application:

$> git clone https://github.com/mabembedded/zephyr-sd-spi.git

Third, we need to make sure that the exFAT scheme—which is Windows' default—is used to format our SD card. Lastly, we can open a terminal interface and use the USB connection to connect the nRF52840 development kit to our PC. We can build and flash the application by executing the following commands:

$> cd zephyr-sd-spi
$> cmake –preset build
$> west build && west flash

We should see the following output in the terminal interface:

*** Booting Zephyr OS build zephyr-v3.5.0 ***
[00:00:00.402,770] <inf> sd: Maximum SD clock is under 25MHz, using clock of 24000000Hz
[00:00:00.414,215] <inf> main: Block count 384503808
Sector size 512
Memory Size(MB) 187746
Disk mounted.
Listing dir /SD: ...
[DIR ] System Volume Information
[FILE] test_data.txt (size = 13)
Successfully mounted SD card
main - successfully created file

If we plug in the SD card to our PC and open it up in File Explorer, we should see “test_data.txt” with the string “hello world!” on the first line, as seen below:


Kconfig

The following relevant Kconfig options are enabled in the “prj.conf” file, with a description of each:

  • CONFIG_DISK_ACCESS: This option allows for the disk access subsystem.
  • CONFIG_FILE_SYSTEM: This option allows for the filesystem subsystem.
  • CONFIG_FAT_FILESYSTEM_ELM: This option instructs Zephyr to use the “ELM” FAT FS implementation, found on http://elm-chan.org/.
  • CONFIG_FS_FATFS_MOUNT_MKFS: This option instructs Zephyr to create a disk with a FAT filesystem if none is found.
  • CONFIG_FS_FATFS_EXFAT: This option enables the exFAT partition scheme.
  • CONFIG_DISK_DRIVER_SDMMC: This option enables the SD/EMMC driver.
  • CONFIG_SPI: This option enables the SPI subsystem.
  • CONFIG_GPIO: This option enables the GPIO subsystem.

Devicetree Overlay

Additionally, there are two reasons why we must create a Devicetree overlay. The nRF52840 development kit's pins for the SPI connection to the SparkFun Transflash breakout board must first be updated. Secondly, we need to tell the application firmware that an SD card is plugged in.
As indicated below, we must first add a new entry to the pinctrl block in order to update the SPI pins:

&pinctrl {
        custom_spi: custom_spi {
                group1 {
                        psels = <NRF_PSEL(SPIM_SCK, 0, 26)>,
                                <NRF_PSEL(SPIM_MOSI, 0, 27)>,
                                <NRF_PSEL(SPIM_MISO, 1, 8)>;
                };
        };
};

Then, we need to update the SPI block in the overlay with our custom pinctrl (and also add the GPIO for the CS line):

&spi1 {
        status = "okay";
        pinctrl-0 = <&custom_spi>;
        pinctrl-1 = <&custom_spi>;
        pinctrl-names = "default", "sleep";
        cs-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
.
.
.

The following needs to be added in the “spi1” node to inform the application of the existence of the SD card:

.
.
.
        sdhc0: sdhc@0 {
                compatible = "zephyr,sdhc-spi-slot";
                reg = <0>;
                status = "okay";
                label = "SDHC_0";
                mmc {
                        compatible = "zephyr,sdmmc-disk";
                        status = "okay";
                };
                spi-max-frequency = <24000000>;
        };
};

Application Source

With the Devicetree Overlay and Kconfig installed, we can go over the implementation step-by-step. To make sure our program can accurately read the files on the SD card, I made two helper functions. The prototype for the first function, "lsdir," is as follows:

static int lsdir(const char *path);

·         This function prints all of the directories and files contained in a given path when it receives it as input. The second function, "mount_sd_card," makes use of "lsdir." The following tasks are carried out by this function:

  • Initializes the underlying disk via “disk_access_init.”
  • Retrieves the number of sectors via “disk_access_ioctl” with “DISK_IOCTL_GET_SECTOR_COUNT” as a parameter.
  • Retrieves the sector size via “disk_access_ioctl” with “DISK_IOCTL_GET_SECTOR_SIZE” as a parameter.
  • Prints the total space of the SD card using the information retrieved above.
  • Mounts the SD card. If the SD card was successfully mounted, the function lists the files and directories at the root of the SD card. If not, the function tries to mount again.

In "main," "mount_sd_card" is used as the first function. It initializes the "fs_file_t" data structure, which is displayed below, upon success. Every subsequent file operation will make use of the data structure.

struct fs_file_t data_filp;
fs_file_t_init(&data_filp);

The "fs_unlink" function is then used to remove "test_data.txt" from the SD card's root, if it exists. The following line creates a new file named "test_data.txt" and opens it for writing:

fs_open(&data_filp, "/SD:/test_data.txt", FS_O_WRITE | FS_O_CREATE);

Finally, the following lines are used to write “hello world!” to the file that was created:

sprintf(file_data_buffer, "hello world!\n");
ret = fs_write(&data_filp, file_data_buffer, strlen(file_data_buffer));
fs_close(&data_filp);

Summary

In this blog post, we demonstrated how to mount a microSD card, write data to it, and create a new file on the microSD card using a Zephyr application. Devices in the field that need to periodically write data to off-board memory can benefit from the lessons learned from such an application, particularly in situations where Internet access may be intermittent. We will continue our journey of writing a custom BLE application that runs on Zephyr in the upcoming blog post!

 

If you're looking to enhance your embedded systems with advanced storage capabilities like microSD integration or custom BLE applications, Silicon Signals is here to help. Our team specializes in hardware design, software development, and integration of cutting-edge solutions using Zephyr and other RTOS platforms.

👉 Contact Us Today to explore how we can elevate your projects with tailored embedded systems solutions!

Comments

Popular posts from this blog

How Android System Services Connect Apps and HAL: A Deep Dive

AOSP Passthrough HAL: Architecture, Use Cases & Performance Guide

Getting Started with AOSP: Build Custom Android Solutions