Skip to content

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

nd_storage symbol

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).

nd_storage schematic

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