ND-120 SD/FAT reader slimming plan¶
Target: shrink the hardware FAT reader
Verilog/SD-FAT/circuit/sd_file_reader.v, the
single biggest logic block on the Tang Nano 20K (GW2AR-18C) and the reason the
OSS (yosys/nextpnr) placer runs out of room (July 2026).
Status (checked against the code, 28-SEP-2026): Lever 1 is DONE
(SDFAT_NO_LFN, set in fpga/tang-nano-20k/src/tang20k_defines.v).
Lever 2 is DONE and went further than planned: the contiguity checker is
retired from every build since the engine walks the FAT chain itself
(07-AUG-2026; it now needs -DSDFAT_FORCE_STORAGE_CHECK). Lever 3 was NOT
done and no longer applies: since the block cache (04-AUG-2026) sd_writer
is also the engine that reads every block from the card, so a storage build
cannot drop it (SDFAT_NO_WRITE exists in sd_fat_features.vh but only the
sd-fat-test lint build uses it). Lever 4 (raw-LBA boot) is not built and is
the one lever still open. The defines are documented in
Verilog/docs/build-defines.md and tang20k_defines.v.
How the numbers were produced (so they can be trusted / reproduced)¶
All LUT figures below are from yosys synth_gowin -noflatten (the Gowin
GW2A LUT4 + ALU carry-cell architecture), run in a scratch tree on ext4
(/tmp/fatscan), never against the repo build. The flow per module:
read_verilog <file>
hierarchy -top sd_file_reader
setattr -mod -set keep_hierarchy 1 # baseline only, to split the sub-instance
synth_gowin -noflatten
stat
- yosys is an estimator, not the Gowin vendor mapper. Its LUT4 count and the Gowin toolchain's count differ in absolute terms; the relative attribution between sub-functions is what this plan relies on.
- Gowin counts an ALU carry cell as occupying a LUT slot, so the honest "logic cells" figure is LUT + ALU. On Gowin's own synth Ronny observed ~8930 cells for the reader; yosys gives 7718 LUT + 1429 ALU = 9147 — the same ballpark, which is the cross-check that the attribution is sound.
- The 512-byte sector buffer
secbuf(sd_file_reader.v:289) maps to 1 BSRAM (SDP) block, not LUTs — confirmed bySDP 1in the baseline stat. It is not a LUT lever.
Scratch scripts and raw stat dumps: /tmp/fatscan/*.ys, /tmp/fatscan/stat_*.txt.
(a) sd_file_reader LUT breakdown by function¶
sd_file_reader.v contains two modules: sd_file_reader (the FAT brain)
and sd_card_ctrl (the SD bit engine), in the same file. yosys, keeping the
hierarchy, splits them cleanly:
| Block | Where (file:line) | LUT | ALU | LUT+ALU | Share |
|---|---|---|---|---|---|
sd_card_ctrl SD bit engine: CMD/response shifter, R2 shadow, CRC7, CRC16, CMD17/18/12 data FSM, bit clock |
sd_file_reader.v:1245-1537 |
533 | 205 | 738 | ~8% |
sd_file_reader FAT brain: card-init FSM, mount/BPB/MBR parse, root-dir scan, VFAT+8.3 entry parser, FAT-entry fetch, cluster-chain walk + run merge, CSD decode, geometry math |
sd_file_reader.v:60-1214 |
7185 | 1224 | 8409 | ~92% |
| Whole reader | 7718 | 1429 | 9147 | 100% |
The bit engine is small (~8%). The FAT-parsing brain is 92% of the cost. That is where every lever lives.
Finer attribution inside the FAT brain (measured by ablation)¶
Because almost everything lives in one Verilog module, sub-functions were attributed by synthesizing scratch variants with a feature forced off and diffing against the 9147 baseline:
| Sub-function | Measurement | LUT+ALU delta | Notes |
|---|---|---|---|
| VFAT long-filename (LFN) parser | force the pattr==0x0F branch to skip; opt_clean removes lfn_buf, P_LFNU, lfn_off, the plo/phi variable part-selects |
~1800 (1751 LUT + 51 ALU) | baseline 9147 → 7345. Biggest single removable chunk. |
| FAT32 support | force is32 constant 0 (sd_file_reader.v:971) |
~0 (measured +370 LUT — mapping noise, ALU unchanged at 1429) | The cluster/sector datapath is 32-bit wide for FAT16 too; FAT32 only adds narrow muxes, so removing it frees almost nothing. Not a lever. |
The remaining ~5500 LUT of the FAT brain is spread across the wide
proc[255:0] / raw[247:0] variable bit-selects in the entry parser
(sd_file_reader.v:333-341, states P_SFN..P_CMP :625-712), the 32-bit
geometry math including a 32-bit multiply fatarea = nfat * spf_v
(sd_file_reader.v:404, part of the 1224 ALU), the field-fetch/secbuf
addressing, and the card-init + streaming FSMs. These do not decompose further
without a rewrite; they are not cheap single-ifdef wins.
Explicit answer: subdirectory traversal¶
Our reader does NOT implement subdirectory descent, so there is no subdirectory-traversal logic to remove — that lever yields 0 LUT.
- The root directory is scanned linearly (FAT16 root region) or by walking the
root directory's own cluster chain on FAT32 (
H_DIR_HOP,sd_file_reader.v:1067-1075). That HOP follows the root dir when it spans clusters; it does not descend into nested folders. dir_entry_is_dir(sd_file_reader.v:97, set at:639) is reported but never acted on — nothing ever re-points the scan at a subdirectory's cluster.- So "keep root-dir lookup, drop subdir traversal" is already the design. BOOT.BPUN / TAPE.BPUN living in the root is found by the existing root scan. No edit, no saving — the feature was never built.
(b) What was dead weight in the Tang build (July 2026)¶
At the time the Tang build compiled the full stack with no SDFAT_NO_*
macro. Measured with yosys, for the tape-only case:
| block | LUT | ALU | LUT+ALU | outcome |
|---|---|---|---|---|
sd_writer (CMD24 write engine) |
954 | 127 | 1081 | still in every storage build - it is the block read engine too since Phase 4 |
nd_storage_fatchk (contiguity checker) |
868 | 309 | 1177 | retired from builds |
| VFAT long-name parser in the reader | 1751 | 51 | ~1800 | stripped by SDFAT_NO_LFN on the Tang |
The reader never drives DAT0 and has no CMD24 of its own; the write path is
the separate sd_writer instance in nd_storage.v. FAT32 costs essentially
nothing (section (a)), so it stays.
(c) Ranked slimming levers¶
Ordered by (saving ÷ risk). Savings are yosys LUT+ALU deltas.
Lever 1 — Strip VFAT long-filename parsing (8.3 names only) ~1800 LUT - DONE¶
SDFAT_NO_LFN in sd_file_reader.v; measured 9147 -> 7345. Files must have
8.3 short names (all the fixed card names are).
Lever 2 — Drop the contiguity checker ~1177 LUT - DONE (retired from builds)¶
Lever 3 — Remove the write engine for a read-only build ~1081 LUT - NOT APPLICABLE¶
The planned SDFAT_WRITE gate on the sd_writer instance in nd_storage.v
was never added, and since Phase 4 sd_writer in rd_mode fetches every
block from the card, so it cannot be removed from a storage build.
Lever 4 (ultimate) — RAW-BLOCK boot mode: reserved LBAs, NO FAT parse ~7500 LUT¶
- Saving (analytic estimate): everything except the SD bit engine + card
init + a fixed-range CMD18 streamer survives. That is ~
sd_card_ctrl(738) + card-init FSMH_BOOT..H_CMD16(:737-889, a few hundred LUT) + a small sector counter ≈ 1200–1600 LUT+ALU total, i.e. ~7500 LUT saved vs the 9147 baseline. This removes the entire mount/BPB/MBR parse (:892-1025), the whole entry parser incl. LFN (:297-732), FAT-entry fetch (:1078-1106), cluster-chain walk + run merge (:1108-1159), and CSD decode (:846-867). - Edit point: a new lean top (e.g.
sd_block_reader.v) reusingsd_card_ctrlunchanged, or anSDFAT_RAW_LBAmode in the reader that jumps fromH_CMD16straight to a fixed-e_argCMD18 stream. New module, not a one-lineifdef. - Functional cost: the card must be prepared by writing the image to known
reserved physical sectors (
ddto LBA N), no FAT filesystem — you lose the "drop a .BPUN file on the card" convenience. - Risk: medium (new RTL to write + verify) but conceptually simple and the bit engine is already silicon-proven at 13.5 MHz.
- This is also the floppy/SMD path in Ronny's plan: reserved physical blocks (raw LBA), no FAT, one lean reader shared by every device.
Lever 5 (do NOT bother) — Drop FAT32¶
- Measured saving ~0 (see (a)). The 32-bit datapath is shared with FAT16. Keep dual-format support; it is effectively free.
Minor levers (small, optional)¶
- CSD capacity decode (
card_capacity_mb,H_CSD/H_CSD1:846-867, decode wires:226-238): already written bit-serial to avoid a ~200-LUT barrel shifter (see the author's note:236-237). Removing capacity reporting entirely (tiecard_capacity_mboff, skip CMD9) saves ~100-200 LUT. Cosmetic only. Low priority. dir_entry_*LIST enumeration outputs are already unconnected innd_storage.v:188-194; yosys prunes the unused output ports in-context, but the internal 8.3 name-build feeds the target compare and cannot be pruned. No meaningful standalone lever.
(d) Internet comparison¶
Our reader is a clean-room MIT rewrite (sd_file_reader.v:1-58) that
replaced the GPL WangXuan95 core. Comparing scope fairly:
| Core | FAT16 | FAT32 | Dir scope | Long names (LFN) | Write | Extra outputs | Reported LUTs |
|---|---|---|---|---|---|---|---|
ours (sd_file_reader.v) |
yes | yes | root only | yes (VFAT) | no (reader) | CSD capacity, MBR partition walk, full dir_entry_* LIST enumeration, rich fs_* geometry |
~9147 (yosys) / ~8930 (Gowin) |
| WangXuan95 FPGA-SDcard-Reader (the GPL core we replaced) | yes | yes | root only | no (8.3 only) | no | file locate + length only | none published |
| WangXuan95 FPGA-SDcard-Reader-SPI | yes | yes | root only | no | no | minimal | none published |
| sd2snes / retro FAT cores | typ. FAT32 | — | root/table | usually no | some write | core-specific | none in a comparable form |
Key takeaways:
- We match the reference on the fundamentals (FAT16+FAT32, root-only, 1-bit
DAT0, no tristate) — so on those axes we are not bloated.
- We carry weight the reference does NOT: (1) VFAT long-filename
parsing — the WangXuan95 core is 8.3-only; our LFN overlay is the ~1800-LUT
Lever 1. (2) CSD capacity decode, MBR partition-table walk, and a full
dir_entry_* directory-LIST enumeration with dates/sizes — the reference
just locates one file. These extras are real logic the baseline doesn't spend.
- None of these public cores publish a LUT table, so a like-for-like number
comparison isn't available — but the feature delta is unambiguous: strip
LFN (Lever 1) and our reader's feature set lands back on the WangXuan95
baseline, minus the CSD/MBR/LIST extras.
Verdict: the reader is not fundamentally bloated for what a FAT16/32 root-directory read-by-name core costs — but it does carry ~1800 LUT of VFAT long-name logic and a few hundred LUT of capacity/partition/LIST conveniences that the tape-boot job never uses.
Sources: - WangXuan95/FPGA-SDcard-Reader — FAT16/FAT32, root-dir only, 8.3 names only, read-only, no LUT table published (confirmed via its README). - WangXuan95/FPGA-SDcard-Reader-SPI — SPI variant, same feature scope. - Reading FAT32 inside an FPGA core (ws0.org) — context on keeping in-core FAT parsing small.
(e) What remains: raw-LBA boot¶
Tier A of this plan (levers 1-3) is done as far as it applies (see the status at the top). Tier B is still open:
Tier B — raw-LBA boot (maximum saving, needs new RTL)¶
Lever 4: a lean sd_card_ctrl-based block reader that streams a fixed reserved
LBA range. No FAT at all.
- Est. saving: ~7500 LUT+ALU (reader collapses to ~1200-1600).
- Card recipe:
ddthe image to a reserved sector; no filesystem. - This is the same core the floppy and SMD devices should share (Ronny's reserved-physical-block plan), so the cost is amortized across every future device — build it once.
Note: the storage stack now walks the FAT chain per access and serves disc images from the card by name, so the case for a raw-LBA reader is only the LUT saving; it would give up the drop-a-file-on-the-card workflow.
Analysis produced from source at SD-FAT/circuit/sd_file_reader.v,
nd_storage.v, sd_fat_features.vh and the Tang gprj; LUT figures from yosys
synth_gowin in a scratch tree. Line numbers refer to the July 2026 sources.