Before we can examine the read methods, let us have a look to the required variables and constants. At the top there is the constant which defines the block size, which is fixed to 512 bytes.
const uint16_t blockSize = 512;
To keep the state of the reading process, there is an enumeration ReadState
:
/// The state of the read command /// enum ReadState : uint8_t { ReadStateWait = 0, ///< Waiting for the start of the data. ReadStateReadData = 1, ///< In the middle of data reading. ReadStateReadCRC = 2, ///< Reached end of block, read CRC bytes. ReadStateEnd = 3, ///< The read process has ended (end of block or error). };
I also have an enumeration to store the mode for reading. If we are reading single or multiple blocks. This is an excellent example of a situation where many would choose a boolean value, but an enumeration makes things clearer and brings type safety.
/// The block read mode /// enum ReadMode : uint8_t { ReadModeSingleBlock = 0, ///< Read just a single block. ReadModeMultipleBlocks = 1, ///< Read multiple blocks until stop is sent. };
Some important constants are defined in the next block. They are used to verify the responses from the commands:
/// Reponses and flags. /// const uint8_t R1_IdleState = 0x01; ///< The state if the card is idle. const uint8_t R1_IllegalCommand = 0x04; ///< The flag for an illegal command. const uint8_t R1_ReadyState = 0x00; ///< The ready state. const uint8_t BlockDataStart = 0xfe; ///< Byte to indicate the block data will start.
Next are some variables which are used in the read process. The blockByteCount
is used to count the already read bytes of a block, to check when the 512 byte boundary is reached. With the blockReadState
the current state of the read process is kept. blockReadMode
stores the current mode, single or multi block, to detect the end of the block. If the directory is read, it is stored as linked list. directoryEntry
points to the first element.
/// The byte count in the current block. /// uint16_t blockByteCount; /// The state of the read command. /// ReadState blockReadState; /// The mode for the block read command. /// ReadMode blockReadMode; /// The directory. /// SDCard::DirectoryEntry *directoryEntry = 0;
Variables in a Struct
Probably you noted, I use a struct in the cpp
file to store all variables and define all internal used functions.
/// The state /// struct SDCardState {
There is a global instance of this struct and a global instance of the class. The methods of the class actually call the methods from this struct. Because all these methods are declared as inline, the compiler will remove this call.
/// The global state for the SD Card class /// SDCardState sdCardState; /// The global instance for the SDCard class /// SDCard sdCard; SDCard::Status SDCard::initialize() { return sdCardState.initialize(); }
But what is the reason why I implement this class like this? One reason is, there is no harm. An indirect LDD
command which the compiler produces to access one of these variables uses two clocks. Accessing a variable directly uses two clocks as well. But encapsulating the actual implementation of the SDCard
class allows an easy replacement later, without any change of the interface.
If I would write this C++ code for a desktop system, the implementation of this pattern looks like this:
class Example { public: // interface private: class Private; Private *p; }
All variables of the class and private methods get encapsulated in the Private
which will be declared and defined in the implementation file. Alternatively I can also declare a class ExamplePrivate
, but the result is the same.
Starting the Read Process
There are two commands to start the read process from the card. Command 17 will start reading a single block, and command 18 will start reading blocks in a sequence from the given starting block. See the following illustration for details.
After receiving the response from the command, the card can be busy for a while until the block can get retrieved. As soon the card is ready to send the data, an initial data token is sent. This can be either the byte 0xFE
which is followed by the 512 bytes of data and a 2 byte CRC. Or it can be a error token with bits 5-7 set to 0. The error token has the following format:
Bit 7: 0 Bit 6: 0 Bit 5: 0 Bit 4: Card is Locked. Bit 3: Out of Range. Bit 2: Card ECC Failed. Bit 1: CC Error. Bit 0: Error.
To stop a multi block read, the command 12 is sent at any time. The following byte has to be ignored, then you have to wait (data is low, you receive 0x00 bytes) until you receive the response (R1) from the stop command (CMD 12). After the response, the card is busy for a while until you can send another command.
The Implementation
There are two methods to start the read process. One for reading a single block and a second to start reading multiple blocks. Both methods check if the card is ready and send the command to start the reading blocks. If the command was sent successfully, the variables are initialised for the read process.
inline SDCard::Status startRead(uint32_t block) { // Begin a transaction. chipSelectBegin(); uint8_t result = spiReceive(); if (result != 0xff) { // Check if the chip is idle. chipSelectEnd(); return SDCard::StatusWait; // The chip isn't ready yet. } // Ok, the chip is ready send the read command. result = sendCommand(Cmd_ReadSingleBlock, block); if (result != R1_ReadyState) { error = SDCard::Error_ReadSingleBlockFailed; chipSelectEnd(); return SDCard::StatusError; } // Reset the block byte count blockByteCount = 0; blockReadState = ReadStateWait; blockReadMode = ReadModeSingleBlock; chipSelectEnd(); return SDCard::StatusReady; } inline SDCard::Status startMultiRead(uint32_t startBlock) { // Begin a transaction. chipSelectBegin(); uint8_t result = spiReceive(); if (result != 0xff) { // Check if the chip is idle. chipSelectEnd(); return SDCard::StatusWait; // The chip isn't ready yet. } // Ok, the chip is ready send the read command. result = sendCommand(Cmd_ReadMultiBlock, startBlock); if (result != R1_ReadyState) { error = SDCard::Error_ReadSingleBlockFailed; chipSelectEnd(); return SDCard::StatusError; } // Reset the block byte count blockByteCount = 0; blockReadState = ReadStateWait; blockReadMode = ReadModeMultipleBlocks; chipSelectEnd(); return SDCard::StatusReady; }
Reading Data (Slow)
The read method uses a state variable and a variable to count the current number of read bytes. Depending on the state, it waits for data, reads bytes or skips the CRC. In single block mode, the read process ends at the end of the block. In multi read mode, the state switches back to ReadStateWait
and the process starts over again.
SDCard::Status readData(uint8_t *buffer, uint16_t *byteCount) { // variables uint8_t result; SDCard::Status status = SDCard::StatusReady; uint16_t bytesToRead; // Start the read. chipSelectBegin(); switch (blockReadState) { case ReadStateWait: result = spiReceive(); if (result == 0xff) { status = SDCard::StatusWait; break; } else if (result == BlockDataStart) { blockReadState = ReadStateReadData; // no break! continue with read data. } else { error = SDCard::Error_ReadFailed; blockReadState = ReadStateEnd; status = SDCard::StatusError; // Failed. break; } case ReadStateReadData: bytesToRead = min(blockSize - blockByteCount, *byteCount); for (uint16_t i = 0; i < bytesToRead; ++i) { buffer[i] = spiReceive(); } *byteCount = bytesToRead; blockByteCount += bytesToRead; if (blockByteCount < blockSize) { break; } blockReadState = ReadStateReadCRC; case ReadStateReadCRC: spiSkip(2); blockByteCount = 0; if (blockReadMode == ReadModeSingleBlock) { blockReadState = ReadStateEnd; status = SDCard::StatusEndOfBlock; } else { blockReadState = ReadStateWait; status = SDCard::StatusWait; } break; case ReadStateEnd: status = SDCard::StatusEndOfBlock; break; } chipSelectEnd(); #ifdef SDCARD_DEBUG if (status == SDCard::StatusError) { SDC_DEBUG_PRINT(F("Read error, last byte = 0x")); SDC_DEBUG_PRINTLN(String(result, 16)); } #endif return status; }
Reading Data (Fast)
The method to fast reading data works similar, but has fewer options. It works only in multi block mode and has a fixed amount of bytes which simplifies everything.
inline SDCard::Status readFast4(uint8_t *buffer) { uint8_t readByte; switch (blockReadState) { case ReadStateWait: readByte = spiReceive(); if (readByte == 0xff) { return SDCard::StatusWait; } else if (readByte == BlockDataStart) { blockReadState = ReadStateReadData; return SDCard::StatusWait; } else { blockReadState = ReadStateEnd; chipSelectEnd(); return SDCard::StatusError; // Failed. } case ReadStateReadData: buffer[0] = spiReceive(); buffer[1] = spiReceive(); buffer[2] = spiReceive(); buffer[3] = spiReceive(); blockByteCount += 4; if (blockByteCount >= blockSize) { blockReadState = ReadStateReadCRC; } return SDCard::StatusReady; case ReadStateReadCRC: spiSkip(2); blockByteCount = 0; blockReadState = ReadStateWait; return SDCard::StatusWait; case ReadStateEnd: return SDCard::StatusError; // Failed. } }
Stopping the Read Process
Stopping the read process of a multi block read works in a special way. You send the stop command at any time, but have to skip the first byte after the command before you wait for the result.
inline SDCard::Status stopRead() { if (blockReadMode == ReadModeSingleBlock) { if (blockReadState != ReadStateEnd) { // Make sure we read the rest of the data. uint16_t byteCount = blockSize; while (readData(0, &byteCount) != SDCard::StatusEndOfBlock) { } } } else { // Send Command 12 in a special way chipSelectBegin(); // If not already done spiSend(Cmd_StopTransmission | 0x40); spiSend(0); spiSend(0); spiSend(0); spiSend(0); spiSend(0xff); // Fake CRC // Skip one byte spiSkip(1); uint8_t result; for (uint8_t i = 0; ((result = spiReceive()) & 0x80) && i < 0x10; ++i); if (result != R1_ReadyState) { #ifdef SDCARD_DEBUG SDC_DEBUG_PRINT(F("Error on response, last byte = 0x")); SDC_DEBUG_PRINTLN(String(result, 16)); #endif return SDCard::StatusError; } waitUntilReady(300); } return SDCard::StatusReady; }
Continue with: Writing to DAC
Hi !
Thanks for this interesting tutorial. I noted a mistake, which prevented my code to word. You say “Command 18 will start reading a single block, and command 19 will start reading blocks in a sequence”, but there’s a mistake. Commands for this are CMD17 and CMD18 (CMD19 doesn’t exist in the specification !).
Hope this can help.
Regards,
David
Hi David! You are absolutely right, I will fix this as soon as possible. Thank you very much for reporting this issue! 🙂