nd_storage_fatchk¶
Source: Verilog/SD-FAT/circuit/nd_storage_fatchk.v
Hierarchy: not instantiated by any of the 9 build tops (elaborated by yosys).
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: no build top uses this module, so it was elaborated from its own file with no defines and default parameters. 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 mount-time contiguity checker (fatchk) Design: docs/nd-storage-design.md section 2.4; binding contract: docs/nd-storage-interface-spec.md sections 6 and 8 (contiguous files are REQUIRED in v1, enforced at open). Feature flag: SDFAT_STORAGE_CHECK in sd_fat_features.vh. Invoked by the mount FSM (nd_storage_mount.v M_CHK) after M_PARK, i.e. the reader is parked and phase_write=1: sd_writer owns the SD pins and this module owns sd_writer's command interface while chk_busy (the top's writer-command mux). For n = ceil(size_bytes / (cluster_size * 512)) clusters starting at first_cluster the FAT must read FAT[first_cluster + i] == first_cluster + i + 1 for i < n-1 FAT[first_cluster + n - 1] is a proper end-of-chain mark FAT16 entries are 2-byte little-endian, EOC = masked 16-bit value
= 0xFFF7; FAT32 entries are 4-byte little-endian, only bits [27:0] are the cluster number, EOC = masked value >= 0x0FFFFFF7 (the fat_sec/fat_off/is_eoc shapes of the proven sd_fat_check.v). Unlike sd_fat_check this module does NOT follow chain pointers - it verifies the expected value at every position - so a corrupted FAT can never send it walking; the only guards needed are the up-front cluster-count cap and the per-state watchdog. Verdict is a machine level: chk_ok valid at the chk_done pulse (and held until the next run), no text. FAT sectors are read via sd_writer read mode (CMD17, rx byte stream into an internal 512-byte buffer with the 1-clk registered-read discipline); the current FAT sector is cached and only re-read when the entry index leaves it - consecutive clusters share sectors, so a contiguous file costs ceil(n / entries_per_sector) reads. Not-ok causes: fragmented chain, missing EOC, first_cluster < 2 or cluster_size = 0 with a nonzero size (broken geometry latch), n > HOP_CAP, sd_writer err, watchdog. size_bytes = 0 is ok with zero card traffic (no chain to verify). Read-only - never writes. This file is ORIGINAL ND-120 project code (MIT, like the repository). Last reviewed: 11-JUL-2026 Ronny Hansen
Parameters¶
| Parameter | Default |
|---|---|
WD_MAX |
32'd270_000_000 |
HOP_CAP |
25'd131072 // 1<<17 clusters |
Ports¶
| Direction | Width | Name | Description |
|---|---|---|---|
| input | 1 |
clk_stor |
|
| input | 1 |
rst_stor_n (active low) |
|
| input | 1 |
chk_start |
1-cycle pulse; only when chk_busy=0 |
| output | 1 |
chk_busy |
owns the sd_writer command mux while high |
| output | 1 |
chk_done |
1-cycle pulse |
| output | 1 |
chk_ok |
level: valid at chk_done, held until next run |
| input | 1 |
fs_is_fat32 |
|
| input | [7:0] |
cluster_size |
sectors per cluster (power of two) |
| input | [31:0] |
fat0_sector |
absolute first sector of FAT copy 0 |
| input | [31:0] |
first_cluster |
|
| input | [31:0] |
size_bytes |
|
| output | 1 |
sdw_start |
1-cycle pulse (top drives rd_mode=1) |
| output | [31:0] |
sdw_sector |
|
| input | 1 |
sdw_done |
|
| input | 1 |
sdw_err |
|
| input | 1 |
sdw_rx_we |
|
| input | [8:0] |
sdw_rx_addr |
|
| input | [7:0] |
sdw_rx_data |
Verilog source¶
Verilog/SD-FAT/circuit/nd_storage_fatchk.v on GitHub.
Show the Verilog of nd_storage_fatchk (269 lines)
/****************************************************************************
** nd_storage mount-time contiguity checker (fatchk) **
** **
** Design: docs/nd-storage-design.md section 2.4; binding contract: **
** docs/nd-storage-interface-spec.md sections 6 and 8 (contiguous files **
** are REQUIRED in v1, enforced at open). Feature flag: **
** SDFAT_STORAGE_CHECK in sd_fat_features.vh. **
** **
** Invoked by the mount FSM (nd_storage_mount.v M_CHK) after M_PARK, i.e. **
** the reader is parked and phase_write=1: sd_writer owns the SD pins and **
** this module owns sd_writer's command interface while chk_busy (the **
** top's writer-command mux). For **
** **
** n = ceil(size_bytes / (cluster_size * 512)) **
** **
** clusters starting at first_cluster the FAT must read **
** **
** FAT[first_cluster + i] == first_cluster + i + 1 for i < n-1 **
** FAT[first_cluster + n - 1] is a proper end-of-chain mark **
** **
** FAT16 entries are 2-byte little-endian, EOC = masked 16-bit value **
** >= 0xFFF7; FAT32 entries are 4-byte little-endian, only bits [27:0] **
** are the cluster number, EOC = masked value >= 0x0FFFFFF7 (the **
** fat_sec/fat_off/is_eoc shapes of the proven sd_fat_check.v). Unlike **
** sd_fat_check this module does NOT follow chain pointers - it verifies **
** the expected value at every position - so a corrupted FAT can never **
** send it walking; the only guards needed are the up-front cluster-count **
** cap and the per-state watchdog. Verdict is a machine level: chk_ok **
** valid at the chk_done pulse (and held until the next run), no text. **
** **
** FAT sectors are read via sd_writer read mode (CMD17, rx byte stream **
** into an internal 512-byte buffer with the 1-clk registered-read **
** discipline); the current FAT sector is cached and only re-read when **
** the entry index leaves it - consecutive clusters share sectors, so a **
** contiguous file costs ceil(n / entries_per_sector) reads. **
** **
** Not-ok causes: fragmented chain, missing EOC, first_cluster < 2 or **
** cluster_size = 0 with a nonzero size (broken geometry latch), **
** n > HOP_CAP, sd_writer err, watchdog. size_bytes = 0 is ok with zero **
** card traffic (no chain to verify). Read-only - never writes. **
** **
** This file is ORIGINAL ND-120 project code (MIT, like the repository). **
** **
** Last reviewed: 11-JUL-2026 **
** Ronny Hansen **
*****************************************************************************/
module nd_storage_fatchk #(
parameter [31:0] WD_MAX = 32'd270_000_000,
parameter [24:0] HOP_CAP = 25'd131072 // 1<<17 clusters, guard cap
) (
input wire clk_stor,
input wire rst_stor_n,
// ---- mount handoff (nd_storage_mount M_CHK) ----
input wire chk_start, // 1-cycle pulse; only when chk_busy=0
output wire chk_busy, // owns the sd_writer command mux while high
output reg chk_done, // 1-cycle pulse
output reg chk_ok, // level: valid at chk_done, held until next run
// ---- file/fs facts (latched by the mount at file_found, stable) ----
input wire fs_is_fat32,
input wire [7:0] cluster_size, // sectors per cluster (power of two)
input wire [31:0] fat0_sector, // absolute first sector of FAT copy 0
input wire [31:0] first_cluster,
input wire [31:0] size_bytes,
// ---- sd_writer command interface, read mode (muxed in the top) ----
output reg sdw_start, // 1-cycle pulse (top drives rd_mode=1)
output reg [31:0] sdw_sector,
input wire sdw_done,
input wire sdw_err,
input wire sdw_rx_we,
input wire [8:0] sdw_rx_addr,
input wire [7:0] sdw_rx_data
);
// --------------------------------------------------------- FAT sector cache
reg [7:0] fbuf[0:511];
always @(posedge clk_stor) if (sdw_rx_we) fbuf[sdw_rx_addr] <= sdw_rx_data;
// registered read port (1-clk latency, same discipline as sd_fat_check)
reg [8:0] fb_addr;
reg [7:0] fb_q;
always @(posedge clk_stor) fb_q <= fbuf[fb_addr];
// --------------------------------------------------------- helpers
function [31:0] fat_sec(input [31:0] c);
fat_sec = fat0_sector + (fs_is_fat32 ? (c >> 7) : (c >> 8));
endfunction
function [8:0] fat_off(input [31:0] c);
fat_off = fs_is_fat32 ? {c[6:0], 2'b00} : {c[7:0], 1'b0};
endfunction
function is_eoc(input [31:0] c);
is_eoc = fs_is_fat32 ? (c[27:0] >= 28'hFFFFFF7) : (c[15:0] >= 16'hFFF7);
endfunction
function [4:0] log2cs(input [7:0] cs);
casez (cs)
8'b1???????: log2cs = 5'd7;
8'b01??????: log2cs = 5'd6;
8'b001?????: log2cs = 5'd5;
8'b0001????: log2cs = 5'd4;
8'b00001???: log2cs = 5'd3;
8'b000001??: log2cs = 5'd2;
8'b0000001?: log2cs = 5'd1;
default: log2cs = 5'd0;
endcase
endfunction
// --------------------------------------------------------- FSM
localparam [3:0] K_IDLE = 4'd0;
localparam [3:0] K_CALC = 4'd1; // ceil size/bytes-per-cluster + gates
localparam [3:0] K_STEP = 4'd2; // next entry: cached sector or read
localparam [3:0] K_RD_GO = 4'd3; // pulse sd_writer start (CMD17)
localparam [3:0] K_RD_WAIT = 4'd4; // wait sdw_done / sdw_err
localparam [3:0] K_ENT_SET = 4'd5; // present the entry byte address
localparam [3:0] K_ENT_LAT = 4'd6; // registered-read latency cycle
localparam [3:0] K_ENT_GET = 4'd7; // collect little-endian entry bytes
localparam [3:0] K_CHECK = 4'd8; // expected-next or EOC verdict
localparam [3:0] K_OK = 4'd9;
localparam [3:0] K_BAD = 4'd10;
reg [3:0] s_state;
reg [32:0] s_sum; // size_bytes + bytes_per_cluster - 1 (33-bit safe)
reg [24:0] s_left; // clusters still to verify (current one included)
reg [31:0] s_cur; // cluster whose FAT entry is being verified
reg [31:0] s_cur_sec; // FAT sector currently in fbuf (all-1 = none)
reg [31:0] s_ent; // collected entry value
reg [1:0] s_byte_i;
assign chk_busy = (s_state != K_IDLE);
wire [4:0] s_lg = log2cs(cluster_size); // log2(sectors per cluster)
wire [17:0] s_bpc = {1'b0, cluster_size, 9'd0}; // bytes per cluster
wire [32:0] s_n33 = s_sum >> (s_lg + 5'd9); // ceil(size / bpc)
// entry value with the last byte merged in (used in the final K_ENT_GET)
wire [31:0] s_ent_full = s_ent | ({24'd0, fb_q} << {s_byte_i, 3'd0});
wire [31:0] s_expect = s_cur + 32'd1;
// --------------------------------------------------------- watchdog
reg [3:0] s_state_q;
reg [31:0] s_wd;
always @(posedge clk_stor) begin
if (!rst_stor_n) begin
s_state_q <= K_IDLE;
s_wd <= 32'd0;
end else begin
s_state_q <= s_state;
if (s_state != s_state_q || s_state == K_IDLE || sdw_rx_we)
s_wd <= 32'd0;
else s_wd <= s_wd + 32'd1;
end
end
wire s_wd_hit = (s_wd >= WD_MAX);
// --------------------------------------------------------- main
always @(posedge clk_stor) begin
if (!rst_stor_n) begin
s_state <= K_IDLE;
s_sum <= 33'd0;
s_left <= 25'd0;
s_cur <= 32'd0;
s_cur_sec <= 32'hFFFF_FFFF;
s_ent <= 32'd0;
s_byte_i <= 2'd0;
fb_addr <= 9'd0;
chk_done <= 1'b0;
chk_ok <= 1'b0;
sdw_start <= 1'b0;
sdw_sector <= 32'd0;
end else begin
chk_done <= 1'b0;
sdw_start <= 1'b0;
if (s_wd_hit && s_state != K_IDLE) begin
s_state <= K_BAD; // stuck engine handshake: fail the open
end else begin
case (s_state)
K_IDLE:
if (chk_start) begin
s_sum <= {1'b0, size_bytes} + {15'd0, s_bpc} - 33'd1;
s_cur <= first_cluster;
s_cur_sec <= 32'hFFFF_FFFF; // nothing cached yet
s_state <= K_CALC;
end
K_CALC:
if (size_bytes == 32'd0) begin
s_state <= K_OK; // empty file: no chain to verify, no traffic
end else if (cluster_size == 8'd0 || first_cluster < 32'd2) begin
s_state <= K_BAD; // broken geometry latch / reserved cluster
end else if (s_n33 > {8'd0, HOP_CAP}) begin
s_state <= K_BAD; // cluster-count cap: refuse, no traffic
end else begin
s_left <= s_n33[24:0];
s_state <= K_STEP;
end
K_STEP: // read FAT[s_cur]; re-read the sector only on change
if (fat_sec(s_cur) == s_cur_sec) begin
s_state <= K_ENT_SET;
end else begin
sdw_sector <= fat_sec(s_cur);
s_cur_sec <= fat_sec(s_cur);
s_state <= K_RD_GO;
end
K_RD_GO: begin
sdw_start <= 1'b1; // top mux drives rd_mode=1 while chk_busy
s_state <= K_RD_WAIT;
end
K_RD_WAIT:
if (sdw_err) s_state <= K_BAD; // card error: fail the open
else if (sdw_done) s_state <= K_ENT_SET;
K_ENT_SET: begin
fb_addr <= fat_off(s_cur);
s_byte_i <= 2'd0;
s_ent <= 32'd0;
s_state <= K_ENT_LAT;
end
K_ENT_LAT: s_state <= K_ENT_GET; // fb_q catches up with fb_addr
K_ENT_GET:
if ({1'b0, s_byte_i} == (fs_is_fat32 ? 3'd3 : 3'd1)) begin
s_ent <= s_ent_full;
s_state <= K_CHECK;
end else begin
s_ent <= s_ent_full;
s_byte_i <= s_byte_i + 2'd1;
fb_addr <= fb_addr + 9'd1;
s_state <= K_ENT_LAT;
end
K_CHECK:
if (s_left == 25'd1) begin
// last cluster of the file: a proper end-of-chain mark required
s_state <= is_eoc(s_ent) ? K_OK : K_BAD;
end else if (fs_is_fat32 ? (s_ent[27:0] == s_expect[27:0])
: (s_ent[15:0] == s_expect[15:0])) begin
s_cur <= s_expect; // contiguous so far: verify the next one
s_left <= s_left - 25'd1;
s_state <= K_STEP;
end else begin
s_state <= K_BAD; // fragmented: FAT[c] != c+1
end
K_OK: begin
chk_ok <= 1'b1;
chk_done <= 1'b1;
s_state <= K_IDLE;
end
K_BAD: begin
chk_ok <= 1'b0;
chk_done <= 1'b1;
s_state <= K_IDLE;
end
default: s_state <= K_IDLE;
endcase
end
end
end
endmodule