feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor
Begins the V2 Neural Multiprocessor / Dataflow architecture per docs/v2-description.md, per explicit user request to freeze V1 and start V2 development, copying from V1 what's needed. Scaffold: - hardware/v1/: byte-exact, read-only copy of the current V1 codebase (rtl, testbenches, tools, constraints, a representative subset of synthesis results, and reference docs) -- verified identical via diff/cmp against the live top-level tree before being made filesystem-read-only. The live top-level tree is untouched and remains the project's "production" V1 (see hardware/v1/README.md and hardware/v2/logs/decisions.log DEC-0001 for why copy-not-move). - hardware/v2/: mandatory structure (rtl/sim/constraints/synthesis/ reports/scripts/logs/docs) plus the full logging system required by the spec (development/architecture/simulation/synthesis/timing/ benchmark/decisions/experiments/errors.log). M1 -- Neural Processor (hardware/v2/rtl/neural_processor.v): - 8-stage pipelined perceptron unit (P_IN=8): input align, 8 multipliers, 3-level adder tree, accumulator, bias+activation, INT8 saturation. Genuine 1-tile/cycle throughput, not just a wider combinational datapath. - 7-state FSM (NP_IDLE..NP_ERROR per docs/v2-description.md §6, with 4 baseline states merged into NP_WAIT_OPERANDS -- see decisions.log DEC-0002); valid/ready/data/last stream interfaces per §7. - Bit-exact vs the frozen hardware/v1/rtl/neuron_parallel.v + mac8.v + mac_unit.v: 7/7 tests pass (hardware/v2/sim/tb_neural_processor.v), covering regular/mixed-sign/extreme-INT8 vectors, both activations, a zero-idle-gap back-to-back-tiles throughput check, and an 8-tile job -- verified with Verilator (see below for why). - Real synthesis + place&route (Yosys + nextpnr-ecp5): 0 CHECK problems, Fmax 183.12 MHz at ACC_WIDTH=32 (PASS at 80MHz, ~3x V1's isolated PARALLEL=8 Fmax of 61.71 MHz) and 176.21 MHz at ACC_WIDTH=24 (a user-requested comparison experiment, also bit-exact-verified; see experiments.log EXP-0001/EXP-0002 and benchmark.log). Three real bugs found and resolved during M1 development (full diagnostic record in errors.log): - Two independent, reproducible Icarus Verilog v13.0 scheduling defects (ERR-0001, ERR-0002) that silently produced wrong simulation results for standard sequential Verilog -- confirmed via Verilator 5.050 giving correct results on the same minimal repros. Verilator is now the trusted simulator for hardware/v2/ (decisions.log DEC-0004); Icarus's affected protocol-violation check was removed from the RTL and deferred architecturally to the Neural Director (DEC-0003) rather than chased further. - One real RTL bug (ERR-0003): last0 wasn't gated like valid0, letting a "last tile" tag leak into the pipeline ahead of its actual valid tile on back-to-back jobs. Fixed and verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generates synth/ecp5/spi_neuron_top.lpf (LOCATE/IOBUF constraints) for
|
||||
spi_neuron_top's SPI + PSRAM ports on the real LFE5U-45F-8BG381C part,
|
||||
using Project Trellis's own device database as the source of ball/bank/
|
||||
dual-function data (the same data nextpnr-ecp5 itself uses) -- not
|
||||
invented numbers.
|
||||
|
||||
Requires prjtrellis installed (Homebrew: `brew install prjtrellis`) and
|
||||
its iodb.json for LFE5U-45F. See docs/FPGA-Neural-Hardware-Design.md §7
|
||||
for the full placement rationale (bank/die-edge geometry, why banks 2+3
|
||||
hold the PSRAM bus and bank 7 holds SPI/clock/reset).
|
||||
|
||||
Re-run this whenever the port list of rtl/spi_neuron_top.v's top-level
|
||||
SPI/PSRAM interface changes (ADDR_WIDTH, MEM_DATA_WIDTH, etc.) -- it does
|
||||
NOT try to read the RTL; the port list/widths are hardcoded below and
|
||||
must be kept in sync by hand.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import glob
|
||||
import sys
|
||||
|
||||
CLK_BALL = "H5" # GR_PCLK7_0, bank 7 -- dedicated global clock pad
|
||||
ADDR_BITS = 23 # ADDR_WIDTH (byte address); only [21:0] carry real address, see §3
|
||||
DATA_BITS = 16 # MEM_DATA_WIDTH
|
||||
|
||||
def find_iodb():
|
||||
candidates = glob.glob(
|
||||
"/opt/homebrew/Cellar/prjtrellis/*/share/trellis/database/ECP5/LFE5U-45F/iodb.json"
|
||||
) + glob.glob(
|
||||
"/usr/share/trellis/database/ECP5/LFE5U-45F/iodb.json"
|
||||
)
|
||||
if not candidates:
|
||||
sys.exit("prjtrellis iodb.json not found -- install prjtrellis (brew install prjtrellis)")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def load_balls(iodb_path, package="CABGA381"):
|
||||
d = json.load(open(iodb_path))
|
||||
pkg = d["packages"][package]
|
||||
meta_idx = {}
|
||||
for m in d["pio_metadata"]:
|
||||
meta_idx.setdefault((m["col"], m["row"], m["pio"]), []).append(m)
|
||||
out = {}
|
||||
for ball, info in pkg.items():
|
||||
key = (info["col"], info["row"], info["pio"])
|
||||
metas = meta_idx.get(key, [])
|
||||
bank = metas[0]["bank"] if metas else None
|
||||
funcs = sorted(set(m.get("function", "") for m in metas if m.get("function")))
|
||||
out[ball] = dict(col=info["col"], row=info["row"], bank=bank, funcs=funcs)
|
||||
return out
|
||||
|
||||
|
||||
def bank_balls(rows, bank, exclude=()):
|
||||
items = [(b, v) for b, v in rows.items() if v["bank"] == bank and b not in exclude]
|
||||
plain = sorted((x for x in items if not x[1]["funcs"]), key=lambda x: (x[1]["row"], x[1]["col"], x[0]))
|
||||
special = sorted((x for x in items if x[1]["funcs"]), key=lambda x: (x[1]["row"], x[1]["col"], x[0]))
|
||||
return plain + special
|
||||
|
||||
|
||||
def assign(rows):
|
||||
psram_pool = bank_balls(rows, 2) + bank_balls(rows, 3)
|
||||
ctrl_pool = bank_balls(rows, 7, exclude={CLK_BALL})
|
||||
|
||||
a = {}
|
||||
for i, (ball, _) in enumerate(psram_pool[0:ADDR_BITS - 1]):
|
||||
a[f"psram_a[{i}]"] = ball
|
||||
for i, (ball, _) in enumerate(psram_pool[ADDR_BITS - 1:ADDR_BITS - 1 + DATA_BITS]):
|
||||
a[f"psram_dq[{i}]"] = ball
|
||||
ctrl_names = ["psram_ce_n", "psram_oe_n", "psram_we_n", "psram_lb_n", "psram_ub_n", "psram_zz_n"]
|
||||
for name, (ball, _) in zip(ctrl_names, psram_pool[ADDR_BITS - 1 + DATA_BITS:ADDR_BITS - 1 + DATA_BITS + 6]):
|
||||
a[name] = ball
|
||||
a[f"psram_a[{ADDR_BITS - 1}]"] = psram_pool[ADDR_BITS - 1 + DATA_BITS + 6][0] # always-0 spare bit
|
||||
|
||||
# Application SPI + reset + host attention pins (irq_n/data_ready_n,
|
||||
# added 2026-09-03), all in bank 7 alongside clk -- kept away from
|
||||
# the PSRAM bus (banks 2+3) per the same "opposite edges" rationale
|
||||
# as the SPI signals. flash_sclk/flash_mosi/flash_miso/flash_cs_n
|
||||
# (added 2026-09-04, revised same day to drop the USRMCLK/CCLK
|
||||
# coupling -- see rtl/spi_flash_master.v's header) join the same
|
||||
# pool for the same reason -- this is a fully independent,
|
||||
# ordinary-GPIO SPI bus toward the boot/persistence flash
|
||||
# (rtl/spi_flash_master.v), physically distinct from both the host
|
||||
# SPI above and the dedicated config-SPI pins (never touched by
|
||||
# user logic, see docs/FPGA-Neural-Hardware-Design.md §6). No pin
|
||||
# is shared with any ECP5 config primitive.
|
||||
# flash_sclk appended LAST (not inserted among the other flash
|
||||
# signals) so this regeneration stays additive: flash_mosi/
|
||||
# flash_miso/flash_cs_n keep the exact balls already committed to
|
||||
# the datasheet/schematic notes, only one genuinely new ball is
|
||||
# allocated for flash_sclk.
|
||||
spi_names = ["sclk", "mosi", "miso", "cs_n", "rst", "irq_n", "data_ready_n",
|
||||
"flash_mosi", "flash_miso", "flash_cs_n", "flash_sclk"]
|
||||
for name, (ball, _) in zip(spi_names, ctrl_pool[0:11]):
|
||||
a[name] = ball
|
||||
a["clk"] = CLK_BALL
|
||||
return a
|
||||
|
||||
|
||||
def write_lpf(assignment, path):
|
||||
lines = ["BLOCK ASYNCPATHS;", "BLOCK RESETPATHS;", ""]
|
||||
for sig, ball in sorted(assignment.items()):
|
||||
lines.append(f'LOCATE COMP "{sig}" SITE "{ball}";')
|
||||
lines.append(f'IOBUF PORT "{sig}" IO_TYPE=LVCMOS33;')
|
||||
lines.append("")
|
||||
open(path, "w").write("\n".join(lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else "synth/ecp5/spi_neuron_top.lpf"
|
||||
rows = load_balls(find_iodb())
|
||||
assignment = assign(rows)
|
||||
write_lpf(assignment, out_path)
|
||||
print(f"wrote {len(assignment)} signal constraints to {out_path}")
|
||||
Reference in New Issue
Block a user