Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

SD Card SPI Interface: Initialization, Commands, and Responses

Tech 3

SD cards are Flash-based storage modules widely used in embedded systems for logging runtime data and storing configuration. They expose two host interfaces: the native SD bus and the simpler SPI bus. This guide focuses on SPI mode, command framing, response handling, and a compact C driver demonstrating card initialization and block I/O.

Fundamentals

  • SD vs. MMC: While similar, MMC and SD have diffferent initialization sequences in SPI mode (notably CMD1 for MMC vs. ACMD41 for SD). Many hosts only support SD.
  • SD vs. TF (microSD): microSD uses the same protocol but has fewer pins than a full-size SD card, typically omitting one of the additional ground (VSS) pins.
  • Filesystems: The card stores raw blocks; filesystems (FAT, exFAT, etc.) are layered on top by the host and are not part of the card itself.
  • Controller features: The card internally manages wear leveling, bad blocks, ECC, power and clock control, and security logic, transparent to the host in SPI mode.

Pins and Electrical Notes

  • Full-size SD (SD mode): CLK, CMD, DAT0–DAT3, VDD, VSSx; microSD reduces pin count and typically has a single VSS.
  • SPI wiring to SD:
    • SCLK ↔ CLK
    • MOSI ↔ CMD (DI)
    • MISO ↔ DAT0 (DO)
    • CS ↔ DAT3 (pull-up can be used for card detect in SD mode)
  • If your PCB uses SD mode but leaves some data lines unused, pull them up to prevent floating lines from causing extra current and instability.

Commonly Readable Card Registers

  • OCR: Operation Conditions Register (voltage and CCS, via CMD58)
  • CID: Card Identification (manufacturer info, via CMD10)
  • CSD: Card-Specific Data (capacity/timing, via CMD9)
  • SCR: SD Configuration Register (capabilities/features, via ACMD51)
  • SD Status: Extended status (via ACMD13)

Command Frame on SPI

An SPI command is 6 bytes, clocked MSB first:

  • Byte 1: 0b01iiiiii (i = command index). Practically, send (0x40 | cmd).
  • Bytes 2–5: 32-bit argument, MSB first.
  • Byte 6: CRC7 in the upper 7 bits, LSB is always 1.

Notes on CRC in SPI mode:

  • CRC is disabled by default in SPI after reset, so only CMD0 needs a valid CRC (0x95). Many hosts also provide a proper CRC for CMD8 (0x87) for better interoperability.
  • CMD59 can enable/disable CRC in SPI mode if needed.

SPI-Mode Command Summary

  • CMD0: Go to idle state
  • CMD1: Initialize (MMC); SD cards typically use ACMD41 instead
  • CMD8: Send interface condition (check 2.0 support and voltage)
  • CMD9: Read CSD
  • CMD10: Read CID
  • CMD12: Stop transmission (multi-block read)
  • CMD13: Read card status
  • CMD16: Set block length (512 for SPI use)
  • CMD17: Read single block
  • CMD18: Read multiple blocks (terminate with CMD12)
  • CMD24: Write single block
  • CMD25: Write multiple blocks (terminate with a stop token)
  • CMD27: Program CSD
  • CMD28/CMD29: Set/clear write protection (address-group based)
  • CMD30: Send write protection status
  • CMD32/CMD33: Set first/last block address for erase range
  • CMD38: Erase selected blocks
  • CMD42: Lock/unlock
  • CMD55: Next command is application-specific (ACMD)
  • CMD56: Application-specific R/W data block
  • CMD58: Read OCR (check CCS for SDHC/SDXC)
  • CMD59: CRC on/off in SPI

Application (preceded by CMD55):

  • ACMD13: Read SD status
  • ACMD22: Number of written blocks (32-bit + CRC)
  • ACMD23: Set pre-erase block count (speeds up multi-block write)
  • ACMD41: SD initialization (HCS bit for SDHC/SDXC)
  • ACMD42: Connect/disconnect 50 kΩ pull-up on CD/DAT3
  • ACMD51: Read SCR

Response Types (SPI)

  • R1: 1 byte. Bit-encoded status (Idle, Erase Reset, Illegal Command, CRC Error, Erase Sequence Error, Address Error, Parameter Error)
  • R1b: R1 + busy time (card pulls MISO low until ready)
  • R2: 2-byte status (extended flags)
  • R3: OCR (R1 + 32-bit OCR), e.g., for CMD58
  • R7: Interface condition echo (R1 + 32-bit data), e.g., for CMD8

Initialization Flow (SPI)

  1. Configure MCU SPI and GPIO (CS output high, SPI slow ≤400 kHz initially).
  2. Supply clock with CS high for atleast 74 cycles (send ≥80 dummy clocks).
  3. Issue CMD0 with CRC 0x95 until R1 indicates Idle (0x01).
  4. Issue CMD8 with argument 0x1AA to probe SD v2.0 and voltage; check R7 echo.
  5. SD v2.x path:
    • Loop: CMD55; ACMD41 with HCS=1 until R1 == 0x00.
    • Issue CMD58; read OCR and CCS to distinguish SDHC/SDXC.
  6. SD v1.x/MMC path:
    • Try CMD55; ACMD41 without HCS; if it never goes ready, fall back to MMC with CMD1 until ready.
  7. For non-SDHC cards, set block length to 512 with CMD16.
  8. Increase SPI clock (e.g., to several MHz, within card limits) after initialization.

Reference Implementation: Minimal SPI SD Driver (C)

sdspi.h

#ifndef SDSPI_H
#define SDSPI_H

#include <stdint.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

// Card type flags
#define SDSPI_CARD_NONE     0x00
#define SDSPI_CARD_MMC      0x01
#define SDSPI_CARD_SDSC     0x02  // SD v1/v2, byte addressing
#define SDSPI_CARD_SDHC     0x04  // SDHC/SDXC, block addressing

// SPI-mode command indices
#define CMD0    0x00
#define CMD1    0x01
#define CMD8    0x08
#define CMD9    0x09
#define CMD10   0x0A
#define CMD12   0x0C
#define CMD13   0x0D
#define CMD16   0x10
#define CMD17   0x11
#define CMD18   0x12
#define CMD23   0x17
#define CMD24   0x18
#define CMD25   0x19
#define CMD55   0x37
#define CMD58   0x3A
#define CMD59   0x3B

// Data tokens
#define TOKEN_DATA_START       0xFE
#define TOKEN_WRITE_MULTI      0xFC
#define TOKEN_WRITE_STOP       0xFD

// R1 status bits
#define R1_IDLE                0x01
#define R1_ERASE_RESET         0x02
#define R1_ILLEGAL_CMD         0x04
#define R1_COM_CRC_ERR         0x08
#define R1_ERASE_SEQ_ERR       0x10
#define R1_ADDR_ERR            0x20
#define R1_PARAM_ERR           0x40

// API
int sdspi_init(void);
int sdspi_read_cid(uint8_t cid[16]);
int sdspi_read_csd(uint8_t csd[16]);
uint32_t sdspi_sector_count(void);
int sdspi_read_blocks(uint32_t lba, uint8_t *buf, uint32_t count);
int sdspi_write_blocks(uint32_t lba, const uint8_t *buf, uint32_t count);

#ifdef __cplusplus
}
#endif

#endif // SDSPI_H

sdspi.c

#include "sdspi.h"

// Board-specific SPI hooks (provide implementations elsewhere)
static void spi_set_slow(void);     // ≤400 kHz
static void spi_set_fast(void);     // up to card limit (e.g., 25 MHz)
static uint8_t spi_xfer(uint8_t x); // full-duplex transfer
static void cs_assert(void);        // drive CS low
static void cs_deassert(void);      // drive CS high
static void delay_short(void);      // small delay (e.g., a few hundred µs)

// Internal state
static uint8_t g_card_type = SDSPI_CARD_NONE;

static void clock_dummy_bytes(unsigned n)
{
    while (n--) (void)spi_xfer(0xFF);
}

static int wait_ready(uint32_t ticks)
{
    while (ticks--) {
        if (spi_xfer(0xFF) == 0xFF) return 0;
    }
    return -1;
}

static void deselect_bus(void)
{
    cs_deassert();
    spi_xfer(0xFF); // one extra 8 clocks
}

static int select_card(void)
{
    cs_assert();
    return wait_ready(0xFFFFFF) == 0 ? 0 : -1;
}

static uint8_t send_cmd_raw(uint8_t cmd, uint32_t arg, uint8_t crc)
{
    uint8_t r1;

    deselect_bus();
    if (select_card() != 0) return 0xFF;

    spi_xfer(0x40 | cmd);
    spi_xfer((uint8_t)(arg >> 24));
    spi_xfer((uint8_t)(arg >> 16));
    spi_xfer((uint8_t)(arg >> 8));
    spi_xfer((uint8_t)(arg));
    spi_xfer(crc);

    if (cmd == CMD12) spi_xfer(0xFF); // skip a stuff byte after CMD12

    for (int i = 0; i < 32; ++i) {
        r1 = spi_xfer(0xFF);
        if ((r1 & 0x80) == 0) break;
    }
    return r1;
}

static uint8_t send_cmd(uint8_t cmd, uint32_t arg)
{
    uint8_t crc = 0x01;
    if (cmd == CMD0) crc = 0x95;          // required when CRC is off
    else if (cmd == CMD8) crc = 0x87;     // valid CRC for 0x000001AA
    return send_cmd_raw(cmd, arg, crc);
}

static uint8_t send_acmd(uint8_t acmd, uint32_t arg)
{
    uint8_t r1 = send_cmd(CMD55, 0);
    if (r1 > 1) return r1; // CMD55 failed
    return send_cmd(acmd, arg);
}

static int read_data_block(uint8_t *buf, size_t n)
{
    // Wait for data token
    uint32_t t = 0xFFFFFF;
    uint8_t token;
    do {
        token = spi_xfer(0xFF);
    } while (token == 0xFF && --t);

    if (token != TOKEN_DATA_START) return -1;

    for (size_t i = 0; i < n; ++i) buf[i] = spi_xfer(0xFF);
    // discard CRC
    spi_xfer(0xFF); spi_xfer(0xFF);
    return 0;
}

static int write_data_block(const uint8_t *buf, uint8_t token)
{
    if (wait_ready(0xFFFFFF) != 0) return -1;

    spi_xfer(token);
    if (token != TOKEN_WRITE_STOP) {
        for (size_t i = 0; i < 512; ++i) spi_xfer(buf[i]);
        // dummy CRC
        spi_xfer(0xFF); spi_xfer(0xFF);

        uint8_t resp = spi_xfer(0xFF) & 0x1F;
        if (resp != 0x05) return -2; // data rejected
    }
    return 0;
}

int sdspi_init(void)
{
    uint8_t r1;
    uint8_t r7[4];

    // Hardware/SPI low-speed setup
    cs_deassert();
    spi_set_slow();

    // ≥ 74 clocks with CS high
    clock_dummy_bytes(10);

    // Go to idle
    for (int i = 0; i < 20; ++i) {
        r1 = send_cmd(CMD0, 0);
        if (r1 == R1_IDLE) break;
    }
    if (r1 != R1_IDLE) {
        deselect_bus();
        return -1;
    }

    g_card_type = SDSPI_CARD_NONE;

    // Probe v2.0 support
    r1 = send_cmd(CMD8, 0x1AA);
    if (r1 == R1_IDLE) {
        for (int i = 0; i < 4; ++i) r7[i] = spi_xfer(0xFF);
        if (r7[2] == 0x01 && r7[3] == 0xAA) {
            // ACMD41 with HCS until ready
            uint32_t retry = 0xFFFF;
            do {
                r1 = send_acmd(CMD41, 0x40000000UL);
            } while (r1 && --retry);

            if (retry && send_cmd(CMD58, 0) == 0) {
                uint8_t ocr[4];
                for (int i = 0; i < 4; ++i) ocr[i] = spi_xfer(0xFF);
                g_card_type = (ocr[0] & 0x40) ? SDSPI_CARD_SDHC : SDSPI_CARD_SDSC;
            }
        }
    }

    // v1.x or MMC fallback
    if (g_card_type == SDSPI_CARD_NONE) {
        r1 = send_acmd(CMD41, 0);
        if (r1 <= 1) {
            uint32_t retry = 0xFFFF;
            do {
                r1 = send_acmd(CMD41, 0);
            } while (r1 && --retry);
            g_card_type = SDSPI_CARD_SDSC;
        } else {
            uint32_t retry = 0xFFFF;
            do {
                r1 = send_cmd(CMD1, 0);
            } while (r1 && --retry);
            if (r1 == 0) g_card_type = SDSPI_CARD_MMC;
        }

        if (g_card_type != SDSPI_CARD_NONE) {
            if (send_cmd(CMD16, 512) != 0) g_card_type = SDSPI_CARD_NONE;
        }
    }

    deselect_bus();
    spi_set_fast();

    return (g_card_type == SDSPI_CARD_NONE) ? -2 : 0;
}

int sdspi_read_cid(uint8_t cid[16])
{
    if (!cid) return -1;
    uint8_t r1 = send_cmd(CMD10, 0);
    int rc = -2;
    if (r1 == 0) rc = read_data_block(cid, 16);
    deselect_bus();
    return rc;
}

int sdspi_read_csd(uint8_t csd[16])
{
    if (!csd) return -1;
    uint8_t r1 = send_cmd(CMD9, 0);
    int rc = -2;
    if (r1 == 0) rc = read_data_block(csd, 16);
    deselect_bus();
    return rc;
}

static uint32_t parse_capacity_from_csd(const uint8_t csd[16])
{
    // Returns number of 512-byte sectors, 0 on error
    if (!csd) return 0;

    if ((csd[0] & 0xC0) == 0x40) {
        // CSD v2.0 (SDHC/SDXC)
        uint16_t csize = ((uint16_t)csd[7] & 0x3F) << 16;
        csize |= ((uint16_t)csd[8]) << 8;
        csize |= csd[9];
        return ((uint32_t)csize + 1) << 10; // (C_SIZE+1)*1024 blocks
    } else {
        // CSD v1.0 (SDSC/MMC)
        uint16_t csize = ((uint16_t)(csd[6] & 0x03) << 10) | ((uint16_t)csd[7] << 2) | (csd[8] >> 6);
        uint8_t read_bl_len = csd[5] & 0x0F;
        uint16_t c_size_mult = ((uint16_t)((csd[9] & 0x03) << 1) | ((csd[10] & 0x80) >> 7));
        uint32_t block_len = 1UL << read_bl_len;
        uint32_t mult = 1UL << (c_size_mult + 2);
        uint32_t blocks = (uint32_t)(csize + 1) * mult;
        uint64_t capacity = (uint64_t)blocks * block_len; // bytes
        return (uint32_t)(capacity / 512ULL);
    }
}

uint32_t sdspi_sector_count(void)
{
    uint8_t csd[16];
    if (sdspi_read_csd(csd) != 0) return 0;
    return parse_capacity_from_csd(csd);
}

int sdspi_read_blocks(uint32_t lba, uint8_t *buf, uint32_t count)
{
    if (!buf || count == 0) return -1;

    uint8_t r1;
    uint32_t addr = lba;
    if (g_card_type != SDSPI_CARD_SDHC) addr <<= 9; // byte address for SDSC/MMC

    if (count == 1) {
        r1 = send_cmd(CMD17, addr);
        int rc = -2;
        if (r1 == 0) rc = read_data_block(buf, 512);
        deselect_bus();
        return rc;
    }

    r1 = send_cmd(CMD18, addr);
    if (r1 != 0) {
        deselect_bus();
        return -2;
    }

    for (uint32_t i = 0; i < count; ++i) {
        if (read_data_block(buf + 512 * i, 512) != 0) {
            break;
        }
    }

    send_cmd(CMD12, 0); // stop transmission
    deselect_bus();
    return 0;
}

int sdspi_write_blocks(uint32_t lba, const uint8_t *buf, uint32_t count)
{
    if (!buf || count == 0) return -1;

    uint8_t r1;
    uint32_t addr = lba;
    if (g_card_type != SDSPI_CARD_SDHC) addr <<= 9;

    if (count == 1) {
        r1 = send_cmd(CMD24, addr);
        int rc = -2;
        if (r1 == 0) rc = write_data_block(buf, TOKEN_DATA_START);
        deselect_bus();
        return rc;
    }

    // Pre-erase hint for SD (not MMC)
    if (g_card_type != SDSPI_CARD_MMC) {
        send_acmd(CMD23, count);
    }

    r1 = send_cmd(CMD25, addr);
    if (r1 != 0) {
        deselect_bus();
        return -2;
    }

    for (uint32_t i = 0; i < count; ++i) {
        if (write_data_block(buf + 512 * i, TOKEN_WRITE_MULTI) != 0) {
            deselect_bus();
            return -3;
        }
    }

    // Stop transmission
    (void)write_data_block(NULL, TOKEN_WRITE_STOP);
    deselect_bus();
    return 0;
}

Board binding stubs (example skeleton; implement per MCU):

// Example placeholders — implement with your SPI/GPIO HAL
static void spi_set_slow(void)  { /* set SPI prescaler for ≤400 kHz */ }
static void spi_set_fast(void)  { /* set SPI prescaler for high-speed */ }
static uint8_t spi_xfer(uint8_t x) { /* transfer byte */ return 0xFF; }
static void cs_assert(void)     { /* CS low */ }
static void cs_deassert(void)   { /* CS high */ }
static void delay_short(void)   { /* small delay */ }

Behavior notes:

  • For SDSC/MMC, addresses in CMD17/24 are byte addresses; for SDHC/SDXC, they are block-based (512-byte units).
  • When CRC is disabled in SPI mode, include valid CRC for CMD0 (0x95). Many hosts also use a valid CRC for CMD8 (0x87). Enable/disable CRC with CMD59 if required by your application.
  • After initialization, the block length should be 512 bytes (CMD16) for non-high-capacity cards. High-capacity cards always use 512-byte blocks.
  • Always provide extra clocks after deselecting to allow the card to finish internal operations.
Tags: embedded

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.