Skip to content

sd_fat_test_top

Source: Verilog/fpga/tang-nano-20k/sd-fat-test/src/sd_fat_test_top.v

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

Module hierarchy - All modules

sd_fat_test_top symbol

Schematic

Schematic not generated: netlistsvg failed: RangeError: Maximum call stack size exceeded.

Description

Tang Nano 20K SD + FAT test (Milestone 1 of the SD-BPUN device plan) Interactive UART menu (BL616 USB serial, 9600 8N1) over the reusable SD/FAT library in Verilog/SD-FAT/: SD: OK (persistent card status) 1=LIST 2=DUMP BOOT.BPUN 3=COPY TO TEST.TXT H=HELP

1 re-init the card, mount FAT16/FAT32, list the root directory: size (or

), date and name of every file and subdirectory 2342 10-JAN-2025 BOOT.BPUN 11-JUL-2026 HDD then a disk-info line: card capacity (from the CMD9 CSD), volume size (total sectors) and free space (one FAT #0 scan counting free entries; a SCANNING line is printed first - a 32 GB card takes seconds): CARD 30436 MB VOL 30425 MB FREE 30298 MB 2 find BOOT.BPUN, buffer it (64 KB BRAM) and hex/octal-dump it 3 COPY: read BOOT.BPUN into the buffer, then make TEST.TXT an exact copy. If TEST.TXT's allocation is big enough its data sectors are rewritten in place and only the size field is patched; if it is TOO SMALL (or empty) it is REPLACED: sd_fat_rewrite frees the old cluster chain, allocates a fresh contiguous chain and patches the directory entry, then the data is written. TEST.TXT must already exist on the card (any size). 4 WRBLK1: write a counter pattern (1024 big-endian 16-bit words, word[w] = w) into 1-kiloword block 1 of BOOT.BPUN. ND-120 block framing: 1 block = 1024 words = 2048 bytes = 4 SD sectors; block N of a file lives at file_first_sector + 4N. Refuses when the file is smaller than (N+1)2048 bytes (a shorter file's cluster chain ends inside the block - writing there would corrupt the NEXT file). Validate with 2 (DUMP): block 1 shows the pattern, blocks 0 and 2 are untouched. 8 BLOCK: dump 1-kiloword block N of the target file WITHOUT buffering the file. The reader runs with no_stream=1 (directory match only), the 4 sectors of the block are read one at a time through the sd_writer engine's READ path and dumped. Cost and memory are the same for a 7 KB file and a 75 MB one - which is the point: command 2 cannot look at anything past its 64 KB buffer. The block number is typed on the console in decimal. 9 SECTOR: dump one ABSOLUTE SD sector, number typed in decimal, with the FAT not consulted at all. This is what separates "the directory entry points at the wrong place" from "the card really holds zeros there" - the two look identical through command 2. R RANGE: read a run of consecutive blocks (start block and count typed on the console) the same streaming way, keeping only a 16-bit checksum of every word and a block count - the data never reaches the console, because a 1000-block hex dump at 9600 baud would take about six hours. One progress line per 64 blocks, then a summary: blocks, sectors, checksum, 27 MHz cycles of card traffic, PASS or FAIL. A failed card read stops the run and names the exact sector and block - that is what this command is for: every earlier check read a SINGLE block, and SINTRAN segment handling is the first thing that reads many in a row. N set the target file name at runtime (root directory only, 8.3 name, length-exact - that is all sd_file_reader can match) H detailed help; any other key reprints the menu S1 / S2 (either button) full reset DEFAULT BUILD IS READ-ONLY (src/sd_fat_test_config.vh): 3, 4, 6 are compiled out and the sd_writer engine's rd_mode is tied to a constant 1. A write-capable bitstream must never be pointed at a card that is being diagnosed. Robustness: every wait state has a watchdog - no card, no file, unmountable filesystem, stuck reads and failed writes all print an ERROR line and fall back to the menu; the menu SD: line shows NOT CHECKED / NO CARD / ERROR / OK. SD pins per the Sipeed Tang Nano 20K schematic (verified against sipeed/TangNano-20K-example, nestang and snestang constraint files): CLK=83 CMD=82 DAT0=84 DAT1=85 DAT2=80 DAT3=81. CMD and DAT0-3 are bidirectional per the SD specification; the ONLY tristate drivers sit here at the top-level pads (repo rule: no 'z' inside the FPGA fabric). LEDs (active low): [0] alive [1] FS mounted [2] file found [3] command done [4] truncated [5] error Single 27 MHz clock domain - no PLL, no derived clocks (the SD library and sd_writer divide their bus clocks with enable-style dividers). Reader CLK_DIV=1 (init 137 kHz - inside the spec's 100-400 kHz identification band - data 3.375 MHz); writer CLKDIV=1 (13.5 MHz bit clock, inside the mandatory 25 MHz default-speed limit). Speed tests: menu 6 writes IO.DAT in CMD25 multi-block bursts (ACMD23 pre-erase + up to 128 sectors per burst + CMD12), menu 7 reads it back in CMD18 bursts through the same MIT engine - the file reader only mounts the filesystem and locates the file. With USE_4BIT=1 (default, speed-plan rung c) every writer-engine operation switches the card to the 4-bit DAT bus via CMD55+ACMD6 and moves data on DAT3..DAT0; the reader keeps its proven 1-bit path for mount/locate. Design plan: Verilog/docs/sd-bpun-device-plan.md (section 9). Last reviewed: 11-JUL-2026 Ronny Hansen

Parameters

Parameter Default
CLK_FREQ 27_000_000
BAUD 9600
SIMULATE 0
WD_MAX 270_000_000
FILE_NAME_LEN 9
FILE_NAME "BOOT.BPUN"
FILE2_LEN 8
FILE2_NAME "TEST.TXT"
FILE3_LEN 6
FILE3_NAME "IO.DAT"
IO_BLOCKS 1000
FILE2_83 "TEST TXT"
FILE3_83 "IO DAT"
FDATE 16'h5CEB
BUF_AW 16
USE_4BIT 1 // full speed. (11-JUL "all BAD" incident: 1-bit isolation build proved card+logic healthy; suspect was the snooped RCA

Ports

Direction Width Name Description
input 1 sys_clk 27 MHz crystal
input 1 s1 S1 push button - full reset
input 1 s2 S2 push button - full reset (same, for convenience)
input 1 uart_rxp from BL616 USB serial
output 1 uart_txp to BL616 USB serial
output 1 sd_clk
inout 1 sd_cmd bidirectional: host commands / card responses
inout 1 sd_dat0 bidirectional: card read data / host write data
inout 1 sd_dat1 bidirectional in 4-bit mode (parked high otherwise)
inout 1 sd_dat2
inout 1 sd_dat3
output [5:0] led active low
output 1 ext_start 1-cycle pulse: run test ext_id
output [3:0] ext_id 0 = DDR2, 1 = ND-120 memory path (BRAM)
input 1 ext_busy high while the external test runs
input [7:0] ext_tx_data its character stream...
input 1 ext_tx_valid ...with the same handshake as the others
output 1 ext_tx_busy the shared UART's busy flag

Verilog source

Verilog/fpga/tang-nano-20k/sd-fat-test/src/sd_fat_test_top.v on GitHub.

Show the Verilog of sd_fat_test_top (2727 lines)
/****************************************************************************
** Tang Nano 20K SD + FAT test (Milestone 1 of the SD-BPUN device plan)    **
**                                                                         **
** Interactive UART menu (BL616 USB serial, 9600 8N1) over the reusable    **
** SD/FAT library in Verilog/SD-FAT/:                                      **
**                                                                         **
**   SD: OK                     (persistent card status)                   **
**   1=LIST 2=DUMP BOOT.BPUN 3=COPY TO TEST.TXT H=HELP                     **
**   #                                                                     **
**                                                                         **
**   1  re-init the card, mount FAT16/FAT32, list the root directory:      **
**      size (or <DIR>), date and name of every file and subdirectory      **
**             2342  10-JAN-2025  BOOT.BPUN                                **
**            <DIR>  11-JUL-2026  HDD                                      **
**      then a disk-info line: card capacity (from the CMD9 CSD), volume   **
**      size (total sectors) and free space (one FAT #0 scan counting      **
**      free entries; a SCANNING line is printed first - a 32 GB card      **
**      takes seconds):                                                    **
**             CARD 30436 MB  VOL 30425 MB  FREE 30298 MB                  **
**   2  find BOOT.BPUN, buffer it (64 KB BRAM) and hex/octal-dump it       **
**   3  COPY: read BOOT.BPUN into the buffer, then make TEST.TXT an exact **
**      copy. If TEST.TXT's allocation is big enough its data sectors are  **
**      rewritten in place and only the size field is patched; if it is    **
**      TOO SMALL (or empty) it is REPLACED: sd_fat_rewrite frees the old  **
**      cluster chain, allocates a fresh contiguous chain and patches the  **
**      directory entry, then the data is written. TEST.TXT must already   **
**      exist on the card (any size).                                      **
**   4  WRBLK1: write a counter pattern (1024 big-endian 16-bit words,     **
**      word[w] = w) into 1-kiloword block 1 of BOOT.BPUN. ND-120 block    **
**      framing: 1 block = 1024 words = 2048 bytes = 4 SD sectors; block   **
**      N of a file lives at file_first_sector + 4*N. Refuses when the     **
**      file is smaller than (N+1)*2048 bytes (a shorter file's cluster    **
**      chain ends inside the block - writing there would corrupt the      **
**      NEXT file). Validate with 2 (DUMP): block 1 shows the pattern,     **
**      blocks 0 and 2 are untouched.                                      **
**   8  BLOCK: dump 1-kiloword block N of the target file WITHOUT          **
**      buffering the file. The reader runs with no_stream=1 (directory    **
**      match only), the 4 sectors of the block are read one at a time     **
**      through the sd_writer engine's READ path and dumped. Cost and      **
**      memory are the same for a 7 KB file and a 75 MB one - which is the **
**      point: command 2 cannot look at anything past its 64 KB buffer.    **
**      The block number is typed on the console in decimal.               **
**   9  SECTOR: dump one ABSOLUTE SD sector, number typed in decimal, with **
**      the FAT not consulted at all. This is what separates "the          **
**      directory entry points at the wrong place" from "the card really   **
**      holds zeros there" - the two look identical through command 2.     **
**   R  RANGE: read a run of consecutive blocks (start block and count     **
**      typed on the console) the same streaming way, keeping only a       **
**      16-bit checksum of every word and a block count - the data never   **
**      reaches the console, because a 1000-block hex dump at 9600 baud    **
**      would take about six hours. One progress line per 64 blocks, then  **
**      a summary: blocks, sectors, checksum, 27 MHz cycles of card        **
**      traffic, PASS or FAIL. A failed card read stops the run and names  **
**      the exact sector and block - that is what this command is for:     **
**      every earlier check read a SINGLE block, and SINTRAN segment       **
**      handling is the first thing that reads many in a row.              **
**   N  set the target file name at runtime (root directory only, 8.3      **
**      name, length-exact - that is all sd_file_reader can match)         **
**   H  detailed help; any other key reprints the menu                     **
**   S1 / S2 (either button) full reset                                    **
**                                                                         **
** DEFAULT BUILD IS READ-ONLY (src/sd_fat_test_config.vh): 3, 4, 6 are     **
** compiled out and the sd_writer engine's rd_mode is tied to a constant   **
** 1. A write-capable bitstream must never be pointed at a card that is    **
** being diagnosed.                                                        **
**                                                                         **
** Robustness: every wait state has a watchdog - no card, no file,         **
** unmountable filesystem, stuck reads and failed writes all print an      **
** ERROR line and fall back to the menu; the menu SD: line shows           **
** NOT CHECKED / NO CARD / ERROR / OK.                                     **
**                                                                         **
** SD pins per the Sipeed Tang Nano 20K schematic (verified against       **
** sipeed/TangNano-20K-example, nestang and snestang constraint files):    **
** CLK=83 CMD=82 DAT0=84 DAT1=85 DAT2=80 DAT3=81. CMD and DAT0-3 are       **
** bidirectional per the SD specification; the ONLY tristate drivers sit   **
** here at the top-level pads (repo rule: no 'z' inside the FPGA fabric).  **
**                                                                         **
** LEDs (active low): [0] alive  [1] FS mounted  [2] file found            **
**                    [3] command done  [4] truncated  [5] error           **
**                                                                         **
** Single 27 MHz clock domain - no PLL, no derived clocks (the SD          **
** library and sd_writer divide their bus clocks with enable-style         **
** dividers). Reader CLK_DIV=1 (init 137 kHz - inside the spec's           **
** 100-400 kHz identification band - data 3.375 MHz); writer CLKDIV=1      **
** (13.5 MHz bit clock, inside the mandatory 25 MHz default-speed limit).  **
**                                                                         **
** Speed tests: menu 6 writes IO.DAT in CMD25 multi-block bursts (ACMD23   **
** pre-erase + up to 128 sectors per burst + CMD12), menu 7 reads it back  **
** in CMD18 bursts through the same MIT engine - the file reader only      **
** mounts the filesystem and locates the file. With USE_4BIT=1 (default,   **
** speed-plan rung c) every writer-engine operation switches the card to   **
** the 4-bit DAT bus via CMD55+ACMD6 and moves data on DAT3..DAT0; the     **
** reader keeps its proven 1-bit path for mount/locate.                    **
**                                                                         **
** Design plan: Verilog/docs/sd-bpun-device-plan.md (section 9).           **
**                                                                         **
** Last reviewed: 11-JUL-2026                                              **
** Ronny Hansen                                                            **
*****************************************************************************/

`include "sd_fat_test_config.vh"
`include "sd_fat_features.vh"

module sd_fat_test_top #(
    parameter            CLK_FREQ      = 27_000_000,   // crystal, used directly
    parameter            BAUD          = 9600,
    parameter            SIMULATE      = 0,            // 1: shorter SD init (sim only)
    parameter [31:0]     WD_MAX        = 270_000_000,  // watchdog: 10 s at 27 MHz
    parameter            FILE_NAME_LEN = 9,
    parameter [52*8-1:0] FILE_NAME     = "BOOT.BPUN",
    parameter            FILE2_LEN     = 8,
    parameter [52*8-1:0] FILE2_NAME    = "TEST.TXT",   // COPY target (pre-created)
    parameter            FILE3_LEN     = 6,
    parameter [52*8-1:0] FILE3_NAME    = "IO.DAT",     // speed-test file (pre-created)
    parameter            IO_BLOCKS     = 1000,         // speed test: 1KW blocks
    parameter [87:0]     FILE2_83      = "TEST    TXT", // 8.3 form for creation
    parameter [87:0]     FILE3_83      = "IO      DAT",
    parameter [15:0]     FDATE         = 16'h5CEB,      // 11-JUL-2026 (create stamp)
    parameter            BUF_AW        = 16,           // 2^16 = 64 KB buffer
    // USE_4BIT - SD bus width for ALL bulk data through the sd_writer
    // engine (speed tests, CHECK/COPY/WRBLK1 FAT + payload traffic):
    //   1 = DAT0..DAT3, four data wires: ~4x throughput (sim: 5.9 MB/s
    //       write / 6.4 MB/s read). THE production / full-speed setting.
    //       Each operation switches the card via CMD55+ACMD6 first, so
    //       the reader's card re-inits between commands are harmless.
    //   0 = DAT0 only (sim: ~1.6 MB/s). For diagnosis, or boards where
    //       only DAT0 is wired. Commands/CRC-status/busy are on their
    //       single lines in BOTH modes; the FAT mount/scan path
    //       (sd_file_reader) always runs 1-bit regardless.
    // Full story: README.md "USE_4BIT" + Verilog/docs/sd-cmd18-block-gap-research.md.
    parameter            USE_4BIT      = 1             // full speed. (11-JUL "all BAD" incident: 1-bit isolation build proved card+logic healthy; suspect was the snooped RCA, now taken from the reader's card_rca export)
) (
    input  sys_clk,   // 27 MHz crystal
    input  s1,        // S1 push button - full reset
    input  s2,        // S2 push button - full reset (same, for convenience)
    input  uart_rxp,  // from BL616 USB serial
    output uart_txp,  // to BL616 USB serial

    // microSD slot, SD-native mode; the reader runs 1-bit, the writer
    // engine switches to 4-bit per operation when USE_4BIT=1 (rung c)
    output sd_clk,
    inout  sd_cmd,    // bidirectional: host commands / card responses
    inout  sd_dat0,   // bidirectional: card read data / host write data
    inout  sd_dat1,   // bidirectional in 4-bit mode (parked high otherwise)
    inout  sd_dat2,
    inout  sd_dat3,

    output [5:0] led  // active low

`ifdef SDFAT_EXT_TEST
    // External test commands (Nexys 4 DDR memory tests). Gated: builds that
    // do not define SDFAT_EXT_TEST are byte-identical to before. The external
    // module runs a self-contained test and prints through this design's UART.
    ,
    output reg       ext_start,     // 1-cycle pulse: run test ext_id
    output reg [3:0] ext_id,        // 0 = DDR2, 1 = ND-120 memory path (BRAM)
    input  wire      ext_busy,      // high while the external test runs
    input  wire [7:0] ext_tx_data,  // its character stream...
    input  wire      ext_tx_valid,  // ...with the same handshake as the others
    output wire      ext_tx_busy    // the shared UART's busy flag
`endif
);

  localparam DELAY_FRAMES = CLK_FREQ / BAUD;

  wire clk = sys_clk;  // single 27 MHz domain, no PLL

  /*******************************************************************************
   ** Reset: power-on counter, reloaded by S1 = full restart                     **
   *******************************************************************************/
  reg s1_r1, s1_r2, s1_r3, s2_r1, s2_r2, s2_r3;
  always @(posedge clk) begin
    s1_r1 <= s1;
    s1_r2 <= s1_r1;
    s1_r3 <= s1_r2;
    s2_r1 <= s2;
    s2_r2 <= s2_r1;
    s2_r3 <= s2_r2;
  end
  wire btn_press = (s1_r2 & ~s1_r3) | (s2_r2 & ~s2_r3);

  reg [10:0] rst_cnt = 0;
  wire sys_rst_n = rst_cnt[10];

  always @(posedge clk) begin
    if (btn_press) rst_cnt <= 0;
    else if (!rst_cnt[10]) rst_cnt <= rst_cnt + 1;
  end

  // The SD reader is held in reset between commands; each menu command
  // releases it for a full init+mount+scan run (= the future tape rewind).
  // COPY resets it a second time (rdrst_cnt) to re-scan for TEST.TXT, and
  // parks it (phase_write) while the sd_writer owns the card.
  reg       cmd_running;
  reg [4:0] rdrst_cnt;
  reg       phase_write;
  wire sd_rst_n = sys_rst_n & cmd_running & (rdrst_cnt == 0) & ~phase_write;

  /*******************************************************************************
   ** Command mode and runtime target file name                                 **
   *******************************************************************************/
  localparam M_DUMP = 4'd0;
  localparam M_LIST = 4'd1;
  localparam M_COPY = 4'd2;
  localparam M_BLK  = 4'd3;  // write the test pattern into 1KW block 1
  localparam M_CHK  = 4'd4;  // validate every root file's cluster chain
  localparam M_IOW  = 4'd5;  // write speed test (rewrites IO.DAT)
  localparam M_IOR  = 4'd6;  // read speed test (streams IO.DAT)
  localparam M_BDMP = 4'd7;  // dump 1KW block N of the file, nothing buffered
  localparam M_SDMP = 4'd8;  // dump one absolute SD sector, FAT bypassed
  localparam M_RDMP = 4'd9;  // read a RANGE of blocks, checksum only

  localparam [31:0] IO_BYTES = IO_BLOCKS * 32'd2048;
  localparam [12:0] IO_SECT  = IO_BLOCKS * 13'd4;
  // KB/s numerator: KB * clock frequency (40-bit: 2000 * 27e6 overflows 32)
  localparam [39:0] SPEED_NUM = 40'd2 * IO_BLOCKS * CLK_FREQ;

  localparam [8:0] BLK_NO = 9'd1;  // the block the menu command updates

  reg [3:0] mode;
  reg       copy_phase;  // COPY: 0 = read BOOT.BPUN, 1 = locate TEST.TXT
  reg       list_done;   // LIST: listing captured - the reader's automatic
                         // restart after the free scan must not append again
  wire      mode_list = (mode == M_LIST);

  wire [52*8-1:0] tgt_boot, tgt_test, tgt_io;
  generate
    genvar tk;
    for (tk = 0; tk < 52; tk = tk + 1) begin : g_target_names
      assign tgt_boot[8*tk+:8] =
          (tk < FILE_NAME_LEN) ? FILE_NAME[8*(FILE_NAME_LEN-1-tk)+:8] : 8'h00;
      assign tgt_test[8*tk+:8] =
          (tk < FILE2_LEN) ? FILE2_NAME[8*(FILE2_LEN-1-tk)+:8] : 8'h00;
      assign tgt_io[8*tk+:8] =
          (tk < FILE3_LEN) ? FILE3_NAME[8*(FILE3_LEN-1-tk)+:8] : 8'h00;
    end
  endgenerate

  // ---- runtime target file name (menu key N) ------------------------------
  // Typed on the console; empty (rt_len = 0, the state after reset) means the
  // compile-time FILE_NAME parameter is used, so nothing changes for an
  // operator who never presses N. sd_file_reader matches ROOT-DIRECTORY
  // entries only and compares the name LENGTH-EXACTLY (case-insensitively),
  // which is why the help text spells that out.
  reg [52*8-1:0] rt_name;
  reg [7:0]      rt_len;
  wire           rt_use = (rt_len != 8'd0);

  wire copy2 = (mode == M_COPY) && copy_phase;
  wire io_mode = (mode == M_IOW) || (mode == M_IOR);
  // block dump (8), sector dump (9) and the range read (R): nothing is
  // buffered, so all three are "streaming" modes as far as the buffer, the
  // engine direction and the error texts are concerned
  wire dump_mode = (mode == M_BDMP) || (mode == M_SDMP) || (mode == M_RDMP);
  wire [52*8-1:0] target_name = io_mode ? tgt_io : copy2 ? tgt_test
                              : rt_use ? rt_name : tgt_boot;
  // the absolute-sector dump never looks at a file: target_len 0 = scan only
  wire scan_only = mode_list || (mode == M_CHK) || (mode == M_SDMP);
  wire [7:0] target_len = scan_only ? 8'd0
                        : io_mode ? FILE3_LEN[7:0]
                        : copy2 ? FILE2_LEN[7:0]
                        : rt_use ? rt_len : FILE_NAME_LEN[7:0];
  // BLOCK: stop at the directory match. Reading a 75 MB file into a 64 KB
  // buffer only to look at 2 KB of it is what this command exists to avoid,
  // and no_stream also ends the reader's run at a clean card boundary.
  wire no_stream = (mode == M_BDMP) || (mode == M_RDMP);

  /*******************************************************************************
   ** SD card + FAT filesystem library (Verilog/SD-FAT/, board-independent)     **
   *******************************************************************************/
  wire        rd_sdclk;
  wire        rd_cmd_o, rd_cmd_oe;
  wire [3:0]  card_stat;
  wire [1:0]  card_type;
  wire [1:0]  fs_type;
  wire        file_found;
  wire        f_outen;
  wire [7:0]  f_outbyte;
  wire        scan_done;
  wire [31:0] file_size;
  wire        dir_valid;
  wire [52*8-1:0] dir_name;
  wire [7:0]  dir_len;
  wire [31:0] dir_size;
  wire [15:0] dir_date;
  wire [31:0] dir_cluster;
  wire        dir_isdir;
  wire [31:0] file_first_sector;
  wire [7:0]  fs_cs;
  wire [31:0] fs_fat0, fs_spf, fs_dbase, fs_total, fs_rootclus;
  wire [7:0]  fs_nfat;
  wire [31:0] fnd_dir_sec;
  wire [3:0]  fnd_dir_idx;
  wire [31:0] fnd_cluster;
  wire [31:0] rd_cap_mb;  // card capacity from the CMD9 CSD (0 = unknown)
  wire [15:0] rd_rca;     // CMD3-published RCA

  sd_file_reader #(
      .CLK_DIV (3'd1),      // 27 MHz: init 137 kHz (spec 100-400 kHz), data 3.375 MHz
      .SIMULATE(SIMULATE)
  ) u_sd (
      .rstn           (sd_rst_n),
      .clk            (clk),
      .sdclk          (rd_sdclk),
      .sdcmd_i        (sd_cmd),
      .sdcmd_o        (rd_cmd_o),
      .sdcmd_oe       (rd_cmd_oe),
      .sddat0         (sd_dat0),
      .card_stat      (card_stat),
      .card_type      (card_type),
      .filesystem_type(fs_type),
      .file_found     (file_found),
      .outen          (f_outen),
      .outbyte        (f_outbyte),
      .scan_done      (scan_done),
      .found_file_size(file_size),
      .target_name    (target_name),
      .no_stream      (no_stream),
      .target_len     (target_len),
      .dir_entry_valid(dir_valid),
      .dir_entry_name (dir_name),
      .dir_entry_len  (dir_len),
      .dir_entry_size (dir_size),
      .dir_entry_date (dir_date),
      .dir_entry_cluster(dir_cluster),
      .dir_entry_is_dir(dir_isdir),
      .found_file_first_sector(file_first_sector),
      .fs_cluster_size(fs_cs),
      .fs_fat0_sector(fs_fat0),
      .fs_sectors_per_fat(fs_spf),
      .fs_num_fats(fs_nfat),
      .fs_data_base_sector(fs_dbase),
      .fs_total_sectors(fs_total),
      .fs_root_cluster(fs_rootclus),
      .found_dir_entry_sector(fnd_dir_sec),
      .found_dir_entry_index(fnd_dir_idx),
      .found_file_cluster(fnd_cluster),
      .card_capacity_mb(rd_cap_mb),
      .card_rca       (rd_rca)
  );

  // card init done = sector-read FSM reached its CMD17 idle state (8) or beyond
  wire card_ready = (card_stat >= 4'd8);

  /*******************************************************************************
   ** RCA hold: the burst writer needs the CMD3-assigned RCA for CMD55 (the    **
   ** ACMD23/ACMD6 prefixes), but the reader that owns it is parked - reset -  **
   ** while the writer runs (phase_write). Track the reader's exported RCA     **
   ** while it is live and hold the last good value through the parking.       **
   ** (This replaces the old CMD-line response snoop: with CMD9's 136-bit R2   **
   ** now following CMD3, a bit pattern inside the CSD could have retriggered  **
   ** the snoop and corrupted the captured RCA.)                               **
   *******************************************************************************/
  reg [15:0] card_rca;

  always @(posedge clk) begin
    if (!sys_rst_n) card_rca <= 16'h0000;
    else if (card_ready) card_rca <= rd_rca;
  end

  /*******************************************************************************
   ** SD sector writer (project code, MIT) - owns the card during COPY writes   **
   *******************************************************************************/
  reg             wr_start;
  reg  [31:0]     tgt_sector;
  reg  [12:0]     nsec, seccnt;
  reg  [8:0]      wr_burst;  // burst length for the next engine start (menus 6/7)
  reg  [BUF_AW:0] copy_len;

  wire       wr_sdclk, wr_cmd_o, wr_cmd_oe, wr_dat0_o, wr_dat0_oe;
  wire       wr_dat1_o, wr_dat1_oe, wr_dat2_o, wr_dat2_oe, wr_dat3_o, wr_dat3_oe;
  wire       s_use4 = (USE_4BIT != 0);
  wire       wr_busy, wr_done, wr_err, wr_block_next;
  wire [8:0] wr_rd_addr;
  wire [7:0] wr_data_in;

  // menus 6/7 burst in chunks of up to 128 sectors (IO.DAT is contiguous)
  wire [12:0] sec_left  = nsec - seccnt;
  wire [8:0]  burst_now = (sec_left > 13'd128) ? 9'd128 : sec_left[8:0];

  // engine control is muxed: the FAT surgeon owns it while fx_busy,
  // the data-sector loop otherwise
  wire        fx_busy, fx_done, fx_err;
  wire [1:0]  fx_err_kind;  // surgeon verdict: 0 no-space, 1 read, 2 write, 3 corrupt
  reg         fx_start, fx_realloc, fx_create;
  wire [31:0] fx_new_first;
  wire        fx_eng_start, fx_eng_rd;
  wire [31:0] fx_eng_sector;
  wire        eng_rx_we;
  wire [8:0]  eng_rx_addr;
  wire [7:0]  eng_rx_data;
  wire [7:0]  fx_tx_data;

  // free-space scanner (LIST info line) - shares the engine like the checker
  reg         fsn_start;
  wire        fsn_busy, fsn_done, fsn_err, fsn_tick;
  wire        fsn_eng_start, fsn_eng_rd;
  wire [31:0] fsn_eng_sector;
  wire [31:0] fsn_free_clusters, fsn_free_mb;

  // geometry/target latches (captured while the reader still holds them)
  reg        cp_is32;
  reg [7:0]  cp_cs, cp_nfat;
  reg [31:0] cp_fat0, cp_spf, cp_dbase, cp_total;
  reg [31:0] cp_rootclus;
  reg [31:0] cp_dir_sec;
  reg [3:0]  cp_dir_idx;
  reg [31:0] cp_old_cluster;
  reg [31:0] cp_cap;  // card capacity in MB, held for the LIST info line

`ifdef SDFAT_WRITE
  sd_writer #(
      .CLKDIV(8'd1)  // 27 MHz / 2 = 13.5 MHz bit clock (25 MHz default-speed limit)
  ) u_wr (
      .clk       (clk),
      .rst_n     (sys_rst_n),
      .sd_clk_o  (wr_sdclk),
      .sd_cmd_i  (sd_cmd),
      .sd_cmd_o  (wr_cmd_o),
      .sd_cmd_oe (wr_cmd_oe),
      .sd_dat0_i (sd_dat0),
      .sd_dat0_o (wr_dat0_o),
      .sd_dat0_oe(wr_dat0_oe),
      .sd_dat1_i (sd_dat1),
      .sd_dat1_o (wr_dat1_o),
      .sd_dat1_oe(wr_dat1_oe),
      .sd_dat2_i (sd_dat2),
      .sd_dat2_o (wr_dat2_o),
      .sd_dat2_oe(wr_dat2_oe),
      .sd_dat3_i (sd_dat3),
      .sd_dat3_o (wr_dat3_o),
      .sd_dat3_oe(wr_dat3_oe),
      .use_4bit  (s_use4),
      // this design's reader re-initialises the card between commands, so a
      // negotiated width never survives: renegotiate every operation
      .width_hold(1'b0),
      .start     (fx_busy ? fx_eng_start : ck_busy ? ck_eng_start
                          : fsn_busy ? fsn_eng_start : wr_start),
`ifdef SDFAT_TEST_READONLY
      // READ-ONLY BUILD: a constant, so the engine's CMD24/CMD25 write paths
      // have no reachable driver and synthesis prunes them. This is the last
      // line of defence behind the missing menu keys - see
      // src/sd_fat_test_config.vh for why the tool defaults this way.
      .rd_mode   (1'b1),
`else
      .rd_mode   (fx_busy ? fx_eng_rd : ck_busy ? ck_eng_rd
                          : fsn_busy ? fsn_eng_rd
                          : ((mode == M_IOR) || dump_mode)),
`endif
      .sector    (fx_busy ? fx_eng_sector : ck_busy ? ck_eng_sector
                          : fsn_busy ? fsn_eng_sector
                          : tgt_sector + {19'b0, seccnt}),
      .busy      (wr_busy),
      .done      (wr_done),
      .err       (wr_err),
      .burst_len ((fx_busy || ck_busy || fsn_busy) ? 9'd1 : wr_burst),
      .rca       (card_rca),
      .block_next(wr_block_next),
      .rd_addr   (wr_rd_addr),
      .rd_data   (fx_busy ? fx_tx_data : wr_data_in),
      .rx_we     (eng_rx_we),
      .rx_addr   (eng_rx_addr),
      .rx_data   (eng_rx_data)
  );
`else
  // write engine stripped: commands depending on it answer NOT IMPLEMENTED
  assign wr_sdclk = 1'b0;
  assign wr_cmd_o = 1'b1;
  assign wr_cmd_oe = 1'b0;
  assign wr_dat0_o = 1'b1;
  assign wr_dat0_oe = 1'b0;
  assign wr_dat1_o = 1'b1;
  assign wr_dat1_oe = 1'b0;
  assign wr_dat2_o = 1'b1;
  assign wr_dat2_oe = 1'b0;
  assign wr_dat3_o = 1'b1;
  assign wr_dat3_oe = 1'b0;
  assign wr_busy = 1'b0;
  assign wr_done = 1'b0;
  assign wr_err = 1'b1;
  assign wr_block_next = 1'b0;
  assign wr_rd_addr = 9'd0;
  assign eng_rx_we = 1'b0;
  assign eng_rx_addr = 9'd0;
  assign eng_rx_data = 8'h00;
`endif

  // ---- CHECK: root entry table captured during the scan -------------------
  reg [127:0] vt_name  [0:15];
  reg [31:0] vt_cluster[0:15];
  reg [31:0] vt_size   [0:15];
  reg [15:0] vt_isdir;
  reg [4:0]  vt_n;

  always @(posedge clk) begin
    if (!sd_rst_n) begin
      vt_n <= 0;
    end else if (mode == M_CHK && dir_valid && dir_len != 0 && !vt_n[4]) begin
      vt_name[vt_n[3:0]]    <= dir_name[127:0];
      vt_cluster[vt_n[3:0]] <= dir_cluster;
      vt_size[vt_n[3:0]]    <= dir_size;
      vt_isdir[vt_n[3:0]]   <= dir_isdir;
      vt_n <= vt_n + 5'd1;
    end
  end

  reg         ck_start;
  wire        ck_busy, ck_done, ck_err;
  wire [3:0]  ck_t_idx;
  wire        ck_eng_start, ck_eng_rd;
  wire [31:0] ck_eng_sector;
  wire        ck_we;
  wire [7:0]  ck_byte;
  reg  [4:0]  vt_n_r;  // latched entry count for the checker run

`ifdef SDFAT_CHECK
  sd_fat_check u_ck (
      .clk             (clk),
      .rst_n           (sys_rst_n),
      .start           (ck_start),
      .busy            (ck_busy),
      .done            (ck_done),
      .err             (ck_err),
      .fs_is_fat32     (cp_is32),
      .cluster_size    (cp_cs),
      .fat0_sector     (cp_fat0),
      .data_base_sector(cp_dbase),
      .total_sectors   (cp_total),
      .n_entries       (vt_n_r),
      .t_idx           (ck_t_idx),
      .t_name          (vt_name[ck_t_idx]),
      .t_cluster       (vt_cluster[ck_t_idx]),
      .t_size          (vt_size[ck_t_idx]),
      .t_isdir         (vt_isdir[ck_t_idx]),
      .eng_start       (ck_eng_start),
      .eng_rd          (ck_eng_rd),
      .eng_sector      (ck_eng_sector),
      .eng_done        (wr_done),
      .eng_err         (wr_err),
      .eng_rx_we       (eng_rx_we),
      .eng_rx_addr     (eng_rx_addr),
      .eng_rx_data     (eng_rx_data),
      .ck_we           (ck_we),
      .ck_byte         (ck_byte)
  );
`else
  assign ck_busy = 1'b0;
  assign ck_done = 1'b0;
  assign ck_err = 1'b1;
  assign ck_t_idx = 4'd0;
  assign ck_eng_start = 1'b0;
  assign ck_eng_rd = 1'b0;
  assign ck_eng_sector = 32'd0;
  assign ck_we = 1'b0;
  assign ck_byte = 8'h00;
`endif

`ifdef SDFAT_FREESCAN
  sd_fat_freescan u_fsn (
      .clk             (clk),
      .rst_n           (sys_rst_n),
      .start           (fsn_start),
      .busy            (fsn_busy),
      .done            (fsn_done),
      .err             (fsn_err),
      .sec_tick        (fsn_tick),
      .fs_is_fat32     (cp_is32),
      .cluster_size    (cp_cs),
      .fat0_sector     (cp_fat0),
      .sectors_per_fat (cp_spf),
      .data_base_sector(cp_dbase),
      .total_sectors   (cp_total),
      .eng_start       (fsn_eng_start),
      .eng_rd          (fsn_eng_rd),
      .eng_sector      (fsn_eng_sector),
      .eng_done        (wr_done),
      .eng_err         (wr_err),
      .eng_rx_we       (eng_rx_we),
      .eng_rx_addr     (eng_rx_addr),
      .eng_rx_data     (eng_rx_data),
      .free_clusters   (fsn_free_clusters),
      .free_mb         (fsn_free_mb)
  );
`else
  // stripped: LIST prints the CARD/VOL sizes and reports FREE N/A
  assign fsn_busy = 1'b0;
  assign fsn_done = 1'b0;
  assign fsn_err = 1'b1;
  assign fsn_tick = 1'b0;
  assign fsn_eng_start = 1'b0;
  assign fsn_eng_rd = 1'b0;
  assign fsn_eng_sector = 32'd0;
  assign fsn_free_clusters = 32'd0;
  assign fsn_free_mb = 32'd0;
`endif

`ifdef SDFAT_REWRITE
  sd_fat_rewrite u_fx (
      .clk              (clk),
      .rst_n            (sys_rst_n),
      .start            (fx_start),
      .realloc          (fx_realloc),
      .busy             (fx_busy),
      .done             (fx_done),
      .err              (fx_err),
      .err_kind         (fx_err_kind),
      .fs_is_fat32      (cp_is32),
      .cluster_size     (cp_cs),
      .fat0_sector      (cp_fat0),
      .sectors_per_fat  (cp_spf),
      .num_fats         (cp_nfat),
      .data_base_sector (cp_dbase),
      .total_sectors    (cp_total),
      .root_start       (cp_fat0 + cp_spf * {24'b0, cp_nfat}),
      .root_secs        ((cp_dbase + {24'b0, cp_cs} * 32'd2) -
                         (cp_fat0 + cp_spf * {24'b0, cp_nfat})),
      .root_cluster     (cp_rootclus),
      .create           (fx_create),
      .name83           ((mode == M_IOW) ? FILE3_83 : FILE2_83),
      .fdate            (FDATE),
      .dir_sector       (cp_dir_sec),
      .dir_index        (cp_dir_idx),
      .old_first_cluster(cp_old_cluster),
      .new_size         ((mode == M_IOW) ? IO_BYTES : {{(31-BUF_AW){1'b0}}, copy_len}),
      .new_first_cluster(fx_new_first),
      .eng_start        (fx_eng_start),
      .eng_rd           (fx_eng_rd),
      .eng_sector       (fx_eng_sector),
      .eng_busy         (wr_busy),
      .eng_done         (wr_done),
      .eng_err          (wr_err),
      .eng_rx_we        (eng_rx_we),
      .eng_rx_addr      (eng_rx_addr),
      .eng_rx_data      (eng_rx_data),
      .eng_tx_addr      (wr_rd_addr),
      .eng_tx_data      (fx_tx_data)
  );
`else
  assign fx_busy = 1'b0;
  assign fx_done = 1'b0;
  assign fx_err = 1'b1;
  assign fx_err_kind = 2'd0;
  assign fx_new_first = 32'd0;
  assign fx_eng_start = 1'b0;
  assign fx_eng_rd = 1'b0;
  assign fx_eng_sector = 32'd0;
  assign fx_tx_data = 8'h00;
`endif

  /*******************************************************************************
   ** SD pin muxes - the ONLY tristate drivers, at the pads (repo rule).        **
   **                                                                            **
   ** EVERY pad must use the single-ternary form  oe ? val : 1'bz  - it is the  **
   ** only tristate idiom yosys maps to a real IOBUF. The previous DAT1-3       **
   ** expression (park-driven-1 with 1'bz in the INNER ternary branch) was      **
   ** silently collapsed to an always-driving OBUF: the FPGA fought the card    **
   ** on DAT1-3 through every 4-bit read data phase and all 4-bit reads failed  **
   ** on silicon while simulating perfectly (z-semantics in sim, none in the    **
   ** netlist). Proven from the synthesis netlist 12-JUL-2026: sd_dat1-3 came  **
   ** out as direction "output"/OBUF, sd_cmd/sd_dat0 (this form) as inout.     **
   ** The pads idle released; the slot's external 10K pull-ups (R53-R57) hold  **
   ** the lines high, incl. DAT3 at CMD0 (SD-native mode select). The pad oe   **
   ** wires below also feed the simulation contention monitors.                 **
   *******************************************************************************/
  assign sd_clk = phase_write ? wr_sdclk : rd_sdclk;
  wire cmd_oe = phase_write ? wr_cmd_oe : rd_cmd_oe;
  wire cmd_o  = phase_write ? wr_cmd_o : rd_cmd_o;
  wire s_dat0_pad_oe = phase_write & wr_dat0_oe;
  wire s_dat1_pad_oe = phase_write & wr_dat1_oe;
  wire s_dat2_pad_oe = phase_write & wr_dat2_oe;
  wire s_dat3_pad_oe = phase_write & wr_dat3_oe;
  assign sd_cmd  = cmd_oe ? cmd_o : 1'bz;
  assign sd_dat0 = s_dat0_pad_oe ? wr_dat0_o : 1'bz;
  assign sd_dat1 = s_dat1_pad_oe ? wr_dat1_o : 1'bz;
  assign sd_dat2 = s_dat2_pad_oe ? wr_dat2_o : 1'bz;
  assign sd_dat3 = s_dat3_pad_oe ? wr_dat3_o : 1'bz;

  /*******************************************************************************
   ** LIST mode: pack "      size  DD-MMM-YYYY  NAME" lines into the buffer     **
   *******************************************************************************/
  function [31:0] pow10(input [3:0] i);
    case (i)
      4'd0: pow10 = 32'd1;
      4'd1: pow10 = 32'd10;
      4'd2: pow10 = 32'd100;
      4'd3: pow10 = 32'd1000;
      4'd4: pow10 = 32'd10000;
      4'd5: pow10 = 32'd100000;
      4'd6: pow10 = 32'd1000000;
      4'd7: pow10 = 32'd10000000;
      4'd8: pow10 = 32'd100000000;
      default: pow10 = 32'd1000000000;
    endcase
  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 [23:0] month3(input [3:0] m);
    case (m)
      4'd1:  month3 = "JAN";
      4'd2:  month3 = "FEB";
      4'd3:  month3 = "MAR";
      4'd4:  month3 = "APR";
      4'd5:  month3 = "MAY";
      4'd6:  month3 = "JUN";
      4'd7:  month3 = "JUL";
      4'd8:  month3 = "AUG";
      4'd9:  month3 = "SEP";
      4'd10: month3 = "OCT";
      4'd11: month3 = "NOV";
      4'd12: month3 = "DEC";
      default: month3 = "???";
    endcase
  endfunction

  localparam S_DIRPAD = "     <DIR>";  // 10 chars, right-aligned like the sizes

  localparam PK_IDLE   = 4'd0;
  localparam PK_DEC    = 4'd1;  // decimal emit engine (size / day / year)
  localparam PK_DIRSTR = 4'd2;
  localparam PK_SPA    = 4'd3;  // two spaces after the size column
  localparam PK_DASH1  = 4'd4;
  localparam PK_MON    = 4'd5;
  localparam PK_DASH2  = 4'd6;
  localparam PK_SPB    = 4'd7;  // two spaces after the date column
  localparam PK_WR     = 4'd8;  // the name
  localparam PK_CR     = 4'd9;
  localparam PK_LF     = 4'd10;
  localparam PK_CL     = 4'd11;  // 8 hex digits of the first cluster
  localparam PK_SPC    = 4'd12;  // two spaces after the cluster column
  localparam PK_SPD    = 4'd13;  // two spaces after the absolute-sector column

  reg [3:0]      pk_state, pk_ret;
  reg [52*8-1:0] pk_name;
  reg [7:0]      pk_len, pk_i;
  reg [15:0]     pk_date;
  reg [31:0]     pk_cluster;
  reg [31:0]     pk_abs;     // first absolute sector of this entry
  reg            pk_we;
  reg [7:0]      pk_byte;
  reg [31:0]     pk_dval;    // decimal engine value
  reg [3:0]      pk_dpow;    // current power-of-ten index
  reg [3:0]      pk_ddig;    // current digit
  reg            pk_dpad;    // 1 = zero-pad (day/year), 0 = space-pad (size)
  reg            pk_dstart;  // a nonzero digit has been emitted

  wire pk_print_num = pk_dpad || pk_dstart || (pk_ddig != 0) || (pk_dpow == 0);

  always @(posedge clk) begin
    if (!sd_rst_n) begin
      pk_state  <= PK_IDLE;
      pk_ret    <= PK_IDLE;
      pk_name   <= 0;
      pk_len    <= 0;
      pk_i      <= 0;
      pk_date   <= 0;
      pk_we     <= 1'b0;
      pk_byte   <= 8'h00;
      pk_dval   <= 0;
      pk_dpow   <= 0;
      pk_ddig   <= 0;
      pk_dpad   <= 1'b0;
      pk_dstart <= 1'b0;
    end else begin
      pk_we <= 1'b0;
      case (pk_state)
        PK_IDLE:
        if (mode_list && !list_done && dir_valid && dir_len != 0) begin
          pk_name    <= dir_name;
          pk_len     <= dir_len;
          pk_date    <= dir_date;
          pk_cluster <= dir_cluster;
          // first ABSOLUTE sector of the entry, straight from the geometry
          // the reader publishes: data_base_sector is already biased so that
          // cluster c starts at base + cluster_size*c. Printed so the
          // operator can feed it to command 9 (or compare it against what
          // the ND-120 storage stack thinks the file's address is) without
          // doing the arithmetic by hand. Clusters 0/1 do not exist (an
          // empty file records cluster 0), so those print 0.
          pk_abs     <= (dir_cluster < 32'd2) ? 32'd0
                        : (fs_dbase + {24'b0, fs_cs} * dir_cluster);
          pk_i       <= 0;
          if (dir_isdir) begin
            pk_state <= PK_DIRSTR;
          end else begin
            pk_dval   <= dir_size;
            pk_dpow   <= 4'd9;
            pk_ddig   <= 0;
            pk_dpad   <= 1'b0;
            pk_dstart <= 1'b0;
            pk_ret    <= PK_SPA;
            pk_state  <= PK_DEC;
          end
        end

        PK_DEC:  // one decimal number, MSD first, one char per emit cycle
        if (pk_dval >= pow10(pk_dpow)) begin
          pk_dval <= pk_dval - pow10(pk_dpow);
          pk_ddig <= pk_ddig + 4'd1;
        end else begin
          pk_byte   <= pk_print_num ? (8'h30 + {4'b0, pk_ddig}) : 8'h20;
          pk_we     <= 1'b1;
          pk_dstart <= pk_dstart | (pk_ddig != 0);
          pk_ddig   <= 0;
          if (pk_dpow == 0) pk_state <= pk_ret;
          else pk_dpow <= pk_dpow - 4'd1;
        end

        PK_DIRSTR: begin
          pk_byte <= S_DIRPAD[8*(9-pk_i)+:8];
          pk_we   <= 1'b1;
          if (pk_i == 8'd9) begin
            pk_i     <= 0;
            pk_state <= PK_SPA;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_SPA: begin
          pk_byte <= 8'h20;
          pk_we   <= 1'b1;
          if (pk_i == 8'd1) begin
            // load the day (2 digits, zero-padded)
            pk_i     <= 0;
            pk_dval  <= {27'b0, pk_date[4:0]};
            pk_dpow  <= 4'd1;
            pk_ddig  <= 0;
            pk_dpad  <= 1'b1;
            pk_ret   <= PK_DASH1;
            pk_state <= PK_DEC;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_DASH1: begin
          pk_byte  <= "-";
          pk_we    <= 1'b1;
          pk_i     <= 0;
          pk_state <= PK_MON;
        end

        PK_MON: begin
          pk_byte <= month3(pk_date[8:5]) >> (8 * (2 - pk_i));
          pk_we   <= 1'b1;
          if (pk_i == 8'd2) begin
            pk_i     <= 0;
            pk_state <= PK_DASH2;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_DASH2: begin
          pk_byte  <= "-";
          pk_we    <= 1'b1;
          // load the year (4 digits, zero-padded; FAT epoch 1980)
          pk_dval  <= 32'd1980 + {25'b0, pk_date[15:9]};
          pk_dpow  <= 4'd3;
          pk_ddig  <= 0;
          pk_dpad  <= 1'b1;
          pk_ret   <= PK_SPB;
          pk_state <= PK_DEC;
        end

        PK_SPB: begin
          pk_byte <= 8'h20;
          pk_we   <= 1'b1;
          if (pk_i == 8'd1) begin
            pk_i     <= 0;
            pk_state <= PK_CL;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_CL: begin  // first cluster, 8 hex digits
          pk_byte <= hexd(pk_cluster[31-4*pk_i[2:0]-:4]);
          pk_we   <= 1'b1;
          if (pk_i == 8'd7) begin
            pk_i     <= 0;
            pk_state <= PK_SPC;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_SPC: begin
          pk_byte <= 8'h20;
          pk_we   <= 1'b1;
          if (pk_i == 8'd1) begin
            // load the first absolute sector (10 digits, space-padded)
            pk_i      <= 0;
            pk_dval   <= pk_abs;
            pk_dpow   <= 4'd9;
            pk_ddig   <= 0;
            pk_dpad   <= 1'b0;
            pk_dstart <= 1'b0;
            pk_ret    <= PK_SPD;
            pk_state  <= PK_DEC;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_SPD: begin
          pk_byte <= 8'h20;
          pk_we   <= 1'b1;
          if (pk_i == 8'd1) begin
            pk_i     <= 0;
            pk_state <= PK_WR;
          end else pk_i <= pk_i + 8'd1;
        end

        PK_WR:
        if (pk_i < pk_len) begin
          pk_byte <= pk_name[8*pk_i+:8];
          pk_we   <= 1'b1;
          pk_i    <= pk_i + 8'd1;
        end else pk_state <= PK_CR;

        PK_CR: begin
          pk_byte  <= 8'h0D;
          pk_we    <= 1'b1;
          pk_state <= PK_LF;
        end

        PK_LF: begin
          pk_byte  <= 8'h0A;
          pk_we    <= 1'b1;
          pk_state <= PK_IDLE;
        end

        default: pk_state <= PK_IDLE;
      endcase
    end
  end

  /*******************************************************************************
   ** LIST disk-info line: "CARD c MB  VOL v MB  FREE f MB" appended to the    **
   ** buffer after the file listing. CARD = CMD9 CSD capacity (latched in      **
   ** cp_cap), VOL = total sectors / 2048, FREE = the free-space scanner's     **
   ** result (or N/A when the scan is stripped/failed). Decimal, no padding.   **
   *******************************************************************************/
  localparam S_IL_CARD = "CARD ";           // 5 chars
  localparam S_IL_VOL  = " MB  VOL ";       // 9 chars
  localparam S_IL_FREE = " MB  FREE ";      // 10 chars
  localparam S_IL_SPC1 = " MB  SPC ";       // 9 chars, follows a numeric FREE
  localparam S_IL_SPC2 = " SPC ";           // 5 chars, follows "N/A"
  localparam S_IL_DBAS = " DBASE ";         // 7 chars
  localparam S_IL_TAIL = "\015\012";        // 2 chars
  localparam S_IL_NA   = "N/A";             // 3 chars

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

  // MB values stay below 10^7 (10 TB) for every CSD we decode, so this
  // decimal engine is 24-bit / 8 digits (the 32-bit pow10 would waste LUTs)
  function [23:0] pow10i(input [2:0] i);
    case (i)
      3'd0: pow10i = 24'd1;
      3'd1: pow10i = 24'd10;
      3'd2: pow10i = 24'd100;
      3'd3: pow10i = 24'd1000;
      3'd4: pow10i = 24'd10000;
      3'd5: pow10i = 24'd100000;
      3'd6: pow10i = 24'd1000000;
      default: pow10i = 24'd10000000;
    endcase
  endfunction

  localparam IL_IDLE = 2'd0;
  localparam IL_STR  = 2'd1;  // current segment's characters
  localparam IL_DEC  = 2'd2;  // one decimal number, leading zeros skipped

  // Segment order (a segment is a label, optionally followed by a decimal
  // number). SPC and DBASE are the two numbers an operator needs to turn a
  // LIST first-cluster column into an absolute sector by hand:
  //   first sector = DBASE + SPC * first cluster
  // which is exactly what the listing's own sector column computes.
  //   0 "CARD "      capacity      -> 1
  //   1 " MB  VOL "  volume MB     -> 2
  //   2 " MB  FREE " free MB       -> 3   (or, with no scanner, -> 6)
  //   3 " MB  SPC "  sectors/clus  -> 5
  //   4 " SPC "      sectors/clus  -> 5   (the same after an "N/A" free)
  //   5 " DBASE "    data base sec -> 7
  //   6 "N/A"        no number     -> 4
  //   7 CR LF        no number     -> end
  reg [1:0]  il_state;
  reg [2:0]  il_seg;
  reg [3:0]  il_si;
  reg        il_we;
  reg [7:0]  il_byte;
  reg [23:0] il_dval;
  reg [2:0]  il_dpow;
  reg [3:0]  il_ddig;
  reg        il_dstart;
  reg        il_go;         // pulsed by the menu FSM
  reg        il_have_free;  // 0: the FREE column prints N/A

  wire il_busy = (il_state != IL_IDLE);

  reg [7:0] il_ch;
  always @(*) begin
    case (il_seg)
      3'd0: il_ch = ilchr({40'b0, S_IL_CARD}, 4'd5, il_si);
      3'd1: il_ch = ilchr({8'b0, S_IL_VOL}, 4'd9, il_si);
      3'd2: il_ch = ilchr(S_IL_FREE, 4'd10, il_si);
      3'd3: il_ch = ilchr({8'b0, S_IL_SPC1}, 4'd9, il_si);
      3'd4: il_ch = ilchr({40'b0, S_IL_SPC2}, 4'd5, il_si);
      3'd5: il_ch = ilchr({24'b0, S_IL_DBAS}, 4'd7, il_si);
      3'd6: il_ch = ilchr({56'b0, S_IL_NA}, 4'd3, il_si);
      default: il_ch = ilchr({64'b0, S_IL_TAIL}, 4'd2, il_si);
    endcase
  end

  // segment that follows a printed number
  function [2:0] il_nxt(input [2:0] s);
    case (s)
      3'd0: il_nxt = 3'd1;
      3'd1: il_nxt = 3'd2;
      3'd2: il_nxt = 3'd3;
      3'd3: il_nxt = 3'd5;
      3'd4: il_nxt = 3'd5;
      default: il_nxt = 3'd7;
    endcase
  endfunction

  always @(posedge clk) begin
    if (!sys_rst_n) begin
      il_state  <= IL_IDLE;
      il_seg    <= 0;
      il_si     <= 0;
      il_we     <= 1'b0;
      il_byte   <= 8'h00;
      il_dval   <= 0;
      il_dpow   <= 0;
      il_ddig   <= 0;
      il_dstart <= 1'b0;
    end else begin
      il_we <= 1'b0;
      case (il_state)
        IL_IDLE:
        if (il_go) begin
          il_seg   <= 0;
          il_si    <= 0;
          il_state <= IL_STR;
        end

        IL_STR:
        if (il_ch == 8'h00) begin
          il_dpow   <= 3'd7;
          il_ddig   <= 0;
          il_dstart <= 1'b0;
          case (il_seg)
            3'd0: begin
              il_dval  <= cp_cap[23:0];       // CARD capacity in MB
              il_state <= IL_DEC;
            end
            3'd1: begin
              il_dval  <= {3'd0, cp_total[31:11]};  // sectors * 512 / 1 MB
              il_state <= IL_DEC;
            end
            3'd2:
            if (il_have_free) begin
              il_dval  <= fsn_free_mb[23:0];
              il_state <= IL_DEC;
            end else begin
              il_seg <= 3'd6;                 // "N/A", then straight to SPC
              il_si  <= 0;
            end
            3'd3, 3'd4: begin
              il_dval  <= {16'd0, cp_cs};     // sectors per cluster
              il_state <= IL_DEC;
            end
            3'd5: begin
              il_dval  <= cp_dbase[23:0];     // biased data base sector
              il_state <= IL_DEC;
            end
            3'd6: begin                       // "N/A" carries no number
              il_seg <= 3'd4;
              il_si  <= 0;
            end
            default: il_state <= IL_IDLE;     // segment 7 = CR LF: line done
          endcase
        end else begin
          il_we   <= 1'b1;
          il_byte <= il_ch;
          il_si   <= il_si + 4'd1;
        end

        IL_DEC:  // MSD first; leading zeros skipped entirely (no padding)
        if (il_dval >= pow10i(il_dpow)) begin
          il_dval <= il_dval - pow10i(il_dpow);
          il_ddig <= il_ddig + 4'd1;
        end else begin
          if (il_dstart || il_ddig != 0 || il_dpow == 0) begin
            il_we   <= 1'b1;
            il_byte <= 8'h30 + {4'b0, il_ddig};
          end
          il_dstart <= il_dstart | (il_ddig != 0);
          il_ddig   <= 0;
          if (il_dpow == 0) begin
            il_seg   <= il_nxt(il_seg);  // next segment header / tail
            il_si    <= 0;
            il_state <= IL_STR;
          end else il_dpow <= il_dpow - 3'd1;
        end

        default: il_state <= IL_IDLE;
      endcase
    end
  end

  /*******************************************************************************
   ** IO speed report: KB/s = SPEED_NUM / io_cycles (40/32-bit restoring       **
   ** divider), formatted as "WRITE NNNNN KB/S" into the text buffer           **
   *******************************************************************************/
`ifdef SDFAT_SPEED
  localparam SP_IDLE = 3'd0;
  localparam SP_DIV  = 3'd1;
  localparam SP_LBL  = 3'd2;
  localparam SP_DIG  = 3'd3;
  localparam SP_TAIL = 3'd4;

  reg [2:0]  spf_state;
  reg        spf_we;
  reg [7:0]  spf_byte;
  reg        spf_rd_r;
  reg [3:0]  spf_i;
  reg [39:0] div_q;
  reg [40:0] div_r;
  reg [5:0]  div_i;
  reg [31:0] div_d;
  reg [16:0] sp_val;
  reg [3:0]  sp_pow, sp_dig;

  wire spf_busy = (spf_state != SP_IDLE);

  wire [40:0] div_shift = {div_r[39:0], SPEED_NUM[6'd39-div_i]};
  wire        div_ge    = (div_shift >= {9'b0, div_d});
  wire [39:0] div_qn    = {div_q[38:0], div_ge};

  function [7:0] spstr(input rd, input tail, input [3:0] i);
    begin
      if (tail) begin
        case (i)  // " KB/S" CR LF
          4'd0: spstr = " ";
          4'd1: spstr = "K";
          4'd2: spstr = "B";
          4'd3: spstr = "/";
          4'd4: spstr = "S";
          4'd5: spstr = 8'h0D;
          4'd6: spstr = 8'h0A;
          default: spstr = 8'h00;
        endcase
      end else if (rd) begin
        case (i)
          4'd0: spstr = "R";
          4'd1: spstr = "E";
          4'd2: spstr = "A";
          4'd3: spstr = "D";
          4'd4: spstr = " ";
          default: spstr = 8'h00;
        endcase
      end else begin
        case (i)
          4'd0: spstr = "W";
          4'd1: spstr = "R";
          4'd2: spstr = "I";
          4'd3: spstr = "T";
          4'd4: spstr = "E";
          4'd5: spstr = " ";
          default: spstr = 8'h00;
        endcase
      end
    end
  endfunction

  always @(posedge clk) begin
    if (!sys_rst_n) begin
      spf_state <= SP_IDLE;
      spf_we    <= 1'b0;
      spf_byte  <= 0;
      spf_rd_r  <= 1'b0;
      spf_i     <= 0;
      div_q     <= 0;
      div_r     <= 0;
      div_i     <= 0;
      div_d     <= 1;
      sp_val    <= 0;
      sp_pow    <= 0;
      sp_dig    <= 0;
    end else begin
      spf_we <= 1'b0;
      case (spf_state)
        SP_IDLE:
        if (spf_go) begin
          spf_rd_r  <= spf_read;
          div_d     <= (io_cycles == 0) ? 32'd1 : io_cycles;
          div_q     <= 0;
          div_r     <= 0;
          div_i     <= 0;
          spf_state <= SP_DIV;
        end

        SP_DIV: begin
          div_r <= div_ge ? (div_shift - {9'b0, div_d}) : div_shift;
          div_q <= div_qn;
          if (div_i == 6'd39) begin
            sp_val    <= (div_qn > 40'd99999) ? 17'd99999 : div_qn[16:0];
            spf_i     <= 0;
            spf_state <= SP_LBL;
          end else div_i <= div_i + 6'd1;
        end

        SP_LBL:
        if (spstr(spf_rd_r, 1'b0, spf_i) == 8'h00) begin
          sp_pow    <= 4'd4;
          sp_dig    <= 0;
          spf_state <= SP_DIG;
        end else begin
          spf_we   <= 1'b1;
          spf_byte <= spstr(spf_rd_r, 1'b0, spf_i);
          spf_i    <= spf_i + 4'd1;
        end

        SP_DIG:  // 5 decimal digits, zero padded
        if ({15'b0, sp_val} >= pow10(sp_pow)) begin
          sp_val <= sp_val - pow10(sp_pow);  // sp_pow <= 4: fits in 17 bits
          sp_dig <= sp_dig + 4'd1;
        end else begin
          spf_we   <= 1'b1;
          spf_byte <= 8'h30 + {4'b0, sp_dig};
          sp_dig   <= 0;
          if (sp_pow == 0) begin
            spf_i     <= 0;
            spf_state <= SP_TAIL;
          end else sp_pow <= sp_pow - 4'd1;
        end

        SP_TAIL:
        if (spstr(1'b0, 1'b1, spf_i) == 8'h00) begin
          spf_state <= SP_IDLE;
        end else begin
          spf_we   <= 1'b1;
          spf_byte <= spstr(1'b0, 1'b1, spf_i);
          spf_i    <= spf_i + 4'd1;
        end

        default: spf_state <= SP_IDLE;
      endcase
    end
  end
`else
  wire       spf_busy = 1'b0;
  wire       spf_we = 1'b0;
  wire [7:0] spf_byte = 8'h00;
`endif

  /*******************************************************************************
   ** 64 KB byte buffer (BRAM): file bytes (DUMP/COPY) or list lines (LIST) in, **
   ** hex_dumper / buf_text_printer / sd_writer out                             **
   *******************************************************************************/
  // SDFAT_BUF_BLOCKRAM: Vivado maps this array to DISTRIBUTED RAM by default
  // (measured 20-AUG-2026 on xc7a100t: 24,576 RAMD64E cells against 19,000
  // sites, so place_design fails outright) even though the write and the read
  // are both clocked. The attribute forces block RAM. It is gated so that
  // builds which do not define it - Tang Nano 20K (Gowin) and Basys3 - compile
  // byte-identically to before.
`ifdef SDFAT_BUF_BLOCKRAM
  (* ram_style = "block" *)
`endif
  reg [7:0] file_buf[0:(1 << BUF_AW)-1];

  reg [BUF_AW:0] wptr;
  reg            trunc;

  // COPY phase 1 (locating TEST.TXT) must not disturb the buffered file
  wire       wr_gate = (mode == M_COPY) && copy_phase;
  wire [7:0] wr_byte = (mode == M_CHK) ? ck_byte
                     : io_mode ? spf_byte
                     : mode_list ? (il_we ? il_byte : pk_byte) : f_outbyte;
  wire       wr_req  = !wr_gate && ((mode == M_CHK) ? ck_we
                                    : io_mode ? spf_we
                                    : mode_list ? (pk_we | il_we) : f_outen);
  wire       buf_we  = wr_req && !wptr[BUF_AW];

  // BLOCK (8) / SECTOR (9): sectors land in the buffer straight off the
  // engine's read port as they arrive, at most 4 sectors (2048 bytes) per
  // command whatever the file size - the buffer is only a landing pad here,
  // never a copy of the file. seccnt is the sector index inside the block.
  wire              rx_buf_we   = phase_write && dump_mode && eng_rx_we;
  wire [BUF_AW-1:0] rx_buf_addr = {{(BUF_AW-11){1'b0}}, seccnt[1:0], eng_rx_addr};

  // ONE write port, explicitly muxed. Written as two if/else branches with
  // two different address expressions - which is what this was - Vivado
  // counts them as two separate write ports, needs a third for the read, and
  // gives up on block RAM ("Infeasible attribute ram_style", measured
  // 20-AUG-2026: 6144 RAM64M primitives instead of 16 RAMB36, which no longer
  // fits any part here). The priority is unchanged: rx_buf_we wins.
  wire [BUF_AW-1:0] buf_waddr = rx_buf_we ? rx_buf_addr : wptr[BUF_AW-1:0];
  wire [7:0]        buf_wdata = rx_buf_we ? eng_rx_data : wr_byte;
  wire              buf_wen   = rx_buf_we | buf_we;

  always @(posedge clk) begin
    if (buf_wen) file_buf[buf_waddr] <= buf_wdata;
  end

  // the buffer pointer clears with the reader reset EXCEPT while the
  // reader is merely parked (phase_write): the CHECK report and the COPY
  // write phase need the buffer to stay alive through that
  always @(posedge clk) begin
    if (!sys_rst_n || (!sd_rst_n && !phase_write)) begin
      wptr  <= 0;
      trunc <= 0;
    end else begin
      if (buf_we) wptr <= wptr + 1;
      if (wr_req && wptr[BUF_AW]) trunc <= 1;
    end
  end

  wire [BUF_AW:0]   widx = {seccnt[BUF_AW-9:0], 9'b0} + {8'b0, wr_rd_addr};
  // speed-test pattern: distinct per sector and offset, cheap to generate
  wire [7:0] io_pat = seccnt[7:0] ^ wr_rd_addr[7:0] ^ {seccnt[12:9], 4'hA};
  wire [BUF_AW-1:0] dump_addr, text_addr;
  reg  [7:0]        buf_rdata;
  wire              tp_busy;
  // the text printer wins even while the reader is parked (phase_write):
  // the CHECK/SPEED/LIST reports print with the card still parked so the
  // reader is not restarted just to be reset mid-command at the menu
  // in the BLOCK/SECTOR dumps the reader stays parked (phase_write) while
  // hex_dumper prints, so the dumper - not the write-data path - owns the
  // read port there
  wire [BUF_AW-1:0] rd_addr = tp_busy ? text_addr
                            : (phase_write && !dump_mode) ? widx[BUF_AW-1:0]
                            : dump_addr;
  always @(posedge clk) buf_rdata <= file_buf[rd_addr];

  // COPY: buffer bytes, zero-filled past the file end (real tapes had zero
  // trailers). WRBLK1: counter pattern - 1024 big-endian 16-bit words,
  // word[w] = w, so every sector of the block is distinct and a misplaced
  // write cannot pass the dump check.
  wire [10:0] blk_off  = widx[10:0];              // byte offset inside the block
  wire [9:0]  blk_word = blk_off[10:1];           // word index 0..1023
  wire [7:0]  blk_pat  = blk_off[0] ? blk_word[7:0] : {6'b0, blk_word[9:8]};
  assign wr_data_in = (mode == M_BLK) ? blk_pat
                    : (mode == M_IOW) ? io_pat
                    : (widx < copy_len) ? buf_rdata : 8'h00;

  /*******************************************************************************
   ** UART: rx = menu keys, tx shared by the three printers                     **
   *******************************************************************************/
  wire [7:0] rx_data;
  wire       rx_valid;

  uart_rx #(
      .DELAY_FRAMES(DELAY_FRAMES)
  ) u_rx (
      .clk(clk),
      .rst_n(sys_rst_n),
      .rxd(uart_rxp),
      .rx_data(rx_data),
      .rx_valid(rx_valid)
  );

  reg        sp_start;
  reg  [5:0] sp_msg;
  wire       sp_busy;
  wire [7:0] sp_tx_data;
  wire       sp_tx_valid;

  status_printer u_sp (
      .clk(clk),
      .rst_n(sys_rst_n),
      .start(sp_start),
      .msg(sp_msg),
      .busy(sp_busy),
      .tx_data(sp_tx_data),
      .tx_valid(sp_tx_valid),
      .tx_busy(tx_busy)
  );

  reg        hd_start;
  wire       hd_busy;
  wire [7:0] hd_tx_data;
  wire       hd_tx_valid;

  // BLOCK/SECTOR dump exactly the sectors just read (nsec * 512 bytes);
  // every other command dumps what the buffer collected
  wire [BUF_AW:0] hd_length = dump_mode ? {{(BUF_AW-11){1'b0}}, nsec[2:0], 9'b0}
                                        : wptr;

  hex_dumper #(
      .ADDR_W(BUF_AW)
  ) u_dump (
      .clk(clk),
      .rst_n(sys_rst_n),
      .start(hd_start),
      .length(hd_length),
      .busy(hd_busy),
      .mem_addr(dump_addr),
      .mem_data(buf_rdata),
      .tx_data(hd_tx_data),
      .tx_valid(hd_tx_valid),
      .tx_busy(tx_busy)
  );

  reg        tp_start;
  wire [7:0] tp_tx_data;
  wire       tp_tx_valid;

  buf_text_printer #(
      .ADDR_W(BUF_AW)
  ) u_text (
      .clk(clk),
      .rst_n(sys_rst_n),
      .start(tp_start),
      .length(wptr),
      .busy(tp_busy),
      .mem_addr(text_addr),
      .mem_data(buf_rdata),
      .tx_data(tp_tx_data),
      .tx_valid(tp_tx_valid),
      .tx_busy(tx_busy)
  );

  // the FSM serializes the printers; the pulse selects whose byte is latched
  // menu-key echo (owner request 12-JUL-2026): every ACCEPTED key is
  // echoed back as <key> CR LF before its command output starts, so a
  // captured console log has copy-pasteable command boundaries
  reg        ec_valid;
  reg  [7:0] ec_byte;

`ifdef SDFAT_EXT_TEST
  // the external test owns the console while it runs - nothing else prints
  // during a memory test, so top priority is safe and keeps its lines intact
  wire [7:0] tx_data = ext_tx_valid ? ext_tx_data
                     : hd_tx_valid ? hd_tx_data : tp_tx_valid ? tp_tx_data
                     : ec_valid ? ec_byte : sp_tx_data;
  wire       tx_valid = ext_tx_valid | hd_tx_valid | tp_tx_valid | ec_valid | sp_tx_valid;
  assign     ext_tx_busy = tx_busy;
`else
  wire [7:0] tx_data = hd_tx_valid ? hd_tx_data : tp_tx_valid ? tp_tx_data
                     : ec_valid ? ec_byte : sp_tx_data;
  wire       tx_valid = hd_tx_valid | tp_tx_valid | ec_valid | sp_tx_valid;
`endif
  wire       tx_busy;

  uart_tx #(
      .DELAY_FRAMES(DELAY_FRAMES)
  ) u_tx (
      .clk(clk),
      .rst_n(sys_rst_n),
      .tx_data(tx_data),
      .tx_valid(tx_valid),
      .tx_busy(tx_busy),
      .txd(uart_txp)
  );

  /*******************************************************************************
   ** Menu FSM                                                                  **
   *******************************************************************************/
  // status_printer message codes (must match status_printer.v)
  localparam MSG_BANNER = 6'd0;
  localparam MSG_FS_UNKNOWN = 6'd6;
  localparam MSG_FILE_FOUND = 6'd7;
  localparam MSG_FILE_NOTFND = 6'd8;
  localparam MSG_ERR_CARD_TO = 6'd9;
  localparam MSG_ERR_SCAN_TO = 6'd10;
  localparam MSG_ERR_TRUNC = 6'd11;
  localparam MSG_MENU = 6'd12;
  localparam MSG_NOTIMPL = 6'd13;
  localparam MSG_SD_STATUS0 = 6'd14;  // + sd_status
  localparam MSG_COPYING = 6'd18;
  localparam MSG_COPY_DONE = 6'd19;
  localparam MSG_ERR_NOTGT = 6'd20;
  localparam MSG_ERR_SMALL = 6'd21;
  localparam MSG_ERR_WRITE = 6'd22;
  localparam MSG_BLK_WRITING = 6'd23;
  localparam MSG_BLK_DONE = 6'd24;
  localparam MSG_ERR_RANGE = 6'd25;
  localparam MSG_HELP1 = 6'd26;  // ..MSG_HELP7 = 32, printed in sequence
  localparam MSG_HELP7 = 6'd32;
  localparam MSG_ERR_IOSZ = 6'd33;
  localparam MSG_IOW_RUN = 6'd34;
  localparam MSG_IOR_RUN = 6'd35;
  localparam MSG_SCANNING = 6'd36;
  localparam MSG_ERR_READ = 6'd37;
  localparam MSG_ERR_FATRD = 6'd38;
  localparam MSG_ERR_FATCOR = 6'd39;
  localparam MSG_MENU2 = 6'd40;
  localparam MSG_ASK_BLK = 6'd41;
  localparam MSG_ASK_SEC = 6'd42;
  localparam MSG_ASK_NAME = 6'd43;
  localparam MSG_ERR_NUM = 6'd44;
  localparam MSG_AT_SEC = 6'd45;
  localparam MSG_NAME_SET = 6'd46;
  localparam MSG_HELP8 = 6'd47;   // ..MSG_HELP13 = 52
  localparam MSG_HELP13 = 6'd52;
  localparam MSG_ASK_STA = 6'd53;
  localparam MSG_ASK_CNT = 6'd54;
  localparam MSG_R_RUN = 6'd55;
  localparam MSG_R_AT = 6'd56;
  localparam MSG_R_BLOCKS = 6'd57;
  localparam MSG_R_SECS = 6'd58;
  localparam MSG_R_CHK = 6'd59;
  localparam MSG_R_CYC = 6'd60;
  localparam MSG_R_PASS = 6'd61;
  localparam MSG_R_FAIL = 6'd62;

  localparam ST_BANNER   = 6'd0;
  localparam ST_MENU     = 6'd1;   // print the SD status line, then
  localparam ST_MENU2    = 6'd15;  // the menu itself, then
  localparam ST_MENU3    = 6'd31;  // its second line (64-char message limit)
  localparam ST_KEY      = 6'd2;
  localparam ST_CARD     = 6'd3;
  localparam ST_FS       = 6'd4;
  localparam ST_FILE     = 6'd5;
  localparam ST_STREAM   = 6'd6;
  localparam ST_DUMP     = 6'd7;
  localparam ST_DUMP_W   = 6'd8;
  localparam ST_LSCAN    = 6'd9;
  localparam ST_LPRINT   = 6'd10;
  localparam ST_LPRINT_W = 6'd11;
  localparam ST_CMD_END  = 6'd12;
  localparam ST_PRINT    = 6'd13;  // pulse sp_start, then
  localparam ST_PRINT_W  = 6'd14;  // wait for the message to finish
  localparam ST_C_FIND   = 6'd16;  // COPY: wait for the TEST.TXT scan
  localparam ST_C_WRITE  = 6'd17;  // COPY/WRBLK1/BLOCK/SECTOR: kick a sector
  localparam ST_C_NEXT   = 6'd18;  // and wait for that sector to finish
  localparam ST_HELP     = 6'd19;  // print MSG_HELP1..12 in sequence
  localparam ST_C_FIX    = 6'd20;  // COPY: kick sd_fat_rewrite
  localparam ST_C_FIX_W  = 6'd21;  // COPY: wait for it, then write the data
  localparam ST_CHK      = 6'd22;  // CHECK: park the reader, kick the checker
  localparam ST_CHK_W    = 6'd23;  // CHECK: wait, then print the report
  localparam ST_SPEED    = 6'd24;  // IO speed: kick the KB/s formatter
  localparam ST_SPEED_W  = 6'd25;  // IO speed: wait, then print the line
  localparam ST_FREE     = 6'd26;  // LIST: park the reader, kick the free scan
  localparam ST_FREE_W   = 6'd27;  // LIST: wait for the FAT scan
  localparam ST_INFO     = 6'd28;  // LIST: kick the disk-info line formatter
  localparam ST_INFO_W   = 6'd29;  // LIST: wait, then print the whole buffer
  localparam ST_ECHO     = 6'd30;  // echo the accepted menu key (+ CR-LF)
  localparam ST_NUM      = 6'd32;  // BLOCK/SECTOR: read a decimal number in
  localparam ST_NAME     = 6'd33;  // N: read a file name in
  localparam ST_HEXOUT   = 6'd34;  // print 8 hex digits + CR LF (hx_val)
  localparam ST_SWAIT    = 6'd35;  // SECTOR: let the reader's run end cleanly
  localparam ST_R_NEXT   = 6'd36;  // RANGE: one block done - advance or report
  localparam ST_R_SUM    = 6'd37;  // RANGE: print the summary, field by field
  localparam ST_R_ERR    = 6'd38;  // RANGE: a read failed - print the sector
  localparam ST_R_ERR2   = 6'd39;  // RANGE: ...and the block, then summarise
`ifdef SDFAT_EXT_TEST
  localparam ST_EXT_W    = 6'd40;  // external test running - wait for ext_busy
  reg ext_kicked;
`endif

  // persistent SD status shown in the menu (survives between commands)
  localparam SD_NOTCHK = 2'd0;
  localparam SD_NOCARD = 2'd1;
  localparam SD_ERROR  = 2'd2;
  localparam SD_OK     = 2'd3;

  reg [5:0]  state;
  reg [5:0]  next_after_print;
  reg [7:0]  echo_ch;    // the accepted key being echoed
  reg [5:0]  echo_next;  // state after the echo finishes
  reg [1:0]  ec_i;       // echo byte index: key, CR, LF
  reg        ec_crlf;    // 0 = echo the key alone (console number entry)
  reg        ec_gap;     // one-cycle gap for tx_busy to assert

  // ---- console number entry (BLOCK / SECTOR) and name entry (N) ----------
  reg [31:0] num_val;    // decimal accumulator
  reg        num_dig;    // at least one digit seen (an empty entry is an error)
  reg [23:0] blk_no;     // 1KW block index inside the file
  reg [31:0] sec_no;     // absolute SD sector number
  reg [31:0] hx_val;     // value printed by ST_HEXOUT
  reg [3:0]  hx_i;       // 0..7 digits, 8 = CR, 9 = LF
  reg [5:0]  hx_next;    // state after the hex value has been printed

  // num_val * 10, built from shifts (no multiplier for a console keypress).
  // The guard below stops the accumulator at 10 digits: 4294967295 is the
  // largest sector number a 32-bit SD address can carry (2 TB), so anything
  // beyond that is a typo, not an address.
  wire [31:0] num_x10 = {num_val[28:0], 3'b0} + {num_val[30:0], 1'b0};
  wire        num_fits = (num_val < 32'd400_000_000);

  // ---- multi-block range read (menu R) ------------------------------------
  // Segment handling in SINTRAN is the first thing that reads MANY
  // consecutive blocks in one go, and everything validated before this
  // command only ever read ONE block. So this reads a run of blocks the
  // same way command 8 reads one - sector by sector, nothing buffered - and
  // keeps only a running checksum and a block count. The console never sees
  // the data: a 1000-block hex dump at 9600 baud would take about six hours.
  reg [23:0] rng_start;  // first block of the run
  reg [23:0] rng_cnt;    // number of blocks requested
  reg [23:0] rng_blk;    // blocks completed so far
  reg [15:0] rng_sum;    // sum of every 16-bit big-endian word read
  reg [7:0]  rng_hi;     // high byte of the word being assembled
  reg        num_second; // number entry: 0 = start block, 1 = block count
  reg [2:0]  r_i;        // summary field index

  // sum of the two block indices: (start + count) * 2048 is the number of
  // bytes the file must have. 25-bit sum shifted by 11 = 36 bits, so the
  // arithmetic is done in 40 - in 32 it would wrap and pass a bad range.
  wire [39:0] rng_need = ({15'b0, rng_start} + {15'b0, rng_cnt}) << 11;

  // bytes a file must contain for block blk_no to lie entirely inside it:
  // (blk_no + 1) * 2048, computed in 40 bits because a 24-bit block index
  // times 2048 is 35 bits - the same expression in 32 bits would wrap and
  // let an out-of-range block through
  wire [39:0] blk_need = ({16'b0, blk_no} + 40'd1) << 11;
  reg        io_run;      // IO speed test: cycle counter enable
  reg [31:0] io_cycles;
  reg        spf_go, spf_read;
  reg [1:0]  sd_status;
  reg [31:0] wd;  // watchdog for the waiting states
  reg        err_flag, done_flag;

  assign led = ~{err_flag,               // led[5] error (last command)
                 trunc,                  // led[4] truncated
                 done_flag,              // led[3] last command completed
                 file_found,             // led[2] file found
                 fs_type[1],             // led[1] FS mounted
                 sys_rst_n};             // led[0] alive

  always @(posedge clk) begin
    if (!sys_rst_n) begin
      state            <= ST_BANNER;
      next_after_print <= ST_BANNER;
      cmd_running      <= 1'b0;
`ifdef SDFAT_EXT_TEST
      ext_start        <= 1'b0;
      ext_id           <= 4'd0;
      ext_kicked       <= 1'b0;
`endif
      rdrst_cnt        <= 0;
      phase_write      <= 1'b0;
      mode             <= M_DUMP;
      copy_phase       <= 1'b0;
      copy_len         <= 0;
      tgt_sector       <= 0;
      nsec             <= 0;
      seccnt           <= 0;
      wr_burst         <= 9'd1;
      wr_start         <= 1'b0;
      fx_start         <= 1'b0;
      ck_start         <= 1'b0;
      fsn_start        <= 1'b0;
      il_go            <= 1'b0;
      il_have_free     <= 1'b0;
      list_done        <= 1'b0;
      cp_cap           <= 0;
      vt_n_r           <= 0;
      spf_go           <= 1'b0;
      spf_read         <= 1'b0;
      io_run           <= 1'b0;
      io_cycles        <= 0;
      fx_realloc       <= 1'b0;
      cp_is32          <= 1'b0;
      cp_cs            <= 0;
      cp_nfat          <= 0;
      cp_fat0          <= 0;
      cp_spf           <= 0;
      cp_dbase         <= 0;
      cp_total         <= 0;
      cp_rootclus      <= 0;
      cp_dir_sec       <= 0;
      cp_dir_idx       <= 0;
      cp_old_cluster   <= 0;
      sp_start         <= 1'b0;
      sp_msg           <= MSG_BANNER;
      hd_start         <= 1'b0;
      tp_start         <= 1'b0;
      wd               <= 0;
      err_flag         <= 1'b0;
      done_flag        <= 1'b0;
      sd_status        <= SD_NOTCHK;
      echo_ch          <= 8'h00;
      echo_next        <= ST_MENU;
      ec_i             <= 2'd0;
      ec_crlf          <= 1'b1;
      ec_gap           <= 1'b0;
      ec_valid         <= 1'b0;
      ec_byte          <= 8'h00;
      num_val          <= 0;
      num_dig          <= 1'b0;
      blk_no           <= 0;
      sec_no           <= 0;
      hx_val           <= 0;
      hx_i             <= 0;
      hx_next          <= ST_MENU;
      rt_name          <= 0;
      rt_len           <= 0;
      rng_start        <= 0;
      rng_cnt          <= 0;
      rng_blk          <= 0;
      rng_sum          <= 0;
      rng_hi           <= 0;
      num_second       <= 1'b0;
      r_i              <= 0;
    end else begin
      sp_start  <= 1'b0;
      hd_start  <= 1'b0;
      tp_start  <= 1'b0;
      wr_start  <= 1'b0;
      fx_start  <= 1'b0;
      ck_start  <= 1'b0;
      fsn_start <= 1'b0;
      il_go     <= 1'b0;
      spf_go    <= 1'b0;
      ec_valid  <= 1'b0;
      if (rdrst_cnt != 0) rdrst_cnt <= rdrst_cnt - 5'd1;
      if (io_run) io_cycles <= io_cycles + 1;

      // RANGE: fold every 16-bit word into the checksum as its bytes arrive
      // off the engine. ND-120 words are big-endian on the card, so the even
      // byte offset is the high half. Kept in this process (not in the
      // buffer's) so rng_sum has exactly one driver.
      //
      // ROTATE-then-add, not a plain sum: the checksum has to notice a block
      // read from the WRONG PLACE, and a plain 16-bit sum cannot. Blocks are
      // 1024 words, so shifting a run by whole blocks shifts it by a
      // multiple of 1024 words, and for any position-linear content the
      // difference that makes to a plain sum is a multiple of 2^20 - always
      // zero in 16 bits. Rotating the accumulator by one bit per word
      // destroys that cancellation for one wire and one adder.
      if ((mode == M_RDMP) && phase_write && eng_rx_we) begin
        if (!eng_rx_addr[0]) rng_hi <= eng_rx_data;
        else rng_sum <= {rng_sum[14:0], rng_sum[15]} + {rng_hi, eng_rx_data};
      end

      case (state)
        ST_BANNER: begin
          sp_msg           <= MSG_BANNER;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end

        ST_MENU: begin
          cmd_running      <= 1'b0;
          phase_write      <= 1'b0;
          copy_phase       <= 1'b0;
          sp_msg           <= MSG_SD_STATUS0 + {4'b0000, sd_status};
          next_after_print <= ST_MENU2;
          state            <= ST_PRINT;
        end

        ST_MENU2: begin
          sp_msg           <= MSG_MENU;
          next_after_print <= ST_MENU3;
          state            <= ST_PRINT;
        end

        // the menu does not fit in one 64-character status message, so its
        // tail (and the "# " prompt) is a second one
        ST_MENU3: begin
          sp_msg           <= MSG_MENU2;
          next_after_print <= ST_KEY;
          state            <= ST_PRINT;
        end

        ST_KEY:
        if (rx_valid) begin
          echo_ch    <= rx_data;  // echoed as <key> CR LF for accepted keys
          ec_i       <= 2'd0;
          ec_crlf    <= 1'b1;
          ec_gap     <= 1'b0;
          err_flag   <= 1'b0;
          done_flag  <= 1'b0;
          wd         <= 0;
          copy_phase <= 1'b0;
          fx_create  <= 1'b0;
          list_done  <= 1'b0;
          case (rx_data)
            "1": begin
              mode        <= M_LIST;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
            end
            "2": begin
              mode        <= M_DUMP;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
            end
            "3": begin
`ifdef SDFAT_REWRITE
              mode        <= M_COPY;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "4": begin
`ifdef SDFAT_TEST_READONLY
              // the read-only build contains no block-write path at all
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`else
`ifdef SDFAT_WRITE
              mode        <= M_BLK;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
`endif
            end
            "5": begin
`ifdef SDFAT_CHECK
              mode        <= M_CHK;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "6": begin
`ifdef SDFAT_SPEED
              mode        <= M_IOW;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "7": begin
`ifdef SDFAT_SPEED
              mode        <= M_IOR;
              cmd_running <= 1'b1;
              echo_next   <= ST_CARD;
              state       <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            // ---- streaming diagnostics: both ask for a number first ------
            // Both read their sectors through the sd_writer engine's READ
            // path, so they need that engine compiled in (SDFAT_WRITE names
            // the module, not the direction - the read-only build keeps it
            // and ties its rd_mode to 1).
            "8": begin
`ifdef SDFAT_WRITE
              mode             <= M_BDMP;
              num_val          <= 0;
              num_dig          <= 1'b0;
              sp_msg           <= MSG_ASK_BLK;
              next_after_print <= ST_NUM;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "9": begin
`ifdef SDFAT_WRITE
              mode             <= M_SDMP;
              num_val          <= 0;
              num_dig          <= 1'b0;
              sp_msg           <= MSG_ASK_SEC;
              next_after_print <= ST_NUM;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "R", "r": begin
`ifdef SDFAT_WRITE
              mode             <= M_RDMP;
              num_val          <= 0;
              num_dig          <= 1'b0;
              num_second       <= 1'b0;   // start block first, then the count
              sp_msg           <= MSG_ASK_STA;
              next_after_print <= ST_NUM;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`else
              sp_msg           <= MSG_NOTIMPL;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
`endif
            end
            "N", "n": begin
              rt_len           <= 0;   // typing a name replaces the old one
              rt_name          <= 0;
              sp_msg           <= MSG_ASK_NAME;
              next_after_print <= ST_NAME;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
            end
            "H", "h", "?": begin
              sp_msg           <= MSG_HELP1;
              next_after_print <= ST_HELP;
              echo_next        <= ST_PRINT;
              state            <= ST_ECHO;
            end
`ifdef SDFAT_EXT_TEST
            // ---- external memory tests (Nexys 4 DDR) --------------------
            // The external module owns the console until it clears ext_busy;
            // the card is not touched, so no reader phase is entered here.
            "M", "m": begin
              ext_id      <= 4'd0;      // DDR2
              cmd_running <= 1'b1;
              echo_next   <= ST_EXT_W;
              state       <= ST_ECHO;
            end
            "B", "b": begin
              ext_id      <= 4'd1;      // ND-120 memory path (BRAM)
              cmd_running <= 1'b1;
              echo_next   <= ST_EXT_W;
              state       <= ST_ECHO;
            end
`endif
            default: state <= ST_MENU;  // anything else: reprint the menu
          endcase
        end

        // ---- echo the accepted key + CR-LF, then run the command --------
        // ec_crlf = 0 echoes the character alone (console number/name entry,
        // where the line ends only when the operator presses CR); entering
        // with ec_i = 1 and ec_crlf = 1 emits just the CR LF.
        ST_ECHO:
        if (ec_gap) ec_gap <= 1'b0;  // let tx_busy assert before polling
        else if (ec_i == (ec_crlf ? 2'd3 : 2'd1)) state <= echo_next;
        else if (!tx_busy) begin
          ec_valid <= 1'b1;
          ec_byte  <= (ec_i == 2'd0) ? echo_ch : (ec_i == 2'd1) ? 8'h0D : 8'h0A;
          ec_i     <= ec_i + 2'd1;
          ec_gap   <= 1'b1;
        end

        // ---- decimal number entry (commands 8 and 9) --------------------
        ST_NUM:
        if (rx_valid) begin
          if (rx_data >= "0" && rx_data <= "9") begin
            if (num_fits) num_val <= num_x10 + {28'b0, rx_data[3:0]};
            num_dig   <= 1'b1;
            echo_ch   <= rx_data;
            ec_i      <= 2'd0;
            ec_crlf   <= 1'b0;   // echo the digit, stay on the same line
            ec_gap    <= 1'b0;
            echo_next <= ST_NUM;
            state     <= ST_ECHO;
          end else if (rx_data == 8'h0D || rx_data == 8'h0A) begin
            ec_i    <= 2'd1;     // CR LF only, then act on the number
            ec_crlf <= 1'b1;
            ec_gap  <= 1'b0;
            if (!num_dig) begin
              sp_msg           <= MSG_ERR_NUM;
              err_flag         <= 1'b1;
              next_after_print <= ST_MENU;
              echo_next        <= ST_PRINT;
            end else if ((mode == M_RDMP) && !num_second) begin
              // RANGE takes two numbers: this was the start block, now ask
              // for the count (the CR LF goes out first, then the prompt)
              rng_start        <= num_val[23:0];
              num_val          <= 0;
              num_dig          <= 1'b0;
              num_second       <= 1'b1;
              sp_msg           <= MSG_ASK_CNT;
              next_after_print <= ST_NUM;
              echo_next        <= ST_PRINT;
            end else begin
              blk_no      <= num_val[23:0];
              sec_no      <= num_val;
              rng_cnt     <= num_val[23:0];
              cmd_running <= 1'b1;    // release the reader: init + mount
              wd          <= 0;
              echo_next   <= ST_CARD;
            end
            state <= ST_ECHO;
          end else begin
            // any other key aborts the entry - safer than guessing
            sp_msg           <= MSG_ERR_NUM;
            err_flag         <= 1'b1;
            next_after_print <= ST_MENU;
            echo_ch          <= rx_data;
            ec_i             <= 2'd0;
            ec_crlf          <= 1'b1;
            ec_gap           <= 1'b0;
            echo_next        <= ST_PRINT;
            state            <= ST_ECHO;
          end
        end

        // ---- file name entry (command N) --------------------------------
        // Lower case is folded to upper case for the console echo only; the
        // reader compares case-insensitively either way. The length limit is
        // 12 = 8 + '.' + 3, the longest 8.3 name there is.
        ST_NAME:
        if (rx_valid) begin
          if (rx_data == 8'h0D || rx_data == 8'h0A) begin
            sp_msg           <= (rt_len == 0) ? MSG_ERR_NUM : MSG_NAME_SET;
            err_flag         <= (rt_len == 0);
            next_after_print <= ST_MENU;
            ec_i             <= 2'd1;   // CR LF only
            ec_crlf          <= 1'b1;
            ec_gap           <= 1'b0;
            echo_next        <= ST_PRINT;
            state            <= ST_ECHO;
          end else if (rx_data > 8'h20 && rt_len < 8'd12) begin
            rt_name[8*rt_len+:8] <= (rx_data >= "a" && rx_data <= "z")
                                    ? (rx_data - 8'd32) : rx_data;
            rt_len    <= rt_len + 8'd1;
            echo_ch   <= (rx_data >= "a" && rx_data <= "z")
                         ? (rx_data - 8'd32) : rx_data;
            ec_i      <= 2'd0;
            ec_crlf   <= 1'b0;
            ec_gap    <= 1'b0;
            echo_next <= ST_NAME;
            state     <= ST_ECHO;
          end
        end

        // ---- print hx_val as 8 hex digits + CR LF ------------------------
        ST_HEXOUT:
        if (ec_gap) ec_gap <= 1'b0;
        else if (hx_i == 4'd10) state <= hx_next;
        else if (!tx_busy) begin
          ec_valid <= 1'b1;
          ec_byte  <= (hx_i < 4'd8) ? hexd(hx_val[31-4*hx_i[2:0]-:4])
                     : (hx_i == 4'd8) ? 8'h0D : 8'h0A;
          hx_i     <= hx_i + 4'd1;
          ec_gap   <= 1'b1;
        end

        ST_CARD:
        if (card_ready) begin
          // card_type 1/2/3 maps directly onto MSG_CARD_SDV1/SDV2/SDHC
          sp_msg           <= {4'b0000, card_type};
          // the absolute-sector dump deliberately skips the FS: line - it
          // must work on a card whose filesystem does not mount at all
          next_after_print <= (mode == M_SDMP) ? ST_SWAIT : ST_FS;
          state            <= ST_PRINT;
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_CARD_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_NOCARD;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_FS:
        if (fs_type[1]) begin
          // fs_type 2/3 maps onto MSG_FS_FAT16/FAT32 (= fs_type + 2)
          sp_msg           <= 6'd2 + {4'b0000, fs_type};
          sd_status        <= SD_OK;
          next_after_print <= scan_only ? ST_LSCAN : ST_FILE;
          state            <= ST_PRINT;
        end else if (scan_done) begin
          sp_msg           <= MSG_FS_UNKNOWN;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        // ---- DUMP / COPY phase 0: read BOOT.BPUN into the buffer -------
        ST_FILE:
        if (file_found) begin
          if (mode == M_IOR && file_size != IO_BYTES) begin
            sp_msg           <= MSG_ERR_IOSZ;
            err_flag         <= 1'b1;
            next_after_print <= ST_MENU;
            state            <= ST_PRINT;
          end else if (mode == M_IOR) begin
            // size OK - but the reader is still mid-transaction: file_found
            // rises WHILE the directory sector is streaming from the card,
            // and the reader then streams the found file itself. Parking it
            // here left a REAL card in the SENDING-DATA state, where the
            // writer's next command is illegal and gets no response -> err
            // (silicon failure 11-JUL-2026; a stateless card model masked
            // it). Wait for scan_done like every other command: ST_STREAM
            // captures the sector and parks the reader cleanly.
            state <= ST_STREAM;
          end else begin
            sp_msg           <= MSG_FILE_FOUND;
            next_after_print <= ST_STREAM;
            state            <= ST_PRINT;
          end
        end else if (scan_done) begin
          if (mode == M_IOW) begin
            // IO.DAT does not exist yet: create it and go straight to the
            // allocation + timed write
            cp_is32        <= (fs_type == 2'd3);
            cp_cs          <= fs_cs;
            cp_fat0        <= fs_fat0;
            cp_spf         <= fs_spf;
            cp_nfat        <= fs_nfat;
            cp_dbase       <= fs_dbase;
            cp_total       <= fs_total;
            cp_rootclus    <= fs_rootclus;
            cp_old_cluster <= 0;
            fx_create      <= 1'b1;
            fx_realloc     <= 1'b1;
            phase_write    <= 1'b1;
            sp_msg         <= MSG_IOW_RUN;
            next_after_print <= ST_C_FIX;
            state          <= ST_PRINT;
          end else begin
            sp_msg           <= MSG_FILE_NOTFND;
            err_flag         <= 1'b1;
            next_after_print <= ST_MENU;
            state            <= ST_PRINT;
          end
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_STREAM:
        if (scan_done) begin
          if (trunc) begin
            // DUMP: still dump what fit; COPY: abort (never write a
            // truncated copy)
            sp_msg           <= MSG_ERR_TRUNC;
            err_flag         <= (mode == M_COPY);
            next_after_print <= (mode == M_COPY) ? ST_MENU : ST_DUMP;
            state            <= ST_PRINT;
          end else if (mode == M_COPY) begin
            // phase 1: re-scan for TEST.TXT while announcing the copy
            copy_len         <= wptr;
            copy_phase       <= 1'b1;
            rdrst_cnt        <= 5'd31;
            sp_msg           <= MSG_COPYING;
            next_after_print <= ST_C_FIND;
            state            <= ST_PRINT;
          end else if (mode == M_IOR) begin
            // reader finished (scan_done: it streamed IO.DAT once in 1-bit
            // and sent its CMD12 - the card is back in TRANSFER state and
            // idle): NOW capture the sector and park it for the timed
            // CMD18 burst read through the writer engine
            tgt_sector       <= file_first_sector;
            nsec             <= IO_SECT;
            seccnt           <= 0;
            phase_write      <= 1'b1;
            sp_msg           <= MSG_IOR_RUN;
            next_after_print <= ST_C_WRITE;
            state            <= ST_PRINT;
          end else if (mode == M_IOW) begin
            // rewrite IO.DAT from scratch: free + allocate IO_BLOCKS blocks
            cp_is32        <= (fs_type == 2'd3);
            cp_cs          <= fs_cs;
            cp_fat0        <= fs_fat0;
            cp_spf         <= fs_spf;
            cp_nfat        <= fs_nfat;
            cp_dbase       <= fs_dbase;
            cp_total       <= fs_total;
            cp_rootclus    <= fs_rootclus;
            cp_dir_sec     <= fnd_dir_sec;
            cp_dir_idx     <= fnd_dir_idx;
            cp_old_cluster <= fnd_cluster;
            fx_realloc     <= 1'b1;   // 6 always deletes the old file first
            fx_create      <= 1'b0;
            phase_write    <= 1'b1;
            sp_msg         <= MSG_IOW_RUN;
            next_after_print <= ST_C_FIX;
            state          <= ST_PRINT;
          end else if (mode == M_RDMP) begin
            // RANGE: the whole run must lie inside the file, and an empty
            // run is a typo, not a request
            if (rng_cnt == 24'd0 || {8'b0, file_size} < rng_need) begin
              sp_msg           <= MSG_ERR_RANGE;
              err_flag         <= 1'b1;
              next_after_print <= ST_MENU;
              state            <= ST_PRINT;
            end else begin
              // capture BEFORE phase_write parks the reader (same edge)
              tgt_sector       <= file_first_sector + {6'b0, rng_start, 2'b00};
              hx_val           <= file_first_sector + {6'b0, rng_start, 2'b00};
              nsec             <= 13'd4;   // the loop runs ONE block at a time
              seccnt           <= 0;
              rng_blk          <= 0;
              rng_sum          <= 0;
              rng_hi           <= 0;
              io_cycles        <= 0;       // 27 MHz cycles, card traffic only
              phase_write      <= 1'b1;
              hx_i             <= 0;
              hx_next          <= ST_C_WRITE;
              sp_msg           <= MSG_R_RUN;  // "...AT SECTOR " + 8 hex digits
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
          end else if (mode == M_BDMP) begin
            // 1KW block read: same framing as WRBLK1 (ND-120 block = 1024
            // 16-bit words = 2048 bytes = 4 SD sectors, block N at
            // file_first_sector + 4*N, which assumes the file occupies
            // consecutive sectors - command 9 is how that assumption gets
            // checked). Refuse a block that starts past the end of the
            // file: those sectors belong to another file and reporting
            // them as this file's data is exactly the confusion this tool
            // exists to remove.
            if ({8'b0, file_size} < blk_need) begin
              sp_msg           <= MSG_ERR_RANGE;
              err_flag         <= 1'b1;
              next_after_print <= ST_MENU;
              state            <= ST_PRINT;
            end else begin
              // capture BEFORE phase_write parks the reader (same edge)
              tgt_sector       <= file_first_sector + {6'b0, blk_no, 2'b00};
              hx_val           <= file_first_sector + {6'b0, blk_no, 2'b00};
              nsec             <= 13'd4;
              seccnt           <= 0;
              phase_write      <= 1'b1;
              hx_i             <= 0;
              hx_next          <= ST_C_WRITE;
              sp_msg           <= MSG_AT_SEC;   // "AT SECTOR " + 8 hex digits
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
          end else if (mode == M_BLK) begin
            // 1KW block write: the block must lie entirely inside the file
            // (past its end = inside the NEXT file's clusters)
            if (file_size < ({23'b0, BLK_NO} + 32'd1) * 32'd2048) begin
              sp_msg           <= MSG_ERR_RANGE;
              err_flag         <= 1'b1;
              next_after_print <= ST_MENU;
              state            <= ST_PRINT;
            end else begin
              // capture BEFORE phase_write parks the reader (same edge)
              tgt_sector       <= file_first_sector + {25'b0, BLK_NO, 2'b00};
              nsec             <= 13'd4;  // 1 block = 4 SD sectors
              seccnt           <= 0;
              phase_write      <= 1'b1;
              sp_msg           <= MSG_BLK_WRITING;
              next_after_print <= ST_C_WRITE;
              state            <= ST_PRINT;
            end
          end else begin
            state <= ST_DUMP;
          end
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        // ---- SECTOR (9): the FAT is not consulted at all -----------------
        // The reader still runs to completion first: parking it mid-command
        // leaves a real card in the SENDING-DATA state and the next command
        // on the bus gets no answer (silicon failure 11-JUL-2026, see the
        // note in ST_FILE). scan_done arrives even when the volume did not
        // mount, which is what makes this command usable on a broken card.
        ST_SWAIT:
        if (scan_done) begin
          tgt_sector       <= sec_no;
          hx_val           <= sec_no;
          nsec             <= 13'd1;
          seccnt           <= 0;
          phase_write      <= 1'b1;
          hx_i             <= 0;
          hx_next          <= ST_C_WRITE;
          sp_msg           <= MSG_AT_SEC;
          next_after_print <= ST_HEXOUT;
          state            <= ST_PRINT;
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_DUMP: begin
          hd_start <= 1'b1;
          state    <= ST_DUMP_W;
        end

        ST_DUMP_W: if (!hd_start && !hd_busy) state <= ST_CMD_END;

        // ---- LIST -------------------------------------------------------
        ST_LSCAN:
        if (scan_done && pk_state == PK_IDLE) begin
          if (mode == M_CHK) begin
            // capture the geometry before phase_write parks the reader
            cp_is32  <= (fs_type == 2'd3);
            cp_cs    <= fs_cs;
            cp_fat0  <= fs_fat0;
            cp_spf   <= fs_spf;
            cp_nfat  <= fs_nfat;
            cp_dbase <= fs_dbase;
            cp_total <= fs_total;
            cp_rootclus <= fs_rootclus;
            vt_n_r   <= vt_n;
            phase_write <= 1'b1;
            wd       <= 0;
            state    <= ST_CHK;
          end else begin
            // LIST: freeze the listing, capture the geometry + capacity
            // (same edge - the reader parks next cycle), then scan FAT #0
            // for the free count and append the disk-info line
            list_done <= 1'b1;
            cp_is32   <= (fs_type == 2'd3);
            cp_cs     <= fs_cs;
            cp_fat0   <= fs_fat0;
            cp_spf    <= fs_spf;
            cp_nfat   <= fs_nfat;
            cp_dbase  <= fs_dbase;
            cp_total  <= fs_total;
            cp_rootclus <= fs_rootclus;
            cp_cap    <= rd_cap_mb;
`ifdef SDFAT_FREESCAN
            phase_write      <= 1'b1;
            sp_msg           <= MSG_SCANNING;
            next_after_print <= ST_FREE;
            state            <= ST_PRINT;
`else
            il_have_free <= 1'b0;  // no scanner: the line says FREE N/A
            state        <= ST_INFO;
`endif
          end
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_LPRINT: begin
          tp_start <= 1'b1;
          state    <= ST_LPRINT_W;
        end

        ST_LPRINT_W: if (!tp_start && !tp_busy) state <= ST_CMD_END;

        // ---- LIST tail: free-space scan + disk-info line ------------------
        ST_FREE:
        if (!fsn_busy && !fsn_start) begin
          fsn_start <= 1'b1;
          wd        <= 0;
          state     <= ST_FREE_W;
        end

        ST_FREE_W:
        if (fsn_done) begin
          il_have_free <= 1'b1;
          state        <= ST_INFO;
        end else if (fsn_err || wd == WD_MAX) begin
          il_have_free <= 1'b0;  // scan failed: the line says FREE N/A
          state        <= ST_INFO;
        end else begin
          if (fsn_tick) wd <= 0;  // per-sector progress feeds the watchdog
          else wd <= wd + 1;
        end

        ST_INFO:
        if (!il_busy && !il_go) begin
          il_go <= 1'b1;
          state <= ST_INFO_W;
        end

        ST_INFO_W:
        if (!il_go && !il_busy) begin
          // reader stays parked through the print (see ST_CHK_W)
          state <= ST_LPRINT;
        end

        // ---- CHECK: walk every root file's cluster chain -----------------
        ST_CHK:
        if (!ck_busy && !ck_start) begin
          ck_start <= 1'b1;
          wd       <= 0;
          state    <= ST_CHK_W;
        end

        ST_CHK_W:
        if (ck_done) begin
          // reader stays parked while the report prints (ST_MENU releases
          // it together with cmd_running - no truncated restart commands)
          state <= ST_LPRINT;
        end else if (ck_err || wd == WD_MAX) begin
          phase_write      <= 1'b0;
          // CHECK only READS (FAT chains through the engine): the truthful
          // failure text is a FAT read error, never "SD WRITE FAILED"
          sp_msg           <= MSG_ERR_FATRD;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        // ---- IO speed: divide, format, print ------------------------------
        ST_SPEED:
        if (!spf_busy && !spf_go) begin
          spf_go   <= 1'b1;
          spf_read <= (mode == M_IOR);
          wd       <= 0;
          state    <= ST_SPEED_W;
        end

        ST_SPEED_W:
        if (!spf_go && !spf_busy) begin
          // reader stays parked through the print (see ST_CHK_W)
          state <= ST_LPRINT;  // print "WRITE/READ NNNNN KB/S"
        end else if (wd == WD_MAX) begin
          phase_write      <= 1'b0;
          sp_msg           <= (mode == M_IOR) ? MSG_ERR_READ : MSG_ERR_WRITE;
          err_flag         <= 1'b1;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        // ---- HELP: MSG_HELP1..12 then back to the menu -------------------
        // The help texts are not one contiguous run of message codes: 1..7
        // were allocated before the error/status codes 33..39, so 8..12 sit
        // above those and the sequence hops once.
        ST_HELP:
        if (sp_msg == MSG_HELP13) begin
          state <= ST_MENU;
        end else begin
          sp_msg           <= (sp_msg == MSG_HELP7) ? MSG_HELP8 : (sp_msg + 6'd1);
          next_after_print <= ST_HELP;
          state            <= ST_PRINT;
        end

        // ---- COPY phases 1-2: locate TEST.TXT, rewrite it in place ------
        ST_C_FIND:
        if (scan_done) begin
          if (!file_found) begin
            // TEST.TXT does not exist: create it (fresh 8.3 root entry)
            cp_is32        <= (fs_type == 2'd3);
            cp_cs          <= fs_cs;
            cp_fat0        <= fs_fat0;
            cp_spf         <= fs_spf;
            cp_nfat        <= fs_nfat;
            cp_dbase       <= fs_dbase;
            cp_total       <= fs_total;
            cp_rootclus    <= fs_rootclus;
            cp_old_cluster <= 0;
            fx_create      <= 1'b1;
            fx_realloc     <= 1'b1;
            phase_write    <= 1'b1;
            wd             <= 0;
            state          <= ST_C_FIX;
          end else begin
            // capture EVERYTHING before phase_write parks the reader
            // (same clock edge; the reader's registers reset next cycle)
            tgt_sector     <= file_first_sector;  // kept when realloc=0
            nsec           <= {4'b0, (copy_len + 17'd511) >> 9};
            seccnt         <= 0;
            cp_is32        <= (fs_type == 2'd3);
            cp_cs          <= fs_cs;
            cp_fat0        <= fs_fat0;
            cp_spf         <= fs_spf;
            cp_nfat        <= fs_nfat;
            cp_dbase       <= fs_dbase;
            cp_total       <= fs_total;
            cp_rootclus    <= fs_rootclus;
            cp_dir_sec     <= fnd_dir_sec;
            cp_dir_idx     <= fnd_dir_idx;
            cp_old_cluster <= fnd_cluster;
            // too small (or empty) -> full replacement; else size patch only
            fx_realloc     <= (file_size < {{(31 - BUF_AW){1'b0}}, copy_len});
            fx_create      <= 1'b0;
            phase_write    <= 1'b1;
            wd             <= 0;
            state          <= ST_C_FIX;
          end
        end else if (wd == WD_MAX) begin
          sp_msg           <= MSG_ERR_SCAN_TO;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_C_FIX:
        if (!fx_busy && !fx_start) begin
          fx_start <= 1'b1;
          wd       <= 0;
          state    <= ST_C_FIX_W;
        end

        ST_C_FIX_W:
        if (fx_done) begin
          // the (possibly new) contiguous data area of the target
          tgt_sector <= cp_dbase + {24'b0, cp_cs} * fx_new_first;
          if (mode == M_IOW) begin
            nsec      <= IO_SECT;
            seccnt    <= 0;
            io_cycles <= 0;
            io_run    <= 1'b1;  // the write clock starts with the first sector
          end
          wd         <= 0;
          state      <= ST_C_WRITE;
        end else if (fx_err || wd == WD_MAX) begin
          phase_write      <= 1'b0;
          // truthful surgeon verdicts: only a COMPLETED scan without a
          // free run is "NO CONTIGUOUS FREE SPACE"; a failed FAT read is a
          // read error (silicon 12-JUL-2026 printed no-space for garbage
          // FAT reads and sent the operator hunting a full card)
          sp_msg           <= !fx_err ? MSG_ERR_WRITE
                            : (fx_err_kind == 2'd0) ? MSG_ERR_SMALL
                            : (fx_err_kind == 2'd1) ? MSG_ERR_FATRD
                            : (fx_err_kind == 2'd3) ? MSG_ERR_FATCOR
                            : MSG_ERR_WRITE;
          err_flag         <= 1'b1;
          sd_status        <= SD_ERROR;
          next_after_print <= ST_MENU;
          state            <= ST_PRINT;
        end else wd <= wd + 1;

        ST_C_WRITE:
        if (seccnt >= nsec) begin
          if (mode == M_RDMP) begin
            io_run <= 1'b0;   // stop the clock before any console traffic
            state  <= ST_R_NEXT;
          end else if (dump_mode) begin
            // the sectors are in the buffer; the reader stays parked while
            // hex_dumper prints them (see ST_CHK_W for why)
            state <= ST_DUMP;
          end else if (io_mode) begin
            io_run <= 1'b0;
            state  <= ST_SPEED;  // phase_write stays up: the report uses the buffer
          end else begin
            // reader stays parked through the print (see ST_CHK_W)
            sp_msg           <= (mode == M_BLK) ? MSG_BLK_DONE : MSG_COPY_DONE;
            next_after_print <= ST_CMD_END;
            state            <= ST_PRINT;
          end
        end else if (!wr_busy && !wr_start) begin
          // menus 6/7 go in CMD25/CMD18 bursts of up to 128 sectors;
          // COPY/WRBLK1 keep the proven single-sector path (burst_len=1)
          wr_burst <= io_mode ? burst_now : 9'd1;
          // RANGE times the CARD traffic only: the counter runs while
          // sectors move and is stopped around every console line
          if (mode == M_RDMP) io_run <= 1'b1;
          if (mode == M_IOR && seccnt == 0) begin
            io_cycles <= 0;
            io_run    <= 1'b1;  // the read clock starts with the first burst
          end
          wr_start <= 1'b1;
          wd       <= 0;
          state    <= ST_C_NEXT;
        end

        ST_C_NEXT:
        if (wr_done) begin
          seccnt <= seccnt + 13'd1;  // the final block of the burst
          state  <= ST_C_WRITE;
        end else if (wr_block_next) begin
          seccnt <= seccnt + 13'd1;  // block k>=1 of the burst: advance the source
          wd     <= 0;
        end else if (wr_err || wd == WD_MAX) begin
          // menu 7/8/9/R are READS through the engine: report them as such
          sp_msg    <= ((mode == M_IOR) || dump_mode) ? MSG_ERR_READ
                                                     : MSG_ERR_WRITE;
          err_flag  <= 1'b1;
          sd_status <= SD_ERROR;
          if (mode == M_RDMP) begin
            // THE point of the range command: name the exact place it died,
            // then still print the summary. The reader stays parked for it
            // (ST_MENU releases it) exactly as after a completed run.
            io_run           <= 1'b0;
            next_after_print <= ST_R_ERR;
          end else begin
            phase_write      <= 1'b0;
            next_after_print <= ST_MENU;
          end
          state <= ST_PRINT;
        end else wd <= wd + 1;

        // ---- RANGE: one block finished ----------------------------------
        // Nothing here scales with the run length: the same 4-sector loop is
        // re-armed one block further on, and only the counters move.
        ST_R_NEXT: begin
          rng_blk <= rng_blk + 24'd1;
          if (rng_blk + 24'd1 >= rng_cnt) begin
            r_i   <= 0;
            state <= ST_R_SUM;
          end else begin
            tgt_sector <= tgt_sector + 32'd4;  // next block = 4 more sectors
            seccnt     <= 0;
            // a sign of life every 64 blocks (128 KB): at 9600 baud one
            // line per block would cost more time than the reading does
            if (rng_blk[5:0] == 6'd63) begin
              hx_val           <= {8'b0, rng_blk + 24'd1};
              hx_i             <= 0;
              hx_next          <= ST_C_WRITE;
              sp_msg           <= MSG_R_AT;
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end else state <= ST_C_WRITE;
          end
        end

        // ---- RANGE: a card read failed - say exactly where ---------------
        // The sector is the block base plus the index the engine was on;
        // both registers still hold the failing values here.
        ST_R_ERR: begin
          sp_msg           <= MSG_AT_SEC;
          hx_val           <= tgt_sector + {19'b0, seccnt};
          hx_i             <= 0;
          hx_next          <= ST_R_ERR2;
          next_after_print <= ST_HEXOUT;
          state            <= ST_PRINT;
        end

        ST_R_ERR2: begin
          sp_msg           <= MSG_R_AT;   // "AT BLOCK " + 8 hex digits
          hx_val           <= {8'b0, rng_blk};
          hx_i             <= 0;
          hx_next          <= ST_R_SUM;
          r_i              <= 0;
          next_after_print <= ST_HEXOUT;
          state            <= ST_PRINT;
        end

        // ---- RANGE: the summary, one labelled hex field per step ---------
        // err_flag is set by the read loop when the card failed, so the same
        // sequence reports a completed run and an aborted one - an aborted
        // one has already named the block and sector it died on.
        ST_R_SUM: begin
          r_i <= r_i + 3'd1;
          case (r_i)
            3'd0: begin
              sp_msg           <= MSG_R_BLOCKS;
              hx_val           <= {8'b0, rng_blk};
              hx_i             <= 0;
              hx_next          <= ST_R_SUM;
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
            3'd1: begin
              sp_msg           <= MSG_R_SECS;
              hx_val           <= {6'b0, rng_blk, 2'b00};  // 4 sectors a block
              hx_i             <= 0;
              hx_next          <= ST_R_SUM;
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
            3'd2: begin
              sp_msg           <= MSG_R_CHK;
              hx_val           <= {16'b0, rng_sum};
              hx_i             <= 0;
              hx_next          <= ST_R_SUM;
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
            3'd3: begin
              sp_msg           <= MSG_R_CYC;
              hx_val           <= io_cycles;  // 27 MHz; seconds = n / 27000000
              hx_i             <= 0;
              hx_next          <= ST_R_SUM;
              next_after_print <= ST_HEXOUT;
              state            <= ST_PRINT;
            end
            default: begin
              sp_msg           <= err_flag ? MSG_R_FAIL : MSG_R_PASS;
              next_after_print <= err_flag ? ST_MENU : ST_CMD_END;
              state            <= ST_PRINT;
            end
          endcase
        end

        // ---- common tail ------------------------------------------------
        ST_CMD_END: begin
          done_flag <= 1'b1;
          state     <= ST_MENU;
        end

`ifdef SDFAT_EXT_TEST
        // Kick the external test once, then wait for it to release ext_busy.
        // Kicking here rather than in ST_KEY keeps the echoed key and the
        // test's own output from interleaving.
        ST_EXT_W:
        if (!ext_kicked) begin
          ext_start  <= 1'b1;
          ext_kicked <= 1'b1;
        end else begin
          ext_start <= 1'b0;
          if (!ext_busy && !ext_start) begin
            ext_kicked <= 1'b0;
            state      <= ST_CMD_END;
          end
        end
`endif

        ST_PRINT: begin
          sp_start <= 1'b1;
          wd       <= 0;
          state    <= ST_PRINT_W;
        end

        ST_PRINT_W: if (!sp_start && !sp_busy) state <= next_after_print;

        default: state <= ST_BANNER;
      endcase
    end
  end

endmodule