SD card storage

Introduction

SD Cards and their variants have become ubiquitous in today's technology landscape.

Their tiny form factor and easy interface, combined with the enormous quantities of data that they can store, makes them extremely practical for a number of applications.

While different physical shapes exist, they all share the same electronic interface.

Nowadays these can found in basically any embedded system that needs to store or access large amounts of data.

Dooba is no exception to this. Using these types of memory cards is both easy and cheap, which is why we provide a driver for SD cards that follows and integrates with the storage model, which means we can mount them into our VFS to have access to files / directories.

How do they work?

Technically, these cards are flash-based storage devices which expose two interfaces: SDIO and SPI.

SDIO is a rather complex proprietary high-frequency interface - quite difficult to use from a microcontroller such as the one in the ioNode.

SPI is a very common generic interface - this is what we will be using to communicate with these cards. This means we will need to use one DIO (Digital I/O) pin to control the card's CS or chip select line.

Wiring diagram

Below is an example wiring diagram for connecting a MicroSD socket to the ioNode:

In the example shown above, the MicroSD's CS (Chip Select) signal is connected to the ioNode's pin 29.

A diagram showing all the available pins can be found here: ioNode.

Using the sdcard library

In order to use SD cards we must first add the 'sdcard' library to the list of dependencies in our dfe.conf:

# ...
deps:
  - sdcard

Using substrate

Let's declare this (micro)SD card in our substrate:

# MicroSD socket using pin 29 for CS
uses :sdcard_storage, name: 'sdc0', cs_pin: 29

This will create a storage device pointer 'sdc0' (struct storage *sdc0;).

From there, we can simply use the generic storage model's interface to read and write data:

 1 #include "substrate.h"
 2 
 3 void main()
 4 {
 5     uint8_t buffer[10];
 6 
 7     // Initialize substrate
 8     substrate_init();
 9 
10     // Read some data into our buffer (9 bytes @ offset 0x32)
11     if(storage_read(sdc0, 0x32, buffer, 9))    { /* An error occurred while reading! */ }
12 
13     // Write some data from our buffer (12 bytes @ offset 0x28)
14     if(storage_write(sdc0, 0x28, buffer, 12))  { /* An error occurred while writing! */ }
15 
16     // Synchronize our device (flush any buffers)
17     if(storage_sync(sdc0))                     { /* An error occurred while syncing! */ }
18 
19     // ...
20 }

Another possibility is to use the VFS (Virtual File System) to actually mount that MicroSD card with a file system. This will allow us to read and write files and directories easily, which is usually more practical than directly accessing arbitrary bytes.

Manual initialization (without substrate)

For those who prefer to go without the substrate, we can still initialize the SD card manually:

 1 #include <sdcard/storage.h>
 2 
 3 // Storage interface to our SD card
 4 struct sdcard_storage sdcs0;
 5 struct storage *sdc0;
 6 
 7 void main()
 8 {
 9     uint8_t buffer[10];
10 
11     // Initialize SD card using pin 29 for CS
12     sdcard_init_storage(&sdcs0, 29);
13     sdc0 = &(sdcs0.st);
14 
15     // Read some data into our buffer (9 bytes @ offset 0x32)
16     if(storage_read(stor, 0x32, buffer, 9))    { /* An error occurred while reading! */ }
17 
18     // Write some data from our buffer (12 bytes @ offset 0x28)
19     if(storage_write(stor, 0x28, buffer, 12))  { /* An error occurred while writing! */ }
20 
21     // Synchronize our device (flush any buffers)
22     if(storage_sync(stor))                     { /* An error occurred while syncing! */ }
23 
24     // ...
25 }

What about partitions?

Common hard drives can have up to 4 partitions (using the classic 'MBR' scheme), which allow 'splitting' a large storage into multiple smaller units, possibly each formatted with different file systems.

While most SD cards are formatted without partitions (the entire card is formatted as just one file system), it is perfectly valid to have them partitioned just like common hard drives.

Just like any other storage device, we can use the 'mbr' partition library to have every partition on the card appear as another independent storage device.

The SD card driver substrate makes this even easier - just add the 'partitions' parameter to the 'uses' statement in order to control whether to detect partitions or not:

# MicroSD socket using pin 29 for CS
uses :sdcard_storage, name: 'sdc0', cs_pin: 29, partitions: false

The example above will disable the detection of partitions on the card (by default, partitions are automatically detected).

When partitions are used, they will be made available as 'XXXpY', with 'XXX' being the storage device name, and 'Y' being the partition number. For example with our 'sdc0' card above, the following storage devices would be automatically created:

  • struct storage *sdc0; - the entire SD card
  • struct storage *sdc0p0; - first partition (0) of the card
  • struct storage *sdc0p1; - second partition (1) of the card
  • struct storage *sdc0p2; - third partition (2) of the card
  • struct storage *sdc0p3; - fourth and last partition (3) of the card