From 0848b176df2edfd371de653d9faf55d6b0bffdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:42:54 +0200 Subject: [PATCH] AUTO_MEMORIES_REGFILES: register files as macros of placed standard cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUTO_MEMORIES_REGFILES lists register-file specs; OpenROAD's generate_regfile builds each as placed standard cells during canonicalization. In mode netlist a file is a macro through synthesis and macro placement and then dissolves into its cells. AUTO_MEMORIES_MACRO_PLACE names the files placed as macros; the others are inlined in synthesis. Carried: - OpenROAD 0010: The-OpenROAD-Project/OpenROAD#11669, its net diff at its head, as one patch. - ORFS 0089, 0091, 0099: the three commits of The-OpenROAD-Project/OpenROAD-flow-scripts#4651 as rebuilt on #4650 (blackboxes.txt lines carry each register file's area), without its example designs (asap7/regfile, asap7/riscv32i-regfile, src/regfile). They apply after 0090 (#4650, #1205). 0090 loses the hunks it carried for the retired 0071 (its register files' DEF die area); the rest of it is unchanged. STRUCTURED_MEMORIES and STRUCTURED_PLACEMENT (ORFS patches 0071, 0078, 0082, 0084) are retired. A flow that still sets either one fails and the error names the replacement: at load time in arguments or sources, and at analysis in user_arguments or on a stage rule. test/structured_netlist, test/planned_parent, the XiangShan flows, tools/structured_gen/test and tools/macro_select move to the new variables. test/regfile is the flow's own fixture: the dissolve, every register clocked (macro and inlined), global route, simulation against the RTL, and synthesis with SYNTH_MINIMUM_KEEP_SIZE=1. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 8 +- config_mk_parser.py | 8 +- ideas/structured-macros.md | 5 + orfs_source.bzl | 51 +- .../0010-openroad-ram-generate-regfile.patch | 3661 +++++++++++++++++ patches/0071-orfs-structured-memories.patch | 230 -- patches/0078-orfs-structured-netlists.patch | 316 -- ...-orfs-structured-netlists-dead-cells.patch | 73 - ...-orfs-structured-netlists-dont-touch.patch | 145 - .../0089-orfs-auto-memories-regfiles.patch | 984 +++++ .../0090-orfs-auto-memories-keep-cost.patch | 56 +- .../0091-orfs-auto-memories-macro-place.patch | 541 +++ ...99-orfs-regfile-inline-parameterized.patch | 39 + private/attrs.bzl | 7 - private/design_dsl.bzl | 2 + private/rules.bzl | 31 +- private/stages.bzl | 26 + test/BUILD | 51 + .../designs/asap7/xiangshan/BUILD | 17 +- .../designs/asap7/xiangshan/plan/netlists.txt | 5 - .../asap7/xiangshan/plan/place_macros.tcl | 51 +- .../designs/asap7/xiangshan/xiangshan.bzl | 38 +- test/planned_parent/BUILD.bazel | 20 +- test/planned_parent/leftover_macros.tcl | 12 +- test/planned_parent/leftover_macros_test.py | 28 +- test/planned_parent/plan/netlists.txt | 5 - test/planned_parent/plan/place_macros.tcl | 51 +- test/planned_parent/plan_drift_test.py | 1 - test/planned_parent/planned.bzl | 12 +- test/planned_parent/regen.sh | 2 +- test/regfile/BUILD.bazel | 159 + test/regfile/RegFile.regfile | 22 + test/regfile/check_clocked.py | 29 + test/regfile/check_dissolve.py | 59 + test/regfile/clocked_probe.tcl | 16 + test/regfile/constraint.sdc | 7 + test/regfile/dissolve_probe.tcl | 71 + test/regfile/regfile_top.v | 60 + test/regfile/sim/BUILD.bazel | 84 + test/regfile/sim/regfile.regfile | 17 + test/regfile/sim/tb_main.cc | 60 + test/regfile/sim/tb_top.v | 23 + test/retired_arguments_test.bzl | 24 + test/structured_netlist/BUILD.bazel | 56 +- test/structured_netlist/SmallFile.regfile | 4 +- test/structured_netlist/placement.txt | 2 - test/structured_netlist/scaffold.bzl | 14 +- test/structured_netlist/scaffold_gen.py | 24 +- test/structured_netlist/small_file.sv | 62 - tools/macro_select/plan_floorplan.py | 129 +- tools/macro_select/plan_floorplan_test.py | 36 +- tools/structured_gen/BUILD.bazel | 52 + tools/structured_gen/golden/banked.regfile | 21 + tools/structured_gen/golden/folds.regfile | 17 + tools/structured_gen/golden/netlist.regfile | 19 + tools/structured_gen/golden/plain.regfile | 18 + .../structured_gen/golden/registered.regfile | 21 + tools/structured_gen/golden_test.sh | 36 + tools/structured_gen/test/BUILD.bazel | 7 +- tools/structured_gen/test/rf_parent.sv | 2 +- 60 files changed, 6416 insertions(+), 1211 deletions(-) create mode 100644 patches/0010-openroad-ram-generate-regfile.patch delete mode 100644 patches/0071-orfs-structured-memories.patch delete mode 100644 patches/0078-orfs-structured-netlists.patch delete mode 100644 patches/0082-orfs-structured-netlists-dead-cells.patch delete mode 100644 patches/0084-orfs-structured-netlists-dont-touch.patch create mode 100644 patches/0089-orfs-auto-memories-regfiles.patch create mode 100644 patches/0091-orfs-auto-memories-macro-place.patch create mode 100644 patches/0099-orfs-regfile-inline-parameterized.patch delete mode 100644 test/coremark_joule/designs/asap7/xiangshan/plan/netlists.txt delete mode 100644 test/planned_parent/plan/netlists.txt create mode 100644 test/regfile/BUILD.bazel create mode 100644 test/regfile/RegFile.regfile create mode 100644 test/regfile/check_clocked.py create mode 100644 test/regfile/check_dissolve.py create mode 100644 test/regfile/clocked_probe.tcl create mode 100644 test/regfile/constraint.sdc create mode 100644 test/regfile/dissolve_probe.tcl create mode 100644 test/regfile/regfile_top.v create mode 100644 test/regfile/sim/BUILD.bazel create mode 100644 test/regfile/sim/regfile.regfile create mode 100644 test/regfile/sim/tb_main.cc create mode 100644 test/regfile/sim/tb_top.v create mode 100644 test/retired_arguments_test.bzl delete mode 100644 test/structured_netlist/placement.txt delete mode 100644 test/structured_netlist/small_file.sv create mode 100644 tools/structured_gen/golden/banked.regfile create mode 100644 tools/structured_gen/golden/folds.regfile create mode 100644 tools/structured_gen/golden/netlist.regfile create mode 100644 tools/structured_gen/golden/plain.regfile create mode 100644 tools/structured_gen/golden/registered.regfile create mode 100755 tools/structured_gen/golden_test.sh diff --git a/MODULE.bazel b/MODULE.bazel index 3fc2d2ad8..71d2cebbc 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -103,6 +103,12 @@ archive_override( # pull request carries a gtest. Retires at a bump onto an OpenROAD # that has it. "//patches:0004-openroad-dpl-window-supply.patch", + # 0010: generate_regfile in the ram module, a multi-port register + # file as placed standard cells, which AUTO_MEMORIES_REGFILES + # (ORFS patch 0089) runs. The net diff of upstream + # The-OpenROAD-Project/OpenROAD#11669; retires at a bump onto an + # OpenROAD that has it. + "//patches:0010-openroad-ram-generate-regfile.patch", ], strip_prefix = "OpenROAD-22b7c537718e77cd3e7b6c0e883d966bf7e92563", urls = ["https://github.com/The-OpenROAD-Project/OpenROAD/archive/22b7c537718e77cd3e7b6c0e883d966bf7e92563.tar.gz"], @@ -853,7 +859,7 @@ http_archive( "//test/coremark_joule/xiangshan:patches/0007-xiangshan-rob-cell-packed-ports.patch", # Carried, not upstreamed: the ROB entry payload as a register-file # module, RobEntryFile, which the planned parent's - # STRUCTURED_MEMORIES names (see the patch header). + # AUTO_MEMORIES_REGFILES names (see the patch header). "//test/coremark_joule/xiangshan:patches/0008-xiangshan-rob-entry-file.patch", ], sha256 = "e4b48e602db1ee00835ea8e99aef9585b16fd34f9049fdfc37c5208625c6a002", diff --git a/config_mk_parser.py b/config_mk_parser.py index 7ed7f788f..fa887b511 100644 --- a/config_mk_parser.py +++ b/config_mk_parser.py @@ -61,13 +61,13 @@ # canonicalization, so they have to be staged like any other source # rather than passed as a string. "ADDITIONAL_MEMORIES", - # STRUCTURED_MEMORIES: register-file spec files gen_memories.py hands - # to tools/structured_gen during canonicalization; staged like the - # .memories files above. - "STRUCTURED_MEMORIES", # SYNTH_VERILOG_SURGERY: the script that edits copies of the sources # before synthesis; run during canonicalization, so staged. "SYNTH_VERILOG_SURGERY", + # AUTO_MEMORIES_REGFILES: register-file spec files gen_memories.py + # hands to OpenROAD's generate_regfile during canonicalization; + # staged like the .memories files above. + "AUTO_MEMORIES_REGFILES", } # Variables that are purely for path resolution, not passed to orfs_flow() diff --git a/ideas/structured-macros.md b/ideas/structured-macros.md index 5005bffe5..51ca2c776 100644 --- a/ideas/structured-macros.md +++ b/ideas/structured-macros.md @@ -2,6 +2,11 @@ *Plan, 2026-09-21, for the record in PR #1055.* +*2026-10-06: STRUCTURED_MEMORIES and STRUCTURED_PLACEMENT (ORFS patches +0071, 0078, 0082, 0084) are replaced by AUTO_MEMORIES_REGFILES (ORFS patch +0089 on OpenROAD patch 0010): a register file is a macro through +synthesis and macro placement and dissolves into its cells after it.* + ![The parent with its generated macros, and the tile structure of one generated array](structured-macros.png) ## Why diff --git a/orfs_source.bzl b/orfs_source.bzl index 5332babc9..204390c79 100644 --- a/orfs_source.bzl +++ b/orfs_source.bzl @@ -78,14 +78,6 @@ ORFS_PATCHES = [ # cells of their own type. # Not upstreamed -- retire at a bump onto an ORFS with such a hook. Label("//patches:0068-orfs-synth-techmap-hook.patch"), - # STRUCTURED_MEMORIES: a design lists register-file spec files, one - # per module, and gen_memories.py runs tools/structured_gen for each - # -- a placed standard-cell macro checked against the module's - # ports, its views beside the AUTO_MEMORIES ones, the module in - # blackboxes.txt. Explicit by design; XiangShan's register files are - # Reg(Vec) and no detector sees them. Not upstreamed -- retire at a - # bump onto an ORFS that can declare a generated macro per module. - Label("//patches:0071-orfs-structured-memories.patch"), Label("//patches:0073-orfs-fakeram-declared-pins-write-mask.patch"), Label("//patches:0074-orfs-skip-extract-fa.patch"), # With GPL_TIMING_DRIVEN and GPL_ROUTABILITY_DRIVEN both off, the place @@ -93,29 +85,18 @@ ORFS_PATCHES = [ # pins-blind solve, place_pins and a second solve. Not upstreamed -- # retire at a bump onto an ORFS that places pins with global placement. Label("//patches:0077-orfs-gpl-place-ios.patch"), - # 0078: STRUCTURED_MEMORIES in mode netlist: the generated array's cells - # placed FIRM into the parent at floorplan (STRUCTURED_PLACEMENT) instead - # of a macro. Carried, not upstreamed. See ideas/structured-macros.md. - Label("//patches:0078-orfs-structured-netlists.patch"), # 0081: the memory detector names a slang-uniquified module # (`array_512x17$Frontend...`) for its definition and collapses the # copies: read_slang blackboxes by definition name, so the uniquified # names never matched and the arrays were flattened into their tables # (Frontend's TAGE partition, 64 x 8704 bits). Carried, not upstreamed. Label("//patches:0081-orfs-memories-slang-uniquified-names.patch"), - # 0082: structured_netlists.tcl skips a placed netlist's cells that - # eliminate_dead_logic removed in the synth ODB step (a dead bit of the - # array takes its whole column) and reports them; FLW-0005 stays for a - # netlist with no cell in the design. Builds on 0078; retires with it. - Label("//patches:0082-orfs-structured-netlists-dead-cells.patch"), # 0083: the macro step skips rtl_macro_placer when MACRO_PLACEMENT_TCL - # fixed every macro; run anyway it refuses the FIRM cells of a placed - # structured netlist (MPL-0050). Carried, not upstreamed. + # fixed every macro (a planned parent: its blocks and register files + # placed by the plan). Written for the FIRM cells of the retired + # structured netlists, which it refused (MPL-0050). Carried, not + # upstreamed. Label("//patches:0083-orfs-macro-place-all-fixed.patch"), - # 0084: a placed netlist's cells are dont_touch as well as FIRM; - # repair_design upsized 378 of them in place and the wider masters - # overlapped their fixed neighbours. Builds on 0078; retires with it. - Label("//patches:0084-orfs-structured-netlists-dont-touch.patch"), # 0085: SKIP_PIN_ACCESS runs the global route without building access # points. pin_access fails on a macro pin it cannot reach (DRT-0073), # which on a hierarchical design is one pin of one block between a @@ -147,9 +128,9 @@ ORFS_PATCHES = [ # its name, so keep_hierarchy -min_cost (SYNTH_MINIMUM_KEEP_SIZE) # accepts it; any AUTO_MEMORIES design with a keep threshold stopped # on "Missing cost information on instanced blackbox". Upstream as - # The-OpenROAD-Project/OpenROAD-flow-scripts#4650; also gives 0071's - # register files their area. Retires at a bump onto an ORFS that - # carries #4650. + # The-OpenROAD-Project/OpenROAD-flow-scripts#4650; 0089's register + # files write their area after it. Retires at a bump onto an ORFS + # that carries #4650. Label("//patches:0090-orfs-auto-memories-keep-cost.patch"), # 0094: SYNTH_VERILOG_SURGERY, a script that edits copies of # VERILOG_FILES before synthesis reads them, so vendored RTL stays as @@ -157,6 +138,24 @@ ORFS_PATCHES = [ # The-OpenROAD-Project/OpenROAD-flow-scripts#4649; retires at a # //:bump onto an ORFS that carries it. Label("//patches:0094-orfs-synth-verilog-surgery.patch"), + # 0089: AUTO_MEMORIES_REGFILES: register files listed by spec are + # built by OpenROAD's generate_regfile (OpenROAD patch 0010) as + # macros of placed standard cells; in `mode netlist` each dissolves + # into its cells at the end of macro placement, its core FIRM where + # the macro was placed, its address decode left to placement. + # Replaces STRUCTURED_MEMORIES (0071, 0078, 0082, 0084). Upstream as + # The-OpenROAD-Project/OpenROAD-flow-scripts#4651, without its + # example designs; retires at a bump onto an ORFS that carries it. + Label("//patches:0089-orfs-auto-memories-regfiles.patch"), + # 0091: AUTO_MEMORIES_MACRO_PLACE lists the register files placed as + # macros; any other AUTO_MEMORIES_REGFILES register file is inlined, + # its generated netlist read in synthesis in the module's place. On + # 0089; the second commit of ORFS #4651. + Label("//patches:0091-orfs-auto-memories-macro-place.patch"), + # 0099: an inlined register file in place of a parameterized module: + # its instances' parameters dropped before the netlist is read. On + # 0091; the third commit of ORFS #4651. + Label("//patches:0099-orfs-regfile-inline-parameterized.patch"), ] # Generate the BUILD file for any design directory that has a config.mk diff --git a/patches/0010-openroad-ram-generate-regfile.patch b/patches/0010-openroad-ram-generate-regfile.patch new file mode 100644 index 000000000..5004c3bf0 --- /dev/null +++ b/patches/0010-openroad-ram-generate-regfile.patch @@ -0,0 +1,3661 @@ +From 691d8e15184d6d7baf40944536dc5980f8b9a685 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=C3=98yvind=20Harboe?= +Date: Fri, 9 Oct 2026 10:00:00 +0200 +Subject: [PATCH] ram: generate_regfile, a multi-port register file as placed + standard cells +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The net diff of The-OpenROAD-Project/OpenROAD#11669 at its head +691d8e15, all eight commits as one patch: generate_regfile in src/ram, +async_reset, SystemVerilog port headers and `unused `, +write_priority, Port fields named in the spec reader, and RAM-0052 for +what mode netlist leaves unplaced. + +generate_regfile builds a register file with many read and write ports +over few words as an array of placed standard cells, from a spec that +names the module, its words and bits, its ports by the RTL's own port +names, and the cells to use. It writes structural Verilog, DEF, an +abstract LEF and a timing model. ORFS's AUTO_MEMORIES_REGFILES (ORFS +patch 0089, The-OpenROAD-Project/OpenROAD-flow-scripts#4651) runs it +during canonicalization. + +Upstream as The-OpenROAD-Project/OpenROAD#11669; retires at a bump onto +an OpenROAD that has it. + +diff --git a/src/ram/BUILD b/src/ram/BUILD +index 2cb8a34b5e9..fa9f8c8ce2e 100644 +--- a/src/ram/BUILD ++++ b/src/ram/BUILD +@@ -18,6 +18,11 @@ cc_library( + "src/MakeRam.cpp", + "src/layout.cpp", + "src/ram.cpp", ++ "src/regfile.cpp", ++ "src/regfile.h", ++ "src/regfile_cmd.cpp", ++ "src/views.cpp", ++ "src/views.h", + ":swig", + ":tcl", + ], +@@ -25,6 +30,7 @@ cc_library( + "include/ram/MakeRam.h", + "include/ram/layout.h", + "include/ram/ram.h", ++ "include/ram/regfile.h", + ], + includes = [ + "include", +@@ -36,6 +42,8 @@ cc_library( + "//src/drt", + "//src/grt", + "//src/odb/src/db", ++ "//src/odb/src/defout", ++ "//src/odb/src/lefout", + "//src/pdn", + "//src/ppl", + "//src/sta:opensta_lib", +diff --git a/src/ram/CMakeLists.txt b/src/ram/CMakeLists.txt +index d0008e8664d..81aaa4ab9a4 100644 +--- a/src/ram/CMakeLists.txt ++++ b/src/ram/CMakeLists.txt +@@ -15,6 +15,9 @@ target_sources(ram + src/ram.cpp + src/layout.cpp + src/MakeRam.cpp ++ src/regfile.cpp ++ src/regfile_cmd.cpp ++ src/views.cpp + ) + + target_include_directories(ram +diff --git a/src/ram/README.md b/src/ram/README.md +index 94c5df30369..034eb02a14c 100644 +--- a/src/ram/README.md ++++ b/src/ram/README.md +@@ -80,9 +80,70 @@ generate_ram [-mask_size bits] + | `-use_latch` | If set to `1`, uses a two-phase latch-based design instead of flip-flops for the storage elements. Functionally equivalent to flip-flop design with smaller storage cell area. Default: `0`. | + | `-write_behavioral_verilog` | Write a behavioral Verilog model of the RAM array to the specified file. | + ++### Register File Generation ++ ++The `generate_regfile` command builds a register file, many read and ++write ports over few words, as an array of placed standard cells: word ++rows by bit columns, each tile a flop, its write select and an AND per ++read port, the read word lines decoded in a header column and the read ++bit lines as OR trees in a footer row. Every cell is placed by ++construction. The block is built into the current database, whose ++libraries must hold the cells the spec names. ++ ++```tcl ++generate_regfile -spec file ++ [-check_ports verilog_file] ++ [-verilog file] ++ [-def file] ++ [-lef file] ++ [-liberty file] ++``` ++ ++#### Options ++ ++| Switch Name | Description | ++| ---------------------- | -------------------------------------- | ++| `-spec` | The spec file, below. | ++| `-check_ports` | A Verilog file declaring the spec's module. The command stops if the module's ports are not the ports the spec names, at the widths the spec implies. It reads ANSI headers, a SystemVerilog `#(...)` parameter list and widths written in its parameters' defaults, packed dimensions multiplied; a port declaration it cannot read stops it too. | ++| `-verilog` | Write the cells as structural Verilog. | ++| `-def` | Write the placed block as DEF. | ++| `-lef` | Write an abstract LEF of the block. | ++| `-liberty` | Write a timing model; a `_pre_layout.lib` beside it has ideal clocks. | ++ ++#### Spec ++ ++One key per line, `#` comments: ++ ++| Key | Description | ++| ---------------------- | -------------------------------------- | ++| `module name` | The module and block name. | ++| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent; every cell is placed, the address inverters placed but movable so the macro's own repair can size them. `netlist`: the block is placed as a macro and then dissolves into its parent's cells: the core (storage flops and read bitlines) is fixed where it lies, and the periphery (address inverters, decode, hold) is left unplaced, RAM-0052 says how much, for the parent's placer and resizer, which alone know the drivers and loads outside the array; the abstract's pins sit where their connections land in the array. Which shape is better, and whether the periphery should be unplaced or placed but movable, is still being measured. | ++| `words n`, `bits n` | Depth and width. | ++| `clock port` | The clock port. | ++| `reset port` | A reset port the RTL has and the array ignores. | ++| `write_priority or\|last` | Two ports writing one word in one cycle: `or` (the default) ORs their data, for an RTL that never does it; `last`, the later write port in the spec wins, as an RTL's later assignment does. | ++| `unused port...` | One-bit inputs the RTL module has and the array ignores (ibex's `test_en_i`), so the block keeps the module's ports. | ++| `async_reset port low\|high` | The reset clears every stored word to zero asynchronously, active low or high; the storage flops are `cell flop_r` with their set tied off by `cell tie_hi`, one tie (and for an active-high reset one inverter) per word. Read-address registers are not reset. One of `reset` or `async_reset`. | ++| `read addr data` | A read port, by the module's port names. Repeat per port. | ++| `read_banked addr0 data0 addr1 data1 ...` | A read port with an address and a data port per bank. | ++| `write addr data [en]` | A write port. Repeat per port. | ++| `cell role master` | The cells: `flop`, `and2`, `or2`, `ao22`, `inv`, `tap`; with `async_reset` also `flop_r` (pins D, CLK, RESETN, SETN, QN by default) and `tie_hi` (pin H). | ++| `pins role pin...` | The pin names of a cell role (`flop`, `flop_r`, `tie_hi`, `and2`, `or2`, `ao22`, `inv`), for a library whose names differ from the defaults. | ++| `flop_output Q\|QN` | Whether the flop's output is inverted. | ++| `pin_layer layer`, `pin_layer_v layer` | The layers of the pins on the left and right edges, and on the top and bottom. | ++| `pin_track offset pitch` | The track grid the pins are centred on, in microns. | ++| `banks n`, `bank_columns n`, `bank_order contiguous\|interleaved` | Word columns side by side, how many of them stand in a row, and how words fall into them. | ++| `bit_folds n` | The word split into `n` bit bands stacked one above the other. | ++| `read_latency 0\|1` | `1` registers each read address on the clock first. | ++| `zero_word n` | A word with no storage that reads 0 and drops writes (RISC-V x0). | ++| `tap_columns n`, `service_sites n` | A tap column every `n` bit columns, and free sites beside each for the clock tree. | ++| `store_name pattern`, `read_reg_name pattern` | Instance names of the flops, from `{word}`, `{bit}`, `{port}` and `{bank}`. | ++| `lib knob value` | The timing model's parameters: `gate_delay_ps`, `wire_factor`, `input_load_ff`, `clock_load_ff`, `leakage_nw_per_cell`, `output_max_cap_ff`, `hold_ps`. | ++ + ## Example scripts + +-See [test/make_8x8_sky130.tcl](test/make_8x8_sky130.tcl). ++See [test/make_8x8_sky130.tcl](test/make_8x8_sky130.tcl) and ++[test/generate_regfile_asap7.tcl](test/generate_regfile_asap7.tcl). + + ## Regression tests + +diff --git a/src/ram/include/ram/regfile.h b/src/ram/include/ram/regfile.h +new file mode 100644 +index 00000000000..663887e0ec4 +--- /dev/null ++++ b/src/ram/include/ram/regfile.h +@@ -0,0 +1,33 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++#pragma once ++ ++#include ++ ++namespace odb { ++class dbDatabase; ++} ++ ++namespace utl { ++class Logger; ++} ++ ++namespace ram { ++ ++// A multi-port register file as placed standard cells: word rows by bit ++// columns, every cell of its core placed by construction (see ++// src/regfile.h). Reads the spec file, builds the block into `db`, whose ++// libraries must hold the spec's cells, and writes each view whose path ++// is not empty. `check_ports` names a Verilog file whose module the spec ++// must match port for port before anything is built. ++void generateRegfile(odb::dbDatabase* db, ++ utl::Logger* logger, ++ const std::string& spec_path, ++ const std::string& verilog_path, ++ const std::string& def_path, ++ const std::string& lef_path, ++ const std::string& liberty_path, ++ const std::string& check_ports); ++ ++} // namespace ram +diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i +index 8b225fcc890..c4584484fa3 100644 +--- a/src/ram/src/ram.i ++++ b/src/ram/src/ram.i +@@ -8,6 +8,7 @@ + + #include "ord/OpenRoad.hh" + #include "ram/ram.h" ++#include "ram/regfile.h" + #include "utl/Logger.h" + + namespace ord { +@@ -96,6 +97,17 @@ generate_ram_netlist_cmd(int mask_size, + tapcell, max_tap_dist); + } + ++void generate_regfile_cmd(const char* spec_path, ++ const char* verilog_path, ++ const char* def_path, ++ const char* lef_path, ++ const char* liberty_path, ++ const char* check_ports) ++{ ++ generateRegfile(ord::getDb(), ord::getLogger(), spec_path, verilog_path, ++ def_path, lef_path, liberty_path, check_ports); ++} ++ + void ram_pdngen(const char* power_net_name, const char* ground_net_name, + const char* route_name, int route_width, + const char* ver_name, int ver_width, int ver_pitch, +diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl +index 4e58c501636..505b551342a 100644 +--- a/src/ram/src/ram.tcl ++++ b/src/ram/src/ram.tcl +@@ -298,3 +298,28 @@ proc generate_ram { args } { + + ram::ram_routing + } ++ ++sta::define_cmd_args "generate_regfile" {-spec file ++ [-verilog file] ++ [-def file] ++ [-lef file] ++ [-liberty file] ++ [-check_ports verilog_file]} ++ ++# A multi-port register file as placed standard cells, from a spec file ++# (src/regfile.h): the block is built into the current database, whose ++# libraries must hold the spec's cells, and each view asked for is written. ++proc generate_regfile { args } { ++ sta::parse_key_args "generate_regfile" args \ ++ keys { -spec -verilog -def -lef -liberty -check_ports } flags {} ++ sta::check_argc_eq0 "generate_regfile" $args ++ if { ![info exists keys(-spec)] } { ++ utl::error RAM 51 "generate_regfile needs -spec." ++ } ++ set views {} ++ foreach k { -verilog -def -lef -liberty -check_ports } { ++ lappend views [expr { [info exists keys($k)] ? $keys($k) : "" }] ++ } ++ ram::generate_regfile_cmd $keys(-spec) {*}$views ++ ord::design_created ++} +diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp +new file mode 100644 +index 00000000000..fb4b113bda1 +--- /dev/null ++++ b/src/ram/src/regfile.cpp +@@ -0,0 +1,1642 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++// generate_regfile: a multi-port register file as a placed standard-cell ++// macro, built directly in odb. See regfile.h and README.md. ++#include "regfile.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "odb/db.h" ++#include "odb/dbTypes.h" ++#include "odb/geom.h" ++#include "utl/Logger.h" ++ ++namespace ram::regfile { ++ ++namespace { ++ ++using odb::dbBlock; ++using odb::dbBTerm; ++using odb::dbInst; ++using odb::dbMaster; ++using odb::dbNet; ++ ++std::string Bit(const std::string& bus, int i) ++{ ++ return bus + "[" + std::to_string(i) + "]"; ++} ++ ++int AddrBits(int words) ++{ ++ int a = 0; ++ while ((1 << a) < words) { ++ ++a; ++ } ++ return std::max(a, 1); ++} ++ ++[[noreturn]] void Refuse(const std::string& what) ++{ ++ throw std::runtime_error(what); ++} ++ ++// `pattern` with each `{key}` replaced by its value. ++std::string Fmt(std::string pattern, ++ const std::vector>& values) ++{ ++ for (const auto& [key, value] : values) { ++ const std::string k = "{" + key + "}"; ++ for (size_t at = pattern.find(k); at != std::string::npos; ++ at = pattern.find(k, at)) { ++ pattern.replace(at, k.size(), std::to_string(value)); ++ } ++ } ++ return pattern; ++} ++ ++// The pins of each cell kind, asap7's names by default; a spec may ++// override them with `pins ...` in the same order. ++struct Pins ++{ ++ std::vector flop = {"D", "CLK", "QN"}; // data, clock, out ++ // data, clock, reset, set (both active low), out ++ std::vector flop_r = {"D", "CLK", "RESETN", "SETN", "QN"}; ++ std::vector tie_hi = {"H"}; ++ std::vector and2 = {"A", "B", "Y"}; ++ std::vector or2 = {"A", "B", "Y"}; ++ std::vector ao22 = {"A1", "A2", "B1", "B2", "Y"}; ++ std::vector aoi22 = {"A1", "A2", "B1", "B2", "Y"}; ++ std::vector nand2 = {"A", "B", "Y"}; ++ std::vector nor2 = {"A", "B", "Y"}; ++ std::vector inv = {"A", "Y"}; ++ std::vector tie_lo = {"L"}; ++ std::vector icg = {"ENA", "SE", "CLK", "GCLK"}; ++ bool flop_inverted = true; // the flop's output is QN ++}; ++ ++Pins g_pins; // set by ReadSpec, read by Generate ++ ++// Everything the builder needs while it lays cells into rows. ++class Builder ++{ ++ public: ++ Builder(odb::dbDatabase* db, utl::Logger* logger, const Spec& spec) ++ : db_(db), logger_(logger), spec_(spec) ++ { ++ } ++ ++ dbBlock* Run(); ++ ++ private: ++ dbMaster* Master(const std::string& name, const char* role) ++ { ++ dbMaster* m = db_->findMaster(name.c_str()); ++ if (m == nullptr) { ++ Refuse(std::string("cell for ") + role ++ + " not in the loaded LEF: " + name); ++ } ++ return m; ++ } ++ void CheckPins(dbMaster* m, const std::vector& pins) ++ { ++ for (const auto& p : pins) { ++ if (m->findMTerm(p.c_str()) == nullptr) { ++ Refuse("cell " + m->getName() + " has no pin " + p ++ + "; give its pins with `pins ...` in the spec"); ++ } ++ } ++ } ++ dbNet* Net(const std::string& name) ++ { ++ dbNet* n = block_->findNet(name.c_str()); ++ if (n == nullptr) { ++ n = dbNet::create(block_, name.c_str()); ++ } ++ return n; ++ } ++ dbBTerm* Input(const std::string& name) ++ { ++ dbNet* n = Net(name); ++ dbBTerm* t = dbBTerm::create(n, name.c_str()); ++ t->setIoType(odb::dbIoType::INPUT); ++ t->setSigType(odb::dbSigType::SIGNAL); ++ return t; ++ } ++ dbBTerm* Output(const std::string& name, dbNet* net) ++ { ++ dbBTerm* t = dbBTerm::create(net, name.c_str()); ++ t->setIoType(odb::dbIoType::OUTPUT); ++ t->setSigType(odb::dbSigType::SIGNAL); ++ return t; ++ } ++ // A cell dropped into a row cursor: placed left to right, FIRM. ++ struct Cursor ++ { ++ int row = 0; ++ int x = 0; ++ }; ++ dbInst* Place(Cursor& c, ++ dbMaster* m, ++ const std::string& name, ++ const std::vector>& conns) ++ { ++ // Cells are named after the net they drive, with a suffix: Verilog ++ // has one namespace for wires and instances, and yosys refuses a ++ // netlist where a cell and a wire share a name. ++ std::string inst_name = name; ++ if (block_->findNet(name.c_str()) != nullptr) { ++ inst_name += "_g"; ++ } ++ dbInst* inst = dbInst::create(block_, m, inst_name.c_str()); ++ for (const auto& [pin, net] : conns) { ++ odb::dbITerm* it = inst->findITerm(pin.c_str()); ++ if (it == nullptr) { ++ Refuse("cell " + m->getName() + " has no pin " + pin); ++ } ++ it->connect(net); ++ } ++ inst->setOrient(c.row % 2 == 0 ? odb::dbOrientType::R0 ++ : odb::dbOrientType::MX); ++ inst->setLocation(core_x0_ + c.x, core_y0_ + c.row * row_h_); ++ inst->setPlacementStatus(odb::dbPlacementStatus::FIRM); ++ if (periphery_) { ++ periphery_insts_.push_back(inst); ++ } ++ c.x += static_cast(m->getWidth()); ++ max_x_ = std::max(max_x_, c.x); ++ return inst; ++ } ++ // A one-hot select for `bits` address literals and an enable: an AND2 ++ // tree over the literals, placed at the cursor. ++ dbNet* Decode(Cursor& c, ++ const std::string& prefix, ++ const std::vector& literals) ++ { ++ std::vector level = literals; ++ int k = 0; ++ while (level.size() > 1) { ++ std::vector next; ++ for (size_t i = 0; i + 1 < level.size(); i += 2) { ++ dbNet* y = Net(prefix + "_a" + std::to_string(k++)); ++ Place(c, ++ and2_, ++ y->getName(), ++ {{g_pins.and2[0], level[i]}, ++ {g_pins.and2[1], level[i + 1]}, ++ {g_pins.and2[2], y}}); ++ next.push_back(y); ++ } ++ if (level.size() % 2 == 1) { ++ next.push_back(level.back()); ++ } ++ level = next; ++ } ++ return level.front(); ++ } ++ // OR tree over `leaves`, each node placed by a caller-supplied hook so ++ // the tree can run down a column rather than sit in a footer. ++ dbNet* OrTree(const std::string& prefix, ++ const std::vector& leaves, ++ const std::function& where) ++ { ++ std::function rec ++ = [&](int lo, int hi, int& k) -> dbNet* { ++ if (hi - lo == 1) { ++ return leaves[lo]; ++ } ++ int mid = (lo + hi) / 2; ++ dbNet* a = rec(lo, mid, k); ++ dbNet* b = rec(mid, hi, k); ++ dbNet* y = Net(prefix + "_o" + std::to_string(k++)); ++ Place(where(lo, hi), ++ or2_, ++ y->getName(), ++ {{g_pins.or2[0], a}, {g_pins.or2[1], b}, {g_pins.or2[2], y}}); ++ return y; ++ }; ++ int k = 0; ++ return rec(0, static_cast(leaves.size()), k); ++ } ++ // A read bitline: OR over words of (sel & q), in inverting gates. A ++ // word pair is one AOI22, ~(sel_a & q_a | sel_b & q_b), in the pair's ++ // first tile; an odd word out is a NAND2. Over those active-low leaves ++ // a balanced tree alternates NAND2 (low in, high out) and NOR2 (high ++ // in, low out), each node in the second tile of its middle pair, and ++ // the root takes two high inputs with an OR2, so the bitline leaves on ++ // the library's OR2 drive. Where a split leaves the two halves at ++ // different polarities an inverter evens them. One gate level per ++ // pair of words instead of an AND2 and an OR2 per level. ++ struct Polar ++ { ++ dbNet* net; ++ bool low; ++ }; ++ dbNet* Bitline(const std::string& prefix, ++ const std::vector& sel, ++ const std::vector& q, ++ const std::function& tile, ++ const std::vector& words) ++ { ++ const int n = static_cast(q.size()); ++ const int L = (n + 1) / 2; ++ int k = 0; ++ auto first = [&](int l) { return words[2 * l]; }; ++ auto second = [&](int l) { ++ return 2 * l + 1 < n ? words[2 * l + 1] : words[2 * l]; ++ }; ++ std::vector leaf(L); ++ for (int l = 0; l < L; ++l) { ++ dbNet* y = Net(prefix + "_l" + std::to_string(l)); ++ if (2 * l + 1 < n) { ++ Place(tile(first(l)), ++ aoi22_, ++ y->getName(), ++ {{g_pins.aoi22[0], sel[2 * l]}, ++ {g_pins.aoi22[1], q[2 * l]}, ++ {g_pins.aoi22[2], sel[2 * l + 1]}, ++ {g_pins.aoi22[3], q[2 * l + 1]}, ++ {g_pins.aoi22[4], y}}); ++ } else { ++ Place(tile(first(l)), ++ nand2_, ++ y->getName(), ++ {{g_pins.nand2[0], sel[2 * l]}, ++ {g_pins.nand2[1], q[2 * l]}, ++ {g_pins.nand2[2], y}}); ++ } ++ leaf[l] = Polar{y, true}; ++ } ++ auto invert = [&](Polar p, Cursor& c) { ++ dbNet* y = Net(prefix + "_i" + std::to_string(k++)); ++ Place( ++ c, inv_, y->getName(), {{g_pins.inv[0], p.net}, {g_pins.inv[1], y}}); ++ return Polar{y, !p.low}; ++ }; ++ std::function rec ++ = [&](int lo, int hi, bool root) -> Polar { ++ if (hi - lo == 1) { ++ return leaf[lo]; ++ } ++ const int mid = (lo + hi) / 2; ++ Polar a = rec(lo, mid, false); ++ Polar b = rec(mid, hi, false); ++ Cursor& c = tile(second(mid)); ++ if (a.low != b.low) { ++ // the shallower half is the one a level behind ++ if (mid - lo < hi - mid) { ++ a = invert(a, c); ++ } else { ++ b = invert(b, c); ++ } ++ } ++ dbNet* y = Net(prefix + "_o" + std::to_string(k++)); ++ dbMaster* m = a.low ? nand2_ : (root ? or2_ : nor2_); ++ const std::vector& p ++ = a.low ? g_pins.nand2 : (root ? g_pins.or2 : g_pins.nor2); ++ Place(c, m, y->getName(), {{p[0], a.net}, {p[1], b.net}, {p[2], y}}); ++ return Polar{y, !a.low && !root}; ++ }; ++ Polar out = rec(0, L, true); ++ if (out.low) { ++ out = invert(out, tile(second(0))); ++ } ++ return out.net; ++ } ++ // The row of a tile or header with the least in it so far: cells of ++ // different widths pack evenly, which a round robin does not do. ++ static Cursor& Least(std::vector& rows) ++ { ++ return *std::min_element( ++ rows.begin(), rows.end(), [](const Cursor& a, const Cursor& b) { ++ return a.x < b.x; ++ }); ++ } ++ void Pin(dbBTerm* t, odb::dbTechLayer* layer, int x, int y) ++ { ++ odb::dbBPin* bp = odb::dbBPin::create(t); ++ int w = std::max(static_cast(layer->getWidth()), 1); ++ odb::dbBox::create(bp, layer, x - w / 2, y - w / 2, x + w / 2, y + w / 2); ++ bp->setPlacementStatus(odb::dbPlacementStatus::FIRM); ++ } ++ ++ odb::dbDatabase* db_; ++ utl::Logger* logger_; ++ const Spec& spec_; ++ dbBlock* block_ = nullptr; ++ odb::dbSite* site_ = nullptr; ++ odb::dbTechLayer* layer_h_ = nullptr; // left/right edge pins ++ odb::dbTechLayer* layer_v_ = nullptr; // top/bottom edge pins ++ dbMaster* flop_ = nullptr; ++ dbMaster* store_ = nullptr; // the storage flop: flop_, or flop_r ++ dbMaster* tie_hi_ = nullptr; ++ dbMaster* and2_ = nullptr; ++ dbMaster* or2_ = nullptr; ++ dbMaster* ao22_ = nullptr; ++ dbMaster* aoi22_ = nullptr; ++ dbMaster* nand2_ = nullptr; ++ dbMaster* nor2_ = nullptr; ++ dbMaster* inv_ = nullptr; ++ dbMaster* tap_ = nullptr; ++ dbMaster* tie_lo_ = nullptr; ++ dbMaster* icg_ = nullptr; ++ int dbu_ = 1000; ++ int site_w_ = 0; ++ int row_h_ = 0; ++ int core_x0_ = 0; ++ int core_y0_ = 0; ++ int pin_w_ = 0; ++ int max_x_ = 0; ++ // One entry per (word, bit) tile in row-major order: its row cursors, ++ // which the read OR trees keep filling after the tile's own cells, and ++ // the x where the tile ends, checked once the trees are in. ++ struct Tile ++ { ++ std::vector rows; ++ int x_end = 0; ++ }; ++ std::vector tiles_; ++ // The periphery: the address band and each word's decode. In netlist ++ // mode these are left unplaced (see Run). ++ bool periphery_ = false; ++ std::vector periphery_insts_; ++}; ++ ++dbBlock* Builder::Run() ++{ ++ const Spec& s = spec_; ++ if (s.words < 2 || s.bits < 1 || s.read.empty() || s.write.empty()) { ++ Refuse( ++ "a register file needs at least 2 words, 1 bit, 1 read and 1 " ++ "write port"); ++ } ++ flop_ = Master(s.cells.flop, "flop"); ++ and2_ = Master(s.cells.and2, "and2"); ++ or2_ = Master(s.cells.or2, "or2"); ++ ao22_ = Master(s.cells.ao22, "ao22"); ++ aoi22_ = Master(s.cells.aoi22, "aoi22"); ++ nand2_ = Master(s.cells.nand2, "nand2"); ++ nor2_ = Master(s.cells.nor2, "nor2"); ++ inv_ = Master(s.cells.inv, "inv"); ++ tap_ = s.cells.tap.empty() ? nullptr : Master(s.cells.tap, "tap"); ++ tie_lo_ = s.cells.tie_lo.empty() ? nullptr : Master(s.cells.tie_lo, "tie_lo"); ++ CheckPins(flop_, g_pins.flop); ++ CheckPins(and2_, g_pins.and2); ++ CheckPins(or2_, g_pins.or2); ++ CheckPins(ao22_, g_pins.ao22); ++ CheckPins(aoi22_, g_pins.aoi22); ++ CheckPins(nand2_, g_pins.nand2); ++ CheckPins(nor2_, g_pins.nor2); ++ CheckPins(inv_, g_pins.inv); ++ const bool gated = s.write_style == "clock_gate"; ++ if (gated) { ++ icg_ = Master(s.cells.icg, "icg"); ++ CheckPins(icg_, g_pins.icg); ++ } ++ store_ = flop_; ++ if (s.async_reset) { ++ store_ = Master(s.cells.flop_r, "flop_r"); ++ CheckPins(store_, g_pins.flop_r); ++ tie_hi_ = Master(s.cells.tie_hi, "tie_hi"); ++ CheckPins(tie_hi_, g_pins.tie_hi); ++ } ++ ++ odb::dbTech* tech = db_->getTech(); ++ site_ = flop_->getSite(); ++ if (site_ == nullptr) { ++ Refuse("flop " + flop_->getName() + " has no site"); ++ } ++ site_w_ = site_->getWidth(); ++ row_h_ = site_->getHeight(); ++ layer_h_ = tech->findLayer(s.pin_layer_h.c_str()); ++ layer_v_ = tech->findLayer(s.pin_layer_v.c_str()); ++ if (layer_h_ == nullptr || layer_v_ == nullptr) { ++ Refuse("pin layers not in the tech LEF: " + s.pin_layer_h + " " ++ + s.pin_layer_v); ++ } ++ pin_w_ = std::max( ++ static_cast(std::max(layer_h_->getWidth(), layer_v_->getWidth())), ++ 1); ++ ++ odb::dbChip* chip = db_->getChip(); ++ if (chip == nullptr) { ++ chip = odb::dbChip::create(db_, tech, s.module.c_str()); ++ } ++ if (chip->getBlock() != nullptr) { ++ Refuse("the database already has a top block (" ++ + chip->getBlock()->getName() + "); one generation per database"); ++ } ++ block_ = dbBlock::create(chip, s.module.c_str()); ++ if (block_ == nullptr) { ++ Refuse("could not create block " + s.module); ++ } ++ block_->setDefUnits(tech->getLefUnits()); ++ dbu_ = tech->getLefUnits(); ++ ++ const int W = static_cast(s.write.size()); ++ const int R = static_cast(s.read.size()); ++ const int A = AddrBits(s.words); ++ ++ // ---- ports and their nets ------------------------------------------ ++ dbNet* clock = Net(s.clock); ++ Input(s.clock); ++ if (!s.reset.empty()) { ++ // Chisel gives every module a reset; a register file without one ++ // still has the port. It exists on the macro, connected to nothing, ++ // unless `async_reset` makes it the storage flops' reset. ++ Input(s.reset); ++ } ++ for (const auto& u : s.unused) { ++ Input(u); ++ } ++ // Banks first: a banked read port has one address per bank, of the ++ // bank-local width. ++ const int banks = std::max(s.banks, 1); ++ if (s.words % banks != 0) { ++ Refuse("words (" + std::to_string(s.words) ++ + ") is not a multiple of banks (" + std::to_string(banks) + ")"); ++ } ++ const int words_per_bank = s.words / banks; ++ const int A_bank = AddrBits(words_per_bank); ++ const bool interleaved = s.bank_order == "interleaved"; ++ auto bank_of ++ = [&](int n) { return interleaved ? n % banks : n / words_per_bank; }; ++ auto local_of ++ = [&](int n) { return interleaved ? n / banks : n % words_per_bank; }; ++ if (s.zero_word >= s.words || (s.zero_word >= 0 && words_per_bank < 2)) { ++ Refuse("zero_word " + std::to_string(s.zero_word) ++ + " is not a word of a bank that keeps another"); ++ } ++ // Banks in a grid: bank k stands in column k % bank_cols, bank row ++ // k / bank_cols, bank rows stacked upward from the footer. ++ const int bank_cols = s.bank_columns > 0 ? s.bank_columns : banks; ++ if (bank_cols > banks || banks % bank_cols != 0) { ++ Refuse("bank_columns (" + std::to_string(bank_cols) ++ + ") does not divide banks (" + std::to_string(banks) + ")"); ++ } ++ const int bank_rows = banks / bank_cols; ++ // Bit folds: band f holds bits [f * bits_per_fold, (f + 1) * bits_per_fold) ++ // of every word, stacked above band f - 1 inside the bank; the last ++ // band is short when the folds do not divide the bits. ++ const int folds = std::max(s.bit_folds, 1); ++ if (folds > s.bits) { ++ Refuse("bit_folds (" + std::to_string(folds) + ") exceeds bits (" ++ + std::to_string(s.bits) + ")"); ++ } ++ const int bits_per_fold = (s.bits + folds - 1) / folds; ++ for (int r = 0; r < R; ++r) { ++ if (s.read[r].banked() ++ && static_cast(s.read[r].bank_addr.size()) != banks) { ++ Refuse("read port " + std::to_string(r) + " names " ++ + std::to_string(s.read[r].bank_addr.size()) ++ + " banks, the array has " + std::to_string(banks)); ++ } ++ } ++ // raddr[r][k] is bank k's literals for a banked port; raddr[r][0] the ++ // one address of a plain port. ++ std::vector>> raddr(R); ++ std::vector> waddr(W), wdata(W); ++ std::vector wen(W, nullptr); ++ for (int r = 0; r < R; ++r) { ++ if (s.read[r].banked()) { ++ raddr[r].resize(banks); ++ for (int k = 0; k < banks; ++k) { ++ for (int i = 0; i < A_bank; ++i) { ++ raddr[r][k].push_back(Net(Bit(s.read[r].bank_addr[k], i))); ++ Input(Bit(s.read[r].bank_addr[k], i)); ++ } ++ } ++ } else { ++ raddr[r].resize(1); ++ for (int i = 0; i < A; ++i) { ++ raddr[r][0].push_back(Net(Bit(s.read[r].addr, i))); ++ Input(Bit(s.read[r].addr, i)); ++ } ++ } ++ } ++ for (int w = 0; w < W; ++w) { ++ for (int i = 0; i < A; ++i) { ++ waddr[w].push_back(Net(Bit(s.write[w].addr, i))); ++ Input(Bit(s.write[w].addr, i)); ++ } ++ for (int b = 0; b < s.bits; ++b) { ++ wdata[w].push_back(Net(Bit(s.write[w].data, b))); ++ Input(Bit(s.write[w].data, b)); ++ } ++ if (!s.write[w].en.empty()) { ++ wen[w] = Net(s.write[w].en); ++ Input(s.write[w].en); ++ } ++ } ++ dbNet* vdd = Net("VDD"); ++ vdd->setSigType(odb::dbSigType::POWER); ++ vdd->setSpecial(); ++ dbNet* vss = Net("VSS"); ++ vss->setSigType(odb::dbSigType::GROUND); ++ vss->setSpecial(); ++ ++ // ---- geometry -------------------------------------------------------- ++ // A word is `rows_per_word` standard rows tall; a bit column holds one ++ // tile per word. The header column to the left holds the decode for ++ // that word: R read selects, W write selects, the hold term. ++ // ++ // Cells per tile: flop, inverter (Q from QN), W-way write mux as AO22 ++ // pairs and an OR2 tree, and per read port one cell of that port's ++ // bitline: a word pair's AOI22 leaf in the pair's first tile, a node ++ // of the NAND2/NOR2 tree over the leaves in its second (see the read ++ // bitlines below). A fix-up inverter rides with a node. ++ // W + 1 terms (the writes and the hold) in pairs: AO22s, an odd last ++ // term an AND2 ++ // Clock-gated, the hold term is gone; one write port leaves no mux. ++ const int write_terms = W + (gated ? 0 : 1); ++ const int write_pairs = write_terms > 1 ? (write_terms + 1) / 2 : 0; ++ const int read_node_w = static_cast( ++ std::max({nand2_->getWidth(), nor2_->getWidth(), or2_->getWidth()}) ++ + inv_->getWidth()); ++ // A tile holds a leaf or a node per port; the odd word out of a bank ++ // with an odd count holds its NAND2 leaf and a node. ++ const int read_cell_w ++ = std::max({static_cast(aoi22_->getWidth()), ++ read_node_w, ++ static_cast(nand2_->getWidth() ++ + std::max({nand2_->getWidth(), ++ nor2_->getWidth(), ++ or2_->getWidth()}))}); ++ const int tile_cells_w = static_cast( ++ store_->getWidth() + inv_->getWidth() + write_pairs * ao22_->getWidth() ++ + std::max(write_pairs - 1, 0) * or2_->getWidth() + R * read_cell_w); ++ int rows_per_word = 1; ++ // Keep a tile near square-ish: at most ~40 sites wide. ++ while (tile_cells_w / rows_per_word > 40 * site_w_ && rows_per_word < 8) { ++ rows_per_word *= 2; ++ } ++ // Rows are filled least-first, so a row is at most one widest cell over ++ // the average; the header carries that slack. ++ const int widest = static_cast(std::max({store_->getWidth(), ++ ao22_->getWidth(), ++ aoi22_->getWidth(), ++ or2_->getWidth(), ++ and2_->getWidth()})); ++ // A tile is as wide as its fullest row once its cells are in, in the ++ // order the builder drops them: the flop, Q's inverter, the write mux, ++ // then a bitline cell per port. The flop alone is most of a row, so ++ // an average-plus-widest-cell estimate leaves a third of the array empty. ++ int tile_w = 0; ++ { ++ std::vector fill(rows_per_word, 0); ++ auto drop ++ = [&](int w) { *std::min_element(fill.begin(), fill.end()) += w; }; ++ drop(static_cast(store_->getWidth())); ++ if (g_pins.flop_inverted) { ++ drop(static_cast(inv_->getWidth())); ++ } ++ for (int i = 0; i < write_pairs; ++i) { ++ drop(static_cast(ao22_->getWidth())); ++ } ++ for (int i = 0; i + 1 < write_pairs; ++i) { ++ drop(static_cast(or2_->getWidth())); ++ } ++ for (int r = 0; r < R; ++r) { ++ drop(read_cell_w); ++ } ++ tile_w = (*std::max_element(fill.begin(), fill.end()) + site_w_ - 1) ++ / site_w_ * site_w_; ++ } ++ // Header: per word, (R+W) decodes of (A+1) literals as AND2 trees, the ++ // address inverters live above the array. hold_n = ~(OR of write ++ // selects), or the word's clock gate enabled by that OR. ++ const int header_cells_w = static_cast( ++ (R + W) * A * and2_->getWidth() + (W - 1) * or2_->getWidth() ++ + (gated ? icg_->getWidth() : inv_->getWidth()) ++ + (s.write_priority == "last" ++ ? (W - 1) ++ * (inv_->getWidth() + and2_->getWidth() + or2_->getWidth()) ++ : 0) ++ + (s.async_reset ++ ? tie_hi_->getWidth() + (s.reset_active_low ? 0 : inv_->getWidth()) ++ : 0)); ++ const int header_w = ((header_cells_w + rows_per_word - 1) / rows_per_word ++ + widest + site_w_ - 1) ++ / site_w_ * site_w_; ++ // A service column every `tap_columns` bit columns: the tap cell and ++ // then `service_sites` empty sites on every row. The placement is ++ // legal without them; what needs the room is what comes after -- the ++ // clock tree's buffers and any repair -- and the slack inside a tile ++ // is a few sites at a time, which a BUFx24 cannot use. Contiguous free ++ // sites next to the flops are where a clock buffer wants to be. ++ const int tap_w = (tap_ ? static_cast(tap_->getWidth()) : 0) ++ + std::max(s.service_sites, 0) * site_w_; ++ const int tap_every = std::max(s.tap_columns, 1); ++ const int tap_cols = (tap_ || s.service_sites > 0) ++ ? (bits_per_fold + tap_every - 1) / tap_every ++ : 0; ++ // Banks: the word column folded into `banks` columns, `bank_cols` of ++ // them side by side and the rest stacked, each with its own header and ++ // service columns, so a 256-word file is not ten times taller than it ++ // is wide and a 128-bit one not ten times wider than tall. A plain read ++ // port's bitline is one OR tree per bank and a final OR across banks in ++ // a footer band; a banked port's bank trees are its outputs and need no ++ // footer. ++ const int bank_w = header_w + bits_per_fold * tile_w + tap_cols * tap_w; ++ // A bank is `folds` bands tall, each band every word of the bank. ++ const int band_h_rows = words_per_bank * rows_per_word; ++ const int bank_h_rows = folds * band_h_rows; ++ int plain_reads = 0; ++ for (int r = 0; r < R; ++r) { ++ plain_reads += s.read[r].banked() ? 0 : 1; ++ } ++ // Footer: (banks-1) OR2 per read port per bit, as many rows as it takes. ++ const int core_w = bank_cols * bank_w; ++ const int footer_cells_w = banks > 1 ++ ? (banks - 1) * plain_reads * s.bits ++ * static_cast(or2_->getWidth()) ++ : 0; ++ const int footer_rows ++ = footer_cells_w > 0 ? (footer_cells_w + core_w - 1) / core_w + 1 : 0; ++ // The address band above the array: an inverter per address literal, ++ // and with a read latency a flop ahead of each read one, each cell with ++ // a spare site after it; as many rows as that takes. ++ int read_addr_bits = 0; ++ for (int r = 0; r < R; ++r) { ++ read_addr_bits += s.read[r].banked() ? banks * A_bank : A; ++ } ++ const int inv_cell_w = static_cast(inv_->getWidth()) + site_w_; ++ const int reg_cell_w ++ = s.read_latency > 0 ? static_cast(flop_->getWidth()) + site_w_ : 0; ++ const int band_w ++ = read_addr_bits * (inv_cell_w + reg_cell_w) + W * A * inv_cell_w; ++ const int inv_rows = std::max(1, (band_w + core_w - 1) / core_w); ++ const int total_rows = footer_rows + bank_rows * bank_h_rows + inv_rows; ++ ++ // Core at a site multiple in from the die so a parent's ring fits. ++ core_x0_ = 10 * site_w_; ++ core_y0_ = 2 * row_h_; ++ const int die_w = core_w + 20 * site_w_; ++ const int die_h = total_rows * row_h_ + 4 * row_h_; ++ block_->setDieArea(odb::Rect(0, 0, die_w, die_h)); ++ for (int r = 0; r < total_rows; ++r) { ++ std::string rn = "ROW_" + std::to_string(r); ++ odb::dbRow::create( ++ block_, ++ rn.c_str(), ++ site_, ++ core_x0_, ++ core_y0_ + r * row_h_, ++ r % 2 == 0 ? odb::dbOrientType::R0 : odb::dbOrientType::MX, ++ odb::dbRowDir::HORIZONTAL, ++ core_w / site_w_, ++ site_w_); ++ } ++ ++ // ---- address inverters, top band ------------------------------------ ++ // Periphery: their inputs are the address ports, their outputs the ++ // widest nets of the block. In netlist mode they are left unplaced for ++ // the parent (see the end of Run). In a macro, the macro's own repair ++ // may upsize one, and a cell that may be resized is one that may move: ++ // PLACED, not FIRM, with a spare site after each for INVx1 to INVx2. ++ std::vector>> raddr_n(R); ++ std::vector> waddr_n(W); ++ periphery_ = true; ++ { ++ Cursor c{total_rows - 1, 0}; ++ // A cell and its spare site, on the next band row down if this one ++ // is full. ++ auto band = [&](dbMaster* m, ++ const std::string& name, ++ const std::vector>& conns) { ++ if (c.x + static_cast(m->getWidth()) > core_w) { ++ c = Cursor{c.row - 1, 0}; ++ } ++ Place(c, m, name, conns) ++ ->setPlacementStatus(odb::dbPlacementStatus::PLACED); ++ c.x += site_w_; ++ }; ++ for (int r = 0; r < R; ++r) { ++ raddr_n[r].resize(raddr[r].size()); ++ for (size_t k = 0; k < raddr[r].size(); ++k) { ++ for (size_t i = 0; i < raddr[r][k].size(); ++i) { ++ const std::string base ++ = "rd" + std::to_string(r) + "_k" + std::to_string(k); ++ if (s.read_latency > 0) { ++ // The registered address: the flop's QN is the inverted ++ // literal, one inverter gives the true one (the other way ++ // round for a Q flop). The decode reads only these. ++ const std::string reg = s.read_reg_name.empty() ++ ? base + "_areg" + std::to_string(i) ++ : Fmt(s.read_reg_name, ++ {{"port", r}, ++ {"bank", static_cast(k)}, ++ {"bit", static_cast(i)}}); ++ dbNet* out = Net(base + "_aq" + std::to_string(i)); ++ band(flop_, ++ reg, ++ {{g_pins.flop[0], raddr[r][k][i]}, ++ {g_pins.flop[1], clock}, ++ {g_pins.flop[2], out}}); ++ dbNet* other = Net(base + "_ai" + std::to_string(i)); ++ band(inv_, ++ other->getName(), ++ {{g_pins.inv[0], out}, {g_pins.inv[1], other}}); ++ raddr[r][k][i] = g_pins.flop_inverted ? other : out; ++ raddr_n[r][k].push_back(g_pins.flop_inverted ? out : other); ++ continue; ++ } ++ dbNet* y = Net(base + "_na" + std::to_string(i)); ++ band(inv_, ++ y->getName(), ++ {{g_pins.inv[0], raddr[r][k][i]}, {g_pins.inv[1], y}}); ++ raddr_n[r][k].push_back(y); ++ } ++ } ++ } ++ for (int w = 0; w < W; ++w) { ++ for (int i = 0; i < A; ++i) { ++ dbNet* y = Net("wr" + std::to_string(w) + "_na" + std::to_string(i)); ++ band(inv_, ++ y->getName(), ++ {{g_pins.inv[0], waddr[w][i]}, {g_pins.inv[1], y}}); ++ waddr_n[w].push_back(y); ++ } ++ } ++ if (c.row < total_rows - inv_rows) { ++ Refuse("address band overflow: widen the band estimate"); ++ } ++ } ++ periphery_ = false; ++ ++ // ---- per band and word: header decode, then tiles across the bits ---- ++ // The decode is per (band, word): a band is its own rows, so the word ++ // selects it needs are made again beside it. Cheap next to the tiles. ++ std::vector> rsel(R, std::vector(s.words)); ++ std::vector> wsel(W, std::vector(s.words)); ++ std::vector hold(s.words); ++ std::vector gclk(s.words, nullptr); ++ // Read bitline inputs per (r, b), down the column: each word's select ++ // and stored value, paired into AOI22 leaves once all tiles are in. ++ std::vector>> leaves( ++ R, std::vector>(s.bits)); ++ std::vector>> leaf_sel( ++ R, std::vector>(s.bits)); ++ // The word each leaf reads, for its bank and the tile its OR node sits in. ++ std::vector>> leaf_word( ++ R, std::vector>(s.bits)); ++ tiles_.assign(static_cast(s.words) * s.bits, Tile{}); ++ ++ auto literals = ++ [&](const std::vector& a, const std::vector& an, int n) { ++ std::vector l; ++ for (size_t i = 0; i < a.size(); ++i) { ++ l.push_back(((n >> i) & 1) ? a[i] : an[i]); ++ } ++ return l; ++ }; ++ ++ for (int f = 0; f < folds; ++f) { ++ for (int n = 0; n < s.words; ++n) { ++ if (n == s.zero_word) { ++ continue; // no storage, no selects: it reads 0, a write is dropped ++ } ++ const int bank = bank_of(n); ++ const int row0 = footer_rows + (bank / bank_cols) * bank_h_rows ++ + f * band_h_rows + local_of(n) * rows_per_word; ++ const int bank_x0 = (bank % bank_cols) * bank_w; ++ // Header column, spread over this word's rows. ++ std::vector hc; ++ for (int k = 0; k < rows_per_word; ++k) { ++ hc.push_back(Cursor{row0 + k, bank_x0}); ++ } ++ auto hcur = [&]() -> Cursor& { return Least(hc); }; ++ periphery_ = true; ++ std::string wn = "w" + std::to_string(n) ++ + (folds > 1 ? "_f" + std::to_string(f) : ""); ++ for (int r = 0; r < R; ++r) { ++ const bool banked = s.read[r].banked(); ++ rsel[r][n] = Decode(hcur(), ++ wn + "_rsel" + std::to_string(r), ++ literals(raddr[r][banked ? bank : 0], ++ raddr_n[r][banked ? bank : 0], ++ banked ? local_of(n) : n)); ++ } ++ std::vector wsels; ++ for (int w = 0; w < W; ++w) { ++ std::vector l = literals(waddr[w], waddr_n[w], n); ++ if (wen[w] != nullptr) { ++ l.push_back(wen[w]); ++ } ++ wsel[w][n] = Decode(hcur(), wn + "_wsel" + std::to_string(w), l); ++ wsels.push_back(wsel[w][n]); ++ } ++ if (s.write_priority == "last" && W > 1) { ++ // A later port wins: each write select masked by the later ones'. ++ // The clock gate and hold still see every write (wsels). ++ dbNet* later = wsel[W - 1][n]; ++ for (int w = W - 2; w >= 0; --w) { ++ const std::string k = wn + "_wp" + std::to_string(w); ++ dbNet* later_n = Net(k + "_ln"); ++ Place(hcur(), ++ inv_, ++ later_n->getName(), ++ {{g_pins.inv[0], later}, {g_pins.inv[1], later_n}}); ++ dbNet* masked = Net(k + "_sel"); ++ Place(hcur(), ++ and2_, ++ masked->getName(), ++ {{g_pins.and2[0], wsel[w][n]}, ++ {g_pins.and2[1], later_n}, ++ {g_pins.and2[2], masked}}); ++ if (w > 0) { ++ dbNet* any_later = Net(k + "_l"); ++ Place(hcur(), ++ or2_, ++ any_later->getName(), ++ {{g_pins.or2[0], later}, ++ {g_pins.or2[1], wsel[w][n]}, ++ {g_pins.or2[2], any_later}}); ++ later = any_later; ++ } ++ wsel[w][n] = masked; ++ } ++ } ++ // hold = ~(wsel0 | wsel1 | ...): the word keeps its value. ++ dbNet* any_write ++ = wsels.size() == 1 ++ ? wsels[0] ++ : OrTree(wn + "_anyw", wsels, [&](int, int) -> Cursor& { ++ return hcur(); ++ }); ++ if (gated) { ++ // The word's clock runs only on a write. SE is the latch's other ++ // enable (ENA | SE); tied to the same select it changes nothing. ++ gclk[n] = Net(wn + "_gclk"); ++ Place(hcur(), ++ icg_, ++ wn + "_icg", ++ {{g_pins.icg[0], any_write}, ++ {g_pins.icg[1], any_write}, ++ {g_pins.icg[2], clock}, ++ {g_pins.icg[3], gclk[n]}}); ++ } else { ++ hold[n] = Net(wn + "_hold"); ++ Place(hcur(), ++ inv_, ++ hold[n]->getName(), ++ {{g_pins.inv[0], any_write}, {g_pins.inv[1], hold[n]}}); ++ } ++ // The word's reset and set, for `async_reset`: an active-high reset ++ // inverted here, and the set tied off, one of each per word so no ++ // net fans out past one word's bits. ++ dbNet* resetn = nullptr; ++ dbNet* setn = nullptr; ++ if (s.async_reset) { ++ resetn = Net(s.reset); ++ if (!s.reset_active_low) { ++ resetn = Net(wn + "_resetn"); ++ Place(hcur(), ++ inv_, ++ resetn->getName(), ++ {{g_pins.inv[0], Net(s.reset)}, {g_pins.inv[1], resetn}}); ++ } ++ setn = Net(wn + "_setn"); ++ Place(hcur(), tie_hi_, setn->getName(), {{g_pins.tie_hi[0], setn}}); ++ } ++ periphery_ = false; ++ for (auto& c : hc) { ++ if (c.x > bank_x0 + header_w) { ++ Refuse("header column overflow at word " + std::to_string(n) ++ + ": widen the header estimate"); ++ } ++ } ++ ++ // Tiles, this band's bits. ++ int x = bank_x0 + header_w; ++ for (int bl = 0; bl < bits_per_fold && f * bits_per_fold + bl < s.bits; ++ ++bl) { ++ const int b = f * bits_per_fold + bl; ++ if (tap_cols > 0 && bl % tap_every == 0) { ++ if (tap_) { ++ for (int k = 0; k < rows_per_word; ++k) { ++ Cursor tc{row0 + k, x}; ++ Place(tc, ++ tap_, ++ "tap_" + wn + "_c" + std::to_string(bl / tap_every) + "_r" ++ + std::to_string(k), ++ {}); ++ } ++ } ++ x += tap_w; // the rest of the service column stays empty ++ } ++ std::string tn = wn + "_b" + std::to_string(b); ++ std::vector tc; ++ for (int k = 0; k < rows_per_word; ++k) { ++ tc.push_back(Cursor{row0 + k, x}); ++ } ++ auto tcur = [&]() -> Cursor& { return Least(tc); }; ++ ++ // Storage. The flop's QN holds ~value; Q is recovered by an inverter. ++ // One gated write port: D is the write data bit. ++ dbNet* d = gated && W == 1 ? wdata[0][b] : Net(tn + "_d"); ++ dbNet* qn = Net(tn + "_qn"); ++ dbNet* q = g_pins.flop_inverted ? Net(tn + "_q") : qn; ++ const std::string ff_name ++ = s.store_name.empty() ++ ? tn + "_ff" ++ : Fmt(s.store_name, {{"word", n}, {"bit", b}}); ++ if (s.async_reset) { ++ Place(tcur(), ++ store_, ++ ff_name, ++ {{g_pins.flop_r[0], d}, ++ {g_pins.flop_r[1], gated ? gclk[n] : clock}, ++ {g_pins.flop_r[2], resetn}, ++ {g_pins.flop_r[3], setn}, ++ {g_pins.flop_r[4], qn}}); ++ } else { ++ Place(tcur(), ++ flop_, ++ ff_name, ++ {{g_pins.flop[0], d}, ++ {g_pins.flop[1], gated ? gclk[n] : clock}, ++ {g_pins.flop[2], qn}}); ++ } ++ if (g_pins.flop_inverted) { ++ Place(tcur(), ++ inv_, ++ tn + "_qinv", ++ {{g_pins.inv[0], qn}, {g_pins.inv[1], q}}); ++ } ++ // Write mux: d = OR_w (wsel_w & wdata_w[b]) | (hold & q), as AO22 ++ // pairs then an OR2 tree. ++ std::vector> terms; ++ for (int w = 0; w < W; ++w) { ++ terms.emplace_back(wsel[w][n], wdata[w][b]); ++ } ++ if (!gated) { ++ terms.emplace_back(hold[n], q); ++ } ++ std::vector partial; ++ for (size_t i = 0; write_pairs > 0 && i < terms.size(); i += 2) { ++ dbNet* y = Net(tn + "_wm" + std::to_string(i / 2)); ++ if (i + 1 < terms.size()) { ++ Place(tcur(), ++ ao22_, ++ y->getName(), ++ {{g_pins.ao22[0], terms[i].first}, ++ {g_pins.ao22[1], terms[i].second}, ++ {g_pins.ao22[2], terms[i + 1].first}, ++ {g_pins.ao22[3], terms[i + 1].second}, ++ {g_pins.ao22[4], y}}); ++ } else { ++ Place(tcur(), ++ and2_, ++ y->getName(), ++ {{g_pins.and2[0], terms[i].first}, ++ {g_pins.and2[1], terms[i].second}, ++ {g_pins.and2[2], y}}); ++ } ++ partial.push_back(y); ++ } ++ dbNet* mux = partial.empty() ? d ++ : partial.size() == 1 ++ ? partial[0] ++ : OrTree(tn + "_wo", ++ partial, ++ [&](int, int) -> Cursor& { return tcur(); }); ++ // The OR tree's root must be the flop's D net: alias by re-connecting. ++ if (mux != d) { ++ // Move every iterm on `mux` to `d`. ++ std::vector its(mux->getITerms().begin(), ++ mux->getITerms().end()); ++ for (auto* it : its) { ++ it->disconnect(); ++ it->connect(d); ++ } ++ dbNet::destroy(mux); ++ } ++ // This word's inputs to each port's bitline. ++ for (int r = 0; r < R; ++r) { ++ leaves[r][b].push_back(q); ++ leaf_sel[r][b].push_back(rsel[r][n]); ++ leaf_word[r][b].push_back(n); ++ } ++ // Keep the row cursors: the read OR trees fill the tile's leftover. ++ tiles_[static_cast(n) * s.bits + b] = Tile{tc, x + tile_w}; ++ x += tile_w; ++ } ++ } ++ } ++ ++ // ---- read bitlines: OR trees down each column ------------------------ ++ // A node covering words [lo, hi) is dropped into the tile of word ++ // (lo+hi)/2 in that bit column, round-robin over its rows. ++ std::vector> rdata(R); ++ std::vector footer; ++ for (int k = 0; k < footer_rows; ++k) { ++ footer.push_back(Cursor{k, 0}); ++ } ++ for (int r = 0; r < R; ++r) { ++ for (int b = 0; b < s.bits; ++b) { ++ std::string prefix = "rd" + std::to_string(r) + "_b" + std::to_string(b); ++ std::vector bank_roots; ++ for (int k = 0; k < banks; ++k) { ++ // The bank's words in entry order; ascending words keep it. ++ std::vector bank_q, bank_sel; ++ std::vector bank_words; ++ for (size_t i = 0; i < leaves[r][b].size(); ++i) { ++ if (bank_of(leaf_word[r][b][i]) == k) { ++ bank_q.push_back(leaves[r][b][i]); ++ bank_sel.push_back(leaf_sel[r][b][i]); ++ bank_words.push_back(leaf_word[r][b][i]); ++ } ++ } ++ bank_roots.push_back(Bitline( ++ prefix + "_k" + std::to_string(k), ++ bank_sel, ++ bank_q, ++ [&](int word) -> Cursor& { ++ return Least(tiles_[word * s.bits + b].rows); ++ }, ++ bank_words)); ++ } ++ // Each root becomes an output net with the RTL's name: one per ++ // bank for a banked port, one after the footer OR for a plain one. ++ auto emit = [&](dbNet* root, const std::string& name) { ++ std::vector its(root->getITerms().begin(), ++ root->getITerms().end()); ++ dbNet* out = Net(name); ++ for (auto* it : its) { ++ it->disconnect(); ++ it->connect(out); ++ } ++ dbNet::destroy(root); ++ Output(name, out); ++ rdata[r].push_back(out); ++ }; ++ if (s.read[r].banked()) { ++ for (int k = 0; k < banks; ++k) { ++ emit(bank_roots[k], Bit(s.read[r].bank_data[k], b)); ++ } ++ } else { ++ dbNet* root ++ = banks == 1 ++ ? bank_roots[0] ++ : OrTree(prefix + "_f", bank_roots, [&](int, int) -> Cursor& { ++ return Least(footer); ++ }); ++ emit(root, Bit(s.read[r].data, b)); ++ } ++ } ++ } ++ for (size_t i = 0; i < tiles_.size(); ++i) { ++ for (const auto& c : tiles_[i].rows) { ++ if (c.x > tiles_[i].x_end) { ++ Refuse( ++ "tile " + std::to_string(i) + " overflowed by " ++ + std::to_string(c.x - tiles_[i].x_end) ++ + " dbu once its read OR trees were in: widen the tile estimate"); ++ } ++ } ++ } ++ for (const auto& c : footer) { ++ if (c.x > core_w) { ++ Refuse("footer overflow: widen the footer estimate"); ++ } ++ } ++ if (max_x_ > core_w) { ++ Refuse("placement ran past the core: " + std::to_string(max_x_) + " > " ++ + std::to_string(core_w)); ++ } ++ ++ // ---- pins on the die edge -------------------------------------------- ++ // Read data along the bottom and write data along the top, on the ++ // vertical layer; addresses, enables and the clock on the left, on the ++ // horizontal layer. Centres sit on the platform's track grid for that ++ // layer so a parent's macro placer can align them (MPL-0005 otherwise). ++ const int track_off = static_cast(s.pin_track_offset_um * dbu_); ++ const int track_pitch ++ = std::max(static_cast(s.pin_track_pitch_um * dbu_), 1); ++ auto on_track = [&](int v) { ++ int k = (v - track_off + track_pitch - 1) / track_pitch; ++ return track_off + std::max(k, 0) * track_pitch; ++ }; ++ const int edge = pin_w_; // pin centre this far in from the die edge ++ int px = on_track(core_x0_ + track_pitch); ++ auto bottom = [&](const std::string& name) { ++ Pin(block_->findBTerm(name.c_str()), layer_v_, px, edge); ++ px += track_pitch; ++ }; ++ for (int r = 0; r < R; ++r) { ++ for (int b = 0; b < s.bits; ++b) { ++ if (s.read[r].banked()) { ++ for (int k = 0; k < banks; ++k) { ++ bottom(Bit(s.read[r].bank_data[k], b)); ++ } ++ } else { ++ bottom(Bit(s.read[r].data, b)); ++ } ++ } ++ } ++ const int px_bottom_end = px; ++ px = on_track(core_x0_ + track_pitch); ++ for (int w = 0; w < W; ++w) { ++ for (int b = 0; b < s.bits; ++b) { ++ Pin(block_->findBTerm(Bit(s.write[w].data, b).c_str()), ++ layer_v_, ++ px, ++ die_h - edge); ++ px += track_pitch; ++ } ++ } ++ int py = on_track(core_y0_ + track_pitch); ++ auto left = [&](const std::string& name) { ++ Pin(block_->findBTerm(name.c_str()), layer_h_, edge, py); ++ py += track_pitch; ++ }; ++ left(s.clock); ++ if (!s.reset.empty()) { ++ left(s.reset); ++ } ++ for (const auto& u : s.unused) { ++ left(u); ++ } ++ for (int r = 0; r < R; ++r) { ++ for (size_t k = 0; k < raddr[r].size(); ++k) { ++ for (size_t i = 0; i < raddr[r][k].size(); ++i) { ++ left(s.read[r].banked() ++ ? Bit(s.read[r].bank_addr[k], static_cast(i)) ++ : Bit(s.read[r].addr, static_cast(i))); ++ } ++ } ++ } ++ for (int w = 0; w < W; ++w) { ++ for (int i = 0; i < A; ++i) { ++ left(Bit(s.write[w].addr, i)); ++ } ++ if (wen[w] != nullptr) { ++ left(s.write[w].en); ++ } ++ } ++ if (std::max(px, px_bottom_end) > die_w || py > die_h) { ++ Refuse( ++ "more pins than the die edge holds at the track pitch; the array " ++ "is too narrow for its ports (widen with fewer banks or more bits)"); ++ } ++ ++ logger_->report( ++ "generate_regfile: {} {}x{} {}R{}W in {} bank(s) as {} x {}, {} bit " ++ "fold(s): " ++ "{} rows of {} sites, die {} x {} um, {} instances", ++ s.module, ++ s.words, ++ s.bits, ++ R, ++ W, ++ banks, ++ bank_cols, ++ bank_rows, ++ folds, ++ total_rows, ++ core_w / site_w_, ++ die_w / static_cast(tech->getLefUnits()), ++ die_h / static_cast(tech->getLefUnits()), ++ block_->getInsts().size()); ++ // In netlist mode the core alone is the generator's: the tiles and the ++ // read bitlines, FIRM. The periphery -- address inverters, read address ++ // registers, each word's decode, hold -- is left unplaced, so the parent's ++ // flow places, sizes and buffers it like any logic: it alone knows the ++ // drivers and loads outside the array. Fixed by the generator, an ++ // address inverter drove its literal into every word's decode at a ++ // 3.7 ns slew on XiangShan's ROB that nothing was allowed to repair. The ++ // header column and the address band stay as free sites beside the core. ++ // A macro is the generator's to the last cell and keeps its periphery. ++ if (s.mode == "netlist") { ++ // The abstract a parent's macro placer sees before the block dissolves ++ // into its rows: each port's pin where its connections land, at the ++ // centroid of the cells on its net -- a write data bit over its ++ // column's write muxes, a read data bit at its bitline's root, an ++ // address among its inverters and decode. Edge pins would tell the ++ // placer's flips nothing about where the wires go; these pins are ++ // never routed, the block dissolves first. ++ std::vector>> moves; ++ for (dbBTerm* t : block_->getBTerms()) { ++ dbNet* net = t->getNet(); ++ if (net == nullptr) { ++ continue; ++ } ++ long long sx = 0, sy = 0; ++ int n = 0; ++ for (odb::dbITerm* it : net->getITerms()) { ++ const odb::Rect b = it->getInst()->getBBox()->getBox(); ++ sx += b.xCenter(); ++ sy += b.yCenter(); ++ ++n; ++ } ++ if (n > 0) { ++ // on the track grid, as the edge pins are (MPL-0005 otherwise) ++ moves.push_back({t, ++ {on_track(static_cast(sx / n)), ++ on_track(static_cast(sy / n))}}); ++ } ++ } ++ for (auto& [t, xy] : moves) { ++ std::vector old(t->getBPins().begin(), t->getBPins().end()); ++ for (odb::dbBPin* bp : old) { ++ odb::dbBPin::destroy(bp); ++ } ++ Pin(t, layer_h_, xy.first, xy.second); ++ } ++ for (dbInst* inst : periphery_insts_) { ++ inst->setPlacementStatus(odb::dbPlacementStatus::UNPLACED); ++ } ++ } ++ return block_; ++} ++ ++} // namespace ++ ++Spec ReadSpec(const std::string& path) ++{ ++ std::ifstream in(path); ++ if (!in) { ++ Refuse("cannot read spec " + path); ++ } ++ Spec s; ++ g_pins = Pins(); ++ std::string line; ++ int lineno = 0; ++ while (std::getline(in, line)) { ++ ++lineno; ++ auto hash = line.find('#'); ++ if (hash != std::string::npos) { ++ line = line.substr(0, hash); ++ } ++ std::istringstream ss(line); ++ std::string key; ++ if (!(ss >> key)) { ++ continue; ++ } ++ std::vector v; ++ for (std::string t; ss >> t;) { ++ v.push_back(t); ++ } ++ auto need = [&](size_t n) { ++ if (v.size() != n) { ++ Refuse(path + ":" + std::to_string(lineno) + ": `" + key + "` takes " ++ + std::to_string(n) + " value(s)"); ++ } ++ }; ++ if (key == "module") { ++ need(1); ++ s.module = v[0]; ++ } else if (key == "mode") { ++ need(1); ++ if (v[0] != "macro" && v[0] != "netlist") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": mode is macro or netlist, not `" + v[0] + "`"); ++ } ++ s.mode = v[0]; ++ } else if (key == "words") { ++ need(1); ++ s.words = std::stoi(v[0]); ++ } else if (key == "bits") { ++ need(1); ++ s.bits = std::stoi(v[0]); ++ } else if (key == "clock") { ++ need(1); ++ s.clock = v[0]; ++ } else if (key == "reset") { ++ need(1); ++ if (!s.reset.empty()) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": one of `reset` or `async_reset`, once"); ++ } ++ s.reset = v[0]; ++ } else if (key == "write_priority") { ++ need(1); ++ if (v[0] != "or" && v[0] != "last") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `write_priority or|last`"); ++ } ++ s.write_priority = v[0]; ++ } else if (key == "unused") { ++ if (v.empty()) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `unused ...` names at least one port"); ++ } ++ s.unused.insert(s.unused.end(), v.begin(), v.end()); ++ } else if (key == "async_reset") { ++ need(2); ++ if (!s.reset.empty()) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": one of `reset` or `async_reset`, once"); ++ } ++ if (v[1] != "low" && v[1] != "high") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `async_reset low|high`"); ++ } ++ s.reset = v[0]; ++ s.async_reset = true; ++ s.reset_active_low = v[1] == "low"; ++ } else if (key == "read") { ++ need(2); ++ s.read.push_back(Port{.addr = v[0], ++ .data = v[1], ++ .en = "", ++ .bank_addr = {}, ++ .bank_data = {}}); ++ } else if (key == "read_banked") { ++ if (v.size() < 2 || v.size() % 2 != 0) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `read_banked` takes addr data pairs, one per bank"); ++ } ++ Port port; ++ for (size_t i = 0; i < v.size(); i += 2) { ++ port.bank_addr.push_back(v[i]); ++ port.bank_data.push_back(v[i + 1]); ++ } ++ s.read.push_back(port); ++ } else if (key == "write") { ++ if (v.size() != 2 && v.size() != 3) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `write` takes addr data [en]"); ++ } ++ s.write.push_back(Port{.addr = v[0], ++ .data = v[1], ++ .en = v.size() == 3 ? v[2] : "", ++ .bank_addr = {}, ++ .bank_data = {}}); ++ } else if (key == "cell") { ++ need(2); ++ const std::string& kind = v[0]; ++ if (kind == "flop") { ++ s.cells.flop = v[1]; ++ } else if (kind == "flop_r") { ++ s.cells.flop_r = v[1]; ++ } else if (kind == "tie_hi") { ++ s.cells.tie_hi = v[1]; ++ } else if (kind == "and2") { ++ s.cells.and2 = v[1]; ++ } else if (kind == "or2") { ++ s.cells.or2 = v[1]; ++ } else if (kind == "ao22") { ++ s.cells.ao22 = v[1]; ++ } else if (kind == "aoi22") { ++ s.cells.aoi22 = v[1]; ++ } else if (kind == "nand2") { ++ s.cells.nand2 = v[1]; ++ } else if (kind == "nor2") { ++ s.cells.nor2 = v[1]; ++ } else if (kind == "inv") { ++ s.cells.inv = v[1]; ++ } else if (kind == "tap") { ++ s.cells.tap = v[1]; ++ } else if (kind == "tie_lo") { ++ s.cells.tie_lo = v[1]; ++ } else if (kind == "icg") { ++ s.cells.icg = v[1]; ++ } else { ++ Refuse(path + ":" + std::to_string(lineno) + ": unknown cell kind " ++ + kind); ++ } ++ } else if (key == "pins") { ++ if (v.size() < 2) { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": `pins ...`"); ++ } ++ std::vector pins(v.begin() + 1, v.end()); ++ const std::string& kind = v[0]; ++ auto set = [&](std::vector& dst, size_t n) { ++ if (pins.size() != n) { ++ Refuse(path + ":" + std::to_string(lineno) + ": `pins " + kind ++ + "` takes " + std::to_string(n) + " pin names"); ++ } ++ dst = pins; ++ }; ++ if (kind == "flop") { ++ set(g_pins.flop, 3); ++ } else if (kind == "flop_r") { ++ set(g_pins.flop_r, 5); ++ } else if (kind == "tie_hi") { ++ set(g_pins.tie_hi, 1); ++ } else if (kind == "and2") { ++ set(g_pins.and2, 3); ++ } else if (kind == "or2") { ++ set(g_pins.or2, 3); ++ } else if (kind == "ao22") { ++ set(g_pins.ao22, 5); ++ } else if (kind == "aoi22") { ++ set(g_pins.aoi22, 5); ++ } else if (kind == "nand2") { ++ set(g_pins.nand2, 3); ++ } else if (kind == "nor2") { ++ set(g_pins.nor2, 3); ++ } else if (kind == "inv") { ++ set(g_pins.inv, 2); ++ } else if (kind == "icg") { ++ set(g_pins.icg, 4); ++ } else { ++ Refuse(path + ":" + std::to_string(lineno) + ": unknown pins kind " ++ + kind); ++ } ++ } else if (key == "write_style") { ++ need(1); ++ if (v[0] != "mux" && v[0] != "clock_gate") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": write_style is mux or clock_gate"); ++ } ++ s.write_style = v[0]; ++ } else if (key == "flop_output") { ++ need(1); ++ if (v[0] == "QN") { ++ g_pins.flop_inverted = true; ++ } else if (v[0] == "Q") { ++ g_pins.flop_inverted = false; ++ } else { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": flop_output is Q or QN"); ++ } ++ } else if (key == "pin_layer" || key == "pin_layer_h") { ++ need(1); ++ s.pin_layer_h = v[0]; ++ } else if (key == "pin_layer_v") { ++ need(1); ++ s.pin_layer_v = v[0]; ++ } else if (key == "pin_track") { ++ need(2); ++ s.pin_track_offset_um = std::stod(v[0]); ++ s.pin_track_pitch_um = std::stod(v[1]); ++ } else if (key == "tap_columns") { ++ need(1); ++ s.tap_columns = std::stoi(v[0]); ++ } else if (key == "service_sites") { ++ need(1); ++ s.service_sites = std::stoi(v[0]); ++ } else if (key == "banks") { ++ need(1); ++ s.banks = std::stoi(v[0]); ++ } else if (key == "bank_columns") { ++ need(1); ++ s.bank_columns = std::stoi(v[0]); ++ } else if (key == "bit_folds") { ++ need(1); ++ s.bit_folds = std::stoi(v[0]); ++ } else if (key == "read_latency") { ++ need(1); ++ if (v[0] != "0" && v[0] != "1") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": read_latency is 0 or 1"); ++ } ++ s.read_latency = std::stoi(v[0]); ++ } else if (key == "bank_order") { ++ need(1); ++ if (v[0] != "contiguous" && v[0] != "interleaved") { ++ Refuse(path + ":" + std::to_string(lineno) ++ + ": bank_order is contiguous or interleaved"); ++ } ++ s.bank_order = v[0]; ++ } else if (key == "zero_word") { ++ need(1); ++ s.zero_word = std::stoi(v[0]); ++ } else if (key == "store_name") { ++ need(1); ++ s.store_name = v[0]; ++ } else if (key == "read_reg_name") { ++ need(1); ++ s.read_reg_name = v[0]; ++ } else if (key == "lib") { ++ need(2); ++ double x = std::stod(v[1]); ++ const std::string& knob = v[0]; ++ if (knob == "gate_delay_ps") { ++ s.lib.gate_delay_ps = x; ++ } else if (knob == "wire_factor") { ++ s.lib.wire_factor = x; ++ } else if (knob == "input_load_ff") { ++ s.lib.input_load_ff = x; ++ } else if (knob == "clock_load_ff") { ++ s.lib.clock_load_ff = x; ++ } else if (knob == "leakage_nw_per_cell") { ++ s.lib.leakage_nw_per_cell = x; ++ } else if (knob == "output_max_cap_ff") { ++ s.lib.output_max_cap_ff = x; ++ } else if (knob == "hold_ps") { ++ s.lib.hold_ps = x; ++ } else { ++ Refuse(path + ":" + std::to_string(lineno) + ": unknown lib knob " ++ + knob); ++ } ++ } else { ++ Refuse(path + ":" + std::to_string(lineno) + ": unknown key `" + key ++ + "`"); ++ } ++ } ++ if (s.module.empty()) { ++ Refuse(path + ": no `module`"); ++ } ++ if (s.cells.flop.empty() || s.cells.and2.empty() || s.cells.or2.empty() ++ || s.cells.ao22.empty() || s.cells.inv.empty()) { ++ Refuse(path + ": cells flop, and2, or2, ao22 and inv are all required"); ++ } ++ if (s.async_reset && (s.cells.flop_r.empty() || s.cells.tie_hi.empty())) { ++ Refuse(path + ": `async_reset` needs `cell flop_r` and `cell tie_hi`"); ++ } ++ return s; ++} ++ ++odb::dbBlock* Generate(odb::dbDatabase* db, ++ utl::Logger* logger, ++ const Spec& spec) ++{ ++ Builder b(db, logger, spec); ++ return b.Run(); ++} ++ ++void WriteVerilog(odb::dbBlock* block, const std::string& path) ++{ ++ std::ofstream out(path); ++ if (!out) { ++ Refuse("cannot write " + path); ++ } ++ // Group bterms into buses by `name[i]`. ++ struct Bus ++ { ++ odb::dbIoType io; ++ int hi = -1; ++ bool scalar = false; ++ }; ++ std::map buses; ++ std::vector order; ++ for (odb::dbBTerm* t : block->getBTerms()) { ++ std::string n = t->getName(); ++ if (t->getSigType() != odb::dbSigType::SIGNAL) { ++ continue; ++ } ++ auto lb = n.rfind('['); ++ std::string base = n; ++ int idx = -1; ++ if (lb != std::string::npos && n.back() == ']') { ++ base = n.substr(0, lb); ++ idx = std::stoi(n.substr(lb + 1, n.size() - lb - 2)); ++ } ++ auto it = buses.find(base); ++ if (it == buses.end()) { ++ order.push_back(base); ++ it = buses.emplace(base, Bus{t->getIoType(), -1, idx < 0}).first; ++ } ++ it->second.hi = std::max(it->second.hi, idx); ++ } ++ out << "// Generated by generate_regfile. Structural: every cell placed.\n"; ++ out << "module " << block->getName() << "(\n"; ++ for (size_t i = 0; i < order.size(); ++i) { ++ const Bus& b = buses[order[i]]; ++ out << " " << (b.io == odb::dbIoType::INPUT ? "input" : "output"); ++ if (!b.scalar) { ++ out << " [" << b.hi << ":0]"; ++ } ++ out << " " << order[i] << (i + 1 < order.size() ? ",\n" : "\n"); ++ } ++ out << ");\n"; ++ for (odb::dbNet* n : block->getNets()) { ++ if (n->getBTerms().begin() != n->getBTerms().end()) { ++ continue; // a port ++ } ++ if (n->getSigType() == odb::dbSigType::POWER ++ || n->getSigType() == odb::dbSigType::GROUND) { ++ continue; ++ } ++ out << " wire \\" << n->getName() << " ;\n"; ++ } ++ for (odb::dbInst* inst : block->getInsts()) { ++ out << " " << inst->getMaster()->getName() << " \\" << inst->getName() ++ << " ("; ++ bool first = true; ++ for (odb::dbITerm* it : inst->getITerms()) { ++ odb::dbNet* n = it->getNet(); ++ if (n == nullptr) { ++ continue; ++ } ++ if (n->getSigType() == odb::dbSigType::POWER ++ || n->getSigType() == odb::dbSigType::GROUND) { ++ continue; ++ } ++ out << (first ? "" : ", ") << "." << it->getMTerm()->getName() << "("; ++ // A port net is written by its port name, buses with [i] intact. ++ out << (n->getBTerms().begin() != n->getBTerms().end() ++ ? n->getName() ++ : "\\" + n->getName() + " ") ++ << ")"; ++ first = false; ++ } ++ out << ");\n"; ++ } ++ out << "endmodule\n"; ++} ++ ++} // namespace ram::regfile +diff --git a/src/ram/src/regfile.h b/src/ram/src/regfile.h +new file mode 100644 +index 00000000000..649ac3fe814 +--- /dev/null ++++ b/src/ram/src/regfile.h +@@ -0,0 +1,182 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++// generate_regfile: a multi-port register file as a placed standard-cell ++// macro, built directly in odb. ++// ++// A register file synthesised to flops is a flop per bit, a W-way write ++// mux on each flop's D and an R-way read mux tree per bit: dense, and the ++// legaliser cannot spread it. Here it is laid out the way a bitcell array ++// is: word rows by bit columns, one tile per (word, bit) holding the flop, ++// its write select and R read AND gates; the read wordlines run along the ++// rows and are decoded in a header column, the read bitlines are OR trees ++// in a footer row. Every cell is placed by construction; nothing is left ++// to a placer. ++#pragma once ++ ++#include ++#include ++ ++namespace odb { ++class dbDatabase; ++class dbBlock; ++class dbMaster; ++class dbNet; ++class dbInst; ++} // namespace odb ++ ++namespace utl { ++class Logger; ++} ++ ++namespace ram::regfile { ++ ++// One read or write port, with the RTL's own pin names so the macro drops ++// in for the module it replaces. ++struct Port ++{ ++ std::string addr; // bus name; bits are [i] ++ std::string data; // bus name; bits are [i] ++ std::string en; // write ports only; empty means always enabled ++ // A banked read port (Chisel RegfileBank): one address and one data bus ++ // per bank, the bank mux outside. addr/data above are unused then. ++ std::vector bank_addr; ++ std::vector bank_data; ++ bool banked() const { return !bank_addr.empty(); } ++}; ++ ++// The cells the array is built from, by name in the loaded LEF. ++struct Cells ++{ ++ std::string flop; // D flip-flop, Q output (QN accepted, see below) ++ // the storage flop with `async_reset`: an asynchronous reset (RESETN) ++ // and set (SETN, tied off) flop ++ std::string flop_r; ++ std::string tie_hi; // ties flop_r's set off, one per word ++ std::string and2; // read select AND ++ std::string or2; // bitline OR tree node ++ std::string ao22; // write mux node: (sel & new) | (hold & old) ++ // read bitline: AOI22 leaf per word pair, NAND2/NOR2 tree over them ++ std::string aoi22 = "AOI22xp5_ASAP7_75t_R"; ++ std::string nand2 = "NAND2xp5_ASAP7_75t_R"; ++ std::string nor2 = "NOR2xp33_ASAP7_75t_R"; ++ std::string inv; // decoder inverters ++ std::string tap; // well tap, one column per tap_columns ++ // integrated clock gate, one per word with `write_style clock_gate` ++ std::string icg = "ICGx1_ASAP7_75t_R"; ++ std::string tie_lo; // for unused inputs ++}; ++ ++// Knobs of the model liberty (views.h), asap7-shaped defaults, set in ++// the spec with `lib `. Picoseconds, femtofarads, ++// nanowatts, so a spec reads in plain units. ++struct LibModel ++{ ++ double gate_delay_ps = 25.0; // one loaded gate level ++ double wire_factor = 1.3; // on top of the gate levels ++ double input_load_ff = 0.6; // per driven cell input ++ double clock_load_ff = 10.0; // the block's clock pin as the parent sees it ++ double leakage_nw_per_cell = 1.0; ++ double output_max_cap_ff = 50.0; ++ double hold_ps = 0.0; ++}; ++ ++struct Spec ++{ ++ // "macro" (an abstract and a model liberty for a parent to place) or ++ // "netlist" (the placed cells dropped into the parent's rows by the ++ // flow); the generator builds the same block either way. The flow's ++ // memories step reads this key too, and decides which views to ask for. ++ std::string mode = "macro"; ++ std::string module; // generated module and block name ++ int words = 0; ++ int bits = 0; ++ std::string clock = "clock"; ++ std::string reset; // optional: a port the RTL has and the array ignores ++ // `async_reset low|high`: the reset clears every stored word to ++ // zero asynchronously, as an RTL `always @(posedge clk or negedge ++ // rst_n)` register file does; the storage flops are `cell flop_r`. The ++ // port is `reset` above. Read-address registers (read_latency) are not ++ // reset. ++ bool async_reset = false; ++ bool reset_active_low = true; ++ // `unused `: one-bit inputs the RTL module has and the array ++ // ignores (ibex's test_en_i), so the block has the module's ports. ++ std::vector unused; ++ std::vector read; ++ std::vector write; ++ Cells cells; ++ int tap_columns = 8; // a tap column every N bit columns ++ int service_sites = 40; // free sites beside each tap, for the clock tree ++ int banks = 1; // word columns side by side; words % banks == 0 ++ // How many of the banks stand side by side; the rest stack below them. ++ // 0 means all of them, one row of banks. The outline is bank_columns ++ // bank widths wide and banks/bank_columns bank heights tall: a wide ++ // word (128 bits) wants its banks stacked, a narrow one wants them ++ // side by side. banks % bank_columns == 0. ++ int bank_columns = 0; ++ // The word split into this many bit bands, stacked: a 564-bit word ++ // is one band 564 tiles wide, or four bands of 141 one above the ++ // other, each with its own copy of the word decode; the last band is ++ // short when the folds do not divide. Banks fold words; this folds bits. ++ int bit_folds = 1; ++ // Read latency in cycles: 0 reads combinationally from the address ++ // ports; 1 registers each read address on the clock first, the way ++ // firtool writes a Chisel register file read through RegNext(addr) ++ // (XiangShan's IntRegFile, FpRegFile and VfRegFile). The macro ++ // replaces the whole module, so it has to carry that register. ++ int read_latency = 0; ++ // How words fall into banks: "contiguous" (bank n / words_per_bank, ++ // the default) or "interleaved" (bank n % banks, entry n / banks, as ++ // XiangShan's IntRegFile banks its read: bank k's entry l is word ++ // l * banks + k). ++ std::string bank_order = "contiguous"; ++ // A word that is the constant zero and has no storage (RISC-V x0): it ++ // reads 0 and a write to it is dropped. -1 for none. ++ int zero_word = -1; ++ // How a word keeps its value between writes: "mux" (the default), a ++ // free-running clock and a hold term in every bit's write mux; or ++ // "clock_gate", one integrated clock gate per word enabled by any of ++ // its write selects, as generate_ram writes, so a bit's D is the write ++ // data itself with one write port and a select mux of the writes with ++ // more. ++ std::string write_style = "mux"; ++ // Two ports writing one word in one cycle: "or" (the default) ORs their ++ // data, for an RTL that never does it; "last", the later port in the ++ // spec wins, as an RTL's later assignment does (cva6's commit ports). ++ std::string write_priority = "or"; ++ // Instance names of the flops, as patterns, so a flop carries the name ++ // of the RTL register bit it implements (an equivalence check matches ++ // sequential instances by name). {word} {bit} for the storage, ++ // {port} {bank} {bit} for a read address register. Empty keeps the ++ // generator's own names. ++ std::string store_name; ++ std::string read_reg_name; ++ LibModel lib; ++ // Pins: left and right edges on the horizontal layer, top and bottom on ++ // the vertical one, centred on that layer's track grid as the platform ++ // makes it, so a parent's macro placer can align them. ++ std::string pin_layer_h = "M4"; ++ std::string pin_layer_v = "M5"; ++ double pin_track_offset_um = 0.012; ++ double pin_track_pitch_um = 0.048; ++}; ++ ++// Reads a spec from a small `key value` text file (see README.md). ++// Throws std::runtime_error with the offending line on anything it does ++// not understand: a spec is either complete or refused. ++Spec ReadSpec(const std::string& path); ++ ++// Builds the netlist and placement of `spec` into a new block of `db`, ++// whose tech and libs must already hold the cells named in the spec. ++// Returns the block. Throws std::runtime_error naming the cell or pin ++// when the library does not fit the spec. ++odb::dbBlock* Generate(odb::dbDatabase* db, ++ utl::Logger* logger, ++ const Spec& spec); ++ ++// Writes the block as a structural Verilog module, cells as instances, ++// for simulation and for the flow's netlist view. ++void WriteVerilog(odb::dbBlock* block, const std::string& path); ++ ++} // namespace ram::regfile +diff --git a/src/ram/src/regfile_cmd.cpp b/src/ram/src/regfile_cmd.cpp +new file mode 100644 +index 00000000000..d90332ba559 +--- /dev/null ++++ b/src/ram/src/regfile_cmd.cpp +@@ -0,0 +1,93 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++#include ++#include ++#include ++ ++#include "odb/db.h" ++#include "odb/defout.h" ++#include "ram/regfile.h" ++#include "regfile.h" ++#include "utl/Logger.h" ++#include "views.h" ++ ++namespace ram { ++ ++void generateRegfile(odb::dbDatabase* db, ++ utl::Logger* logger, ++ const std::string& spec_path, ++ const std::string& verilog_path, ++ const std::string& def_path, ++ const std::string& lef_path, ++ const std::string& liberty_path, ++ const std::string& check_ports) ++{ ++ try { ++ regfile::Spec spec = regfile::ReadSpec(spec_path); ++ if (!check_ports.empty()) { ++ // The spec against the RTL it stands in for, before anything is ++ // built: a block whose ports are not the module's is a silent ++ // miswire at the parent. ++ auto ports = regfile::ReadModulePorts(check_ports, spec.module); ++ auto problems = regfile::CheckPorts(spec, ports); ++ if (!problems.empty()) { ++ std::string all; ++ for (const auto& p : problems) { ++ all += "\n " + p; ++ } ++ throw std::runtime_error("spec does not match module " + spec.module ++ + " in " + check_ports + ":" + all); ++ } ++ } ++ odb::dbBlock* block = regfile::Generate(db, logger, spec); ++ // In mode netlist the generator fixes the core and leaves the ++ // periphery to the parent, which alone knows the drivers and loads ++ // outside the array; the block on its own shows those cells unplaced. ++ int fixed = 0; ++ int unplaced = 0; ++ for (odb::dbInst* inst : block->getInsts()) { ++ if (!inst->getPlacementStatus().isPlaced()) { ++ ++unplaced; ++ } else if (inst->getPlacementStatus().isFixed()) { ++ ++fixed; ++ } ++ } ++ if (unplaced > 0) { ++ logger->info(utl::RAM, ++ 52, ++ "mode {}: {} periphery instances left unplaced for the " ++ "parent to place, size and buffer; {} fixed.", ++ spec.mode, ++ unplaced, ++ fixed); ++ } ++ if (!verilog_path.empty()) { ++ regfile::WriteVerilog(block, verilog_path); ++ } ++ if (!def_path.empty()) { ++ odb::DefOut writer(logger); ++ if (!writer.writeBlock(block, def_path.c_str())) { ++ throw std::runtime_error("cannot write " + def_path); ++ } ++ } ++ if (!lef_path.empty()) { ++ regfile::WriteLef(block, logger, lef_path); ++ } ++ if (!liberty_path.empty()) { ++ // A flow's memories directory holds .lib and _pre_layout.lib; ++ // the model is the same file twice, said so in its comment. ++ regfile::WriteLiberty(block, spec, spec.lib, liberty_path, false); ++ std::string pre = liberty_path; ++ auto dot = pre.rfind(".lib"); ++ if (dot != std::string::npos) { ++ pre = pre.substr(0, dot) + "_pre_layout.lib"; ++ regfile::WriteLiberty(block, spec, spec.lib, pre, true); ++ } ++ } ++ } catch (const std::exception& e) { ++ logger->error(utl::RAM, 50, "generate_regfile: {}", e.what()); ++ } ++} ++ ++} // namespace ram +diff --git a/src/ram/src/views.cpp b/src/ram/src/views.cpp +new file mode 100644 +index 00000000000..1fe0e3bba55 +--- /dev/null ++++ b/src/ram/src/views.cpp +@@ -0,0 +1,662 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++// generate_regfile: abstract LEF, model liberty, and the spec-against-RTL ++// port check. See views.h. ++#include "views.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "odb/db.h" ++#include "odb/lefout.h" ++#include "regfile.h" ++#include "utl/Logger.h" ++ ++namespace ram::regfile { ++ ++namespace { ++ ++int Log2Ceil(int n) ++{ ++ int k = 0; ++ while ((1 << k) < n) { ++ ++k; ++ } ++ return k; ++} ++ ++int AddrWidth(int words) ++{ ++ return std::max(Log2Ceil(words), 1); ++} ++ ++// An integer constant expression of a module header: decimal numbers, ++// parameters by name, + - * / and parentheses. Throws naming what it ++// cannot evaluate: a width the check cannot read is a refusal, not a ++// port to skip. ++class ConstExpr ++{ ++ public: ++ ConstExpr(const std::string& text, const std::map& params) ++ : text_(text), params_(params) ++ { ++ } ++ long Eval() ++ { ++ long v = Sum(); ++ Space(); ++ if (pos_ != text_.size()) { ++ Fail(); ++ } ++ return v; ++ } ++ ++ private: ++ void Space() ++ { ++ while (pos_ < text_.size() ++ && std::isspace(static_cast(text_[pos_]))) { ++ ++pos_; ++ } ++ } ++ [[noreturn]] void Fail() ++ { ++ throw std::runtime_error("cannot evaluate `" + text_ ++ + "` in a module header"); ++ } ++ long Sum() ++ { ++ long v = Product(); ++ for (;;) { ++ Space(); ++ if (pos_ < text_.size() && (text_[pos_] == '+' || text_[pos_] == '-')) { ++ const char op = text_[pos_++]; ++ const long r = Product(); ++ v = op == '+' ? v + r : v - r; ++ } else { ++ return v; ++ } ++ } ++ } ++ long Product() ++ { ++ long v = Atom(); ++ for (;;) { ++ Space(); ++ if (pos_ < text_.size() && (text_[pos_] == '*' || text_[pos_] == '/')) { ++ const char op = text_[pos_++]; ++ const long r = Atom(); ++ if (op == '/' && r == 0) { ++ Fail(); ++ } ++ v = op == '*' ? v * r : v / r; ++ } else { ++ return v; ++ } ++ } ++ } ++ long Atom() ++ { ++ Space(); ++ if (pos_ >= text_.size()) { ++ Fail(); ++ } ++ if (text_[pos_] == '(') { ++ ++pos_; ++ const long v = Sum(); ++ Space(); ++ if (pos_ >= text_.size() || text_[pos_] != ')') { ++ Fail(); ++ } ++ ++pos_; ++ return v; ++ } ++ if (text_[pos_] == '-') { ++ ++pos_; ++ return -Atom(); ++ } ++ if (std::isdigit(static_cast(text_[pos_]))) { ++ long v = 0; ++ while (pos_ < text_.size() ++ && std::isdigit(static_cast(text_[pos_]))) { ++ v = v * 10 + (text_[pos_++] - '0'); ++ } ++ return v; ++ } ++ size_t start = pos_; ++ while (pos_ < text_.size() ++ && (std::isalnum(static_cast(text_[pos_])) ++ || text_[pos_] == '_')) { ++ ++pos_; ++ } ++ auto it = params_.find(text_.substr(start, pos_ - start)); ++ if (start == pos_ || it == params_.end()) { ++ Fail(); ++ } ++ return it->second; ++ } ++ std::string text_; ++ const std::map& params_; ++ size_t pos_ = 0; ++}; ++ ++// `text` without its // and /* */ comments. ++std::string StripComments(const std::string& text) ++{ ++ std::string out; ++ out.reserve(text.size()); ++ for (size_t i = 0; i < text.size(); ++i) { ++ if (text.compare(i, 2, "//") == 0) { ++ i = text.find('\n', i); ++ if (i == std::string::npos) { ++ break; ++ } ++ out += '\n'; ++ } else if (text.compare(i, 2, "/*") == 0) { ++ i = text.find("*/", i + 2); ++ if (i == std::string::npos) { ++ break; ++ } ++ ++i; ++ } else { ++ out += text[i]; ++ } ++ } ++ return out; ++} ++ ++// The text between the parenthesis at `open` and its match. ++std::string Balanced(const std::string& text, size_t open, size_t* end) ++{ ++ int depth = 0; ++ for (size_t i = open; i < text.size(); ++i) { ++ if (text[i] == '(') { ++ ++depth; ++ } else if (text[i] == ')' && --depth == 0) { ++ *end = i; ++ return text.substr(open + 1, i - open - 1); ++ } ++ } ++ throw std::runtime_error("unbalanced parentheses in a module header"); ++} ++ ++// Splits at the commas outside brackets and parentheses. ++std::vector TopLevelCommas(const std::string& text) ++{ ++ std::vector out; ++ int depth = 0; ++ std::string cur; ++ for (char c : text) { ++ if (c == '(' || c == '[' || c == '{') { ++ ++depth; ++ } else if (c == ')' || c == ']' || c == '}') { ++ --depth; ++ } ++ if (c == ',' && depth == 0) { ++ out.push_back(cur); ++ cur.clear(); ++ } else { ++ cur += c; ++ } ++ } ++ out.push_back(cur); ++ return out; ++} ++ ++std::string F(double v) ++{ ++ std::ostringstream s; ++ s.precision(6); ++ s << std::fixed << v; ++ return s.str(); ++} ++ ++// The two-point tables FakeRAM's libs use: constant across slew and load, ++// which is what a model is. ++void Table2(std::ostream& o, ++ const char* kind, ++ const std::string& tmpl, ++ double value_ns, ++ int indent) ++{ ++ std::string pad(indent, ' '); ++ o << pad << kind << "(" << tmpl << ") {\n" ++ << pad << " index_1 (\"0.009, 0.227\");\n" ++ << pad << " index_2 (\"0.005, 0.500\");\n" ++ << pad << " values ( \\\n" ++ << pad << " \"" << F(value_ns) << ", " << F(value_ns) << "\", \\\n" ++ << pad << " \"" << F(value_ns) << ", " << F(value_ns) << "\" \\\n" ++ << pad << " )\n" ++ << pad << "}\n"; ++} ++ ++void Constraint(std::ostream& o, ++ const std::string& cell, ++ const std::string& clk, ++ double setup_ns, ++ double hold_ns) ++{ ++ o << " timing() {\n" ++ << " related_pin : " << clk << ";\n" ++ << " timing_type : setup_rising ;\n"; ++ Table2(o, "rise_constraint", cell + "_constraint_template", setup_ns, 12); ++ Table2(o, "fall_constraint", cell + "_constraint_template", setup_ns, 12); ++ o << " }\n" ++ << " timing() {\n" ++ << " related_pin : " << clk << ";\n" ++ << " timing_type : hold_rising ;\n"; ++ Table2(o, "rise_constraint", cell + "_constraint_template", hold_ns, 12); ++ Table2(o, "fall_constraint", cell + "_constraint_template", hold_ns, 12); ++ o << " }\n"; ++} ++ ++} // namespace ++ ++void WriteLef(odb::dbBlock* block, utl::Logger* logger, const std::string& path) ++{ ++ std::ofstream out(path); ++ if (!out) { ++ throw std::runtime_error("cannot write " + path); ++ } ++ odb::lefout writer(logger, out); ++ writer.setBloatOccupiedLayers(true); ++ writer.writeAbstractLef(block); ++} ++ ++void WriteLiberty(odb::dbBlock* block, ++ const Spec& spec, ++ const LibModel& m, ++ const std::string& path, ++ bool pre_layout) ++{ ++ std::ofstream o(path); ++ if (!o) { ++ throw std::runtime_error("cannot write " + path); ++ } ++ const std::string cell = spec.module; ++ const int A = AddrWidth(spec.words); ++ const int R = static_cast(spec.read.size()); ++ const int W = static_cast(spec.write.size()); ++ const int banks = std::max(spec.banks, 1); ++ const int words_per_bank = spec.words / banks; ++ ++ // Gate levels, from the structure the generator built (regfile.cc). ++ const int read_levels ++ = 1 + Log2Ceil(A) + 1 + Log2Ceil(words_per_bank) + Log2Ceil(banks); ++ const int write_levels = 1 + Log2Ceil(A + 1) + 1 + Log2Ceil((W + 2) / 2); ++ const double read_ns = read_levels * m.gate_delay_ps * m.wire_factor / 1000.0; ++ const double setup_ns ++ = write_levels * m.gate_delay_ps * m.wire_factor / 1000.0; ++ const double hold_ns = m.hold_ps / 1000.0; ++ // Loads, from fanout: an address literal reaches one inverter and half ++ // the words' decodes; a data bit reaches every word's write mux; an ++ // enable reaches every word's decode. ++ const double addr_pf = (spec.words / 2 + 1) * m.input_load_ff / 1000.0; ++ const double data_pf = spec.words * m.input_load_ff / 1000.0; ++ const double en_pf = spec.words * m.input_load_ff / 1000.0; ++ const double clk_pf = m.clock_load_ff / 1000.0; ++ const odb::Rect die = block->getDieArea(); ++ const double dbu = block->getDbUnitsPerMicron(); ++ const double area_um2 = die.dx() / dbu * (die.dy() / dbu); ++ const double leakage_uw ++ = block->getInsts().size() * m.leakage_nw_per_cell / 1000.0; ++ ++ o << "library(" << cell << ") {\n" ++ << " technology (cmos);\n" ++ << " delay_model : table_lookup;\n" ++ << " revision : 1.0;\n" ++ << " comment : \"generate_regfile model" ++ << (pre_layout ? ", pre-layout" : "") ++ << ": loads from fanout, arcs from gate depth; replaced by the routed " ++ "block's abstract for a measured run\";\n" ++ << " time_unit : \"1ns\";\n" ++ << " voltage_unit : \"1V\";\n" ++ << " current_unit : \"1uA\";\n" ++ << " leakage_power_unit : \"1uW\";\n" ++ << " nom_process : 1;\n" ++ << " nom_temperature : 25.000;\n" ++ << " nom_voltage : 0.700;\n" ++ << " capacitive_load_unit (1,pf);\n" ++ << " pulling_resistance_unit : \"1kohm\";\n" ++ << " operating_conditions(tt_1.0_25.0) {\n" ++ << " process : 1;\n" ++ << " temperature : 25.000;\n" ++ << " voltage : 0.700;\n" ++ << " tree_type : balanced_tree;\n" ++ << " }\n" ++ << " default_cell_leakage_power : 0;\n" ++ << " default_fanout_load : 1;\n" ++ << " default_inout_pin_cap : 0.0;\n" ++ << " default_input_pin_cap : 0.0;\n" ++ << " default_output_pin_cap : 0.0;\n" ++ << " default_max_transition : 0.227;\n" ++ << " default_operating_conditions : tt_1.0_25.0;\n" ++ << " default_leakage_power_density : 0.0;\n" ++ << " slew_derate_from_library : 1.000;\n" ++ << " slew_lower_threshold_pct_fall : 20.000;\n" ++ << " slew_upper_threshold_pct_fall : 80.000;\n" ++ << " slew_lower_threshold_pct_rise : 20.000;\n" ++ << " slew_upper_threshold_pct_rise : 80.000;\n" ++ << " input_threshold_pct_fall : 50.000;\n" ++ << " input_threshold_pct_rise : 50.000;\n" ++ << " output_threshold_pct_fall : 50.000;\n" ++ << " output_threshold_pct_rise : 50.000;\n" ++ << " lu_table_template(" << cell << "_delay_template) {\n" ++ << " variable_1 : input_net_transition;\n" ++ << " variable_2 : total_output_net_capacitance;\n" ++ << " index_1 (\"1000, 1001\");\n" ++ << " index_2 (\"1000, 1001\");\n" ++ << " }\n" ++ << " lu_table_template(" << cell << "_slew_template) {\n" ++ << " variable_1 : total_output_net_capacitance;\n" ++ << " index_1 (\"1000, 1001\");\n" ++ << " }\n" ++ << " lu_table_template(" << cell << "_constraint_template) {\n" ++ << " variable_1 : related_pin_transition;\n" ++ << " variable_2 : constrained_pin_transition;\n" ++ << " index_1 (\"1000, 1001\");\n" ++ << " index_2 (\"1000, 1001\");\n" ++ << " }\n" ++ << " library_features(report_delay_calculation);\n"; ++ const int A_bank = AddrWidth(words_per_bank); ++ // One bus type per width in use. ++ std::set widths = {A, spec.bits, A_bank}; ++ for (int w : widths) { ++ o << " type (" << cell << "_bus_" << w << ") {\n" ++ << " base_type : array ;\n" ++ << " data_type : bit ;\n" ++ << " bit_width : " << w << ";\n" ++ << " bit_from : " << w - 1 << ";\n" ++ << " bit_to : 0 ;\n" ++ << " downto : true ;\n" ++ << " }\n"; ++ } ++ o << "cell(" << cell << ") {\n" ++ << " area : " << F(area_um2) << ";\n" ++ << " is_macro_cell : true;\n" ++ << " interface_timing : true;\n"; ++ // Clock. ++ o << " pin(" << spec.clock << ") {\n" ++ << " direction : input;\n" ++ << " capacitance : " << F(clk_pf) << ";\n" ++ << " clock : true;\n" ++ << " }\n"; ++ std::vector quiet = spec.unused; ++ if (!spec.reset.empty()) { ++ quiet.insert(quiet.begin(), spec.reset); ++ } ++ for (const auto& name : quiet) { ++ o << " pin(" << name << ") {\n" ++ << " direction : input;\n" ++ << " capacitance : 0.001000;\n" ++ << " }\n"; ++ } ++ // Read ports: address in, data out combinational from that address; with ++ // a read latency the address is a flop's D (setup and hold to the clock) ++ // and the data comes from the clock, one flop level more. A banked port ++ // is one such pair per bank, with the bank-local width. ++ const bool registered = spec.read_latency > 0; ++ const double flop_ns = m.gate_delay_ps * m.wire_factor / 1000.0; ++ auto read_pair = [&](const std::string& addr, ++ const std::string& data, ++ int a_width, ++ double read_ns_here) { ++ o << " bus(" << addr << ") {\n" ++ << " bus_type : " << cell << "_bus_" << a_width << ";\n" ++ << " direction : input;\n" ++ << " capacitance : " ++ << F(registered ? m.input_load_ff / 1000.0 : addr_pf) << ";\n"; ++ if (registered) { ++ Constraint(o, cell, spec.clock, flop_ns, hold_ns); ++ } ++ o << " }\n" ++ << " bus(" << data << ") {\n" ++ << " bus_type : " << cell << "_bus_" << spec.bits << ";\n" ++ << " direction : output;\n" ++ << " max_capacitance : " << F(m.output_max_cap_ff / 1000.0) ++ << ";\n"; ++ // The arc per data bit, related to every address bit: a bus-to-bus ++ // arc between an A-bit address and a B-bit data would be read as ++ // bitwise pairs of different sizes (STA-1216). Every data bit ++ // depends on every address bit here. ++ std::string addr_bits; ++ for (int i = 0; i < a_width; ++i) { ++ addr_bits += (i ? " " : "") + addr + "[" + std::to_string(i) + "]"; ++ } ++ for (int b = 0; b < spec.bits; ++b) { ++ o << " pin(" << data << "[" << b << "]) {\n" ++ << " direction : output;\n" ++ << " timing() {\n"; ++ if (registered) { ++ o << " related_pin : " << spec.clock << " ;\n" ++ << " timing_type : rising_edge;\n"; ++ } else { ++ o << " related_pin : \"" << addr_bits << "\" ;\n" ++ << " timing_type : combinational;\n" ++ << " timing_sense : non_unate;\n"; ++ } ++ const double arc_ns = read_ns_here + (registered ? flop_ns : 0.0); ++ Table2(o, "cell_rise", cell + "_delay_template", arc_ns, 16); ++ Table2(o, "cell_fall", cell + "_delay_template", arc_ns, 16); ++ o << " rise_transition(" << cell << "_slew_template) {\n" ++ << " index_1 (\"0.005, 0.500\");\n" ++ << " values (\"0.009, 0.227\")\n" ++ << " }\n" ++ << " fall_transition(" << cell << "_slew_template) {\n" ++ << " index_1 (\"0.005, 0.500\");\n" ++ << " values (\"0.009, 0.227\")\n" ++ << " }\n" ++ << " }\n" ++ << " }\n"; ++ } ++ o << " }\n"; ++ }; ++ // A banked port's read has no footer level. ++ const int bank_read_levels ++ = 1 + Log2Ceil(A_bank) + 1 + Log2Ceil(words_per_bank); ++ const double bank_read_ns ++ = bank_read_levels * m.gate_delay_ps * m.wire_factor / 1000.0; ++ for (int r = 0; r < R; ++r) { ++ if (spec.read[r].banked()) { ++ for (size_t k = 0; k < spec.read[r].bank_addr.size(); ++k) { ++ read_pair(spec.read[r].bank_addr[k], ++ spec.read[r].bank_data[k], ++ A_bank, ++ bank_read_ns); ++ } ++ } else { ++ read_pair(spec.read[r].addr, spec.read[r].data, A, read_ns); ++ } ++ } ++ // Write ports: everything constrained to the clock. ++ for (int w = 0; w < W; ++w) { ++ o << " bus(" << spec.write[w].addr << ") {\n" ++ << " bus_type : " << cell << "_bus_" << A << ";\n" ++ << " direction : input;\n" ++ << " capacitance : " << F(addr_pf) << ";\n"; ++ Constraint(o, cell, spec.clock, setup_ns, hold_ns); ++ o << " }\n" ++ << " bus(" << spec.write[w].data << ") {\n" ++ << " bus_type : " << cell << "_bus_" << spec.bits << ";\n" ++ << " direction : input;\n" ++ << " capacitance : " << F(data_pf) << ";\n"; ++ Constraint(o, cell, spec.clock, setup_ns, hold_ns); ++ o << " }\n"; ++ if (!spec.write[w].en.empty()) { ++ o << " pin(" << spec.write[w].en << ") {\n" ++ << " direction : input;\n" ++ << " capacitance : " << F(en_pf) << ";\n"; ++ Constraint(o, cell, spec.clock, setup_ns, hold_ns); ++ o << " }\n"; ++ } ++ } ++ o << " cell_leakage_power : " << F(leakage_uw) << ";\n" ++ << "}\n" ++ << "}\n"; ++} ++ ++std::vector ReadModulePorts(const std::string& verilog, ++ const std::string& module) ++{ ++ std::ifstream in(verilog); ++ if (!in) { ++ throw std::runtime_error("cannot read " + verilog); ++ } ++ std::string text((std::istreambuf_iterator(in)), ++ std::istreambuf_iterator()); ++ std::regex head("\\bmodule\\s+" + module + "\\s*(#\\s*)?\\("); ++ std::smatch mh; ++ if (!std::regex_search(text, mh, head)) { ++ throw std::runtime_error("module " + module + " is not in " + verilog); ++ } ++ // From the header on, without comments, which may hold parentheses: a ++ // linear scan, as std::regex recurses per character and a generated ++ // core's Verilog runs to megabytes. ++ text = StripComments(text.substr(mh.position(0))); ++ if (!std::regex_search(text, mh, head) || mh.position(0) != 0) { ++ throw std::runtime_error("module " + module + ": cannot read its header"); ++ } ++ size_t open = mh.length(0) - 1; ++ size_t close = 0; ++ // A SystemVerilog parameter list, `#(parameter int W = 32, ...)`: its ++ // defaults size the ports. ++ std::map params; ++ if (mh[1].matched) { ++ std::string plist = Balanced(text, open, &close); ++ std::regex param( ++ "^\\s*(?:parameter|localparam)?\\s*(?:[A-Za-z_][A-Za-z_0-9:]*\\s+)*" ++ "(?:\\[[^\\]]*\\]\\s*)?([A-Za-z_][A-Za-z_0-9]*)\\s*=\\s*([\\s\\S]+?)" ++ "\\s*$"); ++ for (const auto& p : TopLevelCommas(plist)) { ++ std::smatch m; ++ if (std::regex_match(p, m, param)) { ++ try { ++ params[m[1].str()] = ConstExpr(m[2].str(), params).Eval(); ++ } catch (const std::runtime_error&) { ++ // A type or string parameter: no port width can use it here, ++ // and one that does fails below, naming it. ++ } ++ } ++ } ++ open = text.find('(', close + 1); ++ if (open == std::string::npos) { ++ throw std::runtime_error("module " + module + ": header never closes"); ++ } ++ } ++ std::string header = Balanced(text, open, &close); ++ // ANSI ports, one entry per comma: `input [4:0] a, b` declares two, the ++ // second inheriting the first's direction and width, whether it is on ++ // the same line or the next. Packed dimensions multiply. An entry that ++ // is not a port declaration this can read is an error, never skipped. ++ std::vector ports; ++ std::regex decl( ++ "^\\s*(input|output|inout)?\\s*(?:(?:wire|reg|logic|var|signed)\\s+)*" ++ "((?:\\[[^\\]]*\\]\\s*)*)" ++ "([A-Za-z_][A-Za-z_0-9$]*)\\s*$"); ++ std::regex dim("\\[([^:\\]]+):([^\\]]+)\\]"); ++ bool input = true; ++ int width = 1; ++ for (const auto& entry : TopLevelCommas(header)) { ++ if (entry.find_first_not_of(" \t\r\n") == std::string::npos) { ++ continue; ++ } ++ std::smatch m; ++ if (!std::regex_match(entry, m, decl)) { ++ throw std::runtime_error("module " + module ++ + ": cannot read the port declaration `" + entry ++ + "`"); ++ } ++ if (m[1].matched) { ++ input = m[1].str() != "output"; ++ width = 1; ++ } ++ const std::string dims = m[2].str(); ++ if (!dims.empty() || m[1].matched) { ++ width = 1; ++ for (std::sregex_iterator it(dims.begin(), dims.end(), dim), done; ++ it != done; ++ ++it) { ++ const long hi = ConstExpr((*it)[1].str(), params).Eval(); ++ const long lo = ConstExpr((*it)[2].str(), params).Eval(); ++ width *= static_cast(std::labs(hi - lo) + 1); ++ } ++ } ++ ports.push_back(RtlPort{m[3].str(), width, input}); ++ } ++ return ports; ++} ++ ++std::vector CheckPorts(const Spec& spec, ++ const std::vector& ports) ++{ ++ const int A = AddrWidth(spec.words); ++ std::map want; ++ want[spec.clock] = RtlPort{spec.clock, 1, true}; ++ if (!spec.reset.empty()) { ++ want[spec.reset] = RtlPort{spec.reset, 1, true}; ++ } ++ for (const auto& u : spec.unused) { ++ want[u] = RtlPort{u, 1, true}; ++ } ++ const int banks = std::max(spec.banks, 1); ++ const int A_bank = AddrWidth(spec.words / banks); ++ for (const auto& r : spec.read) { ++ if (r.banked()) { ++ for (size_t k = 0; k < r.bank_addr.size(); ++k) { ++ want[r.bank_addr[k]] = RtlPort{r.bank_addr[k], A_bank, true}; ++ want[r.bank_data[k]] = RtlPort{r.bank_data[k], spec.bits, false}; ++ } ++ } else { ++ want[r.addr] = RtlPort{r.addr, A, true}; ++ want[r.data] = RtlPort{r.data, spec.bits, false}; ++ } ++ } ++ for (const auto& w : spec.write) { ++ want[w.addr] = RtlPort{w.addr, A, true}; ++ want[w.data] = RtlPort{w.data, spec.bits, true}; ++ if (!w.en.empty()) { ++ want[w.en] = RtlPort{w.en, 1, true}; ++ } ++ } ++ std::map have; ++ for (const auto& p : ports) { ++ have[p.name] = p; ++ } ++ std::vector problems; ++ for (const auto& [name, w] : want) { ++ auto it = have.find(name); ++ if (it == have.end()) { ++ problems.push_back("spec names port " + name + ", the module has none"); ++ continue; ++ } ++ if (it->second.width != w.width) { ++ problems.push_back( ++ "port " + name + " is " + std::to_string(it->second.width) ++ + " bits in the module, " + std::to_string(w.width) + " in the spec"); ++ } ++ if (it->second.input != w.input) { ++ problems.push_back("port " + name + " is " ++ + (it->second.input ? "an input" : "an output") ++ + " in the module and the other way in the spec"); ++ } ++ } ++ for (const auto& [name, p] : have) { ++ if (want.find(name) == want.end()) { ++ problems.push_back("module port " + name + " is not in the spec"); ++ } ++ } ++ return problems; ++} ++ ++} // namespace ram::regfile +diff --git a/src/ram/src/views.h b/src/ram/src/views.h +new file mode 100644 +index 00000000000..4b312577e3b +--- /dev/null ++++ b/src/ram/src/views.h +@@ -0,0 +1,67 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++// Copyright (c) 2026, The OpenROAD Authors ++ ++// generate_regfile: the views a parent's flow consumes before the block is ++// routed -- an abstract LEF from the placed block and a model liberty -- ++// and the check that a spec's ports are the RTL module's. ++// ++// The lib is a model, the way FakeRAM's is, so it lands in seconds inside ++// the parent's synthesis: pin loads from fanout, arcs from the known gate ++// depth of the read and write paths, a constant leakage per cell. It is ++// replaced by the routed block's own abstract for a measured run; the ++// numbers here are written so that swap changes as little as possible. ++#pragma once ++ ++#include ++#include ++ ++#include "regfile.h" ++ ++namespace odb { ++class dbBlock; ++} ++ ++namespace utl { ++class Logger; ++} ++ ++namespace ram::regfile { ++ ++// Abstract LEF of the placed block: outline, pins, obstruction on every ++// layer the cells occupy. Bloats the occupied layers into one obstruction, ++// as write_abstract_lef -bloat_occupied_layers does. ++void WriteLef(odb::dbBlock* block, ++ utl::Logger* logger, ++ const std::string& path); ++ ++// Model liberty for the generated block, see LibModel. `pre_layout` only ++// changes the comment: the model has no clock tree to be ideal about. ++void WriteLiberty(odb::dbBlock* block, ++ const Spec& spec, ++ const LibModel& model, ++ const std::string& path, ++ bool pre_layout); ++ ++// One RTL port as declared: name, width in bits (1 for a scalar), input? ++struct RtlPort ++{ ++ std::string name; ++ int width = 1; ++ bool input = true; ++}; ++ ++// The ports of `module` in a Verilog file, firtool's declaration style: ++// ANSI header, and consecutive same-direction ports packed on bare ++// continuation lines that inherit direction and width. Throws ++// std::runtime_error when the module is not in the file. ++std::vector ReadModulePorts(const std::string& verilog, ++ const std::string& module); ++ ++// Refuses, with every difference named, a spec whose ports are not the ++// module's: a port the spec names that the module lacks, a width that ++// differs, a direction that differs, or a module port the spec never ++// mentions. Returns the list of problems; empty means they agree. ++std::vector CheckPorts(const Spec& spec, ++ const std::vector& ports); ++ ++} // namespace ram::regfile +diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD +index 071fa2c0f0b..9a834644a3e 100644 +--- a/src/ram/test/BUILD ++++ b/src/ram/test/BUILD +@@ -13,10 +13,32 @@ COMPULSORY_TESTS = [ + ] + + PASSFAIL_TESTS = [ ++ "generate_regfile_asap7", ++ "generate_regfile_clock_gate_asap7", ++ "generate_regfile_modes_asap7", ++ "generate_regfile_reset_asap7", ++ "generate_regfile_reset_high_asap7", ++ "generate_regfile_sv_ports_asap7", ++ "generate_regfile_write_priority_asap7", + ] + + ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS + ++# What every generate_regfile test reads: the asap7 cells, their ++# liberty for constant propagation, the base spec's module. Every test ++# but generate_regfile_asap7 (whose own glob has it) adds the spec. ++REGFILE_TEST_DATA = [ ++ "asap7/asap7_tech_1x_201209.lef", ++ "asap7/asap7sc7p5t_28_R_1x_220121a.lef", ++ "asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz", ++ "asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz", ++ "asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz", ++ "asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib", ++ "asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz", ++ "generate_regfile_asap7_rtl.v", ++ "regfile_checks.tcl", ++] ++ + filegroup( + name = "regression_resources", + # Dependencies could be specified more narrowly per test case, +@@ -51,6 +73,28 @@ filegroup( + test_name + ".*", + ], + ) + { ++ "generate_regfile_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7_rtl_extra.v", ++ ], ++ "generate_regfile_clock_gate_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ ], ++ "generate_regfile_modes_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ ], ++ "generate_regfile_reset_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ ], ++ "generate_regfile_reset_high_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ ], ++ "generate_regfile_sv_ports_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ "generate_regfile_sv_ports_asap7_rtl.sv", ++ ], ++ "generate_regfile_write_priority_asap7": REGFILE_TEST_DATA + [ ++ "generate_regfile_asap7.regfile", ++ ], + "make_7x7_2r1w_nangate45": [ + "make_7x7_2r1w_nangate45_behavioral.vok", + "Nangate45/Nangate45.lef", +diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt +index 8545b3dbca9..a02f25bf641 100644 +--- a/src/ram/test/CMakeLists.txt ++++ b/src/ram/test/CMakeLists.txt +@@ -5,5 +5,12 @@ or_integration_tests( + # golden .defok matches the clang build used by bazel, not the gcc + # build typically used by cmake. See src/ram/test/BUILD. + make_7x7_nangate45 ++ PASSFAIL_TESTS ++ generate_regfile_asap7 ++ generate_regfile_clock_gate_asap7 ++ generate_regfile_modes_asap7 ++ generate_regfile_reset_asap7 ++ generate_regfile_reset_high_asap7 ++ generate_regfile_sv_ports_asap7 ++ generate_regfile_write_priority_asap7 + ) +- +diff --git a/src/ram/test/asap7 b/src/ram/test/asap7 +new file mode 120000 +index 00000000000..16dbc1fd7b7 +--- /dev/null ++++ b/src/ram/test/asap7 +@@ -0,0 +1 @@ ++../../../test/asap7 +\ No newline at end of file +diff --git a/src/ram/test/generate_regfile_asap7.regfile b/src/ram/test/generate_regfile_asap7.regfile +new file mode 100644 +index 00000000000..ad31829371c +--- /dev/null ++++ b/src/ram/test/generate_regfile_asap7.regfile +@@ -0,0 +1,22 @@ ++# An 8 x 4 register file, two read and two write ports, in two banks, ++# with word 0 reading zero, laid out for a parent that places its decode ++# (mode netlist). ++module rf8x4 ++mode netlist ++words 8 ++bits 4 ++banks 2 ++clock clock ++read r0_addr r0_data ++read r1_addr r1_data ++write w0_addr w0_data w0_en ++write w1_addr w1_data w1_en ++zero_word 0 ++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 2 +diff --git a/src/ram/test/generate_regfile_asap7.tcl b/src/ram/test/generate_regfile_asap7.tcl +new file mode 100644 +index 00000000000..2e26e14c4d8 +--- /dev/null ++++ b/src/ram/test/generate_regfile_asap7.tcl +@@ -0,0 +1,87 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_asap7 ++# generate_regfile builds the register file its spec describes: every ++# read port reads every word, word 0 reads zero, a write port loads only ++# the word it addresses and every other word holds, and the abstract and ++# the timing model name every port. A spec whose ports are not the RTL ++# module's is refused. Checked by constant propagation through the ++# generated cells, not against a golden netlist. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++ ++set failed [catch { ++ tee -quiet -variable log { ++ generate_regfile -spec generate_regfile_asap7.regfile \ ++ -check_ports generate_regfile_asap7_rtl_extra.v ++ } ++} message] ++check "a port the spec does not name is refused" { set failed } 1 ++check "the refusal is RAM-0050" { string match "*RAM-0050*" $message } 1 ++check "the refusal names the port" \ ++ { string match "*module port w2_en is not in the spec*" $log } 1 ++ ++set lef_file [make_result_file generate_regfile_asap7.lef] ++set lib_file [make_result_file generate_regfile_asap7.lib] ++tee -quiet -variable gen_log [list generate_regfile \ ++ -spec generate_regfile_asap7.regfile \ ++ -check_ports generate_regfile_asap7_rtl.v \ ++ -lef $lef_file -liberty $lib_file] ++ ++# mode netlist: the generator fixes the core and leaves the periphery ++# (address inverters, decode, hold) to the parent, which places, sizes ++# and buffers it with the drivers and loads outside the array. ++set placement [rf_placement] ++check "mode netlist fixes every storage flop" \ ++ { dict get $placement flops_not_fixed } 0 ++check "mode netlist leaves the periphery unplaced" \ ++ { expr { [dict get $placement unplaced] > 0 } } 1 ++check "and says so" { string match "*RAM-0052*mode netlist*" $gen_log } 1 ++ ++set words 8 ++set bits 4 ++set abits 3 ++for { set w 1 } { $w < $words } { incr w } { ++ rf_store $w $bits [rf_word_value $w $bits] ++} ++ ++foreach port { r0 r1 } { ++ for { set a 0 } { $a < $words } { incr a } { ++ rf_drive ${port}_addr $abits $a ++ set expected [expr { $a == 0 ? 0 : [rf_word_value $a $bits] }] ++ check "$port reads word $a" { rf_read ${port}_data $bits } $expected ++ } ++} ++ ++# With a hold term per bit (write_style mux, the default) a word's flops ++# load the write data when the word is written and their own value ++# otherwise. ++proc check_writes { port other } { ++ rf_drive_bit ${other}_en 0 ++ rf_drive_bit ${port}_en 1 ++ foreach a { 1 3 6 } { ++ set data [expr { (~[rf_word_value $a 4]) & 15 }] ++ rf_drive ${port}_addr 3 $a ++ rf_drive ${port}_data 4 $data ++ for { set w 1 } { $w < 8 } { incr w } { ++ set expected [expr { $w == $a ? $data : [rf_word_value $w 4] }] ++ check "$port writing word $a: word $w loads" { rf_next $w 4 } $expected ++ } ++ } ++ rf_drive_bit ${port}_en 0 ++ for { set w 1 } { $w < 8 } { incr w } { ++ check "no write: word $w holds" { rf_next $w 4 } [rf_word_value $w 4] ++ } ++} ++check_writes w0 w1 ++check_writes w1 w0 ++ ++check "the abstract names every port" { rf_ports_missing_from $lef_file lef } {} ++check "the timing model names every port" \ ++ { rf_ports_missing_from $lib_file lib } {} ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_asap7_rtl.v b/src/ram/test/generate_regfile_asap7_rtl.v +new file mode 100644 +index 00000000000..36a3e3a9993 +--- /dev/null ++++ b/src/ram/test/generate_regfile_asap7_rtl.v +@@ -0,0 +1,8 @@ ++// The module generate_regfile_asap7.regfile describes, its ports ++// declared several to a line, as hand-written RTL often does. ++module rf8x4 (input clock, ++ input [2:0] r0_addr, r1_addr, ++ output [3:0] r0_data, r1_data, ++ input [2:0] w0_addr, input [3:0] w0_data, input w0_en, ++ input [2:0] w1_addr, input [3:0] w1_data, input w1_en); ++endmodule +diff --git a/src/ram/test/generate_regfile_asap7_rtl_extra.v b/src/ram/test/generate_regfile_asap7_rtl_extra.v +new file mode 100644 +index 00000000000..688dd3ac99c +--- /dev/null ++++ b/src/ram/test/generate_regfile_asap7_rtl_extra.v +@@ -0,0 +1,9 @@ ++// generate_regfile_asap7_rtl.v with a port the spec does not name ++// (w2_en): the port check must refuse the spec. ++module rf8x4 (input clock, ++ input [2:0] r0_addr, r1_addr, ++ output [3:0] r0_data, r1_data, ++ input [2:0] w0_addr, input [3:0] w0_data, input w0_en, ++ input [2:0] w1_addr, input [3:0] w1_data, input w1_en, ++ input w2_en); ++endmodule +diff --git a/src/ram/test/generate_regfile_clock_gate_asap7.tcl b/src/ram/test/generate_regfile_clock_gate_asap7.tcl +new file mode 100644 +index 00000000000..3707bb41402 +--- /dev/null ++++ b/src/ram/test/generate_regfile_clock_gate_asap7.tcl +@@ -0,0 +1,50 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_clock_gate_asap7 ++# generate_regfile with a clock gate per word (write_style clock_gate): ++# every read port reads every word, and a write opens the clock gate of ++# the word it addresses, and only that one, with the write data on its ++# flops' D. Checked by constant propagation through the generated cells. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++set spec [rf_spec generate_regfile_asap7.regfile \ ++ generate_regfile_clock_gate_asap7.regfile {"write_style clock_gate"}] ++generate_regfile -spec $spec -check_ports generate_regfile_asap7_rtl.v ++ ++for { set w 1 } { $w < 8 } { incr w } { ++ rf_store $w 4 [rf_word_value $w 4] ++} ++foreach port { r0 r1 } { ++ for { set a 0 } { $a < 8 } { incr a } { ++ rf_drive ${port}_addr 3 $a ++ set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] ++ check "$port reads word $a" { rf_read ${port}_data 4 } $expected ++ } ++} ++ ++proc check_writes { port other } { ++ rf_drive_bit ${other}_en 0 ++ rf_drive_bit ${port}_en 1 ++ foreach a { 1 3 6 } { ++ set data [expr { (~[rf_word_value $a 4]) & 15 }] ++ rf_drive ${port}_addr 3 $a ++ rf_drive ${port}_data 4 $data ++ for { set w 1 } { $w < 8 } { incr w } { ++ check "$port writing word $a: word $w's clock gate" \ ++ { rf_value w${w}_icg/ENA } [expr { $w == $a ? 1 : 0 }] ++ } ++ check "$port writing word $a: its flops load the data" { rf_next $a 4 } $data ++ } ++ rf_drive_bit ${port}_en 0 ++ for { set w 1 } { $w < 8 } { incr w } { ++ check "no write: word $w's clock gate is shut" { rf_value w${w}_icg/ENA } 0 ++ } ++} ++check_writes w0 w1 ++check_writes w1 w0 ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_modes_asap7.tcl b/src/ram/test/generate_regfile_modes_asap7.tcl +new file mode 100644 +index 00000000000..6ac445d0755 +--- /dev/null ++++ b/src/ram/test/generate_regfile_modes_asap7.tcl +@@ -0,0 +1,28 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_modes_asap7 ++# mode macro: the register file is a macro to its parent, every instance ++# placed by the generator, none left to the parent, and nothing to say ++# about it. (mode netlist, which leaves the periphery to the parent, is ++# checked by generate_regfile_asap7.) ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++ ++set spec [rf_spec generate_regfile_asap7.regfile \ ++ generate_regfile_modes_asap7.regfile {"mode macro"}] ++tee -quiet -variable gen_log [list generate_regfile \ ++ -spec $spec -check_ports generate_regfile_asap7_rtl.v] ++ ++set placement [rf_placement] ++check "mode macro places every instance" \ ++ { dict get $placement unplaced } 0 ++check "mode macro fixes every storage flop" \ ++ { dict get $placement flops_not_fixed } 0 ++check "mode macro reports nothing left unplaced" \ ++ { string match "*RAM-0052*" $gen_log } 0 ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_reset_asap7.tcl b/src/ram/test/generate_regfile_reset_asap7.tcl +new file mode 100644 +index 00000000000..0830ae2680f +--- /dev/null ++++ b/src/ram/test/generate_regfile_reset_asap7.tcl +@@ -0,0 +1,55 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_reset_asap7 ++# generate_regfile with an asynchronous reset, active low (async_reset ++# rst_n low): every storage flop is the spec's reset flop, its reset ++# (RESETN, which clears the stored value) is asserted exactly while rst_n ++# is active, its set is tied off, and the file reads its words once reset ++# is released. Checked by constant propagation through the generated ++# cells. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++set spec [rf_spec generate_regfile_asap7.regfile generate_regfile_reset_asap7.regfile { ++ "async_reset rst_n low" ++ "cell flop_r DFFASRHQNx1_ASAP7_75t_R" ++ "cell tie_hi TIEHIx1_ASAP7_75t_R" ++}] ++generate_regfile -spec $spec ++ ++set flops {} ++for { set w 1 } { $w < 8 } { incr w } { ++ for { set b 0 } { $b < 4 } { incr b } { ++ lappend flops w${w}_b${b}_ff ++ } ++} ++set flop_r [[ord::get_db] findMaster DFFASRHQNx1_ASAP7_75t_R] ++foreach ff $flops { ++ check "$ff is the reset flop" \ ++ { [[ord::get_db_block] findInst $ff] getMaster } $flop_r ++} ++ ++proc check_reset { flops state value } { ++ foreach ff $flops { ++ check "rst_n $state: $ff RESETN" { rf_value $ff/RESETN } $value ++ check "rst_n $state: $ff SETN is tied off" { rf_value $ff/SETN } 1 ++ } ++} ++rf_drive_bit rst_n 0 ++check_reset $flops active 0 ++rf_drive_bit rst_n 1 ++check_reset $flops released 1 ++ ++for { set w 1 } { $w < 8 } { incr w } { ++ rf_store $w 4 [rf_word_value $w 4] ++} ++for { set a 0 } { $a < 8 } { incr a } { ++ rf_drive r0_addr 3 $a ++ set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] ++ check "released: r0 reads word $a" { rf_read r0_data 4 } $expected ++} ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_reset_high_asap7.tcl b/src/ram/test/generate_regfile_reset_high_asap7.tcl +new file mode 100644 +index 00000000000..da87418cd4a +--- /dev/null ++++ b/src/ram/test/generate_regfile_reset_high_asap7.tcl +@@ -0,0 +1,55 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_reset_high_asap7 ++# generate_regfile with an asynchronous reset, active high (async_reset ++# rst high): every storage flop is the spec's reset flop, its reset ++# (RESETN, which clears the stored value) is asserted exactly while rst ++# is active, its set is tied off, and the file reads its words once reset ++# is released. Checked by constant propagation through the generated ++# cells. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++set spec [rf_spec generate_regfile_asap7.regfile generate_regfile_reset_high_asap7.regfile { ++ "async_reset rst high" ++ "cell flop_r DFFASRHQNx1_ASAP7_75t_R" ++ "cell tie_hi TIEHIx1_ASAP7_75t_R" ++}] ++generate_regfile -spec $spec ++ ++set flops {} ++for { set w 1 } { $w < 8 } { incr w } { ++ for { set b 0 } { $b < 4 } { incr b } { ++ lappend flops w${w}_b${b}_ff ++ } ++} ++set flop_r [[ord::get_db] findMaster DFFASRHQNx1_ASAP7_75t_R] ++foreach ff $flops { ++ check "$ff is the reset flop" \ ++ { [[ord::get_db_block] findInst $ff] getMaster } $flop_r ++} ++ ++proc check_reset { flops state value } { ++ foreach ff $flops { ++ check "rst $state: $ff RESETN" { rf_value $ff/RESETN } $value ++ check "rst $state: $ff SETN is tied off" { rf_value $ff/SETN } 1 ++ } ++} ++rf_drive_bit rst 1 ++check_reset $flops active 0 ++rf_drive_bit rst 0 ++check_reset $flops released 1 ++ ++for { set w 1 } { $w < 8 } { incr w } { ++ rf_store $w 4 [rf_word_value $w 4] ++} ++for { set a 0 } { $a < 8 } { incr a } { ++ rf_drive r0_addr 3 $a ++ set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] ++ check "released: r0 reads word $a" { rf_read r0_data 4 } $expected ++} ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_sv_ports_asap7.tcl b/src/ram/test/generate_regfile_sv_ports_asap7.tcl +new file mode 100644 +index 00000000000..ef43a8c13b7 +--- /dev/null ++++ b/src/ram/test/generate_regfile_sv_ports_asap7.tcl +@@ -0,0 +1,64 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_sv_ports_asap7 ++# The port check reads a SystemVerilog module header: a #(...) parameter ++# list, port widths written in the parameters' defaults, and an input the ++# register file does not use (`unused`). It refuses a width that differs ++# once the parameters are evaluated, and a declaration it cannot read, ++# rather than skipping it. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++set spec [rf_spec generate_regfile_asap7.regfile \ ++ generate_regfile_sv_ports_asap7.regfile {"unused test_en_i"}] ++ ++# The same module with other text: a copy of the RTL in the results dir. ++proc rtl_variant { name from to } { ++ set in [open generate_regfile_sv_ports_asap7_rtl.sv r] ++ set text [read $in] ++ close $in ++ set file [make_result_file $name] ++ set out [open $file w] ++ puts -nonewline $out [string map [list $from $to] $text] ++ close $out ++ return $file ++} ++ ++# generate_regfile's error and its log for `rtl`; tee runs its script ++# unsubstituted, so the command is built first. ++proc refusal { rtl } { ++ global spec ++ set cmd [list generate_regfile -spec $spec -check_ports $rtl] ++ set failed [catch { tee -quiet -variable log $cmd } message] ++ return [list $failed $message $log] ++} ++ ++lassign [refusal [rtl_variant wide.sv "W = 4" "W = 8"]] failed message log ++check "a width that differs once W is evaluated is refused" { set failed } 1 ++check "the refusal is RAM-0050" { string match "*RAM-0050*" $message } 1 ++check "the refusal names the port and both widths" \ ++ { string match "*port r0_data is 8 bits in the module, 4 in the spec*" $log } 1 ++ ++lassign [refusal [rtl_variant typed.sv "logic \[A-1:0\] r1_addr" \ ++ "rf_pkg::addr_t r1_addr"]] failed message log ++check "a declaration the check cannot read is refused" { set failed } 1 ++check "the refusal quotes it" \ ++ { string match "*cannot read the port declaration*rf_pkg::addr_t r1_addr*" $log } 1 ++ ++generate_regfile -spec $spec -check_ports generate_regfile_sv_ports_asap7_rtl.sv ++check "the unused input is a port of the register file" \ ++ { expr { [[ord::get_db_block] findBTerm test_en_i] ne "NULL" } } 1 ++ ++for { set w 1 } { $w < 8 } { incr w } { ++ rf_store $w 4 [rf_word_value $w 4] ++} ++for { set a 0 } { $a < 8 } { incr a } { ++ rf_drive r1_addr 3 $a ++ set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] ++ check "r1 reads word $a" { rf_read r1_data 4 } $expected ++} ++ ++exit_summary +diff --git a/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv b/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv +new file mode 100644 +index 00000000000..bfda9bdd337 +--- /dev/null ++++ b/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv +@@ -0,0 +1,20 @@ ++// rf8x4 as a SystemVerilog module: a parameter list, port widths written ++// in its parameters, and an input the register file does not use. ++module rf8x4 #( ++ parameter int unsigned W = 4, ++ parameter int unsigned A = 3 ++) ( ++ input logic clock, ++ input logic test_en_i, ++ input logic [A-1:0] r0_addr, ++ output logic [W-1:0] r0_data, ++ input logic [A-1:0] r1_addr, ++ output logic [W-1:0] r1_data, ++ input logic [A-1:0] w0_addr, ++ input logic [W-1:0] w0_data, ++ input logic w0_en, ++ input logic [A-1:0] w1_addr, ++ input logic [W-1:0] w1_data, ++ input logic w1_en ++); ++endmodule +diff --git a/src/ram/test/generate_regfile_write_priority_asap7.tcl b/src/ram/test/generate_regfile_write_priority_asap7.tcl +new file mode 100644 +index 00000000000..39374756281 +--- /dev/null ++++ b/src/ram/test/generate_regfile_write_priority_asap7.tcl +@@ -0,0 +1,36 @@ ++# SPDX-License-Identifier: BSD-3-Clause ++# Copyright (c) 2026, The OpenROAD Authors ++# ++# Test: ram/generate_regfile_write_priority_asap7 ++# generate_regfile with `write_priority last`: when both write ports ++# write one word in one cycle, the word loads the later port's data; ++# writes to different words each load their own. Checked by constant ++# propagation through the generated cells. ++ ++source "helpers.tcl" ++source "regfile_checks.tcl" ++ ++rf_read_libraries ++set spec [rf_spec generate_regfile_asap7.regfile \ ++ generate_regfile_write_priority_asap7.regfile {"write_priority last"}] ++generate_regfile -spec $spec -check_ports generate_regfile_asap7_rtl.v ++ ++for { set w 1 } { $w < 8 } { incr w } { ++ rf_store $w 4 [rf_word_value $w 4] ++} ++rf_drive_bit w0_en 1 ++rf_drive_bit w1_en 1 ++foreach a { 1 4 7 } { ++ rf_drive w0_addr 3 $a ++ rf_drive w1_addr 3 $a ++ rf_drive w0_data 4 5 ++ rf_drive w1_data 4 10 ++ check "both ports write word $a: it loads w1's data" { rf_next $a 4 } 10 ++} ++rf_drive w0_addr 3 2 ++rf_drive w1_addr 3 6 ++check "w0 writes word 2: it loads w0's data" { rf_next 2 4 } 5 ++check "w1 writes word 6: it loads w1's data" { rf_next 6 4 } 10 ++check "word 3 holds" { rf_next 3 4 } [rf_word_value 3 4] ++ ++exit_summary +diff --git a/src/ram/test/regfile_checks.tcl b/src/ram/test/regfile_checks.tcl +new file mode 100644 +index 00000000000..56b5ae3ce8a +--- /dev/null ++++ b/src/ram/test/regfile_checks.tcl +@@ -0,0 +1,136 @@ ++# Checks of a generated register file by what it computes, not by how it ++# is laid out: case analysis on its inputs and on its storage, and ++# OpenSTA's constant propagation through its cells. Sourced by the ++# generate_regfile tests after helpers.tcl. ++ ++proc rf_read_libraries { } { ++ read_lef asap7/asap7_tech_1x_201209.lef ++ read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef ++ foreach kind { AO OA SIMPLE } { ++ read_liberty asap7/asap7sc7p5t_${kind}_RVT_FF_nldm_211120.lib.gz ++ } ++ read_liberty asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz ++ read_liberty asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib ++} ++ ++# A spec file: the base spec with `lines` appended, in the results dir. ++proc rf_spec { base name lines } { ++ set spec [make_result_file $name] ++ set out [open $spec w] ++ set in [open $base r] ++ puts -nonewline $out [read $in] ++ close $in ++ foreach line $lines { ++ puts $out $line ++ } ++ close $out ++ return $spec ++} ++ ++# The propagated value of a port (by name) or an instance pin. ++proc rf_value { name } { ++ set port [get_ports -quiet $name] ++ if { $port ne "" } { ++ return [sta::pin_sim_logic_value [sta::get_port_pin $port]] ++ } ++ return [sta::pin_sim_logic_value [get_pins $name]] ++} ++ ++proc rf_drive_bit { name value } { ++ set_case_analysis $value [get_ports $name] ++} ++ ++proc rf_drive { bus width value } { ++ for { set i 0 } { $i < $width } { incr i } { ++ rf_drive_bit "$bus\[$i\]" [expr { ($value >> $i) & 1 }] ++ } ++} ++ ++# The integer an output bus carries, or X if any bit is not a constant. ++proc rf_read { bus width } { ++ set v 0 ++ for { set i 0 } { $i < $width } { incr i } { ++ set b [rf_value "$bus\[$i\]"] ++ if { $b ne "0" && $b ne "1" } { ++ return X ++ } ++ set v [expr { $v | ($b << $i) }] ++ } ++ return $v ++} ++ ++# Word `word` holding `value`: the storage flop of each bit holds the ++# inverse on QN, which is where the value is set. ++proc rf_store { word bits value } { ++ for { set b 0 } { $b < $bits } { incr b } { ++ set_case_analysis [expr { 1 - (($value >> $b) & 1) }] \ ++ [get_pins w${word}_b${b}_ff/QN] ++ } ++} ++ ++# The integer word `word`'s flops would load: their D inputs. ++proc rf_next { word bits } { ++ set v 0 ++ for { set b 0 } { $b < $bits } { incr b } { ++ set d [rf_value w${word}_b${b}_ff/D] ++ if { $d ne "0" && $d ne "1" } { ++ return X ++ } ++ set v [expr { $v | ($d << $b) }] ++ } ++ return $v ++} ++ ++# A value per word, distinct across words, for the storage. ++proc rf_word_value { word bits } { ++ return [expr { ($word * 5 + 3) & ((1 << $bits) - 1) }] ++} ++ ++# The ports of the generated block missing from a view: the abstract ++# (`PIN `) or the timing model (`pin()`, or a `bus()` ++# for a bus bit). ++proc rf_ports_missing_from { file kind } { ++ set in [open $file r] ++ set text [read $in] ++ close $in ++ set missing {} ++ foreach bterm [[ord::get_db_block] getBTerms] { ++ set name [$bterm getName] ++ set base [lindex [split $name {[}] 0] ++ if { $kind eq "lef" } { ++ set found [expr { [string first "PIN $name\n" $text] >= 0 }] ++ } else { ++ set found [expr { ++ [string first "pin($name)" $text] >= 0 ++ || [string first "bus($base)" $text] >= 0 ++ }] ++ } ++ if { !$found } { ++ lappend missing $name ++ } ++ } ++ return $missing ++} ++ ++# The block's instances by placement: fixed, placed (movable) and ++# unplaced, and how many of the storage flops (DFF*) are not fixed. ++proc rf_placement { } { ++ set n [dict create fixed 0 placed 0 unplaced 0 flops_not_fixed 0] ++ foreach inst [[ord::get_db_block] getInsts] { ++ set status [$inst getPlacementStatus] ++ if { $status == "FIRM" || $status == "LOCKED" || $status == "COVER" } { ++ dict incr n fixed ++ } elseif { $status == "PLACED" || $status == "SUGGESTED" } { ++ dict incr n placed ++ } else { ++ dict incr n unplaced ++ } ++ if { ++ [string match "DFF*" [[$inst getMaster] getName]] ++ && $status != "FIRM" ++ } { ++ dict incr n flops_not_fixed ++ } ++ } ++ return $n ++} diff --git a/patches/0071-orfs-structured-memories.patch b/patches/0071-orfs-structured-memories.patch deleted file mode 100644 index 180240763..000000000 --- a/patches/0071-orfs-structured-memories.patch +++ /dev/null @@ -1,230 +0,0 @@ -AUTO_MEMORIES: STRUCTURED_MEMORIES, register files replaced by a generated macro, by name - -bazel-orfs carries this against ORFS; it is not upstreamed. - -A many-port register file synthesised to flops is what a legaliser -cannot spread, and a core in XiangShan's class has these as compiled -bitcells. bazel-orfs's tools/structured_gen builds one as a placed -standard-cell macro from a small spec. This is the hook that lets a -design say which modules get that treatment. - -STRUCTURED_MEMORIES lists spec files, one per module. Nothing is -detected: XiangShan's register files are Reg(Vec) and never become a -yosys memory, and a Mem rewrite would change which writer wins. -gen_memories.py runs the generator (STRUCTURED_GEN, set by the build -system) per spec with --check-ports against the Verilog file declaring -the module, writes .lef/.lib/_pre_layout.lib beside the -AUTO_MEMORIES views, appends the module to blackboxes.txt after FakeRAM -has written it, and records what it did in structured.json. A spec -whose ports are not the module's stops the build there. - -ORFS's detection is yosys-only and gen_memories.py no longer takes -VERILOG_FILES; this patch passes them back as --verilog, searched only -for the module a spec names. - -Retires when ORFS grows a way to declare a generated macro per module; -then `//:bump` onto that commit drops this patch. - -diff --git a/flow/Makefile b/flow/Makefile ---- a/flow/Makefile -+++ b/flow/Makefile -@@ -277,10 +277,14 @@ - --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,$(STRUCTURED_MEMORIES),--structured-spec $(file)) \ -+ $(if $(STRUCTURED_GEN),--structured-gen $(STRUCTURED_GEN)) \ -+ $(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file)) - - $(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \ -- $(wildcard $(SCRIPTS_DIR)/memories/*.py) -+ $(STRUCTURED_MEMORIES) $(wildcard $(SCRIPTS_DIR)/memories/*.py) - $(UNSET_AND_MAKE) do-auto-memories - - yosys-dependencies: $(RESULTS_DIR)/memories.json -diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py ---- a/flow/scripts/memories/gen_memories.py -+++ b/flow/scripts/memories/gen_memories.py -@@ -20,6 +20,8 @@ - from __future__ import annotations - - import argparse -+import json -+import re - import sys - from pathlib import Path - -@@ -34,12 +36,85 @@ - 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 _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: STRUCTURED_MEMORIES names module {module}, " -+ f"which none of the Verilog files declare" -+ ) -+ -+ -+def run_structured( -+ specs: list[Path], -+ generator: str | None, -+ lefs: list[Path], -+ verilog: list[Path], -+ out_dir: Path, -+) -> list[str]: -+ """Generate a placed macro for every listed register file. -+ -+ Explicit by design: a module is replaced only because a spec names -+ it, and a spec that does not match the module's ports stops the -+ build here rather than miswire the parent. Views land beside the -+ AUTO_MEMORIES ones and the module joins blackboxes.txt, so synthesis -+ and every later stage treat it as one more memory macro. -+ """ -+ if not specs: -+ return [] -+ if not generator: -+ raise SystemExit( -+ "gen_memories: STRUCTURED_MEMORIES is set but STRUCTURED_GEN is not; " -+ "the build system that carries the generator has to name it" -+ ) -+ if not lefs: -+ raise SystemExit("gen_memories: STRUCTURED_MEMORIES needs TECH_LEF and SC_LEF") -+ modules = [] -+ report = [] -+ for spec in specs: -+ module = _spec_module(spec) -+ rtl = _file_defining(module, verilog) -+ cmd = [generator, "--spec", str(spec), "--check-ports", str(rtl)] -+ for lef in lefs: -+ cmd += ["--lef", str(lef)] -+ cmd += [ -+ "--lef-out", str(out_dir / f"{module}.lef"), -+ "--lib-out", str(out_dir / f"{module}.lib"), -+ "--def", str(out_dir / f"{module}.def"), -+ "--verilog", str(out_dir / f"{module}.v"), -+ ] -+ subprocess.check_call(cmd) -+ modules.append(module) -+ report.append({"module": module, "spec": str(spec)}) -+ sys.stderr.write(f"gen_memories: {module} -> structured macro ({spec.name})\n") -+ with (out_dir / "blackboxes.txt").open("a") as f: -+ f.write("".join(f"{m}\n" for m in modules)) -+ (out_dir / "structured.json").write_text(json.dumps(report, indent=2) + "\n") -+ return modules -+ -+ - def run( - yosys_json: Path, - memories_files: list[Path], - platform: str, - out_dir: Path, - json_path: Path, -+ verilog: list[Path] | None = None, -+ structured_specs: list[Path] | None = None, -+ structured_gen: str | None = None, -+ lefs: list[Path] | None = None, - ) -> int: - if platform != "asap7": - sys.stderr.write(f"gen_memories: unsupported platform {platform}\n") -@@ -73,6 +148,11 @@ - ] - ) - -+ # After FakeRAM has written blackboxes.txt for the inferred memories. -+ run_structured( -+ structured_specs or [], structured_gen, lefs or [], verilog or [], out_dir -+ ) -+ - for mem in sorted(memories, key=lambda m: m.name): - verdict = "macro" if mem.idiomatic else "flops" - sys.stderr.write( -@@ -101,6 +181,32 @@ - 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 STRUCTURED_MEMORIES module (repeatable).", -+ ) -+ p.add_argument( -+ "--structured-spec", -+ action="append", -+ default=[], -+ type=Path, -+ help="Register-file spec for STRUCTURED_MEMORIES (repeatable).", -+ ) -+ p.add_argument( -+ "--structured-gen", -+ default=None, -+ help="The generator STRUCTURED_MEMORIES runs (STRUCTURED_GEN).", -+ ) -+ 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", -@@ -121,6 +227,10 @@ - args.platform, - args.out_dir, - args.json, -+ args.verilog, -+ args.structured_spec, -+ args.structured_gen, -+ args.lef, - ) - - -diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml ---- a/flow/scripts/variables.yaml -+++ b/flow/scripts/variables.yaml -@@ -211,6 +211,25 @@ - docs/user/AutoMemories.md. - stages: - - synth -+STRUCTURED_MEMORIES: -+ description: > -+ Experimental. Space-separated list of register-file spec files, one -+ per RTL module to replace with a generated, placed standard-cell -+ macro 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; the generator checks the spec against the module's ports, -+ writes `.lef`, `.lib` and `_pre_layout.lib` -+ under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like -+ an AUTO_MEMORIES macro. Explicit by design: nothing is detected. -+ Requires AUTO_MEMORIES=1 and STRUCTURED_GEN. -+ stages: -+ - synth -+STRUCTURED_GEN: -+ description: > -+ Path of the generator STRUCTURED_MEMORIES runs. Set by the build -+ system that carries the generator, not by a design. -+ stages: -+ - synth - SYNTH_MEMORY_MAX_BITS: - description: > - Maximum number of bits for memory synthesis. diff --git a/patches/0078-orfs-structured-netlists.patch b/patches/0078-orfs-structured-netlists.patch deleted file mode 100644 index d060898f6..000000000 --- a/patches/0078-orfs-structured-netlists.patch +++ /dev/null @@ -1,316 +0,0 @@ -From e2163fb30ec89850c0ae0d1de954eda2aadc1852 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?=C3=98yvind=20Harboe?= -Date: Mon, 21 Sep 2026 09:36:03 +0200 -Subject: [PATCH] memories: STRUCTURED_MEMORIES in mode netlist, placed by - construction into the parent -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -A generated register file as a macro takes its cells out of the parent's -placer, legaliser, clock tree and router at once, and costs the parent -a boundary: no cross-module optimisation, a wall of pins to route -around, its own clock tree. That is right for a terminal block at an -edge and wrong for an array in the middle of the logic. mode netlist in -the spec keeps the module blackboxed in synthesis (no abc on it), links -the generator's structural netlist when the design is loaded, and at -floorplan drops its placed cells FIRM onto the parent's rows where -STRUCTURED_PLACEMENT says, snapped to the site and row grid with the row -parity matched. Global placement, the legaliser and tapcell treat them -as fixed; the clock tree and the router see ordinary cells. - -Carried by bazel-orfs; not upstreamed. - -Signed-off-by: Øyvind Harboe ---- - flow/scripts/floorplan.tcl | 4 ++ - flow/scripts/load.tcl | 15 +++++ - flow/scripts/memories/gen_memories.py | 74 +++++++++++++++++++-- - flow/scripts/structured_netlists.tcl | 96 +++++++++++++++++++++++++++ - flow/scripts/variables.yaml | 17 ++++- - 5 files changed, 201 insertions(+), 5 deletions(-) - create mode 100644 flow/scripts/structured_netlists.tcl - -diff --git a/flow/scripts/floorplan.tcl b/flow/scripts/floorplan.tcl ---- a/flow/scripts/floorplan.tcl -+++ b/flow/scripts/floorplan.tcl -@@ -99,6 +99,10 @@ - log_cmd make_tracks - } - -+# STRUCTURED_MEMORIES in `mode netlist`: their placed cells go onto the -+# rows now, FIRM, before tapcells and the power grid, where the plan says. -+source $::env(SCRIPTS_DIR)/structured_netlists.tcl -+ - # Configure global routing: FASTROUTE_TCL script or - # set_global_routing_layer_adjustment/set_routing_layers - if { [env_var_exists_and_non_empty FASTROUTE_TCL] } { -diff --git a/flow/scripts/load.tcl b/flow/scripts/load.tcl ---- a/flow/scripts/load.tcl -+++ b/flow/scripts/load.tcl -@@ -30,6 +30,21 @@ - read_lef $lef - } - } -+ # STRUCTURED_MEMORIES in `mode netlist`: the generator's structural -+ # netlist defines the blackboxed module, cell for cell, so the link -+ # gives the parent the array's standard cells instead of a macro. -+ if { [env_var_equals AUTO_MEMORIES 1] } { -+ set netlists "$::env(RESULTS_DIR)/memories/netlists.txt" -+ if { [file exists $netlists] } { -+ set f [open $netlists r] -+ foreach m [split [string trim [read $f]] "\n"] { -+ if { $m ne "" } { -+ log_cmd read_verilog $::env(RESULTS_DIR)/memories/$m.v -+ } -+ } -+ close $f -+ } -+ } - read_verilog $::env(RESULTS_DIR)/$design_file - log_cmd link_design {*}[hier_options] $::env(DESIGN_NAME) - } elseif { $ext == ".odb" } { -diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py ---- 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 -+ /netlists.txt STRUCTURED_MEMORIES modules in `mode netlist`: -+ linked as placed netlists, not macros - /blackboxes.txt converted module names, one per line — - what synthesis blackboxes - -@@ -45,6 +47,53 @@ - raise SystemExit(f"gen_memories: {spec}: no `module` line") - - -+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) -@@ -82,25 +131,42 @@ - if not lefs: - raise SystemExit("gen_memories: STRUCTURED_MEMORIES needs TECH_LEF and SC_LEF") - modules = [] -+ netlists = [] - report = [] - for spec in specs: - module = _spec_module(spec) -+ mode = _spec_mode(spec) - rtl = _file_defining(module, verilog) - cmd = [generator, "--spec", str(spec), "--check-ports", str(rtl)] - for lef in lefs: - cmd += ["--lef", str(lef)] -+ if mode == "macro": -+ cmd += [ -+ "--lef-out", str(out_dir / f"{module}.lef"), -+ "--lib-out", str(out_dir / f"{module}.lib"), -+ ] - cmd += [ -- "--lef-out", str(out_dir / f"{module}.lef"), -- "--lib-out", str(out_dir / f"{module}.lib"), - "--def", str(out_dir / f"{module}.def"), - "--verilog", str(out_dir / f"{module}.v"), - ] - subprocess.check_call(cmd) - modules.append(module) -- report.append({"module": module, "spec": str(spec)}) -- sys.stderr.write(f"gen_memories: {module} -> structured macro ({spec.name})\n") -+ entry = {"module": module, "spec": str(spec), "mode": mode} -+ if mode == "netlist": -+ # Placed netlist: no abstract, no model liberty. The module is -+ # still blackboxed in synthesis; its structural netlist is -+ # linked when the design is loaded and its cells are dropped -+ # FIRM onto the parent's rows at floorplan -+ # (structured_netlists.tcl, STRUCTURED_PLACEMENT). -+ info = _def_placement(out_dir / f"{module}.def", out_dir / f"{module}.place") -+ entry.update(info) -+ (out_dir / f"{module}.netlist.json").write_text(json.dumps(entry, indent=2) + "\n") -+ netlists.append(module) -+ report.append(entry) -+ sys.stderr.write(f"gen_memories: {module} -> structured {mode} ({spec.name})\n") - with (out_dir / "blackboxes.txt").open("a") as f: - f.write("".join(f"{m}\n" for m in modules)) -+ (out_dir / "netlists.txt").write_text("".join(f"{m}\n" for m in netlists)) - (out_dir / "structured.json").write_text(json.dumps(report, indent=2) + "\n") - return modules - -diff --git a/flow/scripts/structured_netlists.tcl b/flow/scripts/structured_netlists.tcl -new file mode 100644 ---- /dev/null -+++ b/flow/scripts/structured_netlists.tcl -@@ -0,0 +1,96 @@ -+# Placed netlists: a STRUCTURED_MEMORIES module generated in `mode netlist` -+# is linked into the design as standard cells (load.tcl) and its cells, -+# placed by construction in the generator's own frame, are dropped here -+# onto the parent's rows, FIRM, at the location STRUCTURED_PLACEMENT gives -+# for each instance. Global placement, the legaliser and tapcell insertion -+# treat them as fixed; the clock tree and the router see ordinary cells. -+# -+# STRUCTURED_PLACEMENT is a file of lines -+# -+# -+# -+# one per instance of a netlist-mode module. The location is the array's -+# lower-left corner, snapped to the parent's site and row grid, moved up -+# one row when the array's first row and the parent's row there differ in -+# orientation. An instance without a line, or a cell the netlist names that -+# the design does not have, is an error: nothing is placed by guess. -+# AUTO_MEMORIES is a synthesis-stage variable and is not in the floorplan's -+# environment; the memories directory travels with the results, so the -+# netlists list is the guard. -+if { 1 } { -+ set netlists_file "$::env(RESULTS_DIR)/memories/netlists.txt" -+ if { [file exists $netlists_file] } { -+ set f [open $netlists_file r] -+ set netlist_modules [lsearch -all -inline -not -exact [split [string trim [read $f]] "\n"] ""] -+ close $f -+ } else { -+ set netlist_modules {} -+ } -+ if { [llength $netlist_modules] > 0 } { -+ if { ![env_var_exists_and_non_empty STRUCTURED_PLACEMENT] } { -+ utl::error FLW 2 "STRUCTURED_MEMORIES in mode netlist ([join $netlist_modules {, }]) need STRUCTURED_PLACEMENT, a file of lines" -+ } -+ set block [ord::get_db_block] -+ set dbu [[ord::get_db_tech] getDbUnitsPerMicron] -+ set core [$block getCoreArea] -+ # 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 f [open $::env(STRUCTURED_PLACEMENT) r] -+ set lines [split [string trim [read $f]] "\n"] -+ close $f -+ set placed_modules {} -+ foreach line $lines { -+ set line [string trim [lindex [split $line "#"] 0]] -+ if { $line eq "" } { continue } -+ lassign $line module path x_um y_um -+ if { [lsearch -exact $netlist_modules $module] < 0 } { -+ utl::error FLW 3 "STRUCTURED_PLACEMENT names $module, which is not a netlist-mode STRUCTURED_MEMORIES module ($netlist_modules)" -+ } -+ set info_file "$::env(RESULTS_DIR)/memories/$module.netlist.json" -+ set jf [open $info_file r] -+ set info [read $jf] -+ close $jf -+ regexp {"row0_orient":\s*"(\w+)"} $info -> row0 -+ regexp {"die":\s*\[\s*(-?\d+),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\s*\]} $info -> dx0 dy0 dx1 dy1 -+ # snap the corner to the site and row grid of the core -+ set X [expr { [$core xMin] + int(round(($x_um * $dbu - [$core xMin]) / double($site_w))) * $site_w }] -+ set Y [expr { [$core yMin] + int(round(($y_um * $dbu - [$core yMin]) / double($row_h))) * $row_h }] -+ if { [dict exists $row_orient $Y] && [dict get $row_orient $Y] ne $row0 } { -+ set Y [expr { $Y + $row_h }] -+ } -+ if { $X < [$core xMin] || $Y < [$core yMin] || $X + ($dx1 - $dx0) > [$core xMax] || $Y + ($dy1 - $dy0) > [$core yMax] } { -+ utl::error FLW 4 "STRUCTURED_PLACEMENT: $path ($module, [expr {($dx1-$dx0)/double($dbu)}] x [expr {($dy1-$dy0)/double($dbu)}] um) at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um leaves the core" -+ } -+ set pf [open "$::env(RESULTS_DIR)/memories/$module.place" r] -+ set n 0 -+ while { [gets $pf pl] >= 0 } { -+ lassign $pl name cx cy orient -+ if { $name eq "" } { continue } -+ set inst [$block findInst "$path/$name"] -+ if { $inst == "NULL" } { -+ utl::error FLW 5 "STRUCTURED_PLACEMENT: $path/$name of netlist $module is not in the design (is $path an instance of $module, and the netlist linked?)" -+ } -+ $inst setOrient $orient -+ $inst setLocation [expr { $X + $cx - $dx0 }] [expr { $Y + $cy - $dy0 }] -+ $inst setPlacementStatus FIRM -+ incr n -+ } -+ close $pf -+ lappend placed_modules $module -+ utl::info FLW 6 "structured netlist $module at $path: $n cells FIRM at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um" -+ } -+ foreach m $netlist_modules { -+ if { [lsearch -exact $placed_modules $m] < 0 } { -+ utl::error FLW 7 "netlist-mode module $m has no line in STRUCTURED_PLACEMENT" -+ } -+ } -+ } -+} -diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml ---- a/flow/scripts/variables.yaml -+++ b/flow/scripts/variables.yaml -@@ -244,10 +244,25 @@ - the cells; the generator checks the spec against the module's ports, - writes `.lef`, `.lib` and `_pre_layout.lib` - under `$(RESULTS_DIR)/memories/`, and the module is blackboxed like -- an AUTO_MEMORIES macro. Explicit by design: nothing is detected. -+ an AUTO_MEMORIES macro. A spec with `mode netlist` writes no abstract: -+ the module is blackboxed in synthesis, its structural netlist is -+ linked when the design is loaded, and its placed cells are dropped -+ FIRM onto the parent's rows at floorplan where STRUCTURED_PLACEMENT -+ says, so the parent keeps one placement, clock tree and route across -+ it. Explicit by design: nothing is detected. - Requires AUTO_MEMORIES=1 and STRUCTURED_GEN. - stages: - - synth -+STRUCTURED_PLACEMENT: -+ description: > -+ Experimental. With STRUCTURED_MEMORIES specs in `mode netlist`, the -+ file of ` ` lines that says where -+ each instance of a generated array is dropped onto the parent's rows, -+ FIRM, at floorplan (lower-left corner; snapped to the site and row -+ grid, moved up one row when the array's first row and the parent's -+ differ in orientation). Every netlist-mode instance needs a line. -+ stages: -+ - floorplan - STRUCTURED_GEN: - description: > - Path of the generator STRUCTURED_MEMORIES runs. Set by the build diff --git a/patches/0082-orfs-structured-netlists-dead-cells.patch b/patches/0082-orfs-structured-netlists-dead-cells.patch deleted file mode 100644 index 72f646b9d..000000000 --- a/patches/0082-orfs-structured-netlists-dead-cells.patch +++ /dev/null @@ -1,73 +0,0 @@ -From 7fe7de61a2fbaf00747e9184f149cd0d7d0c50d2 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?=C3=98yvind=20Harboe?= -Date: Mon, 21 Sep 2026 17:15:05 +0200 -Subject: [PATCH] memories: a placed netlist's cells that dead-logic - elimination removed are skipped -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -synth_odb.tcl runs eliminate_dead_logic on the linked design. A bit of a -generated array that the parent never reads or writes -- XiangShan's -RobEntryFile has one such bit of its 21, RenameBufferFile two of 20 -- -loses its whole column: 15 120 flops, write muxes and read gates per bit, -present in the generator's Verilog and .place, gone from the design. -structured_netlists.tcl then stopped on the first such cell (FLW-0005) -and the parent's floorplan failed. - -The cells that are gone are skipped and counted (FLW-0008, with the -first name), the rest are placed as before, and FLW-0005 is kept for the -case it was written for: none of the netlist's cells in the design, which -is what an unlinked netlist looks like. - -Not upstreamed (builds on carried patch 0078). Retires with it. - -Co-Authored-By: Claude Fable 5.1 -Signed-off-by: Øyvind Harboe ---- - flow/scripts/structured_netlists.tcl | 19 ++++++++++++++++++- - 1 file changed, 18 insertions(+), 1 deletion(-) - -diff --git a/flow/scripts/structured_netlists.tcl b/flow/scripts/structured_netlists.tcl ---- a/flow/scripts/structured_netlists.tcl -+++ b/flow/scripts/structured_netlists.tcl -@@ -69,14 +69,25 @@ - if { $X < [$core xMin] || $Y < [$core yMin] || $X + ($dx1 - $dx0) > [$core xMax] || $Y + ($dy1 - $dy0) > [$core yMax] } { - utl::error FLW 4 "STRUCTURED_PLACEMENT: $path ($module, [expr {($dx1-$dx0)/double($dbu)}] x [expr {($dy1-$dy0)/double($dbu)}] um) at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um leaves the core" - } -+ # A cell of the netlist may legitimately be gone: the synth ODB step -+ # runs eliminate_dead_logic on the linked design, and a bit of the -+ # array the parent never reads or writes (a dead field of the entry) -+ # takes its whole column of flops, muxes and read gates with it. -+ # Those cells are skipped, and reported; the placement of the rest -+ # is unchanged. Only a netlist with none of its cells in the design -+ # is an error, which is what "not linked" looks like. - set pf [open "$::env(RESULTS_DIR)/memories/$module.place" r] - set n 0 -+ set missing 0 -+ set first_missing "" - while { [gets $pf pl] >= 0 } { - lassign $pl name cx cy orient - if { $name eq "" } { continue } - set inst [$block findInst "$path/$name"] - if { $inst == "NULL" } { -- utl::error FLW 5 "STRUCTURED_PLACEMENT: $path/$name of netlist $module is not in the design (is $path an instance of $module, and the netlist linked?)" -+ incr missing -+ if { $first_missing eq "" } { set first_missing "$path/$name" } -+ continue - } - $inst setOrient $orient - $inst setLocation [expr { $X + $cx - $dx0 }] [expr { $Y + $cy - $dy0 }] -@@ -84,6 +95,12 @@ - incr n - } - close $pf -+ if { $n == 0 } { -+ utl::error FLW 5 "STRUCTURED_PLACEMENT: no cell of netlist $module is in the design under $path (first looked for $first_missing; is $path an instance of $module, and the netlist linked?)" -+ } -+ if { $missing > 0 } { -+ utl::info FLW 8 "structured netlist $module at $path: $missing of [expr {$n + $missing}] placed cells are not in the design (removed as dead logic, e.g. $first_missing); their sites stay empty" -+ } - lappend placed_modules $module - utl::info FLW 6 "structured netlist $module at $path: $n cells FIRM at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um" - } diff --git a/patches/0084-orfs-structured-netlists-dont-touch.patch b/patches/0084-orfs-structured-netlists-dont-touch.patch deleted file mode 100644 index 5dc00ddcb..000000000 --- a/patches/0084-orfs-structured-netlists-dont-touch.patch +++ /dev/null @@ -1,145 +0,0 @@ -From ad7cac59f69ceaf9c5b2b8f71bd4c25b0f20416d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?=C3=98yvind=20Harboe?= -Date: Tue, 22 Sep 2026 09:02:03 +0200 -Subject: [PATCH] memories: a placed netlist's cells are dont_touch as well as - FIRM -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -FIRM keeps the legaliser off the generator's cells; nothing kept the -resizer off them. repair_design on XiangShan's planned parent upsized -378 address inverters of the three placed register files in place, -INVx1 to INVx2, and the wider master overlapped its fixed neighbour, so -detailed placement passed its legalisation and failed check_placement. -The cells are final by construction: mark them dont_touch when they are -placed. Buffers that a repair adds around the array are new cells and -stay allowed. - -The core is what the generator placed: a cell of the netlist it left -unplaced (the periphery, an address decode) is the parent's like any -synthesised cell, and a cell or net is the core's own only when every -terminal of it is a placed cell. The parent's flow then places, sizes and -buffers the periphery: fixed by the generator, an address inverter drove -its literal into every word's decode, XiangShan's ROB had 72 such nets -of 100 to 257 pins at a 3.7 ns slew, and nothing was allowed to repair -them. - -Not upstreamed (builds on carried patch 0078). Retires with it. - -Co-Authored-By: Claude Fable 5.1 -Signed-off-by: Øyvind Harboe - -The array's internal nets are dont_touch as well. repair_design buffers -a long or overloaded net by inserting a cell before its loads, and a -dont_touch load refuses (ODB-1211), which fails the stage (RSZ-3006): -XiangShan's RenameBufferFile, a 305 um array, on a read-mux output net -between two of its own cells. The resizer skips a dont_touch net -outright. A net that leaves the array (a port's net, the clock) stays -repairable; a violation left inside is the generator's to fix. ---- - flow/scripts/structured_netlists.tcl | 68 +++++++++++++++++++++++++++- - 1 file changed, 67 insertions(+), 1 deletion(-) - -diff --git a/flow/scripts/structured_netlists.tcl b/flow/scripts/structured_netlists.tcl ---- a/flow/scripts/structured_netlists.tcl -+++ b/flow/scripts/structured_netlists.tcl -@@ -30,6 +30,31 @@ - if { ![env_var_exists_and_non_empty STRUCTURED_PLACEMENT] } { - utl::error FLW 2 "STRUCTURED_MEMORIES in mode netlist ([join $netlist_modules {, }]) need STRUCTURED_PLACEMENT, a file of lines" - } -+ # The placed cells are the netlist's core, the generator's own; a cell -+ # of the netlist it left unplaced (its periphery: address decode) is -+ # the parent's, like any synthesised cell. A cell is on the core's -+ # boundary when one of its nets reaches a port or a cell outside the -+ # core. -+ proc netlist_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 -+ } -+ # a net is the core's own when every terminal of it is a placed cell -+ # and none is a port of the parent -+ proc netlist_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 block [ord::get_db_block] - set dbu [[ord::get_db_tech] getDbUnitsPerMicron] - set core [$block getCoreArea] -@@ -79,6 +104,8 @@ - set pf [open "$::env(RESULTS_DIR)/memories/$module.place" r] - set n 0 - set missing 0 -+ set placed_insts {} -+ set placed [dict create] - set first_missing "" - while { [gets $pf pl] >= 0 } { - lassign $pl name cx cy orient -@@ -92,9 +119,48 @@ - $inst setOrient $orient - $inst setLocation [expr { $X + $cx - $dx0 }] [expr { $Y + $cy - $dy0 }] - $inst setPlacementStatus FIRM -+ lappend placed_insts $inst -+ dict set placed [$inst getName] 1 - incr n - } - close $pf -+ foreach inst $placed_insts { -+ # The generator's cell is final: FIRM keeps the legaliser off it, -+ # dont_touch keeps the resizer off it. Without the second, -+ # repair_design upsizes an inverter in place, the wider master -+ # overlaps its fixed neighbour, and check_placement fails the stage -+ # (take 23: 378 address inverters, INVx1 -> INVx2). Buffers the -+ # repair adds around the array are new cells and stay allowed. -+ # 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. -+ if { ![[$inst getMaster] isSequential] && ![netlist_boundary_cell $inst $placed] } { -+ $inst setDoNotTouch 1 -+ } -+ } -+ # The wires between the array's own cells are the generator's too. -+ # repair_design buffers a long or overloaded net by inserting a cell -+ # before its loads, and a dont_touch load refuses (ODB-1211, then -+ # RSZ-3006 fails the stage: take 23, RenameBufferFile's rd7_b9_k3_o86, -+ # a 300 um array's read-mux output). The resizer skips a dont_touch -+ # net outright, so the internal nets are marked; a net that leaves -+ # the array (a port's net, the clock) stays repairable. A violation -+ # left inside is the generator's to fix, with a buffer of its own. -+ set internal_nets 0 -+ foreach inst $placed_insts { -+ foreach it [$inst getITerms] { -+ set net [$it getNet] -+ if { $net eq "NULL" || [$net isDoNotTouch] } { continue } -+ if { [netlist_internal_net $net $placed] } { -+ $net setDoNotTouch 1 -+ incr internal_nets -+ } -+ } -+ } - if { $n == 0 } { - utl::error FLW 5 "STRUCTURED_PLACEMENT: no cell of netlist $module is in the design under $path (first looked for $first_missing; is $path an instance of $module, and the netlist linked?)" - } -@@ -102,7 +168,7 @@ - utl::info FLW 8 "structured netlist $module at $path: $missing of [expr {$n + $missing}] placed cells are not in the design (removed as dead logic, e.g. $first_missing); their sites stay empty" - } - lappend placed_modules $module -- utl::info FLW 6 "structured netlist $module at $path: $n cells FIRM at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um" -+ utl::info FLW 6 "structured netlist $module at $path: $n cells FIRM at [expr {$X/double($dbu)}] [expr {$Y/double($dbu)}] um, $internal_nets internal nets dont_touch" - } - foreach m $netlist_modules { - if { [lsearch -exact $placed_modules $m] < 0 } { diff --git a/patches/0089-orfs-auto-memories-regfiles.patch b/patches/0089-orfs-auto-memories-regfiles.patch new file mode 100644 index 000000000..cc1eab432 --- /dev/null +++ b/patches/0089-orfs-auto-memories-regfiles.patch @@ -0,0 +1,984 @@ +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] 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 +--- +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/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. diff --git a/patches/0090-orfs-auto-memories-keep-cost.patch b/patches/0090-orfs-auto-memories-keep-cost.patch index 5235aab46..f559ba7d4 100644 --- a/patches/0090-orfs-auto-memories-keep-cost.patch +++ b/patches/0090-orfs-auto-memories-keep-cost.patch @@ -18,9 +18,7 @@ not a blackbox is an error: its name did not match its definition. This is The-OpenROAD-Project/OpenROAD-flow-scripts#4650 at its head (a3305f118b), applied on the patches carried before it. Beyond the -upstream pull request, so the carried tree stays consistent: -STRUCTURED_MEMORIES (patch 0071) writes its modules' areas too, from the -generator's DEF die, which is the area its .lib states; and FakeRAM's +upstream pull request, so the carried tree stays consistent, FakeRAM's spsram_mask_test (patch 0073) reads the new line format. Not upstreamed yet; retires at a bump onto an ORFS that carries #4650. @@ -38,12 +36,13 @@ diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md 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 -@@ -12,8 +12,9 @@ +@@ -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 - /netlists.txt STRUCTURED_MEMORIES modules in `mode netlist`: - linked as placed netlists, not macros - /blackboxes.txt converted module names, one per line — - what synthesis blackboxes + /blackboxes.txt ` ` per line, area in um^2 as @@ -52,51 +51,6 @@ diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_m Everything downstream consumes these files; nothing else is passed between the generator and the flow. -@@ -94,6 +95,19 @@ - return {"die": die, "row0_orient": row0, "row_height": row_h, "cells": len(lines)} - - -+def _def_area(def_path: Path) -> float: -+ """The generator's die area in um^2, what its .lib states as the -+ macro's area: blackboxes.txt carries it for synthesis's cost.""" -+ text = def_path.read_text() -+ units = re.search(r"UNITS\s+DISTANCE\s+MICRONS\s+(\d+)", text) -+ m = re.search(r"DIEAREA\s*\(\s*(-?\d+)\s+(-?\d+)\s*\)\s*\(\s*(-?\d+)\s+(-?\d+)\s*\)", text) -+ if not units or not m: -+ raise SystemExit(f"gen_memories: {def_path} has no UNITS or DIEAREA") -+ x0, y0, x1, y1 = (int(v) for v in m.groups()) -+ dbu = int(units.group(1)) -+ return (x1 - x0) / dbu * ((y1 - y0) / dbu) -+ -+ - 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) -@@ -133,6 +147,7 @@ - modules = [] - netlists = [] - report = [] -+ areas = [] - for spec in specs: - module = _spec_module(spec) - mode = _spec_mode(spec) -@@ -151,6 +166,7 @@ - ] - subprocess.check_call(cmd) - modules.append(module) -+ areas.append(_def_area(out_dir / f"{module}.def")) - entry = {"module": module, "spec": str(spec), "mode": mode} - if mode == "netlist": - # Placed netlist: no abstract, no model liberty. The module is -@@ -165,7 +181,7 @@ - report.append(entry) - sys.stderr.write(f"gen_memories: {module} -> structured {mode} ({spec.name})\n") - with (out_dir / "blackboxes.txt").open("a") as f: -- f.write("".join(f"{m}\n" for m in modules)) -+ f.write("".join(f"{m} {a:.6f}\n" for m, a in zip(modules, areas))) - (out_dir / "netlists.txt").write_text("".join(f"{m}\n" for m in netlists)) - (out_dir / "structured.json").write_text(json.dumps(report, indent=2) + "\n") - return modules diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py diff --git a/patches/0091-orfs-auto-memories-macro-place.patch b/patches/0091-orfs-auto-memories-macro-place.patch new file mode 100644 index 000000000..ad5fe723c --- /dev/null +++ b/patches/0091-orfs-auto-memories-macro-place.patch @@ -0,0 +1,541 @@ +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] 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 +--- +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/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. diff --git a/patches/0099-orfs-regfile-inline-parameterized.patch b/patches/0099-orfs-regfile-inline-parameterized.patch new file mode 100644 index 000000000..b2a7c7797 --- /dev/null +++ b/patches/0099-orfs-regfile-inline-parameterized.patch @@ -0,0 +1,39 @@ +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] 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 +--- +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" + } + } diff --git a/private/attrs.bzl b/private/attrs.bzl index a2bd73dff..ea7bb4f60 100644 --- a/private/attrs.bzl +++ b/private/attrs.bzl @@ -183,13 +183,6 @@ def yosys_only_attrs(): allow_files = True, default = CONFIG_FAKERAM, ), - "_structured_gen": attr.label( - doc = "The register-file generator STRUCTURED_MEMORIES runs; " + - "staged only when a design sets that variable.", - executable = True, - cfg = "exec", - default = Label("@bazel-orfs//tools/structured_gen:structured_gen"), - ), "yosys": attr.label( doc = "Yosys binary. Override to use a custom or locally-built yosys.", executable = True, diff --git a/private/design_dsl.bzl b/private/design_dsl.bzl index 7a13bb759..c71a51f25 100644 --- a/private/design_dsl.bzl +++ b/private/design_dsl.bzl @@ -52,6 +52,8 @@ _EXPORTED_EXTS = [ "gds.gz", # SYNTH_VERILOG_SURGERY scripts, staged from a design's config.mk "py", + # AUTO_MEMORIES_REGFILES specs, for tests that build one on its own + "regfile", ] _EXPORTS_SENTINEL = "_orfs_design_exports_sentinel" diff --git a/private/rules.bzl b/private/rules.bzl index b87bea5b2..441da3a0b 100644 --- a/private/rules.bzl +++ b/private/rules.bzl @@ -76,6 +76,7 @@ load( "get_sources", "get_stage_args", "keep_modules", + "refuse_retired_arguments", ) # --- Shared helpers --- @@ -1907,6 +1908,7 @@ def _yosys_impl(ctx): use_pre_layout = True, ), ) + refuse_retired_arguments(ctx.label, all_arguments) analysis_args = config_arguments(ctx, all_arguments) analysis_json = declare_artifact(ctx, "results", "1_synth.analysis.json") @@ -2101,19 +2103,23 @@ def _yosys_impl(ctx): fail("AUTO_MEMORIES=1 but no FakeRAM run.py among the files of " + str(ctx.attr._fakeram.label) + " in " + str(ctx.label)) - # STRUCTURED_MEMORIES: the register-file generator runs inside the - # memories step, so the binary is staged into the canonicalize - # sandbox and named to make through STRUCTURED_GEN. The views it - # writes land in the same results/memories directory and travel on - # OrfsInfo.memories. - if all_arguments.get("STRUCTURED_MEMORIES"): + # AUTO_MEMORIES_REGFILES: OpenROAD's generate_regfile builds each + # listed register file inside the memories step, so openroad is a + # tool of the canonicalize action, named to make through + # OPENROAD_EXE. The views land in results/memories beside the + # FakeRAM ones and travel on OrfsInfo.memories. + regfile_tools = [] + if all_arguments.get("AUTO_MEMORIES_REGFILES"): if not auto_memories: - fail("STRUCTURED_MEMORIES is set but AUTO_MEMORIES is not 1: the " + - "generated views ride the AUTO_MEMORIES memories step, so " + - "both are needed. In " + str(ctx.label)) - fakeram_inputs = fakeram_inputs + [ctx.executable._structured_gen] + fail("AUTO_MEMORIES_REGFILES is set but AUTO_MEMORIES is not 1: " + + "the register files ride the AUTO_MEMORIES memories step, " + + "so both are needed. In " + str(ctx.label)) + regfile_tools = [depset( + [ctx.executable.openroad], + transitive = [ctx.attr.openroad[DefaultInfo].default_runfiles.files], + )] fakeram_env = fakeram_env | { - "STRUCTURED_GEN": ctx.executable._structured_gen.path, + "OPENROAD_EXE": ctx.executable.openroad.path, } # The yosys side never reads the SDC: synthesis is clock-agnostic @@ -2180,7 +2186,7 @@ def _yosys_impl(ctx): ), outputs = [canon_output] + canon_logs + memories_outputs + memories_inferred, - tools = yosys_inputs(ctx), + tools = depset(transitive = [yosys_inputs(ctx)] + regfile_tools), progress_message = "Canonicalizing RTL for %s" % module_top(ctx), ) @@ -2765,6 +2771,7 @@ def _make_impl( use_pre_layout = use_pre_layout, ), ) + refuse_retired_arguments(ctx.label, all_arguments) # Write this stage's analysis arguments to .json for downstream stages, then # merge inherited .json files and this stage's .json into the final .mk. diff --git a/private/stages.bzl b/private/stages.bzl index 2d5cdd0a8..e7186ccbb 100644 --- a/private/stages.bzl +++ b/private/stages.bzl @@ -184,6 +184,29 @@ def keep_modules(arguments): """ return [m for m in arguments.get("SYNTH_KEEP_MODULES", "").split(" ") if m] +# Variables bazel-orfs once carried and has retired, with what replaced +# them. ORFS ignores a variable it does not know, so a flow still setting +# one would build without the register files it asks for. +RETIRED_ARGUMENTS = { + "STRUCTURED_MEMORIES": "AUTO_MEMORIES_REGFILES lists the register-file " + + "specs, built by OpenROAD's generate_regfile", + "STRUCTURED_PLACEMENT": "AUTO_MEMORIES_MACRO_PLACE lists the register " + + "files placed as macros, which dissolve into " + + "their cells after macro placement; place them " + + "with MACRO_PLACEMENT_TCL", +} + +def refuse_retired_arguments(label, arguments): + """Fails on a retired variable, naming its replacement. + + Args: + label: the target, for the message. + arguments: the stage's merged arguments. + """ + for key, instead in RETIRED_ARGUMENTS.items(): + if key in arguments: + fail("{} is retired: {}. In {}".format(key, instead, label)) + def check_variables(variables, label): """Checks that all variable names are known in ORFS variables.yaml. @@ -191,6 +214,9 @@ def check_variables(variables, label): variables: iterable of variable names to check. label: description of where the variables came from (for error messages). """ + for v in variables: + if v in RETIRED_ARGUMENTS: + refuse_retired_arguments(label, {v: ""}) unknown = sorted([v for v in variables if v not in ALL_VARIABLE_TO_STAGES]) if unknown: fail( diff --git a/test/BUILD b/test/BUILD index d5486aac9..87d6ded9c 100644 --- a/test/BUILD +++ b/test/BUILD @@ -13,6 +13,7 @@ load(":keep_modules_test.bzl", "keep_modules_test_suite") load(":mock_pin_knobs_test.bzl", "mock_pin_knobs_test") load(":provider_test_rule.bzl", "lib_pre_layout_test") load(":quick_pins_test_rule.bzl", "quick_pins_data_dep_test") +load(":retired_arguments_test.bzl", "retired_argument_test") load(":sdc_clock_period_test.bzl", "runfiles_contents_test", "synth_action_inputs_test") load(":slang_frontend_test.bzl", "slang_frontend_test_suite") load(":source_input_propagation_test.bzl", "source_input_propagation_test") @@ -2690,3 +2691,53 @@ sh_test( "@orfs//tools/FakeRAM2.0:all_files", ], ) + +# A retired variable is refused, naming its replacement: ORFS ignores a +# variable it does not know, so a flow still setting one would build +# without the register files it asks for. In arguments or sources the +# unknown-variable check refuses it at load, which no test can catch; +# these pass it through user_arguments, the escape hatch that check +# exempts, and the stage rule refuses it at analysis. +orfs_synth( + name = "retired_structured_memories_synth", + arguments = LB_ARGS | { + "SDC_FILE": "$(location :constraints-sram)", + }, + data = [":constraints-sram"], + module_top = "lb_32x128", + tags = ["manual"], + user_arguments = { + "STRUCTURED_MEMORIES": "$(location :constraints-sram)", + }, + verilog_files = LB_VERILOG_FILES, +) + +orfs_floorplan( + name = "retired_structured_placement_floorplan", + src = ":lb_32x128_synth", + arguments = LB_ARGS, + sources = LB_SOURCES, + tags = ["manual"], + user_arguments = { + "STRUCTURED_PLACEMENT": "1", + }, + variant = "retired", +) + +[ + retired_argument_test( + name = "%s_refused_test" % name, + message = message, + target_under_test = ":" + name, + ) + for name, message in [ + ( + "retired_structured_memories_synth", + "STRUCTURED_MEMORIES is retired: AUTO_MEMORIES_REGFILES", + ), + ( + "retired_structured_placement_floorplan", + "STRUCTURED_PLACEMENT is retired: AUTO_MEMORIES_MACRO_PLACE", + ), + ] +] diff --git a/test/coremark_joule/designs/asap7/xiangshan/BUILD b/test/coremark_joule/designs/asap7/xiangshan/BUILD index e377ade5a..b8c01de7c 100644 --- a/test/coremark_joule/designs/asap7/xiangshan/BUILD +++ b/test/coremark_joule/designs/asap7/xiangshan/BUILD @@ -51,16 +51,17 @@ exports_files( visibility = ["//test/planned_parent:__pkg__"], ) -# The parent's three placed netlists, at real size in a parent of flops: -# the battery's rung for what the resizer, CTS and the router do to an -# array of this size (test/structured_netlist, scaffold.bzl). +# The register-file specs: the parent's three placed netlists, at real +# size in a parent of flops, are the battery's rung for what the resizer, +# CTS and the router do to an array of this size (test/structured_netlist, +# scaffold.bzl); every spec is generate_regfile's golden case +# (tools/structured_gen:golden_test). exports_files( - [ - "IntRegFile.regfile", - "RenameBufferFile.regfile", - "RobEntryFile.regfile", + glob(["*.regfile"]), + visibility = [ + "//test/structured_netlist:__pkg__", + "//tools/structured_gen:__pkg__", ], - visibility = ["//test/structured_netlist:__pkg__"], ) # The planned parent: the blocks the macro selection tables named, each a diff --git a/test/coremark_joule/designs/asap7/xiangshan/plan/netlists.txt b/test/coremark_joule/designs/asap7/xiangshan/plan/netlists.txt deleted file mode 100644 index 2b623233b..000000000 --- a/test/coremark_joule/designs/asap7/xiangshan/plan/netlists.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by plan_floorplan.py: STRUCTURED_PLACEMENT, the parent's -# placed netlists' lower-left corners in um: -RobEntryFile core/backend/inner_ctrlBlock/rob/robEntryFile 20.944 1482.358 -IntRegFile core/backend/inner_intRegion/intDataPath/intRegFile 350.128 1431.868 -RenameBufferFile core/backend/inner_ctrlBlock/rob/rab/renameBuffer 766.928 1482.628 diff --git a/test/coremark_joule/designs/asap7/xiangshan/plan/place_macros.tcl b/test/coremark_joule/designs/asap7/xiangshan/plan/place_macros.tcl index 5feb6f2a5..5fc218131 100644 --- a/test/coremark_joule/designs/asap7/xiangshan/plan/place_macros.tcl +++ b/test/coremark_joule/designs/asap7/xiangshan/plan/place_macros.tcl @@ -46,6 +46,22 @@ foreach {lo hi} { incr bands } puts "place_macros.tcl: $bands soft placement blockage(s) over the blocks' bands; the rows stay" +# The plan's register files (AUTO_MEMORIES_REGFILES in mode netlist) are +# macros until the end of this step, each found by its instance and placed +# where the plan put it; they dissolve into their cells after it. +foreach {inst x y} { + core/backend/inner_ctrlBlock/rob/robEntryFile 20.944 1482.358 + core/backend/inner_intRegion/intDataPath/intRegFile 350.128 1431.868 + core/backend/inner_ctrlBlock/rob/rab/renameBuffer 766.928 1482.628 +} { + set i [$block findInst $inst] + if { $i eq "NULL" } { + utl::error FLW 2 "plan: no register file instance $inst" + } + place_macro -macro_name $inst -location [list $x $y] -orientation R0 -exact + $i setPlacementStatus FIRM + puts "place_macros.tcl: register file $inst at $x $y um, R0" +} # Macros the plan does not name (the parent's own generated register files # and memories) go around the planned blocks, which are FIRM and stay put. set rest {} @@ -54,40 +70,5 @@ foreach inst [$block getInsts] { } if { [llength $rest] > 0 } { puts "place_macros.tcl: [llength $rest] macros not in the plan; rtl_macro_placer places them around the planned blocks" - # rtl_macro_placer refuses a FIRM standard cell in its area (MPL-0050), - # and the plan's placed netlists are FIRM from floorplan on. For the - # macro step only they are PLACED, and a hard placement blockage over - # each netlist's cells, which rtl_macro_placer keeps macros off, stands - # in for them; after it, both are undone. - set firm {} - array set box {} - foreach inst [$block getInsts] { - if { [[$inst getMaster] isBlock] || [$inst getPlacementStatus] ne "FIRM" } { continue } - lappend firm [list $inst [$inst getLocation] [$inst getOrient]] - set key [file dirname [$inst getName]] - set b [$inst getBBox] - if { [info exists box($key)] } { - lassign $box($key) x0 y0 x1 y1 - set box($key) [list [expr { min($x0, [$b xMin]) }] [expr { min($y0, [$b yMin]) }] [expr { max($x1, [$b xMax]) }] [expr { max($y1, [$b yMax]) }]] - } else { - set box($key) [list [$b xMin] [$b yMin] [$b xMax] [$b yMax]] - } - } - set stand_ins {} - foreach key [array names box] { - lassign $box($key) x0 y0 x1 y1 - lappend stand_ins [odb::dbBlockage_create $block $x0 $y0 $x1 $y1] - } - foreach f $firm { [lindex $f 0] setPlacementStatus PLACED } - puts "place_macros.tcl: [llength $firm] FIRM netlist cells stand aside for the macro step, [llength $stand_ins] blockage(s) in their place" rtl_macro_placer -halo_width 2 -halo_height 2 - # rtl_macro_placer moves a cluster's standard cells to one point: put - # every netlist cell back where the plan put it - foreach f $firm { - lassign $f inst loc orient - $inst setOrient $orient - $inst setLocation {*}$loc - $inst setPlacementStatus FIRM - } - foreach bl $stand_ins { odb::dbBlockage_destroy $bl } } diff --git a/test/coremark_joule/designs/asap7/xiangshan/xiangshan.bzl b/test/coremark_joule/designs/asap7/xiangshan/xiangshan.bzl index 7b8f2d688..efec0ed68 100644 --- a/test/coremark_joule/designs/asap7/xiangshan/xiangshan.bzl +++ b/test/coremark_joule/designs/asap7/xiangshan/xiangshan.bzl @@ -189,7 +189,7 @@ XS_BLOCKS = { "sources": { "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCK_grid_strategy.tcl"], "SDC_FILE": ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"], - "STRUCTURED_MEMORIES": ["//test/coremark_joule/designs/asap7/xiangshan:IntRegFile.regfile"], + "AUTO_MEMORIES_REGFILES": ["//test/coremark_joule/designs/asap7/xiangshan:IntRegFile.regfile"], }, }, "Dispatch": { @@ -274,7 +274,7 @@ XS_BLOCKS = { "sources": { "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCK_grid_strategy.tcl"], "SDC_FILE": ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"], - "STRUCTURED_MEMORIES": ["//test/coremark_joule/designs/asap7/xiangshan:FtqEntryQueue.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueRedirect.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueResolve.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueCommit.regfile"], + "AUTO_MEMORIES_REGFILES": ["//test/coremark_joule/designs/asap7/xiangshan:FtqEntryQueue.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueRedirect.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueResolve.regfile", "//test/coremark_joule/designs/asap7/xiangshan:FtqMetaQueueCommit.regfile"], }, }, "HPerfMonitor_3": { @@ -802,7 +802,7 @@ XS_BLOCKS = { "sources": { "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCK_grid_strategy.tcl"], "SDC_FILE": ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"], - "STRUCTURED_MEMORIES": ["//test/coremark_joule/designs/asap7/xiangshan:FpRegFile.regfile"], + "AUTO_MEMORIES_REGFILES": ["//test/coremark_joule/designs/asap7/xiangshan:FpRegFile.regfile"], }, }, "Rename": { @@ -859,7 +859,7 @@ XS_BLOCKS = { "sources": { "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCK_grid_strategy.tcl"], "SDC_FILE": ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"], - "STRUCTURED_MEMORIES": ["//test/coremark_joule/designs/asap7/xiangshan:RenameBufferFile.regfile", "//test/coremark_joule/designs/asap7/xiangshan:RobEntryFile.regfile"], + "AUTO_MEMORIES_REGFILES": ["//test/coremark_joule/designs/asap7/xiangshan:RenameBufferFile.regfile", "//test/coremark_joule/designs/asap7/xiangshan:RobEntryFile.regfile"], }, }, "Sbuffer": { @@ -1024,7 +1024,7 @@ XS_BLOCKS = { "sources": { "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCK_grid_strategy.tcl"], "SDC_FILE": ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"], - "STRUCTURED_MEMORIES": ["//test/coremark_joule/designs/asap7/xiangshan:VfRegFile.regfile"], + "AUTO_MEMORIES_REGFILES": ["//test/coremark_joule/designs/asap7/xiangshan:VfRegFile.regfile"], }, }, "VectorDecodeChannel": { @@ -1071,14 +1071,20 @@ XS_BLOCK_DEFAULT = dict(XS_BLOCKS["Bpu"], arguments = {k: v for k, v in XS_BLOCK # odb-debug in seconds instead. _PLAN_DROPS = ["CORE_UTILIZATION", "CORE_ASPECT_RATIO", "CORE_MARGIN", "DETAIL_PLACEMENT_ARGS"] -def _structured_memories(own_sources, keep, blocks): +def _macro_place(specs): + """Every register file placed as a macro, by the module its spec + names (.regfile): the plan places them, and inlined they + would have no instance to place.""" + return " ".join([s.split(":")[-1].removesuffix(".regfile") for s in specs]) + +def _regfiles(own_sources, keep, blocks): """The generated register files a planned flow hardens: its own, plus those of every block of the old table whose module it now keeps (the Ftq queues inside Frontend, the int register file and the ROB's files in the parent), each once.""" - out = list(own_sources.get("STRUCTURED_MEMORIES", [])) + out = list(own_sources.get("AUTO_MEMORIES_REGFILES", [])) for k in [k for k in keep.split(" ") if k]: - for m in blocks.get(k, {}).get("sources", {}).get("STRUCTURED_MEMORIES", []): + for m in blocks.get(k, {}).get("sources", {}).get("AUTO_MEMORIES_REGFILES", []): if m not in out: out.append(m) return out @@ -1110,9 +1116,10 @@ def _planned_block(cfg, entry, plan_dir, block, blocks): arguments["OPENROAD_HIERARCHICAL"] = "1" arguments["ASAP7_USE_VT"] = XS_VT sources = dict(cfg["sources"]) - mems = _structured_memories(cfg["sources"], entry.get("SYNTH_KEEP_MODULES", ""), blocks) + mems = _regfiles(cfg["sources"], entry.get("SYNTH_KEEP_MODULES", ""), blocks) if mems: - sources["STRUCTURED_MEMORIES"] = mems + sources["AUTO_MEMORIES_REGFILES"] = mems + arguments["AUTO_MEMORIES_MACRO_PLACE"] = _macro_place(mems) sources["IO_CONSTRAINTS"] = [":%s/%s_pins.tcl" % (plan_dir, block)] sources["SDC_FILE"] = ["//test/coremark_joule/designs/asap7/xiangshan:constraints.sdc"] return arguments, sources @@ -1255,14 +1262,13 @@ def xiangshan_flow(name, plan, blocks = XS_BLOCKS, parent = XS_PARENT, tags = [" # the memories of what the parent still holds; a hardened block's # are the block's own parent_keep = " ".join([k for k in arguments.get("SYNTH_KEEP_MODULES", "").split(" ") if k and k not in macros]) - mems = _structured_memories(parent["sources"], parent_keep, blocks) + mems = _regfiles(parent["sources"], parent_keep, blocks) if mems: - sources["STRUCTURED_MEMORIES"] = mems + sources["AUTO_MEMORIES_REGFILES"] = mems + arguments["AUTO_MEMORIES_MACRO_PLACE"] = _macro_place(mems) + + # the plan's blocks and register files, placed by the plan sources["MACRO_PLACEMENT_TCL"] = [":%s/place_macros.tcl" % plan_dir] - if plan["parent"].get("netlists"): - # the generated arrays dropped FIRM into the parent at floorplan - # (STRUCTURED_MEMORIES in mode netlist, patch 0078) - sources["STRUCTURED_PLACEMENT"] = [":%s/netlists.txt" % plan_dir] # Every legalization of the parent -- placement's, CTS's two, global # route's -- first moves each movable cell the resizer or CTS left diff --git a/test/planned_parent/BUILD.bazel b/test/planned_parent/BUILD.bazel index 0fe84c01e..d6624538c 100644 --- a/test/planned_parent/BUILD.bazel +++ b/test/planned_parent/BUILD.bazel @@ -1,5 +1,6 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_python//python:defs.bzl", "py_binary", "py_test") +load(":plan/plan.bzl", "PLAN") load(":planned.bzl", "mini_planned_flow") # The planned parent in miniature: the XiangShan study's shape at a @@ -175,19 +176,20 @@ py_test( main = "plan_drift_test.py", ) -# A generated macro the plan does not name is placed beside the plan's FIRM -# netlists: rtl_macro_placer refuses a FIRM cell in its area (MPL-0050). +# A generated macro the plan does not name is placed beside the plan's +# register files, which dissolve where the plan put them. py_test( name = "leftover_macros_test", srcs = ["leftover_macros_test.py"], - args = [ - "$(rootpath :leftover_macros.json)", - "$(rootpath :plan/netlists.txt)", - ], - data = [ - ":leftover_macros.json", - ":plan/netlists.txt", + args = ["$(rootpath :leftover_macros.json)"] + [ + "%s=%s,%s" % ( + n["instance"], + n["x_um"], + n["y_um"], + ) + for n in PLAN["parent"]["netlists"] ], + data = [":leftover_macros.json"], main = "leftover_macros_test.py", ) diff --git a/test/planned_parent/leftover_macros.tcl b/test/planned_parent/leftover_macros.tcl index 4ff341557..bc91d70d6 100644 --- a/test/planned_parent/leftover_macros.tcl +++ b/test/planned_parent/leftover_macros.tcl @@ -2,19 +2,16 @@ # the plan does not name with its placement status and box, every placed # netlist's box, how many of its cells are not FIRM and how many of those are # core cells, and whether any such macro overlaps a netlist. The core is -# what the generator placed, the cells its .place file names (patch 0084's -# rule); the rest of the netlist is its periphery, the parent's, which the -# macro step may give a location. The box is the FIRM cells, the array the +# what the generator placed, the cells its .place file names; the rest of +# the netlist is its periphery, the parent's, left for global placement. The box is the FIRM cells, the array the # plan placed. Declared beside the flow it probes. source $::env(SCRIPTS_DIR)/load.tcl load_design 2_floorplan.odb 2_floorplan.sdc set block [ord::get_db_block] # the core: every cell a netlist's .place file names, under its instance +# (NETLISTS: the plan's pairs) set core [dict create] -set pf [open $::env(STRUCTURED_PLACEMENT) r] -while { [gets $pf line] >= 0 } { - if { [string match "#*" $line] || [llength $line] < 2 } { continue } - lassign $line module path +foreach {module path} $::env(NETLISTS) { set place "$::env(RESULTS_DIR)/memories/$module.place" if { ![file exists $place] } { error "leftover_macros.tcl: no $place for $path" } set f [open $place r] @@ -23,7 +20,6 @@ while { [gets $pf line] >= 0 } { } close $f } -close $pf set macros {} array set box {} array set loose {} diff --git a/test/planned_parent/leftover_macros_test.py b/test/planned_parent/leftover_macros_test.py index c053792d2..a322ee53a 100644 --- a/test/planned_parent/leftover_macros_test.py +++ b/test/planned_parent/leftover_macros_test.py @@ -1,17 +1,16 @@ """A generated macro the plan does not name is placed beside the plan's netlists. -The planned parent holds both: the register files the plan places as FIRM -netlists at floorplan, and generated macros the plan does not name, which -place_macros.tcl hands to rtl_macro_placer. rtl_macro_placer refuses a FIRM -standard cell in its area (MPL-0050), so on XSTile, with the regions -flattened, the floorplan stopped. The miniature's FileM is such a macro. +The planned parent holds both: the register files the plan places, macros +until they dissolve into their cells after the macro step, and generated +macros the plan does not name, which place_macros.tcl hands to +rtl_macro_placer. The miniature's FileM is such a macro. What is checked on the floorplan checkpoint: FileM is placed, it overlaps -none of the netlists, and every netlist is FIRM again after the macro step, -where the plan put it and still an array (rtl_macro_placer moves a -cluster's cells to one point, so a netlist it saw as movable collapses), -but for its periphery: the generator leaves the address decode to the -parent, to place and size like any logic. +none of the netlists, and every netlist's core is FIRM where the plan put +it and still an array, but for its periphery: the generator leaves the +address decode to the parent, to place and size like any logic. + + leftover_macros_test.py =,... """ import json @@ -43,11 +42,10 @@ def test_placed_beside_netlists(self): "%s overlaps the netlist %s" % (m["name"], n["name"]), ) planned = {} - with open(sys.argv[2]) as f: - for line in f: - p = line.split() - if len(p) == 4 and not line.startswith("#"): - planned[p[1]] = (float(p[2]), float(p[3])) + for arg in sys.argv[2:]: + inst, xy = arg.split("=") + x, y = xy.split(",") + planned[inst] = (float(x), float(y)) dbu = d["dbu"] for n in d["netlists"]: self.assertGreater( diff --git a/test/planned_parent/plan/netlists.txt b/test/planned_parent/plan/netlists.txt deleted file mode 100644 index 290e4fc4d..000000000 --- a/test/planned_parent/plan/netlists.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by plan_floorplan.py: STRUCTURED_PLACEMENT, the parent's -# placed netlists' lower-left corners in um: -FileA u_filea 20.944 148.717 -FileB u_fileb 61.744 148.717 -FileC u_filec 20.944 107.400 diff --git a/test/planned_parent/plan/place_macros.tcl b/test/planned_parent/plan/place_macros.tcl index 5629e3c48..6bdb60ac6 100644 --- a/test/planned_parent/plan/place_macros.tcl +++ b/test/planned_parent/plan/place_macros.tcl @@ -45,6 +45,22 @@ foreach {lo hi} { incr bands } puts "place_macros.tcl: $bands soft placement blockage(s) over the blocks' bands; the rows stay" +# The plan's register files (AUTO_MEMORIES_REGFILES in mode netlist) are +# macros until the end of this step, each found by its instance and placed +# where the plan put it; they dissolve into their cells after it. +foreach {inst x y} { + u_filea 20.944 148.717 + u_fileb 61.744 148.717 + u_filec 20.944 107.400 +} { + set i [$block findInst $inst] + if { $i eq "NULL" } { + utl::error FLW 2 "plan: no register file instance $inst" + } + place_macro -macro_name $inst -location [list $x $y] -orientation R0 -exact + $i setPlacementStatus FIRM + puts "place_macros.tcl: register file $inst at $x $y um, R0" +} # Macros the plan does not name (the parent's own generated register files # and memories) go around the planned blocks, which are FIRM and stay put. set rest {} @@ -53,40 +69,5 @@ foreach inst [$block getInsts] { } if { [llength $rest] > 0 } { puts "place_macros.tcl: [llength $rest] macros not in the plan; rtl_macro_placer places them around the planned blocks" - # rtl_macro_placer refuses a FIRM standard cell in its area (MPL-0050), - # and the plan's placed netlists are FIRM from floorplan on. For the - # macro step only they are PLACED, and a hard placement blockage over - # each netlist's cells, which rtl_macro_placer keeps macros off, stands - # in for them; after it, both are undone. - set firm {} - array set box {} - foreach inst [$block getInsts] { - if { [[$inst getMaster] isBlock] || [$inst getPlacementStatus] ne "FIRM" } { continue } - lappend firm [list $inst [$inst getLocation] [$inst getOrient]] - set key [file dirname [$inst getName]] - set b [$inst getBBox] - if { [info exists box($key)] } { - lassign $box($key) x0 y0 x1 y1 - set box($key) [list [expr { min($x0, [$b xMin]) }] [expr { min($y0, [$b yMin]) }] [expr { max($x1, [$b xMax]) }] [expr { max($y1, [$b yMax]) }]] - } else { - set box($key) [list [$b xMin] [$b yMin] [$b xMax] [$b yMax]] - } - } - set stand_ins {} - foreach key [array names box] { - lassign $box($key) x0 y0 x1 y1 - lappend stand_ins [odb::dbBlockage_create $block $x0 $y0 $x1 $y1] - } - foreach f $firm { [lindex $f 0] setPlacementStatus PLACED } - puts "place_macros.tcl: [llength $firm] FIRM netlist cells stand aside for the macro step, [llength $stand_ins] blockage(s) in their place" rtl_macro_placer -halo_width 2 -halo_height 2 - # rtl_macro_placer moves a cluster's standard cells to one point: put - # every netlist cell back where the plan put it - foreach f $firm { - lassign $f inst loc orient - $inst setOrient $orient - $inst setLocation {*}$loc - $inst setPlacementStatus FIRM - } - foreach bl $stand_ins { odb::dbBlockage_destroy $bl } } diff --git a/test/planned_parent/plan_drift_test.py b/test/planned_parent/plan_drift_test.py index c628e6f36..08dc784d7 100644 --- a/test/planned_parent/plan_drift_test.py +++ b/test/planned_parent/plan_drift_test.py @@ -36,7 +36,6 @@ def test_plan_dir_matches_the_generators(self): "BlockC_pins.tcl", "BlockD_pins.tcl", "place_macros.tcl", - "netlists.txt", "plan.bzl", ]: with open(os.path.join(out, f)) as a, open( diff --git a/test/planned_parent/planned.bzl b/test/planned_parent/planned.bzl index e98858d06..3b0ea3e28 100644 --- a/test/planned_parent/planned.bzl +++ b/test/planned_parent/planned.bzl @@ -18,7 +18,8 @@ BASE_ARGS = TURNAROUND_ARGS | { def mini_planned_flow(rtl, files): """The blocks named in PLAN, each at its planned outline with its pins on the planned side, abstracted at cts; the parent at the planned die, - the blocks placed by the plan, the files as placed netlists, to grt. + the blocks placed by the plan, the files as register files the plan + places and that dissolve into their cells after the macro step, to grt. Args: rtl: the genrule that writes blocks.sv, files.sv and top.sv. @@ -47,13 +48,14 @@ def mini_planned_flow(rtl, files): "CORE_AREA": PLAN["parent"]["CORE_AREA"], "DIE_AREA": PLAN["parent"]["DIE_AREA"], "RTLMP_MIN_CHANNEL_SIZE": "4 4", + # the plan places every file as a macro, by its module (.regfile) + "AUTO_MEMORIES_MACRO_PLACE": " ".join([f.split(":")[-1].removesuffix(".regfile") for f in files]), } parent_sources = { "MACRO_PLACEMENT_TCL": [":plan/place_macros.tcl"], "PDN_TCL": ["//flow:platforms/asap7/openRoad/pdn/BLOCKS_grid_strategy.tcl"], "SDC_FILE": [":constraints.sdc"], - "STRUCTURED_MEMORIES": files, - "STRUCTURED_PLACEMENT": [":plan/netlists.txt"], + "AUTO_MEMORIES_REGFILES": files, } parent_macros = [":%s_generate_abstract" % b for b in sorted(PLAN["macros"])] orfs_flow( @@ -108,6 +110,10 @@ def mini_planned_flow(rtl, files): script = ":leftover_macros.tcl", sources = parent_sources, user_arguments = { + "NETLISTS": " ".join([ + "%s %s" % (n["module"], n["instance"]) + for n in PLAN["parent"]["netlists"] + ]), "OUTPUT_JSON": "$(location leftover_macros.json)", }, ) diff --git a/test/planned_parent/regen.sh b/test/planned_parent/regen.sh index 4f3752d85..b71485066 100755 --- a/test/planned_parent/regen.sh +++ b/test/planned_parent/regen.sh @@ -7,7 +7,7 @@ out=tmp/planned_parent_regen rm -rf "$out" python3 test/planned_parent/mini_gen.py --out "$out" python3 tools/macro_select/plan_floorplan.py "$out/plan.json" --out "$out/plan_out.json" --emit "$out" > /dev/null -for f in BlockA_pins.tcl BlockB_pins.tcl BlockC_pins.tcl BlockD_pins.tcl place_macros.tcl netlists.txt plan.bzl; do +for f in BlockA_pins.tcl BlockB_pins.tcl BlockC_pins.tcl BlockD_pins.tcl place_macros.tcl plan.bzl; do cp "$out/$f" test/planned_parent/plan/$f done echo "test/planned_parent/plan regenerated" diff --git a/test/regfile/BUILD.bazel b/test/regfile/BUILD.bazel new file mode 100644 index 000000000..c8ee89b21 --- /dev/null +++ b/test/regfile/BUILD.bazel @@ -0,0 +1,159 @@ +load("@bazel-orfs//:openroad.bzl", "orfs_flow", "orfs_run") +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_python//python:defs.bzl", "py_test") +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +# AUTO_MEMORIES_REGFILES (ORFS patch 0089 on OpenROAD patch 0010) in its +# smallest form: one 16 x 8 register file with two read and two write +# ports in a parent of flops, seconds to global route. regfile_top places +# it as a macro (AUTO_MEMORIES_MACRO_PLACE, patch 0091), which dissolves +# into its cells after macro placement; the inlined variant does not +# list it, so synthesis reads the generated netlist in the module's place. +[ + orfs_flow( + name = "regfile_top", + arguments = { + "AUTO_MEMORIES": "1", + # 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. + "CORE_AREA": "2 2 58 43", + "DIE_AREA": "0 0 60 45", + "PLACE_DENSITY": "0.5", + } | macro_place, + last_stage = "grt", + sources = { + "AUTO_MEMORIES_REGFILES": [":RegFile.regfile"], + "SDC_FILE": [":constraint.sdc"], + }, + variant = variant, + verilog_files = ["regfile_top.v"], + ) + for variant, macro_place in [ + ( + None, + {"AUTO_MEMORIES_MACRO_PLACE": "RegFile"}, + ), + ("inlined", {}), + ] +] + +orfs_run( + name = "dissolve_probe", + src = ":regfile_top_floorplan", + outs = ["dissolve_probe.json"], + script = ":dissolve_probe.tcl", + user_arguments = { + "OUTPUT_JSON": "$(location dissolve_probe.json)", + }, +) + +py_test( + name = "check_dissolve_test", + size = "small", + srcs = ["check_dissolve.py"], + args = ["$(rootpath :dissolve_probe.json)"], + data = [":dissolve_probe.json"], + main = "check_dissolve.py", +) + +# Every register clocked, the register file in a module below the top: +# dissolved after the macro step, and inlined in synthesis. +[ + [ + orfs_run( + name = "%s_clocked_probe" % name, + src = ":%s_floorplan" % name, + outs = ["%s_clocked_probe.json" % name], + script = ":clocked_probe.tcl", + user_arguments = { + "OUTPUT_JSON": "$(location %s_clocked_probe.json)" % name, + }, + ), + py_test( + name = "%s_clocked_test" % name, + size = "small", + srcs = ["check_clocked.py"], + args = ["$(rootpath :%s_clocked_probe.json)" % name], + data = [":%s_clocked_probe.json" % name], + main = "check_clocked.py", + ), + ] + for name in [ + "regfile_top", + "regfile_top_inlined", + ] +] + +# The dissolved file through repair_design, CTS, the legaliser and global +# route: its core's cells dont_touch but its boundary repairable, its +# flops rewired by CTS, its decode placed and sized by the parent. +build_test( + name = "regfile_grt_build_test", + targets = [ + ":regfile_top_grt", + ":regfile_top_inlined_grt", + ], +) + +# A register file with a keep threshold. keep_hierarchy -min_cost +# (SYNTH_MINIMUM_KEEP_SIZE) needs a cost for every instanced blackbox, +# and a register-file macro is a blackbox made from the RTL's own +# module: its line in blackboxes.txt carries the area its generated .lib +# states (ORFS patches 0090 and 0089), or synthesis stops with "Missing +# cost information on instanced blackbox". The threshold is 1, not 0: +# yosys reads -min_cost 0 as no threshold and never asks for a cost. +orfs_flow( + name = "regfile_top", + arguments = { + "AUTO_MEMORIES": "1", + "AUTO_MEMORIES_MACRO_PLACE": "RegFile", + "SYNTH_HIERARCHICAL": "1", + "SYNTH_MINIMUM_KEEP_SIZE": "1", + "SYNTH_NUM_PARTITIONS": "1", + }, + last_stage = "synth", + sources = { + "AUTO_MEMORIES_REGFILES": [":RegFile.regfile"], + "SDC_FILE": [":constraint.sdc"], + }, + variant = "keep", + verilog_files = ["regfile_top.v"], +) + +filegroup( + name = "regfile_top_keep_synth_netlist", + srcs = [":regfile_top_keep_synth"], + output_group = "1_2_yosys.v", +) + +# Synthesis reached its netlist, and the register file is a macro +# instance, not a module of its own. +sh_test( + name = "regfile_top_keep_synth_test", + size = "small", + srcs = ["//test:synth_discovery_netlist_test.sh"], + args = [ + "$(location :regfile_top_keep_synth_netlist)", + "regfile_top", + "!RegFile", + ], + data = [":regfile_top_keep_synth_netlist"], +) + +# The register file's line in blackboxes.txt is `RegFile `. +genrule( + name = "regfile_top_keep_blackboxes", + srcs = [":regfile_top_keep_synth"], + outs = ["regfile_top_keep_blackboxes.txt"], + cmd = "files=$$(echo $(locations :regfile_top_keep_synth) | tr ' ' '\\n'); " + + "bb=$$(echo \"$$files\" | grep '/memories$$')/blackboxes.txt; " + + "grep -Eq '^RegFile [0-9]+[.][0-9]+$$' $$bb || " + + "{ echo 'no `RegFile ` line in blackboxes.txt'; cat $$bb; exit 1; }; " + + "cp $$bb $@", +) + +build_test( + name = "regfile_top_keep_blackboxes_test", + targets = [":regfile_top_keep_blackboxes"], +) diff --git a/test/regfile/RegFile.regfile b/test/regfile/RegFile.regfile new file mode 100644 index 000000000..b5f7ea063 --- /dev/null +++ b/test/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/test/regfile/check_clocked.py b/test/regfile/check_clocked.py new file mode 100644 index 000000000..1036c808a --- /dev/null +++ b/test/regfile/check_clocked.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""After the dissolve, STA sees every register clocked. + +regfile_dissolve.tcl (ORFS patch 0089) replaces the register file macro +with its cells inside the macro's module. A cell connected to the flat +clock net but not to the module's clock net is invisible to STA's clock +propagation: its flops are no endpoints until CTS rewires them, and +global placement and repair at place never see the paths through the +array. clocked_probe.tcl counts the registers and the clocked ones. +""" + +import json +import sys +import unittest + +PROBE = None + + +class Clocked(unittest.TestCase): + def test_every_register_clocked(self): + with open(PROBE) as f: + p = json.load(f) + self.assertGreater(p["registers"], 0) + self.assertEqual(p["clocked"], p["registers"]) + + +if __name__ == "__main__": + PROBE = sys.argv.pop(1) + unittest.main() diff --git a/test/regfile/check_dissolve.py b/test/regfile/check_dissolve.py new file mode 100644 index 000000000..fb8399441 --- /dev/null +++ b/test/regfile/check_dissolve.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""test/regfile's regfile_top after the macro step: the register file dissolved. + +The flow saw RegFile as a macro through synthesis and macro placement; +regfile_dissolve.tcl (ORFS patch 0089) then replaced it with its cells. +dissolve_probe.tcl counts what is there; this asserts what should be: + +- no RegFile macro is left; +- the core, the cells memories/RegFile.place names, is in the design, + FIRM, each on a row of its own orientation; bit 0 is never read, so + its column went with the dead logic and fewer cells than the .place + lines remain; +- the core's flops are not dont_touch (CTS rewires their clock pins), + some of its other cells and nets are; +- the periphery, the address decode, is left unplaced for global + placement, none of it FIRM. +""" + +import json +import sys +import unittest + +PROBE = None + + +class Dissolved(unittest.TestCase): + def setUp(self): + with open(PROBE) as f: + self.p = json.load(f) + + def test_macro_gone(self): + self.assertEqual(self.p["macros"], 0) + + def test_core_firm_on_rows(self): + p = self.p + self.assertGreater(p["core"], 0) + self.assertEqual(p["core_firm"], p["core"]) + self.assertEqual(p["core_off_row"], 0) + + def test_dead_column_removed(self): + self.assertLess(self.p["core"], self.p["place_lines"]) + + def test_dont_touch(self): + p = self.p + self.assertGreater(p["core_flops"], 0) + self.assertGreater(p["core_dont_touch"], 0) + self.assertLessEqual(p["core_dont_touch"], p["core"] - p["core_flops"]) + self.assertGreater(p["internal_nets_dont_touch"], 0) + + def test_periphery_unplaced(self): + p = self.p + self.assertGreater(p["periphery"], 0) + self.assertEqual(p["periphery_placed"], 0) + self.assertEqual(p["periphery_firm"], 0) + + +if __name__ == "__main__": + PROBE = sys.argv.pop(1) + unittest.main() diff --git a/test/regfile/clocked_probe.tcl b/test/regfile/clocked_probe.tcl new file mode 100644 index 000000000..46da29a0e --- /dev/null +++ b/test/regfile/clocked_probe.tcl @@ -0,0 +1,16 @@ +# Every register of a stage's ODB and how many of them STA sees clocked, +# as JSON for check_clocked.py. A register that STA does not see clocked +# is no timing endpoint or startpoint: placement and repair cannot see the +# paths through it. +source $::env(SCRIPTS_DIR)/util.tcl +source $::env(SCRIPTS_DIR)/read_liberty.tcl +read_db $::env(ODB_FILE) +read_sdc $::env(SDC_FILE) +set registers [all_registers -cells] +set clocked [all_registers -cells -clock [all_clocks]] +set out [open $::env(OUTPUT_JSON) w] +puts $out "{" +puts $out " \"registers\": [llength $registers]," +puts $out " \"clocked\": [llength $clocked]" +puts $out "}" +close $out diff --git a/test/regfile/constraint.sdc b/test/regfile/constraint.sdc new file mode 100644 index 000000000..00013668d --- /dev/null +++ b/test/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/test/regfile/dissolve_probe.tcl b/test/regfile/dissolve_probe.tcl new file mode 100644 index 000000000..e931c6809 --- /dev/null +++ b/test/regfile/dissolve_probe.tcl @@ -0,0 +1,71 @@ +# What the macro step left of test/regfile's regfile_top register file, as JSON: +# whether the RegFile macro is gone, its cells by kind, and the facts +# check_dissolve.py asserts. The core is the cells memories/RegFile.place +# names; the rest under u_rf/ is the periphery. +# The stage's ODB is ODB_FILE, in another repository's results directory: +# read it directly, and the generator's files from beside it. +read_db $::env(ODB_FILE) +set block [ord::get_db_block] +set place [dict create] +set f [open [file dirname $::env(ODB_FILE)]/memories/RegFile.place r] +while { [gets $f line] >= 0 } { + if { [lindex $line 0] ne "" } { dict set place "u_rf/[lindex $line 0]" 1 } +} +close $f +set row_orient [dict create] +foreach row [$block getRows] { + dict set row_orient [[$row getBBox] yMin] [$row getOrient] +} +set macros 0 +set core 0 +set core_firm 0 +set core_dont_touch 0 +set core_flops 0 +set off_row 0 +set periphery 0 +set periphery_placed 0 +set stray_firm 0 +foreach inst [$block getInsts] { + set master [$inst getMaster] + if { [$master getName] eq "RegFile" } { incr macros } + set name [$inst getName] + if { ![string match u_rf/* $name] } { continue } + if { [dict exists $place $name] } { + incr core + if { [$inst getPlacementStatus] eq "FIRM" } { incr core_firm } + if { [$inst isDoNotTouch] } { incr core_dont_touch } + if { [$master isSequential] } { incr core_flops } + set y [[$inst getBBox] yMin] + if { ![dict exists $row_orient $y] } { + incr off_row + } else { + # a cell flipped about x sits on an MX row, an upright one on an R0 row + set ro [expr { [dict get $row_orient $y] in {R0 MY} ? "R0" : "MX" }] + set co [expr { [$inst getOrient] in {R0 MY} ? "R0" : "MX" }] + if { $ro ne $co } { incr off_row } + } + } else { + incr periphery + if { [$inst isPlaced] } { incr periphery_placed } + if { [$inst getPlacementStatus] eq "FIRM" } { incr stray_firm } + } +} +set internal_nets 0 +foreach net [$block getNets] { + if { [string match u_rf/* [$net getName]] && [$net isDoNotTouch] } { incr internal_nets } +} +set out [open $::env(OUTPUT_JSON) w] +puts $out "{" +puts $out " \"place_lines\": [dict size $place]," +puts $out " \"macros\": $macros," +puts $out " \"core\": $core," +puts $out " \"core_firm\": $core_firm," +puts $out " \"core_dont_touch\": $core_dont_touch," +puts $out " \"core_flops\": $core_flops," +puts $out " \"core_off_row\": $off_row," +puts $out " \"periphery\": $periphery," +puts $out " \"periphery_placed\": $periphery_placed," +puts $out " \"periphery_firm\": $stray_firm," +puts $out " \"internal_nets_dont_touch\": $internal_nets" +puts $out "}" +close $out diff --git a/test/regfile/regfile_top.v b/test/regfile/regfile_top.v new file mode 100644 index 000000000..43fdf1c67 --- /dev/null +++ b/test/regfile/regfile_top.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/test/regfile/sim/BUILD.bazel b/test/regfile/sim/BUILD.bazel new file mode 100644 index 000000000..e3590a5f6 --- /dev/null +++ b/test/regfile/sim/BUILD.bazel @@ -0,0 +1,84 @@ +# What generate_regfile builds computes what the RTL it replaces does: +# riscv32i's register file as generate_regfile writes it, simulated beside +# src/riscv32i/regfile.v under random reads and writes, every read +# compared. One netlist per write style: a hold mux per bit, and a clock +# gate per word. +load("@rules_cc//cc:defs.bzl", "cc_test") +load("@rules_verilator//verilator:defs.bzl", "verilator_cc_library") +load("@rules_verilog//verilog:defs.bzl", "verilog_library") +load("//tools/lib_to_verilog:lib_to_verilog.bzl", "stdcell_verilog") + +package(default_visibility = ["//visibility:private"]) + +stdcell_verilog( + name = "asap7_stdcell", + lefs = ["@orfs//flow:platforms/asap7/lef/asap7sc7p5t_28_R_1x_220121a.lef"], + libs = [ + "@orfs//flow:platforms/asap7/lib/NLDM/asap7sc7p5t_AO_RVT_TT_nldm_211120.lib.gz", + "@orfs//flow:platforms/asap7/lib/NLDM/asap7sc7p5t_INVBUF_RVT_TT_nldm_220122.lib.gz", + "@orfs//flow:platforms/asap7/lib/NLDM/asap7sc7p5t_OA_RVT_TT_nldm_211120.lib.gz", + "@orfs//flow:platforms/asap7/lib/NLDM/asap7sc7p5t_SEQ_RVT_TT_nldm_220123.lib", + "@orfs//flow:platforms/asap7/lib/NLDM/asap7sc7p5t_SIMPLE_RVT_TT_nldm_211120.lib.gz", + ], +) + +[ + [ + genrule( + name = "regfile_%s_v" % style, + srcs = [ + ":regfile.regfile", + "@orfs//flow/designs/src/riscv32i:regfile.v", + "@orfs//flow:platforms/asap7/lef/asap7_tech_1x_201209.lef", + "@orfs//flow:platforms/asap7/lef/asap7sc7p5t_28_R_1x_220121a.lef", + ], + outs = ["regfile_%s.v" % style], + cmd = """ +grep -v '^write_style' $(location :regfile.regfile) > $(@D)/regfile_{style}.regfile +{extra} +cat > $(@D)/regfile_{style}.tcl < $(@D)/regfile_{style}.log 2>&1 || {{ cat $(@D)/regfile_{style}.log; exit 1; }} +sed 's/^module regfile(/module regfile_gen(/' $(@D)/regfile_{style}.gen.v > $@ +""".format( + extra = "echo 'write_style clock_gate' >> $(@D)/regfile_%s.regfile" % style if style == "clock_gate" else "", + style = style, + ), + tools = ["@openroad"], + ), + verilog_library( + name = "tb_%s_rtl" % style, + srcs = [ + "tb_top.v", + ":regfile_%s.v" % style, + ":asap7_stdcell.v", + ":asap7_stdcell_empty.v", + "@orfs//flow/designs/src/riscv32i:regfile.v", + ], + ), + verilator_cc_library( + name = "tb_%s" % style, + module = ":tb_%s_rtl" % style, + module_top = "tb_top", + trace_mode = "none", + vopts = [ + "-Wno-fatal", + "--x-initial", + "0", + ], + ), + cc_test( + name = "regfile_%s_sim_test" % style, + size = "small", + srcs = ["tb_main.cc"], + deps = [":tb_%s" % style], + ), + ] + for style in [ + "mux", + "clock_gate", + ] +] diff --git a/test/regfile/sim/regfile.regfile b/test/regfile/sim/regfile.regfile new file mode 100644 index 000000000..04120ccd6 --- /dev/null +++ b/test/regfile/sim/regfile.regfile @@ -0,0 +1,17 @@ +# riscv32i's `regfile` (src/riscv32i/regfile.v) as generate_regfile builds +# it, for simulation against that RTL: 32 x 32, two combinational reads, +# one write, x0 reads 0. BUILD.bazel appends the write style. +module regfile +words 32 +bits 32 +clock clk +read ra1 rd1 +read ra2 rd2 +write wa3 wd3 we3 +zero_word 0 +cell icg ICGx1_ASAP7_75t_R +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 diff --git a/test/regfile/sim/tb_main.cc b/test/regfile/sim/tb_main.cc new file mode 100644 index 000000000..5de7d4673 --- /dev/null +++ b/test/regfile/sim/tb_main.cc @@ -0,0 +1,60 @@ +// Random reads and writes on riscv32i's register file and on the netlist +// generate_regfile writes for it; every read of both ports compared. +// Every word is written once first, unchecked: an unwritten word reads +// what each side's power-up state happens to be (the generated flops +// hold the inverse of the value). +#include +#include +#include + +#include "Vtb_top.h" + +namespace { + +int Check(Vtb_top& tb, int cycle) { + tb.eval(); + if (tb.rd1_rtl != tb.rd1_gen || tb.rd2_rtl != tb.rd2_gen) { + std::printf( + "cycle %d: ra1 %u ra2 %u: rtl %08x %08x, generated %08x %08x\n", + cycle, tb.ra1, tb.ra2, tb.rd1_rtl, tb.rd2_rtl, tb.rd1_gen, + tb.rd2_gen); + return 1; + } + return 0; +} + +// One clock: inputs set while the clock is low, the write on its rising +// edge; with `check`, both reads compared before and after it. +int Cycle(Vtb_top& tb, int cycle, bool check, bool we, int wa, uint32_t wd, + int ra1, int ra2) { + tb.clk = 0; + tb.we3 = we; + tb.wa3 = wa; + tb.wd3 = wd; + tb.ra1 = ra1; + tb.ra2 = ra2; + tb.eval(); + int bad = check ? Check(tb, cycle) : 0; + tb.clk = 1; + tb.eval(); + bad += check ? Check(tb, cycle) : 0; + return bad; +} + +} // namespace + +int main() { + Vtb_top tb; + std::mt19937 rng(1); + int bad = 0; + int cycle = 0; + for (int w = 0; w < 32; ++w, ++cycle) { + Cycle(tb, cycle, false, true, w, rng(), w, w); + } + for (int i = 0; i < 20000 && bad < 10; ++i, ++cycle) { + bad += Cycle(tb, cycle, true, rng() % 2, rng() % 32, rng(), rng() % 32, + rng() % 32); + } + std::printf("%d cycles, %d mismatches\n", cycle, bad); + return bad == 0 ? 0 : 1; +} diff --git a/test/regfile/sim/tb_top.v b/test/regfile/sim/tb_top.v new file mode 100644 index 000000000..78a38e7bb --- /dev/null +++ b/test/regfile/sim/tb_top.v @@ -0,0 +1,23 @@ +// riscv32i's register file and the netlist generate_regfile writes for +// it, on the same inputs, both reads of each side out. +module tb_top ( + input clk, + input we3, + input [4:0] ra1, + input [4:0] ra2, + input [4:0] wa3, + input [31:0] wd3, + output [31:0] rd1_rtl, + output [31:0] rd2_rtl, + output [31:0] rd1_gen, + output [31:0] rd2_gen +); + regfile rtl ( + .clk(clk), .we3(we3), .ra1(ra1), .ra2(ra2), .wa3(wa3), .wd3(wd3), + .rd1(rd1_rtl), .rd2(rd2_rtl) + ); + regfile_gen gen ( + .clk(clk), .we3(we3), .ra1(ra1), .ra2(ra2), .wa3(wa3), .wd3(wd3), + .rd1(rd1_gen), .rd2(rd2_gen) + ); +endmodule diff --git a/test/retired_arguments_test.bzl b/test/retired_arguments_test.bzl new file mode 100644 index 000000000..e570fe05e --- /dev/null +++ b/test/retired_arguments_test.bzl @@ -0,0 +1,24 @@ +"""analysistest: a retired variable is refused, naming what replaced it. + +STRUCTURED_MEMORIES and STRUCTURED_PLACEMENT were bazel-orfs's own ORFS +patches, retired for AUTO_MEMORIES_REGFILES and AUTO_MEMORIES_MACRO_PLACE. +ORFS ignores a variable it does not know, so a flow that still set one +would build, without the register files it asked for. The refusal is a +fail() in the stage rules, so it holds for orfs_flow and a standalone +stage alike. +""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") + +def _refused_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, ctx.attr.message) + return analysistest.end(env) + +retired_argument_test = analysistest.make( + _refused_impl, + attrs = { + "message": attr.string(mandatory = True), + }, + expect_failure = True, +) diff --git a/test/structured_netlist/BUILD.bazel b/test/structured_netlist/BUILD.bazel index ff499fc43..2ab10aa61 100644 --- a/test/structured_netlist/BUILD.bazel +++ b/test/structured_netlist/BUILD.bazel @@ -1,53 +1,9 @@ -load("@bazel-orfs//:openroad.bzl", "odb_debug", "orfs_flow") -load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_python//python:defs.bzl", "py_binary") load(":scaffold.bzl", "array_scaffold") -# STRUCTURED_MEMORIES in mode netlist: a generated register file dropped -# FIRM into its parent's rows at floorplan instead of hardened as a macro -# (patch 0078). The parent keeps one placement, clock tree and route -# across it. small_top_place is the check: the file's cells are in the -# placed design, FIRM, at the location placement.txt gives. -orfs_flow( - name = "small_top", - arguments = { - "AUTO_MEMORIES": "1", - "CORE_AREA": "10 10 190 190", - "DIE_AREA": "0 0 200 200", - "PLACE_DENSITY": "0.5", - "SKIP_REPORT_METRICS": "1", - }, - last_stage = "grt", - pdk = "//flow:asap7", - sources = { - "SDC_FILE": [":constraints.sdc"], - "STRUCTURED_MEMORIES": [":SmallFile.regfile"], - "STRUCTURED_PLACEMENT": [":placement.txt"], - }, - verilog_files = [":small_file.sv"], -) - -odb_debug( - name = "small_top_place_odb_debug", - src = ":small_top_place", -) - -odb_debug( - name = "small_top_grt_odb_debug", - src = ":small_top_grt", -) - -# The battery's first negative: a placed netlist gets through repair_design -# (its cells dont_touch, patch 0084), CTS (its flops rewired, its buffers -# seated in the service columns), the legaliser and global route, with one -# dead column skipped at floorplan (0082) and no macro placer run on a -# fully fixed macro set (0083). Seconds, where the parent took a night. -build_test( - name = "small_top_grt_build_test", - targets = [":small_top_grt"], -) - -# XiangShan's three placed netlists at real size, each in a parent of flops +# The smallest register file is //test/regfile's regfile_top. +# +# XiangShan's three register files at real size, each in a parent of flops # (scaffold_gen.py, scaffold.bzl): the rung between the 30 um files of the # miniature planned parent and the parent itself. A 305 um array grows # nets the resizer wants to buffer, which the small files never do @@ -82,3 +38,9 @@ array_scaffold( spec = XS_SPECS + "IntRegFile.regfile", width_um = 406.404, ) + +# generate_regfile's golden case (tools/structured_gen:golden_test). +exports_files( + ["SmallFile.regfile"], + visibility = ["//tools/structured_gen:__pkg__"], +) diff --git a/test/structured_netlist/SmallFile.regfile b/test/structured_netlist/SmallFile.regfile index f626b1e64..f54269247 100644 --- a/test/structured_netlist/SmallFile.regfile +++ b/test/structured_netlist/SmallFile.regfile @@ -1,5 +1,5 @@ -# SmallFile as a placed netlist: generated, then dropped FIRM into -# small_top's rows at floorplan (see STRUCTURED_PLACEMENT in the BUILD). +# SmallFile, the 16 x 8 2R2W register file of the golden test +# (tools/structured_gen:golden_test), in mode netlist. module SmallFile mode netlist words 16 diff --git a/test/structured_netlist/placement.txt b/test/structured_netlist/placement.txt deleted file mode 100644 index 5ac3a213e..000000000 --- a/test/structured_netlist/placement.txt +++ /dev/null @@ -1,2 +0,0 @@ -# : the array's lower-left corner -SmallFile u_file 20 20 diff --git a/test/structured_netlist/scaffold.bzl b/test/structured_netlist/scaffold.bzl index f8e14d4f2..cac790bcf 100644 --- a/test/structured_netlist/scaffold.bzl +++ b/test/structured_netlist/scaffold.bzl @@ -11,7 +11,8 @@ def _r(x): return int(x * 1000 + 0.5) / 1000.0 def array_scaffold(name, spec, width_um, height_um): - """The scaffold flow for one spec: _scaffold_, to global route. + """The scaffold flow for one spec: _scaffold_, to CTS: a smoke test that + the array dissolves and CTS seats its buffers; nothing reads a route. Args: name: the module the spec names (RenameBufferFile, ...). @@ -25,8 +26,8 @@ def array_scaffold(name, spec, width_um, height_um): native.genrule( name = top + "_rtl", srcs = [spec], - outs = [top + ".sv", top + "_placement.txt"], - cmd = "$(execpath :scaffold_gen) --spec $(location {}) --sv $(location {}.sv) --placement $(location {}_placement.txt) --corner '{} {}'".format( + outs = [top + ".sv", top + "_macro_placement.tcl"], + cmd = "$(execpath :scaffold_gen) --spec $(location {}) --sv $(location {}.sv) --macro-placement $(location {}_macro_placement.tcl) --corner '{} {}'".format( spec, top, top, @@ -43,15 +44,16 @@ def array_scaffold(name, spec, width_um, height_um): "AUTO_MEMORIES": "1", "CORE_AREA": "{} {} {} {}".format(CORE_UM, CORE_UM, _r(die_w - CORE_UM), _r(die_h - CORE_UM)), "DIE_AREA": "0 0 {} {}".format(_r(die_w), _r(die_h)), + "AUTO_MEMORIES_MACRO_PLACE": name, "PLACE_DENSITY": "0.5", "SYNTH_HDL_FRONTEND": "slang", }, - last_stage = "grt", + last_stage = "cts", pdk = "//flow:asap7", sources = { "SDC_FILE": [":constraints.sdc"], - "STRUCTURED_MEMORIES": [spec], - "STRUCTURED_PLACEMENT": [":" + top + "_placement.txt"], + "AUTO_MEMORIES_REGFILES": [spec], + "MACRO_PLACEMENT_TCL": [":" + top + "_macro_placement.tcl"], }, verilog_files = [":" + top + ".sv"], ) diff --git a/test/structured_netlist/scaffold_gen.py b/test/structured_netlist/scaffold_gen.py index f4c7dfc32..ed393eb17 100644 --- a/test/structured_netlist/scaffold_gen.py +++ b/test/structured_netlist/scaffold_gen.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 """The scaffold around one register-file spec: a parent of flops, at real size. - scaffold_gen.py --spec RenameBufferFile.regfile --sv out.sv --placement out.txt - -Reads a structured_gen spec (mode netlist) and writes the behavioural model -of its module in the port shape the generator gives it, and a parent that -registers every pin: one flop per address, data and enable bit, the read -data xor-reduced into a sum. The flow blackboxes the model and links the -generated netlist in its place, so the parent's place, CTS and route see + scaffold_gen.py --spec RenameBufferFile.regfile --sv out.sv --macro-placement out.tcl + +Reads a register-file spec (mode netlist) and writes the behavioural model +of its module in the port shape the generator gives it, a parent that +registers every pin -- one flop per address, data and enable bit, the read +data xor-reduced into a sum -- and the MACRO_PLACEMENT_TCL that puts the +file's macro inside the ring of flops. The flow blackboxes the model and +dissolves the generated macro into its cells, so the parent's place, CTS +and route see the real array with the real cell count, minutes after the spec changes. What the miniature planned parent cannot show: its files are 30 um wide and never grow a net the resizer wants to buffer; XiangShan's 305 um @@ -140,7 +142,7 @@ def main(argv): ) ap.add_argument("--spec", required=True) ap.add_argument("--sv", required=True) - ap.add_argument("--placement", required=True) + ap.add_argument("--macro-placement", required=True) ap.add_argument( "--top", default=None, help="parent module name (default _scaffold)" ) @@ -156,11 +158,11 @@ def main(argv): f.write(model(spec)) f.write("\n") f.write(parent(spec, top, "u_file")) - with open(a.placement, "w") as f: + with open(a.macro_placement, "w") as f: + f.write("# %s's lower-left corner in the parent, um\n" % spec["module"]) f.write( - "# : the array's lower-left corner\n" + "place_macro -macro_name u_file -location {%s} -orientation R0\n" % a.corner ) - f.write("%s u_file %s\n" % (spec["module"], a.corner)) print("%s: %d ports, parent %s" % (spec["module"], len(ports(spec)), top)) diff --git a/test/structured_netlist/small_file.sv b/test/structured_netlist/small_file.sv deleted file mode 100644 index b610d6fab..000000000 --- a/test/structured_netlist/small_file.sv +++ /dev/null @@ -1,62 +0,0 @@ -// A 16 x 8 register file with two read and two write ports, in the port -// shape utils.RegVecFile gives XiangShan's files: what STRUCTURED_MEMORIES -// replaces. Behavioural, for simulation; the flow never synthesises it. -module SmallFile( - input clock, - input [3:0] io_readPorts_0_addr, - output [7:0] io_readPorts_0_data, - input [3:0] io_readPorts_1_addr, - output [7:0] io_readPorts_1_data, - input io_writePorts_0_wen, - input [3:0] io_writePorts_0_addr, - input [7:0] io_writePorts_0_data, - input io_writePorts_1_wen, - input [3:0] io_writePorts_1_addr, - input [7:0] io_writePorts_1_data -); - reg [7:0] mem [0:15]; - always @(posedge clock) begin - if (io_writePorts_0_wen) mem[io_writePorts_0_addr] <= io_writePorts_0_data; - if (io_writePorts_1_wen) mem[io_writePorts_1_addr] <= io_writePorts_1_data; - end - assign io_readPorts_0_data = mem[io_readPorts_0_addr]; - assign io_readPorts_1_data = mem[io_readPorts_1_addr]; -endmodule - -// The parent: registers around one file, some logic of its own. -module small_top( - input clock, - input reset, - 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 - SmallFile u_file( - .clock(clock), - .io_readPorts_0_addr(ra0_q), .io_readPorts_0_data(d0), - .io_readPorts_1_addr(ra1_q), .io_readPorts_1_data(d1), - .io_writePorts_0_wen(we0_q), .io_writePorts_0_addr(wa0_q), .io_writePorts_0_data(wd0_q), - .io_writePorts_1_wen(we1_q), .io_writePorts_1_addr(wa1_q), .io_writePorts_1_data(wd1_q) - ); - // Bit 0 of the file is never read: a dead column, which the synth ODB - // step's eliminate_dead_logic removes from the placed netlist. The - // parent's placement script has to skip those cells (FLW-0008) rather - // than stop on the first one, as it did on XiangShan's RobEntryFile. - 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/tools/macro_select/plan_floorplan.py b/tools/macro_select/plan_floorplan.py index 94c40e5ac..f205cba16 100644 --- a/tools/macro_select/plan_floorplan.py +++ b/tools/macro_select/plan_floorplan.py @@ -82,11 +82,12 @@ def load_plan(path): "w_um": 318.4, "h_um": 192.5}, ...] } - netlists (optional) are the generated arrays dropped FIRM into the - parent (STRUCTURED_MEMORIES in mode netlist): the planner lays them - in a row along the top of the logic region, left to right, a gap - apart, and emits their corners as netlists.txt for - STRUCTURED_PLACEMENT; an entry may fix its own x_um and y_um instead. + netlists (optional) are the parent's generated register files + (AUTO_MEMORIES_REGFILES in mode netlist), macros until they dissolve + into their cells after the macro step: the planner lays them in a row + along the top of the logic region, left to right, a gap apart, and + place_macros.tcl places each by its instance; an entry may fix its + own x_um and y_um instead. pin_partners (optional) is probe_pin_partners.tcl's dump, one "pin " line per block pin, partner a block @@ -116,7 +117,9 @@ def load_plan(path): plan = json.load(f) partners = plan.get("pin_partners") if partners and not os.path.isabs(partners): - plan["pin_partners"] = os.path.join(os.path.dirname(os.path.abspath(path)), partners) + plan["pin_partners"] = os.path.join( + os.path.dirname(os.path.abspath(path)), partners + ) return plan @@ -163,7 +166,11 @@ def shape(macro, tech, margins, core_margin=0.0): if content and density: # (f side - 2 m)(f depth - 2 m) = content / density: the core keeps # its absolute margins when the die scales - a, b, c = side * depth, -2.0 * core_margin * (side + depth), (2.0 * core_margin) ** 2 - content / density + a, b, c = ( + side * depth, + -2.0 * core_margin * (side + depth), + (2.0 * core_margin) ** 2 - content / density, + ) mock = (-b + math.sqrt(b * b - 4.0 * a * c)) / (2.0 * a) mock = min(1.0, max(mock, need / side)) channel = max( @@ -212,7 +219,9 @@ def _extents(placed): } -def assign_sides(shapes, region_area, gap, margin, min_w=0.0, min_h=0.0, aspect_max=None): +def assign_sides( + shapes, region_area, gap, margin, min_w=0.0, min_h=0.0, aspect_max=None +): """Macros to the region's sides so the die is smallest. Every assignment is tried for up to eight macros (65536 cases), the @@ -237,7 +246,9 @@ def assign_sides(shapes, region_area, gap, margin, min_w=0.0, min_h=0.0, aspect_ c //= 4 if not held: continue - w, h, dw, dh = _die_for(placed, region_area, _extents(placed), gap, margin, min_w, min_h) + w, h, dw, dh = _die_for( + placed, region_area, _extents(placed), gap, margin, min_w, min_h + ) # the region's elongation, when capped: an assignment over the # cap loses to any under it, and among themselves by die area aspect = max(w, h) / min(w, h) @@ -246,10 +257,16 @@ def assign_sides(shapes, region_area, gap, margin, min_w=0.0, min_h=0.0, aspect_ best = ((over, dw * dh), placed, w, h) else: placed = {s: [] for s in SIDES} - for sh in sorted(shapes, key=lambda s: (s.get("region_side") is None, -s["pin_side_um"])): - side = sh.get("region_side") or min(SIDES, key=lambda s: _span(placed[s], gap)) + for sh in sorted( + shapes, key=lambda s: (s.get("region_side") is None, -s["pin_side_um"]) + ): + side = sh.get("region_side") or min( + SIDES, key=lambda s: _span(placed[s], gap) + ) placed[side].append(sh) - w, h, dw, dh = _die_for(placed, region_area, _extents(placed), gap, margin, min_w, min_h) + w, h, dw, dh = _die_for( + placed, region_area, _extents(placed), gap, margin, min_w, min_h + ) best = ((0, dw * dh), placed, w, h) return best[1], best[2], best[3] @@ -269,7 +286,12 @@ def layout(plan): region_area = parent["cell_area_um2"] / parent["density"] min_w, min_h = netlist_row(plan) placed, region_w, region_h = assign_sides( - shapes, region_area, gap, parent["core_margin_um"], min_w, min_h, + shapes, + region_area, + gap, + parent["core_margin_um"], + min_w, + min_h, margins.get("region_aspect_max"), ) extent = _extents(placed) @@ -308,7 +330,9 @@ def layout(plan): m = dict(sh) x, y = _snap(x, y, tech) own_s, own_d = sh["own_side_um"], sh["own_depth_um"] - own_w, own_h = (own_s, own_d) if side in ("bottom", "top") else (own_d, own_s) + own_w, own_h = ( + (own_s, own_d) if side in ("bottom", "top") else (own_d, own_s) + ) m.update( { "region_side": side, @@ -726,6 +750,20 @@ def place_exact(m, segs, tech): incr bands }} puts "place_macros.tcl: $bands soft placement blockage(s) over the blocks' bands; the rows stay" +# The plan's register files (AUTO_MEMORIES_REGFILES in mode netlist) are +# macros until the end of this step, each found by its instance and placed +# where the plan put it; they dissolve into their cells after it. +foreach {{inst x y}} {{ +{netlists} +}} {{ + set i [$block findInst $inst] + if {{ $i eq "NULL" }} {{ + utl::error FLW 2 "plan: no register file instance $inst" + }} + place_macro -macro_name $inst -location [list $x $y] -orientation R0 -exact + $i setPlacementStatus FIRM + puts "place_macros.tcl: register file $inst at $x $y um, R0" +}} # Macros the plan does not name (the parent's own generated register files # and memories) go around the planned blocks, which are FIRM and stay put. set rest {{}} @@ -734,42 +772,7 @@ def place_exact(m, segs, tech): }} if {{ [llength $rest] > 0 }} {{ puts "place_macros.tcl: [llength $rest] macros not in the plan; rtl_macro_placer places them around the planned blocks" - # rtl_macro_placer refuses a FIRM standard cell in its area (MPL-0050), - # and the plan's placed netlists are FIRM from floorplan on. For the - # macro step only they are PLACED, and a hard placement blockage over - # each netlist's cells, which rtl_macro_placer keeps macros off, stands - # in for them; after it, both are undone. - set firm {{}} - array set box {{}} - foreach inst [$block getInsts] {{ - if {{ [[$inst getMaster] isBlock] || [$inst getPlacementStatus] ne "FIRM" }} {{ continue }} - lappend firm [list $inst [$inst getLocation] [$inst getOrient]] - set key [file dirname [$inst getName]] - set b [$inst getBBox] - if {{ [info exists box($key)] }} {{ - lassign $box($key) x0 y0 x1 y1 - set box($key) [list [expr {{ min($x0, [$b xMin]) }}] [expr {{ min($y0, [$b yMin]) }}] [expr {{ max($x1, [$b xMax]) }}] [expr {{ max($y1, [$b yMax]) }}]] - }} else {{ - set box($key) [list [$b xMin] [$b yMin] [$b xMax] [$b yMax]] - }} - }} - set stand_ins {{}} - foreach key [array names box] {{ - lassign $box($key) x0 y0 x1 y1 - lappend stand_ins [odb::dbBlockage_create $block $x0 $y0 $x1 $y1] - }} - foreach f $firm {{ [lindex $f 0] setPlacementStatus PLACED }} - puts "place_macros.tcl: [llength $firm] FIRM netlist cells stand aside for the macro step, [llength $stand_ins] blockage(s) in their place" rtl_macro_placer -halo_width 2 -halo_height 2 - # rtl_macro_placer moves a cluster's standard cells to one point: put - # every netlist cell back where the plan put it - foreach f $firm {{ - lassign $f inst loc orient - $inst setOrient $orient - $inst setLocation {{*}}$loc - $inst setPlacementStatus FIRM - }} - foreach bl $stand_ins {{ odb::dbBlockage_destroy $bl }} }} """ @@ -968,7 +971,9 @@ def write(name, text): n=len(pins), side=sd, region=( - "*" if lo is None else "{:.3f}-{:.3f}".format(lo, hi) + "*" + if lo is None + else "{:.3f}-{:.3f}".format(lo, hi) ), pins=" ".join(pins), ) @@ -996,7 +1001,9 @@ def write(name, text): if text is not None: write(pm["name"] + suffix + "_pins.tcl", text) if not suffix: - m.update({k: pm[k] for k in ("pin_segments", "pins_placed_exact") if k in pm}) + m.update( + {k: pm[k] for k in ("pin_segments", "pins_placed_exact") if k in pm} + ) w, h = m["own_w_um"], m["own_h_um"] entry = { "DIE_AREA": _area(0, 0, w, h), @@ -1012,21 +1019,19 @@ def write(name, text): entry["SYNTH_KEEP_MODULES"] = " ".join(keep) bzl["macros"][m["name"]] = entry rows.append(" {} {:.3f} {:.3f}".format(m["name"], m["x_um"], m["y_um"])) + netlists = place_netlists(out, plan) write( "place_macros.tcl", - PLACE_TCL.format(rows="\n".join(rows), bands="\n".join(block_bands(out))), + PLACE_TCL.format( + rows="\n".join(rows), + bands="\n".join(block_bands(out)), + netlists="".join( + " {} {:.3f} {:.3f}\n".format(i_, x, y) + for m_, i_, x, y, w, h in netlists + ).rstrip("\n"), + ), ) - netlists = place_netlists(out, plan) if netlists: - write( - "netlists.txt", - "# Generated by plan_floorplan.py: STRUCTURED_PLACEMENT, the parent's\n" - "# placed netlists' lower-left corners in um: \n" - + "".join( - "{} {} {:.3f} {:.3f}\n".format(m_, i_, x, y) - for m_, i_, x, y, w, h in netlists - ), - ) bzl["parent"]["netlists"] = [ {"module": m_, "instance": i_, "x_um": x, "y_um": y, "w_um": w, "h_um": h} for m_, i_, x, y, w, h in netlists diff --git a/tools/macro_select/plan_floorplan_test.py b/tools/macro_select/plan_floorplan_test.py index c16308fe2..b90ee553a 100644 --- a/tools/macro_select/plan_floorplan_test.py +++ b/tools/macro_select/plan_floorplan_test.py @@ -314,7 +314,6 @@ def test_emit_places_exactly_when_the_tech_has_layers(self): ) - class MockFootprintTest(unittest.TestCase): """A block's outline is its own flow's; the parent places its footprint, the outline scaled so its core holds the measured content at @@ -362,7 +361,12 @@ def test_footprint_for_the_parent_outline_for_the_block(self): # need more side core = (m["w_um"] - 20) * (m["h_um"] - 20) content = plan_floorplan.plan_macro(p, m["name"])["content_um2"] - need = m["pins"] * TECH["pin_pitch_um"] * MARGINS["pin_side"] / TECH["pin_layers"] + need = ( + m["pins"] + * TECH["pin_pitch_um"] + * MARGINS["pin_side"] + / TECH["pin_layers"] + ) if m["two_sides"]: need /= 2.0 if f > need / m["own_side_um"] + 1e-3: @@ -376,7 +380,9 @@ def rows(name): text = open(os.path.join(d, name)).read() return [l.split() for l in text.splitlines() if l.startswith(" a")] - own, mock = rows(target["name"] + "_pins.tcl"), rows(target["name"] + "_mock_pins.tcl") + own, mock = rows(target["name"] + "_pins.tcl"), rows( + target["name"] + "_mock_pins.tcl" + ) self.assertEqual(len(own), 40) self.assertEqual([r[0] for r in own], [r[0] for r in mock]) # same order for r in own: @@ -389,7 +395,12 @@ def rows(name): def test_pins_floor_the_footprint(self): m = {"name": "Wide", "pins": 13000, "area_um2": 400000, "content_um2": 1000} sh = plan_floorplan.shape(m, LATTICE, dict(MARGINS, block_density=0.6), 10) - need = 13000 * LATTICE["pin_pitch_um"] * MARGINS["pin_side"] / LATTICE["pin_layers"] + need = ( + 13000 + * LATTICE["pin_pitch_um"] + * MARGINS["pin_side"] + / LATTICE["pin_layers"] + ) if sh["two_sides"]: need /= 2.0 self.assertGreaterEqual(sh["fp_side_um"] + 0.01, need) @@ -403,15 +414,20 @@ class HeldSideTest(unittest.TestCase): def test_region_side_holds(self): free = plan_floorplan.layout(plan([dict(m) for m in LayoutTest.MACROS])) sides = {m["name"]: m["region_side"] for m in free["macros"]} - held = [dict(m, region_side=OPPOSITE[sides[m["name"]]]) for m in LayoutTest.MACROS] + held = [ + dict(m, region_side=OPPOSITE[sides[m["name"]]]) for m in LayoutTest.MACROS + ] out = plan_floorplan.layout(plan(held)) for m in out["macros"]: self.assertEqual(m["region_side"], OPPOSITE[sides[m["name"]]]) - self.assertGreaterEqual(out["die_area_mm2"], free["die_area_mm2"] - 1e-9) # the free choice is the smallest + self.assertGreaterEqual( + out["die_area_mm2"], free["die_area_mm2"] - 1e-9 + ) # the free choice is the smallest OPPOSITE = {"top": "bottom", "bottom": "top", "left": "right", "right": "left"} + class MirroredOrderTest(unittest.TestCase): def test_both_ends_of_an_interface_share_one_order(self): d = tempfile.mkdtemp(prefix="plan_mirror.") @@ -456,8 +472,8 @@ def test_netlists_in_a_row_along_the_region_top(self): self.assertEqual(rows[2][2:4], (50.0, 60.0)) d = tempfile.mkdtemp(prefix="plan_netlists.") plan_floorplan.emit(out, p, d) - text = open(os.path.join(d, "netlists.txt")).read() - self.assertIn("A top/a %.3f %.3f" % (rows[0][2], rows[0][3]), text) + text = open(os.path.join(d, "place_macros.tcl")).read() + self.assertIn(" top/a %.3f %.3f\n" % (rows[0][2], rows[0][3]), text) bzl = open(os.path.join(d, "plan.bzl")).read() self.assertIn('"netlists"', bzl) @@ -552,7 +568,9 @@ def test_per_macro_pin_side_margin_and_channel(self): out = plan_floorplan.layout(plan(macros)) by = {m["name"]: m for m in out["macros"]} was = {m["name"]: m for m in base["macros"]} - self.assertGreater(by["MemBlock"]["pin_side_um"], was["MemBlock"]["pin_side_um"]) + self.assertGreater( + by["MemBlock"]["pin_side_um"], was["MemBlock"]["pin_side_um"] + ) self.assertEqual(by["MemBlock"]["channel_um"], 150) for n in ("Frontend", "VecRegion", "FpRegion"): self.assertEqual(by[n]["pin_side_um"], was[n]["pin_side_um"]) diff --git a/tools/structured_gen/BUILD.bazel b/tools/structured_gen/BUILD.bazel index 38971ee55..707bc185c 100644 --- a/tools/structured_gen/BUILD.bazel +++ b/tools/structured_gen/BUILD.bazel @@ -2,6 +2,7 @@ # OpenROAD's own libraries rather than emitted as text for a later flow # to place. See README.md. load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") +load("@rules_shell//shell:sh_test.bzl", "sh_test") package(default_visibility = ["//visibility:public"]) @@ -48,3 +49,54 @@ cc_test( data = ASAP7_LEFS, deps = [":regfile"], ) + +# OpenROAD's generate_regfile (patch 0010) builds every spec and writes +# all its views. It was byte for byte this tool's output +# until the read gates and the clock gate moved it on. The tiny synthetic +# specs in golden/ cover every spec feature (banks, bank columns, bit +# folds, registered read with the RTL's names, banked and interleaved +# reads, the zero word, netlist mode) in seconds. +GOLDEN_TINY = glob(["golden/*.regfile"]) + +# The register-file specs the flow uses, at real size: minutes per spec. +GOLDEN_XIANGSHAN = [ + "//test/coremark_joule/designs/asap7/xiangshan:%s.regfile" % m + for m in [ + "FpRegFile", + "FtqEntryQueue", + "FtqMetaQueueCommit", + "FtqMetaQueueRedirect", + "FtqMetaQueueResolve", + "IntRegFile", + "RenameBufferFile", + "RobEntryFile", + "VfRegFile", + ] +] + ["//test/structured_netlist:SmallFile.regfile"] + +[ + sh_test( + name = name, + size = size, + srcs = ["golden_test.sh"], + args = [ + "$(rootpath @openroad//:openroad)", + ] + ["$(rootpath {})".format(l) for l in ASAP7_LEFS] + [ + "$(rootpath {})".format(s) + for s in specs + ], + data = [ + "@openroad", + ] + ASAP7_LEFS + specs, + tags = tags, + ) + for name, size, specs, tags in [ + ("golden_test", "small", GOLDEN_TINY, []), + ( + "golden_xiangshan_test", + "enormous", + GOLDEN_XIANGSHAN, + ["manual"], + ), + ] +] diff --git a/tools/structured_gen/golden/banked.regfile b/tools/structured_gen/golden/banked.regfile new file mode 100644 index 000000000..6003b684e --- /dev/null +++ b/tools/structured_gen/golden/banked.regfile @@ -0,0 +1,21 @@ +# Banked and interleaved reads, a zero word (x0), banks stacked two by two. +module GoldenBanked +mode netlist +words 16 +bits 3 +clock clock +read io_r0_addr io_r0_data +read_banked io_rb_addr_0 io_rb_data_0 io_rb_addr_1 io_rb_data_1 io_rb_addr_2 io_rb_data_2 io_rb_addr_3 io_rb_data_3 +write io_w0_addr io_w0_data io_w0_en +write io_w1_addr io_w1_data +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 +banks 4 +bank_columns 2 +bank_order interleaved +zero_word 0 diff --git a/tools/structured_gen/golden/folds.regfile b/tools/structured_gen/golden/folds.regfile new file mode 100644 index 000000000..d6ac64d3c --- /dev/null +++ b/tools/structured_gen/golden/folds.regfile @@ -0,0 +1,17 @@ +# A word folded into three bands of bits, the last one short. +module GoldenFolds +words 4 +bits 7 +clock clock +read io_r0_addr io_r0_data +write io_w0_addr io_w0_data io_w0_en +write io_w1_addr io_w1_data io_w1_en +write io_w2_addr io_w2_data io_w2_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 +bit_folds 3 diff --git a/tools/structured_gen/golden/netlist.regfile b/tools/structured_gen/golden/netlist.regfile new file mode 100644 index 000000000..83fbf4f91 --- /dev/null +++ b/tools/structured_gen/golden/netlist.regfile @@ -0,0 +1,19 @@ +# The same in netlist mode: the periphery left to the parent. +module GoldenNetlist +mode netlist +words 8 +bits 4 +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 2 +banks 2 diff --git a/tools/structured_gen/golden/plain.regfile b/tools/structured_gen/golden/plain.regfile new file mode 100644 index 000000000..9c9d36901 --- /dev/null +++ b/tools/structured_gen/golden/plain.regfile @@ -0,0 +1,18 @@ +# 8 x 4, two read and two write ports, two banks side by side, taps. +module GoldenPlain +words 8 +bits 4 +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 2 +banks 2 diff --git a/tools/structured_gen/golden/registered.regfile b/tools/structured_gen/golden/registered.regfile new file mode 100644 index 000000000..1cd8ff499 --- /dev/null +++ b/tools/structured_gen/golden/registered.regfile @@ -0,0 +1,21 @@ +# A registered read with the RTL's register names, as firtool writes +# XiangShan's datapath register files; a reset port the array ignores. +module GoldenRegistered +mode netlist +words 4 +bits 4 +clock clock +reset reset +read io_readPorts_0_addr io_readPorts_0_data +read io_readPorts_1_addr io_readPorts_1_data +write io_writePorts_0_addr io_writePorts_0_data io_writePorts_0_wen +read_latency 1 +store_name mem_{word}[{bit}] +read_reg_name io_readPorts_{port}_data_REG[{bit}] +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/tools/structured_gen/golden_test.sh b/tools/structured_gen/golden_test.sh new file mode 100755 index 000000000..97e69136f --- /dev/null +++ b/tools/structured_gen/golden_test.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# OpenROAD's generate_regfile (carried patch 0010) builds every spec here +# and writes all its views. The specs cover every spec feature; whether +# what they build computes the RTL's function is test/regfile/sim's to +# check. This tool's own output is no longer the reference: +# generate_regfile has moved past it (inverting read gates, the clock +# gate per word). +# golden_test.sh ... +set -euo pipefail + +OPENROAD="$1" TECH="$2" CELLS="$3" +shift 3 +OUT="${TEST_TMPDIR:-$(mktemp -d)}" +FAILS=0 +for spec in "$@"; do + name="$(basename "$spec" .regfile)" + b="$OUT/$name" + mkdir -p "$b" + cat >"$b/run.tcl" <"$b/log" 2>&1; then + echo "FAIL: $name: generate_regfile failed" + tail -3 "$b/log" + FAILS=$((FAILS + 1)) + continue + fi + for f in rf.v rf.def rf.lef rf.lib rf_pre_layout.lib; do + [ -s "$b/$f" ] || { echo "FAIL: $name: no $f"; FAILS=$((FAILS + 1)); } + done + echo "ok: $name" +done +[ "$FAILS" -eq 0 ] || { echo "$FAILS failed"; exit 1; } +echo "all built" diff --git a/tools/structured_gen/test/BUILD.bazel b/tools/structured_gen/test/BUILD.bazel index 0ade08b40..a7dd6aec2 100644 --- a/tools/structured_gen/test/BUILD.bazel +++ b/tools/structured_gen/test/BUILD.bazel @@ -61,14 +61,15 @@ build_test( ) # The same register file replaced *by name* inside a parent: the parent's -# RTL instantiates rf32x32, STRUCTURED_MEMORIES lists the spec, and the -# memories step runs the generator, writes the views beside the +# RTL instantiates rf32x32, AUTO_MEMORIES_REGFILES lists the spec, and the +# memories step runs generate_regfile, writes the views beside the # AUTO_MEMORIES ones and blackboxes the module. The floorplan then places # one macro. This is the hook the XiangShan register files use. orfs_flow( name = "rf_parent", arguments = { "AUTO_MEMORIES": "1", + "AUTO_MEMORIES_MACRO_PLACE": "rf32x32", # A die the 168 x 19 um macro fits in with room around it; a # utilisation-derived square would not (IFP-0002). "CORE_AREA": "2 2 238 98", @@ -81,7 +82,7 @@ orfs_flow( last_stage = "floorplan", sources = { "SDC_FILE": [":constraints.sdc"], - "STRUCTURED_MEMORIES": [":rf32x32.spec"], + "AUTO_MEMORIES_REGFILES": [":rf32x32.spec"], }, verilog_files = ["rf_parent.sv"], ) diff --git a/tools/structured_gen/test/rf_parent.sv b/tools/structured_gen/test/rf_parent.sv index 30636ae18..ddba54644 100644 --- a/tools/structured_gen/test/rf_parent.sv +++ b/tools/structured_gen/test/rf_parent.sv @@ -1,6 +1,6 @@ // A parent whose register file is replaced by name. rf32x32 is the RTL // the spec stands in for -- a Reg(Vec)-style file with one-hot writes -// -- and rf_parent is the logic around it. STRUCTURED_MEMORIES names the +// -- and rf_parent is the logic around it. AUTO_MEMORIES_REGFILES names the // spec; synthesis blackboxes rf32x32 and the generated macro takes its // place, and this RTL remains the simulation model. module rf32x32(