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

Schematic¶
Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance TAPE_SDFAT_SOURCE.u_nd_storage.u_mount. 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 open/preload FSM (mount) Design: docs/nd-storage-design.md section 2.3; binding contract docs/nd-storage-interface-spec.md sections 6 and 7. Invoked by the block engine (nd_storage_engine.v) once per granted open request: M_IDLE : wait for mnt_start; latch the client; clear that client's open_ok/open_err (only a new open_req rebuilds a slot). Phase 4: PRELOAD_MASK is gone, so every client in range opens for real - only an out-of-range client index fails here without touching the card. M_INIT : phase_write<=0 (reader owns the SD pins), hold the reader in reset a few more cycles, then release it for a FULL init+mount+scan run - the per-open re-init that doubles as the proven rewind/card-swap recovery. M_CARD : wait for card init (card_stat >= 8); watchdog -> NOCARD. M_SCAN : wait for file_found; latch size/first-sector/first-cluster and geometry at the SAME edge (the ST_C_FIND lesson: the reader is parked later and its registers reset with it). Phase 4: NO size-versus-slot test - that refusal is what made a 75 MB image impossible against a 256 KB slot - and NO staging of the payload. The reader runs with no_stream=1, so it stops at the directory match and this state waits for its scan_done before parking. Waiting matters: parking the reader mid-transfer leaves the card streaming and the next card user (the contiguity checker, or simply the next open) fails. scan_done without file_found (missing file / bad FS) or watchdog -> park + fail. M_LOAD : DEAD under Phase 4 - unreachable, kept for one release so the change stays reviewable against v1. It used to: consume the outen/outbyte stream -> 4-byte big-endian packer (byte 4m lands in mem_wdata[31:24]) -> 8x32 sync FIFO -> mem-port writes at {SLOT_BASE_BLK[c],9'b0} + byte_cnt[21:2]; the tail word is zero-padded. The FIFO only absorbs jitter (1 byte per ~16-64 clk_stor from the reader vs ~10-40 clk_stor per mem write); overflow sets the sticky load_fifo_ovf flag the testbench asserts on. M_PARK : reader back into reset, phase_write<=1 (sd_writer owns the card again). M_CHK : SDFAT_STORAGE_CHECK contiguity gate (nd_storage_fatchk.v): pulse chk_start once, wait for chk_done; chk_ok -> M_OK, not ok (fragmented file / FAT read error) -> M_FAIL. The failed open can simply be retried with a new open_req. Without the feature flag the state passes straight through to M_OK (the card recipe alone guarantees contiguity). M_OK : open_ok[c]<=1, size_bytes/n_blocks (=ceil(size/2048)) / first_sector published, mnt_done with mnt_err=0. SIZE CEILING: n_blocks is 16 bits, so an image >= 128 MiB (2^27 B) is silently mis-sized - see the long note at the r_nblk assignment. An image merely bigger than its drive GEOMETRY is fine and is a different matter entirely. M_FAIL : open_err[c]<=1, mnt_done with mnt_err=1. open_ok stays up across later write errors (spec section 7); only a new open_req for that client clears and rebuilds it (done here at the M_IDLE handshake). Everything runs in clk_stor. The mem port is muxed onto the shared device port by the top (nd_storage.v) while mnt_busy is high; the engine is parked in E_OPEN for that whole time by construction. Last reviewed: 11-JUL-2026 Ronny Hansen
Parameters¶
| Parameter | Default |
|---|---|
N_CLIENTS |
7 |
WD_MAX |
32'd270_000_000 |
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 |
mnt_start |
1-cycle pulse per granted open |
| input | [2:0] |
mnt_client |
stable while the engine waits |
| output | 1 |
mnt_done |
1-cycle pulse |
| output | 1 |
mnt_err |
valid with mnt_done |
| output | 1 |
mnt_nocard |
|
| output | 1 |
mnt_busy |
owns the reader + mem port while high |
| output | 1 |
rd_run |
1 = release the reader's reset |
| output | 1 |
phase_write |
1 = sd_writer owns the SD pins |
| output | [2:0] |
cur_client |
target_name/target_len mux select |
| input | [3:0] |
card_stat |
init step counter; >= 8 = ready (from sd_file_reader.card_stat) |
| input | 1 |
file_found |
level: target matched (found_* valid) (from sd_file_reader.file_found) |
| input | 1 |
scan_done |
sticky level: run finished (from sd_file_reader.scan_done) |
| input | [31:0] |
found_size |
|
| input | [31:0] |
found_first_sector |
|
| input | [31:0] |
found_cluster |
captured for the fatchk checker |
| input | [7:0] |
fs_cluster_size |
captured for the fatchk checker |
| input | [31:0] |
fs_fat0_sector |
captured for the fatchk checker |
| input | 1 |
fs_is_fat32 |
captured for the fatchk checker |
| input | 1 |
outen |
1-cycle pulse per file byte (from sd_file_reader.outen) |
| input | [7:0] |
outbyte |
|
| output | 1 |
mem_start |
1-cycle pulse, only when mem_busy=0 |
| output | 1 |
mem_we |
the mount only ever writes |
| output | [19:0] |
mem_addr |
|
| output | [31:0] |
mem_wdata |
|
| input | 1 |
mem_busy |
|
| input | 1 |
mem_done |
|
| output | 1 |
chk_start |
1-cycle pulse in M_CHK |
| input | 1 |
chk_done |
1-cycle pulse from the checker |
| input | 1 |
chk_ok |
level, valid at chk_done |
| output | [31:0] |
chk_first_cluster |
latched with file_found |
| output | [7:0] |
chk_cluster_size |
|
| output | [31:0] |
chk_fat0_sector |
|
| output | 1 |
chk_is_fat32 |
latched with file_found |
| output | [31:0] |
chk_size |
= the latched found_file_size |
| output | [N_CLIENTS-1:0] |
open_ok |
|
| output | [N_CLIENTS-1:0] |
open_err |
|
| output | [N_CLIENTS*32-1:0] |
size_bytes |
|
| output | [N_CLIENTS*16-1:0] |
n_blocks |
ceil(size/2048) |
| output | [N_CLIENTS*28-1:0] |
first_cluster |
file first FAT cluster |
| output | [N_CLIENTS*32-1:0] |
first_sector |
|
| output | 1 |
st_upd |
1-cycle pulse with mnt_done (card was touched) |
| output | [1:0] |
st_val |
1 = NOCARD, 2 = ERROR, 3 = OK |
| output | 1 |
load_fifo_ovf |
Verilog source¶
Verilog/SD-FAT/circuit/nd_storage_mount.v on GitHub.
Show the Verilog of nd_storage_mount (581 lines)
/****************************************************************************
** nd_storage open/preload FSM (mount) **
** **
** Design: docs/nd-storage-design.md section 2.3; binding contract **
** docs/nd-storage-interface-spec.md sections 6 and 7. Invoked by the **
** block engine (nd_storage_engine.v) once per granted open request: **
** **
** M_IDLE : wait for mnt_start; latch the client; clear that client's **
** open_ok/open_err (only a new open_req rebuilds a slot). **
** Phase 4: PRELOAD_MASK is gone, so every client in range **
** opens for real - only an out-of-range client index fails **
** here without touching the card. **
** M_INIT : phase_write<=0 (reader owns the SD pins), hold the reader **
** in reset a few more cycles, then release it for a FULL **
** init+mount+scan run - the per-open re-init that doubles **
** as the proven rewind/card-swap recovery. **
** M_CARD : wait for card init (card_stat >= 8); watchdog -> NOCARD. **
** M_SCAN : wait for file_found; latch size/first-sector/first-cluster **
** and geometry at the SAME edge (the ST_C_FIND lesson: the **
** reader is parked later and its registers reset with it). **
** Phase 4: NO size-versus-slot test - that refusal is what **
** made a 75 MB image impossible against a 256 KB slot - and **
** NO staging of the payload. The reader runs with **
** no_stream=1, so it stops at the directory match and this **
** state waits for its scan_done before parking. Waiting **
** matters: parking the reader mid-transfer leaves the card **
** streaming and the next card user (the contiguity checker, **
** or simply the next open) fails. **
** scan_done without file_found (missing file / bad FS) or **
** watchdog -> park + fail. **
** M_LOAD : DEAD under Phase 4 - unreachable, kept for one release so **
** the change stays reviewable against v1. It used to: **
** consume the outen/outbyte stream -> 4-byte big-endian **
** packer (byte 4m lands in mem_wdata[31:24]) -> 8x32 sync **
** FIFO -> mem-port writes at {SLOT_BASE_BLK[c],9'b0} + **
** byte_cnt[21:2]; the tail word is zero-padded. The FIFO **
** only absorbs jitter (1 byte per ~16-64 clk_stor from the **
** reader vs ~10-40 clk_stor per mem write); overflow sets **
** the sticky load_fifo_ovf flag the testbench asserts on. **
** M_PARK : reader back into reset, phase_write<=1 (sd_writer owns **
** the card again). **
** M_CHK : SDFAT_STORAGE_CHECK contiguity gate (nd_storage_fatchk.v): **
** pulse chk_start once, wait for chk_done; chk_ok -> M_OK, **
** not ok (fragmented file / FAT read error) -> M_FAIL. The **
** failed open can simply be retried with a new open_req. **
** Without the feature flag the state passes straight through **
** to M_OK (the card recipe alone guarantees contiguity). **
** M_OK : open_ok[c]<=1, size_bytes/n_blocks (=ceil(size/2048)) / **
** first_sector published, mnt_done with mnt_err=0. **
** SIZE CEILING: n_blocks is 16 bits, so an image >= 128 MiB **
** (2^27 B) is silently mis-sized - see the long note at the **
** r_nblk assignment. An image merely bigger than its drive **
** GEOMETRY is fine and is a different matter entirely. **
** M_FAIL : open_err[c]<=1, mnt_done with mnt_err=1. **
** **
** open_ok stays up across later write errors (spec section 7); only a **
** new open_req for that client clears and rebuilds it (done here at **
** the M_IDLE handshake). **
** **
** Everything runs in clk_stor. The mem port is muxed onto the shared **
** device port by the top (nd_storage.v) while mnt_busy is high; the **
** engine is parked in E_OPEN for that whole time by construction. **
** **
** Last reviewed: 11-JUL-2026 **
** Ronny Hansen **
*****************************************************************************/
`include "sd_fat_features.vh"
module nd_storage_mount #(
parameter N_CLIENTS = 7, // 1..8
parameter [31:0] WD_MAX = 32'd270_000_000,
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,
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,
// ---- engine handoff (nd_storage_engine E_OPEN) ----
input wire mnt_start, // 1-cycle pulse per granted open
input wire [2:0] mnt_client, // stable while the engine waits
output reg mnt_done, // 1-cycle pulse
output reg mnt_err, // valid with mnt_done
// Valid with mnt_done: this open failed because there is NO CARD (or
// the card never initialised), as opposed to the card being fine and
// this client's file not being on it. The engine turns the two into
// different reason codes because they need different operator action.
output reg mnt_nocard,
output wire mnt_busy, // owns the reader + mem port while high
// ---- reader control / observation (sd_file_reader at the top) ----
output reg rd_run, // 1 = release the reader's reset
output reg phase_write, // 1 = sd_writer owns the SD pins
output reg [2:0] cur_client, // target_name/target_len mux select
input wire [3:0] card_stat, //! init step counter; >= 8 = ready (from sd_file_reader.card_stat)
input wire file_found, //! level: target matched (found_* valid) (from sd_file_reader.file_found)
input wire scan_done, //! sticky level: run finished (from sd_file_reader.scan_done)
input wire [31:0] found_size,
input wire [31:0] found_first_sector,
input wire [31:0] found_cluster, // captured for the fatchk checker
input wire [7:0] fs_cluster_size, // captured for the fatchk checker
input wire [31:0] fs_fat0_sector, // captured for the fatchk checker
input wire fs_is_fat32, // captured for the fatchk checker
input wire outen, //! 1-cycle pulse per file byte (from sd_file_reader.outen)
input wire [7:0] outbyte,
// ---- mem port (muxed onto the device port while mnt_busy) ----
output reg mem_start, // 1-cycle pulse, only when mem_busy=0
output wire mem_we, // the mount only ever writes
output reg [19:0] mem_addr,
output reg [31:0] mem_wdata,
input wire mem_busy,
input wire mem_done,
// ---- fatchk handoff (nd_storage_fatchk.v, SDFAT_STORAGE_CHECK) ----
output reg chk_start, // 1-cycle pulse in M_CHK
input wire chk_done, // 1-cycle pulse from the checker
input wire chk_ok, // level, valid at chk_done
output reg [31:0] chk_first_cluster, // latched with file_found
output reg [7:0] chk_cluster_size,
output reg [31:0] chk_fat0_sector,
output reg chk_is_fat32, // latched with file_found
output wire [31:0] chk_size, // = the latched found_file_size
// ---- per-client open results (clk_stor; quasi-static levels) ----
output reg [N_CLIENTS-1:0] open_ok,
output reg [N_CLIENTS-1:0] open_err,
output wire [N_CLIENTS*32-1:0] size_bytes,
output wire [N_CLIENTS*16-1:0] n_blocks, // ceil(size/2048)
output wire [N_CLIENTS*28-1:0] first_cluster, // file first FAT cluster
output wire [N_CLIENTS*32-1:0] first_sector,
// ---- sd_status feed for the top (spec section 7) ----
output reg st_upd, // 1-cycle pulse with mnt_done (card was touched)
output reg [1:0] st_val, // 1 = NOCARD, 2 = ERROR, 3 = OK
// ---- testbench hook: preload FIFO never overflows (sticky) ----
output reg load_fifo_ovf
);
// ------------------------------------------------------------- slot map
function [31:0] slot_base;
input [2:0] c;
begin
case (c)
3'd0: slot_base = SLOT0_BASE_BLK;
3'd1: slot_base = SLOT1_BASE_BLK;
3'd2: slot_base = SLOT2_BASE_BLK;
3'd3: slot_base = SLOT3_BASE_BLK;
3'd4: slot_base = SLOT4_BASE_BLK;
3'd5: slot_base = SLOT5_BASE_BLK;
3'd6: slot_base = SLOT6_BASE_BLK;
default: slot_base = SLOT7_BASE_BLK;
endcase
end
endfunction
function [31:0] slot_size;
input [2:0] c;
begin
case (c)
3'd0: slot_size = SLOT0_SIZE_BLK;
3'd1: slot_size = SLOT1_SIZE_BLK;
3'd2: slot_size = SLOT2_SIZE_BLK;
3'd3: slot_size = SLOT3_SIZE_BLK;
3'd4: slot_size = SLOT4_SIZE_BLK;
3'd5: slot_size = SLOT5_SIZE_BLK;
3'd6: slot_size = SLOT6_SIZE_BLK;
default: slot_size = SLOT7_SIZE_BLK;
endcase
end
endfunction
// slot capacity in bytes (max slot 640 blocks = 1.25 MB, fits 32 bits)
wire [31:0] s_slot_bytes = slot_size(cur_client) << 11;
// ------------------------------------------------------------- FSM
reg s_got_geom; // target matched; size/first/cluster already latched
localparam [3:0] M_IDLE = 4'd0;
localparam [3:0] M_INIT = 4'd1;
localparam [3:0] M_CARD = 4'd2;
localparam [3:0] M_SCAN = 4'd3;
localparam [3:0] M_LOAD = 4'd4;
localparam [3:0] M_PARK = 4'd5;
localparam [3:0] M_CHK = 4'd6;
localparam [3:0] M_OK = 4'd7;
localparam [3:0] M_FAIL = 4'd8;
reg [3:0] s_state;
reg [3:0] s_rstcnt; // extra reader-reset cycles in M_INIT
reg s_mask_fail; // fail without ever touching the card (SMD v1)
reg s_nocard; // fail reason: card never initialized
reg s_chk_run; // M_CHK: chk_start issued, waiting for chk_done
reg [31:0] s_size; // latched found_file_size
reg [31:0] s_first; // latched found_file_first_sector
assign chk_size = s_size; // stable from the file_found latch to mnt_done
assign mnt_busy = (s_state != M_IDLE);
// STALE, kept deliberately visible (09-AUG-2026). This dates from the
// preload era, when the mount copied whole images into the region and
// therefore only ever WROTE. Under Phase 4 the mount does not preload at
// all: its mem_start fires only inside M_LOAD, which is unreachable. So
// this output is inert today - but it is a hard-wired 1 on a shared port's
// write-enable, and nd_storage.v:569 additionally FORCES the muxed mem_we
// to 1 for the whole time mnt_busy is high. That is only safe because the
// engine parks in E_OPEN across a mount. If either of those two facts ever
// stops being true, a region READ issued during a mount silently becomes a
// WRITE and corrupts the region instead of returning data.
//
// Do not "tidy" this to 1'b0 in isolation - the mux in nd_storage.v is the
// other half and they have to change together.
assign mem_we = 1'b1; // preload-era: the mount only ever wrote
wire s_card_ready = (card_stat >= 4'd8);
// ------------------------------------------------------------- results
reg [31:0] r_size [0:N_CLIENTS-1];
reg [15:0] r_nblk [0:N_CLIENTS-1];
reg [31:0] r_first [0:N_CLIENTS-1];
reg [27:0] r_fclu [0:N_CLIENTS-1]; // first cluster, for the FAT walker
genvar gc;
generate
for (gc = 0; gc < N_CLIENTS; gc = gc + 1) begin : g_res
assign size_bytes[32*gc +: 32] = r_size[gc];
assign n_blocks[16*gc +: 16] = r_nblk[gc];
assign first_sector[32*gc +: 32] = r_first[gc];
assign first_cluster[28*gc +: 28] = r_fclu[gc];
end
endgenerate
// ------------------------------------------------------------- watchdog
// reset on any state change and on load progress (outen / mem_done)
reg [3:0] s_state_q;
reg [31:0] s_wd;
always @(posedge clk_stor) begin
if (!rst_stor_n) begin
s_state_q <= M_IDLE;
s_wd <= 32'd0;
end else begin
s_state_q <= s_state;
if (s_state != s_state_q || s_state == M_IDLE || outen || mem_done)
s_wd <= 32'd0;
else s_wd <= s_wd + 32'd1;
end
end
wire s_wd_hit = (s_wd >= WD_MAX);
// ------------------------------------------------------------- packer+FIFO
// outen/outbyte -> big-endian 32-bit words (byte 4m first = [31:24]),
// buffered in a small same-clock FIFO, drained by the mem-port writer.
reg [23:0] s_pack; // last up-to-3 bytes received
reg [21:0] s_bcnt; // file byte counter (byte_cnt of the design doc)
reg s_flushed; // tail word (if any) pushed after scan_done
reg [31:0] s_fifo [0:7];
reg [3:0] s_wptr;
reg [3:0] s_rptr;
reg [18:0] s_wordcnt; // mem words written (= byte_cnt[21:2] in order)
reg s_outst; // one mem op outstanding
wire [3:0] s_ff_fill = s_wptr - s_rptr;
wire s_ff_empty = (s_wptr == s_rptr);
wire s_ff_full = (s_ff_fill == 4'd8);
// zero-padded tail word for the flush push
reg [31:0] s_tail_w;
always @(*) begin
case (s_bcnt[1:0])
2'd1: s_tail_w = {s_pack[7:0], 24'd0};
2'd2: s_tail_w = {s_pack[15:0], 16'd0};
2'd3: s_tail_w = {s_pack[23:0], 8'd0};
default: s_tail_w = 32'd0; // no partial word: nothing to push
endcase
end
wire [31:0] s_base_blk = slot_base(cur_client);
wire [19:0] s_base_word = {s_base_blk[10:0], 9'd0};
wire s_load_done = scan_done && s_flushed && s_ff_empty && !s_outst;
// ------------------------------------------------------------- main
always @(posedge clk_stor) begin : main
integer i;
if (!rst_stor_n) begin
s_state <= M_IDLE;
s_rstcnt <= 4'd0;
s_mask_fail <= 1'b0;
s_nocard <= 1'b0;
s_size <= 32'd0;
s_first <= 32'd0;
cur_client <= 3'd0;
rd_run <= 1'b0;
phase_write <= 1'b1; // out of reset the writer owns the pins
mnt_done <= 1'b0;
mnt_err <= 1'b0;
mnt_nocard <= 1'b0;
mem_start <= 1'b0;
mem_addr <= 20'd0;
mem_wdata <= 32'd0;
chk_start <= 1'b0;
chk_first_cluster <= 32'd0;
chk_cluster_size <= 8'd0;
chk_fat0_sector <= 32'd0;
chk_is_fat32 <= 1'b0;
s_chk_run <= 1'b0;
open_ok <= {N_CLIENTS{1'b0}};
open_err <= {N_CLIENTS{1'b0}};
st_upd <= 1'b0;
st_val <= 2'd0;
load_fifo_ovf <= 1'b0;
s_pack <= 24'd0;
s_bcnt <= 22'd0;
s_flushed <= 1'b0;
s_wptr <= 4'd0;
s_rptr <= 4'd0;
s_wordcnt <= 19'd0;
s_outst <= 1'b0;
for (i = 0; i < N_CLIENTS; i = i + 1) begin
r_size[i] <= 32'd0;
r_nblk[i] <= 16'd0;
r_first[i] <= 32'd0;
r_fclu[i] <= 28'd0;
end
end else begin
mnt_done <= 1'b0;
mem_start <= 1'b0;
chk_start <= 1'b0;
st_upd <= 1'b0;
// ---- preload datapath (active in M_LOAD only) --------------------
if (s_state == M_LOAD) begin
if (outen) begin
s_pack <= {s_pack[15:0], outbyte};
s_bcnt <= s_bcnt + 22'd1;
if (s_bcnt[1:0] == 2'd3) begin
if (s_ff_full) load_fifo_ovf <= 1'b1; // sticky; tb asserts 0
s_fifo[s_wptr[2:0]] <= {s_pack, outbyte};
s_wptr <= s_wptr + 4'd1;
end
end else if (scan_done && !s_flushed) begin
// stream over: zero-pad and push the partial tail word (if any)
s_flushed <= 1'b1;
if (s_bcnt[1:0] != 2'd0) begin
if (s_ff_full) load_fifo_ovf <= 1'b1;
s_fifo[s_wptr[2:0]] <= s_tail_w;
s_wptr <= s_wptr + 4'd1;
end
end
// drain: one outstanding mem write at a time
if (!s_outst && !s_ff_empty && !mem_busy) begin
mem_start <= 1'b1;
mem_addr <= s_base_word + {1'd0, s_wordcnt};
mem_wdata <= s_fifo[s_rptr[2:0]];
s_rptr <= s_rptr + 4'd1;
s_wordcnt <= s_wordcnt + 19'd1;
s_outst <= 1'b1;
end
if (mem_done) s_outst <= 1'b0;
end
// ---- FSM ----------------------------------------------------------
case (s_state)
M_IDLE: begin
rd_run <= 1'b0;
phase_write <= 1'b1;
if (mnt_start) begin
cur_client <= mnt_client;
open_ok[mnt_client] <= 1'b0; // only a new open rebuilds a slot
open_err[mnt_client] <= 1'b0;
s_nocard <= 1'b0;
s_got_geom <= 1'b0;
s_rstcnt <= 4'd15;
// Compare at FULL width. N_CLIENTS[2:0] truncates: at the
// 8-client map it is 3'b000, so "mnt_client >= 0" would be
// always true and NO client would ever mount - a silent, total
// failure that looks like an SD card fault on hardware.
if ({29'd0, mnt_client} >= N_CLIENTS) begin
// Phase 4: PRELOAD_MASK is gone. It used to fail every client
// outside the mask here (the disc classes), because v1 could
// only serve an image it had staged whole into the region.
// Nothing is staged now - the region is a cache - so every
// client in range opens.
s_mask_fail <= 1'b1;
s_state <= M_FAIL;
end else begin
s_mask_fail <= 1'b0;
s_state <= M_INIT;
end
end
end
M_INIT: begin
phase_write <= 1'b0; // reader owns the SD pins
if (s_rstcnt != 4'd0) begin
s_rstcnt <= s_rstcnt - 4'd1; // a clean reset window first
end else begin
rd_run <= 1'b1; // full init+mount+scan run begins
s_state <= M_CARD;
end
end
M_CARD: begin
if (s_card_ready) begin
s_state <= M_SCAN;
end else if (s_wd_hit || scan_done) begin
// no card / init failure (an unmountable card can also fall
// straight to scan_done)
s_nocard <= !scan_done;
rd_run <= 1'b0;
phase_write <= 1'b1;
s_state <= M_FAIL;
end
end
M_SCAN: begin
// file_found is a LEVEL, not a pulse: it stays high once the entry
// matches. Guard the latch so this branch cannot swallow the
// scan_done test below for the rest of the scan.
if (file_found && !s_got_geom) begin
// same-edge capture: these registers vanish when the reader is
// parked, so everything the load and the step-5 checker need is
// latched NOW
s_size <= found_size;
s_first <= found_first_sector;
chk_first_cluster <= found_cluster;
chk_cluster_size <= fs_cluster_size;
chk_fat0_sector <= fs_fat0_sector;
chk_is_fat32 <= fs_is_fat32;
// Phase 4: NO size-versus-slot refusal. This test is what made
// a 75 MB Winchester image impossible against a 256 KB slot -
// the mount failed, every request answered zero-fill, and the
// guest saw a controller that finished with no error and no
// data. An image is now limited only by the 16-bit block count
// (128 MB), because the region caches it instead of holding it.
// ...and NO staging of the file contents either. M_LOAD used to
// stream the whole image into the region here; that IS the
// preload, and it is what a cache replaces. A mount now only
// establishes geometry - size, first sector, and the cluster
// facts the contiguity checker needs - and parks. Blocks are
// fetched by the engine on demand, so the image size no longer
// bounds anything and a 75 MB open costs the same as a 64 KB one.
//
// M_LOAD and its streaming datapath (s_pack/s_bcnt/s_wptr/...)
// are now unreachable and left for a separate removal, so this
// change stays reviewable against the v1 behaviour.
//
// EXPERIMENT (mechanism check): do NOT park here. Parking the
// reader the instant the directory entry matches leaves the card
// mid-transfer, and the next user of the card fails - the
// contiguity checker on this open, or simply the next open when
// the checker is stripped. Wait for the reader to stop by itself
// instead, which happens at a clean command boundary.
s_got_geom <= 1'b1;
end
if (scan_done || s_wd_hit) begin
if ((s_got_geom || file_found) && !s_wd_hit) begin
s_state <= M_PARK; // clean stop: safe to take the card
end else begin
// file not found / unmountable filesystem / stuck
rd_run <= 1'b0;
phase_write <= 1'b1;
s_state <= M_FAIL;
end
end
end
M_LOAD: begin
if (s_load_done) begin
s_state <= M_PARK;
end else if (s_wd_hit) begin
rd_run <= 1'b0;
phase_write <= 1'b1;
s_state <= M_FAIL;
end
end
M_PARK: begin
rd_run <= 1'b0; // reader parked until the next open
phase_write <= 1'b1; // sd_writer owns the card again
s_chk_run <= 1'b0;
s_state <= M_CHK;
end
M_CHK: begin
`ifdef SDFAT_STORAGE_CHECK
// contiguity gate (nd_storage_fatchk.v): one start pulse, then
// wait for the verdict. The checker owns the sd_writer command
// mux (rd_mode=1) while chk_busy; phase_write is already 1, so
// the writer owns the SD pins. A fragmented file (or a FAT read
// error) fails the open; retry with a new open_req.
if (!s_chk_run) begin
chk_start <= 1'b1;
s_chk_run <= 1'b1;
end else if (chk_done) begin
s_chk_run <= 1'b0;
s_state <= chk_ok ? M_OK : M_FAIL;
end else if (s_wd_hit) begin
// backstop only: the checker carries its own watchdog and the
// sd_writer always terminates via its internal timeouts
s_chk_run <= 1'b0;
s_state <= M_FAIL;
end
`else
// feature stripped: pass straight through - the card recipe
// alone guarantees contiguous files (design section 3)
s_state <= M_OK;
`endif
end
M_OK: begin
open_ok[cur_client] <= 1'b1;
r_size[cur_client] <= s_size;
// ceil(size / 2048). The slice is [26:11], i.e. a 16-BIT block
// count, so this silently truncates any file at or above 2^27
// bytes = 128 MiB: the high size bits are simply dropped and the
// client is told the image is a different, smaller size than it
// really is. 150 MiB stores 11,264 blocks instead of 76,800 and
// most reads then fail the range check; exactly 128 MiB stores 0
// and EVERY read is refused.
//
// This is NOT the same thing as an oversized image being harmless.
// An image that is merely bigger than its drive geometry is fine
// by design - the controller (ND_WINCHESTER / ND_SMD) refuses any
// CHS past its own GEO_* bounds before the storage stack is asked
// for anything, so those sectors are never addressed. That holds
// for any size BELOW this 128 MiB ceiling.
//
// No ND disc image in this project approaches 128 MiB (the biggest
// Winchester drive is 75.5 MB), so nothing is broken today. But the
// failure mode is a silent wrong answer, which is the one thing
// nd_storage_status.vh exists to eliminate: a mount of an image
// >= 128 MiB should fail with NDS_ERR_RANGE instead of mis-sizing
// it. Widening the slice alone is not enough - n_blocks is a
// 16-bit port (see the port list above) and the engine compares
// against it, so the port width has to grow with it.
r_nblk[cur_client] <= s_size[26:11] + {15'd0, |s_size[10:0]};
r_first[cur_client] <= s_first;
r_fclu[cur_client] <= chk_first_cluster[27:0];
mnt_err <= 1'b0;
mnt_nocard <= 1'b0;
mnt_done <= 1'b1;
st_upd <= 1'b1;
st_val <= 2'd3; // SD_OK
s_state <= M_IDLE;
end
M_FAIL: begin
open_err[cur_client] <= 1'b1;
mnt_err <= 1'b1;
mnt_nocard <= s_nocard;
mnt_done <= 1'b1;
if (!s_mask_fail) begin
// mask failures never touched the card: no status update
st_upd <= 1'b1;
st_val <= s_nocard ? 2'd1 : 2'd2; // SD_NOCARD / SD_ERROR
end
s_mask_fail <= 1'b0;
s_state <= M_IDLE;
end
default: s_state <= M_IDLE;
endcase
end
end
endmodule