A SD card is a storage device which contains a microcontroller. We can communicate with this microcontroller using different interfaces. The one I will describe here is the SPI interface. The SPI interface is flexible with the timing and requires only four connections. There are also many shields for the Arduino with ready solutions to access a SD card.

One problem accessing a SD card from an Arduino board is the different voltage. SD cards working with a supply voltage between 2.7 and 3.6 volts. The outputs of the Arduino board provide 5 volts which will kill the SD card instantly (do not try this). You need a voltage converter to convert the 5V output/inputs to a suitable voltage range.
I am using the Adafruit data logger shield, where this is already done. The SD card is connected to pins 10, 11, 12 and 13 of the Arduino board.

You will find all the detail specification on the website of the SD card association. Look in the downloads section for “simplified specifications”.
Put the SD Card into SPI Mode
To put the SD card into SPI mode you have to wait until the card is properly powered on, set data in and chip select to high and send at least 74 clocks. The speed of the SPI line should be below 400kHz to keep backward compatibility with slower SD cards. Because the Arduino controller itself takes some time until the code starts, we do not actually have to wait for the SD card.
/// Initialize the SD Card /// inline SDCard::Status initialize() { // Keep the start time to detect time-outs. uint16_t startTime = static_cast<uint16_t>(millis()); uint32_t argument = 0; uint8_t result = 0; uint32_t responseValue = 0; // Initialize the SPI library pinMode(SDCARD_CSPINNUM, OUTPUT); onlyChipSelectEnd(); SPI.begin();
The initialization starts with the declaration of some variables we need later. In the next step we configure the chip select pin (pin 10) as digital output. The method onlyChipSelectEnd()
sets the chip select line to high, this means the chip is not selected. At the end we initialize the SPI library calling SPI.begin()
.
/// Only chip select. /// inline void onlyChipSelectBegin() { SDCARD_CSPORT &= ~_BV(SDCARD_CSPIN); } /// Only chip select. /// inline void onlyChipSelectEnd() { SDCARD_CSPORT |= _BV(SDCARD_CSPIN); }
Despite this methods using direct port manipulation, they actually just put the chip select line to low (onlyChipSelectBegin()
) or high (onlyChipSelectEnd()
).
Back to the initialization function. In the next step we set the speed of the SPI line to 250kHz, for backward compatibility. We are doing this be starting a new transaction and setting the speed bit-order (MSBFIRST
) and mode (SPI_MODE0
).
// Speed should be <400kHz for the initialization. spiSettings = SPISettings(250000, MSBFIRST, SPI_MODE0); SPI.beginTransaction(spiSettings); // Send >74 clocks to prepare the card. onlyChipSelectBegin(); spiWait(100); onlyChipSelectEnd(); spiWait(2);
Next we select the chip (onlyChipSelectBegin()
) and send some clocks (actually 800 clocks), using the spiWait()
method, which just sends 1 bits and discards any received value. After the initial clocks, we unselect the chip (onlyChipSelectEnd()
) and send another 16 clocks using spiWait()
just to pass some time.
Before we continue with the initialization, I will explain the simple SPI helper functions I use in the code everywhere. There are four functions:
/// Send a byte over the SPI bus /// inline void spiSend(uint8_t value) { SPI.transfer(value); } /// Receive a byte from the SPI bus. /// inline uint8_t spiReceive() { return SPI.transfer(0xff); } /// Skip a number of bytes from the SPI bus. /// inline void spiSkip(uint8_t count) { for (uint8_t i = 0; i < count; ++i) { spiReceive(); } } /// Wait while sending clocks on the SPI bus /// inline void spiWait(uint8_t count) { for (uint8_t i = 0; i < count; ++i) { spiSend(0xff); } }
spiSend()
sends a single byte over the SPI line to the SD card. spiReceive()
receives a single byte over the SPI line from the SD card. Actually a SPI works always in both directions, while a bit is sent using the data out line, in the same time a bit is received using the data in line. Theoretically a device could simultaneously send and receive data using a SPI. Practically the SD card does either receive or send data, but normally not simultaneously.
spiSkip()
Just ignores a number of bytes. They are received but not used. At least the spiWait()
does exactly the same as spiSkip()
. Basically I just forgot to merge this two methods into one. 🙂
At this point, the SD card should have entered the SPI mode. The SPI mode is a little bit simpler than the native operating mode. For example checksums are usually just ignored, which makes everything simpler.
The SPI Mode
The principle to send a command is always the same. You send a byte with the command number, where the 6th and 7th bit are set to 0 and 1. It is followed by four bytes which is called the “argument” of the command. Usually it is interpreted as 32bit value. At the end of the command is the CRC for the command.
Now you have to wait for zero to eight bytes, until you receive the first byte of the response. There are commands with more than one byte of response, but most commands respond with one single byte. This response has the 7th bit always set to 0.
The chip select has to be kept low while sending the command, argument, checksum and until the response is received. If data is read, you have to keep the chip select low while receiving or sending the data. This is especially important if you have multiple devices on the same SPI line. You can only interrupt reading or writing between whole blocks. Even if you set chip select to high, most SD card will interpret the clock signal and you will get unexpected results.
Format of a Command

List of Relevant Commands
This is a list of commands which are relevant for my software implementation. You find all other commands, for example to write data to the SD card, in the specification.
Command | ID | Argument | Response |
---|---|---|---|
Go Idle State | CMD0 | 0 | R1 |
Send If Cond | CMD8 | … | R7 |
Stop Transmission | CMD12 | 0 | R1 |
Set Block Length | CMD16 | Length | R1 |
Read Single Block | CMD17 | Block | R1 |
Read Multiple Blocks | CMD18 | Start Block | R1 |
Application Command | CMD55 | 0 | R1 |
Read OCR | CMD58 | … | R3 |
There are also a set of “application” commands. They are sent by first sending the “application command” with the ID 55, then a second command with the ID of the application command itself. I use only the application command 41 (ACMD41), which sends and receives the capacity of the SD card.
Send a Command
For sending commands to the SD card, I first declare two enumerations. The first enumeration defines the different return types.
/// The response type. /// enum ResponseType : uint16_t { ResponseMask = 0x00c0, ///< The mask for the responses. Response1 = 0 << 6, ///< One byte response. Response3 = 1 << 6, ///< One byte plus 32bit value Response7 = 1 << 6, ///< One byte plus 32bit value (supported voltage) };
If the : uint16_t
is unknown to you, this was added some time ago to the c++ standard to declare the integer type to which the enumeration value is converted, and how a enumeration value is stored.
I use a 16 bit value for the enumeration, but only use bit 6 and 7 to define the three different results. The reason why I use a 16bit integer is the enumeration of the commands shown below.
/// The command to send. /// enum Command : uint16_t { Cmd_GoIdleState = 0 | Response1, ///< Go idle state. Cmd_SendIfCond = 8 | Response7, ///< Verify SD Memory Card interface operating condition. Cmd_StopTransmission = 12 | Response1, ///< Stop reading blocks. Cmd_SetBlockLenght = 16 | Response1, ///< Set the block length Cmd_ReadSingleBlock = 17 | Response1, ///< Read one block. Cmd_ReadMultiBlock = 18 | Response1, ///< Read multiple blocks. Cmd_ApplicationCommand = 55 | Response1, ///< Escape for application specific command. Cmd_ReadOCR = 58 | Response3, ///< Retrieve the OCR register. ACmd_Flag = 0x100, ///< The flag for app commands. ACmd_SendOpCond = 41 | ACmd_Flag | Response1, ///< Sends host capacity support information and activates the card's initialization process. };
In the command enumeration I combine the result codes into the command codes. Actually I could also simple define constant variables for all the the commands and codes, but the enumeration is type checked by the compiler which adds extra safety. There is also a flag ACmd_Flag
to mark the application commands.
The sendCommand()
method sends a command to the SD card and also reads the response.
/// Send a command to the SD card synchronous. /// /// @param command The command to send. /// @param argument A pointer to the 4 byte buffer for the argument. /// @param responseValue A ponter to the variable where to save the 32bit value for R3/R7 responses. 0 = ignore. /// inline uint8_t sendCommand(Command command, uint32_t argument, uint32_t *responseValue = 0) { uint8_t result; uint8_t responseValues[4]; // Check if this is an app command if ((command & ACmd_Flag) != 0) { // For app commands, send a command 55 first. spiSend(55 | 0x40); spiSend(0); spiSend(0); spiSend(0); spiSend(0); for (uint8_t i = 0; ((result = spiReceive()) & 0x80) && i < 0x10; ++i); // now send the app command. } // Send the command. spiSend((command & 0x3f) | 0x40); spiSend(argument >> 24); spiSend(argument >> 16); spiSend(argument >> 8); spiSend(argument); uint8_t crc = 0xff; if (command == Cmd_GoIdleState) { crc = 0x95; } else if (command == Cmd_SendIfCond) { crc = 0x87; } spiSend(crc); // There can be 0-8 no command return values until the return is received. for (uint8_t i = 0; ((result = spiReceive()) & 0x80) && i < 0x10; ++i); const uint8_t response = (command & ResponseMask); if (response != Response1) { // Read the next 4 bytes for (uint8_t i = 0; i < 4; ++i) { responseValues[i] = spiReceive(); } // If there is a pointer, assign the response value (MSB). if (responseValue != 0) { *responseValue = (static_cast<uint32_t>(responseValues[0]) << 24) | (static_cast<uint32_t>(responseValues[1]) << 16) | (static_cast<uint32_t>(responseValues[2]) << 8) | static_cast<uint32_t>(responseValues[3]); } } return result; }
About the CRC in SPI Mode
The CRC codes are in most cases ignored, except for the “go idle state” command and for the “send if cond” command where (for unknown reasons)[1] the CRC has to be correct . Because both commands have a fixed argument of 0, the CRC is precalculated and is sent for these commands. For all other commands, a fake CRC of 0xFF
is sent (bit 0 have always to be 1).
Wait Until the Card is Ready
Before a command can be send, the application should check if a 0xFF
can be read, which indicates the card is ready. There are two methods to implement this. The first method waits until the card is ready for a command. You can pass a timeout in milliseconds as argument, if the card is ready in time, the method returns true
. If the time runs out you get false
.
/// Wait until the chip isn't busy anymore /// inline bool waitUntilReady(uint16_t timeoutMillis) { uint16_t startTime = millis(); do { const uint8_t result = spiReceive(); if (result == 0xff) { return true; } } while ((static_cast<uint16_t>(millis()) - startTime) < timeoutMillis); return false; }
There is a second method, which combines the sendCommand
and waitUntilReady
command. To make sure the SD card is ready before the command is sent.
/// Wait until the card is ready, then send the command. /// /// Same parameters as sendCommand(). /// inline uint8_t waitAndSendCommand(Command command, uint32_t argument, uint32_t *responseValue = 0) { waitUntilReady(300); return sendCommand(command, argument, responseValue); }
As you can see, the result of the waitUntilReady
is ignored, because the sendCommand
will anyway fail in this case.
The Response
Depending in the command, there are different responses. The specification is calling them R1
, R3
or R7
. There are more responses, but these are the most important. See the following illustration how the responses look like:

The R1
response is just one byte. The 7th bit is set to 0 to set it apart from the “busy” response were the 7th bit is set to 1. All other bits, except bit 0 indicate an error.
The R3
response starts like the R1
response with the status byte. It is followed by four bytes with the current operation condition register (OCR).
The R7
response is a special one. It is only used in combination with the “send if cond” command CMD8. You send a condition to the card, which responds with the same condition if it is supported. You also send a “check pattern” in the lowest 8 bits, which the card responds to indicate a success.
Initialisation Continued
Let us continue with the initialize()
method. At the current stage the SD card is in SPI mode. Now we can send commands to the card. As first command we send the “go idle state” command, which start the initialisation process of the card. This can take some time.
onlyChipSelectBegin(); // Send the CMD0 while (waitAndSendCommand(Cmd_GoIdleState, 0) != R1_IdleState) { if ((static_cast<uint16_t>(millis())-startTime) > initTimeout) { error = SDCard::Error_TimeOut; goto initFail; } }
The second command we send is the command “send if cond”, which is only supported with newer cards (SD card version 2). We have to send special argument 0x000001AA
and a correct CRC. The 0x_____1__
part of the argument checks if the voltage of 2.73.6V is accepted. The 0x______AA
part is a random check pattern. This could be any value.
If the command is supported and the voltage range is accepted, the card responds with the actual voltage range and our check pattern (0xAA). I my implementation I just compare the check pattern I sent.
// Try to send CMD8 to check SD Card version. result = waitAndSendCommand(Cmd_SendIfCond, 0x01aa, &responseValue); if ((result & R1_IllegalCommand) != 0) { cardType = CardTypeSD1; } else { if ((responseValue & 0x000000ff) != 0x000000aa) { error = SDCard::Error_SendIfCondFailed; goto initFail; } cardType = CardTypeSD2; }
For newer cards, we send the application command “send op cond” with bit 30 set. This is the HCS flag which enables fast transfer rates. By getting this bit to 1, we indicate that we support SDHC and SDCX cards.
// Send the ACMD41 to initialize the card. if (cardType == CardTypeSD2) { argument = 0x40000000; // Enable HCS Flag } else { argument = 0x00000000; } while (waitAndSendCommand(ACmd_SendOpCond, argument) != R1_ReadyState) { if ((static_cast<uint16_t>(millis())-startTime) > initTimeout) { error = SDCard::Error_TimeOut; goto initFail; } }
Next, I check if the card is a SDHC card. This is done with the “read OCR” command. Each SD card has an “operation condition register” short OCR, which contains interesting flags about the current condition of the SD card and can also be used to check the type of the card. Actually I do this for fun, there is no real use for the check. When I implemented the initialisation, I followed the flow diagram in the specification.
// Check if we have a SDHC card if (cardType == CardTypeSD2) { if (waitAndSendCommand(Cmd_ReadOCR, 0, &responseValue) != R1_ReadyState) { error = SDCard::Error_ReadOCRFailed; goto initFail; } // Check "Card Capacity Status (CCS)", bit 30 which is only valid // if the "Card power up status bit", bit 31 is set. if ((responseValue & 0xc0000000) != 0) { cardType = CardTypeSDHC; } }
On success, I set the block size to 512 byte. Newer cards have to block size fixed to 512 bytes. The set block size command is ignored. I set the block size to support old cards. Personally I do not own such old SD cards, so I could never test if the code is working with non SDHC cards or even SD cards version 1.
// Set the block size to 512byte. if (waitAndSendCommand(Cmd_SetBlockLenght, blockSize) != R1_ReadyState) { error = SDCard::Error_SetBlockLengthFailed; goto initFail; } onlyChipSelectEnd(); SPI.endTransaction();
On success, I set the chip select high, and end the SPI transaction. The rest of the method sets the SPI settings which are used for further SPI transactions (if enabled), debug outputs and error handling. I also set SPI.usingInterrupt(255)
, because I used the same code from timer interrupts.
// now rise the clock speed to maximum. spiSettings = SPISettings(32000000, MSBFIRST, SPI_MODE0); // Debug output #ifdef SDCARD_DEBUG SDC_DEBUG_PRINT(String(F("Card type "))); switch (cardType) { case CardTypeSD1: SDC_DEBUG_PRINTLN(String(F("SD1"))); break; case CardTypeSD2: SDC_DEBUG_PRINTLN(String(F("SD2"))); break; case CardTypeSDHC: SDC_DEBUG_PRINTLN(String(F("SDHC"))); break; } #endif // Make sure we can use SPI in the timer interrupt. SPI.usingInterrupt(255); // Using from timer. return SDCard::StatusReady; initFail: onlyChipSelectEnd(); SPI.endTransaction(); return SDCard::StatusError; }
This is everything you need to initialise the SD card and prepare it to read from it.
Continue with: Accessing the SD Card (Part 3)
1. As martin pointed out in a comment. The CRC codes for the initial commands are required to avoid an accidental initialisation of the SD card from static or other currents.
crc in cmd 0 and 8 is needed for determining if the card is in a device. since these cards can pickup static and other stuff there is a chance they might be initialized while you hav them on table or in your pocket. and it’s easier to add crc than taking the risk :3
A good point I did not think about. I edited the page and added the reason as footnote.
Hi,
I’d love to play with Micro-Disk image tools but the download for the Windows exectuable doesn’t seem to work properly. I’m sorry for posting this comment here but I couldn’t post a comment on the Micro Disk post page.
What exactly does not work?
Ah, I found the problem. Thank you for reporting it. I also enabled comments for the Micro Disk page and added SHA1 hashes to verify the downloaded file.
I had to move the downloads to GitHub, they should work now. I was not aware WordPress blocks executables and setups.
Hello, this has been an EXCELLENT tutorial and documentation on various difficult-to-find areas of arduino programming 🙂
i’m interested only in the sdcard code, and i began trying it out right away, however i’m using a DUE and i don’t know if the SPI ports are the same as the uno or a mega. my shield perfectly works with the sdfat library and i thought i’d move on to another library with my own format, just like your example. but i have difficulty initializing it, i include SPI and sdcard.h, i call the sdCard.initialize, but i only get as far as BEFORE getting a reply from CMD0. then i get initialisation error due to timeout. i’ve changed the PORT commands to digitalWrite(SDCARD_CSPINNUM,LOW); and HIGH for the begin and end chipselect functions respectively
i also replaced the select pin with mine (53)
any idea what could be wrong?
thank you for your time!
Hard to say without having a look to your project. Porting code to a different platform is always a challenge. Best you start with a logic analyser or oscilloscope and measure the signals you get at the SD card. Check if the chip select signal is issued, also check if the data lines are working as expected. Compare with a working library. Is everything is ok, consider it could be just a timing problem. My library depends on the millis() function to check for a timeout, if this function does not behave as expected – you can get a timeout for no reason. To debug this kind of problems, add serial outputs at no time critical places to see on your console how the code progresses and what kind of output you really get from the SD card. But at mentioned before, start making sure the hardware works as expected. Then systematically check what could went wrong.
i am positive the hardware works because i’ve been working with it the past 2 months with UTFT and sdFat, loading from and saving files to the card. i wish i had an oscillator to check what’s going on with the timings… if it is a timing problem how could i solve it? i’ve inserted prints in almost every line of sdcard.cpp and it’s stuck at sending the CMD0 command (i guess the card doesn’t get in SPI mode before?)
sorry for double posting, i’d happily share my code if it’s of any use. it’s basically just a setup and initialisation of the sd card, no more code added yet 🙂
I do not think sharing the code would help a lot. The code makes only sense in combination of the hardware — and to be honest: I can not fix the problems for you. Systematically searching for the problem is a very rewarding process – but you need some essential tools for it of course. Best would be, if you find someone experienced locally which could have a look. Maybe you can borrow a logic analyser or oscilloscope somewhere. Learning how to search and find the problem is essential and this is a great opportunity to grow this skill.
Thanks for making this tutorial.
I searched the key word “sd card pin define” and find this page. When I connected a card to my board with the pinout shown in this page, the whole system instantly got a shutdown and the SD card is then dead.
After checking for a while, I found it’s because the V_IN and the GND are shorted due to there is a typo in the pinout image: the pin3 (3.3V) and the pin4 (GND) are marked incorrectly. Pin3 should be GND and Pin4 should be 3.3V. It would be helpful if this image can be modified. This can save more SD cards’ life in the future… :p
Thank you very much for your comment. You were absolutely right, this is a bad mistake from my side. Sorry for all the troubles! I fixed the diagram, now it shows the correct pin assignment.
You listed “CMD37” as the APP CMD, but the command IDs are listed in decimal in the spec. The APP CMD is CMD55. 55 = x37 so that’s probably the root of the mixup.
Thank you, nice catch! Indeed, this was wrong. Happened while I converted the hex values into decimal values. I fixed the numbers in the article.
It is also wrong in the code for the cat protector project, the code works, because I use the decimal value 55 directly instead of the constant.