From e82ccee3ed9957120d1e612de2ffb8a7bf7ac96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 3 Nov 2026 09:09:37 +0100 Subject: [PATCH] AUTO_MEMORIES_REGFILES: register files of placed standard cells, built by generate_ram 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. memories/regfile_ram.py translates each to OpenROAD's generate_ram_netlist options (ports, banks, folds, the zero word, reset, read latency, write style and priority, cells), checks it against the module's ports from synthesis (a stray, missing or mis-sized port stops the build), and writes a wrapper in the module's place around the RAM, _ram: an active-high reset inverted for an active-low flop, addresses narrowed or widened. A register file is inlined unless AUTO_MEMORIES_MACRO_PLACE names it: its cells are then placed, sized and buffered with the rest of the design. A named one is a macro to synthesis and macro placement (an abstract LEF and a liberty of its area beside the FakeRAM views, in blackboxes.txt; a parameterized module's instances lose parameters the netlist does not declare), and 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 periphery left to global placement and the resizer; then eliminate_dead_logic. Where a row holds several words they sit side by side, and a word whose bits would run more than 150 um is folded into more rows: XiangShan's RobEntryFile and RenameBufferFile, dissolved into a parent, did not legalize otherwise (the parent buffers the word's selects inside the full array). `mode netlist`, the default, is the only mode. A spec with `mode macro` stops: a timing model written from generate_ram's netlist before any clock tree is built has the whole unbuffered clock net as its clock pin, 8.5 pF for 64 words of 256 bits on asap7, 32 pF for XiangShan's 954-bit Ftq queue, whose clock then arrived 7.7 ns late in its parent. A register file wanted as a macro is a design of its own: run it through the flow, where CTS builds its clock tree, and give the parent its abstract as a macro. A module named in a spec whose memory has an initial value stops the build: generate_ram's storage has none, and the value would be lost without a word. The spec's read_latency is generate_ram's -register_read_address. The RAMs the wrappers instantiate as macros are listed in regfile_cores.txt: they are in blackboxes.txt but no module of the sources, so the check that every AUTO_MEMORIES module is a black box after read_slang skips them. Uses generate_ram's register-file options (OpenROAD#11723), in tools/OpenROAD. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 130 ++++ docs/user/FlowVariables.md | 4 + flow/Makefile | 8 +- flow/scripts/macro_place.tcl | 28 + flow/scripts/memories/detect.py | 17 + flow/scripts/memories/detect_test.py | 19 + flow/scripts/memories/gen_memories.py | 235 +++++++- flow/scripts/memories/gen_memories_test.py | 85 +++ flow/scripts/memories/regfile_ram.py | 671 +++++++++++++++++++++ flow/scripts/memories/schema.py | 16 +- flow/scripts/regfile_dissolve.tcl | 352 +++++++++++ flow/scripts/synth_preamble.tcl | 67 +- flow/scripts/variables.json | 12 + flow/scripts/variables.yaml | 31 + 14 files changed, 1667 insertions(+), 8 deletions(-) create mode 100644 flow/scripts/memories/regfile_ram.py create mode 100644 flow/scripts/regfile_dissolve.tcl diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index d6c9b7aa47..2ff1dc513f 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -122,6 +122,134 @@ 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_ram` builds each as an array of placed +standard cells: a flop per bit, its write logic 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 +... +``` + +`memories/regfile_ram.py` translates the spec to `generate_ram_netlist` +options (ports, banks, folds, the zero word, reset, read latency, write +style and priority, cells) and checks it against the module's ports, +from synthesis: a port in one and not the other, or a data port of +another width, stops the build rather than miswire the parent. The RAM +is a block `_ram`, and a wrapper takes the module's place, +connecting the module's ports to the RAM's (an active-high reset +inverted for an active-low flop, an address narrowed or widened). What +happens next depends on `AUTO_MEMORIES_MACRO_PLACE`, the register files +to place as macros: + +- **Not listed (the default): inlined.** Synthesis reads the wrapper + and the RAM's netlist in the module's place; from there on their 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.** `_ram` is a macro to synthesis and macro + placement: its abstract `_ram.lef` and a liberty of its area and + pins land beside the FakeRAM views and it joins `blackboxes.txt`. In + `mode netlist` (the only one) `generate_ram` places only the array's + core and leaves its periphery (decode, select and input buffers) to + the parent, and the macro dissolves into its cells at the end of macro + placement (`regfile_dissolve.tcl`): the core lands FIRM where the + macro was placed, flipped as the macro was, on the parent's rows; the + periphery is left to global placement and the resizer, and dead logic + is eliminated as at synthesis. A large design whose floorplan plans + its register files lists them. +- **A macro of its own.** A register file wanted as a macro to the end + is a design of its own: run its module through the flow, its spec + listed and macro-placed so it dissolves there, and give the parent + its abstract as a macro. Its clock tree is then built and its timing + model is written from it. `mode macro` stops with that advice: a + timing model written from `generate_ram`'s netlist before any clock + tree would have the whole unbuffered clock net as one pin. + +A word holds its value between writes either through a hold mux per +bit (`write_style mux`, the default here) or through one integrated +clock gate per word, enabled by its write selects (`write_style +clock_gate`, `generate_ram`'s own default). Where a row holds more than +one word (`bank_columns`), each word's bits sit side by side, so its +selects and write enables are no longer than its bits; and a word whose +bits would run more than 150 um is folded into more rows. `tap_columns` +and `service_sites` leave empty columns in the array for the buffers a +parent puts on the nets that cross it. + +Listed register files appear in `memories.json` with kind `regfile`; +the rest have kind `fakeram` or `flops`. + +Moving the dissolve into OpenROAD is left for later: it would make it +faster, testable at unit level and maintained with the database it +edits. + +### 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 | Yes: `unused` names `test_en_i` and `dummy_instr_id_i`, ports the array does not use, and `async_reset rst_ni low` resets the flops to 0 as ibex does; simulated against ibex's RTL. | +| 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. + +### Status: at parity on riscv32i, on a smaller core + +riscv32i with its register file generated and inlined (a spec in +`AUTO_MEMORIES_REGFILES`, `zero_word 0`), at riscv32i's own +utilisation, against `designs/asap7/riscv32i`. To global route, 950 ps +clock, minimum period from placement parasitics on the global-route +database: + +| | flops | register file | +|---|---|---| +| minimum period | 950 ps | 943 ps | +| core area | 4 012 um2 | 3 503 um2 | + +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 **asap7 only.** `gen_memories.py` rejects any other platform. The @@ -183,3 +311,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 b1339b4928..449ec84425 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -104,6 +104,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_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. memories/regfile_ram.py checks the spec against the module's ports and has OpenROAD's generate_ram build it as `_ram`, under `$(RESULTS_DIR)/memories/`, with a wrapper in the module's place. 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 periphery 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.| | @@ -351,6 +353,8 @@ configuration file. - [ABC_SCRIPT](#ABC_SCRIPT) - [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) - [DFF_LIB_FILE](#DFF_LIB_FILE) diff --git a/flow/Makefile b/flow/Makefile index 759e13578d..bcef4554d9 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -279,10 +279,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,$(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)) $(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/scripts/macro_place.tcl b/flow/scripts/macro_place.tcl index 0789129e42..556cc30ccc 100644 --- a/flow/scripts/macro_place.tcl +++ b/flow/scripts/macro_place.tcl @@ -3,10 +3,38 @@ erase_non_stage_variables floorplan load_design 2_1_floorplan.odb 2_1_floorplan.sdc source_step_tcl PRE MACRO_PLACE +# A register file in `mode netlist` dissolves onto the parent's rows +# after macro placement, and the rows are then cut MACRO_ROWS_HALO_X/Y +# around every other macro: a halo that wide keeps them off its core, +# which fills its outline to the edge. +set regfile_list "$::env(RESULTS_DIR)/memories/regfiles.txt" +if { [env_var_equals AUTO_MEMORIES 1] && [file exists $regfile_list] } { + set f [open $regfile_list r] + set regfile_masters [split [string trim [read $f]] "\n"] + close $f + set dbu [[ord::get_db_tech] getDbUnitsPerMicron] + set halo {} + foreach v {MACRO_ROWS_HALO_X MACRO_ROWS_HALO_Y} { + set um [expr { [env_var_exists_and_non_empty $v] ? $::env($v) : 0 }] + lappend halo [expr { round($um * $dbu) }] + } + lassign $halo x y + foreach inst [[ord::get_db_block] getInsts] { + if { [[$inst getMaster] getName] in $regfile_masters } { + # the instance's own halo, left bottom right top, which the macro + # placer keeps clear of other macros + odb::dbBox_create $inst $x $y $x $y + } + } +} + 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/detect.py b/flow/scripts/memories/detect.py index 2343350429..83ab8ce65f 100644 --- a/flow/scripts/memories/detect.py +++ b/flow/scripts/memories/detect.py @@ -305,6 +305,23 @@ def scan_yosys_json( return dedupe(out) +def initialized_modules(data: dict | str | Path, top: str | None = None) -> set[str]: + """Modules holding a memory with an initial value: a $mem_v2 whose INIT + has a 0 or 1 bit (yosys writes an uninitialized memory's INIT as x).""" + if isinstance(data, (str, Path)): + data = json.loads(Path(data).read_text()) + out = set() + for mod_name, mod_info in data.get("modules", {}).items(): + name = mod_name[1:] if mod_name.startswith("\\") else mod_name + for cell in mod_info.get("cells", {}).values(): + if cell.get("type", "") not in ("$mem_v2", "$mem"): + continue + init = str(cell.get("parameters", {}).get("INIT", "")) + if "0" in init or "1" in init: + out.add(definition_name(name, top)) + return out + + def definition_name(module: str, top: str | None) -> str: """`array_64x114$top.u0` -> `array_64x114` for top module `top`. diff --git a/flow/scripts/memories/detect_test.py b/flow/scripts/memories/detect_test.py index c1f70e0ef7..dd5940b216 100644 --- a/flow/scripts/memories/detect_test.py +++ b/flow/scripts/memories/detect_test.py @@ -270,5 +270,24 @@ def test_inline_array_with_other_logic_is_named_per_cell(self): self.assertEqual([m.name for m in mems], ["am_regs.Memory"]) +class InitializedModulesTest(unittest.TestCase): + def test_only_a_memory_with_a_0_or_1_bit_is_initialized(self): + def module(init): + params = {"SIZE": 2, "WIDTH": 2, "RD_PORTS": 1, "WR_PORTS": 1} + if init is not None: + params["INIT"] = init + return {"cells": {"\\m": {"type": "$mem_v2", "parameters": params}}} + + data = { + "modules": { + "\\none": module(None), + "\\all_x": module("xxxx"), + "\\zeros": module("0000"), + "\\some$top.u0": module("xx1x"), + } + } + self.assertEqual(detect.initialized_modules(data, "top"), {"zeros", "some"}) + + if __name__ == "__main__": unittest.main() diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 98e3febea6..6256bab2d4 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -10,6 +10,14 @@ /.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`) + /inline.txt register files whose generated netlist + replaces their module in synthesis (those + not in AUTO_MEMORIES_MACRO_PLACE) + /regfile_cores.txt the RAMs those wrappers instantiate as + macros: in blackboxes.txt, but no module of + the sources, so not there after read_slang /blackboxes.txt ` ` per line, area in um^2 as the .lib states it — what synthesis blackboxes and the cost it gives each @@ -21,6 +29,8 @@ from __future__ import annotations import argparse +import json +import re import sys from pathlib import Path @@ -30,11 +40,173 @@ import detect # noqa: E402 import idiomatic # noqa: E402 +import regfile_ram # noqa: E402 import schema # noqa: E402 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 netlist`, the only mode, of a register-file spec. + + `mode macro` stops: a timing model written from generate_ram's + netlist before any clock tree is built has the whole unbuffered + clock net as its clock pin (8.5 pF for 64 x 256 bits on asap7). A + register file wanted as a macro is a design of its own: run it + through the flow and give its abstract to the parent as a macro.""" + for line in spec.read_text().splitlines(): + parts = line.split("#", 1)[0].split() + if len(parts) == 2 and parts[0] == "mode": + if parts[1] == "macro": + raise SystemExit( + f"gen_memories: {spec}: mode macro is not supported. A " + "register file wanted as a macro is a design of its own: " + "run it through the flow and give the parent its " + "abstract as a macro. Here, mode netlist dissolves it " + "into this design." + ) + if parts[1] != "netlist": + raise SystemExit( + f"gen_memories: {spec}: mode is netlist, not {parts[1]}" + ) + return "netlist" + + +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 _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], + out_dir: Path, + macro_place: list[str] | None = None, +) -> list[schema.Memory]: + """Generate every listed register file with OpenROAD's generate_ram, + through regfile_ram.py: a wrapper in the RTL module's place connects + its ports to the generated RAM's. + + 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 + 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. + """ + macro_place = set(macro_place or []) + if not specs: + (out_dir / "regfiles.txt").write_text("") + (out_dir / "inline.txt").write_text("") + (out_dir / "regfile_cores.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( + "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" + ) + 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 = [] + cores = [] + inline = [] + for spec, module in zip(specs, modules): + mode = _spec_mode(spec) + # the RTL module's place taken by a wrapper around the RAM + shape = mode if module in macro_place else "inline" + regfile_ram.check(spec) + ram = regfile_ram.generate(spec, module, shape, openroad, lefs, out_dir) + memories.append( + _regfile_memory(spec, module, f"register file, generate_ram, {shape}") + ) + inline.append(module) + if ram["macro"]: + cores.append(ram["block"]) + if ram["dissolve"]: + dissolve.append(ram["block"]) + sys.stderr.write( + f"gen_memories: {module} -> register file, generate_ram, " + f"{shape} ({spec.name})\n" + ) + with (out_dir / "blackboxes.txt").open("a") as f: + f.write( + "".join( + f"{name} {_lib_area(out_dir / f'{name}.lib')}\n" + for name in [m.name for m in memories if m.name not in inline] + cores + ) + ) + (out_dir / "regfiles.txt").write_text("".join(f"{m}\n" for m in dissolve)) + (out_dir / "inline.txt").write_text("".join(f"{m}\n" for m in inline)) + (out_dir / "regfile_cores.txt").write_text("".join(f"{m}\n" for m in cores)) + return memories + + def run( yosys_json: Path, memories_files: list[Path], @@ -42,6 +214,10 @@ def run( out_dir: Path, json_path: Path, top: str | None = None, + 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") @@ -49,12 +225,29 @@ def run( found = detect.scan_yosys_json(yosys_json, top) + # generate_ram builds storage with no initial value: a register file + # with one would lose it without a word + initialized = detect.initialized_modules(yosys_json, top) + for spec in regfile_specs or []: + module = _spec_module(spec) + if module in initialized: + raise SystemExit( + f"gen_memories: {module} ({spec}) has a memory with an initial " + "value, which a register file from generate_ram does not " + "have; remove it from AUTO_MEMORIES_REGFILES" + ) + idiomatic.apply(found) overrides: list[schema.Memory] = [] 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) @@ -75,8 +268,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 [], out_dir, macro_place + ) + if regfiles: + listed = {m.name for m in regfiles} + memories = [m for m in memories if m.name not in listed] + regfiles + schema.dump(memories, json_path, platform) + 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} " @@ -103,6 +306,32 @@ def main(argv: list[str] | None = None) -> int: help="User-supplied .memories file merged onto the " "detected set (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_ram (OPENROAD_EXE).", + ) + p.add_argument( + "--lef", + action="append", + default=[], + 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", @@ -129,6 +358,10 @@ def main(argv: list[str] | None = None) -> int: args.out_dir, args.json, args.top, + 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 e71aed7844..116cc596d3 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -114,9 +114,12 @@ 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", + "regfile_cores.txt", + "regfiles.txt", ], ) # one ` ` line; the area is the one the .lib states, @@ -224,6 +227,88 @@ 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(), "") + self.assertEqual((d / "memories" / "inline.txt").read_text(), "") + self.assertEqual((d / "memories" / "regfile_cores.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) + + def test_initialized_register_file_stops(self): + # a spec naming a module whose memory has an initial value stops: + # generate_ram's storage has none, and the value would be lost + d = Path(self.tmp.name) + netlist = json.loads(json.dumps(SAMPLE_YOSYS_NETLIST)) + cell = netlist["modules"]["\\cache_tags"]["cells"]["\\$mem_0"] + cell["parameters"]["INIT"] = "x" * 75 + "01" * 12 + "1" + netlist_json = d / "memories_inferred.json" + netlist_json.write_text(json.dumps(netlist)) + spec = d / "cache_tags.regfile" + spec.write_text("module cache_tags\n") + with self.assertRaises(SystemExit) as stop: + gen_memories.run( + netlist_json, + [], + "asap7", + d / "memories", + d / "memories.json", + regfile_specs=[spec], + ) + self.assertIn("cache_tags", str(stop.exception)) + self.assertIn("initial value", str(stop.exception)) + + def test_spec_mode(self): + # mode netlist is the only mode, and the default; mode macro stops + # with the advice to run the register file as a design of its own + d = Path(self.tmp.name) + spec = d / "rf.regfile" + spec.write_text("module rf\n") + self.assertEqual(gen_memories._spec_mode(spec), "netlist") + spec.write_text("module rf\nmode netlist\n") + self.assertEqual(gen_memories._spec_mode(spec), "netlist") + spec.write_text("module rf\nmode macro\n") + with self.assertRaises(SystemExit) as stop: + gen_memories._spec_mode(spec) + self.assertIn("design of its own", str(stop.exception)) + spec.write_text("module rf\nmode other\n") + with self.assertRaises(SystemExit): + gen_memories._spec_mode(spec) + + def test_spec_check(self): + # a spec missing a key the translation needs, or with a count that + # is not a positive integer, stops before generate_ram runs + d = Path(self.tmp.name) + spec = d / "rf.regfile" + good = "module rf\nwords 32\nbits 8\nclock clk\nread ra rd\nwrite wa wd we\n" + spec.write_text(good) + gen_memories.regfile_ram.check(spec) + for bad, message in ( + (good.replace("clock clk\n", ""), "no `clock`"), + (good.replace("read ra rd\n", ""), "no `read` or `read_banked`"), + (good.replace("words 32", "words 0"), "`words` takes one positive"), + (good.replace("bits 8", "bits eight"), "`bits` takes one positive"), + (good + "bit_folds 2 3\n", "`bit_folds` takes one positive"), + (good + "colour red\n", "unknown key `colour`"), + ): + spec.write_text(bad) + with self.assertRaises(SystemExit) as stop: + gen_memories.regfile_ram.check(spec) + self.assertIn(message, str(stop.exception)) if __name__ == "__main__": diff --git a/flow/scripts/memories/regfile_ram.py b/flow/scripts/memories/regfile_ram.py new file mode 100644 index 0000000000..69ad9aa796 --- /dev/null +++ b/flow/scripts/memories/regfile_ram.py @@ -0,0 +1,671 @@ +"""AUTO_MEMORIES_REGFILES through OpenROAD's generate_ram. + +A register-file spec (KEYS below) is translated to +generate_ram_netlist's options, and the generated RAM, a block named +_ram, stands in for the RTL module through a wrapper written in +the module's place: its ports, from the design's yosys JSON, connected +to generate_ram's (clk, rst_n, D0, we0, addr_w0, addr_r0, Q0, ...), an +active-high reset inverted for an active-low flop. The wrapper is +always inlined; the RAM is + +- inlined: its netlist follows the wrapper in .v; +- mode netlist: a macro with its periphery unplaced, which + regfile_dissolve.tcl dissolves after macro placement from + _ram.v (one instance a line) and _ram.place. + +The flops take the RTL's names where the spec gives them (store_name, +read_reg_name), for equivalence checking: renamed in the database from +generate_ram's own. With `periphery verilog` generate_ram writes the +periphery as a Verilog module, _ram, around the core, +_ram_core, which is then what is inlined or the macro. +""" + +from __future__ import annotations + +import json +import math +import os +import subprocess +from pathlib import Path + +# the longest a word's bits may run in a row before it is folded, um +WORD_SPAN_UM = 150 + +# the keys a spec may have: a misspelt one is refused, not ignored +KEYS = { + "module", + "mode", + "words", + "bits", + "clock", + "read", + "read_banked", + "write", + "write_priority", + "write_style", + "zero_word", + "banks", + "bank_columns", + "bank_order", + "bank_column_layout", + "read_latency", + "bit_folds", + "async_reset", + "reset", + "unused", + "store_name", + "read_reg_name", + "cell", + "periphery", + "pin_layer", + "tap_columns", + "service_sites", +} + +# a spec's `cell ` roles, as generate_ram's -cells names them +CELL_ROLES = { + "inv": "inv", + "and2": "and2", + "or2": "or2", + "ao22": "ao22", + "aoi22": "aoi22", + "nand2": "nand2", + "nor2": "nor2", + "icg": "clock_gate", + "tie_hi": "tie", + "buffer": "buffer", +} + + +# AUTO_MEMORIES is asap7's +DEFAULT_CELLS = { + "aoi22": "AOI22xp5_ASAP7_75t_R", + "nand2": "NAND2xp5_ASAP7_75t_R", + "nor2": "NOR2x1_ASAP7_75t_R", + "buffer": "BUFx2_ASAP7_75t_R", +} + + +def parse(spec: Path) -> list[list[str]]: + """The spec's lines as [key, value, ...], comments and blanks out.""" + lines = [] + for line in spec.read_text().splitlines(): + words = line.split("#", 1)[0].split() + if words: + lines.append(words) + return lines + + +def _values(lines: list[list[str]], key: str) -> list[list[str]]: + return [line[1:] for line in lines if line[0] == key] + + +def _one(lines: list[list[str]], key: str, default: str = "") -> str: + values = _values(lines, key) + return values[-1][0] if values and values[-1] else default + + +# keys with a count, which must be a positive integer +COUNTS = ("words", "bits", "bit_folds", "banks", "bank_columns") + + +def check(spec: Path) -> None: + """Refuse a spec with a key this translation does not read, a missing + key it needs, or a count that is not a positive integer.""" + lines = parse(spec) + for line in lines: + if line[0] not in KEYS: + raise SystemExit(f"regfile_ram: {spec}: unknown key `{line[0]}`") + if line[0] in COUNTS and ( + len(line) != 2 or not line[1].isdigit() or int(line[1]) == 0 + ): + raise SystemExit( + f"regfile_ram: {spec}: `{line[0]}` takes one positive integer," + f" not `{' '.join(line[1:])}`" + ) + keys = {line[0] for line in lines} + for key in ("module", "words", "bits", "clock", "write"): + if key not in keys: + raise SystemExit(f"regfile_ram: {spec}: no `{key}`") + if not keys & {"read", "read_banked"}: + raise SystemExit(f"regfile_ram: {spec}: no `read` or `read_banked`") + + +def _address_bits(words: int) -> int: + return max(1, math.ceil(math.log2(words))) + + +def _args(lines: list[list[str]], block: str, shape: str) -> list[str]: + """generate_ram_netlist's options for the spec.""" + words = int(_one(lines, "words")) + bits = int(_one(lines, "bits")) + folds = int(_one(lines, "bit_folds", "1")) + cells = {line[1]: line[2] for line in lines if line[0] == "cell"} + async_reset = _values(lines, "async_reset") + storage = cells.get("flop_r" if async_reset else "flop") + args = [ + "-name", + block, + "-mask_size", + str(-(-bits // folds)), + "-word_size", + str(bits), + "-num_words", + str(words), + "-r_ports", + str(len(_values(lines, "read"))), + "-w_ports", + str(len(_values(lines, "write"))), + "-write_style", + _one(lines, "write_style", "mux"), + # a balanced AND2 tree per word: the row decoder's chain is + # slower on a register file's read path + "-decoder", + "word", + ] + if len(_values(lines, "write")) > 1: + # two ports writing one word in a cycle: OR their data by + # default, as firtool's register files do + args += ["-write_priority", _one(lines, "write_priority", "or")] + if int(_one(lines, "bank_columns", "1")) > 1 and not _values( + lines, "bank_column_layout" + ): + # a row's words each contiguous: interleaved, a word's selects and + # write enables run the whole row (XiangShan's RobEntryFile: 6,483 + # slew violations in a parent, against 608) + args += ["-bank_column_layout", "contiguous"] + if storage: + args += ["-storage_cell", storage] + if folds > 1: + # a word's bits in folds rows, a slice each + args += ["-bit_folds", str(folds)] + if _values(lines, "read_banked"): + args += ["-rb_ports", str(len(_values(lines, "read_banked")))] + for key in ( + "zero_word", + "banks", + "bank_columns", + "bank_order", + "bank_column_layout", + ): + if _values(lines, key): + args += [f"-{key}", _one(lines, key)] + # the spec's read_latency is generate_ram's read address register + if _values(lines, "read_latency"): + args += ["-register_read_address", _one(lines, "read_latency")] + if async_reset: + args += ["-reset_value", "0"] + # asap7 cells where the spec names none, and a buffer for the select + # buffers (the smallest asap7 buffer is the hold buffer HB1xp67) + cells = DEFAULT_CELLS | cells + roles = [ + f"{CELL_ROLES[role]} {master}" + for role, master in cells.items() + if role in CELL_ROLES + ] + if roles: + args += ["-cells", "{" + " ".join(roles) + "}"] + # service columns: `service_sites` empty sites after + # every `tap_columns` storage columns, room for a parent's buffers in + # the array; none by default + sites = int(_one(lines, "service_sites", "0")) + if sites > 0: + every = _one(lines, "tap_columns", "8") + args += ["-service_columns", f"{{{every} {sites}}}"] + if _one(lines, "periphery") == "verilog": + args += ["-periphery", "verilog", "-pin_layer", _one(lines, "pin_layer", "M4")] + elif shape == "netlist": + args += ["-periphery", "unplaced", "-pin_layer", _one(lines, "pin_layer", "M4")] + return args + + +# The second session's Tcl: the generated block's ports, and for a macro +# its views, from its database. +_VIEWS_TCL = r""" +set block [ord::get_db_block] +set f [open {out}/{block}.ports w] +foreach term [$block getBTerms] {{ + if {{[$term getSigType] in {{POWER GROUND}}}} {{ continue }} + puts $f "[$term getName] [$term getIoType]" +}} +close $f +""" + +_MACRO_TCL = r""" +# the netlist, one instance a line, as regfile_dissolve.tcl reads it +set f [open {out}/{block}.v w] +foreach inst [$block getInsts] {{ + set conns {{}} + foreach it [$inst getITerms] {{ + set net [$it getNet] + if {{$net eq "NULL" || [$net getSigType] in {{POWER GROUND}}}} {{ continue }} + lappend conns ".[[$it getMTerm] getName](\\[$net getName] )" + }} + puts $f "[[$inst getMaster] getName] \\[$inst getName] ([join $conns {{, }}]);" +}} +close $f +# where each placed cell is, as the generator laid it out +set f [open {out}/{block}.place w] +foreach inst [$block getInsts] {{ + if {{![$inst isPlaced]}} {{ continue }} + lassign [$inst getLocation] x y + puts $f "[$inst getName] $x $y [$inst getOrient]" +}} +close $f +write_abstract_lef {out}/{block}.lef +# a liberty of its area and pins: a blackbox to synthesis +set dbu [[ord::get_db_tech] getDbUnitsPerMicron] +set die [$block getDieArea] +set area [expr {{double([$die dx]) / $dbu * [$die dy] / $dbu}}] +set f [open {out}/{block}.lib w] +puts $f "library({block}) {{" +puts $f " delay_model : table_lookup;" +puts $f " time_unit : \"1ns\";" +puts $f " voltage_unit : \"1V\";" +puts $f " current_unit : \"1uA\";" +puts $f " leakage_power_unit : \"1uW\";" +puts $f " capacitive_load_unit (1,pf);" +puts $f " pulling_resistance_unit : \"1kohm\";" +set buses [dict create] +foreach term [$block getBTerms] {{ + if {{[$term getSigType] in {{POWER GROUND}}}} {{ continue }} + set name [$term getName] + set dir [expr {{[$term getIoType] eq "INPUT" ? "input" : "output"}}] + if {{[regexp {{^(.*)\[(\d+)\]$}} $name -> base index]}} {{ + set hi [expr {{[dict exists $buses $base] ? max([lindex [dict get $buses $base] 1], $index) : $index}}] + dict set buses $base [list $dir $hi] + }} else {{ + dict set buses $name [list $dir -1] + }} +}} +set widths {{}} +dict for {{name info}} $buses {{ + set hi [lindex $info 1] + if {{$hi >= 0 && [lsearch $widths [expr {{$hi + 1}}]] < 0}} {{ lappend widths [expr {{$hi + 1}}] }} +}} +foreach w $widths {{ + puts $f " type ({block}_bus_$w) {{ base_type : array; data_type : bit; bit_width : $w; bit_from : [expr {{$w - 1}}]; bit_to : 0; downto : true; }}" +}} +puts $f " cell({block}) {{" +puts $f " area : $area;" +puts $f " is_macro_cell : true;" +puts $f " dont_touch : true;" +dict for {{name info}} $buses {{ + lassign $info dir hi + if {{$hi < 0}} {{ + puts $f " pin($name) {{ direction : $dir; capacitance : 0.001; }}" + }} else {{ + puts $f " bus($name) {{ bus_type : {block}_bus_[expr {{$hi + 1}}]; direction : $dir; capacitance : 0.001; }}" + }} +}} +puts $f " }}" +puts $f "}}" +close $f +""" + + +def _word_at(lines: list[list[str]], row: int, w: int) -> int: + """The word generate_ram (-decoder word) puts at (row, word in the + row): RamGen::wordAt.""" + words = int(_one(lines, "words")) + banks = int(_one(lines, "banks", "1")) + columns = int(_one(lines, "bank_columns", "1")) + per_bank = words // banks + bank = (row // per_bank) * columns + w + i = row % per_bank + if _one(lines, "bank_order", "contiguous") == "interleaved": + return i * banks + bank + return bank * per_bank + i + + +def _renames(lines: list[list[str]]) -> list[tuple[str, str]]: + """generate_ram's flop names to the spec's store_name and + read_reg_name, the RTL's names for equivalence checking.""" + renames = [] + words = int(_one(lines, "words")) + bits = int(_one(lines, "bits")) + columns = int(_one(lines, "bank_columns", "1")) + store = _one(lines, "store_name") + per_fold = bits // int(_one(lines, "bit_folds", "1")) + if store: + for row in range(words // columns): + for w in range(columns): + word = _word_at(lines, row, w) + if word == int(_one(lines, "zero_word", "-1")): + continue + for bit in range(bits): + renames.append( + ( + f"storage_{row}_{w}_{bit // per_fold * per_fold}" + f".bit{bit}.bit", + store.replace("{word}", str(word)).replace( + "{bit}", str(bit) + ), + ) + ) + reg = _one(lines, "read_reg_name") + # (with the periphery as Verilog the address registers are in the + # wrapper, under generate_ram's names) + if ( + reg + and _one(lines, "read_latency", "0") != "0" + and _one(lines, "periphery") != "verilog" + ): + banks = int(_one(lines, "banks", "1")) + a = _address_bits(words) + ab = _address_bits(words // banks) + # generate_ram's ports: writes, then plain reads, then each banked + # read port's banks; the spec's read ports, plain and banked, in + # the order it lists them + first_read = len(_values(lines, "write")) + plain = len(_values(lines, "read")) + spec_port = 0 + plain_seen = 0 + banked_seen = 0 + for line in lines: + if line[0] == "read": + p = first_read + plain_seen + plain_seen += 1 + for i in range(a): + renames.append( + ( + f"decoder_p{p}.addr_reg_p{p}_a{i}", + reg.replace("{port}", str(spec_port)) + .replace("{bank}", "0") + .replace("{bit}", str(i)), + ) + ) + spec_port += 1 + elif line[0] == "read_banked": + for b in range((len(line) - 1) // 2): + p = first_read + plain + banked_seen * banks + b + for i in range(ab): + renames.append( + ( + f"decoder_p{p}.addr_reg_p{p}_a{i}", + reg.replace("{port}", str(spec_port)) + .replace("{bank}", str(b)) + .replace("{bit}", str(i)), + ) + ) + banked_seen += 1 + spec_port += 1 + return renames + + +def _module_ports(yosys_json: Path, module: str) -> dict[str, tuple[str, int]]: + """The RTL module's ports, name -> (direction, width), from the + design's yosys JSON; empty where it does not have the module.""" + if not yosys_json.exists(): + return {} + modules = json.loads(yosys_json.read_text()).get("modules", {}) + for name, body in modules.items(): + if name.lstrip("\\") == module: + return { + port: (info["direction"], len(info["bits"])) + for port, info in body.get("ports", {}).items() + } + return {} + + +def _buses(port_lines: list[str]) -> dict[str, tuple[str, int]]: + """generate_ram's ports, base name -> (direction, width).""" + buses: dict[str, tuple[str, int]] = {} + for line in port_lines: + name, io = line.split() + base, width = name, 1 + if name.endswith("]") and "[" in name: + base = name[: name.rindex("[")] + width = int(name[name.rindex("[") + 1 : -1]) + 1 + direction = "input" if io == "INPUT" else "output" + old = buses.get(base, (direction, 0))[1] + buses[base] = (direction, max(old, width)) + return buses + + +def _wrapper( + lines: list[list[str]], + module: str, + block: str, + ram_ports: dict[str, tuple[str, int]], + rtl_ports: dict[str, tuple[str, int]], +) -> str: + """The module the RTL's stands in for: its ports to generate_ram's.""" + words = int(_one(lines, "words")) + bits = int(_one(lines, "bits")) + banks = int(_one(lines, "banks", "1")) + conns: dict[str, str] = {} + widths: dict[str, tuple[str, int]] = {} + + def port(name: str, direction: str, width: int) -> str: + widths.setdefault(name, rtl_ports.get(name, (direction, width))) + return name + + def fit(name: str, width: int) -> str: + """An RTL address as wide as generate_ram's.""" + have = widths[name][1] + if have == width: + return name + if have > width: + return f"{name}[{width - 1}:0]" + return f"{{{width - have}'b0, {name}}}" + + conns["clk"] = port(_one(lines, "clock", "clock"), "input", 1) + # ports the RTL has and the array ignores, any number a line + for key in ("reset", "unused"): + for values in _values(lines, key): + for name in values: + port(name, "input", 1) + for values in _values(lines, "async_reset"): + name = port(values[0], "input", 1) + low = values[1] == "low" if len(values) > 1 else True + if "rst_n" in ram_ports: + conns["rst_n"] = name if low else f"~{name}" + else: + conns["rst"] = f"~{name}" if low else name + reads = _values(lines, "read") + a = _address_bits(words) + for i, (addr, data) in enumerate(reads): + conns[f"addr_r{i}"] = fit(port(addr, "input", a), a) + conns["Q" if len(reads) == 1 else f"Q{i}"] = port(data, "output", bits) + writes = _values(lines, "write") + for w, values in enumerate(writes): + suffix = "" if len(writes) == 1 else str(w) + conns[f"addr_w{suffix}"] = fit(port(values[0], "input", a), a) + conns[f"D{suffix}"] = port(values[1], "input", bits) + conns[f"we{suffix}"] = ( + port(values[2], "input", 1) if len(values) > 2 else "1'b1" + ) + ab = _address_bits(words // banks) + for k, values in enumerate(_values(lines, "read_banked")): + for b in range(len(values) // 2): + conns[f"addr_rb{k}_b{b}"] = fit(port(values[2 * b], "input", ab), ab) + conns[f"Qrb{k}_b{b}"] = port(values[2 * b + 1], "output", bits) + for name in ram_ports: + if name not in conns: + raise SystemExit( + f"regfile_ram: {module}: generate_ram's port {name} has no " + "counterpart in the spec" + ) + # the spec's ports are the module's: a stray or missing one would be + # left unconnected in the parent, not refused + if rtl_ports: + stray = sorted(set(widths) - set(rtl_ports)) + missing = sorted(set(rtl_ports) - set(widths)) + if stray or missing: + raise SystemExit( + f"regfile_ram: {module}: the spec's ports are not the " + f"module's: not in the module {stray}, not in the spec " + f"{missing} (list ports the array ignores with `unused`)" + ) + data = [values[1] for values in reads + writes] + [ + name for values in _values(lines, "read_banked") for name in values[1::2] + ] + for name in data: + if rtl_ports[name][1] != bits: + raise SystemExit( + f"regfile_ram: {module}: {name} is " + f"{rtl_ports[name][1]} bits, the spec's bits {bits}" + ) + # the RTL's ports in its own order where the JSON has them + order = list(rtl_ports) or list(widths) + for name in widths: + if name not in order: + order.append(name) + decl = [] + for name in order: + direction, width = widths[name] if name in widths else rtl_ports[name] + decl.append(f" {direction} {'' if width == 1 else f'[{width - 1}:0] '}{name}") + body = ",\n".join(decl) + inst = ",\n".join(f" .{p}({c})" for p, c in conns.items()) + return ( + f"// generated by regfile_ram.py: {module} as generate_ram's {block}\n" + f"module {module} (\n{body}\n);\n {block} ram (\n{inst}\n );\nendmodule\n" + ) + + +def _module_header_ports(verilog: str, module: str) -> dict[str, tuple[str, int]]: + """A module's ports, name -> (direction, width), from its header.""" + body = verilog[verilog.index(f"module {module} (") :] + body = body[: body.index(");")] + ports = {} + for line in body.splitlines(): + words = line.strip().rstrip(",").split() + if words and words[0] in ("input", "output"): + width = 1 + if len(words) == 3 and words[1].startswith("["): + width = int(words[1][1 : words[1].index(":")]) + 1 + ports[words[-1]] = (words[0], width) + return ports + + +def generate( + spec: Path, + module: str, + shape: str, + openroad: str, + lefs: list[Path], + out_dir: Path, +) -> dict: + """Build the spec's RAM, shape inline or netlist; return what + the flow lists: the block, and whether it is a macro and dissolves.""" + lines = parse(spec) + ram = f"{module}_ram" + # -periphery verilog: the block is the core, the periphery a module + verilog_periphery = _one(lines, "periphery") == "verilog" + block = f"{ram}_core" if verilog_periphery else ram + periphery_v = out_dir / f"{ram}.periphery.v" + libs = os.environ.get("LIB_FILES", "").split() + if not libs: + raise SystemExit("regfile_ram: generate_ram needs LIB_FILES") + reads = "".join(f"read_liberty {lib}\n" for lib in libs) + gen = out_dir / f"{module}.generate.tcl" + outline = out_dir / f"{block}.outline" + + def build(spec_lines: list[list[str]]) -> tuple[float, float]: + gen.write_text( + reads + + "".join(f"read_lef {lef}\n" for lef in lefs) + + "generate_ram_netlist " + + " ".join(_args(spec_lines, ram, shape)) + + (f" -wrapper_verilog {periphery_v}" if verilog_periphery else "") + + f"\nwrite_db {out_dir / f'{block}.odb'}\n" + + "set die [[ord::get_db_block] getDieArea]\n" + + f"set f [open {outline} w]\n" + + 'puts $f "[$die dx] [$die dy] [[ord::get_db_tech] getDbUnitsPerMicron]"\n' + + "close $f\n" + ) + subprocess.check_call([openroad, "-exit", "-no_init", "-no_splash", str(gen)]) + width, height, _ = outline.read_text().split() + return float(width), float(height) + + width, height = build(lines) + # where the spec names no folds, fold a word into a power of two of + # rows while + # + # - the array is more than 8 times as wide as tall, toward the square + # (XiangShan's IntRegFile, 12 reads and 9 writes, 48 to one); a strip + # of a few to one can be the shape that fits (riscv32i's, 6, sits + # under its FakeRAMs), so that alone does not fold; + # - or one word's bits span more than WORD_SPAN_UM: its selects and + # write enables, driven from one cell, are that long, and a parent + # buffers them inside the full array, where nothing legalizes + # (XiangShan's RenameBufferFile, a word a row 258 um wide, failed; + # RobEntryFile at 74 um and IntRegFile at 96 um a word did not). + # + # A spec names bit_folds to choose. + bits = int(_one(lines, "bits")) + dbu = float(outline.read_text().split()[2]) + # the words side by side in a row, each contiguous (see _args) unless + # the spec interleaves them, when a word spans the row + row_words = ( + int(_one(lines, "bank_columns", "1")) + if _one(lines, "bank_column_layout", "contiguous") == "contiguous" + else 1 + ) + span_um = width / dbu / row_words + if not _values(lines, "bit_folds"): + folds = 1 + # a short last fold is allowed, an empty one is not + while ( + folds * 2 <= bits + and (folds * 2 - 1) * -(-bits // (folds * 2)) < bits + and ( + (width > 8 * height and (folds * 2) ** 2 <= 2 * width / height) + or span_um / folds > WORD_SPAN_UM + ) + ): + folds *= 2 + if folds > 1: + lines = lines + [["bit_folds", str(folds)]] + build(lines) + views = out_dir / f"{module}.views.tcl" + tcl = reads + f"read_db {out_dir / f'{block}.odb'}\n" + # the RTL's flop names, for equivalence checking + renames = _renames(lines) + if renames: + (out_dir / f"{block}.renames").write_text( + "".join(f"{{{old}}} {{{new}}}\n" for old, new in renames) + ) + tcl += ( + f"set f [open {out_dir / f'{block}.renames'} r]\n" + "while {[gets $f line] >= 0} {\n" + " lassign $line old new\n" + " set inst [[ord::get_db_block] findInst $old]\n" + ' if {$inst eq "NULL"} { error "regfile_ram: no $old to name $new" }\n' + " $inst rename $new\n" + "}\n" + "close $f\n" + ) + tcl += _VIEWS_TCL.format(out=out_dir, block=block) + if shape == "inline": + tcl += f"write_verilog {out_dir / f'{block}.netlist.v'}\n" + else: + tcl += _MACRO_TCL.format(out=out_dir, block=block) + views.write_text(tcl) + subprocess.check_call([openroad, "-exit", "-no_init", "-no_splash", str(views)]) + core_ports = _buses((out_dir / f"{block}.ports").read_text().splitlines()) + ram_ports = core_ports + periphery = "" + if verilog_periphery: + periphery = periphery_v.read_text() + ram_ports = _module_header_ports(periphery, ram) + rtl_ports = _module_ports(out_dir.parent / "memories_inferred.json", module) + wrapper = _wrapper(lines, module, ram, ram_ports, rtl_ports) + if periphery: + wrapper += "\n" + periphery + if shape == "inline": + wrapper += "\n" + (out_dir / f"{block}.netlist.v").read_text() + else: + # the macro, a blackbox to synthesis + decl = ",\n".join( + f" {d} {'' if w == 1 else f'[{w - 1}:0] '}{n}" + for n, (d, w) in core_ports.items() + ) + wrapper += f"\n(* blackbox *)\nmodule {block} (\n{decl}\n);\nendmodule\n" + (out_dir / f"{module}.v").write_text(wrapper) + return { + "block": block, + "macro": shape != "inline", + "dissolve": shape == "netlist", + } diff --git a/flow/scripts/memories/schema.py b/flow/scripts/memories/schema.py index 754ae8373c..9141b9cc76 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_ram 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, 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..a797461fbe --- /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 regfile_ram.py 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/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 268f5cd9da..3a1872c8c7 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -83,6 +83,55 @@ 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] +} + +# AUTO_MEMORIES: the RAMs the register file wrappers instantiate as +# macros. They are in blackboxes.txt but are no module of the sources: +# they arrive with the wrappers' netlists, after read_slang. +proc auto_memories_regfile_cores { } { + if { ![env_var_equals AUTO_MEMORIES 1] } { + return {} + } + set f "$::env(RESULTS_DIR)/memories/regfile_cores.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] { + # 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 regfile_ram.py'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" + } +} + proc read_design_sources { } { # We are reading Verilog sources source $::env(SCRIPTS_DIR)/synth_stdcells.tcl @@ -136,7 +185,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" } @@ -144,7 +193,15 @@ proc read_design_sources { } { lappend slang_args {*}$::env(SYNTH_SLANG_ARGS) yosys read_slang {*}$slang_args - auto_memories_check_blackboxed [auto_memories_blackboxes] + set cores [auto_memories_regfile_cores] + set sourced {} + foreach m [auto_memories_blackboxes] { + if { [lsearch -exact $cores $m] < 0 } { + lappend sourced $m + } + } + auto_memories_check_blackboxed $sourced + read_auto_memories_inline # Workaround for yosys-slang#119 setattr -unset init @@ -166,7 +223,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] } { @@ -202,6 +259,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 50c1517476..7ac2d93f3b 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -74,6 +74,18 @@ "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. memories/regfile_ram.py checks the spec against the module's ports and has OpenROAD's generate_ram build it as `_ram`, under `$(RESULTS_DIR)/memories/`, with a wrapper in the module's place. 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 periphery 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 2cd82356fd..a2119190de 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -256,6 +256,37 @@ SYNTH_VERILOG_SURGERY_ARGS: Arguments to SYNTH_VERILOG_SURGERY, before the ones the flow adds. 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. + memories/regfile_ram.py checks the spec against the module's ports + and has OpenROAD's generate_ram build it as `_ram`, under + `$(RESULTS_DIR)/memories/`, with a wrapper in the module's place. 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 periphery 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.