Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,6 @@ output_files_*/
*.qsf.bak
*.md.bak
*.md.bak.gz

# regenerated iostd summary codec (content-free; produce via iostd_codec_mine.py --analyze)
results/iostd_codec.json
43 changes: 43 additions & 0 deletions docs/mined_codec_data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# Mined codec data (device-general)

Notes on two mined result files under `results/` — facts about the EP4CE silicon
(no target design in them), produced with this repo's tooling.

## `results/rcf_wire_cells.json` — routing wire → CRAM cells (102 wires)
Maps a named routing wire to the exact CRAM `[offset, bitpos]` cells that encode it,
across **five** switch classes — `C4:` (24), `C16:` (4), `R4:` (38), `R24:` (4),
`LOCAL_INTERCONNECT:` (32) — e.g.

```json
"R24:X9Y12S0I23": [[225382, 3], [225786, 3]],
"LOCAL_INTERCONNECT:X13Y10S0I30": [[100508, 4], [100929, 4], [101773, 4]]
```

Produced by RCF back-annotation: compile pair designs that force a known route,
capture the exact wires Quartus used (`quartus_cdb --back_annotate=routing`) and the
CRAM diff, then signature-correlate wire ↔ cell. Solid positive data — a direct
contribution to the routing codec. **Note:** the producing miner is *not* included
here (it's coupled to the larger mining pipeline); the method above is the
reproduction recipe, and the `.json` stands on its own as consumable data.

## `results/iostd_specimens.jsonl` — raw IOB electrical specimens (a documented dead end)
25 raw `full_diff` specimens (per record: `{pin, label, cells}`) from building the
same buffer with a matrix of IOB settings (I/O standard / drive / slew / pull /
registered) on three anchor pins in different banks. Feed them to
`fuzz/iostd_codec_mine.py --analyze` to reproduce the (weak) codec.

**Honest negative — read before using.** The intended product was a per-setting
*position-invariant* electrical codec (variant-vs-ref diff cancels routing → only the
electrical bits remain, intersected across pins). It did **not** pan out: each setting
moves ~26–122 cells per pin, but the cross-pin invariant intersection is 0–3 cells
with pairwise Jaccard ~0.02–0.16 — and even those 1–3 cells are largely a
normalisation artifact (min-offset anchoring forces `[0,0]` into every non-empty
setting). `drive_max`/`slew_fast`/`fast_oreg` are no-ops (default LVTTL is already
max-drive/default-slew; a combinational buffer has no output register). So the
electrical bits are not cleanly position-invariant under min-offset normalisation.
The **raw specimens are committed on purpose** so the dead end is *re-analyzable* —
try a better alignment (per-bank, or anchoring to the IOB base rather than the delta
min) without re-spending the Quartus time. The regenerated summary
(`results/iostd_codec.json`) is intentionally not committed — it's content-free; run
`--analyze` to produce it.
2 changes: 1 addition & 1 deletion fuzz/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# --- Paths ---
QUARTUS_BIN = os.path.expanduser("~/intelFPGA_lite/21.1/quartus/bin")
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORK_DIR = os.path.join(PROJECT_ROOT, "work")
WORK_DIR = os.environ.get("FUZZ_WORK_DIR") or os.path.join(PROJECT_ROOT, "work")
RESULTS_DIR = os.path.join(PROJECT_ROOT, "results")
RBF_DIR = os.path.join(RESULTS_DIR, "rbf")
TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "templates")
Expand Down
144 changes: 144 additions & 0 deletions fuzz/dense_fill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# SPDX-License-Identifier: GPL-3.0-or-later
"""Dense-fill route-forcing payload: saturate one LAB column with a snake chain.

The two-LUT pair (plan_d_prime_factory.compile_edge) forces ONE wire per
Quartus compile → only ~3-5 new bits, because a single wire's cells are mostly
shared infra already explained. This module instead packs a whole LAB column
with a chain of pinned LUT cells, each fed by the previous one, so a SINGLE
compile forces dozens-to-hundreds of distinct wires — every cell's inbound
LOCAL_INTERCONNECT mux + the routing feeding it lands in the target column,
exactly where the hard-to-reach "dark" routing bits live.

Why a chain (not independent pairs): each cell drives the next (real fanout)
and the last drives a registered output, so `dont_touch` can't fold anything
away and we need only ONE input pin + one clock for the whole column. Snake
ordering (row by row, N alternating direction) keeps consecutive cells
physically adjacent so the forced hops are short, well-defined C4/LOCAL routes.

Cells cycle through all four input ports so different LI-mux inputs get
exercised across the column in one shot.
"""
from __future__ import annotations

from config import LAB_Y

PORTS = ("dataa", "datab", "datac", "datad")


def snake_placements(x: int, ns=(0, 8, 16, 24), rows=None, pattern="row"):
"""Return an ordered [(x, y, n), ...] visiting every cell of column x.

The chain wires consecutive entries together, so the VISITATION ORDER
determines which physical routing wires get forced. This is the ONE
diversity knob that matters — validated 2026-07-20: port rotation and LUT
mask add ~0 new bits (routing cells don't depend on LUT pin or truth
table), but a different traversal order adds ~75 new routing bits/compile
because it forces a different set of C4/LOCAL hops.

Patterns (all cover the same cell set, different consecutive-pair profile):
row — Y outer, N inner (short N-steps then row jumps)
col — N outer, Y inner (row-steps then N jumps; longer C4)
rowstr2 — rows in stride-2 order (0,2,4,.. then 1,3,..) → 2-row C4 hops
colstr2 — cols/N traversal, rows stride-2 inner
diag — diagonal walk (both Y and N advance each step)
"""
rows = list(rows) if rows is not None else list(LAB_Y)
ns = list(ns)

def boustro(seq, i):
return seq if (i % 2 == 0) else list(reversed(seq))

out = []
if pattern == "row":
for i, y in enumerate(rows):
for n in boustro(ns, i):
out.append((x, y, n))
elif pattern == "col":
for i, n in enumerate(ns):
for y in boustro(rows, i):
out.append((x, y, n))
elif pattern == "rowstr2":
order = rows[0::2] + rows[1::2]
for i, y in enumerate(order):
for n in boustro(ns, i):
out.append((x, y, n))
elif pattern == "colstr2":
order = rows[0::2] + rows[1::2]
for i, n in enumerate(ns):
for y in boustro(order, i):
out.append((x, y, n))
elif pattern == "diag":
cells = [(y, n) for y in rows for n in ns]
cells.sort(key=lambda yn: (rows.index(yn[0]) + ns.index(yn[1]),
ns.index(yn[1])))
out = [(x, y, n) for (y, n) in cells]
elif pattern.startswith("perm"):
# Deterministic pseudo-random traversal: chains physically DISTANT
# cells, so the forced hops span long distances -> exercises long
# lines (C16/R24) and multi-span C4/R4 that adjacency patterns never
# touch. Seed from the suffix (perm1, perm2, ...); no RNG needed.
seed = int(pattern[4:] or "1")
cells = [(x, y, n) for y in rows for n in ns]
k = len(cells)
# LCG-style index permutation with a seed-dependent odd stride
stride = (2 * ((seed * 2654435761) % (k // 2)) + 1)
order = sorted(range(k), key=lambda i: ((i * stride + seed * 40503) % k))
out = [cells[i] for i in order]
else:
raise ValueError(f"unknown pattern {pattern!r}")
return out


def gen_column_fill(placements, name: str = "fuzz_top", mask: int = 0x8888,
port_offset: int = 0):
"""Build a chained-LUT Verilog module + placement dict for one column.

placements: ordered [(x, y, n), ...]; consecutive entries are chained.
mask: LUT truth-table — different masks set different LUT SRAM cells, so
sweeping it covers the LUT-SRAM dark bits a single mask misses.
port_offset: rotates which input port each cell uses. Cell i drives
PORTS[(i+port_offset)%4]; sweeping offset 0..3 makes every LE receive
its signal on every port across passes → covers all four inbound
LOCAL_INTERCONNECT mux groups per LE (a big share of routing dark bits).
Returns (verilog_str, placement_map) mapping cell name -> LCCOMB_X_Y_N.
"""
k = len(placements)
if k < 2:
raise ValueError("need >=2 placements for a chain")

lines = []
placement = {}
for i, (x, y, n) in enumerate(placements):
inst = f"cell_{i}"
placement[inst] = f"LCCOMB_X{x}_Y{y}_N{n}"
port = PORTS[(i + port_offset) % 4]
# cell 0 is driven by top-level pin A; cell i>0 by the previous combout
src = "A" if i == 0 else f"w{i-1}"
pin_ports = []
for p in PORTS:
sig = src if p == port else "1'b0"
pin_ports.append(f" .{p}({sig})")
pin_ports_str = ",\n".join(pin_ports)
lines.append(f""" wire w{i};
cycloneive_lcell_comb #(
.lut_mask(16'h{mask:04X}),
.sum_lutc_input("datac"),
.dont_touch("on")
) {inst} (
{pin_ports_str},
.combout(w{i})
);""")

body = "\n".join(lines)
verilog = f"""module {name}(
input wire CLK,
input wire A,
output reg Q
);
{body}

always @(posedge CLK)
Q <= w{k-1};
endmodule
"""
return verilog, placement
Loading