Project 11: SPI Driver for SD Card

Build an SPI driver that initializes an SD card, reads/writes sectors, and optionally implements FAT16/FAT32 to read actual files.

Quick Reference

Attribute Value
Difficulty Level 3 - Advanced
Time Estimate 2-3 weeks
Language C (primary), Rust (alternative)
Prerequisites Project 3 (bare-metal), Project 4 (peripheral experience), bit manipulation
Key Topics SPI Protocol, SD Card Commands, CRC Calculation, FAT Filesystem

1. Learning Objectives

After completing this project, you will:

  • Understand the SPI protocol at the bit level (MOSI, MISO, SCK, CS)
  • Master SD card initialization in SPI mode
  • Implement the SD command/response protocol
  • Handle different SD card types (SDSC, SDHC, SDXC)
  • Calculate CRC7 and CRC16 for commands and data
  • Read and write 512-byte sectors
  • Parse the FAT32 filesystem structure
  • Navigate directories and read files
  • Understand the MBR and partition tables

2. Theoretical Foundation

2.1 Core Concepts

The SPI Protocol

SPI (Serial Peripheral Interface) is a synchronous serial protocol using four signals:

SPI Bus Connections:
                    ┌─────────────────────┐
                    │                     │
    ┌───────────┐   │   ┌───────────┐     │
    │  Master   │   │   │   Slave   │     │
    │   (MCU)   │   │   │ (SD Card) │     │
    │           │   │   │           │     │
    │  MOSI  ───┼───┼───►  MOSI     │     │
    │  MISO  ◄──┼───┼───   MISO     │     │
    │  SCK   ───┼───┼───►  SCK      │     │
    │  CS    ───┼───┼───►  CS       │     │
    │           │   │   │           │     │
    └───────────┘   │   └───────────┘     │
                    │                     │
                    └─────────────────────┘

Signal Functions:
  MOSI (Master Out, Slave In)  - Data from master to slave
  MISO (Master In, Slave Out)  - Data from slave to master
  SCK  (Serial Clock)          - Clock signal from master
  CS   (Chip Select)           - Active low, selects slave

Data Transfer (CPOL=0, CPHA=0 - Mode 0):
       ┌───┐   ┌───┐   ┌───┐   ┌───┐
SCK ───┘   └───┘   └───┘   └───┘   └───
       │       │       │       │
MOSI ──┼─D7────┼─D6────┼─D5────┼─D4────...
       │       │       │       │
MISO ──┼─D7────┼─D6────┼─D5────┼─D4────...
       ↑       ↑       ↑       ↑
    Sample  Sample  Sample  Sample

Data is sampled on the rising edge of SCK

Unlike I2C, SPI is full-duplex: data can flow in both directions simultaneously. There’s no addressing - chip select signals determine which device is active.

SD Card SPI Mode

SD cards support two communication modes: native SD mode (4-bit parallel) and SPI mode. SPI mode is slower but simpler, requiring only 4 wires and no complex timing.

SD Card in SPI Mode:
┌─────────────────────────────────────────────────────────┐
│                    SD Card                              │
│                                                         │
│   Pin 1 (CS)  ◄── Directly connected to GPIO            │
│   Pin 2 (MOSI) ◄── SPI MOSI                             │
│   Pin 3 (VSS)  ── Ground                                │
│   Pin 4 (VDD)  ── 3.3V                                  │
│   Pin 5 (SCK)  ◄── SPI Clock                            │
│   Pin 6 (VSS)  ── Ground                                │
│   Pin 7 (MISO) ──► SPI MISO                             │
│                                                         │
│   Note: Pins 8 and 9 (DAT1, DAT2) not used in SPI mode  │
└─────────────────────────────────────────────────────────┘

MicroSD Card Pinout (SPI Mode):
     ┌───────────────────┐
     │  1  2  3  4  5  6 │
     │  7  8             │
     └───────────────────┘
     1: DAT2 (not used)    5: CLK (SCK)
     2: CD/DAT3 (CS)       6: VSS (GND)
     3: CMD (MOSI)         7: DAT0 (MISO)
     4: VDD (3.3V)         8: DAT1 (not used)

SD Card Command Format

All SD commands follow a fixed 6-byte format:

SD Command Frame (48 bits):
┌────────┬──────────────────────────────────┬──────────┐
│ Start  │         Argument                 │  CRC7    │
│  0 1   │                                  │   + 1    │
│ cmd[6] │        32 bits                   │  7 bits  │
└────────┴──────────────────────────────────┴──────────┘
  byte 0           bytes 1-4                  byte 5

Start bits: 01 (binary)
Command: 6-bit command number (0-63)
Argument: 32-bit command argument
CRC7: 7-bit CRC of first 40 bits
End bit: Always 1

Example - CMD0 (GO_IDLE_STATE):
  0x40 = 01 000000  (START + CMD0)
  0x00 0x00 0x00 0x00  (no argument)
  0x95 = CRC7 + stop bit (pre-calculated for CMD0)

SD Card Response Types

Response Formats:
┌─────────────────────────────────────────────────────────────────┐
│ R1 (1 byte) - Basic response                                    │
│ ┌─┬─┬─┬─┬─┬─┬─┬─┐                                               │
│ │0│P│A│E│S│C│E│I│  0=always 0, I=idle, others=error bits       │
│ └─┴─┴─┴─┴─┴─┴─┴─┘                                               │
│   Bit 0: In idle state (initialization in progress)             │
│   Bit 1: Erase reset                                            │
│   Bit 2: Illegal command                                        │
│   Bit 3: Communication CRC error                                │
│   Bit 4: Erase sequence error                                   │
│   Bit 5: Address error                                          │
│   Bit 6: Parameter error                                        │
├─────────────────────────────────────────────────────────────────┤
│ R3 (5 bytes) - OCR register response                            │
│ [R1][OCR - 4 bytes]                                             │
├─────────────────────────────────────────────────────────────────┤
│ R7 (5 bytes) - Interface condition response                     │
│ [R1][Command Version][Reserved][Voltage][Check Pattern]         │
└─────────────────────────────────────────────────────────────────┘

SD Card Initialization Sequence

SD Card SPI Mode Initialization:
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: Power Up                                                │
│   • Wait 1ms after power stable                                 │
│   • CS high, send 74+ clock pulses (10+ bytes of 0xFF)         │
│                                                                 │
│ Step 2: CMD0 - GO_IDLE_STATE                                    │
│   • CS low, send CMD0, CS high                                  │
│   • Expected R1 = 0x01 (in idle state)                         │
│                                                                 │
│ Step 3: CMD8 - SEND_IF_COND (identify card type)               │
│   • Argument: 0x000001AA (voltage + check pattern)             │
│   • SDv2+ cards respond with R7                                │
│   • SDv1 cards return illegal command error                    │
│                                                                 │
│ Step 4: ACMD41 - SD_SEND_OP_COND (start initialization)        │
│   • Requires CMD55 prefix (APP_CMD)                            │
│   • For SDHC: Argument includes HCS bit (0x40000000)           │
│   • Repeat until R1 = 0x00 (ready)                             │
│                                                                 │
│ Step 5: CMD58 - READ_OCR (check CCS bit for SDHC)              │
│   • CCS=1: SDHC/SDXC, uses block addressing                    │
│   • CCS=0: SDSC, uses byte addressing                          │
│                                                                 │
│ Step 6: Increase SPI speed to 25MHz                            │
└─────────────────────────────────────────────────────────────────┘

FAT32 Filesystem Structure

FAT32 Volume Layout:
┌─────────────────────────────────────────────────────────────────┐
│                    Master Boot Record (Sector 0)                │
│  • Boot code (446 bytes)                                        │
│  • Partition table (4 x 16 bytes)                               │
│  • Boot signature (0x55AA)                                      │
├─────────────────────────────────────────────────────────────────┤
│                    Reserved Sectors                             │
│  • Volume Boot Record (VBR) at partition start                  │
│  • FSInfo structure                                             │
│  • Backup boot sector                                           │
├─────────────────────────────────────────────────────────────────┤
│                    FAT Region                                   │
│  • FAT1 (primary)                                               │
│  • FAT2 (backup copy)                                           │
│  • Each FAT entry = 32 bits (FAT32)                            │
│  • Entry points to next cluster or end-of-chain marker         │
├─────────────────────────────────────────────────────────────────┤
│                    Data Region                                  │
│  • Cluster 2 = Root directory (FAT32)                          │
│  • Clusters 2-N = File/directory data                          │
│  • Cluster size = sectors per cluster * 512                    │
└─────────────────────────────────────────────────────────────────┘

FAT Entry Values (FAT32):
  0x00000000  = Free cluster
  0x00000001  = Reserved
  0x00000002 - 0x0FFFFFEF = Next cluster in chain
  0x0FFFFFF0 - 0x0FFFFFF6 = Reserved
  0x0FFFFFF7  = Bad cluster
  0x0FFFFFF8 - 0x0FFFFFFF = End of chain

2.2 Why This Matters

SD cards are the universal storage medium for embedded systems. From data loggers to cameras to game consoles, SD cards provide removable, high-capacity storage. Building an SD card driver teaches:

  • Protocol complexity: Multi-phase initialization with error handling
  • State machines: Managing card state through initialization
  • Filesystem internals: Understanding how files are organized on disk
  • Performance optimization: DMA, multi-block transfers, buffering

Industry usage:

  • Every digital camera, dash cam, and action camera uses SD cards
  • Data loggers in industrial, scientific, and automotive applications
  • Firmware updates loaded from SD cards
  • Game consoles store game data and saves

2.3 Historical Context

SD (Secure Digital) cards were introduced in 1999 by SanDisk, Panasonic, and Toshiba as an evolution of MMC (MultiMediaCard). Key milestones:

  • 1999: SD 1.0 - Up to 2GB, 25 MB/s
  • 2006: SDHC (High Capacity) - Up to 32GB, changed addressing
  • 2009: SDXC (Extended Capacity) - Up to 2TB, requires exFAT
  • 2017: SDUC (Ultra Capacity) - Up to 128TB

The SPI mode was included to enable simple interfacing with microcontrollers, though it sacrifices performance for simplicity. Many commercial products still use SPI mode for its straightforward implementation.

2.4 Common Misconceptions

Misconception 1: “SD cards are simple memory devices”

  • Reality: SD cards contain a sophisticated controller that manages wear leveling, error correction, and flash translation. You communicate with the controller, not raw flash.

Misconception 2: “All SD cards initialize the same way”

  • Reality: SDv1, SDv2 (SDSC), and SDHC/SDXC cards have different initialization sequences. You must detect the card type.

Misconception 3: “SPI mode is always 400kHz”

  • Reality: 400kHz is only for initialization. After init, you can switch to 25MHz or higher.

Misconception 4: “FAT32 is the only filesystem”

  • Reality: SDSC uses FAT12/FAT16, SDHC uses FAT32, SDXC uses exFAT by default (but can be formatted FAT32).

Misconception 5: “Sector addressing is simple multiplication”

  • Reality: SDSC uses byte addressing (sector * 512), while SDHC/SDXC uses block addressing (sector number directly).

3. Project Specification

3.1 What You Will Build

A complete SD card storage system that:

  • Implements SPI master mode
  • Initializes SD/SDHC cards in SPI mode
  • Reads and writes 512-byte sectors
  • Parses MBR and FAT32 structures
  • Lists directories and reads files
  • Works on STM32 or similar Cortex-M boards

3.2 Functional Requirements

  1. SPI driver: Initialize, transfer bytes, configure speed
  2. SD initialization: Full init sequence for SDSC and SDHC
  3. Card detection: Identify card type (SDSC, SDHC, SDXC)
  4. Sector operations: Read and write 512-byte sectors
  5. MBR parsing: Find FAT32 partition
  6. FAT32 support: Parse boot sector, FAT, root directory
  7. File operations: List directory, read file contents

3.3 Non-Functional Requirements

  1. Compatibility: Support SDSC and SDHC cards
  2. Speed: 25MHz SPI after initialization
  3. Reliability: Proper error handling and timeouts
  4. Memory: Minimize RAM usage (single sector buffer)
  5. Portability: Clean separation of SPI and SD layers

3.4 Example Usage / Output

=== SD Card Driver Test ===

Initializing SPI at 400kHz...
Sending CMD0 (GO_IDLE)... OK (R1=0x01)
Sending CMD8 (SEND_IF_COND)... OK, SDHC card detected
Sending ACMD41 (SD_SEND_OP_COND)...
  Attempt 1: busy
  Attempt 2: busy
  Attempt 3: ready!
Switching to 25MHz SPI mode

Card info:
  Type: SDHC
  Capacity: 16 GB (31116288 sectors)
  Manufacturer: SanDisk

Reading sector 0 (MBR)...
  Boot signature: 0x55AA
  Partition 1: FAT32, starts at sector 2048

Mounting FAT32...
  Sectors per cluster: 64
  Reserved sectors: 32
  FAT size: 15104 sectors
  Root directory: cluster 2

Directory listing of /:
  HELLO.TXT       42 bytes
  FIRMWARE.BIN    65536 bytes
  DATA/           <DIR>
  README.MD       1024 bytes

Reading HELLO.TXT:
"Hello from SD card!"

Write test:
  Writing "Test 12345" to TEST.TXT... OK
  Reading back... "Test 12345"

3.5 Real World Outcome

What success looks like:

  1. Working SPI communication: Bytes transfer correctly
  2. Card initialization: Both SDSC and SDHC cards initialize
  3. Sector access: Can read/write any sector
  4. File reading: Can read files from FAT32 filesystem
  5. Deep understanding: You can explain SD protocol and FAT32 structure

4. Solution Architecture

4.1 High-Level Design

┌─────────────────────────────────────────────────────────────────┐
│                    SD Card Storage System                        │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                  Application Layer                       │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │    │
│  │  │   File API  │  │  Directory  │  │   Format    │      │    │
│  │  │  file_read  │  │ dir_list    │  │  format_sd  │      │    │
│  │  │ file_write  │  │ dir_create  │  │             │      │    │
│  │  │  file_seek  │  │             │  │             │      │    │
│  │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘      │    │
│  └─────────│────────────────│────────────────│─────────────┘    │
│            │                │                │                   │
│            ▼                ▼                ▼                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                  Filesystem Layer                        │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │                    FAT32 Driver                    │  │    │
│  │  │  • MBR/partition parsing                          │  │    │
│  │  │  • Boot sector parsing                            │  │    │
│  │  │  • FAT chain traversal                            │  │    │
│  │  │  • Directory entry parsing                        │  │    │
│  │  │  • Cluster ↔ sector translation                   │  │    │
│  │  └───────────────────────────┬───────────────────────┘  │    │
│  └──────────────────────────────│──────────────────────────┘    │
│                                 │                                │
│                                 ▼                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                    SD Card Layer                         │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │  sd_init()      sd_read_sector()  sd_write_sector()│  │    │
│  │  │  sd_command()   sd_get_response()                  │  │    │
│  │  │  sd_wait_ready()                                   │  │    │
│  │  └───────────────────────────┬───────────────────────┘  │    │
│  └──────────────────────────────│──────────────────────────┘    │
│                                 │                                │
│                                 ▼                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                    SPI Driver Layer                      │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │  spi_init()      spi_transfer()  spi_set_speed() │  │    │
│  │  │  cs_low()        cs_high()                        │  │    │
│  │  └───────────────────────────┬───────────────────────┘  │    │
│  └──────────────────────────────│──────────────────────────┘    │
│                                 │                                │
│                                 ▼                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   Hardware Layer                         │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │  SPI Peripheral     GPIO (CS)    Clock Config     │  │    │
│  │  └───────────────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

4.2 Key Components

Component Purpose Key Functions
SPI Driver Low-level SPI communication spi_init(), spi_transfer()
SD Card Driver SD protocol implementation sd_init(), sd_read_sector()
FAT32 Driver Filesystem parsing fat_mount(), fat_read_file()
File API High-level file operations file_open(), file_read()
CRC Module CRC7 and CRC16 calculation crc7(), crc16()

4.3 Data Structures

// SPI configuration
typedef struct {
    SPI_TypeDef *spi;         // SPI peripheral
    GPIO_TypeDef *cs_port;    // CS GPIO port
    uint16_t cs_pin;          // CS pin number
    uint32_t speed_hz;        // Clock speed
} spi_config_t;

// SD card state
typedef struct {
    spi_config_t *spi;
    uint8_t card_type;        // SD_TYPE_SDSC, SD_TYPE_SDHC
    uint32_t capacity;        // Total sectors
    uint8_t initialized;
} sd_card_t;

// FAT32 volume information
typedef struct {
    sd_card_t *sd;
    uint32_t partition_start; // First sector of partition
    uint32_t fat_start;       // First sector of FAT
    uint32_t data_start;      // First sector of data region
    uint32_t root_cluster;    // Root directory cluster
    uint32_t sectors_per_cluster;
    uint32_t fat_size;        // FAT size in sectors
} fat32_t;

// Directory entry (32 bytes)
typedef struct __attribute__((packed)) {
    char name[8];             // 8.3 filename
    char ext[3];              // Extension
    uint8_t attr;             // Attributes
    uint8_t reserved;
    uint8_t create_time_ms;
    uint16_t create_time;
    uint16_t create_date;
    uint16_t access_date;
    uint16_t cluster_high;    // High 16 bits of cluster
    uint16_t modify_time;
    uint16_t modify_date;
    uint16_t cluster_low;     // Low 16 bits of cluster
    uint32_t file_size;
} fat_dir_entry_t;

// Sector buffer (512 bytes)
uint8_t sector_buffer[512];

4.4 Algorithm Overview

FUNCTION sd_init():
    // Phase 1: SPI slow mode
    spi_set_speed(400000)  // 400 kHz for init

    // Phase 2: Power up sequence
    cs_high()
    FOR i = 0 TO 9:
        spi_transfer(0xFF)  // 80+ clock pulses

    // Phase 3: Enter idle state
    cs_low()
    response = sd_command(CMD0, 0x00000000)
    IF response != 0x01:
        RETURN ERROR

    // Phase 4: Check card type (CMD8)
    response = sd_command(CMD8, 0x000001AA)
    IF response == 0x01:
        // SDv2 or later
        read_r7_response()
        card_type = SD_TYPE_V2

    // Phase 5: Initialize card (ACMD41)
    REPEAT (max 1000 times):
        sd_command(CMD55, 0)  // APP_CMD prefix
        IF card_type == SD_TYPE_V2:
            response = sd_command(ACMD41, 0x40000000)  // HCS bit
        ELSE:
            response = sd_command(ACMD41, 0)
        IF response == 0x00:
            BREAK
        delay_ms(10)

    // Phase 6: Check CCS bit for SDHC
    IF card_type == SD_TYPE_V2:
        sd_command(CMD58, 0)
        read_ocr()
        IF ocr.ccs == 1:
            card_type = SD_TYPE_SDHC

    // Phase 7: Switch to high speed
    spi_set_speed(25000000)  // 25 MHz

    RETURN SUCCESS


FUNCTION sd_read_sector(sector, buffer):
    // SDSC uses byte address, SDHC uses block address
    IF card_type == SD_TYPE_SDSC:
        address = sector * 512
    ELSE:
        address = sector

    // Send read command
    sd_command(CMD17, address)

    // Wait for data token
    REPEAT (max 10000 times):
        token = spi_transfer(0xFF)
        IF token == 0xFE:
            BREAK

    // Read 512 bytes
    FOR i = 0 TO 511:
        buffer[i] = spi_transfer(0xFF)

    // Read CRC (2 bytes, ignored)
    spi_transfer(0xFF)
    spi_transfer(0xFF)

    RETURN SUCCESS

5. Implementation Guide

5.1 Development Environment Setup

# Verify toolchain
$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Arm Embedded Toolchain) 12.2.1

# Create project directory
$ mkdir -p ~/projects/sd-card-driver
$ cd ~/projects/sd-card-driver

# Create initial file structure
$ touch main.c spi.c spi.h sd.c sd.h fat32.c fat32.h
$ touch startup.s linker.ld Makefile

# Hardware connections (STM32F4):
#   PA5 (SPI1_SCK)  ──── SD CLK
#   PA6 (SPI1_MISO) ──── SD MISO (DAT0)
#   PA7 (SPI1_MOSI) ──── SD MOSI (CMD)
#   PA4 (GPIO)      ──── SD CS (CD/DAT3)
#   3.3V            ──── SD VCC
#   GND             ──── SD GND

# Prepare test SD card:
$ # Format as FAT32
$ # Create test file: echo "Hello from SD card!" > HELLO.TXT

5.2 Project Structure

sd-card-driver/
├── main.c              # Demo application
├── spi.c               # SPI driver implementation
├── spi.h               # SPI driver interface
├── sd.c                # SD card driver
├── sd.h                # SD card interface
├── fat32.c             # FAT32 filesystem driver
├── fat32.h             # FAT32 interface
├── crc.c               # CRC7/CRC16 implementation
├── crc.h               # CRC interface
├── stm32f4xx.h         # Register definitions
├── startup.s           # Vector table and reset handler
├── linker.ld           # Linker script
├── Makefile
└── README.md

5.3 The Core Question You’re Answering

“How does a simple 4-wire interface enable communication with a complex storage device containing a filesystem, and how are files organized on that device?”

Before coding, understand: An SD card isn’t just memory - it’s a computer. It has a processor that manages flash wear, error correction, and protocol translation. Your job is to speak its protocol correctly.

5.4 Concepts You Must Understand First

Stop and research these before coding:

  1. SPI Timing Modes
    • What do CPOL and CPHA mean?
    • Which mode does SD card use?
    • Book Reference: “Making Embedded Systems” Chapter 9 - White
  2. SD Card States
    • What is “idle state” vs “ready state”?
    • Why does initialization require repeated commands?
    • Book Reference: SD Physical Layer Specification
  3. CRC Calculations
    • Why does SD card require CRC7 for commands?
    • When is CRC16 used?
    • Book Reference: Any embedded protocols book
  4. FAT32 Clusters
    • What is a cluster and why does it exist?
    • How does the FAT chain work?
    • Book Reference: “FAT32 File System Specification” - Microsoft

5.5 Questions to Guide Your Design

Before implementing, think through these:

  1. Initialization Handling
    • What if the card doesn’t respond to CMD0?
    • How do you distinguish SDSC from SDHC?
  2. Error Recovery
    • What if a read times out?
    • How do you handle CRC errors?
  3. Performance
    • Why start at 400kHz and switch to 25MHz?
    • Should you implement multi-block transfers?
  4. Filesystem Complexity
    • Do you need to support long filenames?
    • What about subdirectories?

5.6 Thinking Exercise

Trace the SD Init Sequence

Before coding, trace the complete initialization for an SDHC card:

Power-on:
  1. Wait 1ms for voltage stabilization
  2. CS=HIGH, send 80+ clock pulses
     - Card enters native SD mode

Enter SPI mode (CMD0):
  3. CS=LOW
  4. Send: 0x40 0x00 0x00 0x00 0x00 0x95
     - 0x40 = 01 + CMD0 (GO_IDLE_STATE)
     - 0x95 = Pre-calculated CRC7 for CMD0
  5. Wait for response (keep clocking with MOSI=0xFF)
  6. Response: 0x01 (in idle state, SPI mode active)
  7. CS=HIGH

Check card type (CMD8):
  8. CS=LOW
  9. Send: 0x48 0x00 0x00 0x01 0xAA 0x87
     - CMD8 with voltage range + check pattern
  10. Response: 0x01 0x00 0x00 0x01 0xAA
      - 0x01 = R1 (idle state)
      - 0x000001AA = echo back (SDv2+ card)
  11. CS=HIGH

Initialize (ACMD41, repeated):
  12. CS=LOW
  13. Send CMD55: 0x77 0x00 0x00 0x00 0x00 0x65
  14. Response: 0x01 (idle)
  15. Send ACMD41: 0x69 0x40 0x00 0x00 0x00 0x77
      - HCS bit set for SDHC support
  16. Response: 0x01 (still initializing)
  17. Repeat steps 12-16 until response = 0x00

Questions while tracing:
- Why send 0xFF when waiting for response?
- What does bit 0 of R1 mean?
- Why is ACMD41 repeated?

5.7 Hints in Layers

Hint 1: SPI Configuration

// Configure SPI1 for SD card (Mode 0: CPOL=0, CPHA=0)
void spi_init(void) {
    // Enable clocks
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // Configure GPIO: PA5=SCK, PA6=MISO, PA7=MOSI as AF5
    GPIOA->MODER &= ~((3<<10) | (3<<12) | (3<<14));
    GPIOA->MODER |= (2<<10) | (2<<12) | (2<<14);  // AF mode
    GPIOA->AFR[0] |= (5<<20) | (5<<24) | (5<<28); // AF5 = SPI1

    // Configure PA4 as CS output
    GPIOA->MODER &= ~(3<<8);
    GPIOA->MODER |= (1<<8);  // Output mode

    // Configure SPI: Master, software CS, clock/256 (slow for init)
    SPI1->CR1 = 0;
    SPI1->CR1 |= SPI_CR1_MSTR;      // Master mode
    SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // Software CS
    SPI1->CR1 |= (7 << 3);          // Baud rate: fPCLK/256
    SPI1->CR1 |= SPI_CR1_SPE;       // Enable SPI
}

uint8_t spi_transfer(uint8_t data) {
    while (!(SPI1->SR & SPI_SR_TXE));
    SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE));
    return SPI1->DR;
}

Hint 2: SD Command Function

uint8_t sd_command(uint8_t cmd, uint32_t arg) {
    uint8_t response;
    uint8_t crc = 0x01;  // Dummy CRC (CRC disabled after init)

    // Pre-calculated CRCs for init commands
    if (cmd == CMD0) crc = 0x95;
    if (cmd == CMD8) crc = 0x87;

    // Send command
    spi_transfer(0x40 | cmd);           // Command byte
    spi_transfer((arg >> 24) & 0xFF);   // Argument byte 3
    spi_transfer((arg >> 16) & 0xFF);   // Argument byte 2
    spi_transfer((arg >> 8) & 0xFF);    // Argument byte 1
    spi_transfer(arg & 0xFF);           // Argument byte 0
    spi_transfer(crc);                  // CRC + stop bit

    // Wait for response (up to 8 bytes)
    for (int i = 0; i < 8; i++) {
        response = spi_transfer(0xFF);
        if (!(response & 0x80)) break;  // Valid response starts with 0
    }

    return response;
}

Hint 3: Sector Read Function

bool sd_read_sector(uint32_t sector, uint8_t *buffer) {
    uint32_t addr = (card_type == SD_TYPE_SDHC) ? sector : sector * 512;

    cs_low();

    // Send READ_SINGLE_BLOCK command
    if (sd_command(CMD17, addr) != 0x00) {
        cs_high();
        return false;
    }

    // Wait for data token (0xFE)
    int timeout = 100000;
    while (spi_transfer(0xFF) != 0xFE) {
        if (--timeout == 0) {
            cs_high();
            return false;
        }
    }

    // Read 512 bytes
    for (int i = 0; i < 512; i++) {
        buffer[i] = spi_transfer(0xFF);
    }

    // Read and discard CRC
    spi_transfer(0xFF);
    spi_transfer(0xFF);

    cs_high();
    return true;
}

Hint 4: FAT32 Cluster to Sector

uint32_t cluster_to_sector(fat32_t *fat, uint32_t cluster) {
    // Clusters 0 and 1 are reserved
    // Cluster 2 = first cluster in data region
    return fat->data_start + (cluster - 2) * fat->sectors_per_cluster;
}

uint32_t get_next_cluster(fat32_t *fat, uint32_t cluster) {
    // Calculate which sector of FAT contains this entry
    uint32_t fat_offset = cluster * 4;  // 4 bytes per FAT32 entry
    uint32_t fat_sector = fat->fat_start + (fat_offset / 512);
    uint32_t entry_offset = fat_offset % 512;

    // Read FAT sector
    sd_read_sector(fat_sector, sector_buffer);

    // Extract 32-bit entry (little-endian)
    uint32_t next = sector_buffer[entry_offset]
                  | (sector_buffer[entry_offset + 1] << 8)
                  | (sector_buffer[entry_offset + 2] << 16)
                  | (sector_buffer[entry_offset + 3] << 24);

    return next & 0x0FFFFFFF;  // Mask off reserved bits
}

5.8 The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the difference between SDSC and SDHC cards”
    • SDSC: Up to 2GB, byte addressing, FAT12/FAT16
    • SDHC: 4GB-32GB, block addressing, FAT32
    • Block addressing simplifies driver (no multiply by 512)
  2. “Why does SD init start at 400kHz?”
    • SD spec requires <=400kHz during init
    • Card clock tree not fully configured yet
    • Ensures compatibility with all cards
    • After init, can switch to 25MHz or higher
  3. “What is the purpose of the FAT?”
    • File Allocation Table maps file clusters
    • Each entry points to next cluster or end-of-chain
    • Enables non-contiguous file storage
    • Two copies for redundancy
  4. “How would you handle a card that doesn’t respond?”
    • Check hardware connections
    • Verify CS line is working
    • Try longer init delays
    • Check voltage levels (3.3V required)
    • Try with known-working card
  5. “What’s the difference between CMD and ACMD?”
    • CMD = Standard command
    • ACMD = Application-specific command
    • ACMD requires CMD55 prefix
    • ACMD41 is actually two commands

5.9 Books That Will Help

Topic Book Chapter
SPI protocol “Making Embedded Systems” by White Ch. 9
SD card protocol SD Physical Layer Specification All
FAT filesystem “FAT32 File System Specification” All
Filesystem design “Operating Systems: Three Easy Pieces” Ch. 39-42

5.10 Implementation Phases

Phase 1: SPI Communication (2-3 hours)

  • Configure SPI peripheral and GPIO
  • Implement spi_transfer() function
  • Test by sending bytes and checking with logic analyzer

Phase 2: SD Initialization (4-6 hours)

  • Implement power-up sequence
  • Send CMD0, wait for response
  • Implement CMD8 for card type detection
  • Implement ACMD41 loop
  • Verify card enters ready state

Phase 3: Sector Read (3-4 hours)

  • Implement CMD17 (read single block)
  • Handle data token and timeout
  • Read 512 bytes into buffer
  • Test by reading sector 0 (MBR)

Phase 4: Sector Write (2-3 hours)

  • Implement CMD24 (write single block)
  • Send data token and 512 bytes
  • Wait for write complete
  • Verify write by reading back

Phase 5: FAT32 Parsing (6-8 hours)

  • Parse MBR to find partition
  • Parse boot sector for FAT parameters
  • Implement cluster chain following
  • Parse directory entries
  • Implement file reading

Phase 6: Demo and Polish (3-4 hours)

  • Create file listing demo
  • Read and display file contents
  • Add error messages
  • Document API

5.11 Key Implementation Decisions

Decision Trade-offs
Single vs multi-block Single: simpler. Multi: faster for large files
CRC checking With: reliable. Without: faster, simpler
Long filename support With: user-friendly. Without: simpler, less RAM
Write support With: full functionality. Without: simpler, safer
DMA transfers With: CPU free during transfer. Without: simpler

6. Testing Strategy

6.1 Unit Tests

Test Description Expected Result
SPI loopback Connect MOSI to MISO Sent byte = received byte
CS toggle Toggle CS, measure Clean transitions
Clock speed Measure SCK frequency 400kHz init, 25MHz after
Byte timing Time single transfer Consistent timing

6.2 Integration Tests

Test Command Expected Result
Card detect CMD0 R1 = 0x01
Card type CMD8 R7 with echo
Init complete ACMD41 R1 = 0x00
Read MBR Read sector 0 0x55AA at end
Read file Read known file Correct content
Write test Write and read back Data matches

6.3 Verification Commands

# Prepare test card on PC
$ sudo fdisk -l /dev/sdX
# Verify FAT32 partition

$ sudo xxd -l 512 /dev/sdX
# Check MBR contents

$ echo "Test content" > /mnt/sd/TEST.TXT
# Create test file

# On embedded system, verify:
# 1. MBR sector ends with 0x55AA
# 2. Partition table shows FAT32 (type 0x0B or 0x0C)
# 3. Can read boot sector
# 4. Can list root directory
# 5. Can read TEST.TXT and match content

7. Common Pitfalls & Debugging

Problem 1: “Card doesn’t respond to CMD0”

  • Why: No power-up clocks, wrong CS polarity, speed too fast
  • Fix: Send 80+ clocks with CS high first, verify CS active low
  • Debug: Use oscilloscope to check clock and CS signals

Problem 2: “CMD8 returns illegal command”

  • Why: This is a SDv1 card, not SDv2+
  • Fix: Skip CMD8, use different ACMD41 argument
  • Debug: Old/low-capacity cards may be SDv1

Problem 3: “ACMD41 never succeeds”

  • Why: Missing CMD55 prefix, wrong argument, timeout too short
  • Fix: Always send CMD55 before ACMD41, wait longer
  • Debug: Check response to CMD55 (should be 0x01)

Problem 4: “Read returns wrong data”

  • Why: SDSC byte addressing vs SDHC block addressing
  • Fix: For SDSC, multiply sector by 512. For SDHC, use sector directly
  • Debug: Print address being sent, verify card type detection

Problem 5: “FAT parsing fails”

  • Why: Wrong partition offset, endianness issues
  • Fix: Parse MBR correctly, use little-endian for all FAT structures
  • Debug: Print raw boot sector, compare with hex dump from PC

Problem 6: “File content is garbage”

  • Why: Wrong cluster calculation, cluster chain broken
  • Fix: Verify data_start calculation, check cluster-to-sector math
  • Debug: Manually calculate expected sector, read directly

8. Extensions & Challenges

8.1 Easy Extensions

Extension Description Learning
Card info Display manufacturer, size CID/CSD parsing
Multi-block read CMD18 for sequential reads Performance
Directory tree Recursive directory listing FAT navigation
File write Create and write files FAT modification

8.2 Advanced Challenges

Challenge Description Learning
DMA transfer Use DMA for SPI DMA peripheral
Long filenames Support VFAT LFN Complex FAT parsing
FAT16 support Support older/smaller cards Multiple FS formats
exFAT support Support SDXC cards Modern filesystem
Wear leveling analysis Measure card wear Flash characteristics
Boot from SD Load firmware from SD Bootloader integration

8.3 Research Topics

  • How do commercial SD drivers (FatFs, ChibiOS) achieve high performance?
  • What is SD card native mode and when would you use it?
  • How do flash translation layers work inside the SD controller?
  • What are the security features of SD cards (CPRM, CID)?

9. Real-World Connections

9.1 Production Systems Using This

System How It Uses SD Cards Notable Feature
Digital cameras Photo/video storage High-speed mode
3D printers G-code file loading FatFs library
Data loggers Sensor data recording Circular logging
Game consoles Save data, updates Multi-partition
Automotive Dash cams, music Reliable writes

9.2 How the Pros Do It

FatFs Library:

  • Universal FAT driver for embedded systems
  • Supports FAT12/16/32, exFAT, LFN
  • Highly configurable for RAM/ROM constraints

ChibiOS/HAL:

  • RTOS with SD card driver
  • DMA support, high performance
  • SDIO (native mode) support

Linux SD Driver:

  • Full MMC/SD/SDIO stack
  • Block device interface
  • Power management, hotplug

10. Self-Assessment Checklist

Before considering this project complete, verify:

  • I can explain the SPI protocol (MOSI, MISO, SCK, CS, modes)
  • My SPI driver transfers bytes correctly at multiple speeds
  • I understand the SD card initialization sequence and why each step matters
  • My driver correctly identifies SDSC vs SDHC cards
  • I can read any sector from the SD card
  • I can write sectors and verify the data
  • I understand FAT32 structure (MBR, boot sector, FAT, data region)
  • I can navigate the FAT chain to find file clusters
  • I can list directory contents
  • I can read file contents correctly
  • I handle errors (timeout, no card, CRC error) gracefully
  • I can answer all the interview questions listed above

Next Steps

After completing this project, you’ll be well-prepared for:

  • Project 13: DMA Audio Player - Load audio files from SD
  • Project 6: Bootloader - Load firmware from SD card
  • Game Console Capstone - Store game assets and save data

The SD card skills you’ve developed enable persistent storage for virtually any embedded application. Combined with I2C displays and other peripherals, you can build complete standalone systems.