SD Card Controller & Bootloader
The original HaDes-V system loaded programs exclusively via UART using a simple Intel HEX loader in the BootROM. This required a host PC to be tethered to the FPGA for every program upload. To make the system self-sufficient and make loading the large files needed for DOOM realistic, an SD card controller was added, allowing DOOM (and other programs) to be stored permanently on a micro SD card and loaded automatically at startup.
SPI Controller (wishbone_spi.sv)
The SD card is accessed through SPI mode 0 (CPOL=0, CPHA=0), implemented as a Wishbone-mapped SPI master peripheral.
Register Map
| Offset | Name | Access | Description |
|---|---|---|---|
| 0x00 | DATA | R/W | Write to transmit a byte, read to receive |
| 0x04 | STATUS | R | Bit 0: busy (1 = transaction in progress) |
| 0x08 | CTRL | R/W | Bit 0: CS (1 = active), Bits 15:8: clock divider |
State Machine
stateDiagram-v2
IDLE --> SHIFT: start_tx & !busy
SHIFT --> SHIFT: sclk_tick & bit_counter > 0<br/>(toggle SCLK, shift data)
SHIFT --> DONE: sclk_tick & bit_counter == 0
DONE --> IDLE: sclk_tick<br/>(capture rx data)
IDLE: SCLK held low, waits for a write to the DATA register. On start, loads the TX byte into the shift register, sets the first bit on MOSI, transitions to SHIFT.
SHIFT: On each SCLK tick (generated from a configurable clock divider), toggles the SCLK line. On the falling edge of SCLK, the next data bit is driven onto MOSI. On the rising edge, the MISO bit is shifted into the shift register. After 8 bits, transitions to DONE.
DONE: The received byte is captured from the shift register, busy is de-asserted, and the state returns to IDLE.
The clock divider defaults to 0x20 (32), providing a ~2.34 MHz SPI clock from the 75 MHz system clock. The software can reconfigure this to 0x02 (2) for ~37.5 MHz during high-speed data transfers after initialization.
BootROM V2.0 (boot_internal.c)
The BootROM was redesigned to support loading programs from SD card as the primary mechanism, with UART as a fallback.
Boot Flow
flowchart TD
A[Start BootROM] --> B[Initialize SPI]
B --> C[Detect SD Card]
C --> D{SD Ready?}
D -- Yes --> E[Read Sector 1]
E --> F{"Intel HEX marker ( : )?"}
F -- Yes --> G[Load program from SD]
F -- No --> H[Fallback to UART]
D -- No --> H
G --> I[Program DDR2]
I --> J[jump to entry]
H --> K[Receive Intel HEX over UART]
K --> I
SD detection: The bootloader duplicates the SD init sequence (CMD0 → CMD8 → ACMD41 polling) to keep the boot code self-contained, separate from the sd_card.c driver used after boot.
Sector-buffered loading: To avoid re-initializing the SD card on every byte, a 512-byte sector buffer is maintained. The bootloader reads one full sector at a time via CMD17, then serves bytes from the buffer until exhausted, at which point the next sector is fetched.
Intel HEX parser: Supports record types 0 (data), 1 (EOF), 2 (segment base address), 3 (start address, SEG), 4 (linear base address), and 5 (start address, LINEAR). Data is written directly to the target address using __program_byte(), which validates the address range:
static int __program_byte(uint32_t address, char data) {
if (&__ram_start <= adr && adr < &__boot_start) {
*adr = data; // BRAM
} else if (address >= DDR2_START && address < DDR2_START + DDR2_SIZE) {
*adr = data; // DDR2
} else {
return -1; // Invalid range
}
}
This allows programs to span both BRAM (for the bootloader and critical sections) and DDR2 (for the main program, WAD data, and framebuffers).
Fallback mode: If no SD card is detected or the card does not contain valid Intel HEX data in sector 1, the bootloader falls back to the original UART-based Intel HEX loading, printing a prompt for the user to send the HEX file over the serial connection.
SD Card Driver (sd_card.c)
The software driver provides two public functions used by the main application (including DOOM):
Initialization (sd_init)
The init sequence follows the standard SPI-mode SD card initialization:
- Wake-up: Send 80 dummy clock cycles with CS de-asserted to ensure the card is ready
- CMD0 (GO_IDLE_STATE, CRC 0x95): Reset the card to SPI mode. Must respond with
0x01(IDLE) - CMD8 (SEND_IF_COND, arg 0x1AA, CRC 0x87): Check voltage range and verify the card supports the required 2.7-3.6V range. The 0x1AA pattern confirms SDv2 compatibility
- ACMD41 (SD_SEND_OP_COND, arg 0x40000000): The HCS bit indicates host supports SDHC/SDXC (>2GB) cards. The command is polled repeatedly (CMD55 + ACMD41) until the card responds with
0x00(ready) or a timeout occurs - Switch to high speed: On success, configure the SPI clock divider from
0x40to0x02and release CS
The driver only supports SDHC/SDXC cards (capacity ≥ 2 GB) via the HCS bit. Legacy SDv1 cards are rejected during init.
Block Read (sd_read_block)
Reading a single 512-byte block follows the CMD17 protocol:
- Select the card and set high-speed clock
- Send CMD17 with the 32-bit block number and dummy CRC (0xFF)
- Wait for the start token (0xFE), with a 10000-iteration timeout
- Read 512 bytes into the provided buffer
- Read and discard the 2-byte CRC
- De-select the card and send one final dummy clock to release MISO