Zero to Photon:
SDController.v: a Verilog SD card controller
2024 . 4 . 24
Photon stores its photos on a 128 GB SD card. When capturing a photo, it's first written to SDRAM, and then transferred from SDRAM to the SD card via SDController.v.

SDController.v should work with any FPGA, but it's been specifically tested and used with the ICE40 and the Yosys/icestorm toolchain.


SDController.v Overview

SDController.v is a Verilog module that provides an interface for reading from / writing to SD cards via the 4-wire SDIO interface.

Photon uses a 102 MHz clock and the SDR104 access mode with SDController.v, resulting in a read/write throughput of 48 MB/sec.

In theory SDController.v can support a faster clock, but the signal integrity of the ICE40 IO pads degrades significantly beyond 102 MHz when wired to an SD card.


SDController.v Interface

SDController.v groups its IO signals into eight ports, described below.

Config Port

The Config port controls the clock speed, clock delay, and pin drive mode of SDController.v:
input wire      config_trigger,
input wire      config_action,
input wire      config_clkSpeed,
input wire[3:0] config_clkDelay,
input wire      config_pinMode,
  • input config_trigger: a toggle that initiates a configuration change
  • input config_action: controls what action is performed when config_trigger is toggled. There are only two options: Reset and Init.

    The Reset action resets SDController.v to its default state (clock speed = slow, clock delay = none, pin mode = open drain).

    The Init action configures SDController.v to use the values from config_* inputs, described below.

  • input config_clkSpeed: controls whether the slow clock (400 kHz) or the fast clock (configurable frequency; Photon uses 102 MHz) is used.

    The slow clock is needed for SD card initialization; once initialized, the fast clock is enabled for maximum throughput.

  • input config_clkDelay: controls the delay for the clock signal that's output to the SD card.

    This permits dynamic delay adjustment to account for FPGA signal delays.

  • input config_pinMode: controls whether SDController.v drives its IOs as push-pull or open-drain.

    For SD card initialization, open-drain IOs are used because SD cards expect 3.3V signalling at 400 kHz.

    After SD card initialization, push-pull IOs are used for 1.8V signalling with the fast clock.

Command Port

The Command port is used to issue commands to the SD card, such as CMD18 / READ_MULTIPLE_BLOCK and CMD25 / WRITE_MULTIPLE_BLOCK:
input wire       cmd_trigger,
input wire[47:0] cmd_data,
input wire[1:0]  cmd_respType,
input wire[1:0]  cmd_datInType,
output wire      cmd_done,
  • input cmd_trigger: a toggle that notifies SDController.v to initiate the command; other cmd_* inputs must be settled when cmd_trigger toggles
  • input cmd_data: the entire 48-bit command; the CRC7 portion is automatically overwritten with the proper CRC
  • input cmd_respType: the response type: none, 48-bit, or 136-bit
  • input cmd_datInType: whether and how the DatIn state machine should trigger as a result of the command

    There are three options: don't trigger DatIn, trigger DatIn for a 512x1 response (eg CMD6 response), or trigger DatIn for a 4096xN response (eg mass data read response)

  • output cmd_done: a toggle that signals when the command is finished sending

Response Port

The Response port contains the SD card's response for the previous command sent with the Command port:
output wire        resp_done,
output wire[135:0] resp_data,
output wire        resp_crcErr,
  • output resp_done: a toggle signalling that the response has been received from the SD card; indicates that the other resp_* signals are valid
  • output resp_data: the 48-bit or 136-bit response (depending on the original command) from the SD card
  • output resp_crcErr: whether there was a CRC7 error in the response received from the SD card

DatOut / DatOutRead Ports

The DatOut / DatOutRead ports are used to perform mass writes to the SD card. Data is read from the client's registers, and then flows out of SDController.v to the SD card.
input wire       datOut_trigger,
output wire      datOut_done,
output wire      datOut_crcErr,
                
output wire      datOutRead_clk,
input wire       datOutRead_ready,
output wire      datOutRead_trigger,
input wire[15:0] datOutRead_data,
input wire       datOutRead_done,
  • input datOut_trigger: toggle that starts the DatOut state machine
  • output datOut_done: signals that the DatOut state machine is finished
  • output datOut_crcErr: signals whether a CRC error occurred during the DatOut state machine
  • output datOutRead_clk: the clock for the DatOutRead port
  • input datOutRead_ready: signals when another SD block is available to be written; only checked at SD block boundaries (ie every 512 bytes)
  • output datOutRead_trigger: signals to the client that datOutRead_data should be updated with the next 16-bit word
  • input datOutRead_data: the next 16-bit word that should be written to the SD card
  • input datOutRead_done: signals that there's no more data to be written; only checked if datOutRead_ready=0

DatIn / DatInWrite Ports

The DatIn / DatInWrite ports are used to perform mass reads from the SD card. Data flows from the SD card into SDController.v, and is then written to the client's registers.
output wire       datIn_done,
output wire       datIn_crcErr,

output wire       datInWrite_rst,
output wire       datInWrite_clk,
input wire        datInWrite_ready,
output wire       datInWrite_trigger,
output wire[15:0] datInWrite_data,
  • output datIn_done: toggle that transitions after each SD block is written
  • output datIn_crcErr: signals whether a CRC error occurred during the DatIn state machine
  • output datInWrite_rst: a single-clock pulse that occurs when the DatIn state machine is started; can be used to reset client logic
  • output datInWrite_clk: the clock for the DatInWrite port
  • input datInWrite_ready: signals when another SD block can be accepted by the client; only checked at SD block boundaries (ie every 512 bytes)
  • output datInWrite_trigger: signals to the client that datInWrite_data contains the next 16-bit word
  • output datInWrite_data: the next 16-bit word that was read from the SD card

Status Port

The Status port contains a single output signal:
output wire status_dat0Idle
  • output status_dat0Idle: indicates the current state of the DAT[0] SD card signal

    This is used by clients to poll the SD card for completion of various operations, such as CMD11 / VOLTAGE_SWITCH and CMD12 / STOP_TRANSMISSION.


Voltage Switching

Traditional SD card initialization requires signalling to operate at 3.3V. Once initialized, the signalling voltage is typically dropped to 1.8V to support a higher clock frequency and data throughput. (Newer SD cards support LVS initialization which allows signalling to start at 1.8V, but many SD cards don't support LVS.)

This voltage-switching behavior is a pain to accomplish with FPGAs, but is possible with some discrete logic. Photon uses the following circuit to allow for low-speed (400 kHz) open-drain 3.3V signalling, in addition to high-speed (102 MHz) push-pull 1.8V signalling:

Photon's SD card 3.3V / 1.8V voltage-switching circuit
For SD card initialization, Photon lets SD_PULLUP_1V8_EN_ float to 1 (by way of the pull-up resistor), which sets the source voltage for the six 20k pull-up resistors to 3.3V, allowing for 3.3V open-drain signalling. (Photon actually uses 2.8V as the high voltage, but that's within the acceptable range for SD card 3.3V signalling.)

After SD card initialization, Photon drives SD_PULLUP_1V8_EN_ to 0, which sets the source voltage for the six 20k pull-ups to 1.8V. In this mode, Photon operates the SD_* signals in 1.8V push-pull mode.