diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 8bc25c1d77..8bb0d2b088 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -25,7 +25,7 @@ into `$(RESULTS_DIR)/memories_inferred.json`, and | `$(RESULTS_DIR)/memories/.lib` | Generated Liberty view per converted memory. | | `$(RESULTS_DIR)/memories/_pre_layout.lib` | Ideal-clock (zero clock-tree insertion) variant for pre-CTS consumers that select lib files themselves. The Makefile flow uses `.lib` throughout. | | `$(RESULTS_DIR)/memories/.lef` | Abstract LEF per converted memory. | -| `$(RESULTS_DIR)/memories/blackboxes.txt` | Names of the converted modules — what synthesis blackboxes. | +| `$(RESULTS_DIR)/memories/blackboxes.txt` | ` ` per converted module — what synthesis blackboxes, and the area in um² its `.lib` states, which `SYNTH_MINIMUM_KEEP_SIZE` costs it by. | Synthesis (canonicalization) blackboxes the converted modules so the liberty view wins over their behavioral bodies; floorplan through final @@ -126,6 +126,124 @@ 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 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 +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: at parity on riscv32i, on a smaller core + +`designs/asap7/riscv32i-regfile` is riscv32i with its register file +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 | 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 **asap7 only.** `gen_memories.py` rejects any other platform. The @@ -187,3 +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 dfb6ab40f2..e5651085dc 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -103,6 +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_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.| | @@ -342,6 +344,8 @@ 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) - [DFF_LIB_FILE](#DFF_LIB_FILE) diff --git a/flow/Makefile b/flow/Makefile index 212474ba84..80a6348773 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -277,10 +277,15 @@ 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)) \ + $(foreach m,$(AUTO_MEMORIES_MACRO_PLACE),--macro-place $(m)) \ + $(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \ + $(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file)) $(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \ - $(wildcard $(SCRIPTS_DIR)/memories/*.py) + $(AUTO_MEMORIES_REGFILES) $(wildcard $(SCRIPTS_DIR)/memories/*.py) $(UNSET_AND_MAKE) do-auto-memories yosys-dependencies: $(RESULTS_DIR)/memories.json diff --git a/flow/designs/asap7/regfile/RegFile.regfile b/flow/designs/asap7/regfile/RegFile.regfile new file mode 100644 index 0000000000..b5f7ea063d --- /dev/null +++ b/flow/designs/asap7/regfile/RegFile.regfile @@ -0,0 +1,22 @@ +# RegFile as a register file of placed standard cells (generate_regfile). +# mode netlist: a macro through synthesis and macro placement, then its +# cells, the core FIRM where the macro was placed, the address decode +# left to placement. +module RegFile +mode netlist +words 16 +bits 8 +clock clock +read io_r0_addr io_r0_data +read io_r1_addr io_r1_data +write io_w0_addr io_w0_data io_w0_en +write io_w1_addr io_w1_data io_w1_en +cell flop DFFHQNx1_ASAP7_75t_R +cell and2 AND2x2_ASAP7_75t_R +cell or2 OR2x2_ASAP7_75t_R +cell ao22 AO22x2_ASAP7_75t_R +cell inv INVx1_ASAP7_75t_R +cell tap TAPCELL_ASAP7_75t_R +pin_layer M4 +tap_columns 8 +service_sites 40 diff --git a/flow/designs/asap7/regfile/config.mk b/flow/designs/asap7/regfile/config.mk new file mode 100644 index 0000000000..e0f1874d73 --- /dev/null +++ b/flow/designs/asap7/regfile/config.mk @@ -0,0 +1,20 @@ +# AUTO_MEMORIES_REGFILES in its smallest form: one register file, listed +# by spec, in a parent of flops. Seconds to global route. +export DESIGN_NICKNAME = regfile +export DESIGN_NAME = regfile_top +export PLATFORM = asap7 + +export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NICKNAME)/regfile.v +export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint.sdc + +export AUTO_MEMORIES = 1 +export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/RegFile.regfile +# Placed as a macro and dissolved after macro placement, the path this +# design exists to exercise; unlisted, it would be inlined. +export AUTO_MEMORIES_MACRO_PLACE = RegFile + +# The file's macro is 28.5 x 10 um; a core sized by utilization alone +# would be square and narrower than the macro and its halo. +export DIE_AREA = 0 0 60 45 +export CORE_AREA = 2 2 58 43 +export PLACE_DENSITY = 0.5 diff --git a/flow/designs/asap7/regfile/constraint.sdc b/flow/designs/asap7/regfile/constraint.sdc new file mode 100644 index 0000000000..00013668d2 --- /dev/null +++ b/flow/designs/asap7/regfile/constraint.sdc @@ -0,0 +1,7 @@ +set clk_name clock +set clk_port_name clock +set clk_period 1000 +set in2reg_max 800 +set reg2out_max 800 +set in2out_max 600 +source $env(PLATFORM_DIR)/constraints.sdc diff --git a/flow/designs/asap7/riscv32i-regfile/config.mk b/flow/designs/asap7/riscv32i-regfile/config.mk new file mode 100644 index 0000000000..142becabc6 --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/config.mk @@ -0,0 +1,11 @@ +# riscv32i with its register file built by AUTO_MEMORIES_REGFILES rather +# than synthesised to flip-flops: the same RTL, constraints and +# utilisation. Not in AUTO_MEMORIES_MACRO_PLACE, so the generated netlist +# is inlined in synthesis. Minutes to global route, the case to measure +# the generator on; docs/user/AutoMemories.md has the numbers. +export DESIGN_NICKNAME = riscv32i-regfile + +include designs/asap7/riscv32i/config.mk + +export AUTO_MEMORIES = 1 +export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/regfile.regfile diff --git a/flow/designs/asap7/riscv32i-regfile/io.tcl b/flow/designs/asap7/riscv32i-regfile/io.tcl new file mode 100644 index 0000000000..aee09d1e74 --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/io.tcl @@ -0,0 +1 @@ +exclude_io_pin_region -region left:* -region right:* -region top:* diff --git a/flow/designs/asap7/riscv32i-regfile/regfile.regfile b/flow/designs/asap7/riscv32i-regfile/regfile.regfile new file mode 100644 index 0000000000..45caaf857a --- /dev/null +++ b/flow/designs/asap7/riscv32i-regfile/regfile.regfile @@ -0,0 +1,19 @@ +# riscv32i's `regfile` (src/riscv32i/regfile.v) as a register file of +# standard cells, inlined in synthesis: 32 x 32, two combinational reads, +# one write through a clock gate per word, x0 reads 0 and drops its +# writes. +module regfile +words 32 +bits 32 +clock clk +read ra1 rd1 +read ra2 rd2 +write wa3 wd3 we3 +zero_word 0 +write_style clock_gate +cell icg ICGx1_ASAP7_75t_R +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/flow/designs/src/regfile/regfile.v b/flow/designs/src/regfile/regfile.v new file mode 100644 index 0000000000..43fdf1c675 --- /dev/null +++ b/flow/designs/src/regfile/regfile.v @@ -0,0 +1,60 @@ +// A 16 x 8 register file with two read ports and two write ports, +// behavioural, as RTL writes one. With AUTO_MEMORIES_REGFILES naming its +// spec the flow never synthesises this module: OpenROAD's +// generate_regfile builds it of placed standard cells instead. +module RegFile( + input clock, + input [3:0] io_r0_addr, + output [7:0] io_r0_data, + input [3:0] io_r1_addr, + output [7:0] io_r1_data, + input io_w0_en, + input [3:0] io_w0_addr, + input [7:0] io_w0_data, + input io_w1_en, + input [3:0] io_w1_addr, + input [7:0] io_w1_data +); + reg [7:0] mem [0:15]; + always @(posedge clock) begin + if (io_w0_en) mem[io_w0_addr] <= io_w0_data; + if (io_w1_en) mem[io_w1_addr] <= io_w1_data; + end + assign io_r0_data = mem[io_r0_addr]; + assign io_r1_data = mem[io_r1_addr]; +endmodule + +// The parent: registers around the file and some logic of its own. +// Bit 0 of the file is never read, so its column is dead logic the flow +// removes after the file dissolves into its cells. +module regfile_top( + input clock, + input [3:0] ra0, ra1, wa0, wa1, + input [7:0] wd0, wd1, + input we0, we1, + output [7:0] rd0, rd1, + output [7:0] sum +); + reg [3:0] ra0_q, ra1_q, wa0_q, wa1_q; + reg [7:0] wd0_q, wd1_q; + reg we0_q, we1_q; + wire [7:0] d0, d1; + always @(posedge clock) begin + ra0_q <= ra0; ra1_q <= ra1; wa0_q <= wa0; wa1_q <= wa1; + wd0_q <= wd0; wd1_q <= wd1; we0_q <= we0; we1_q <= we1; + end + RegFile u_rf( + .clock(clock), + .io_r0_addr(ra0_q), .io_r0_data(d0), + .io_r1_addr(ra1_q), .io_r1_data(d1), + .io_w0_en(we0_q), .io_w0_addr(wa0_q), .io_w0_data(wd0_q), + .io_w1_en(we1_q), .io_w1_addr(wa1_q), .io_w1_data(wd1_q) + ); + reg [6:0] rd0_q, rd1_q, sum_q; + always @(posedge clock) begin + rd0_q <= d0[7:1]; rd1_q <= d1[7:1]; sum_q <= d0[7:1] + d1[7:1]; + end + assign rd0 = {rd0_q, 1'b0}; + assign rd1 = {rd1_q, 1'b0}; + assign sum = {sum_q, 1'b0}; +endmodule diff --git a/flow/scripts/macro_place.tcl b/flow/scripts/macro_place.tcl index 0789129e42..2fca773e9c 100644 --- a/flow/scripts/macro_place.tcl +++ b/flow/scripts/macro_place.tcl @@ -7,6 +7,9 @@ source $::env(SCRIPTS_DIR)/macro_place_util.tcl source_step_tcl POST MACRO_PLACE +# register files in `mode netlist` become their cells +source $::env(SCRIPTS_DIR)/regfile_dissolve.tcl + report_design_area orfs_write_db $::env(RESULTS_DIR)/2_2_floorplan_macro.odb diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 9d179d7908..c9956ef5b7 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -10,8 +10,14 @@ /.lib Liberty view per converted memory /_pre_layout.lib ideal-clock variant for pre-CTS consumers /.lef abstract LEF per converted memory - /blackboxes.txt converted module names, one per line — - what synthesis blackboxes + /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 Everything downstream consumes these files; nothing else is passed between the generator and the flow. @@ -20,6 +26,8 @@ from __future__ import annotations import argparse +import json +import re import sys from pathlib import Path @@ -34,12 +42,252 @@ 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 _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 not named in AUTO_MEMORIES_MACRO_PLACE is inlined: its + generated netlist replaces the module's body in synthesis (inline.txt), + and from there on its cells are placed, sized and buffered with the + rest of the design. No macro, no abstract, no dissolve: the best shape + for a small design, where a macro's outline and channels cost more + core than the array saves. + + A register file named in AUTO_MEMORIES_MACRO_PLACE is a macro to + synthesis and to macro placement: its + abstract LEF and model liberty land beside the FakeRAM views and the + module joins blackboxes.txt. In `mode netlist` it then dissolves into + its placed cells after macro placement (regfile_dissolve.tcl), which + reads its structural netlist and the placement of its core + (.v, .place) and finds it in regfiles.txt. A spec whose ports + are not the module's stops the build here rather than miswire the + parent. + """ + macro_place = set(macro_place or []) + if not specs: + (out_dir / "regfiles.txt").write_text("") + (out_dir / "inline.txt").write_text("") + if macro_place: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_MACRO_PLACE names " + + " ".join(sorted(macro_place)) + + " but AUTO_MEMORIES_REGFILES lists no register file" + ) + return [] + if not openroad: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_REGFILES is set but OPENROAD_EXE is not" + ) + if not lefs: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_REGFILES needs TECH_LEF and SC_LEF" + ) + modules = [_spec_module(spec) for spec in specs] + unknown = sorted(macro_place - set(modules)) + if unknown: + raise SystemExit( + "gen_memories: AUTO_MEMORIES_MACRO_PLACE names " + + " ".join(unknown) + + ", which no AUTO_MEMORIES_REGFILES spec builds; it lists " + + " ".join(modules) + ) + memories = [] + dissolve = [] + 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}" + + 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 = _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) + 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 + 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 + + 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, + macro_place: list[str] | None = None, ) -> int: if platform != "asap7": sys.stderr.write(f"gen_memories: unsupported platform {platform}\n") @@ -53,6 +301,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) @@ -73,8 +326,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, macro_place + ) + if regfiles: + listed = {m.name for m in regfiles} + memories = [m for m in memories if m.name not in listed] + regfiles + schema.dump(memories, json_path, platform) + for mem in sorted(memories, key=lambda m: m.name): - verdict = "macro" if mem.idiomatic else "flops" + verdict = mem.kind sys.stderr.write( f"gen_memories: {mem.name} {mem.rows}x{mem.bits} " f"R={mem.read_ports} W={mem.write_ports} RW={mem.rw_ports} " @@ -101,6 +364,39 @@ 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( + "--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", @@ -121,6 +417,11 @@ def main(argv: list[str] | None = None) -> int: args.platform, args.out_dir, args.json, + args.verilog, + 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 ffd02593d9..aaa39125a1 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import re import sys import tempfile import unittest @@ -113,19 +114,35 @@ 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", + "regfiles.txt", ], ) - self.assertEqual((mems_dir / "blackboxes.txt").read_text(), "mem_128x32\n") + # one ` ` line; the area is the one the .lib states, + # which synthesis costs the blackbox by + [line] = (mems_dir / "blackboxes.txt").read_text().splitlines() + name, area = line.split() + self.assertEqual(name, "mem_128x32") + lib_area = re.search( + r"^\s*area\s*:\s*([0-9.]+)\s*;", + (mems_dir / "mem_128x32.lib").read_text(), + re.M, + ).group(1) + self.assertEqual(float(area), float(lib_area)) + self.assertGreater(float(area), 0) def test_additional_memories_forces_conversion(self): force = Path(self.tmp.name) / "force.memories" force.write_text(FORCE_TAGS) code, d = self.run_generator(memories_files=[force]) self.assertEqual(code, 0) - blackboxes = (d / "memories" / "blackboxes.txt").read_text().split() + blackboxes = [ + line.split()[0] + for line in (d / "memories" / "blackboxes.txt").read_text().splitlines() + ] self.assertEqual(sorted(blackboxes), ["cache_tags", "mem_128x32"]) self.assertTrue((d / "memories" / "cache_tags.lib").exists()) self.assertTrue((d / "memories" / "cache_tags.lef").exists()) @@ -179,6 +196,26 @@ def test_no_memories_still_writes_contract_files(self): self.assertEqual(code, 0) self.assertEqual(json.loads((d / "memories.json").read_text())["memories"], []) self.assertEqual((d / "memories" / "blackboxes.txt").read_text(), "") + self.assertEqual((d / "memories" / "regfiles.txt").read_text(), "") + self.assertEqual((d / "memories" / "inline.txt").read_text(), "") + + 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/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 53353ee46a..8efef2d4e1 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -44,10 +44,11 @@ proc read_checkpoint { file } { } # AUTO_MEMORIES: memory modules whose generated liberty view replaces -# their behavioral body during synthesis. The list is produced by -# scripts/memories/gen_memories.py before canonicalization; see +# their behavioral body during synthesis, each with that view's area in +# um^2. The list is produced by scripts/memories/gen_memories.py before +# canonicalization, one ` ` per line; see # docs/user/AutoMemories.md. -proc auto_memories_blackboxes { } { +proc auto_memories_areas { } { if { ![env_var_equals AUTO_MEMORIES 1] } { return {} } @@ -59,9 +60,59 @@ proc auto_memories_blackboxes { } { set fh [open $f r] set content [string map {\r ""} [read $fh]] close $fh + set areas [dict create] + foreach line [split $content "\n"] { + if { [string trim $line] == "" } { + continue + } + set fields [regexp -all -inline {\S+} $line] + if { + [llength $fields] != 2 + || ![string is double -strict [lindex $fields 1]] + } { + error "$f: expected ` `, got `$line`" + } + dict set areas {*}$fields + } + return $areas +} + +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] { + # 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" + } +} + proc read_design_sources { } { # We are reading Verilog sources source $::env(SCRIPTS_DIR)/synth_stdcells.tcl @@ -115,7 +166,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" } @@ -123,6 +174,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 @@ -144,7 +196,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] } { @@ -178,6 +230,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) { @@ -248,4 +304,34 @@ proc convert_liberty_areas { } { set gate_eq [expr int($area / $found_cell_area)] rtlil::set_attr -mod -uint $box gate_cost_equivalent $gate_eq } + + # An AUTO_MEMORIES macro is a blackbox from the RTL's own module, not a + # Liberty cell: it costs its generated view's area, or + # keep_hierarchy -min_cost refuses it ("Missing cost information on + # instanced blackbox"). Its view is not read in its place, as its pins + # need not be the module's (FakeRAM uses its own names). + # One name per line, not a Tcl list: yosys lists an escaped module with + # its leading backslash (`\1d_ram`), which list parsing reads as an + # escape, and blackboxes.txt names it as read_slang takes it (`1d_ram`). + set modules {} + foreach line [split [tee -q -s result.string select -list-mod =*] "\n"] { + set line [string trim $line] + if { $line ne "" } { + lappend modules [regsub {^\\} $line {}] + } + } + dict for {m area} [auto_memories_areas] { + if { [lsearch -exact $modules $m] < 0 } { + # slang imports a blackboxed module only where it is instanced + continue + } + if { ![rtlil::has_attr -mod $m blackbox] } { + # e.g. slang's per-instance `$`, which + # --blackboxed-module does not match: the body was elaborated + error "AUTO_MEMORIES macro $m is in the design but was not\ + blackboxed; its name does not match the module's definition" + } + rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }] + } } diff --git a/flow/scripts/variables.json b/flow/scripts/variables.json index 61a7b8b8e7..748163efc1 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -68,6 +68,18 @@ "default": 0, "description": "Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and \u2014 when idiomatic as an SRAM macro \u2014 given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.\n" }, + "AUTO_MEMORIES_MACRO_PLACE": { + "description": "Experimental. Space-separated list of module names of AUTO_MEMORIES_REGFILES register files to place as macros, blackboxed in synthesis and placed by the macro placer (and, in `mode netlist`, dissolved after macro placement). A register file not listed is inlined: its generated netlist of standard cells replaces the module's body in synthesis, and its cells are placed, sized and buffered with the rest of the design -- the better shape for a small design, where a macro's outline and channels cost more core than the array saves. A name no spec builds stops the build. Requires AUTO_MEMORIES=1.\n", + "stages": [ + "synth" + ] + }, + "AUTO_MEMORIES_REGFILES": { + "description": "Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells; 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" + ] + }, "BALANCE_ROWS": { "default": 0, "description": "Balance rows during placement.\n", diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml index 2052bd468d..74ba3b2ba7 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -227,6 +227,37 @@ 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/`. 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/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index be30f8f927..42ac55a8a0 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -54,6 +54,7 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): process = Process(ASAP7_PROCESS_CONFIG) timing_data = TimingData(ASAP7_TIMING_CONFIG) + blackboxes = [] for m in converted: name = m["name"] bits = m.get("bits", 32) @@ -82,7 +83,9 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): with open(lef_path, "w") as f: LefExporter(ram).export(f) - (out_dir / "blackboxes.txt").write_text( - "".join(f"{m['name']}\n" for m in converted) - ) + # the area the .lib states, beside the name: synthesis costs the + # blackbox by it + blackboxes.append(f"{name} {ram.physical.get_area(False):.6f}\n") + + (out_dir / "blackboxes.txt").write_text("".join(blackboxes)) return 0