From f4c13c65ebd7d5710aa771d371812d176a4830bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 12 Nov 2026 07:17:53 +0100 Subject: [PATCH 1/2] AUTO_MEMORIES: gen_memories.py writes blackboxes.txt once, last MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blackboxes.txt had two writers. FakeRAM's asap7 backend wrote a line per macro it built, then gen_memories.py appended a line per register file RAM. FakeRAM also ran before the listed register files were taken out of the inventory, so a module both idiomatic and listed in AUTO_MEMORIES_REGFILES got FakeRAM views it does not use, and blackboxes.txt still named the module with FakeRAM's area, beside its RAM when it was placed as a macro. gen_memories.py now resolves the listed register files before FakeRAM runs: their records replace the module's in memories.json, so FakeRAM, which builds a view for every idiomatic record, never sees them, and memories.json has one record per module from the start. Once every view exists it writes blackboxes.txt whole, one ` ` line per macro with the area its .lib states, and stops on a macro named twice. FakeRAM's backend writes only the views. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 4 ++ flow/scripts/memories/gen_memories.py | 83 +++++++++++++++------- flow/scripts/memories/gen_memories_test.py | 83 +++++++++++++++++++++- tools/FakeRAM2.0/orfs_asap7/generate.py | 9 +-- tools/FakeRAM2.0/test/spsram_mask_test.py | 7 +- 5 files changed, 148 insertions(+), 38 deletions(-) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 4fd8e4bb3a..06bbc1d7dd 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -185,6 +185,10 @@ cell flop DFFHQNx1_ASAP7_75t_R ... ``` +A listed module is a register file even where the idiomatic gate would +convert it: FakeRAM builds no view for it, and `memories.json` records +it once, as a register file. + `memories/regfile_ram.py` translates the spec to `generate_ram_netlist` options (ports, banks, folds, the zero word, reset, read latency, write style and priority, cells) and checks it against the module's ports, diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 6256bab2d4..4cab002582 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -22,6 +22,10 @@ the .lib states it — what synthesis blackboxes and the cost it gives each +FakeRAM writes the .lib/.lef of each idiomatic memory and generate_ram +those of each register file; this script alone writes the lists, once +every view exists. + Everything downstream consumes these files; nothing else is passed between the generator and the flow. """ @@ -93,13 +97,20 @@ def _spec_mode(spec: Path) -> str: def _lib_area(lib: Path) -> str: """The cell area `lib` states, in um^2: what blackboxes.txt gives a - register file beside its name, as FakeRAM does for its macros.""" + macro beside its name.""" m = re.search(r"^\s*area\s*:\s*([0-9.eE+-]+)\s*;", lib.read_text(), re.M) if not m: raise SystemExit(f"gen_memories: {lib} states no area") return m.group(1) +def _shape(spec: Path, module: str, macro_place: set[str]) -> str: + """How a register file stands in for its module: its spec's mode when + AUTO_MEMORIES_MACRO_PLACE names it, else inline.""" + mode = _spec_mode(spec) + return mode if module in macro_place else "inline" + + def _regfile_memory(spec: Path, module: str, reason: str) -> schema.Memory: """A register file's memories.json record, its shape from its spec.""" mem = schema.Memory(name=module, kind="regfile", source="listed", spec=str(spec)) @@ -115,13 +126,43 @@ def _regfile_memory(spec: Path, module: str, reason: str) -> schema.Memory: return mem +def regfile_memories( + specs: list[Path], macro_place: list[str] | None = None +) -> list[schema.Memory]: + """The memories.json record of every listed register file.""" + macro_place = set(macro_place or []) + memories = [] + for spec in specs: + module = _spec_module(spec) + shape = _shape(spec, module, macro_place) + memories.append( + _regfile_memory(spec, module, f"register file, generate_ram, {shape}") + ) + return memories + + +def write_blackboxes(out_dir: Path, macros: list[str]) -> None: + """blackboxes.txt: each macro once, with the area its .lib states.""" + areas: dict[str, str] = {} + for name in macros: + if name in areas: + raise SystemExit( + f"gen_memories: two macros are named {name}, and the " + "second's views overwrote the first's" + ) + areas[name] = _lib_area(out_dir / f"{name}.lib") + (out_dir / "blackboxes.txt").write_text( + "".join(f"{name} {area}\n" for name, area in areas.items()) + ) + + def run_regfiles( specs: list[Path], openroad: str | None, lefs: list[Path], out_dir: Path, macro_place: list[str] | None = None, -) -> list[schema.Memory]: +) -> list[str]: """Generate every listed register file with OpenROAD's generate_ram, through regfile_ram.py: a wrapper in the RTL module's place connects its ports to the generated RAM's. @@ -136,7 +177,8 @@ def run_regfiles( A register file named in AUTO_MEMORIES_MACRO_PLACE is a macro to synthesis and to macro placement: its abstract LEF and model liberty land beside the FakeRAM views and the - module joins blackboxes.txt. In `mode netlist` it then dissolves into + RAM it wraps is one of the macros returned, for blackboxes.txt. In + `mode netlist` it then dissolves into its placed cells after macro placement (regfile_dissolve.tcl), which reads its structural netlist and the placement of its core (.v, .place) and finds it in regfiles.txt. A spec whose ports @@ -172,19 +214,14 @@ def run_regfiles( + ", which no AUTO_MEMORIES_REGFILES spec builds; it lists " + " ".join(modules) ) - memories = [] dissolve = [] cores = [] inline = [] for spec, module in zip(specs, modules): - mode = _spec_mode(spec) # the RTL module's place taken by a wrapper around the RAM - shape = mode if module in macro_place else "inline" + shape = _shape(spec, module, macro_place) regfile_ram.check(spec) ram = regfile_ram.generate(spec, module, shape, openroad, lefs, out_dir) - memories.append( - _regfile_memory(spec, module, f"register file, generate_ram, {shape}") - ) inline.append(module) if ram["macro"]: cores.append(ram["block"]) @@ -194,17 +231,10 @@ def run_regfiles( f"gen_memories: {module} -> register file, generate_ram, " f"{shape} ({spec.name})\n" ) - with (out_dir / "blackboxes.txt").open("a") as f: - f.write( - "".join( - f"{name} {_lib_area(out_dir / f'{name}.lib')}\n" - for name in [m.name for m in memories if m.name not in inline] + cores - ) - ) (out_dir / "regfiles.txt").write_text("".join(f"{m}\n" for m in dissolve)) (out_dir / "inline.txt").write_text("".join(f"{m}\n" for m in inline)) (out_dir / "regfile_cores.txt").write_text("".join(f"{m}\n" for m in cores)) - return memories + return cores def run( @@ -249,6 +279,13 @@ def run( if mem.name in override_names: mem.source = "override" + # A listed register file is that, whatever the detector made of its + # module: its record replaces the module's before FakeRAM, which builds + # views for every idiomatic record of memories.json, reads it. + regfiles = regfile_memories(regfile_specs or [], macro_place) + listed = {m.name for m in regfiles} + memories = [m for m in memories if m.name not in listed] + regfiles + out_dir.mkdir(parents=True, exist_ok=True) json_path.parent.mkdir(parents=True, exist_ok=True) schema.dump(memories, json_path, platform) @@ -268,15 +305,13 @@ def run( ] ) - # After FakeRAM has written blackboxes.txt for the inferred memories, - # and recorded in memories.json beside them. - regfiles = run_regfiles( + cores = run_regfiles( regfile_specs or [], openroad, lefs or [], out_dir, macro_place ) - if regfiles: - listed = {m.name for m in regfiles} - memories = [m for m in memories if m.name not in listed] + regfiles - schema.dump(memories, json_path, platform) + + # once every view exists: the macros FakeRAM built, then the register + # files' RAMs + write_blackboxes(out_dir, [m.name for m in converted] + cores) for mem in sorted(memories, key=lambda m: m.name): verdict = mem.kind diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index 23dc6f276f..0f217c23e5 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -5,6 +5,7 @@ import tempfile import unittest from pathlib import Path +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -61,6 +62,16 @@ } } +# a register-file spec for the idiomatic mem_128x32 +REGFILE_SPEC = """\ +module mem_128x32 +words 128 +bits 32 +clock clk +read ra rd +write wa wd we +""" + FORCE_TAGS = """\ { "version": 1, @@ -123,7 +134,7 @@ def test_flow_contract(self): ], ) # one ` ` line; the area is the one the .lib states, - # which synthesis costs the blackbox by + # as it states it, which synthesis costs the blackbox by [line] = (mems_dir / "blackboxes.txt").read_text().splitlines() name, area = line.split() self.assertEqual(name, "mem_128x32") @@ -132,9 +143,77 @@ def test_flow_contract(self): (mems_dir / "mem_128x32.lib").read_text(), re.M, ).group(1) - self.assertEqual(float(area), float(lib_area)) + self.assertEqual(area, lib_area) self.assertGreater(float(area), 0) + def run_with_regfile(self, macro_place): + """mem_128x32, idiomatic, also listed as a register file; a stub + stands in for generate_ram and writes the RAM's .lib.""" + d = Path(self.tmp.name) + netlist_json = d / "memories_inferred.json" + netlist_json.write_text(json.dumps(SAMPLE_YOSYS_NETLIST)) + spec = d / "mem_128x32.regfile" + spec.write_text(REGFILE_SPEC) + + def generate(spec, module, shape, openroad, lefs, out_dir): + block = f"{module}_ram" + if shape != "inline": + (out_dir / f"{block}.lib").write_text( + f"library ({block}) {{\n cell ({block}) {{\n" + " area : 42.5;\n }\n}\n" + ) + return { + "block": block, + "macro": shape != "inline", + "dissolve": shape == "netlist", + } + + with mock.patch.object(gen_memories.regfile_ram, "generate", generate): + code = gen_memories.run( + netlist_json, + [], + "asap7", + d / "memories", + d / "memories.json", + regfile_specs=[spec], + openroad="openroad", + lefs=[d / "tech.lef"], + macro_place=macro_place, + ) + self.assertEqual(code, 0) + return d + + def test_listed_regfile_is_not_a_fakeram(self): + # a module both idiomatic and listed is a register file, once: no + # FakeRAM views, one memories.json record, and blackboxes.txt + # names the RAM that stands in for it, never the module + for macro_place, blackboxes in ( + ([], ""), + (["mem_128x32"], "mem_128x32_ram 42.5\n"), + ): + with self.subTest(macro_place=macro_place): + d = self.run_with_regfile(macro_place) + mems_dir = d / "memories" + for view in ("mem_128x32.lib", "mem_128x32_pre_layout.lib"): + self.assertFalse((mems_dir / view).exists(), view) + self.assertFalse((mems_dir / "mem_128x32.lef").exists()) + self.assertEqual((mems_dir / "blackboxes.txt").read_text(), blackboxes) + doc = json.loads((d / "memories.json").read_text()) + records = [m for m in doc["memories"] if m["name"] == "mem_128x32"] + self.assertEqual(len(records), 1) + self.assertEqual(records[0]["kind"], "regfile") + self.assertFalse(records[0]["idiomatic"]) + + def test_blackboxes_names_each_macro_once(self): + # two macros of one name would share one set of views + d = Path(self.tmp.name) + (d / "ram.lib").write_text("cell (ram) {\n area : 1.0;\n}\n") + gen_memories.write_blackboxes(d, ["ram"]) + self.assertEqual((d / "blackboxes.txt").read_text(), "ram 1.0\n") + with self.assertRaises(SystemExit) as stop: + gen_memories.write_blackboxes(d, ["ram", "ram"]) + self.assertIn("two macros are named ram", str(stop.exception)) + def test_additional_memories_forces_conversion(self): force = Path(self.tmp.name) / "force.memories" force.write_text(FORCE_TAGS) diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index 7e877dba10..2100490a3e 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -11,6 +11,9 @@ A memory whose pins were synthesized by the detector rather than read off the module (port_convention "") has no such names and gets FakeRAM's historical single-port pins, which is what earlier ORFS releases emitted. + +Only the views are written here; gen_memories.py, which runs this, lists +the macros in blackboxes.txt once every view exists. """ import json @@ -129,7 +132,6 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): process = Process(ASAP7_PROCESS_CONFIG) timing_data = TimingData(ASAP7_TIMING_CONFIG) - blackboxes = [] for m in converted: name = m["name"] bits = int(m.get("bits", 32)) @@ -174,9 +176,4 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): with open(lef_path, "w") as f: LefExporter(ram).export(f) - # the area the .lib states, beside the name: synthesis costs the - # blackbox by it - blackboxes.append(f"{name} {ram.physical.get_area(False):.6f}\n") - - (out_dir / "blackboxes.txt").write_text("".join(blackboxes)) return 0 diff --git a/tools/FakeRAM2.0/test/spsram_mask_test.py b/tools/FakeRAM2.0/test/spsram_mask_test.py index a04a9b3910..06696bd06f 100755 --- a/tools/FakeRAM2.0/test/spsram_mask_test.py +++ b/tools/FakeRAM2.0/test/spsram_mask_test.py @@ -210,24 +210,19 @@ def test_emits_views_named_by_the_module(self): inv.write_text(json.dumps(inventory)) out = Path(d) / "memories" self.assertEqual(run_orfs_asap7("asap7", out, inv), 0) + # the views only: gen_memories.py writes blackboxes.txt self.assertEqual( sorted(p.name for p in out.iterdir()), [ "array_64x114.lef", "array_64x114.lib", "array_64x114_pre_layout.lib", - "blackboxes.txt", ], ) lib = (out / "array_64x114.lib").read_text() self.assertIn("cell(array_64x114)", lib) self.assertIn("bus(RW0_wmask)", lib) self.assertIn("pin(RW0_en)", lib) - # ` `: synthesis costs the blackbox by its area - [line] = (out / "blackboxes.txt").read_text().splitlines() - module, area = line.split() - self.assertEqual(module, "array_64x114") - self.assertGreater(float(area), 0) def test_declared_separate_ports_are_refused(self): pins = rw0_pins(32, 5, 0) From 2260790cbe30e2d7112f3903689918b5acb4f6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 12 Nov 2026 07:17:53 +0100 Subject: [PATCH 2/2] AUTO_MEMORIES: one reader for the lists, helpers in util.tcl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With one writer, blackboxes.txt cannot hold a malformed or repeated line, so auto_memories_areas drops its format check: the ` ` words are the dict. auto_memories_areas and auto_memories_names read through one helper, auto_memories_words. auto_memories_blackboxes moves from synth_preamble.tcl to util.tcl beside its siblings, and read_liberty.tcl's comment no longer mentions the _pre_layout views, which no step reads. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/read_liberty.tcl | 4 +-- flow/scripts/synth_preamble.tcl | 11 ++----- flow/scripts/util.tcl | 54 ++++++++++++++------------------- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/flow/scripts/read_liberty.tcl b/flow/scripts/read_liberty.tcl index 52c8e0c01a..cee87a777a 100644 --- a/flow/scripts/read_liberty.tcl +++ b/flow/scripts/read_liberty.tcl @@ -18,9 +18,7 @@ if { [env_var_exists_and_non_empty CORNERS] } { } # AUTO_MEMORIES: liberty views generated pre-synthesis by -# scripts/memories/gen_memories.py, named by blackboxes.txt. The -# _pre_layout variants (ideal clock) are for pre-CTS consumers that -# select lib files themselves. +# scripts/memories/gen_memories.py, named by blackboxes.txt. foreach libFile [auto_memories_views .lib] { if { [env_var_exists_and_non_empty CORNERS] } { foreach corner $::env(CORNERS) { diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 1439b46571..950daa3780 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -43,19 +43,12 @@ proc read_checkpoint { file } { } } -# AUTO_MEMORIES: memory modules whose generated liberty view replaces -# their behavioral body during synthesis, each with that view's area in -# um^2. The list is produced by scripts/memories/gen_memories.py before -# canonicalization, one ` ` per line; see -# docs/user/AutoMemories.md. +# AUTO_MEMORIES: what synthesis does to the modules util.tcl's +# auto_memories_blackboxes names; see docs/user/AutoMemories.md. source $::env(SCRIPTS_DIR)/memories/check_blackboxed.tcl source $::env(SCRIPTS_DIR)/memories/max_bits.tcl source $::env(SCRIPTS_DIR)/memories/drop_parameters.tcl -proc auto_memories_blackboxes { } { - return [dict keys [auto_memories_areas]] -} - proc read_auto_memories_inline { } { foreach m [auto_memories_names inline.txt] { # A parameterized RTL module (ibex_register_file_ff #(...)) is diff --git a/flow/scripts/util.tcl b/flow/scripts/util.tcl index 372bb32fec..9a1768b283 100644 --- a/flow/scripts/util.tcl +++ b/flow/scripts/util.tcl @@ -114,33 +114,30 @@ proc env_var_exists_and_non_empty { env_var } { # AUTO_MEMORIES: do-auto-memories writes into $RESULTS_DIR/memories the # lists below, and every later step reads the files they name, never -# what a directory happens to hold. -# -# blackboxes.txt, ` ` a line: the macros, each with its -# .lib and .lef, and the area synthesis costs it by. -proc auto_memories_areas { } { +# what a directory happens to hold. gen_memories.py writes each list +# whole, after every view exists. + +# The words of one of those lists; none without AUTO_MEMORIES. +proc auto_memories_words { list } { if { ![env_var_equals AUTO_MEMORIES 1] } { return {} } - set f "$::env(RESULTS_DIR)/memories/blackboxes.txt" - set fh [open $f r] - set content [string map {\r ""} [read $fh]] + set fh [open "$::env(RESULTS_DIR)/memories/$list" r] + set content [read $fh] close $fh - set areas [dict create] - foreach line [split $content "\n"] { - if { [string trim $line] == "" } { - continue - } - set fields [regexp -all -inline {\S+} $line] - if { - [llength $fields] != 2 - || ![string is double -strict [lindex $fields 1]] - } { - error "$f: expected ` `, got `$line`" - } - dict set areas {*}$fields - } - return $areas + return [regexp -all -inline {\S+} $content] +} + +# blackboxes.txt, ` ` a line: the macros, each with its +# .lib and .lef, and the area synthesis costs it by. Its +# words are a dict of the area of each module. +proc auto_memories_areas { } { + return [auto_memories_words blackboxes.txt] +} + +# The modules synthesis blackboxes, the macros of blackboxes.txt. +proc auto_memories_blackboxes { } { + return [dict keys [auto_memories_areas]] } # A list of module names, one a line: @@ -151,19 +148,12 @@ proc auto_memories_areas { } { # - regfiles.txt: register files that dissolve into their cells after # macro placement (.v, .place). proc auto_memories_names { list } { - if { ![env_var_equals AUTO_MEMORIES 1] } { - return {} - } - set f "$::env(RESULTS_DIR)/memories/$list" - set fh [open $f r] - set content [read $fh] - close $fh - return [regexp -all -inline {\S+} $content] + return [auto_memories_words $list] } # The view of every macro in blackboxes.txt, by extension (.lib, .lef). proc auto_memories_views { ext } { - return [lmap m [dict keys [auto_memories_areas]] { + return [lmap m [auto_memories_blackboxes] { file join $::env(RESULTS_DIR) memories $m$ext }] }