Skip to content

sd_fat_check

Source: Verilog/SD-FAT/circuit/sd_fat_check.v

Hierarchy: not instantiated by any of the 9 build tops (elaborated by yosys).

Module hierarchy - All modules

sd_fat_check symbol

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

sd_fat_check schematic

Description

FAT cluster-chain validator ("fsck-lite") Walks the cluster chain of every file in a table of root-directory entries (captured by the caller during a directory scan) and emits an ASCII report, one line per entry: BOOT.BPUN CL 00013AF2 LEN 00003 EXP 00003 OK TEST.TXT CL 00000002 LEN 00003 EXP 00003 OK HDD CL 00000004 DIR CHECK DONE LEN = clusters actually reachable by following the FAT until a proper end-of-chain mark; EXP = ceil(size / cluster bytes). A chain that hits a FREE entry, an out-of-range pointer or a loop, or whose length differs from EXP, is flagged BAD - exactly the damage a wrong FAT write leaves behind. Directories are listed but not walked (their length is not derivable from the size field). Uses the sd_writer engine in read mode (CMD17) on an already- initialized card; the reader must be parked. Read-only - this module never writes to the card. This file is ORIGINAL ND-120 project code (MIT, like the repository). Last reviewed: 11-JUL-2026 Ronny Hansen

Ports

Direction Width Name Description
input 1 clk
input 1 rst_n (active low)
input 1 start 1-cycle pulse; only when busy=0
output 1 busy
output 1 done 1-cycle pulse: report finished
output 1 err 1-cycle pulse: engine error (report incomplete)
input 1 fs_is_fat32
input [7:0] cluster_size sectors per cluster (power of two)
input [31:0] fat0_sector
input [31:0] data_base_sector biased, as exported by the reader
input [31:0] total_sectors
input [4:0] n_entries
output [3:0] t_idx
input [127:0] t_name first 16 characters, byte 0 in the low byte
input [31:0] t_cluster
input [31:0] t_size
input 1 t_isdir
output 1 eng_start
output 1 eng_rd
output [31:0] eng_sector
input 1 eng_done
input 1 eng_err
input 1 eng_rx_we
input [8:0] eng_rx_addr
input [7:0] eng_rx_data
output 1 ck_we
output [7:0] ck_byte

Verilog source

Verilog/SD-FAT/circuit/sd_fat_check.v on GitHub.

Show the Verilog of sd_fat_check (425 lines)
/****************************************************************************
** FAT cluster-chain validator ("fsck-lite")                               **
**                                                                         **
** Walks the cluster chain of every file in a table of root-directory      **
** entries (captured by the caller during a directory scan) and emits an   **
** ASCII report, one line per entry:                                       **
**                                                                         **
**   BOOT.BPUN        CL 00013AF2 LEN 00003 EXP 00003 OK                   **
**   TEST.TXT         CL 00000002 LEN 00003 EXP 00003 OK                   **
**   HDD              CL 00000004 DIR                                       **
**   CHECK DONE                                                            **
**                                                                         **
** LEN = clusters actually reachable by following the FAT until a proper   **
** end-of-chain mark; EXP = ceil(size / cluster bytes). A chain that hits  **
** a FREE entry, an out-of-range pointer or a loop, or whose length        **
** differs from EXP, is flagged BAD - exactly the damage a wrong FAT       **
** write leaves behind. Directories are listed but not walked (their       **
** length is not derivable from the size field).                           **
**                                                                         **
** Uses the sd_writer engine in read mode (CMD17) on an already-           **
** initialized card; the reader must be parked. Read-only - this module    **
** never writes to the card.                                               **
**                                                                         **
** This file is ORIGINAL ND-120 project code (MIT, like the repository).   **
**                                                                         **
** Last reviewed: 11-JUL-2026                                              **
** Ronny Hansen                                                            **
*****************************************************************************/

module sd_fat_check (
    input wire clk,
    input wire rst_n,

    input  wire start,  // 1-cycle pulse; only when busy=0
    output wire busy,
    output reg  done,   // 1-cycle pulse: report finished
    output reg  err,    // 1-cycle pulse: engine error (report incomplete)

    // filesystem geometry (as latched from the reader)
    input wire        fs_is_fat32,
    input wire [7:0]  cluster_size,      // sectors per cluster (power of two)
    input wire [31:0] fat0_sector,
    input wire [31:0] data_base_sector,  // biased, as exported by the reader
    input wire [31:0] total_sectors,

    // entry table (combinational read by index at the caller)
    input  wire [4:0]  n_entries,
    output reg  [3:0]  t_idx,
    input  wire [127:0] t_name,    // first 16 characters, byte 0 in the low byte
    input  wire [31:0] t_cluster,
    input  wire [31:0] t_size,
    input  wire        t_isdir,

    // sd_writer engine, read mode only (exclusive use while busy)
    output reg         eng_start,
    output wire        eng_rd,
    output reg  [31:0] eng_sector,
    input  wire        eng_done,
    input  wire        eng_err,
    input  wire        eng_rx_we,
    input  wire [8:0]  eng_rx_addr,
    input  wire [7:0]  eng_rx_data,

    // report byte stream (goes into the caller's text buffer)
    output reg       ck_we,
    output reg [7:0] ck_byte
);

  assign eng_rd = 1'b1;  // this module only reads

  // ------------------------------------------------------------- FAT buffer
  reg [7:0] fbuf[0:511];
  always @(posedge clk) if (eng_rx_we) fbuf[eng_rx_addr] <= eng_rx_data;

  reg [8:0] fb_addr;
  reg [7:0] fb_q;
  always @(posedge clk) 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 [7:0] hexd(input [3:0] n);
    hexd = (n < 4'd10) ? (8'h30 + {4'b0, n}) : (8'h37 + {4'b0, n});
  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
  function [16:0] pow10_5(input [2:0] i);
    case (i)
      3'd0: pow10_5 = 17'd1;
      3'd1: pow10_5 = 17'd10;
      3'd2: pow10_5 = 17'd100;
      3'd3: pow10_5 = 17'd1000;
      default: pow10_5 = 17'd10000;
    endcase
  endfunction

  wire [31:0] usable_sectors = total_sectors - (data_base_sector + {24'b0, cluster_size} * 2);
  wire [31:0] max_cluster = (usable_sectors >> log2cs(cluster_size)) + 32'd1;
  wire [17:0] bytes_per_cluster = {1'b0, cluster_size, 9'b0};

  localparam S_NAME  = " CL ";      // (emitted char-wise, strings below)
  localparam S_LEN   = " LEN ";
  localparam S_EXP   = " EXP ";
  localparam S_OK    = " OK\015\012";
  localparam S_BAD   = " BAD\015\012";
  localparam S_DIR   = " DIR\015\012";
  localparam S_DONE  = "CHECK DONE\015\012";

  function [7:0] strchar(input [8*12-1:0] s, input integer len, input integer i);
    strchar = (i < len) ? s[8*(len-1-i)+:8] : 8'h00;
  endfunction

  // ------------------------------------------------------------- FSM
  localparam C_IDLE    = 5'd0;
  localparam C_ENTRY   = 5'd1;   // next table entry or the DONE line
  localparam C_NAME    = 5'd2;   // 12 name chars (0 -> space)
  localparam C_CLHDR   = 5'd3;   // "CL "
  localparam C_CLHEX   = 5'd4;   // 8 hex digits of the first cluster
  localparam C_KIND    = 5'd5;   // DIR line or LEN walk
  localparam C_EXPCALC = 5'd6;   // EXP = ceil(size/bpc) subtract loop
  localparam C_WALK    = 5'd7;   // chain hop: valid cluster?
  localparam C_WRD     = 5'd8;   // read the FAT sector (engine)
  localparam C_WENT0   = 5'd9;   // fetch the entry bytes
  localparam C_WENT1   = 5'd10;
  localparam C_LENHDR  = 5'd11;  // " LEN "
  localparam C_LENDEC  = 5'd12;  // 5 decimal digits
  localparam C_EXPHDR  = 5'd13;  // " EXP "
  localparam C_EXPDEC  = 5'd14;
  localparam C_VERDICT = 5'd15;  // " OK" / " BAD" + CRLF
  localparam C_DONELN  = 5'd16;  // "CHECK DONE"
  localparam C_FIN     = 5'd17;
  localparam C_ERR     = 5'd18;
  localparam C_EGO     = 5'd19;  // engine read helper
  localparam C_EWAIT   = 5'd20;
  localparam C_BRD     = 5'd21;  // buffered byte read wait
  localparam C_STR     = 5'd22;  // generic string emitter

  reg [4:0] state, e_ret, b_ret, str_ret;
  reg [2:0] str_sel;  // 0 LEN,1 EXP,2 OK,3 BAD,4 DIR,5 DONE,6 CLHDR
  reg [3:0] sidx;
  reg [3:0] nidx;     // name/hex char counter
  reg [31:0] cur, ent_val, cur_sec;
  reg [1:0] byte_i;
  reg [16:0] chain_len, expct, decval;
  reg [2:0] decpow;
  reg [3:0] decdig;
  reg [31:0] szrem;
  reg [17:0] guard;
  reg bad;

  assign busy = (state != C_IDLE);

  reg [7:0] str_ch;
  always @(*) begin
    case (str_sel)
      3'd0: str_ch = strchar({56'b0, S_LEN}, 5, sidx);
      3'd1: str_ch = strchar({56'b0, S_EXP}, 5, sidx);
      3'd2: str_ch = strchar({56'b0, S_OK}, 5, sidx);
      3'd3: str_ch = strchar({48'b0, S_BAD}, 6, sidx);
      3'd4: str_ch = strchar({48'b0, S_DIR}, 6, sidx);
      3'd5: str_ch = strchar(S_DONE, 12, sidx);
      default: str_ch = strchar({64'b0, S_NAME}, 4, sidx);
    endcase
  end

  always @(posedge clk) begin
    if (!rst_n) begin
      state <= C_IDLE;
      e_ret <= C_IDLE;
      b_ret <= C_IDLE;
      str_ret <= C_IDLE;
      str_sel <= 0;
      sidx <= 0;
      nidx <= 0;
      done <= 1'b0;
      err <= 1'b0;
      eng_start <= 1'b0;
      eng_sector <= 0;
      ck_we <= 1'b0;
      ck_byte <= 0;
      t_idx <= 0;
      fb_addr <= 0;
      cur <= 0;
      ent_val <= 0;
      cur_sec <= 0;
      byte_i <= 0;
      chain_len <= 0;
      expct <= 0;
      decval <= 0;
      decpow <= 0;
      decdig <= 0;
      szrem <= 0;
      guard <= 0;
      bad <= 1'b0;
    end else begin
      done <= 1'b0;
      err <= 1'b0;
      eng_start <= 1'b0;
      ck_we <= 1'b0;

      case (state)
        C_IDLE:
        if (start) begin
          t_idx <= 0;
          state <= (n_entries == 0) ? C_DONELN : C_ENTRY;
          if (n_entries == 0) begin
            str_sel <= 3'd5;
            sidx <= 0;
            str_ret <= C_FIN;
            state <= C_STR;
          end
        end

        C_ENTRY: begin
          nidx <= 0;
          state <= C_NAME;
        end

        C_NAME: begin  // 16 chars, NUL padded with spaces
          ck_we <= 1'b1;
          ck_byte <= (t_name[8*nidx+:8] == 8'h00) ? 8'h20 : t_name[8*nidx+:8];
          if (nidx == 4'd15) begin
            nidx <= 0;
            str_sel <= 3'd6;  // "CL "
            sidx <= 0;
            str_ret <= C_CLHEX;
            state <= C_STR;
          end else nidx <= nidx + 4'd1;
        end

        C_CLHEX: begin
          ck_we <= 1'b1;
          ck_byte <= hexd(t_cluster[31-4*nidx-:4]);
          if (nidx == 4'd7) begin
            nidx <= 0;
            state <= C_KIND;
          end else nidx <= nidx + 4'd1;
        end

        C_KIND:
        if (t_isdir) begin
          str_sel <= 3'd4;  // " DIR" CRLF
          sidx <= 0;
          str_ret <= C_FIN;
          state <= C_STR;
        end else begin
          szrem <= t_size;
          expct <= 0;
          state <= C_EXPCALC;
        end

        C_EXPCALC:
        if (szrem == 0) begin
          cur <= fs_is_fat32 ? {4'b0, t_cluster[27:0]} : {16'b0, t_cluster[15:0]};
          chain_len <= 0;
          cur_sec <= 32'hFFFFFFFF;
          guard <= 0;
          bad <= 1'b0;
          state <= C_WALK;
        end else if (szrem >= {14'b0, bytes_per_cluster}) begin
          szrem <= szrem - {14'b0, bytes_per_cluster};
          expct <= expct + 17'd1;
        end else begin
          szrem <= 0;
          expct <= expct + 17'd1;
        end

        // ---- chain walk ---------------------------------------------------
        C_WALK:
        if (cur < 32'd2) begin
          // free/reserved pointer: fine ONLY as "no chain at all" (size 0)
          bad <= bad | (chain_len != 0) | (expct != 0);
          state <= C_LENHDR;
        end else if (is_eoc(cur)) begin
          state <= C_LENHDR;  // proper end of chain
        end else if (cur > max_cluster || guard[17]) begin
          bad <= 1'b1;        // out of range pointer, or a loop
          state <= C_LENHDR;
        end else begin
          chain_len <= chain_len + 17'd1;
          guard <= guard + 18'd1;
          if (fat_sec(cur) == cur_sec) state <= C_WENT0;
          else begin
            eng_sector <= fat_sec(cur);
            cur_sec <= fat_sec(cur);
            e_ret <= C_WENT0;
            state <= C_EGO;
          end
        end

        C_WENT0: begin
          fb_addr <= fat_off(cur);
          byte_i <= 0;
          ent_val <= 0;
          b_ret <= C_WENT1;
          state <= C_BRD;
        end

        C_WENT1: begin
          ent_val <= ent_val | ({24'b0, fb_q} << {byte_i, 3'b000});
          if ({1'b0, byte_i} == (fs_is_fat32 ? 3'd3 : 3'd1)) begin
            state <= C_WALK;
            cur <= fs_is_fat32
                 ? ((ent_val | ({24'b0, fb_q} << {byte_i, 3'b000})) & 32'h0FFFFFFF)
                 : ((ent_val | ({24'b0, fb_q} << {byte_i, 3'b000})) & 32'h0000FFFF);
          end else begin
            byte_i <= byte_i + 2'd1;
            fb_addr <= fb_addr + 9'd1;
            b_ret <= C_WENT1;
            state <= C_BRD;
          end
        end

        // ---- report tail ---------------------------------------------------
        C_LENHDR: begin
          str_sel <= 3'd0;
          sidx <= 0;
          str_ret <= C_LENDEC;
          decval <= chain_len;
          decpow <= 3'd4;
          decdig <= 0;
          state <= C_STR;
        end

        C_LENDEC:
        if (decval >= pow10_5(decpow)) begin
          decval <= decval - pow10_5(decpow);
          decdig <= decdig + 4'd1;
        end else begin
          ck_we <= 1'b1;
          ck_byte <= 8'h30 + {4'b0, decdig};
          decdig <= 0;
          if (decpow == 0) begin
            str_sel <= 3'd1;
            sidx <= 0;
            str_ret <= C_EXPDEC;
            decval <= expct;
            decpow <= 3'd4;
            state <= C_STR;
          end else decpow <= decpow - 3'd1;
        end

        C_EXPDEC:
        if (decval >= pow10_5(decpow)) begin
          decval <= decval - pow10_5(decpow);
          decdig <= decdig + 4'd1;
        end else begin
          ck_we <= 1'b1;
          ck_byte <= 8'h30 + {4'b0, decdig};
          decdig <= 0;
          if (decpow == 0) state <= C_VERDICT;
          else decpow <= decpow - 3'd1;
        end

        C_VERDICT: begin
          str_sel <= (bad || chain_len != expct) ? 3'd3 : 3'd2;  // BAD / OK
          sidx <= 0;
          str_ret <= C_FIN;
          state <= C_STR;
        end

        C_FIN:
        if (t_idx + 5'd1 >= n_entries[4:0]) begin
          str_sel <= 3'd5;  // "CHECK DONE"
          sidx <= 0;
          str_ret <= C_ERR;  // (reused as terminal-done, see below)
          state <= C_STR;
        end else begin
          t_idx <= t_idx + 4'd1;
          state <= C_ENTRY;
        end

        C_ERR: begin  // terminal state: emit done (err path jumps here too)
          done <= 1'b1;
          state <= C_IDLE;
        end

        C_DONELN: state <= C_ERR;

        // ---- helpers -------------------------------------------------------
        C_EGO: begin
          eng_start <= 1'b1;
          state <= C_EWAIT;
        end

        C_EWAIT:
        if (eng_done) state <= e_ret;
        else if (eng_err) begin
          err <= 1'b1;
          state <= C_IDLE;
        end

        C_BRD: state <= b_ret;

        C_STR:
        if (str_ch == 8'h00) state <= str_ret;
        else begin
          ck_we <= 1'b1;
          ck_byte <= str_ch;
          sidx <= sidx + 4'd1;
        end

        default: state <= C_IDLE;
      endcase
    end
  end

endmodule