From 7e4dd4ffdca76bd233b2d91b284e776aa93d9468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 6 Oct 2026 18:06:01 +0200 Subject: [PATCH 1/9] AUTO_MEMORIES: a macro's blackbox costs its generated view's area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With SYNTH_MINIMUM_KEEP_SIZE, synthesis decides which modules to keep with keep_hierarchy -min_cost, which needs a cost for every instanced blackbox. convert_liberty_areas gives Liberty cells one, as a gate equivalent of their area, but an AUTO_MEMORIES macro is a blackbox made from the RTL's own module and has none: any AUTO_MEMORIES design with a keep threshold stopped with "Missing cost information on instanced blackbox". asap7/riscv32i is such a design once its register file is a macro (AUTO_MEMORIES_REGFILES). Each AUTO_MEMORIES blackbox now gets the gate equivalent of the area its generated .lib states. The view is not read in the module's place: its pins need not be the module's (FakeRAM names its own). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 53353ee46a..904bcc1824 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -248,4 +248,30 @@ proc convert_liberty_areas { } { set gate_eq [expr int($area / $found_cell_area)] rtlil::set_attr -mod -uint $box gate_cost_equivalent $gate_eq } + + # An AUTO_MEMORIES macro is a blackbox from the RTL's own module, not a + # Liberty cell: it costs what its generated view's area says, or + # keep_hierarchy -min_cost refuses it ("Missing cost information on + # instanced blackbox"). Its view is not read in its place, as its pins + # need not be the module's (FakeRAM uses its own names). + foreach m [auto_memories_blackboxes] { + set lib "$::env(RESULTS_DIR)/memories/$m.lib" + if { ![file exists $lib] } { + error "AUTO_MEMORIES macro $m has no $lib" + } + set fh [open $lib r] + set text [read $fh] + close $fh + # the cell group of and its area; the brace characters spelled as + # hex escapes, which keep the brace count of this body balanced + set pat "cell\\s*\\(\\s*\"?[string map {$ \\$} $m]\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" + if { ![regexp $pat $text -> area] } { + error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" + } + if { [catch {rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }]}] } { + # not in the design: removed as unused, or never instantiated + continue + } + } } From 96c50087eacecb06283d9acf6c0609d1e265d758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 07:52:42 +0200 Subject: [PATCH 2/9] AUTO_MEMORIES: tclfmt the blackbox cost loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 904bcc1824..68a47d23be 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -268,8 +268,12 @@ proc convert_liberty_areas { } { if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } - if { [catch {rtlil::set_attr -mod -uint $m gate_cost_equivalent \ - [expr { int($area / $found_cell_area) }]}] } { + if { + [catch { + rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }] + }] + } { # not in the design: removed as unused, or never instantiated continue } From 1098c5576d8ff95dc60cacca723470dfa91c58eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 07:52:49 +0200 Subject: [PATCH 3/9] AUTO_MEMORIES: escape the module name in the blackbox area pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only `$` was escaped; yosys and slang module names also carry `\`, `[` and `.`, each a regular-expression metacharacter. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 68a47d23be..a82ae3c985 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -263,8 +263,11 @@ proc convert_liberty_areas { } { set text [read $fh] close $fh # the cell group of and its area; the brace characters spelled as - # hex escapes, which keep the brace count of this body balanced - set pat "cell\\s*\\(\\s*\"?[string map {$ \\$} $m]\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" + # hex escapes, which keep the brace count of this body balanced. The + # module name is escaped whole: yosys and slang names carry `$`, `\`, + # `[` and `.`, each a regular-expression metacharacter. + set name [regsub -all {[][\x7b\x7d()*+?.\\^$|]} $m {\\&}] + set pat "cell\\s*\\(\\s*\"?$name\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } From 8411e9c426f67e9c634c66737488246825805686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:44:30 +0200 Subject: [PATCH 4/9] AUTO_MEMORIES: a listed module present but not blackboxed is an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop caught every set_attr failure as "not in the design", so a listed module whose name did not match its definition went unnoticed. slang's --keep-hierarchy names each elaborated module `$` and --blackboxed-module matches only the definition, so such a name is in the design with its body elaborated: set_attr succeeded on it and the memory became flip-flops. A listed module absent from the design is skipped (slang imports a blackboxed module only where it is instanced); one present but not a blackbox stops synthesis by name; set_attr is no longer caught. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index a82ae3c985..98bd58368b 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -254,7 +254,18 @@ proc convert_liberty_areas { } { # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). + set modules [tee -q -s result.string select -list-mod =*] foreach m [auto_memories_blackboxes] { + if { [lsearch -exact $modules $m] < 0 } { + # slang imports a blackboxed module only where it is instanced + continue + } + if { ![rtlil::has_attr -mod $m blackbox] } { + # e.g. slang's per-instance `$`, which + # --blackboxed-module does not match: the body was elaborated + error "AUTO_MEMORIES macro $m is in the design but was not\ + blackboxed; its name does not match the module's definition" + } set lib "$::env(RESULTS_DIR)/memories/$m.lib" if { ![file exists $lib] } { error "AUTO_MEMORIES macro $m has no $lib" @@ -271,14 +282,7 @@ proc convert_liberty_areas { } { if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } - if { - [catch { - rtlil::set_attr -mod -uint $m gate_cost_equivalent \ - [expr { int($area / $found_cell_area) }] - }] - } { - # not in the design: removed as unused, or never instantiated - continue - } + rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }] } } From a3305f118bfc404097b24ce280968a6bd824d13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:45:25 +0200 Subject: [PATCH 5/9] AUTO_MEMORIES: blackboxes.txt carries each macro's area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop read a macro's area out of its .lib with a regular expression that matched only because FakeRAM writes `area :` first in the cell group. FakeRAM knows the area when it writes the view, so it now writes it beside the module name, ` ` in um^2 per line, and synthesis reads it from there. A line of any other shape is an error. gen_memories_test.py checks the area against the one the .lib states. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 2 +- flow/scripts/memories/gen_memories.py | 5 ++- flow/scripts/memories/gen_memories_test.py | 19 ++++++++- flow/scripts/synth_preamble.tcl | 47 ++++++++++++---------- tools/FakeRAM2.0/orfs_asap7/generate.py | 9 +++-- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 8bc25c1d77..0ae2672e7c 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -25,7 +25,7 @@ into `$(RESULTS_DIR)/memories_inferred.json`, and | `$(RESULTS_DIR)/memories/.lib` | Generated Liberty view per converted memory. | | `$(RESULTS_DIR)/memories/_pre_layout.lib` | Ideal-clock (zero clock-tree insertion) variant for pre-CTS consumers that select lib files themselves. The Makefile flow uses `.lib` throughout. | | `$(RESULTS_DIR)/memories/.lef` | Abstract LEF per converted memory. | -| `$(RESULTS_DIR)/memories/blackboxes.txt` | Names of the converted modules — what synthesis blackboxes. | +| `$(RESULTS_DIR)/memories/blackboxes.txt` | ` ` per converted module — what synthesis blackboxes, and the area in um² its `.lib` states, which `SYNTH_MINIMUM_KEEP_SIZE` costs it by. | Synthesis (canonicalization) blackboxes the converted modules so the liberty view wins over their behavioral bodies; floorplan through final diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 9d179d7908..e6bfd2a2bc 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -10,8 +10,9 @@ /.lib Liberty view per converted memory /_pre_layout.lib ideal-clock variant for pre-CTS consumers /.lef abstract LEF per converted memory - /blackboxes.txt converted module names, one per line — - what synthesis blackboxes + /blackboxes.txt ` ` per line, area in um^2 as + the .lib states it — what synthesis + blackboxes and the cost it gives each Everything downstream consumes these files; nothing else is passed between the generator and the flow. diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index ffd02593d9..74a5929a0f 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import re import sys import tempfile import unittest @@ -118,14 +119,28 @@ def test_flow_contract(self): "mem_128x32_pre_layout.lib", ], ) - self.assertEqual((mems_dir / "blackboxes.txt").read_text(), "mem_128x32\n") + # one ` ` line; the area is the one the .lib states, + # which synthesis costs the blackbox by + [line] = (mems_dir / "blackboxes.txt").read_text().splitlines() + name, area = line.split() + self.assertEqual(name, "mem_128x32") + lib_area = re.search( + r"^\s*area\s*:\s*([0-9.]+)\s*;", + (mems_dir / "mem_128x32.lib").read_text(), + re.M, + ).group(1) + self.assertEqual(float(area), float(lib_area)) + self.assertGreater(float(area), 0) def test_additional_memories_forces_conversion(self): force = Path(self.tmp.name) / "force.memories" force.write_text(FORCE_TAGS) code, d = self.run_generator(memories_files=[force]) self.assertEqual(code, 0) - blackboxes = (d / "memories" / "blackboxes.txt").read_text().split() + blackboxes = [ + line.split()[0] + for line in (d / "memories" / "blackboxes.txt").read_text().splitlines() + ] self.assertEqual(sorted(blackboxes), ["cache_tags", "mem_128x32"]) self.assertTrue((d / "memories" / "cache_tags.lib").exists()) self.assertTrue((d / "memories" / "cache_tags.lef").exists()) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 98bd58368b..06a121c25e 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -44,10 +44,11 @@ proc read_checkpoint { file } { } # AUTO_MEMORIES: memory modules whose generated liberty view replaces -# their behavioral body during synthesis. The list is produced by -# scripts/memories/gen_memories.py before canonicalization; see +# 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. -proc auto_memories_blackboxes { } { +proc auto_memories_areas { } { if { ![env_var_equals AUTO_MEMORIES 1] } { return {} } @@ -59,7 +60,25 @@ proc auto_memories_blackboxes { } { set fh [open $f r] set content [string map {\r ""} [read $fh]] close $fh - return [regexp -all -inline {\S+} $content] + 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 +} + +proc auto_memories_blackboxes { } { + return [dict keys [auto_memories_areas]] } proc read_design_sources { } { @@ -250,12 +269,12 @@ proc convert_liberty_areas { } { } # An AUTO_MEMORIES macro is a blackbox from the RTL's own module, not a - # Liberty cell: it costs what its generated view's area says, or + # Liberty cell: it costs its generated view's area, or # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). set modules [tee -q -s result.string select -list-mod =*] - foreach m [auto_memories_blackboxes] { + dict for {m area} [auto_memories_areas] { if { [lsearch -exact $modules $m] < 0 } { # slang imports a blackboxed module only where it is instanced continue @@ -266,22 +285,6 @@ proc convert_liberty_areas { } { error "AUTO_MEMORIES macro $m is in the design but was not\ blackboxed; its name does not match the module's definition" } - set lib "$::env(RESULTS_DIR)/memories/$m.lib" - if { ![file exists $lib] } { - error "AUTO_MEMORIES macro $m has no $lib" - } - set fh [open $lib r] - set text [read $fh] - close $fh - # the cell group of and its area; the brace characters spelled as - # hex escapes, which keep the brace count of this body balanced. The - # module name is escaped whole: yosys and slang names carry `$`, `\`, - # `[` and `.`, each a regular-expression metacharacter. - set name [regsub -all {[][\x7b\x7d()*+?.\\^$|]} $m {\\&}] - set pat "cell\\s*\\(\\s*\"?$name\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" - if { ![regexp $pat $text -> area] } { - error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" - } rtlil::set_attr -mod -uint $m gate_cost_equivalent \ [expr { int($area / $found_cell_area) }] } diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index be30f8f927..42ac55a8a0 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -54,6 +54,7 @@ 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 = m.get("bits", 32) @@ -82,7 +83,9 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): with open(lef_path, "w") as f: LefExporter(ram).export(f) - (out_dir / "blackboxes.txt").write_text( - "".join(f"{m['name']}\n" for m in converted) - ) + # 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 From b59c1d1cfccfa56705b4398c00322d9779c8a446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:41:04 +0200 Subject: [PATCH 6/9] AUTO_MEMORIES: an escaped macro module gets its cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop looked each blackboxes.txt name up in the module list as a Tcl list. yosys lists an escaped module with its leading backslash, `\1d_ram`, which list parsing reads as an escape, and blackboxes.txt names it as read_slang takes it, `1d_ram`; the lookup missed, the module was taken for absent and skipped, and keep_hierarchy -min_cost stopped with "Missing cost information on instanced blackbox \1d_ram". The list is now read one name per line, without the backslash. Checked with read_slang on a design with `ram` and `\1d_ram` blackboxed: before, the error above; now both get their cost and keep_hierarchy -min_cost runs. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 06a121c25e..cfacc639dd 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -273,7 +273,16 @@ proc convert_liberty_areas { } { # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). - set modules [tee -q -s result.string select -list-mod =*] + # One name per line, not a Tcl list: yosys lists an escaped module with + # its leading backslash (`\1d_ram`), which list parsing reads as an + # escape, and blackboxes.txt names it as read_slang takes it (`1d_ram`). + set modules {} + foreach line [split [tee -q -s result.string select -list-mod =*] "\n"] { + set line [string trim $line] + if { $line ne "" } { + lappend modules [regsub {^\\} $line {}] + } + } dict for {m area} [auto_memories_areas] { if { [lsearch -exact $modules $m] < 0 } { # slang imports a blackboxed module only where it is instanced From 2776b6a8776344f4b217af1dcff8ef6a40d7198f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 6 Oct 2026 17:16:46 +0200 Subject: [PATCH 7/9] AUTO_MEMORIES_REGFILES: register files of placed standard cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A register file, many ports over few words, is not an SRAM: FakeRAM's single-port model does not describe it, and synthesised to flops its mux trees are placed by a placer that does not know they are an array. AUTO_MEMORIES_REGFILES lists spec files, one per RTL module. gen_memories.py runs OpenROAD's generate_regfile on each: it checks the spec against the module's ports and writes the cells (.v), an abstract whose pins sit where their connections land in the array (.lef) and a timing model (.lib, _pre_layout.lib) beside the FakeRAM views. The module joins blackboxes.txt with the area its .lib states, so synthesis and macro placement see a macro that SYNTH_MINIMUM_KEEP_SIZE can cost, and memories.json records it with kind `regfile` (the others get `fakeram` or `flops`). In `mode netlist` the macro dissolves into its cells at the end of macro placement (regfile_dissolve.tcl): the core FIRM where the placer put the macro, flipped as it was, row parity kept; the address decode left to global placement and the resizer; the core's own cells and nets dont_touch except the flops (CTS rewires them) and the cells on the core's boundary (repair_design buffers their nets); then eliminate_dead_logic, as at synthesis. `mode macro` keeps the macro. designs/asap7/regfile is the smallest example: one 16x8 2R2W file in a parent of flops, seconds to global route. Moving parts into OpenROAD, the dissolve in particular, is left for later: faster, unit-testable, maintained with the database it edits. Needs OpenROAD's generate_regfile: https://github.com/The-OpenROAD-Project/OpenROAD/pull/11669 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 105 ++++++ docs/user/FlowVariables.md | 2 + flow/Makefile | 8 +- flow/designs/asap7/regfile/RegFile.regfile | 22 ++ flow/designs/asap7/regfile/config.mk | 17 + flow/designs/asap7/regfile/constraint.sdc | 7 + flow/designs/asap7/riscv32i-regfile/config.mk | 13 + flow/designs/asap7/riscv32i-regfile/io.tcl | 1 + .../asap7/riscv32i-regfile/regfile.regfile | 27 ++ flow/designs/src/regfile/regfile.v | 60 +++ flow/scripts/macro_place.tcl | 3 + flow/scripts/memories/gen_memories.py | 229 +++++++++++- flow/scripts/memories/gen_memories_test.py | 20 + flow/scripts/memories/schema.py | 16 +- flow/scripts/regfile_dissolve.tcl | 352 ++++++++++++++++++ flow/scripts/variables.json | 6 + flow/scripts/variables.yaml | 17 + 17 files changed, 900 insertions(+), 5 deletions(-) create mode 100644 flow/designs/asap7/regfile/RegFile.regfile create mode 100644 flow/designs/asap7/regfile/config.mk create mode 100644 flow/designs/asap7/regfile/constraint.sdc create mode 100644 flow/designs/asap7/riscv32i-regfile/config.mk create mode 100644 flow/designs/asap7/riscv32i-regfile/io.tcl create mode 100644 flow/designs/asap7/riscv32i-regfile/regfile.regfile create mode 100644 flow/designs/src/regfile/regfile.v create mode 100644 flow/scripts/regfile_dissolve.tcl diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 0ae2672e7c..c9c018a867 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -126,6 +126,110 @@ the left edge, alternating horizontal M4 `VDD`/`VSS` straps across the macro (which the platform's PDN macro grid connects to M5), and an `OBS` covering M1 to M4. +## Register files + +A register file, many read and write ports over a few words, is no +SRAM: FakeRAM's single-port model does not describe it, and synthesised +to flops its mux trees are placed by a placer that does not know they +are an array. `AUTO_MEMORIES_REGFILES` lists spec files, one per RTL +module, and OpenROAD's `generate_regfile` builds each as an array of +placed standard cells: a flop per bit, its write mux beside it, the read +trees in columns. The spec names the module, its words and bits, its +read and write ports by the module's own port names, and the cells: + +``` +module RegFile +mode netlist +words 16 +bits 8 +clock clock +read io_r0_addr io_r0_data +write io_w0_addr io_w0_data io_w0_en +cell flop DFFHQNx1_ASAP7_75t_R +... +``` + +`generate_regfile` checks the spec against the module's ports, then +writes `.v` (the cells), `.lef` (an abstract, its pins where +their connections land in the array) and `.lib`/`_pre_layout.lib` +(a timing model) beside the FakeRAM views. The module joins +`blackboxes.txt`, so synthesis and macro placement see a macro. + +In `mode netlist` (the usual one) the macro dissolves into its cells at +the end of macro placement (`regfile_dissolve.tcl`): the array's core +lands FIRM where the macro was placed, flipped as the macro was, on the +parent's rows; the address decode is left to global placement and the +resizer, and dead logic is eliminated as at synthesis. From there on it +is standard cells. `mode macro` keeps the macro to the end. + +Listed register files appear in `memories.json` with kind `regfile`; +the rest have kind `fakeram` or `flops`. `designs/asap7/regfile` is the +smallest example. + +Moving parts of this into OpenROAD, the dissolve in particular, is left +for later: it would make them faster, testable at unit level and +maintained with the database they edit. + +### Designs to measure it on + +What a register file of placed cells buys is a shorter minimum clock +period (the read mux trees are columns, not a cloud) and a shorter +build (fewer cells for synthesis, placement and the resizer to work +on). The ORFS designs whose register file is flip-flops today and its +own module, the seam a spec needs, measured as flops against +`AUTO_MEMORIES_REGFILES`: + +| Design | Module | Today | Shape | Ready? | +|---|---|---|---|---| +| asap7/riscv32i | `regfile` | flip-flops: `reg [31:0] rf[31:0]`, mapped to flops (no AUTO_MEMORIES) | 32 x 32, 2R1W, x0 reads 0 | Yes: the ports are the spec's (`zero_word 0`). Also exercises `OPENROAD_HIERARCHICAL=1` beside two FakeRAM macros. | +| asap7/ibex | `ibex_register_file_ff` | flip-flops: `RegFile = RegFileFF`, the default | 32 x 32, 2R1W, x0 reads 0 | Not yet: `test_en_i` and `dummy_instr_id_i` are ports the array does not use, which a spec cannot yet name, and `rst_ni` resets the flops to 0 where the generator's flops have no reset (the ISA leaves x1-x31 undefined after reset). | +| asap7/cva6 | `ariane_regfile` (`ariane_regfile_ff.sv`) | flip-flops: cv32a65x has `FpgaEn` 0 | 32 x 32, 2R1W | Not yet: the ports are packed arrays (`raddr_i[1:0][4:0]`), one port per slice, which a spec cannot yet address; asynchronous reset as ibex. | +| asap7/swerv_wrapper | `dec_gpr_ctl_*` | flip-flops | 32 x 32, 4R3W | Not yet: read enables, bank ids and scan ports around the array need a seam in the RTL. | + +coralnpu's `Regfile` (8R4W) and `FRegfile` (3R2W) carry their +scoreboard and bypass logic in the same module, and picorv32 and +tinyRocket keep their register file inside the CPU module: those need a +seam in their RTL first. + +`designs/asap7/regfile` is the smallest case, for turnaround rather +than measurement. + +### Status: a prototype, slower and bigger on riscv32i + +`designs/asap7/riscv32i-regfile` is riscv32i with its register file +generated; the study case for what the generator gets wrong, because it +reaches global route in minutes. To global route, against +`designs/asap7/riscv32i`: + +| | flops | register file | +|---|---|---| +| minimum period (950 ps clock, global-route parasitics) | 954 ps | 998 ps | +| place to global route, stage time | 211 s | 536 s | +| core area | 4 012 um2 (62 %) | 7 573 um2 (45 %) | +| standard-cell area, without taps | 1 284 um2 | 1 413 um2 | +| failing endpoints after global route | 79 | 992 | + +Synthesis takes a second either way: a 32 x 32 file is too small for +its synthesis time to show, which is where the larger files of a large +core are expected to gain. + +Why it is slower: every failing endpoint is a flop of the array, all +within 50 ps, and the worst path runs from the instruction through the +array's read path -- the address decode as three serial AND2 stages, +the word select, five levels of OR2 -- into the ALU and back to the +array's write mux: 232 ps from `instr` to read data. The flops +version's same loop closes. `repair_timing` then spends its time at CTS +and global route on 992 endpoints it cannot fix. + +Why it is bigger: standard-cell area is within a tenth of the flops', +and the array's cells fill 58 % of its box. The core is bigger because +`riscv32i-regfile/config.mk` sets 45 % utilisation: at riscv32i's 62 % +no macro placement fits the array beside the four FakeRAMs. + +The rest is the generator's to fix: decode as a tree sized for its +fanout, a read tree of AO22 and NAND/NOR rather than AND2 and OR2, +cells the resizer may swap in place. + ## Platform support **asap7 only.** `gen_memories.py` rejects any other platform. The @@ -187,3 +291,4 @@ bazel-orfs implements this. - [AUTO_MEMORIES](FlowVariables.md#AUTO_MEMORIES) - [ADDITIONAL_MEMORIES](FlowVariables.md#ADDITIONAL_MEMORIES) +- [AUTO_MEMORIES_REGFILES](FlowVariables.md#AUTO_MEMORIES_REGFILES) diff --git a/docs/user/FlowVariables.md b/docs/user/FlowVariables.md index dfb6ab40f2..7fa1cbdd5d 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -103,6 +103,7 @@ configuration file. | ADDITIONAL_SITES| Passed as -additional_sites to initialize_floorplan.| | | ASAP7_USE_VT| A space separated list of VT options to use with the ASAP7 standard cell library: RVT, LVT, SLVT.| RVT| | AUTO_MEMORIES| Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and — when idiomatic as an SRAM macro — given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.| 0| +| AUTO_MEMORIES_REGFILES| Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like an AUTO_MEMORIES macro and placed as one. A spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.| | | BALANCE_ROWS| Balance rows during placement.| 0| | BLOCKS| Blocks used as hard macros in a hierarchical flow. Do note that you have to specify block-specific inputs file in the directory mentioned by Makefile.| | | BUFFER_PORTS_ARGS| Specify arguments to the buffer_ports call during placement. Only used if DONT_BUFFER_PORTS=0.| | @@ -342,6 +343,7 @@ configuration file. - [ABC_LOAD_IN_FF](#ABC_LOAD_IN_FF) - [ADDER_MAP_FILE](#ADDER_MAP_FILE) - [ADDITIONAL_MEMORIES](#ADDITIONAL_MEMORIES) +- [AUTO_MEMORIES_REGFILES](#AUTO_MEMORIES_REGFILES) - [CACHED_REPORTS](#CACHED_REPORTS) - [CLKGATE_MAP_FILE](#CLKGATE_MAP_FILE) - [DFF_LIB_FILE](#DFF_LIB_FILE) diff --git a/flow/Makefile b/flow/Makefile index 212474ba84..53d9dc192d 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -277,10 +277,14 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json --yosys-json $(RESULTS_DIR)/memories_inferred.json \ --out-dir $(RESULTS_DIR)/memories \ --json $(RESULTS_DIR)/memories.json \ - $(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) + $(foreach file,$(VERILOG_FILES),--verilog $(file)) \ + $(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) \ + $(foreach file,$(AUTO_MEMORIES_REGFILES),--regfile-spec $(file)) \ + $(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \ + $(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file)) $(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \ - $(wildcard $(SCRIPTS_DIR)/memories/*.py) + $(AUTO_MEMORIES_REGFILES) $(wildcard $(SCRIPTS_DIR)/memories/*.py) $(UNSET_AND_MAKE) do-auto-memories yosys-dependencies: $(RESULTS_DIR)/memories.json diff --git a/flow/designs/asap7/regfile/RegFile.regfile b/flow/designs/asap7/regfile/RegFile.regfile new file mode 100644 index 0000000000..b5f7ea063d --- /dev/null +++ b/flow/designs/asap7/regfile/RegFile.regfile @@ -0,0 +1,22 @@ +# RegFile as a register file of placed standard cells (generate_regfile). +# mode netlist: a macro through synthesis and macro placement, then its +# cells, the core FIRM where the macro was placed, the address decode +# left to placement. +module RegFile +mode netlist +words 16 +bits 8 +clock clock +read io_r0_addr io_r0_data +read io_r1_addr io_r1_data +write io_w0_addr io_w0_data io_w0_en +write io_w1_addr io_w1_data io_w1_en +cell flop DFFHQNx1_ASAP7_75t_R +cell and2 AND2x2_ASAP7_75t_R +cell or2 OR2x2_ASAP7_75t_R +cell ao22 AO22x2_ASAP7_75t_R +cell inv INVx1_ASAP7_75t_R +cell tap TAPCELL_ASAP7_75t_R +pin_layer M4 +tap_columns 8 +service_sites 40 diff --git a/flow/designs/asap7/regfile/config.mk b/flow/designs/asap7/regfile/config.mk new file mode 100644 index 0000000000..2880287d12 --- /dev/null +++ b/flow/designs/asap7/regfile/config.mk @@ -0,0 +1,17 @@ +# AUTO_MEMORIES_REGFILES in its smallest form: one register file, listed +# by spec, in a parent of flops. Seconds to global route. +export DESIGN_NICKNAME = regfile +export DESIGN_NAME = regfile_top +export PLATFORM = asap7 + +export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NICKNAME)/regfile.v +export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint.sdc + +export AUTO_MEMORIES = 1 +export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/RegFile.regfile + +# The file's macro is 28.5 x 10 um; a core sized by utilization alone +# would be square and narrower than the macro and its halo. +export DIE_AREA = 0 0 60 45 +export CORE_AREA = 2 2 58 43 +export PLACE_DENSITY = 0.5 diff --git a/flow/designs/asap7/regfile/constraint.sdc b/flow/designs/asap7/regfile/constraint.sdc new file mode 100644 index 0000000000..00013668d2 --- /dev/null +++ b/flow/designs/asap7/regfile/constraint.sdc @@ -0,0 +1,7 @@ +set clk_name clock +set clk_port_name clock +set clk_period 1000 +set in2reg_max 800 +set reg2out_max 800 +set in2out_max 600 +source $env(PLATFORM_DIR)/constraints.sdc diff --git a/flow/designs/asap7/riscv32i-regfile/config.mk b/flow/designs/asap7/riscv32i-regfile/config.mk new file mode 100644 index 0000000000..b31d984d8b --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/config.mk @@ -0,0 +1,13 @@ +# riscv32i with its register file built by AUTO_MEMORIES_REGFILES rather +# than synthesised to flip-flops: the same RTL and constraints. The study +# case for why a generated register file is slower and bigger than the +# flops it replaces -- minutes to global route, simple to build, and both +# are true of it today. docs/user/AutoMemories.md has the numbers. +export DESIGN_NICKNAME = riscv32i-regfile + +include designs/asap7/riscv32i/config.mk + +export CORE_UTILIZATION = 45 + +export AUTO_MEMORIES = 1 +export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/regfile.regfile diff --git a/flow/designs/asap7/riscv32i-regfile/io.tcl b/flow/designs/asap7/riscv32i-regfile/io.tcl new file mode 100644 index 0000000000..aee09d1e74 --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/io.tcl @@ -0,0 +1 @@ +exclude_io_pin_region -region left:* -region right:* -region top:* diff --git a/flow/designs/asap7/riscv32i-regfile/regfile.regfile b/flow/designs/asap7/riscv32i-regfile/regfile.regfile new file mode 100644 index 0000000000..af9ae34bb7 --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/regfile.regfile @@ -0,0 +1,27 @@ +# riscv32i's `regfile` (src/riscv32i/regfile.v) as a register file of +# placed standard cells: 32 x 32, two combinational reads, one write, +# x0 reads 0 and drops its writes. +module regfile +mode netlist +words 32 +bits 32 +clock clk +read ra1 rd1 +read ra2 rd2 +write wa3 wd3 we3 +zero_word 0 +write_style clock_gate +cell icg ICGx1_ASAP7_75t_R +# Flat, 58 x 19 um: it stacks under the four FakeRAMs at riscv32i's own +# 62 %, where two folds (32 x 36 um) fit beside them at no utilisation +# above 35 %. The clock tree's free sites are sized for a 32-word file. +bit_folds 1 +service_sites 16 +tap_columns 16 +cell flop DFFHQNx1_ASAP7_75t_R +cell and2 AND2x2_ASAP7_75t_R +cell or2 OR2x2_ASAP7_75t_R +cell ao22 AO22x2_ASAP7_75t_R +cell inv INVx1_ASAP7_75t_R +cell tap TAPCELL_ASAP7_75t_R +pin_layer M4 diff --git a/flow/designs/src/regfile/regfile.v b/flow/designs/src/regfile/regfile.v new file mode 100644 index 0000000000..43fdf1c675 --- /dev/null +++ b/flow/designs/src/regfile/regfile.v @@ -0,0 +1,60 @@ +// A 16 x 8 register file with two read ports and two write ports, +// behavioural, as RTL writes one. With AUTO_MEMORIES_REGFILES naming its +// spec the flow never synthesises this module: OpenROAD's +// generate_regfile builds it of placed standard cells instead. +module RegFile( + input clock, + input [3:0] io_r0_addr, + output [7:0] io_r0_data, + input [3:0] io_r1_addr, + output [7:0] io_r1_data, + input io_w0_en, + input [3:0] io_w0_addr, + input [7:0] io_w0_data, + input io_w1_en, + input [3:0] io_w1_addr, + input [7:0] io_w1_data +); + reg [7:0] mem [0:15]; + always @(posedge clock) begin + if (io_w0_en) mem[io_w0_addr] <= io_w0_data; + if (io_w1_en) mem[io_w1_addr] <= io_w1_data; + end + assign io_r0_data = mem[io_r0_addr]; + assign io_r1_data = mem[io_r1_addr]; +endmodule + +// The parent: registers around the file and some logic of its own. +// Bit 0 of the file is never read, so its column is dead logic the flow +// removes after the file dissolves into its cells. +module regfile_top( + input clock, + input [3:0] ra0, ra1, wa0, wa1, + input [7:0] wd0, wd1, + input we0, we1, + output [7:0] rd0, rd1, + output [7:0] sum +); + reg [3:0] ra0_q, ra1_q, wa0_q, wa1_q; + reg [7:0] wd0_q, wd1_q; + reg we0_q, we1_q; + wire [7:0] d0, d1; + always @(posedge clock) begin + ra0_q <= ra0; ra1_q <= ra1; wa0_q <= wa0; wa1_q <= wa1; + wd0_q <= wd0; wd1_q <= wd1; we0_q <= we0; we1_q <= we1; + end + RegFile u_rf( + .clock(clock), + .io_r0_addr(ra0_q), .io_r0_data(d0), + .io_r1_addr(ra1_q), .io_r1_data(d1), + .io_w0_en(we0_q), .io_w0_addr(wa0_q), .io_w0_data(wd0_q), + .io_w1_en(we1_q), .io_w1_addr(wa1_q), .io_w1_data(wd1_q) + ); + reg [6:0] rd0_q, rd1_q, sum_q; + always @(posedge clock) begin + rd0_q <= d0[7:1]; rd1_q <= d1[7:1]; sum_q <= d0[7:1] + d1[7:1]; + end + assign rd0 = {rd0_q, 1'b0}; + assign rd1 = {rd1_q, 1'b0}; + assign sum = {sum_q, 1'b0}; +endmodule diff --git a/flow/scripts/macro_place.tcl b/flow/scripts/macro_place.tcl index 0789129e42..2fca773e9c 100644 --- a/flow/scripts/macro_place.tcl +++ b/flow/scripts/macro_place.tcl @@ -7,6 +7,9 @@ source $::env(SCRIPTS_DIR)/macro_place_util.tcl source_step_tcl POST MACRO_PLACE +# register files in `mode netlist` become their cells +source $::env(SCRIPTS_DIR)/regfile_dissolve.tcl + report_design_area orfs_write_db $::env(RESULTS_DIR)/2_2_floorplan_macro.odb diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index e6bfd2a2bc..5b677a560f 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -10,6 +10,8 @@ /.lib Liberty view per converted memory /_pre_layout.lib ideal-clock variant for pre-CTS consumers /.lef abstract LEF per converted memory + /regfiles.txt register files to dissolve into their cells + after macro placement (`mode netlist`) /blackboxes.txt ` ` per line, area in um^2 as the .lib states it — what synthesis blackboxes and the cost it gives each @@ -21,6 +23,8 @@ from __future__ import annotations import argparse +import json +import re import sys from pathlib import Path @@ -35,12 +39,190 @@ FAKERAM_RUN_PY = Path(__file__).resolve().parents[3] / "tools/FakeRAM2.0/run.py" +def _spec_module(spec: Path) -> str: + """The `module ` line of a register-file spec.""" + for line in spec.read_text().splitlines(): + parts = line.split("#", 1)[0].split() + if len(parts) == 2 and parts[0] == "module": + return parts[1] + raise SystemExit(f"gen_memories: {spec}: no `module` line") + + +def _spec_value(spec: Path, key: str) -> list[list[str]]: + """Every `key ...` line of a register-file spec, as word lists.""" + out = [] + for line in spec.read_text().splitlines(): + parts = line.split("#", 1)[0].split() + if parts and parts[0] == key: + out.append(parts[1:]) + return out + + +def _spec_mode(spec: Path) -> str: + """`mode macro` (the default) or `mode netlist` of a register-file spec.""" + for line in spec.read_text().splitlines(): + parts = line.split("#", 1)[0].split() + if len(parts) == 2 and parts[0] == "mode": + if parts[1] not in ("macro", "netlist"): + raise SystemExit( + f"gen_memories: {spec}: mode is macro or netlist, not {parts[1]}" + ) + return parts[1] + return "macro" + + +# DEF orientation names to the database's (what dbInst::setOrient takes). +_DEF_ORIENT = { + "N": "R0", + "S": "R180", + "W": "R90", + "E": "R270", + "FN": "MY", + "FS": "MX", + "FW": "MXR90", + "FE": "MYR90", +} + + +def _def_placement(def_path: Path, out_path: Path) -> dict: + """The placed cells of the generator's DEF as `name x y orient` lines, + plus the die and the first row's orientation and height, so the + parent's floorplan can drop the array onto its own rows.""" + text = def_path.read_text() + m = re.search( + r"DIEAREA\s*\(\s*(-?\d+)\s+(-?\d+)\s*\)\s*\(\s*(-?\d+)\s+(-?\d+)\s*\)", text + ) + die = [int(v) for v in m.groups()] if m else None + rows = re.findall(r"^ROW\s+\S+\s+(\S+)\s+(-?\d+)\s+(-?\d+)\s+(\w+)", text, re.M) + row0 = None + row_h = None + if rows: + rows_sorted = sorted(((int(y), o, s_) for s_, x, y, o in rows)) + row0 = _DEF_ORIENT.get(rows_sorted[0][1], rows_sorted[0][1]) + ys = sorted({y for y, _, _ in rows_sorted}) + if len(ys) > 1: + row_h = ys[1] - ys[0] + comp = re.search(r"COMPONENTS\s+\d+\s*;(.*?)END COMPONENTS", text, re.S) + lines = [] + if comp: + for entry in comp.group(1).split(";"): + e = entry.split() + if len(e) < 3 or e[0] != "-": + continue + pm = re.search( + r"\+\s*(PLACED|FIXED|FIRM|COVER)\s*\(\s*(-?\d+)\s+(-?\d+)\s*\)\s*(\w+)", + entry, + ) + if pm: + orient = _DEF_ORIENT.get(pm.group(4), pm.group(4)) + lines.append(f"{e[1]} {pm.group(2)} {pm.group(3)} {orient}") + out_path.write_text("".join(l + "\n" for l in lines)) + return {"die": die, "row0_orient": row0, "row_height": row_h, "cells": len(lines)} + + +def _file_defining(module: str, verilog: list[Path]) -> Path: + """The Verilog file that declares `module`, for the port check.""" + pat = re.compile(rf"^\s*module\s+{re.escape(module)}\b", re.M) + for path in verilog: + if pat.search(path.read_text(errors="replace")): + return path + raise SystemExit( + f"gen_memories: AUTO_MEMORIES_REGFILES names module {module}, " + f"which none of the Verilog files declare" + ) + + +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.""" + 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 run_regfiles( + specs: list[Path], + openroad: str | None, + lefs: list[Path], + verilog: list[Path], + out_dir: Path, +) -> list[schema.Memory]: + """Generate every listed register file with OpenROAD's generate_regfile. + + A register file 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 + 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 + are not the module's stops the build here rather than miswire the + parent. + """ + if not specs: + (out_dir / "regfiles.txt").write_text("") + return [] + if not openroad: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_REGFILES is set but OPENROAD_EXE is not" + ) + if not lefs: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_REGFILES needs TECH_LEF and SC_LEF" + ) + memories = [] + dissolve = [] + for spec in specs: + module = _spec_module(spec) + mode = _spec_mode(spec) + rtl = _file_defining(module, verilog) + tcl = out_dir / f"{module}.generate.tcl" + tcl.write_text( + "".join(f"read_lef {lef}\n" for lef in lefs) + + f"generate_regfile -spec {spec} -check_ports {rtl}" + + f" -verilog {out_dir / f'{module}.v'} -def {out_dir / f'{module}.def'}" + + f" -lef {out_dir / f'{module}.lef'} -liberty {out_dir / f'{module}.lib'}\n" + ) + subprocess.check_call([openroad, "-exit", "-no_init", "-no_splash", str(tcl)]) + mem = schema.Memory( + name=module, kind="regfile", source="listed", spec=str(spec) + ) + words = _spec_value(spec, "words") + bits = _spec_value(spec, "bits") + mem.rows = int(words[0][0]) if words else 0 + mem.bits = int(bits[0][0]) if bits else 0 + mem.read_ports = len(_spec_value(spec, "read")) + len( + _spec_value(spec, "read_banked") + ) + mem.write_ports = len(_spec_value(spec, "write")) + mem.reason = f"register file, mode {mode}" + if mode == "netlist": + _def_placement(out_dir / f"{module}.def", out_dir / f"{module}.place") + dissolve.append(module) + memories.append(mem) + sys.stderr.write( + f"gen_memories: {module} -> register file, mode {mode} ({spec.name})\n" + ) + with (out_dir / "blackboxes.txt").open("a") as f: + f.write( + "".join( + f"{m.name} {_lib_area(out_dir / f'{m.name}.lib')}\n" for m in memories + ) + ) + (out_dir / "regfiles.txt").write_text("".join(f"{m}\n" for m in dissolve)) + return memories + + def run( yosys_json: Path, memories_files: list[Path], platform: str, out_dir: Path, json_path: Path, + verilog: list[Path] | None = None, + regfile_specs: list[Path] | None = None, + openroad: str | None = None, + lefs: list[Path] | None = None, ) -> int: if platform != "asap7": sys.stderr.write(f"gen_memories: unsupported platform {platform}\n") @@ -54,6 +236,11 @@ def run( for path in memories_files: overrides.extend(schema.load(path)) memories = schema.merge(found, overrides) + override_names = {o.name for o in overrides} + for mem in memories: + mem.kind = "fakeram" if mem.idiomatic else "flops" + if mem.name in override_names: + mem.source = "override" out_dir.mkdir(parents=True, exist_ok=True) json_path.parent.mkdir(parents=True, exist_ok=True) @@ -74,8 +261,18 @@ def run( ] ) + # After FakeRAM has written blackboxes.txt for the inferred memories, + # and recorded in memories.json beside them. + regfiles = run_regfiles( + regfile_specs or [], openroad, lefs or [], verilog or [], out_dir + ) + 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) + for mem in sorted(memories, key=lambda m: m.name): - verdict = "macro" if mem.idiomatic else "flops" + verdict = mem.kind sys.stderr.write( f"gen_memories: {mem.name} {mem.rows}x{mem.bits} " f"R={mem.read_ports} W={mem.write_ports} RW={mem.rw_ports} " @@ -102,6 +299,32 @@ def main(argv: list[str] | None = None) -> int: help="User-supplied .memories file merged onto the " "detected set (repeatable).", ) + p.add_argument( + "--verilog", + action="append", + default=[], + type=Path, + help="Verilog file searched for a listed register file's module (repeatable).", + ) + p.add_argument( + "--regfile-spec", + action="append", + default=[], + type=Path, + help="Register-file spec, AUTO_MEMORIES_REGFILES (repeatable).", + ) + p.add_argument( + "--openroad", + default=None, + help="The openroad that runs generate_regfile (OPENROAD_EXE).", + ) + p.add_argument( + "--lef", + action="append", + default=[], + type=Path, + help="Technology and cell LEF for the generator (repeatable).", + ) p.add_argument("--platform", required=True) p.add_argument( "--out-dir", @@ -122,6 +345,10 @@ def main(argv: list[str] | None = None) -> int: args.platform, args.out_dir, args.json, + args.verilog, + args.regfile_spec, + args.openroad, + args.lef, ) diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index 74a5929a0f..1f285d6e85 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -117,6 +117,7 @@ def test_flow_contract(self): "mem_128x32.lef", "mem_128x32.lib", "mem_128x32_pre_layout.lib", + "regfiles.txt", ], ) # one ` ` line; the area is the one the .lib states, @@ -194,6 +195,25 @@ def test_no_memories_still_writes_contract_files(self): self.assertEqual(code, 0) self.assertEqual(json.loads((d / "memories.json").read_text())["memories"], []) self.assertEqual((d / "memories" / "blackboxes.txt").read_text(), "") + self.assertEqual((d / "memories" / "regfiles.txt").read_text(), "") + + def test_regfile_area_is_the_lib_cell_area(self): + # a register file's blackboxes.txt line carries the area its + # generated .lib states, as FakeRAM's do + with tempfile.TemporaryDirectory() as d: + lib = Path(d) / "RegFile.lib" + lib.write_text( + "library (RegFile) {\n cell (RegFile) {\n" + " area : 153.527400;\n }\n}\n" + ) + self.assertEqual(gen_memories._lib_area(lib), "153.527400") + + def test_regfile_lib_without_area_fails(self): + with tempfile.TemporaryDirectory() as d: + lib = Path(d) / "RegFile.lib" + lib.write_text("library (RegFile) {\n cell (RegFile) {\n }\n}\n") + with self.assertRaises(SystemExit): + gen_memories._lib_area(lib) if __name__ == "__main__": diff --git a/flow/scripts/memories/schema.py b/flow/scripts/memories/schema.py index 754ae8373c..b83d892b94 100644 --- a/flow/scripts/memories/schema.py +++ b/flow/scripts/memories/schema.py @@ -61,10 +61,19 @@ class Memory: pins: list[Pin] = field(default_factory=list) # The module whose RTL body simulates this memory: {"file", "module"}. behavioral_model: dict | None = None - # True when the memory is converted to a macro (.lib/.lef emitted and - # the module blackboxed); False leaves it to synthesize as flops. + # True when FakeRAM converts the memory to an SRAM macro (.lib/.lef + # emitted and the module blackboxed); see `kind` for what the flow + # does with the memory in the end. idiomatic: bool = False reason: str = "" + # What the memory becomes: "fakeram" (an SRAM macro), "regfile" (a + # register file generate_regfile builds from `spec`) or "flops". + kind: str = "flops" + # "detected" (yosys inferred it), "listed" (AUTO_MEMORIES_REGFILES + # names it) or "override" (an ADDITIONAL_MEMORIES file). + source: str = "detected" + # The register-file spec generate_regfile reads, for kind "regfile". + spec: str = "" def total_ports(self) -> int: return self.read_ports + self.write_ports + self.rw_ports @@ -129,6 +138,9 @@ def memory_from_dict(d: dict) -> Memory: mem.idiomatic = bool(d["idiomatic"]) if "reason" in d: mem.reason = str(d["reason"]) + for key in ("kind", "source", "spec"): + if key in d: + setattr(mem, key, str(d[key])) return mem diff --git a/flow/scripts/regfile_dissolve.tcl b/flow/scripts/regfile_dissolve.tcl new file mode 100644 index 0000000000..bd6f46c5e6 --- /dev/null +++ b/flow/scripts/regfile_dissolve.tcl @@ -0,0 +1,352 @@ +# Register files in `mode netlist` (AUTO_MEMORIES_REGFILES) dissolve into +# their cells here, right after macro placement: to synthesis and to the +# macro placer each was a macro, its abstract's pins where its wires +# connect, so the placer chose its location and flip; from here on it is +# standard cells in the parent's rows. Before tapcells and the power grid, +# which see only cells. +# +# For each macro instance of a listed module, in the instance's module: +# every cell of the generator's structural netlist (memories/.v) is +# created as /, the nets between them as +# /, and each port's net is the net the macro's pin was +# on; the macro goes. The core -- the cells memories/.place lists -- +# is placed FIRM where the generator laid it out, through the macro's +# location and flip; the rest, the periphery (address decode), is left +# unplaced for global placement to place and the resizer to size. +# eliminate_dead_logic then removes what the parent never reads, as the +# synthesis ODB step does for every other cell. + +proc regfile_netlist { file } { + # one instance per line, as generate_regfile writes it: + # MASTER \name (.PIN(net), .PIN(\net ), ...); + set cells {} + set f [open $file r] + while { [gets $f line] >= 0 } { + if { ![regexp {^\s*(\S+)\s+\\?(\S+)\s*\((.*)\);\s*$} $line -> master name body] } { + continue + } + set conns {} + foreach {- pin net} [regexp -all -inline {\.(\w+)\(\s*\\?([^ )]+)\s*\)} $body] { + lappend conns $pin $net + } + lappend cells [list $master $name $conns] + } + close $f + return $cells +} + +proc regfile_place_file { file } { + set place [dict create] + set f [open $file r] + while { [gets $f line] >= 0 } { + lassign $line name x y orient + if { $name ne "" } { dict set place $name [list $x $y $orient] } + } + close $f + return $place +} + +proc regfile_flip_x { o } { return [dict get {R0 MX MX R0 MY R180 R180 MY} $o] } +proc regfile_flip_y { o } { return [dict get {R0 MY MY R0 MX R180 R180 MX} $o] } +proc regfile_row_kind { o } { return [expr { $o in {R0 MY} ? "R0" : "MX" }] } + +# A cell is on the core's boundary when one of its nets reaches a port or +# a cell outside the core; a net is the core's own when every terminal of +# it is a placed cell of the core. +proc regfile_boundary_cell { inst placed } { + foreach it [$inst getITerms] { + set net [$it getNet] + if { $net eq "NULL" } { continue } + if { [llength [$net getBTerms]] > 0 } { return 1 } + foreach other [$net getITerms] { + if { ![dict exists $placed [[$other getInst] getName]] } { return 1 } + } + } + return 0 +} +proc regfile_internal_net { net placed } { + if { [llength [$net getBTerms]] > 0 } { return 0 } + foreach it [$net getITerms] { + if { ![dict exists $placed [[$it getInst] getName]] } { return 0 } + } + return 1 +} + +set regfile_list "$::env(RESULTS_DIR)/memories/regfiles.txt" +set regfile_modules {} +if { [env_var_equals AUTO_MEMORIES 1] && [file exists $regfile_list] } { + set f [open $regfile_list r] + set regfile_modules [split [string trim [read $f]] "\n"] + close $f + set regfile_modules [lsearch -all -inline -not -exact $regfile_modules ""] +} +if { [llength $regfile_modules] > 0 } { + set block [ord::get_db_block] + set db [ord::get_db] + set core [$block getCoreArea] + set dbu [[ord::get_db_tech] getDbUnitsPerMicron] + # the parent's rows by their bottom, for the parity check + set row_orient [dict create] + set row_h 0 + set site_w 0 + foreach row [$block getRows] { + set bb [$row getBBox] + dict set row_orient [$bb yMin] [$row getOrient] + set row_h [expr { [$bb yMax] - [$bb yMin] }] + set site_w [[$row getSite] getWidth] + } + set dissolved 0 + set fixups {} + foreach module $regfile_modules { + set macros {} + foreach inst [$block getInsts] { + if { [[$inst getMaster] getName] eq $module } { lappend macros $inst } + } + if { [llength $macros] == 0 } { + utl::info FLW 9 "register file $module: no instance in the design (removed as dead logic)" + continue + } + set cells [regfile_netlist "$::env(RESULTS_DIR)/memories/$module.v"] + set place [regfile_place_file "$::env(RESULTS_DIR)/memories/$module.place"] + foreach macro $macros { + set path [$macro getName] + set parent [$macro getModule] + set master [$macro getMaster] + set W [$master getWidth] + set H [$master getHeight] + set bb [$macro getBBox] + set flip [$macro getOrient] + if { $flip ni {R0 MX MY R180} } { + utl::error FLW 10 "register file $path is placed $flip;\ + a register file flips, it does not rotate" + } + # snap the corner to the site and row grid of the core + set X [expr { + [$core xMin] + + int(round(([$bb xMin] - [$core xMin]) / double($site_w))) * $site_w + }] + set Y [expr { + [$core yMin] + + int(round(([$bb yMin] - [$core yMin]) / double($row_h))) * $row_h + }] + # the macro's pins are the module's port bits; the nets they were on, + # flat and in the macro's module. A cell on the flat net alone is + # cut off from its module's net: STA propagates the clock through + # the hierarchy and never reaches it. + set ports [dict create] + foreach mt [$master getMTerms] { dict set ports [$mt getName] 1 } + set port_net [dict create] + set port_modnet [dict create] + foreach it [$macro getITerms] { + set net [$it getNet] + if { $net ne "NULL" } { dict set port_net [[$it getMTerm] getName] $net } + set modnet [$it getModNet] + if { $modnet ne "NULL" } { dict set port_modnet [[$it getMTerm] getName] $modnet } + } + # The macro placer's halo is a blockage tied to the macro; destroyed + # with it, its instance id goes to one of the new cells and global + # placement refuses a blockage on a movable cell (GPL-0003). + foreach bl [$block getBlockages] { + set owner [$bl getInstance] + if { $owner ne "NULL" && [$owner getId] == [$macro getId] } { + odb::dbBlockage_destroy $bl + } + } + odb::dbInst_destroy $macro + set placed [dict create] + set placed_insts {} + foreach cell $cells { + lassign $cell mname name conns + set m [$db findMaster $mname] + if { $m eq "NULL" } { + utl::error FLW 11 "register file $module: no master $mname for $name" + } + set inst [odb::dbInst_create $block $m "$path/$name" 0 $parent] + foreach {pin netname} $conns { + if { [dict exists $port_net $netname] } { + set net [dict get $port_net $netname] + } elseif { [dict exists $ports $netname] } { + # a port bit the parent leaves unconnected + continue + } else { + set full "$path/$netname" + set net [$block findNet $full] + if { $net eq "NULL" } { set net [odb::dbNet_create $block $full] } + } + set it [$inst findITerm $pin] + if { $it eq "NULL" } { + continue + } elseif { [dict exists $port_modnet $netname] } { + $it connect $net [dict get $port_modnet $netname] + } else { + $it connect $net + } + } + if { [dict exists $place $name] } { + lassign [dict get $place $name] cx cy o + set w [$m getWidth] + set h [$m getHeight] + set x [expr { $cx }] + set y [expr { $cy }] + if { $flip in {MX R180} } { + set y [expr { $H - $cy - $h }] + set o [regfile_flip_x $o] + } + if { $flip in {MY R180} } { + set x [expr { $W - $cx - $w }] + set o [regfile_flip_y $o] + } + $inst setOrient $o + $inst setLocation [expr { $X + $x }] [expr { $Y + $y }] + dict set placed [$inst getName] 1 + lappend placed_insts $inst + } + } + # Row parity: a core row lands on a parent row of its own kind, or + # the whole core moves up one row. + if { [llength $placed_insts] > 0 } { + set probe [lindex $placed_insts 0] + set py [[$probe getBBox] yMin] + if { + [dict exists $row_orient $py] + && [regfile_row_kind [dict get $row_orient $py]] ne [regfile_row_kind [$probe getOrient]] + } { + foreach inst $placed_insts { + set l [$inst getLocation] + $inst setLocation [lindex $l 0] [expr { [lindex $l 1] + $row_h }] + } + set Y [expr { $Y + $row_h }] + } + } + if { + $X < [$core xMin] || $Y < [$core yMin] + || $X + $W > [$core xMax] || $Y + $H > [$core yMax] + } { + set w_um [expr { $W / double($dbu) }] + set h_um [expr { $H / double($dbu) }] + set x_um [expr { $X / double($dbu) }] + set y_um [expr { $Y / double($dbu) }] + utl::error FLW 4 "register file $path ($module, $w_um x $h_um um)\ + at $x_um $y_um um leaves the core" + } + set placed_names {} + foreach inst $placed_insts { lappend placed_names [$inst getName] } + lappend fixups [list $module $path $flip $X $Y [llength $cells] $placed $placed_names] + incr dissolved + } + } + if { $dissolved > 0 } { + # What the parent never reads or writes, as at the synthesis ODB: a + # bit no one reads takes its column of the core with it. Before the + # core is fixed, which the elimination would respect. + log_cmd eliminate_dead_logic + # Cut the rows around the macros now, as tapcell would: cut_rows + # leaves a row that holds a fixed cell uncut (ODB-0386), so once the + # core is FIRM a row it shares with a macro would run on under the + # macro and take an endcap there. + set cut_args {} + if { [env_var_exists_and_non_empty TAP_CELL_NAME] } { + lappend cut_args -endcap_master $::env(TAP_CELL_NAME) + } + if { [env_var_exists_and_non_empty MACRO_ROWS_HALO_X] } { + lappend cut_args -halo_width_x $::env(MACRO_ROWS_HALO_X) + } + if { [env_var_exists_and_non_empty MACRO_ROWS_HALO_Y] } { + lappend cut_args -halo_width_y $::env(MACRO_ROWS_HALO_Y) + } + log_cmd cut_rows {*}$cut_args + } + set row_boxes {} + foreach row [$block getRows] { lappend row_boxes [$row getBBox] } + foreach fixup $fixups { + lassign $fixup module path flip X Y ncells placed placed_names + set placed_insts {} + foreach name $placed_names { + set inst [$block findInst $name] + if { $inst eq "NULL" } { + dict unset placed $name + } else { + lappend placed_insts $inst + } + } + foreach inst $placed_insts { + set bb [$inst getBBox] + set on_row 0 + foreach rb $row_boxes { + if { + [$bb xMin] >= [$rb xMin] && [$bb xMax] <= [$rb xMax] + && [$bb yMin] == [$rb yMin] + } { + set on_row 1 + break + } + } + if { !$on_row } { + utl::error FLW 14 "register file $module at $path: [$inst getName]\ + is on no row once the rows are cut around the macros" + } + $inst setPlacementStatus FIRM + } + # The core is final: FIRM keeps the legaliser off it, dont_touch + # keeps the resizer off it. Two kinds of cell are excepted. The + # flops: CTS has to rewire their clock pins and a dont_touch + # instance refuses (ODB-0370). The boundary cells, any with a pin on + # a net that leaves the core: the parent's repair_design has to + # buffer the nets that reach the core and cannot reconnect a + # dont_touch load (RSZ-3006). They stay FIRM, so nothing resizes + # them: OpenROAD keeps a fixed cell's footprint. + foreach inst $placed_insts { + if { ![[$inst getMaster] isSequential] && ![regfile_boundary_cell $inst $placed] } { + $inst setDoNotTouch 1 + } + } + # The wires between the core's own cells are the generator's too: + # repair_design buffers a net by inserting a cell before its loads, + # and a dont_touch load refuses (ODB-1211, then RSZ-3006 fails the + # stage). A net that leaves the core stays repairable. A violation + # left inside is the generator's to fix. + # Each net once: a net that leaves the core would otherwise be + # rechecked from every one of its pins (the reset, the clock). + set internal_nets 0 + set seen_nets [dict create] + foreach inst $placed_insts { + foreach it [$inst getITerms] { + set net [$it getNet] + if { $net eq "NULL" || [dict exists $seen_nets $net] } { continue } + dict set seen_nets $net 1 + if { [$net isDoNotTouch] } { continue } + if { [regfile_internal_net $net $placed] } { + $net setDoNotTouch 1 + incr internal_nets + } + } + } + set dead [expr { [llength $placed_names] - [llength $placed_insts] }] + set at_x [expr { $X / double($dbu) }] + set at_y [expr { $Y / double($dbu) }] + utl::info FLW 6 "register file $module at $path: $ncells cells,\ + [llength $placed_insts] FIRM ($flip at $at_x $at_y um), $dead dead,\ + $internal_nets internal nets dont_touch" + } + # Every flop of a dissolved core is one STA sees clocked: an unclocked + # one is no timing endpoint, and placement and repair would work blind + # to the paths through the array until CTS rewires its clock. + if { $dissolved > 0 && [llength [all_clocks]] > 0 } { + set clocked [dict create] + foreach cell [all_registers -cells -clock [all_clocks]] { + dict set clocked [[sta::sta_to_db_inst $cell] getName] 1 + } + foreach fixup $fixups { + lassign $fixup module path + foreach name [lindex $fixup 7] { + set inst [$block findInst $name] + if { + $inst ne "NULL" && [[$inst getMaster] isSequential] + && ![dict exists $clocked $name] + } { + utl::error FLW 13 "register file $module at $path: STA sees no clock on $name" + } + } + } + } +} diff --git a/flow/scripts/variables.json b/flow/scripts/variables.json index 61a7b8b8e7..66eef69765 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -68,6 +68,12 @@ "default": 0, "description": "Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and \u2014 when idiomatic as an SRAM macro \u2014 given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.\n" }, + "AUTO_MEMORIES_REGFILES": { + "description": "Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like an AUTO_MEMORIES macro and placed as one. A spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.\n", + "stages": [ + "synth" + ] + }, "BALANCE_ROWS": { "default": 0, "description": "Balance rows during placement.\n", diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml index 2052bd468d..dea40d353a 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -227,6 +227,23 @@ ADDITIONAL_MEMORIES: docs/user/AutoMemories.md. stages: - synth +AUTO_MEMORIES_REGFILES: + description: > + Experimental. Space-separated list of register-file spec files, one + per RTL module to build as a register file of placed standard cells + rather than synthesise to flops. Each spec names its module, words, + bits, read and write ports by the RTL's own port names, and the cells; + OpenROAD's generate_regfile checks the spec against the module's + ports and writes `.lef`, `.lib` and + `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the + module is blackboxed like an AUTO_MEMORIES macro and placed as one. A + spec in `mode netlist` then dissolves into its cells after macro + placement: its core FIRM where the macro was placed, in the macro's + orientation, its address decode left to placement. Every register + file appears in memories.json with kind `regfile`. Requires + AUTO_MEMORIES=1. + stages: + - synth SYNTH_MEMORY_MAX_BITS: description: > Maximum number of bits for memory synthesis. From efb5e10a8908c325bd355265c25666ed6f512abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 7 Oct 2026 11:48:25 +0200 Subject: [PATCH 8/9] AUTO_MEMORIES_MACRO_PLACE: register files not listed are inlined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUTO_MEMORIES_REGFILES built every register file as a macro: blackboxed in synthesis, placed by the macro placer and, in `mode netlist`, dissolved into its cells after macro placement. On a small design the macro's outline and channels cost more core than the array saves: riscv32i's register file as a macro does not fit riscv32i's own 62 % utilisation beside its four FakeRAMs (MPL-0003). AUTO_MEMORIES_MACRO_PLACE lists the register files to place as macros. Any other is inlined: gen_memories.py writes only its generated netlist (no abstract, model liberty or taps) and inline.txt, and synthesis reads the RTL module as a blackbox and the netlist in its place. Its cells are then placed, sized and buffered with the rest of the design. A name no spec builds stops the build. asap7/regfile lists its RegFile, which keeps the dissolve its tests exercise. asap7/riscv32i-regfile lists nothing, takes riscv32i's own utilisation, and its spec writes through a clock gate per word (`write_style clock_gate`, OpenROAD#11669): 955 ps at global route against the flip-flops' 954, on a core 12 % smaller. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 99 +++++++++------- docs/user/FlowVariables.md | 4 +- flow/Makefile | 1 + flow/designs/asap7/regfile/config.mk | 3 + flow/designs/asap7/riscv32i-regfile/config.mk | 10 +- .../asap7/riscv32i-regfile/regfile.regfile | 14 +-- flow/scripts/memories/gen_memories.py | 107 +++++++++++++++--- flow/scripts/memories/gen_memories_test.py | 2 + flow/scripts/synth_preamble.tcl | 32 +++++- flow/scripts/variables.json | 8 +- flow/scripts/variables.yaml | 20 +++- 11 files changed, 217 insertions(+), 83 deletions(-) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index c9c018a867..8bb0d2b088 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -149,18 +149,33 @@ cell flop DFFHQNx1_ASAP7_75t_R ... ``` -`generate_regfile` checks the spec against the module's ports, then -writes `.v` (the cells), `.lef` (an abstract, its pins where -their connections land in the array) and `.lib`/`_pre_layout.lib` -(a timing model) beside the FakeRAM views. The module joins -`blackboxes.txt`, so synthesis and macro placement see a macro. - -In `mode netlist` (the usual one) the macro dissolves into its cells at -the end of macro placement (`regfile_dissolve.tcl`): the array's core -lands FIRM where the macro was placed, flipped as the macro was, on the -parent's rows; the address decode is left to global placement and the -resizer, and dead logic is eliminated as at synthesis. From there on it -is standard cells. `mode macro` keeps the macro to the end. +`generate_regfile` checks the spec against the module's ports and +writes `.v`, the array as standard cells. What happens next depends +on `AUTO_MEMORIES_MACRO_PLACE`, the register files to place as macros: + +- **Not listed (the default): inlined.** Synthesis reads the module as a + blackbox and `.v` in its place; from there on its cells are placed, + sized and buffered with the rest of the design. No macro, no abstract, + no dissolve. The better shape for a small design, where a macro's + outline and channels cost more core than the array saves. +- **Listed: a macro.** `generate_regfile` also writes `.lef` (an + abstract, its pins where their connections land in the array) and + `.lib`/`_pre_layout.lib` (a timing model) beside the FakeRAM + views, and the module joins `blackboxes.txt`, so synthesis and macro + placement see a macro. In `mode netlist` (the usual one) the macro + dissolves into its cells at the end of macro placement + (`regfile_dissolve.tcl`): the array's core lands FIRM where the macro + was placed, flipped as the macro was, on the parent's rows; the + address decode is left to global placement and the resizer, and dead + logic is eliminated as at synthesis. `mode macro` keeps the macro to + the end. A large design whose floorplan plans its register files lists + them. + +A word holds its value between writes either through a hold term in +every bit's write mux (`write_style mux`, the default) or through one +integrated clock gate per word, enabled by its write selects +(`write_style clock_gate`, as OpenROAD's `generate_ram` writes): with +one write port a flop's D is then the write data itself. Listed register files appear in `memories.json` with kind `regfile`; the rest have kind `fakeram` or `flops`. `designs/asap7/regfile` is the @@ -194,41 +209,40 @@ seam in their RTL first. `designs/asap7/regfile` is the smallest case, for turnaround rather than measurement. -### Status: a prototype, slower and bigger on riscv32i +### Status: at parity on riscv32i, on a smaller core `designs/asap7/riscv32i-regfile` is riscv32i with its register file -generated; the study case for what the generator gets wrong, because it -reaches global route in minutes. To global route, against -`designs/asap7/riscv32i`: +generated, inlined and written through a clock gate per word, at +riscv32i's own utilisation. To global route, 950 ps clock, global-route +parasitics, against `designs/asap7/riscv32i`: | | flops | register file | |---|---|---| -| minimum period (950 ps clock, global-route parasitics) | 954 ps | 998 ps | -| place to global route, stage time | 211 s | 536 s | -| core area | 4 012 um2 (62 %) | 7 573 um2 (45 %) | -| standard-cell area, without taps | 1 284 um2 | 1 413 um2 | -| failing endpoints after global route | 79 | 992 | - -Synthesis takes a second either way: a 32 x 32 file is too small for -its synthesis time to show, which is where the larger files of a large -core are expected to gain. - -Why it is slower: every failing endpoint is a flop of the array, all -within 50 ps, and the worst path runs from the instruction through the -array's read path -- the address decode as three serial AND2 stages, -the word select, five levels of OR2 -- into the ALU and back to the -array's write mux: 232 ps from `instr` to read data. The flops -version's same loop closes. `repair_timing` then spends its time at CTS -and global route on 992 endpoints it cannot fix. - -Why it is bigger: standard-cell area is within a tenth of the flops', -and the array's cells fill 58 % of its box. The core is bigger because -`riscv32i-regfile/config.mk` sets 45 % utilisation: at riscv32i's 62 % -no macro placement fits the array beside the four FakeRAMs. - -The rest is the generator's to fix: decode as a tree sized for its -fanout, a read tree of AO22 and NAND/NOR rather than AND2 and OR2, -cells the resizer may swap in place. +| minimum period | 954 ps | 955 ps | +| core area | 4 012 um2 | 3 544 um2 | +| standard-cell area, without taps | 1 284 um2 | 973 um2 | +| clock-tree buffers and inverters | 122 | 179 | +| clock gates | 0 | 31 | +| hold, worst slack | +36 ps | +86 ps | + +Both are limited by the same single-cycle path: the instruction into +the register file's read, the ALU, the data memory and back into the +register file's write. The register file's share of it, decode and read +tree, is about 260 ps in either. + +What did not pay, measured on the same design: + +- the register file as a macro dissolved after macro placement: it does + not fit riscv32i's 62 % beside the four FakeRAMs, and at 55 % it is + 959 ps on a 4 644 um2 core; +- a hold mux per bit instead of the clock gate: 994 ps inlined, every + array endpoint carrying the mux; +- predecoded read selects: 960 ps; +- a tree of 2:1 muxes steered by the address bits instead of a decoded + one-hot read: 959 ps, the address bits' fanout (about 1 000 inputs a + port) needs as many buffer levels as the decode it replaces; +- four-input gates in the read tree, stronger cells: 950 to 954 ps, + within what any change to the placement moves. ## Platform support @@ -291,4 +305,5 @@ bazel-orfs implements this. - [AUTO_MEMORIES](FlowVariables.md#AUTO_MEMORIES) - [ADDITIONAL_MEMORIES](FlowVariables.md#ADDITIONAL_MEMORIES) +- [AUTO_MEMORIES_MACRO_PLACE](FlowVariables.md#AUTO_MEMORIES_MACRO_PLACE) - [AUTO_MEMORIES_REGFILES](FlowVariables.md#AUTO_MEMORIES_REGFILES) diff --git a/docs/user/FlowVariables.md b/docs/user/FlowVariables.md index 7fa1cbdd5d..e5651085dc 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -103,7 +103,8 @@ configuration file. | ADDITIONAL_SITES| Passed as -additional_sites to initialize_floorplan.| | | ASAP7_USE_VT| A space separated list of VT options to use with the ASAP7 standard cell library: RVT, LVT, SLVT.| RVT| | AUTO_MEMORIES| Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and — when idiomatic as an SRAM macro — given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.| 0| -| AUTO_MEMORIES_REGFILES| Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like an AUTO_MEMORIES macro and placed as one. A spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.| | +| AUTO_MEMORIES_MACRO_PLACE| Experimental. Space-separated list of module names of AUTO_MEMORIES_REGFILES register files to place as macros, blackboxed in synthesis and placed by the macro placer (and, in `mode netlist`, dissolved after macro placement). A register file not listed is inlined: its generated netlist of standard cells replaces the module's body in synthesis, and its cells are placed, sized and buffered with the rest of the design -- the better shape for a small design, where a macro's outline and channels cost more core than the array saves. A name no spec builds stops the build. Requires AUTO_MEMORIES=1.| | +| AUTO_MEMORIES_REGFILES| Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`. A module named in AUTO_MEMORIES_MACRO_PLACE is blackboxed like an AUTO_MEMORIES macro and placed as one; any other is inlined as its generated netlist. A placed spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.| | | BALANCE_ROWS| Balance rows during placement.| 0| | BLOCKS| Blocks used as hard macros in a hierarchical flow. Do note that you have to specify block-specific inputs file in the directory mentioned by Makefile.| | | BUFFER_PORTS_ARGS| Specify arguments to the buffer_ports call during placement. Only used if DONT_BUFFER_PORTS=0.| | @@ -343,6 +344,7 @@ configuration file. - [ABC_LOAD_IN_FF](#ABC_LOAD_IN_FF) - [ADDER_MAP_FILE](#ADDER_MAP_FILE) - [ADDITIONAL_MEMORIES](#ADDITIONAL_MEMORIES) +- [AUTO_MEMORIES_MACRO_PLACE](#AUTO_MEMORIES_MACRO_PLACE) - [AUTO_MEMORIES_REGFILES](#AUTO_MEMORIES_REGFILES) - [CACHED_REPORTS](#CACHED_REPORTS) - [CLKGATE_MAP_FILE](#CLKGATE_MAP_FILE) diff --git a/flow/Makefile b/flow/Makefile index 53d9dc192d..80a6348773 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -280,6 +280,7 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json $(foreach file,$(VERILOG_FILES),--verilog $(file)) \ $(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) \ $(foreach file,$(AUTO_MEMORIES_REGFILES),--regfile-spec $(file)) \ + $(foreach m,$(AUTO_MEMORIES_MACRO_PLACE),--macro-place $(m)) \ $(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \ $(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file)) diff --git a/flow/designs/asap7/regfile/config.mk b/flow/designs/asap7/regfile/config.mk index 2880287d12..e0f1874d73 100644 --- a/flow/designs/asap7/regfile/config.mk +++ b/flow/designs/asap7/regfile/config.mk @@ -9,6 +9,9 @@ export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint. export AUTO_MEMORIES = 1 export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/RegFile.regfile +# Placed as a macro and dissolved after macro placement, the path this +# design exists to exercise; unlisted, it would be inlined. +export AUTO_MEMORIES_MACRO_PLACE = RegFile # The file's macro is 28.5 x 10 um; a core sized by utilization alone # would be square and narrower than the macro and its halo. diff --git a/flow/designs/asap7/riscv32i-regfile/config.mk b/flow/designs/asap7/riscv32i-regfile/config.mk index b31d984d8b..142becabc6 100644 --- a/flow/designs/asap7/riscv32i-regfile/config.mk +++ b/flow/designs/asap7/riscv32i-regfile/config.mk @@ -1,13 +1,11 @@ # riscv32i with its register file built by AUTO_MEMORIES_REGFILES rather -# than synthesised to flip-flops: the same RTL and constraints. The study -# case for why a generated register file is slower and bigger than the -# flops it replaces -- minutes to global route, simple to build, and both -# are true of it today. docs/user/AutoMemories.md has the numbers. +# than synthesised to flip-flops: the same RTL, constraints and +# utilisation. Not in AUTO_MEMORIES_MACRO_PLACE, so the generated netlist +# is inlined in synthesis. Minutes to global route, the case to measure +# the generator on; docs/user/AutoMemories.md has the numbers. export DESIGN_NICKNAME = riscv32i-regfile include designs/asap7/riscv32i/config.mk -export CORE_UTILIZATION = 45 - export AUTO_MEMORIES = 1 export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/regfile.regfile diff --git a/flow/designs/asap7/riscv32i-regfile/regfile.regfile b/flow/designs/asap7/riscv32i-regfile/regfile.regfile index af9ae34bb7..45caaf857a 100644 --- a/flow/designs/asap7/riscv32i-regfile/regfile.regfile +++ b/flow/designs/asap7/riscv32i-regfile/regfile.regfile @@ -1,8 +1,8 @@ # riscv32i's `regfile` (src/riscv32i/regfile.v) as a register file of -# placed standard cells: 32 x 32, two combinational reads, one write, -# x0 reads 0 and drops its writes. +# standard cells, inlined in synthesis: 32 x 32, two combinational reads, +# one write through a clock gate per word, x0 reads 0 and drops its +# writes. module regfile -mode netlist words 32 bits 32 clock clk @@ -12,16 +12,8 @@ write wa3 wd3 we3 zero_word 0 write_style clock_gate cell icg ICGx1_ASAP7_75t_R -# Flat, 58 x 19 um: it stacks under the four FakeRAMs at riscv32i's own -# 62 %, where two folds (32 x 36 um) fit beside them at no utilisation -# above 35 %. The clock tree's free sites are sized for a 32-word file. -bit_folds 1 -service_sites 16 -tap_columns 16 cell flop DFFHQNx1_ASAP7_75t_R cell and2 AND2x2_ASAP7_75t_R cell or2 OR2x2_ASAP7_75t_R cell ao22 AO22x2_ASAP7_75t_R cell inv INVx1_ASAP7_75t_R -cell tap TAPCELL_ASAP7_75t_R -pin_layer M4 diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 5b677a560f..c9956ef5b7 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -12,6 +12,9 @@ /.lef abstract LEF per converted memory /regfiles.txt register files to dissolve into their cells after macro placement (`mode netlist`) + /inline.txt register files whose generated netlist + replaces their module in synthesis (those + not in AUTO_MEMORIES_MACRO_PLACE) /blackboxes.txt ` ` per line, area in um^2 as the .lib states it — what synthesis blackboxes and the cost it gives each @@ -141,16 +144,52 @@ def _lib_area(lib: Path) -> str: return m.group(1) +def _inline_spec(spec: Path, out: Path) -> Path: + """`spec` without its tap cell: an inlined register file's cells + land on the parent's rows, whose taps the floorplan places.""" + kept = [ + line + for line in spec.read_text().splitlines() + if line.split("#", 1)[0].split()[:2] != ["cell", "tap"] + ] + out.write_text("".join(l + "\n" for l in kept)) + return out + + +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)) + words = _spec_value(spec, "words") + bits = _spec_value(spec, "bits") + mem.rows = int(words[0][0]) if words else 0 + mem.bits = int(bits[0][0]) if bits else 0 + mem.read_ports = len(_spec_value(spec, "read")) + len( + _spec_value(spec, "read_banked") + ) + mem.write_ports = len(_spec_value(spec, "write")) + mem.reason = reason + return mem + + def run_regfiles( specs: list[Path], openroad: str | None, lefs: list[Path], verilog: list[Path], out_dir: Path, + macro_place: list[str] | None = None, ) -> list[schema.Memory]: """Generate every listed register file with OpenROAD's generate_regfile. - A register file is a macro to synthesis and to macro placement: its + A register file not named in AUTO_MEMORIES_MACRO_PLACE is inlined: its + generated netlist replaces the module's body in synthesis (inline.txt), + and from there on its cells are placed, sized and buffered with the + rest of the design. No macro, no abstract, no dissolve: the best shape + for a small design, where a macro's outline and channels cost more + core than the array saves. + + 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 its placed cells after macro placement (regfile_dissolve.tcl), which @@ -159,8 +198,16 @@ def run_regfiles( are not the module's stops the build here rather than miswire the parent. """ + macro_place = set(macro_place or []) if not specs: (out_dir / "regfiles.txt").write_text("") + (out_dir / "inline.txt").write_text("") + if macro_place: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_MACRO_PLACE names " + + " ".join(sorted(macro_place)) + + " but AUTO_MEMORIES_REGFILES lists no register file" + ) return [] if not openroad: raise SystemExit( @@ -170,13 +217,38 @@ def run_regfiles( raise SystemExit( "gen_memories: AUTO_MEMORIES_REGFILES needs TECH_LEF and SC_LEF" ) + modules = [_spec_module(spec) for spec in specs] + unknown = sorted(macro_place - set(modules)) + if unknown: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_MACRO_PLACE names " + + " ".join(unknown) + + ", which no AUTO_MEMORIES_REGFILES spec builds; it lists " + + " ".join(modules) + ) memories = [] dissolve = [] - for spec in specs: - module = _spec_module(spec) + inline = [] + for spec, module in zip(specs, modules): mode = _spec_mode(spec) rtl = _file_defining(module, verilog) tcl = out_dir / f"{module}.generate.tcl" + if module not in macro_place: + inline_spec = _inline_spec(spec, out_dir / f"{module}.inline.regfile") + tcl.write_text( + "".join(f"read_lef {lef}\n" for lef in lefs) + + f"generate_regfile -spec {inline_spec}" + + f" -check_ports {rtl} -verilog {out_dir / f'{module}.v'}\n" + ) + subprocess.check_call( + [openroad, "-exit", "-no_init", "-no_splash", str(tcl)] + ) + memories.append(_regfile_memory(spec, module, "register file, inlined")) + inline.append(module) + sys.stderr.write( + f"gen_memories: {module} -> register file, inlined ({spec.name})\n" + ) + continue tcl.write_text( "".join(f"read_lef {lef}\n" for lef in lefs) + f"generate_regfile -spec {spec} -check_ports {rtl}" @@ -184,18 +256,7 @@ def run_regfiles( + f" -lef {out_dir / f'{module}.lef'} -liberty {out_dir / f'{module}.lib'}\n" ) subprocess.check_call([openroad, "-exit", "-no_init", "-no_splash", str(tcl)]) - mem = schema.Memory( - name=module, kind="regfile", source="listed", spec=str(spec) - ) - words = _spec_value(spec, "words") - bits = _spec_value(spec, "bits") - mem.rows = int(words[0][0]) if words else 0 - mem.bits = int(bits[0][0]) if bits else 0 - mem.read_ports = len(_spec_value(spec, "read")) + len( - _spec_value(spec, "read_banked") - ) - mem.write_ports = len(_spec_value(spec, "write")) - mem.reason = f"register file, mode {mode}" + mem = _regfile_memory(spec, module, f"register file, mode {mode}") if mode == "netlist": _def_placement(out_dir / f"{module}.def", out_dir / f"{module}.place") dissolve.append(module) @@ -206,10 +267,13 @@ def run_regfiles( with (out_dir / "blackboxes.txt").open("a") as f: f.write( "".join( - f"{m.name} {_lib_area(out_dir / f'{m.name}.lib')}\n" for m in memories + f"{m.name} {_lib_area(out_dir / f'{m.name}.lib')}\n" + for m in memories + if m.name not in inline ) ) (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)) return memories @@ -223,6 +287,7 @@ def run( regfile_specs: list[Path] | None = None, openroad: str | None = None, lefs: list[Path] | None = None, + macro_place: list[str] | None = None, ) -> int: if platform != "asap7": sys.stderr.write(f"gen_memories: unsupported platform {platform}\n") @@ -264,7 +329,7 @@ def run( # After FakeRAM has written blackboxes.txt for the inferred memories, # and recorded in memories.json beside them. regfiles = run_regfiles( - regfile_specs or [], openroad, lefs or [], verilog or [], out_dir + regfile_specs or [], openroad, lefs or [], verilog or [], out_dir, macro_place ) if regfiles: listed = {m.name for m in regfiles} @@ -325,6 +390,13 @@ def main(argv: list[str] | None = None) -> int: type=Path, help="Technology and cell LEF for the generator (repeatable).", ) + p.add_argument( + "--macro-place", + action="append", + default=[], + help="A register file placed as a macro, AUTO_MEMORIES_MACRO_PLACE " + "(repeatable); the others are inlined.", + ) p.add_argument("--platform", required=True) p.add_argument( "--out-dir", @@ -349,6 +421,7 @@ def main(argv: list[str] | None = None) -> int: args.regfile_spec, args.openroad, args.lef, + args.macro_place, ) diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index 1f285d6e85..aaa39125a1 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -114,6 +114,7 @@ def test_flow_contract(self): sorted(p.name for p in mems_dir.iterdir()), [ "blackboxes.txt", + "inline.txt", "mem_128x32.lef", "mem_128x32.lib", "mem_128x32_pre_layout.lib", @@ -196,6 +197,7 @@ def test_no_memories_still_writes_contract_files(self): self.assertEqual(json.loads((d / "memories.json").read_text())["memories"], []) self.assertEqual((d / "memories" / "blackboxes.txt").read_text(), "") self.assertEqual((d / "memories" / "regfiles.txt").read_text(), "") + self.assertEqual((d / "memories" / "inline.txt").read_text(), "") def test_regfile_area_is_the_lib_cell_area(self): # a register file's blackboxes.txt line carries the area its diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index cfacc639dd..e650c1cde2 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -81,6 +81,29 @@ proc auto_memories_blackboxes { } { return [dict keys [auto_memories_areas]] } +# AUTO_MEMORIES: register files inlined (AUTO_MEMORIES_REGFILES not in +# AUTO_MEMORIES_MACRO_PLACE): the RTL module is read as a blackbox and its +# generated netlist of standard cells is read in its place. +proc auto_memories_inline { } { + if { ![env_var_equals AUTO_MEMORIES 1] } { + return {} + } + set f "$::env(RESULTS_DIR)/memories/inline.txt" + if { ![file exists $f] } { + return {} + } + set fh [open $f r] + set content [read $fh] + close $fh + return [regexp -all -inline {\S+} $content] +} + +proc read_auto_memories_inline { } { + foreach m [auto_memories_inline] { + read_verilog -overwrite "$::env(RESULTS_DIR)/memories/$m.v" + } +} + proc read_design_sources { } { # We are reading Verilog sources source $::env(SCRIPTS_DIR)/synth_stdcells.tcl @@ -134,7 +157,7 @@ proc read_design_sources { } { # Blackbox AUTO_MEMORIES-detected memory modules so their generated # liberty view wins over their behavioral bodies. - foreach m [auto_memories_blackboxes] { + foreach m [concat [auto_memories_blackboxes] [auto_memories_inline]] { lappend slang_args --blackboxed-module "$m" } @@ -142,6 +165,7 @@ proc read_design_sources { } { lappend slang_args {*}$::env(SYNTH_SLANG_ARGS) yosys read_slang {*}$slang_args + read_auto_memories_inline # Workaround for yosys-slang#119 setattr -unset init @@ -163,7 +187,7 @@ proc read_design_sources { } { if { [env_var_exists_and_non_empty SYNTH_BLACKBOXES] } { error "Non-empty SYNTH_BLACKBOXES unsupported with HDL frontend \"verific\"" } - if { [llength [auto_memories_blackboxes]] > 0 } { + if { [llength [concat [auto_memories_blackboxes] [auto_memories_inline]]] > 0 } { error "AUTO_MEMORIES unsupported with HDL frontend \"$::env(SYNTH_HDL_FRONTEND)\"" } } elseif { ![env_var_exists_and_non_empty SYNTH_HDL_FRONTEND] } { @@ -197,6 +221,10 @@ proc read_design_sources { } { } hierarchy -check -top $::env(DESIGN_NAME) } + if { [llength [auto_memories_inline]] > 0 } { + read_auto_memories_inline + hierarchy -check -top $::env(DESIGN_NAME) + } if { [env_var_exists_and_non_empty SYNTH_BLACKBOXES] } { hierarchy -check -top $::env(DESIGN_NAME) foreach m $::env(SYNTH_BLACKBOXES) { diff --git a/flow/scripts/variables.json b/flow/scripts/variables.json index 66eef69765..748163efc1 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -68,8 +68,14 @@ "default": 0, "description": "Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and \u2014 when idiomatic as an SRAM macro \u2014 given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.\n" }, + "AUTO_MEMORIES_MACRO_PLACE": { + "description": "Experimental. Space-separated list of module names of AUTO_MEMORIES_REGFILES register files to place as macros, blackboxed in synthesis and placed by the macro placer (and, in `mode netlist`, dissolved after macro placement). A register file not listed is inlined: its generated netlist of standard cells replaces the module's body in synthesis, and its cells are placed, sized and buffered with the rest of the design -- the better shape for a small design, where a macro's outline and channels cost more core than the array saves. A name no spec builds stops the build. Requires AUTO_MEMORIES=1.\n", + "stages": [ + "synth" + ] + }, "AUTO_MEMORIES_REGFILES": { - "description": "Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like an AUTO_MEMORIES macro and placed as one. A spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.\n", + "description": "Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`. A module named in AUTO_MEMORIES_MACRO_PLACE is blackboxed like an AUTO_MEMORIES macro and placed as one; any other is inlined as its generated netlist. A placed spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.\n", "stages": [ "synth" ] diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml index dea40d353a..74ba3b2ba7 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -235,15 +235,29 @@ AUTO_MEMORIES_REGFILES: bits, read and write ports by the RTL's own port names, and the cells; OpenROAD's generate_regfile checks the spec against the module's ports and writes `.lef`, `.lib` and - `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`, and the - module is blackboxed like an AUTO_MEMORIES macro and placed as one. A - spec in `mode netlist` then dissolves into its cells after macro + `_pre_layout.lib` under `$(RESULTS_DIR)/memories/`. A module + named in AUTO_MEMORIES_MACRO_PLACE is blackboxed like an AUTO_MEMORIES + macro and placed as one; any other is inlined as its generated + netlist. A placed spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its address decode left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1. stages: - synth +AUTO_MEMORIES_MACRO_PLACE: + description: > + Experimental. Space-separated list of module names of + AUTO_MEMORIES_REGFILES register files to place as macros, blackboxed + in synthesis and placed by the macro placer (and, in `mode netlist`, + dissolved after macro placement). A register file not listed is + inlined: its generated netlist of standard cells replaces the module's + body in synthesis, and its cells are placed, sized and buffered with + the rest of the design -- the better shape for a small design, where a + macro's outline and channels cost more core than the array saves. A + name no spec builds stops the build. Requires AUTO_MEMORIES=1. + stages: + - synth SYNTH_MEMORY_MAX_BITS: description: > Maximum number of bits for memory synthesis. From 468db41104e2f838915d7bf7e31ade5bc720623b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 7 Oct 2026 21:18:30 +0200 Subject: [PATCH 9/9] AUTO_MEMORIES_REGFILES: inline a register file in place of a parameterized module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inlined register file's generated netlist has no parameters; a parameterized RTL module (ibex_register_file_ff #(RV32E, DataWidth, DummyInstructions)) is instantiated with them, and hierarchy refused: "does not have a parameter named ...". The netlist is the module at the widths generate_regfile's port check confirmed, so the parameters are dropped from its instances before it is read. Signed-off-by: Øyvind Harboe Co-Authored-By: Claude Opus 5.5 --- flow/scripts/synth_preamble.tcl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index e650c1cde2..8efef2d4e1 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -100,6 +100,15 @@ proc auto_memories_inline { } { proc read_auto_memories_inline { } { foreach m [auto_memories_inline] { + # A parameterized RTL module (ibex_register_file_ff #(...)) is + # instantiated with parameters the generated netlist has no + # declaration for, which hierarchy refuses. The netlist is that module + # at the instance's widths, as generate_regfile's port check has + # confirmed: drop the parameters from its instances. + set dump [tee -q -s result.string dump t:$m] + foreach {- name} [regexp -all -inline -- {parameter \\(\S+)} $dump] { + setparam -unset $name t:$m + } read_verilog -overwrite "$::env(RESULTS_DIR)/memories/$m.v" } }