SD cards (Secure Digital Cards) drove out most of the other storage media for good reason. They are really easy to access, supporting different protocols. One protocol is SPI (Serial Peripheral Interface) which is a very robust and simple protocol. The cards are standardized by the SD association which also provides simplified specifications for free. This simplified specifications are actually everything you need to write a library to access a SD card.

There are already lots of libraries out there which are accessing SD cards and even support FAT file systems. For my use case they were either too slow or really badly made.

My class uses a own very simple file system which can only have one simple list of files. It uses a fixed block size of 512 bytes, because most SD cards today are in any case fixed to 512 byte blocks. There is also no streaming to other high level methods. You can either read data “slow” using a own buffer size or read data blazing fast using four byte blocks.

In the following sections I will go trough every part of the source code and explain things as good as possible, especially why I implemented things as I did. Best approach is, if you open the source in your favorite text/code editor where you always have a complete overview about the code.

The Interface

#pragma once
//
// A SD-Card Access library for asynchronous timed access in timer interrupts
// --------------------------------------------------------------------------
// (c)2014 by Lucky Resistor. http://luckyresistor.me
// Licensed under the MIT license. See file LICENSE for details.
//
//
// I developed this library to allow perfectly timed audio output, which
// requires that the data from the SD card is loaded in small chunks to 
// create the right timing when playing the samples.
//
// This library assumes the chip select for the SD-Card is on Pin 10.
// The library is tested with the AdaFruit Data Logging Shield.
//


#include <SPI.h>

About #pragma once

The header stats with a pragma statement. The pragma statement was introduced for C and C++ to implement compiler specific functions. It is followed by whatever defines the compiler specific functionality. The statement #pragma once makes sure, the given file is only included once when compiling a single module. It replaces the ugly and dangerous “header guards” and is meanwhile supported by all new compilers. Therefore I strongly recommend using this feature for new source code.

Old-style “header guards” for this header file would look like this:

#ifndef SDCARD_H
#define SDCARD_H
// class definition, etc.
#endif

Using macros for this reason has many disadvantages. One is the danger of name conflicts. If there is somewhere another header file with the same name, using the same macro names for the header guard, it will cause hard to find problems. Another is the compiler/preprocessor has no clue what these macro definitions are actually for, he can not optimize anything, e.g. by caching precompiled header files.

Using #pragma once avoids all these mistakes and speeds up compiling time with any modern C and C++ compiler. It is supported by all modern compiler.

Read my post about preprocessor macros: How and Why to Avoid Preprocessor Macros.

The Copyright Block

If you plan to publish your source code or not, you should always include a header with a copyright notice. The copyright just makes clear your are the author of the code and your will define the terms how your code my be reproduced. If you plan to publish your code, you should also choose an appropriate license. GitHub provides an excellent web page which is helping you to choose the right license for your source code.

The Description

Each interface should also contain a short description which tells about the purpose of the code. This description can also be added as API comment on the class, but nevertheless should exist.

Includes

In this case I include the header file of the SPI library which I use in my class to communicate with the SD card.

Configuration

/// Enable debugging via Serial. You have to setup the serial port before calling initialize().
///
//#define SDCARD_DEBUG

/// Use the SPI transactions for all read calls.
/// If not defined, it is assumed one huge SPI.transaction for the whole read process.
/// Only the CS signal is handled.
///
//#define SDCARD_USE_SPI_TRANSACTIONS 

Here I introduce two macro definitions which can be used to enable additional features which are only used for debugging (SDCARD_DEBUG) and one to enable SPI transactions (SDCARD_USE_SPI_TRANSACTIONS). The SPI transactions make sure the SPI bus is usable for multiple devices, and automatically switches to the right mode and speed. For my purpose this do not make any sense, and it took to much time, therefore I deactivated it, but kept this macro to be able to enable it later.

Create a Namespace

namespace lr {

Namespaces are a feature of C++ which address the problem of name conflicts. There is a “global” namespace, where everything lives which was declared without namespace. Especially the Arduino environment declares a huge amount of variables and constants there, so it is a good practice to put everything you write in a own namespace. Namespaces are only used while compile time, and they do not use any memory at runtime nor make they run your program slower. In my case I choose the namespace lr which stands for Lucky Resistor.

Read my post about using namespaces: How and Why to Use Namespaces.

Declaring Some Values

/// The SD Card access class for asynchronous SD Card access.
///
class SDCard
{
public:
    /// Errors
    ///
    enum Error : uint8_t {
        NoError = 0,
        Error_TimeOut = 1,
        Error_SendIfCondFailed = 2,
        Error_ReadOCRFailed = 3,
        Error_SetBlockLengthFailed = 4,
        Error_ReadSingleBlockFailed = 5,
        Error_ReadFailed = 6,
        Error_UnknownMagic = 7,
    };
    
    /// The status of a command.
    ///
    enum Status : uint8_t {
        StatusReady = 0, ///< The call was successful and the card is ready.
        StatusWait = 1, ///< You have to wait, the card is busy
        StatusError = 2, ///< There was an error. Check error() for details.
        StatusEndOfBlock = 3, ///< Reached the end of the block.
    };

    /// A single directory entry.
    ///
    struct DirectoryEntry {
        uint32_t startBlock; ///< The start block of the file in blocks.
        uint32_t fileSize; ///< The size of the file in bytes.
        char *fileName; ///< Null terminated filename ascii.
        DirectoryEntry *next; ///< The next entry, or a null pointer at the end.
    };

The class declares two enumerations. One for the error handling, and one which is used to signal the current status of an operation to the caller.

The struct DirectoryEntry is used to return some meta information about a file on the SD card. This is used with the simple filesystem I am using for this project.

Initialize Everything

public:
/// Initialize the library and the SD-Card.
/// This call needs some time until the SD-Card is ready for read. It
/// should be placed in the setup() method.
///
/// @return StatusReady on succes, StatusError on any error.
///
Status initialize();

To initialize everything, you have to call the initialize() method in your setup() function. This will initialize the communication with the SD card and prepare everything to read data from the card. This is a blocking call, which can take up to a second to finish, depending on the used SD card.

The return value can be StatusReady if the card could be initialized successfully, or StatusError if there was any error. In case of an error, you can call the method error() to get an error code with the reason for the error.

Read the File Directory

/// Read the SD Card Directory in HCDI format
///
/// @return StatusReady on success, StatusError on any error.
///
Status readDirectory();

After initializing the card, you can use the readDirectory() call to read the file directory from the SD card, which is stored in block 0. If you know the location of you data, you can omit this call. This is a blocking call which can take some milliseconds to finish, depending on the speed if your SD card.

The function returns either StatusReady on success, or StatusError if there was any error. In case of an error, you can call the method error() to get an error code with the reason for the error.

Reading the directory will allocate memory for the filenames. If you have many files this can take a huge amount of memory.

Search a File in the Directory

/// Find a file with the given name
///
/// @return The found directory entry, or 0 if no such file was found.
///
const DirectoryEntry* findFile(const char *fileName);

Use this method to search for a file in the directory which you previously read using readDirectory(). This will either return a pointer to a DirectoryEntry object, or a null pointer if the searched file name was not found.

The returned structure contains the start block and the actual size of the file which you can use with the next methods.

Start Reading a Single Block

/// Start reading the given block.
///
/// @param block The block in (512 byte blocks).
/// @return StatusWait = call again, StatusError = there was an error,
///    StatusReady = reading of the block has started, call readData().
///
Status startRead(uint32_t block);

You have to use this method to start reading a single block. It is an asynchronous method. The return status can be one of these:

  • StatusWait

    Call again! The operation could not be finished in time, probably because the SD card was not ready yet. Call again, until you get another response.

  • StatusReady

    The SD card is now ready to read data using the readData() method.

  • StatusError

    There was an error. Use the error() method to get the reason for the error.

Start Reading Multiple Blocks

/// Start reading from given block until stopRead() is called.
///
/// @param startBlock The first block in (512 byte blocks).
/// @return StatusWait = call again, StatusError = there was an error,
///    StatusReady = reading of the block has started, call readData().
///
Status startMultiRead(uint32_t startBlock);

To read a endless stream of blocks, call this method. It is an asynchronous method. The SD card will deliver blocks in sequence until stopRead() is called. The return status can be one of these:

  • StatusWait

    Call again! The operation could not be finished in time, probably because the SD card was not ready yet. Call again, until you get another response.

  • StatusReady

    The SD card is now ready to read data using either the readData() method or the fast reading methods.

  • StatusError

    There was an error. Use the error() method to get the reason for the error.

Read Data

/// Read data if ready.
///
/// @param buffer The buffer to read the data into.
/// @param byteCount in: The number of bytes to read, out: the actual number of read bytes.
/// @return StatusReady on success, StatusError is there was an error,
///     StatusEndOfBlock if the end of the block was reached.
///
Status readData(uint8_t *buffer, uint16_t *byteCount);

This is a method to “slow” read data from the SD card. You have to call startRead() or startMultiRead() successfully, before you can read data using this method.

You have to pass a pointer to a buffer for the first argument. The second argument has to be a pointer to a uint16_t variable which contains the size of the buffer. If the method succeeds, it will put the actual number of read bytes in this variable. This will always a larger value than 0. You can get the following status results:

  • StatusWait

    Call again! The operation could not be finished in time, probably because the SD card was not ready yet. Call again, until you get another response.

  • StatusReady

    A number of bytes was written in the buffer. The exact number of bytes was written to the variable to which byteCount points to.

  • StatusEndOfBlock

    If you read a single block, this status indicates the end of the block was reached.

  • StatusError

    There was an error. Use the error() method to get the reason for the error.

Start Reading Really Fast

/// Start the fast reading.
///
void startFastRead();

Before you can call readFast4() you have to call this method to initialize the fast reading process.

Read Four Bytes Really Fast

/// Super fast data multi read.
///
/// For extreme situations. It always reads 4 bytes to the given address.
/// 
/// @param buffer A pointer to the buffer to write 4 bytes.
/// @return StatusWait if no bytes were written, StatusReady if bytes were written.
///
Status readFast4(uint8_t *buffer);

This will read four bytes to the given buffer as fast as possible. This method has a minimum overhead and can be used to stream large amounts of data from the SD card. It is an asynchronous method. You can get one of this return values:

  • StatusWait

    Call again! The operation could not be finished in time, probably because the SD card was not ready yet. Call again, until you get another response.

  • StatusReady

    Four bytes were read into the given buffer.

  • StatusError

    There was an error. Use the error() method to get the reason for the error.

Stop Reading Data

/// End reading data.
///
/// You have to call this method in any case. This is a blocking call and
/// and can take a while.
///
/// @return StatusReady = success, block read ended.
/// 
Status stopRead();

Call this method to stop a multi block read operation. This is a blocking call, depending on the position in the last block and the state of the SD card, this call can take some milliseconds to finish. There are only two possible return values:

  • StatusReady

    Reading blocks has successfully stopped.

  • StatusError

    There was an error. Use the error() method to get the reason for the error.

Get the Last Error Message

    /// Get the last error
    ///
    Error error();
};

This will get the last error “message” if you ever got an error status from one of the methods above.

The Global Instance

/// The global instance to access the SD Card
///
extern SDCard sdCard;

}

You can access the global instance of the SD card reader using the lr::sdcard variable. Actually you should not create own instances of the class.

Handling the SPI Transactions

If the macro SDCARD_USE_SPI_TRANSACTIONS is not set, as this is the case by default, you have to handle the SPI transactions manually. Starting and ending a SPI transaction takes some time, therefore the code which is reading from the SD card can actually decide more precisely when to start and end a SPI transaction. If you set the SDCARD_USE_SPI_TRANSACTIONS macro, you can use the interface of the class without any further code required, but some methods are a little bit slower.

You can have a look into the AudioPlayer class to see some examples how to use this class. Or you can look at this fictive example below:

bool initialize()
{
    SDCard::Status status = sdCard.initialize();    
    if (status != SDCard::StatusReady) {
        return false;
    }

    SPI.beginTransaction(SPISettings(32000000, MSBFIRST, SPI_MODE0));
    status = sdCard.readDirectory();
    if (status != SDCard::StatusReady) {
        return false;
    }
    SPI.endTransaction();
    return true;
}

bool play()
{
    const uint32_t startBlock = 1;
    SDCard::Status status;
    SPI.beginTransaction(SPISettings(32000000, MSBFIRST, SPI_MODE0));
    // Wait until we can start a read.
    while((status = sdCard.startMultiRead(startBlock)) == SDCard::StatusWait) {
        delayMicroseconds(1);
    }
    if (status != SDCard::StatusReady) {
        SPI.endTransaction();
        return false;
    }
    const uint16_t bufferSize = 0x100;
    uint8_t buffer[bufferSize];
    sdCard.startFastRead();
    for (uint16_t i = 0; i < bufferSize; ) {
        status = sdCard.readFast4(&buffer[i]);
        if (status == SDCard::StatusReady) {
            i += 4;
        } else if (status == SDCard::StatusError) {
            SPI.endTransaction();
            return false;
        }
    }
    sdCard.stopRead();
    SPI.endTransaction();
    return true; // success
}

All lines with SPI transactions are highlighted. Using a shared constant for the SPISettings would optimize things even further. The value 32000000 for the speed just makes sure it will choose the maximum possible speed to read the data. Probably this can lead to problems with slow SD cards.

Continue with Part 2: Accessing the SD Card (Part 2)