I think a lot of people have, at some point, had a memory card that has failed on them. Most of the time, due to their ubiquitous nature, it is going to have been some variation of an SD card, most likely a microSD card.
Unlike most people, I have always kept my broken memory cards in a little bag in the hopes that one day, in the far future, I might be able to get them working again. And slowly, over time, I have reclaimed nearly all of my "bad cards" from that little bag of despair and put them back into usage. All but one.
I have had a component in the box for reading SD cards for a while now, and the desire to do something with an ESP32 took me, and I wondered if I could get it hooked up to analyze my card.
At this point, nothing I have tried has ever been able to do anything useful with this card since it failed. Every device and operating system it has been in has either said it cannot be read, or doesnt even detect it. So I thought it was headed for the bin, it even had the dreaded permanent black marker spot of doom - the precursor for all my stuff that just has to be scrapped.
But, ever the optimist, I kept hanging on to it, after all it is so small...
The component in question is called a Micro SD Sniffer. It is essentially a bare MicroSD card reader with a passthrough that comes with a breakout for the pins. This allows you to pop a MicroSD in to the slot, then plug that into a host device. The breakout allows you to attach an oscilloscope to the lines from the card itself, so you can essentially see the traffic as the connection is established etc. It can help you establish if there are any problems, but it is much better as a learning tool in my experience.
This isnt an SPI SD Card breakout though, these are bare connections to the SD card itself, so this is a consideration you have to keep in mind when wiring this up.
![]() |
| the Micro SD Sniffer Passthrough |
As you can see from the image above, this has a lot more connections when compared to a regular SD card breakout board. This allows you to configure the component for many different for both SD and SPI mode directly. It turns out that all SD cards have two modes of operation, SD mode and SPI mode. SD mode allows for faster transfer rates and for things like SDIO, whilst SPI is a lot slower - but is almost universally supported by everything that has a memory card slot.
My doomed card is a Sandisk Ultra 64GB MicroSDXC 1 card, apparently, so this should win SD mode, but to begin with I am going to take a look at it in SPI mode - just to make sure I can read something from it.
Because nothing can read this in a normal fashion, I want to see what will happen when it is just told to do something, like return its manufacturing information, I can get that on a desktop operating system for example. To do this, I need to understand how SD cards work, and how they communicate. Thankfully, there is a very helpful set of lecture notes on this exact topic here: SD Card Lecture Notes that describe the process in which you can send raw SPI commands to an SD card and communicate with it over the SPI bus. It gives you complete examples on how to get the card to start listening, send commands and understand the response.
In addition to this, there is another great set of examples, complete with pseudo-code that helped me out a lot available here: https://nodeloop.org/guides. It provides all the information you need to create something that will send raw SPI commands over to an SD card.
One thing to point out here is that the Micro SD Sniffer isnt compatible with all micro-controllers, for instance this wont work correctly with an ESP8266 without modification.
Using a generic ESP32 development board, I can connect the SD sniffer to the SPI bus using pin 5 for chip select like this:
Using Arduino as the platform, we can actually send raw SPI commands to the ESP, together with the information from the references I mentioned above, I can create the raw SPI commands I need to speak to the SD card and see if it responds.
In order to even start speaking to the card, there are a number of things we are going to need to set up in the code first. As I am using a NodeMCU ESP32s clone here, I am going to go ahead and define the SPI pins. This will make it easy for anyone who wants to try this out for themselves. I am also going to go ahead and enumerate the SPI commands I need to use later on to make things a little easier to follow through:
#define PIN_CS 5
#define PIN_SCK 18
#define PIN_MISO 19
#define PIN_MOSI 23
Now I can go ahead and enumerate my SPI commands:
enum SDCommand : uint8_t {
CMD0 = 0, // GO_IDLE_STATE
CMD8 = 8, // SEND_IF_COND
CMD9 = 9, // SEND_CSD
CMD10 = 10, // SEND_CID
CMD41 = 41, // SD_SEND_OP_COND (ACMD41)
CMD55 = 55, // APP_CMD
CMD58 = 58 // READ_OCR
};
The code for this got big and messy very quickly, so enumerating these values is probably the most appropriate thing to do in this scenario. I dont need to change them at any point during the execution and they all need to be strongly typed, so I cant define them.bool begin() {
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, _csPin);
pinMode(_csPin, OUTPUT);
digitalWrite(_csPin, HIGH);
SPI.beginTransaction(_spiSettings);
bool ok = initCard();
SPI.endTransaction();
return ok;
}
On the first line, I start the SPI bus and provide the relevant information before moving on to bring the CS pin high so that it is ready to initialised in the next step. SPISettings _spiSettings;
This is an intentional design pattern, defining your SPI settings in this manner allows you to have multiple settings for SPI devices. For example, if I have a device or component that can be used in different ways on the SPI bus depending on how its settings are configured, then you can manage them in this manner._spiSettings(400000, MSBFIRST, SPI_MODE0)
From this point onwards, the card should be in the correct state to be initialised for communications over SPI. From here I go on to start sending commands to the card itself with initCard().Once this method has completed, it ends the SPI transaction, releasing the settings we had in place.for (int i = 0; i < 10; i++) SPI.transfer(0xFF);
Remember, begin() leaves the CS pin high before it calls initCard(), so all we have to do here is send over the required clock pulses via SPI to get the card to listen.
uint8_t r = 0xFF;
for (int i = 0; i < 5 && r != 0x01; i++) {
r = sendCmd(CMD0, 0, 0x95);
}
if (r != 0x01) return false;
This is the first point where we get to see some information, we are firing a command over via SPI, so the card could respond. If I test this with a card known to be working then the card should return r as 0x01. If it doesnt, then I want the method to return a false, which would return a false to begin().The purpose of this method is to collect various values and construct an SPI command that an SD card is capable of understanding. According to the notes above, "communications with an SD card is performed by sending commands to it and receiving responses from it". The structure of the command is 48 bits long, from looking ant initCard() above, you can see that I am not supplying any bytes when calling the method - the method takes those arguments and constructs our command for us. So far, we have already sent the command for the card to be reset into SPI mode, so sending CMD0 should return something when a working card is used.
uint8_t sendCmd(uint8_t cmd, uint32_t arg, uint8_t crc, uint8_t *tail = nullptr, uint8_t tailLen = 0) {
digitalWrite(_csPin, LOW);
SPI.transfer(0xFF); //this syns up the clocks.
SPI.transfer(0x40 | cmd);
SPI.transfer(arg >> 24);
SPI.transfer(arg >> 16);
SPI.transfer(arg >> 8);
SPI.transfer(arg);
SPI.transfer(crc);
uint8_t r = 0xFF;
for (int i = 0; i < 10; i++) {
r = SPI.transfer(0xFF);
if (!(r & 0x80)) break;
}
for (uint8_t i = 0; i < tailLen; i++) {
tail[i] = SPI.transfer(0xFF);
}
digitalWrite(_csPin, HIGH);
SPI.transfer(0xFF); //extra clock byte so the card releases the MISO line
return r;
}
The first three parameters need to be provided when the method is called, the last two have default values that can be overridden when needed. These provide the SPI number, the argument and the CRC needed for the command we are sending over.It does this by taking the parameters we used when calling the method and performing some bitwise maths. The reason why I have to do it this way is both complex and simple. SPI is what it name states, it is a serial interface. When we use transfer() using the SPI library, each bit is sent from the ESP to the SD card, which places those bits into a specific register. The card itself is able to determine where to store these bits, it has to act on the command and the argument it has received and provide a response - if it can. We have already told the card to work in SPI mode, so it knows to treat incoming bits as SPI communications.
If we take a look at the lecture notes from earlier again, we can see that the structure for an SD card command always starts with a 01, and is followed by a six bit command number. So, by creating an 8 bit value with 0x40 we start of with something we can work with. I am achieving this with the following line:
SPI.transfer(0x40 | cmd);
What I am doing here is creating the number 64, which in binary is 01000000 - the exact format we need for the first part of our SPI command. The bitwise or in the form of the pipe takes our cmd parameter and sets the remaining six bits of our value. So in the case of CMD55, it will set the remaining six bits to 110111, and for CMD0 it will set the remaining six bits as 000000.| Bit | Meaning when set |
|---|---|
| 7 | always 0 |
| 6 | parameter error |
| 5 | address error |
| 4 | erase sequence error |
| 3 | CRC error |
| 2 | illegal command |
| 1 | erase reset |
| 0 | card is in the idle state |
Spoiler, I do.
void printCardInfo() {
SPI.beginTransaction(_spiSettings);
uint8_t csd[16], cid[16];
// read the CSD, then work out the capacity for whichever format it is
if (!readRegister(CMD9, csd)) {
Serial.println("CSD read failed (see R1 data token.");
} else {
printHex("CSD Register: ", csd);
uint8_t csdVer = csd[0] >> 6;
Serial.printf("CSD structure version: %u\n", csdVer);
uint64_t capacityMB = 0;
if (csdVer == 1) {
// CSD v2 (SDHC/SDXC): capacity = (C_SIZE + 1) * 512 KB
uint32_t cSize = ((uint32_t)(csd[7] & 0x3F) << 16) | ((uint32_t)csd[8] << 8) | csd[9];
capacityMB = ((uint64_t)cSize + 1) / 2;
} else if (csdVer == 0) {
// CSD v1 (standard capacity, <= 2 GB)
uint32_t cSize = ((uint32_t)(csd[6] & 0x03) << 10) | ((uint32_t)csd[7] << 2) | (csd[8] >> 6);
uint8_t cSizeMult = ((csd[9] & 0x03) << 1) | (csd[10] >> 7);
uint8_t readBlLen = csd[5] & 0x0F;
uint64_t bytes = ((uint64_t)(cSize + 1)) << (cSizeMult + 2) << readBlLen;
capacityMB = bytes >> 20;
} else {
Serial.println("Unknown CSD structure version.");
}
if (capacityMB) {
Serial.printf("Capacity: %lu MB (~%d GB)\n", (unsigned long)capacityMB, (int)(capacityMB / 1000));
}
}
// reads the CID
if (readRegister(CMD10, cid)) {
printHex("CID Register: ", cid);
Serial.printf("Manufacturer ID: 0x%02X\n", cid[0]);
Serial.printf("OEM ID: %c%c\n", cid[1], cid[2]);
Serial.print("Product Name: ");
for (int i = 3; i <= 7; i++) Serial.write(cid[i]);
Serial.println();
uint32_t sn = ((uint32_t)cid[9] << 24) | ((uint32_t)cid[10] << 16) | ((uint32_t)cid[11] << 8) | cid[12];
Serial.printf("Serial Number: 0x%08lX\n", (unsigned long)sn);
} else {
Serial.println("Failed to read card identity from CID.");
}
SPI.endTransaction();
}
At the start you can I initiate the SPI transaction in the same manner as before, then I create two fixed length arrays of unsigned 8 bit integers with 16 elements each. One is for the response from the CSD register, the other is for the response from the CID register. From here, we break out of this method into readRegister(), its the same pattern I used with initCard() and sendCMD().
bool readRegister(uint8_t cmd, uint8_t *buf) {
digitalWrite(_csPin, LOW);
SPI.transfer(0xFF);
SPI.transfer(0x40 | cmd);
for (int i = 0; i < 4; i++) SPI.transfer(0x00);
SPI.transfer(0xFF);
uint8_t r = 0xFF;
for (int i = 0; i < 10; i++) {
r = SPI.transfer(0xFF);
if (!(r & 0x80)) break;
}
bool success = false;
Serial.printf("CMD%u R1 -> 0x%02X (Expected: 0x00)\n", cmd, r);
if (r == 0x00) {
uint32_t start = millis();
uint8_t token;
do {
token = SPI.transfer(0xFF);
} while (token == 0xFF && millis() - start < 200);
Serial.printf("CMD%u data token -> 0x%02X (Expected: 0xFE)\n", cmd, token);
if (token == 0xFE) { //this is our data ttoken
for (int i = 0; i < 16; i++) buf[i] = SPI.transfer(0xFF);
SPI.transfer(0xFF); //drop two bytes from the CRC across these lines.
SPI.transfer(0xFF);
success = true;
}
}
digitalWrite(_csPin, HIGH);
SPI.transfer(0xFF);
return success;
}
Logically, it does the same thing as sendCMD(), it sets the CS pin low to enable the card and then sends over a command number using the SPI bus using the same bitwise maths as before. After that, it waits for the response in the same manner.
if (r == 0x00) {
uint32_t start = millis();
uint8_t token;
do {
token = SPI.transfer(0xFF);
} while (token == 0xFF && millis() - start < 200);
Serial.printf("CMD%u data token -> 0x%02X (Expected: 0xFE)\n", cmd, token);
if (token == 0xFE) { //this is our data ttoken
for (int i = 0; i < 16; i++) buf[i] = SPI.transfer(0xFF);
SPI.transfer(0xFF); //drop two bytes from the CRC across these lines.
SPI.transfer(0xFF);
success = true;
I am retrieving a response from the card in the form of a command token. This token is specific, and we are looking out for just this value. According to the SD card specification, this value signifies the start of some data being sent from the card. If we dont receive this value, then we need to break the operation here and fail, but if we do get it we are free to carry on.
void printHex(const char *label, uint8_t *b) {
Serial.print(label);
for (int i = 0; i < 16; i++) Serial.printf("%02X ", b[i]);
Serial.println();
}
It just helps print hexadecimal.After keeping hold of this little card for all of those years, I am unable to do anything to bring it back to life. However, I have learned a great deal on the topic of SD cards, and how they can work. And what I have covered here isnt the extent of what you can do with an SD card, there are more commands available to work with. As well as SD mode which is entirely separate from SPI mode.
One thing I regret about this activity is not having a scope that would have let me see the communication take place between the card and the ESP itself, I have two basic ones but they didn't capture the visualizations I wanted.
I am definitely going to come back to this device to try and test out some different things. And I am definitely going to try it out with other micro-controllers.



