Using an SD Card as Code Storage for RISC-V YRV on Tang Nano 9K FPGA
On budget-friendly FPGA boards like the Tang Nano 9K, limited synthesis resources constrain on-chip RAM—making it impractical to store full program code in block RAM. Only 16–20 KB is available for stack and data, sufficient for basic tests but not for complete firmware. This article proposes using an SD card as external read-only memory (ROM), with sector-based 512-byte reads—functionally similar to eXecute-In-Place (XIP) from QSPI Flash, but adapted to the constraints of SPI protocol.
The YRV core interfaces with memory via a 16-bit bus. Read/write logic is implemented in Verilog:
always @ (posedge clk) begin
if (mem_trans[0]) begin
mem_rdata[31:24] <= mcu_mem_bank3 [mem_addr[13:2]];
mem_rdata[23:16] <= mcu_mem_bank2 [mem_addr[13:2]];
mem_rdata[15:8] <= mcu_mem_bank1 [mem_addr[13:2]];
mem_rdata[7:0] <= mcu_mem_bank0 [mem_addr[13:2]];
end
if (mem_wr_byte[3]) mcu_mem_bank3 [mem_addr_reg[13:2]] <= mem_wdata[31:24];
if (mem_wr_byte[2]) mcu_mem_bank2 [mem_addr_reg[13:2]] <= mem_wdata[23:16];
if (mem_wr_byte[1]) mcu_mem_bank1 [mem_addr_reg[13:2]] <= mem_wdata[15:8];
if (mem_wr_byte[0]) mcu_mem_bank0 [mem_addr_reg[13:2]] <= mem_wdata[7:0];
end
State Machine for SD Sector Caching
RAM is reduced to just 512 bytes—one sector. When the CPU accesses an address within the currently cached sector, execution continues immediately with mem_ready asserted (analogous to HREADY in AHB-Lite). On a cache miss—i.e., when accessing outside the current sector—the finite state machine (FSM) triggers a new sector read from the SD card. A byte_cnt counter tracks read progress.
The FSM includes three states:
- IDLE: Waits for a memory access request.
- READ: Reads the requested 512-byte sector from the SD card.
- START: Initiates the read command (sends
rd).
The SD controller is derived from open-source FPGA-SDcard-Reader and MIT 6.111 projects, then adapted for the Tang Nano 9K’s SPI interface (DAT1/DAT2 lines left unconnected). Initialization runs at 400 kHz for robustness—no power-cycle reset required.
Integration into YRV Plus Project
Implemented in labs/99_experimental/99_03_yrv_sd, built atop the basics-graphics-music base. The configuration tang_nano_9k_tm1638_sd sets SD pins to 3.3V. Output uses TM1638: port0 drives BCD segments; port1 selects digit position.
Example assembly code for displaying "HELO" (each character stored in its own sector):
li s0, 0xFFFF0000 # port0 — segment data
li s1, 0xFFFF0002 # port1 — digit position
main_loop:
# 'H' (digit 3)
li t1, 0x8
sh t1, 0(s1)
.rept 100
nop
.endr
li t2, 0b00110111
sh t2, 0(s0)
.rept 1000
nop
.endr
# Repeat similarly for E, L, O
Firmware is written sector-by-sector using HxD.
Test Results:
- ~1,000 NOPs executed between characters (representing real workload).
- "HELO" displays clearly across four 7-segment digits.
- Reliable reset and initialization—no power cycling needed.
- Removing the SD card halts execution cleanly.
Key Takeaways
- SD cards in SPI mode support XIP-like execution when paired with a 512-byte sector cache.
- The FSM minimizes latency;
mem_readykeeps the CPU synchronized during reads. - Ideal for low-bandwidth applications where signal timing is in the millisecond range.
- For smoother performance, consider increasing the cache to 1–2 KB.
- Fully compatible with YRV cores on Gowin FPGAs—even without external SRAM.
Optimization Opportunities
Current implementation takes 6+ clock cycles per sector read (vs. 6 for QSPI). Expanding the cache to 2–4 sectors would reduce cache misses significantly. Multi-bank buffering—mirroring YRV’s original memory logic—is also feasible. For advanced developers: integrate with Wishbone or AXI adapters to scale this solution across larger SoC designs.
— Editorial Team
No comments yet.