nd_storage¶
Source: Verilog/SD-FAT/circuit/nd_storage.v
Where it sits (Simulation): ND120_TOP > nd_storage_devices > nd_storage
- instance path: TAPE_SDFAT_SOURCE.u_nd_storage
Used in: nd_storage_devices (Simulation, Tang, Nexys, QMTECH)
Contains: nd_storage_cache (only on Simulation, Tang, Nexys), nd_storage_engine, nd_storage_mount, sd_file_reader, sd_writer
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance TAPE_SDFAT_SOURCE.u_nd_storage. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).
Description¶
nd_storage - multi-client storage facade (top level) THE one instance that owns the SD card and the SDRAM device region and serves N_CLIENTS client ports, each bound to one fixed root file on the FAT card (spec: docs/nd-storage-interface-spec.md; design: docs/nd-storage-design.md sections 2.1 and 5.1). Feature flag: SDFAT_STORAGE in sd_fat_features.vh (needs SDFAT_WRITE). Owns, exactly like the proven sd_fat_test_top arrangement: - sd_file_reader: held in reset except while a mount runs (rstn = rst & mount's rd_run) - every open is a full card re-init, the proven rewind/card-swap recovery. target_name/target_len are muxed from the FILEn_NAME/LEN parameters by the granted client. - sd_writer: always alive; the engine's write-through path drives its single-sector CMD24 interface (burst_len=0; rca comes from the reader's CMD3 export - the burst interface is a later step). Under SDFAT_STORAGE_CHECK the mount- time contiguity checker (nd_storage_fatchk.v) owns the writer's command pins (start/sector, rd_mode=1) while chk_busy - the mount FSM guarantees the engine and the checker never contend: the check runs inside the mount's exclusive card phase (after M_PARK set phase_write=1) while the engine is parked in E_OPEN. - SD pin mux by phase_write (mount FSM owned): reader owns the pins while a mount runs, the writer otherwise. Only _i/_o/_oe ports - the single tristate lives at the board top (repo rule). - mem port mux: the mount FSM owns the device port while mnt_busy (the engine is parked in E_OPEN then); the engine otherwise. Client map (04-AUG-2026): 0 = TAPE.BPUN (tape-400), 1/2 = FLOPPY1/2. IMG, 3..5 = SMD0..2.IMG, 6/7 = WD0/WD1.IMG (the ST506/8-inch Winchester, ND_WINCHESTER.v). Winchester images are WDn.IMG, NEVER SMDn.IMG. SMD3 was dropped to make room: the grant index is THREE bits so 8 clients is the ceiling. PHASE 4 - NOTHING IS PRELOADED. The 4 MB region used to hold a COPY of each image, which meant an image could never be larger than its slot and the mount refused anything bigger (256 KB for a Winchester, versus a real 75 MB disc). The region is now a CACHE: - CACHE_MASK[c]=1: client c goes through the shared tag directory (nd_storage_cache.v). Image size is bounded only by the 16-bit block count (128 MB), never by the region. - CACHE_MASK[c]=0: DIRECT. One shared staging line at STAGE_BASE_BLK - safe because the arbiter serves one client at a time - and every block request is fetched from the card. Tape and floppy are DIRECT: the card is quick enough and caching them would only spend region. A mount establishes GEOMETRY ONLY (size, first sector, cluster facts): sd_file_reader is run with no_stream=1, so it stops at the directory match. That also ends the run at a clean card boundary - parking the reader mid-transfer leaves the card streaming and the next card user fails. Status outputs: sd_status 0=NOTCHK 1=NOCARD 2=ERROR 3=OK (updated at every mount end, degraded to ERROR by an engine watchdog or a card write error; a later successful open restores OK). card_type/fs_type are LATCHED from the reader while it runs (the reader's own copies reset when the mount FSM parks it). Last reviewed: 11-JUL-2026 Ronny Hansen
Parameters¶
| Parameter | Default |
|---|---|
N_CLIENTS |
8 |
RD_CLK_DIV |
3'd2 |
WR_CLKDIV |
8'd1 |
USE_4BIT |
0 |
WD_MAX |
32'd270_000_000 |
SIMULATE |
0 |
CACHE_MASK |
8'b11111000 |
STAGE_BASE_BLK |
32'd0 |
POOL_BASE_BLK |
32'd1 |
CACHE_SETS |
256 |
CACHE_SETIDX |
8 |
CACHE_WAYS |
4 |
FILE0_NAME |
"TAPE.BPUN" |
FILE0_LEN |
8'd9 |
FILE1_NAME |
"FLOPPY1.IMG" |
FILE1_LEN |
8'd11 |
FILE2_NAME |
"FLOPPY2.IMG" |
FILE2_LEN |
8'd11 |
FILE3_NAME |
"SMD0.IMG" |
FILE3_LEN |
8'd8 |
FILE4_NAME |
"SMD1.IMG" |
FILE4_LEN |
8'd8 |
FILE5_NAME |
"SMD2.IMG" |
FILE5_LEN |
8'd8 |
FILE6_NAME |
"WD0.IMG" |
FILE6_LEN |
8'd7 |
FILE7_NAME |
"WD1.IMG" |
FILE7_LEN |
8'd7 |
SLOT0_BASE_BLK |
32'd0 |
SLOT0_SIZE_BLK |
32'd32 |
SLOT1_BASE_BLK |
32'd32 |
SLOT1_SIZE_BLK |
32'd640 |
SLOT2_BASE_BLK |
32'd672 |
SLOT2_SIZE_BLK |
32'd640 |
SLOT3_BASE_BLK |
32'd1312 |
SLOT3_SIZE_BLK |
32'd160 |
SLOT4_BASE_BLK |
32'd1472 |
SLOT4_SIZE_BLK |
32'd160 |
SLOT5_BASE_BLK |
32'd1632 |
SLOT5_SIZE_BLK |
32'd160 |
SLOT6_BASE_BLK |
32'd1792 |
SLOT6_SIZE_BLK |
32'd128 |
SLOT7_BASE_BLK |
32'd1920 |
SLOT7_SIZE_BLK |
32'd128 |
Ports¶
| Direction | Width | Name | Description |
|---|---|---|---|
| input | 1 |
clk_stor |
|
| input | 1 |
rst_stor_n (active low) |
|
| input | 1 |
clk_cpu |
|
| input | 1 |
rst_cpu_n (active low) |
|
| output | 1 |
sd_clk_o |
|
| input | 1 |
sd_cmd_i |
|
| output | 1 |
sd_cmd_o |
|
| output | 1 |
sd_cmd_oe |
|
| input | 1 |
sd_dat0_i |
|
| output | 1 |
sd_dat0_o |
|
| output | 1 |
sd_dat0_oe |
Verilog source¶
Verilog/SD-FAT/circuit/nd_storage.v on GitHub.
Show the Verilog of nd_storage (715 lines)
/****************************************************************************
** nd_storage - multi-client storage facade (top level) **
** **
** THE one instance that owns the SD card and the SDRAM device region **
** and serves N_CLIENTS client ports, each bound to one fixed root file **
** on the FAT card (spec: docs/nd-storage-interface-spec.md; design: **
** docs/nd-storage-design.md sections 2.1 and 5.1). Feature flag: **
** SDFAT_STORAGE in sd_fat_features.vh (needs SDFAT_WRITE). **
** **
** Owns, exactly like the proven sd_fat_test_top arrangement: **
** - sd_file_reader: held in reset except while a mount runs (rstn = **
** rst & mount's rd_run) - every open is a full card re-init, the **
** proven rewind/card-swap recovery. target_name/target_len are **
** muxed from the FILEn_NAME/LEN parameters by the granted client. **
** - sd_writer: always alive; the engine's write-through path drives **
** its single-sector CMD24 interface (burst_len=0; rca comes from **
** the reader's CMD3 export - the burst interface is a later step). **
** Under SDFAT_STORAGE_CHECK the mount- **
** time contiguity checker (nd_storage_fatchk.v) owns the writer's **
** command pins (start/sector, rd_mode=1) while chk_busy - the mount **
** FSM guarantees the engine and the checker never contend: the **
** check runs inside the mount's exclusive card phase (after M_PARK **
** set phase_write=1) while the engine is parked in E_OPEN. **
** - SD pin mux by phase_write (mount FSM owned): reader owns the pins **
** while a mount runs, the writer otherwise. Only _i/_o/_oe ports - **
** the single tristate lives at the board top (repo rule). **
** - mem port mux: the mount FSM owns the device port while mnt_busy **
** (the engine is parked in E_OPEN then); the engine otherwise. **
** **
** Client map (04-AUG-2026): 0 = TAPE.BPUN (tape-400), 1/2 = FLOPPY1/2. **
** IMG, 3..5 = SMD0..2.IMG, 6/7 = WD0/WD1.IMG (the ST506/8-inch **
** Winchester, ND_WINCHESTER.v). Winchester images are WDn.IMG, NEVER **
** SMDn.IMG. SMD3 was dropped to make room: the grant index is THREE bits **
** so 8 clients is the ceiling. **
** **
** PHASE 4 - NOTHING IS PRELOADED. The 4 MB region used to hold a COPY of **
** each image, which meant an image could never be larger than its slot **
** and the mount refused anything bigger (256 KB for a Winchester, versus **
** a real 75 MB disc). The region is now a CACHE: **
** - CACHE_MASK[c]=1: client c goes through the shared tag directory **
** (nd_storage_cache.v). Image size is bounded only by the 16-bit **
** block count (128 MB), never by the region. **
** - CACHE_MASK[c]=0: DIRECT. One shared staging line at STAGE_BASE_BLK **
** - safe because the arbiter serves one client at a time - and every **
** block request is fetched from the card. Tape and floppy are DIRECT: **
** the card is quick enough and caching them would only spend region. **
** A mount establishes GEOMETRY ONLY (size, first sector, cluster facts): **
** sd_file_reader is run with no_stream=1, so it stops at the directory **
** match. That also ends the run at a clean card boundary - parking the **
** reader mid-transfer leaves the card streaming and the next card user **
** fails. **
** **
** Status outputs: sd_status 0=NOTCHK 1=NOCARD 2=ERROR 3=OK (updated at **
** every mount end, degraded to ERROR by an engine watchdog or a card **
** write error; a later successful open restores OK). card_type/fs_type **
** are LATCHED from the reader while it runs (the reader's own copies **
** reset when the mount FSM parks it). **
** **
** Last reviewed: 11-JUL-2026 **
** Ronny Hansen **
*****************************************************************************/
`include "sd_fat_features.vh"
module nd_storage #(
parameter N_CLIENTS = 8,
parameter [2:0] RD_CLK_DIV = 3'd2, // sd_file_reader (25-50 MHz clk)
// Bit clock and bus width for the DATA path (sd_writer). The 2.7 MHz
// 1-bit default here was the bring-up setting; sd-fat-test proved
// CLKDIV=1 (13.5 MHz) + 4-bit on this exact board with a real SDHC card -
// READ 5981 KB/s vs the 137 KB/s baseline (docs/sd-cmd18-block-gap-research.md,
// 12-JUL-2026). USE_4BIT additionally needs DAT1-3 pinned and wired at
// the board top; it is a parameter so a board without them stays 1-bit.
parameter [7:0] WR_CLKDIV = 8'd1, // sd_writer bit clock divider
parameter integer USE_4BIT = 0, // 1 = 4-bit data bus
parameter [31:0] WD_MAX = 32'd270_000_000,
parameter SIMULATE = 0, // short SD init in sim
// Phase 4. CACHE_MASK[c]=1 -> client c is served through the shared
// cache directory and its image may be ANY size. 0 -> DIRECT: one shared
// staging line, every request fetched from the card. NOTHING is preloaded
// any more; PRELOAD_MASK is gone, and with it the "image must fit its
// slot" rule that capped a Winchester at 256 KB.
parameter [7:0] CACHE_MASK = 8'b11111000, // disc classes cached
// Region layout. One staging line at block 0 is enough for every DIRECT
// client because the arbiter serves exactly one client at a time and a
// DIRECT line never has to survive past its own operation.
parameter [31:0] STAGE_BASE_BLK = 32'd0,
parameter [31:0] POOL_BASE_BLK = 32'd1,
parameter CACHE_SETS = 256,
parameter CACHE_SETIDX = 8,
parameter CACHE_WAYS = 4,
parameter [52*8-1:0] FILE0_NAME = "TAPE.BPUN", parameter [7:0] FILE0_LEN = 8'd9,
parameter [52*8-1:0] FILE1_NAME = "FLOPPY1.IMG", parameter [7:0] FILE1_LEN = 8'd11,
parameter [52*8-1:0] FILE2_NAME = "FLOPPY2.IMG", parameter [7:0] FILE2_LEN = 8'd11,
parameter [52*8-1:0] FILE3_NAME = "SMD0.IMG", parameter [7:0] FILE3_LEN = 8'd8,
parameter [52*8-1:0] FILE4_NAME = "SMD1.IMG", parameter [7:0] FILE4_LEN = 8'd8,
parameter [52*8-1:0] FILE5_NAME = "SMD2.IMG", parameter [7:0] FILE5_LEN = 8'd8,
parameter [52*8-1:0] FILE6_NAME = "WD0.IMG", parameter [7:0] FILE6_LEN = 8'd7,
parameter [52*8-1:0] FILE7_NAME = "WD1.IMG", parameter [7:0] FILE7_LEN = 8'd7,
// slot map in 2048-byte blocks (design section 1.3 defaults)
parameter [31:0] SLOT0_BASE_BLK = 32'd0, parameter [31:0] SLOT0_SIZE_BLK = 32'd32,
parameter [31:0] SLOT1_BASE_BLK = 32'd32, parameter [31:0] SLOT1_SIZE_BLK = 32'd640,
parameter [31:0] SLOT2_BASE_BLK = 32'd672, parameter [31:0] SLOT2_SIZE_BLK = 32'd640,
parameter [31:0] SLOT3_BASE_BLK = 32'd1312, parameter [31:0] SLOT3_SIZE_BLK = 32'd160,
parameter [31:0] SLOT4_BASE_BLK = 32'd1472, parameter [31:0] SLOT4_SIZE_BLK = 32'd160,
parameter [31:0] SLOT5_BASE_BLK = 32'd1632, parameter [31:0] SLOT5_SIZE_BLK = 32'd160,
// SLOTn_* are VESTIGIAL under Phase 4: a slot no longer bounds an image,
// because the region caches blocks instead of holding them. The region
// still cannot exceed 2048 blocks - s_blk_abs in nd_storage_engine is
// [10:0] and feeds mem_addr = {blk_abs, word}, and widening that reaches
// into ND120_CORE, ND3202D, MEM_43 and the SDRAM bridge - but that is
// now a limit on CACHE SIZE, not on image size. The live layout knobs
// are STAGE_BASE_BLK / POOL_BASE_BLK / CACHE_SETS / CACHE_WAYS above.
parameter [31:0] SLOT6_BASE_BLK = 32'd1792, parameter [31:0] SLOT6_SIZE_BLK = 32'd128,
parameter [31:0] SLOT7_BASE_BLK = 32'd1920, parameter [31:0] SLOT7_SIZE_BLK = 32'd128
) (
input wire clk_stor,
input wire rst_stor_n,
input wire clk_cpu,
input wire rst_cpu_n,
// ---- SD pads (single tristate at the board top, repo rule) ----
output wire sd_clk_o,
input wire sd_cmd_i,
output wire sd_cmd_o,
output wire sd_cmd_oe,
input wire sd_dat0_i,
output wire sd_dat0_o,
output wire sd_dat0_oe,
// DAT1-3: driven only in 4-bit mode (USE_4BIT); released otherwise, and
// the slot's external pull-ups hold them high.
//! DEBUG stage-timer taps (24-AUG-2026), see nd_storage_engine.
output wire dbg_sd_busy,
output wire dbg_cache_pend,
input wire sd_dat1_i,
output wire sd_dat1_o,
output wire sd_dat1_oe,
input wire sd_dat2_i,
output wire sd_dat2_o,
output wire sd_dat2_oe,
input wire sd_dat3_i,
output wire sd_dat3_o,
output wire sd_dat3_oe,
// ---- SDRAM device port (clk_stor domain, design section 5.2) ----
output wire mem_start, // 1-cycle pulse, only when mem_busy=0
output wire mem_we,
output wire [19:0] mem_addr, // 32-bit-word address inside the region
output wire [31:0] mem_wdata,
input wire [31:0] mem_rdata, // valid at mem_done, then held
input wire mem_busy,
input wire mem_done, // 1-cycle pulse
// ---- client ports (clk_cpu domain, flattened; spec section 4) ----
input wire [N_CLIENTS-1:0] open_req,
output wire [N_CLIENTS-1:0] open_ok,
output wire [N_CLIENTS-1:0] open_err,
output wire [N_CLIENTS*32-1:0] size_bytes,
input wire [N_CLIENTS-1:0] req,
input wire [N_CLIENTS-1:0] wr,
input wire [N_CLIENTS*16-1:0] block,
output wire [N_CLIENTS-1:0] busy,
output wire [N_CLIENTS-1:0] done,
output wire [N_CLIENTS-1:0] err,
// WHY the operation failed, valid with done when err=1. Codes and the
// rule they enforce are in nd_storage_status.vh: an operation either
// moves the data or completes with err and a reason - never a silent
// success, never a completion that does not arrive.
output wire [N_CLIENTS*4-1:0] err_code,
// Fill-path diagnostic seam, forwarded from nd_storage_engine.v. Pure
// observation - leave unconnected in builds that do not probe it.
output wire [4:0] dbg_state,
output wire [31:0] dbg_lba,
output wire [15:0] dbg_wdata,
output wire [15:0] dbg_rdata,
output wire [15:0] dbg_bufw,
output wire dbg_bufwe,
output wire [15:0] dbg_fsec,
output wire dbg_rx_stb,
output wire [7:0] dbg_rx_raw,
output wire [7:0] dbg_rx_byte,
output wire dbg_past_eof,
output wire [2:0] dbg_grant,
output wire [N_CLIENTS*10-1:0] buf_addr,
output wire [N_CLIENTS*16-1:0] buf_wdata,
output wire [N_CLIENTS-1:0] buf_we,
input wire [N_CLIENTS*16-1:0] buf_rdata,
// ---- status (board LEDs / console, spec section 7) ----
output reg [1:0] sd_status, // 0 NOTCHK, 1 NOCARD, 2 ERROR, 3 OK
output reg [1:0] card_type, // latched: 1 SDv1, 2 SDv2, 3 SDHCv2
output reg [1:0] fs_type // latched: 2 FAT16, 3 FAT32
);
// ------------------------------------------------------- target file names
// FILEn_NAME literals are left-justified; the reader wants byte 0 in the
// low byte. Same reversal generate as sd_fat_test_top's g_target_names.
wire [52*8-1:0] tgt0, tgt1, tgt2, tgt3, tgt4, tgt5, tgt6, tgt7;
generate
genvar tk;
for (tk = 0; tk < 52; tk = tk + 1) begin : g_target_names
assign tgt0[8*tk+:8] = (tk < FILE0_LEN) ? FILE0_NAME[8*(FILE0_LEN-1-tk)+:8] : 8'h00;
assign tgt1[8*tk+:8] = (tk < FILE1_LEN) ? FILE1_NAME[8*(FILE1_LEN-1-tk)+:8] : 8'h00;
assign tgt2[8*tk+:8] = (tk < FILE2_LEN) ? FILE2_NAME[8*(FILE2_LEN-1-tk)+:8] : 8'h00;
assign tgt3[8*tk+:8] = (tk < FILE3_LEN) ? FILE3_NAME[8*(FILE3_LEN-1-tk)+:8] : 8'h00;
assign tgt4[8*tk+:8] = (tk < FILE4_LEN) ? FILE4_NAME[8*(FILE4_LEN-1-tk)+:8] : 8'h00;
assign tgt5[8*tk+:8] = (tk < FILE5_LEN) ? FILE5_NAME[8*(FILE5_LEN-1-tk)+:8] : 8'h00;
assign tgt6[8*tk+:8] = (tk < FILE6_LEN) ? FILE6_NAME[8*(FILE6_LEN-1-tk)+:8] : 8'h00;
assign tgt7[8*tk+:8] = (tk < FILE7_LEN) ? FILE7_NAME[8*(FILE7_LEN-1-tk)+:8] : 8'h00;
end
endgenerate
wire [2:0] s_cur_client; // mount FSM's latched client (stable per open)
reg [52*8-1:0] s_target_name;
reg [7:0] s_target_len;
always @(*) begin
case (s_cur_client)
3'd0: begin s_target_name = tgt0; s_target_len = FILE0_LEN; end
3'd1: begin s_target_name = tgt1; s_target_len = FILE1_LEN; end
3'd2: begin s_target_name = tgt2; s_target_len = FILE2_LEN; end
3'd3: begin s_target_name = tgt3; s_target_len = FILE3_LEN; end
3'd4: begin s_target_name = tgt4; s_target_len = FILE4_LEN; end
3'd5: begin s_target_name = tgt5; s_target_len = FILE5_LEN; end
3'd6: begin s_target_name = tgt6; s_target_len = FILE6_LEN; end
default: begin s_target_name = tgt7; s_target_len = FILE7_LEN; end
endcase
end
// ------------------------------------------------------------- SD reader
wire s_rd_run; // mount FSM: release the reader's reset
wire s_phase_write; // mount FSM: writer owns the SD pins
wire rd_rstn = rst_stor_n & s_rd_run;
wire rd_sdclk;
wire rd_cmd_o, rd_cmd_oe;
wire [3:0] rd_card_stat;
wire [1:0] rd_card_type;
wire [1:0] rd_fs_type;
wire rd_file_found;
wire rd_outen;
wire [7:0] rd_outbyte;
wire rd_scan_done;
wire [31:0] rd_file_size;
wire [31:0] rd_first_sector;
wire [7:0] rd_fs_cs;
wire [31:0] rd_fs_fat0;
wire [31:0] rd_found_cluster;
wire [15:0] rd_card_rca;
sd_file_reader #(
.CLK_DIV (RD_CLK_DIV),
.SIMULATE(SIMULATE)
) u_reader (
.rstn (rd_rstn),
.clk (clk_stor),
.sdclk (rd_sdclk),
.sdcmd_i (sd_cmd_i),
.sdcmd_o (rd_cmd_o),
.sdcmd_oe (rd_cmd_oe),
.sddat0 (sd_dat0_i),
.card_stat (rd_card_stat),
.card_type (rd_card_type),
.filesystem_type (rd_fs_type),
.file_found (rd_file_found),
.outen (rd_outen),
.outbyte (rd_outbyte),
.scan_done (rd_scan_done),
.found_file_size (rd_file_size),
.target_name (s_target_name),
.no_stream (1'b1), // geometry only: blocks come from the cache
.target_len (s_target_len),
.dir_entry_valid (),
.dir_entry_name (),
.dir_entry_len (),
.dir_entry_size (),
.dir_entry_date (),
.dir_entry_cluster (),
.dir_entry_is_dir (),
.found_file_first_sector(rd_first_sector),
.fs_cluster_size (rd_fs_cs),
.fs_fat0_sector (rd_fs_fat0),
.fs_sectors_per_fat (),
.fs_num_fats (),
.fs_data_base_sector (),
.fs_total_sectors (),
.fs_root_cluster (),
.found_dir_entry_sector (),
.found_dir_entry_index (),
.found_file_cluster (rd_found_cluster),
.card_rca (rd_card_rca)
);
// ---------------------------------------------------- card RCA for CMD55
// The card publishes its relative address in the R6 answer to CMD3, and it
// is NEVER zero (zero is reserved - CMD7 uses it to deselect). CMD55
// (APP_CMD) carries that address in arg[31:16]; with the wrong value the
// card does not enter application-command mode and the ACMD that follows is
// taken as an ordinary command. The 4-bit switch (CMD55 + ACMD6) therefore
// depends on this being right.
//
// The reader is held in reset between mounts (every open is a full card
// re-init), which clears its rca back to 0, so latch the last published
// non-zero value and hand THAT to the writer. A re-init republishes through
// the same CMD3, so the latch tracks the current card.
reg [15:0] s_card_rca;
always @(posedge clk_stor or negedge rst_stor_n) begin
if (!rst_stor_n) s_card_rca <= 16'd0;
else if (rd_card_rca != 16'd0) s_card_rca <= rd_card_rca;
end
// ------------------------------------------------------------- SD writer
// Engine write-through path, single-sector CMD24 API. burst_len/rca are
// tied off (bursts are a later optimization). The command pins are muxed:
// the fatchk contiguity checker owns them (rd_mode=1, CMD17 FAT reads)
// while chk_busy, the engine otherwise - never both (mount FSM contract).
wire wr_sdclk;
wire wr_cmd_o, wr_cmd_oe, wr_dat0_o, wr_dat0_oe;
wire sdw_start, sdw_rd_mode, sdw_busy, sdw_done, sdw_err;
wire wr_dat1_o, wr_dat1_oe;
wire wr_dat2_o, wr_dat2_oe;
wire wr_dat3_o, wr_dat3_oe;
wire [8:0] sdw_burst_len;
wire e_sdw_rd_mode;
wire [8:0] e_sdw_burst_len;
wire [31:0] sdw_sector;
wire [8:0] sdw_rd_addr;
wire [7:0] sdw_rd_data;
wire sdw_rx_we;
wire [8:0] sdw_rx_addr;
wire [7:0] sdw_rx_data;
wire e_sdw_start; // engine's command set
wire [31:0] e_sdw_sector;
sd_writer #(
.CLKDIV(WR_CLKDIV)
) u_writer (
.clk (clk_stor),
.rst_n (rst_stor_n),
.sd_clk_o (wr_sdclk),
.sd_cmd_i (sd_cmd_i),
.sd_cmd_o (wr_cmd_o),
.sd_cmd_oe (wr_cmd_oe),
.sd_dat0_i (sd_dat0_i),
.sd_dat0_o (wr_dat0_o),
.sd_dat0_oe(wr_dat0_oe),
.sd_dat1_i (sd_dat1_i),
.sd_dat1_o (wr_dat1_o),
.sd_dat1_oe(wr_dat1_oe),
.sd_dat2_i (sd_dat2_i),
.sd_dat2_o (wr_dat2_o),
.sd_dat2_oe(wr_dat2_oe),
.sd_dat3_i (sd_dat3_i),
.sd_dat3_o (wr_dat3_o),
.sd_dat3_oe(wr_dat3_oe),
.use_4bit (USE_4BIT != 0),
// The reader does a FULL card re-init whenever the mount FSM releases
// its reset, and CMD0 returns the card to 1-bit. So the negotiated
// width may be kept only while the reader is parked - which it is for
// everything except an open. That turns the CMD55+ACMD6 prefix from a
// per-operation cost into a per-mount one.
.width_hold(~s_rd_run),
.start (sdw_start),
.rd_mode (sdw_rd_mode),
.sector (sdw_sector),
.busy (sdw_busy),
.done (sdw_done),
.err (sdw_err),
.burst_len (sdw_burst_len),
.rca (s_card_rca),
.block_next(),
.rd_addr (sdw_rd_addr),
.rd_data (sdw_rd_data),
.rx_we (sdw_rx_we),
.rx_addr (sdw_rx_addr),
.rx_data (sdw_rx_data)
);
// ------------------------------------------------------------- SD pin mux
// The ONLY consumers of the pads; the tristate stays at the board top.
assign sd_clk_o = s_phase_write ? wr_sdclk : rd_sdclk;
assign sd_cmd_o = s_phase_write ? wr_cmd_o : rd_cmd_o;
assign sd_cmd_oe = s_phase_write ? wr_cmd_oe : rd_cmd_oe;
assign sd_dat0_o = wr_dat0_o;
assign sd_dat0_oe = s_phase_write & wr_dat0_oe;
// DAT1-3 only ever driven by the writer, and only in the write phase. The
// board top must use the single-ternary oe ? val : 1'bz form for these:
// a nested ternary is silently collapsed to an always-on driver by yosys,
// which made the FPGA fight the card through every 4-bit read data phase -
// simulating perfectly and failing on silicon (fpga/tang-nano-20k/sd-fat-test/README.md).
assign sd_dat1_o = wr_dat1_o;
assign sd_dat1_oe = s_phase_write & wr_dat1_oe;
assign sd_dat2_o = wr_dat2_o;
assign sd_dat2_oe = s_phase_write & wr_dat2_oe;
assign sd_dat3_o = wr_dat3_o;
assign sd_dat3_oe = s_phase_write & wr_dat3_oe;
// ------------------------------------------------------------- mount FSM
wire mnt_start, mnt_done, mnt_err, mnt_busy;
wire mnt_nocard; // mount failed for lack of a card, not a file
wire [2:0] mnt_client;
wire m_mem_start;
wire [19:0] m_mem_addr;
wire [31:0] m_mem_wdata;
wire mnt_st_upd;
wire [1:0] mnt_st_val;
wire [N_CLIENTS-1:0] s_open_ok_stor;
wire [N_CLIENTS-1:0] s_open_err_stor;
wire [N_CLIENTS*32-1:0] s_size_stor;
wire [N_CLIENTS*16-1:0] s_n_blocks;
wire [N_CLIENTS*32-1:0] s_first_sector;
wire [N_CLIENTS*28-1:0] s_first_cluster;
// fatchk handoff (driven/consumed below; tied off when the flag is off)
wire m_chk_start, m_chk_done, m_chk_ok;
wire [31:0] m_chk_first_cluster, m_chk_fat0_sector, m_chk_size;
wire [7:0] m_chk_cluster_size;
wire m_chk_is_fat32;
nd_storage_mount #(
.N_CLIENTS (N_CLIENTS),
.WD_MAX (WD_MAX),
.SLOT0_BASE_BLK(SLOT0_BASE_BLK), .SLOT0_SIZE_BLK(SLOT0_SIZE_BLK),
.SLOT1_BASE_BLK(SLOT1_BASE_BLK), .SLOT1_SIZE_BLK(SLOT1_SIZE_BLK),
.SLOT2_BASE_BLK(SLOT2_BASE_BLK), .SLOT2_SIZE_BLK(SLOT2_SIZE_BLK),
.SLOT3_BASE_BLK(SLOT3_BASE_BLK), .SLOT3_SIZE_BLK(SLOT3_SIZE_BLK),
.SLOT4_BASE_BLK(SLOT4_BASE_BLK), .SLOT4_SIZE_BLK(SLOT4_SIZE_BLK),
.SLOT5_BASE_BLK(SLOT5_BASE_BLK), .SLOT5_SIZE_BLK(SLOT5_SIZE_BLK),
.SLOT6_BASE_BLK(SLOT6_BASE_BLK), .SLOT6_SIZE_BLK(SLOT6_SIZE_BLK),
.SLOT7_BASE_BLK(SLOT7_BASE_BLK), .SLOT7_SIZE_BLK(SLOT7_SIZE_BLK)
) u_mount (
.clk_stor (clk_stor),
.rst_stor_n (rst_stor_n),
.mnt_start (mnt_start),
.mnt_client (mnt_client),
.mnt_done (mnt_done),
.mnt_err (mnt_err),
.mnt_nocard (mnt_nocard),
.mnt_busy (mnt_busy),
.rd_run (s_rd_run),
.phase_write (s_phase_write),
.cur_client (s_cur_client),
.card_stat (rd_card_stat),
.file_found (rd_file_found),
.scan_done (rd_scan_done),
.found_size (rd_file_size),
.found_first_sector(rd_first_sector),
.found_cluster (rd_found_cluster),
.fs_cluster_size (rd_fs_cs),
.fs_fat0_sector (rd_fs_fat0),
.fs_is_fat32 (rd_fs_type == 2'd3),
.outen (rd_outen),
.outbyte (rd_outbyte),
.mem_start (m_mem_start),
.mem_we (),
.mem_addr (m_mem_addr),
.mem_wdata (m_mem_wdata),
.mem_busy (mem_busy),
.mem_done (mem_done),
.chk_start (m_chk_start),
.chk_done (m_chk_done),
.chk_ok (m_chk_ok),
.chk_first_cluster (m_chk_first_cluster),
.chk_cluster_size (m_chk_cluster_size),
.chk_fat0_sector (m_chk_fat0_sector),
.chk_is_fat32 (m_chk_is_fat32),
.chk_size (m_chk_size),
.open_ok (s_open_ok_stor),
.open_err (s_open_err_stor),
.size_bytes (s_size_stor),
.n_blocks (s_n_blocks),
.first_sector (s_first_sector),
.first_cluster (s_first_cluster),
.st_upd (mnt_st_upd),
.st_val (mnt_st_val),
.load_fifo_ovf ()
);
// ------------------------------------------------------------- engine
wire e_mem_start, e_mem_we;
wire [19:0] e_mem_addr;
wire [31:0] e_mem_wdata;
wire eng_wd_err;
// ------------------------------------------------------- cache directory
wire c_lookup_req, c_lookup_done, c_lookup_hit;
wire [2:0] c_lookup_client, c_lookup_way;
wire [15:0] c_lookup_block;
wire [10:0] c_lookup_line;
wire c_alloc_req, c_alloc_done;
wire [2:0] c_alloc_client, c_alloc_way;
wire [15:0] c_alloc_block;
`ifdef ND_STORAGE_NO_CACHE
// 23-AUG experiment lever: the cache directory is NOT SYNTHESIZED at all -
// stronger than ND_STORAGE_DISCS_UNCACHED (which leaves the logic in the
// netlist with the mask cleared). Separates "cache logic disabled" from
// "cache logic absent" (BSRAM, routing and timing pressure included).
// Only valid together with an all-DIRECT CACHE_MASK: a cached client
// would wait forever on lookup_done. Tie the directory interface idle.
// Tie only the CACHE's outputs; c_alloc_way is driven by the ENGINE
// (nd_storage_engine.v `output reg cache_alloc_way`) - tying it here
// double-drives (measured: Gowin EX2000 on the first S2 build).
assign c_lookup_done = 1'b0;
assign c_lookup_hit = 1'b0;
assign c_lookup_way = 3'd0;
assign c_lookup_line = 11'd0;
assign c_alloc_done = 1'b0;
/* verilator lint_off UNUSEDSIGNAL */
wire unused_cache_reqs = c_lookup_req | c_alloc_req |
(|c_lookup_client) | (|c_lookup_block) |
(|c_alloc_client) | (|c_alloc_block) |
(|c_alloc_way);
/* verilator lint_on UNUSEDSIGNAL */
`else
nd_storage_cache #(
.WAYS(CACHE_WAYS), .SETS(CACHE_SETS), .SETIDX(CACHE_SETIDX),
.POOL_BASE_BLK(POOL_BASE_BLK), .BLKW(16)
) u_cache (
.clk(clk_stor), .rst_n(rst_stor_n),
.lookup_req(c_lookup_req), .lookup_client(c_lookup_client),
.lookup_block(c_lookup_block), .lookup_done(c_lookup_done),
.lookup_hit(c_lookup_hit), .lookup_way(c_lookup_way),
.lookup_line(c_lookup_line),
.alloc_req(c_alloc_req), .alloc_client(c_alloc_client),
.alloc_block(c_alloc_block), .alloc_way(c_alloc_way),
.alloc_done(c_alloc_done),
.inval_req(1'b0), .inval_client(3'd0), .inval_done()
);
`endif
nd_storage_engine #(
.N_CLIENTS (N_CLIENTS),
.WD_MAX (WD_MAX),
.SLOT0_BASE_BLK(SLOT0_BASE_BLK),
.SLOT1_BASE_BLK(SLOT1_BASE_BLK),
.SLOT2_BASE_BLK(SLOT2_BASE_BLK),
.SLOT3_BASE_BLK(SLOT3_BASE_BLK),
.SLOT4_BASE_BLK(SLOT4_BASE_BLK),
.SLOT5_BASE_BLK(SLOT5_BASE_BLK),
.SLOT6_BASE_BLK(SLOT6_BASE_BLK),
.SLOT7_BASE_BLK(SLOT7_BASE_BLK),
.CACHE_MASK (CACHE_MASK),
.STAGE_BASE_BLK (STAGE_BASE_BLK)
) u_engine (
.clk_stor (clk_stor),
.rst_stor_n (rst_stor_n),
.clk_cpu (clk_cpu),
.rst_cpu_n (rst_cpu_n),
.mem_start (e_mem_start),
.mem_we (e_mem_we),
.mem_addr (e_mem_addr),
.mem_wdata (e_mem_wdata),
.mem_rdata (mem_rdata),
.mem_busy (mem_busy),
.mem_done (mem_done),
.mnt_start (mnt_start),
.mnt_client (mnt_client),
.mnt_done (mnt_done),
.mnt_err (mnt_err),
.mnt_nocard (mnt_nocard),
.open_ok_stor (s_open_ok_stor),
.open_err_stor (s_open_err_stor),
.dbg_state (dbg_state),
.dbg_lba (dbg_lba),
.dbg_wdata (dbg_wdata),
.dbg_rdata (dbg_rdata),
.dbg_bufw (dbg_bufw),
.dbg_bufwe (dbg_bufwe),
.dbg_fsec (dbg_fsec),
.dbg_rx_stb (dbg_rx_stb),
.dbg_rx_raw (dbg_rx_raw),
.dbg_rx_byte (dbg_rx_byte),
.dbg_past_eof(dbg_past_eof),
.dbg_grant (dbg_grant),
.size_bytes_stor(s_size_stor),
.err_code (err_code),
.n_blocks (s_n_blocks),
.first_sector (s_first_sector),
.first_cluster (s_first_cluster),
.fat_spc (m_chk_cluster_size),
.fat0_sector (m_chk_fat0_sector),
.fat_is_fat32 (m_chk_is_fat32),
.sdw_start (e_sdw_start),
.sdw_sector (e_sdw_sector),
.sdw_busy (sdw_busy),
.dbg_sd_busy (dbg_sd_busy),
.dbg_cache_pend(dbg_cache_pend),
.sdw_done (sdw_done),
.sdw_err (sdw_err),
.sdw_rd_addr (sdw_rd_addr),
.sdw_rd_data (sdw_rd_data),
.sdw_rd_mode (e_sdw_rd_mode),
.sdw_burst_len (e_sdw_burst_len),
.sdw_rx_we (sdw_rx_we),
.sdw_rx_addr (sdw_rx_addr),
.sdw_rx_data (sdw_rx_data),
.cache_lookup_req (c_lookup_req),
.cache_lookup_client (c_lookup_client),
.cache_lookup_block (c_lookup_block),
.cache_lookup_done (c_lookup_done),
.cache_lookup_hit (c_lookup_hit),
.cache_lookup_way (c_lookup_way),
.cache_lookup_line (c_lookup_line),
.cache_alloc_req (c_alloc_req),
.cache_alloc_client (c_alloc_client),
.cache_alloc_block (c_alloc_block),
.cache_alloc_way (c_alloc_way),
.cache_alloc_done (c_alloc_done),
.eng_wd_err (eng_wd_err),
.open_req (open_req),
.open_ok (open_ok),
.open_err (open_err),
.size_bytes (size_bytes),
.req (req),
.wr (wr),
.block (block),
.busy (busy),
.done (done),
.err (err),
.buf_addr (buf_addr),
.buf_wdata (buf_wdata),
.buf_we (buf_we),
.buf_rdata (buf_rdata)
);
// ------------------------------------------------------------- fatchk
// Mount-time contiguity checker (design section 2.4): owns the sd_writer
// command pins in read mode while chk_busy; runs only from the mount's
// M_CHK state, after M_PARK set phase_write=1 - by construction it never
// contends with the engine's write-through path.
`ifdef SDFAT_STORAGE_CHECK
wire f_chk_busy;
wire f_sdw_start;
wire [31:0] f_sdw_sector;
nd_storage_fatchk #(
.WD_MAX(WD_MAX)
) u_fatchk (
.clk_stor (clk_stor),
.rst_stor_n (rst_stor_n),
.chk_start (m_chk_start),
.chk_busy (f_chk_busy),
.chk_done (m_chk_done),
.chk_ok (m_chk_ok),
.fs_is_fat32 (m_chk_is_fat32),
.cluster_size (m_chk_cluster_size),
.fat0_sector (m_chk_fat0_sector),
.first_cluster(m_chk_first_cluster),
.size_bytes (m_chk_size),
.sdw_start (f_sdw_start),
.sdw_sector (f_sdw_sector),
.sdw_done (sdw_done),
.sdw_err (sdw_err),
.sdw_rx_we (sdw_rx_we),
.sdw_rx_addr (sdw_rx_addr),
.sdw_rx_data (sdw_rx_data)
);
assign sdw_start = f_chk_busy ? f_sdw_start : e_sdw_start;
assign sdw_rd_mode = f_chk_busy ? 1'b1 : e_sdw_rd_mode;
assign sdw_sector = f_chk_busy ? f_sdw_sector : e_sdw_sector;
assign sdw_burst_len = f_chk_busy ? 9'd1 : e_sdw_burst_len;
`else
// feature stripped: the engine is the only sd_writer master and the
// mount's M_CHK state passes straight through (no checker to answer)
assign sdw_start = e_sdw_start;
assign sdw_burst_len = e_sdw_burst_len;
assign sdw_rd_mode = e_sdw_rd_mode;
assign sdw_sector = e_sdw_sector;
assign m_chk_done = 1'b0;
assign m_chk_ok = 1'b0;
`endif
// ------------------------------------------------------------- mem port mux
// Mount and engine are mutually exclusive by arbiter construction (the
// engine sits in E_OPEN while mnt_busy); returns fan out to both.
assign mem_start = mnt_busy ? m_mem_start : e_mem_start;
// The 1'b1 here is preload-era (see nd_storage_mount.v's mem_we): while a
// mount runs, EVERY access on the shared device port is forced to a write,
// whatever the engine asked for. Safe only because the engine is parked in
// E_OPEN for the whole mount. Passing m_mem_we through instead would be
// strictly better, but it is a two-file change - do not do half of it.
assign mem_we = mnt_busy ? 1'b1 : e_mem_we;
assign mem_addr = mnt_busy ? m_mem_addr : e_mem_addr;
assign mem_wdata = mnt_busy ? m_mem_wdata : e_mem_wdata;
// ------------------------------------------------------------- status
// card_type/fs_type: latch while the reader runs (its copies reset when
// the mount FSM parks it again)
always @(posedge clk_stor) begin
if (!rst_stor_n) begin
card_type <= 2'd0;
fs_type <= 2'd0;
end else begin
if (rd_card_type != 2'd0) card_type <= rd_card_type;
if (rd_fs_type >= 2'd2) fs_type <= rd_fs_type;
end
end
// sd_status: mount verdicts win, card-touching errors degrade to ERROR
reg s_wd_err_q;
always @(posedge clk_stor) begin
if (!rst_stor_n) begin
sd_status <= 2'd0; // NOTCHK
s_wd_err_q <= 1'b0;
end else begin
s_wd_err_q <= eng_wd_err;
if (mnt_st_upd) sd_status <= mnt_st_val;
else if ((eng_wd_err && !s_wd_err_q) || sdw_err) sd_status <= 2'd2;
end
end
endmodule