Skip to content

Commit e82ccee

Browse files
oharboeclaude
andcommitted
AUTO_MEMORIES_REGFILES: register files of placed standard cells, built by generate_ram
A register file, many ports over few words, is not an SRAM: FakeRAM's single-port model does not describe it, and synthesised to flops its mux trees are placed by a placer that does not know they are an array. AUTO_MEMORIES_REGFILES lists spec files, one per RTL module. memories/regfile_ram.py translates each to OpenROAD's generate_ram_netlist options (ports, banks, folds, the zero word, reset, read latency, write style and priority, cells), checks it against the module's ports from synthesis (a stray, missing or mis-sized port stops the build), and writes a wrapper in the module's place around the RAM, <m>_ram: an active-high reset inverted for an active-low flop, addresses narrowed or widened. A register file is inlined unless AUTO_MEMORIES_MACRO_PLACE names it: its cells are then placed, sized and buffered with the rest of the design. A named one is a macro to synthesis and macro placement (an abstract LEF and a liberty of its area beside the FakeRAM views, in blackboxes.txt; a parameterized module's instances lose parameters the netlist does not declare), and dissolves into its cells at the end of macro placement (regfile_dissolve.tcl): the core FIRM where the placer put the macro, flipped as it was, row parity kept; the periphery left to global placement and the resizer; then eliminate_dead_logic. Where a row holds several words they sit side by side, and a word whose bits would run more than 150 um is folded into more rows: XiangShan's RobEntryFile and RenameBufferFile, dissolved into a parent, did not legalize otherwise (the parent buffers the word's selects inside the full array). `mode netlist`, the default, is the only mode. A spec with `mode macro` stops: a timing model written from generate_ram's netlist before any clock tree is built has the whole unbuffered clock net as its clock pin, 8.5 pF for 64 words of 256 bits on asap7, 32 pF for XiangShan's 954-bit Ftq queue, whose clock then arrived 7.7 ns late in its parent. A register file wanted as a macro is a design of its own: run it through the flow, where CTS builds its clock tree, and give the parent its abstract as a macro. A module named in a spec whose memory has an initial value stops the build: generate_ram's storage has none, and the value would be lost without a word. The spec's read_latency is generate_ram's -register_read_address. The RAMs the wrappers instantiate as macros are listed in regfile_cores.txt: they are in blackboxes.txt but no module of the sources, so the check that every AUTO_MEMORIES module is a black box after read_slang skips them. Uses generate_ram's register-file options (OpenROAD#11723), in tools/OpenROAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
1 parent c9ac73e commit e82ccee

14 files changed

Lines changed: 1667 additions & 8 deletions

‎docs/user/AutoMemories.md‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,134 @@ the left edge, alternating horizontal M4 `VDD`/`VSS` straps across the
122122
macro (which the platform's PDN macro grid connects to M5), and an `OBS`
123123
covering M1 to M4.
124124

125+
## Register files
126+
127+
A register file, many read and write ports over a few words, is no
128+
SRAM: FakeRAM's single-port model does not describe it, and synthesised
129+
to flops its mux trees are placed by a placer that does not know they
130+
are an array. `AUTO_MEMORIES_REGFILES` lists spec files, one per RTL
131+
module, and OpenROAD's `generate_ram` builds each as an array of placed
132+
standard cells: a flop per bit, its write logic beside it, the read
133+
trees in columns. The spec names the module, its words and bits, its
134+
read and write ports by the module's own port names, and the cells:
135+
136+
```
137+
module RegFile
138+
mode netlist
139+
words 16
140+
bits 8
141+
clock clock
142+
read io_r0_addr io_r0_data
143+
write io_w0_addr io_w0_data io_w0_en
144+
cell flop DFFHQNx1_ASAP7_75t_R
145+
...
146+
```
147+
148+
`memories/regfile_ram.py` translates the spec to `generate_ram_netlist`
149+
options (ports, banks, folds, the zero word, reset, read latency, write
150+
style and priority, cells) and checks it against the module's ports,
151+
from synthesis: a port in one and not the other, or a data port of
152+
another width, stops the build rather than miswire the parent. The RAM
153+
is a block `<m>_ram`, and a wrapper takes the module's place,
154+
connecting the module's ports to the RAM's (an active-high reset
155+
inverted for an active-low flop, an address narrowed or widened). What
156+
happens next depends on `AUTO_MEMORIES_MACRO_PLACE`, the register files
157+
to place as macros:
158+
159+
- **Not listed (the default): inlined.** Synthesis reads the wrapper
160+
and the RAM's netlist in the module's place; from there on their cells
161+
are placed, sized and buffered with the rest of the design. No macro,
162+
no abstract, no dissolve. The better shape for a small design, where a
163+
macro's outline and channels cost more core than the array saves.
164+
- **Listed: a macro.** `<m>_ram` is a macro to synthesis and macro
165+
placement: its abstract `<m>_ram.lef` and a liberty of its area and
166+
pins land beside the FakeRAM views and it joins `blackboxes.txt`. In
167+
`mode netlist` (the only one) `generate_ram` places only the array's
168+
core and leaves its periphery (decode, select and input buffers) to
169+
the parent, and the macro dissolves into its cells at the end of macro
170+
placement (`regfile_dissolve.tcl`): the core lands FIRM where the
171+
macro was placed, flipped as the macro was, on the parent's rows; the
172+
periphery is left to global placement and the resizer, and dead logic
173+
is eliminated as at synthesis. A large design whose floorplan plans
174+
its register files lists them.
175+
- **A macro of its own.** A register file wanted as a macro to the end
176+
is a design of its own: run its module through the flow, its spec
177+
listed and macro-placed so it dissolves there, and give the parent
178+
its abstract as a macro. Its clock tree is then built and its timing
179+
model is written from it. `mode macro` stops with that advice: a
180+
timing model written from `generate_ram`'s netlist before any clock
181+
tree would have the whole unbuffered clock net as one pin.
182+
183+
A word holds its value between writes either through a hold mux per
184+
bit (`write_style mux`, the default here) or through one integrated
185+
clock gate per word, enabled by its write selects (`write_style
186+
clock_gate`, `generate_ram`'s own default). Where a row holds more than
187+
one word (`bank_columns`), each word's bits sit side by side, so its
188+
selects and write enables are no longer than its bits; and a word whose
189+
bits would run more than 150 um is folded into more rows. `tap_columns`
190+
and `service_sites` leave empty columns in the array for the buffers a
191+
parent puts on the nets that cross it.
192+
193+
Listed register files appear in `memories.json` with kind `regfile`;
194+
the rest have kind `fakeram` or `flops`.
195+
196+
Moving the dissolve into OpenROAD is left for later: it would make it
197+
faster, testable at unit level and maintained with the database it
198+
edits.
199+
200+
### Designs to measure it on
201+
202+
What a register file of placed cells buys is a shorter minimum clock
203+
period (the read mux trees are columns, not a cloud) and a shorter
204+
build (fewer cells for synthesis, placement and the resizer to work
205+
on). The ORFS designs whose register file is flip-flops today and its
206+
own module, the seam a spec needs, measured as flops against
207+
`AUTO_MEMORIES_REGFILES`:
208+
209+
| Design | Module | Today | Shape | Ready? |
210+
|---|---|---|---|---|
211+
| 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. |
212+
| asap7/ibex | `ibex_register_file_ff` | flip-flops: `RegFile = RegFileFF`, the default | 32 x 32, 2R1W, x0 reads 0 | Yes: `unused` names `test_en_i` and `dummy_instr_id_i`, ports the array does not use, and `async_reset rst_ni low` resets the flops to 0 as ibex does; simulated against ibex's RTL. |
213+
| 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. |
214+
| 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. |
215+
216+
coralnpu's `Regfile` (8R4W) and `FRegfile` (3R2W) carry their
217+
scoreboard and bypass logic in the same module, and picorv32 and
218+
tinyRocket keep their register file inside the CPU module: those need a
219+
seam in their RTL first.
220+
221+
### Status: at parity on riscv32i, on a smaller core
222+
223+
riscv32i with its register file generated and inlined (a spec in
224+
`AUTO_MEMORIES_REGFILES`, `zero_word 0`), at riscv32i's own
225+
utilisation, against `designs/asap7/riscv32i`. To global route, 950 ps
226+
clock, minimum period from placement parasitics on the global-route
227+
database:
228+
229+
| | flops | register file |
230+
|---|---|---|
231+
| minimum period | 950 ps | 943 ps |
232+
| core area | 4 012 um2 | 3 503 um2 |
233+
234+
Both are limited by the same single-cycle path: the instruction into
235+
the register file's read, the ALU, the data memory and back into the
236+
register file's write. The register file's share of it, decode and read
237+
tree, is about 260 ps in either.
238+
239+
What did not pay, measured on the same design:
240+
241+
- the register file as a macro dissolved after macro placement: it does
242+
not fit riscv32i's 62 % beside the four FakeRAMs, and at 55 % it is
243+
959 ps on a 4 644 um2 core;
244+
- a hold mux per bit instead of the clock gate: 994 ps inlined, every
245+
array endpoint carrying the mux;
246+
- predecoded read selects: 960 ps;
247+
- a tree of 2:1 muxes steered by the address bits instead of a decoded
248+
one-hot read: 959 ps, the address bits' fanout (about 1 000 inputs a
249+
port) needs as many buffer levels as the decode it replaces;
250+
- four-input gates in the read tree, stronger cells: 950 to 954 ps,
251+
within what any change to the placement moves.
252+
125253
## Platform support
126254

127255
**asap7 only.** `gen_memories.py` rejects any other platform. The
@@ -183,3 +311,5 @@ bazel-orfs implements this.
183311

184312
- [AUTO_MEMORIES](FlowVariables.md#AUTO_MEMORIES)
185313
- [ADDITIONAL_MEMORIES](FlowVariables.md#ADDITIONAL_MEMORIES)
314+
- [AUTO_MEMORIES_MACRO_PLACE](FlowVariables.md#AUTO_MEMORIES_MACRO_PLACE)
315+
- [AUTO_MEMORIES_REGFILES](FlowVariables.md#AUTO_MEMORIES_REGFILES)

‎docs/user/FlowVariables.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ configuration file.
104104
| <a name="ADDITIONAL_SITES"></a>ADDITIONAL_SITES| Passed as -additional_sites to initialize_floorplan.| |
105105
| <a name="ASAP7_USE_VT"></a>ASAP7_USE_VT| A space separated list of VT options to use with the ASAP7 standard cell library: RVT, LVT, SLVT.| RVT|
106106
| <a name="AUTO_MEMORIES"></a>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|
107+
| <a name="AUTO_MEMORIES_MACRO_PLACE"></a>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.| |
108+
| <a name="AUTO_MEMORIES_REGFILES"></a>AUTO_MEMORIES_REGFILES| Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells. memories/regfile_ram.py checks the spec against the module's ports and has OpenROAD's generate_ram build it as `<module>_ram`, under `$(RESULTS_DIR)/memories/`, with a wrapper in the module's place. A module named in AUTO_MEMORIES_MACRO_PLACE is blackboxed like an AUTO_MEMORIES macro and placed as one; any other is inlined as its generated netlist. A placed spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its periphery left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.| |
107109
| <a name="BALANCE_ROWS"></a>BALANCE_ROWS| Balance rows during placement.| 0|
108110
| <a name="BLOCKS"></a>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.| |
109111
| <a name="BUFFER_PORTS_ARGS"></a>BUFFER_PORTS_ARGS| Specify arguments to the buffer_ports call during placement. Only used if DONT_BUFFER_PORTS=0.| |
@@ -351,6 +353,8 @@ configuration file.
351353
- [ABC_SCRIPT](#ABC_SCRIPT)
352354
- [ADDER_MAP_FILE](#ADDER_MAP_FILE)
353355
- [ADDITIONAL_MEMORIES](#ADDITIONAL_MEMORIES)
356+
- [AUTO_MEMORIES_MACRO_PLACE](#AUTO_MEMORIES_MACRO_PLACE)
357+
- [AUTO_MEMORIES_REGFILES](#AUTO_MEMORIES_REGFILES)
354358
- [CACHED_REPORTS](#CACHED_REPORTS)
355359
- [CLKGATE_MAP_FILE](#CLKGATE_MAP_FILE)
356360
- [DFF_LIB_FILE](#DFF_LIB_FILE)

‎flow/Makefile‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,14 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json
279279
--yosys-json $(RESULTS_DIR)/memories_inferred.json \
280280
--out-dir $(RESULTS_DIR)/memories \
281281
--json $(RESULTS_DIR)/memories.json \
282-
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file))
282+
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) \
283+
$(foreach file,$(AUTO_MEMORIES_REGFILES),--regfile-spec $(file)) \
284+
$(foreach m,$(AUTO_MEMORIES_MACRO_PLACE),--macro-place $(m)) \
285+
$(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \
286+
$(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file))
283287

284288
$(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \
285-
$(wildcard $(SCRIPTS_DIR)/memories/*.py)
289+
$(AUTO_MEMORIES_REGFILES) $(wildcard $(SCRIPTS_DIR)/memories/*.py)
286290
$(UNSET_AND_MAKE) do-auto-memories
287291

288292
yosys-dependencies: $(RESULTS_DIR)/memories.json

‎flow/scripts/macro_place.tcl‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,38 @@ erase_non_stage_variables floorplan
33
load_design 2_1_floorplan.odb 2_1_floorplan.sdc
44
source_step_tcl PRE MACRO_PLACE
55

6+
# A register file in `mode netlist` dissolves onto the parent's rows
7+
# after macro placement, and the rows are then cut MACRO_ROWS_HALO_X/Y
8+
# around every other macro: a halo that wide keeps them off its core,
9+
# which fills its outline to the edge.
10+
set regfile_list "$::env(RESULTS_DIR)/memories/regfiles.txt"
11+
if { [env_var_equals AUTO_MEMORIES 1] && [file exists $regfile_list] } {
12+
set f [open $regfile_list r]
13+
set regfile_masters [split [string trim [read $f]] "\n"]
14+
close $f
15+
set dbu [[ord::get_db_tech] getDbUnitsPerMicron]
16+
set halo {}
17+
foreach v {MACRO_ROWS_HALO_X MACRO_ROWS_HALO_Y} {
18+
set um [expr { [env_var_exists_and_non_empty $v] ? $::env($v) : 0 }]
19+
lappend halo [expr { round($um * $dbu) }]
20+
}
21+
lassign $halo x y
22+
foreach inst [[ord::get_db_block] getInsts] {
23+
if { [[$inst getMaster] getName] in $regfile_masters } {
24+
# the instance's own halo, left bottom right top, which the macro
25+
# placer keeps clear of other macros
26+
odb::dbBox_create $inst $x $y $x $y
27+
}
28+
}
29+
}
30+
631
source $::env(SCRIPTS_DIR)/macro_place_util.tcl
732

833
source_step_tcl POST MACRO_PLACE
934

35+
# register files in `mode netlist` become their cells
36+
source $::env(SCRIPTS_DIR)/regfile_dissolve.tcl
37+
1038
report_design_area
1139

1240
orfs_write_db $::env(RESULTS_DIR)/2_2_floorplan_macro.odb

‎flow/scripts/memories/detect.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,23 @@ def scan_yosys_json(
305305
return dedupe(out)
306306

307307

308+
def initialized_modules(data: dict | str | Path, top: str | None = None) -> set[str]:
309+
"""Modules holding a memory with an initial value: a $mem_v2 whose INIT
310+
has a 0 or 1 bit (yosys writes an uninitialized memory's INIT as x)."""
311+
if isinstance(data, (str, Path)):
312+
data = json.loads(Path(data).read_text())
313+
out = set()
314+
for mod_name, mod_info in data.get("modules", {}).items():
315+
name = mod_name[1:] if mod_name.startswith("\\") else mod_name
316+
for cell in mod_info.get("cells", {}).values():
317+
if cell.get("type", "") not in ("$mem_v2", "$mem"):
318+
continue
319+
init = str(cell.get("parameters", {}).get("INIT", ""))
320+
if "0" in init or "1" in init:
321+
out.add(definition_name(name, top))
322+
return out
323+
324+
308325
def definition_name(module: str, top: str | None) -> str:
309326
"""`array_64x114$top.u0` -> `array_64x114` for top module `top`.
310327

‎flow/scripts/memories/detect_test.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,5 +270,24 @@ def test_inline_array_with_other_logic_is_named_per_cell(self):
270270
self.assertEqual([m.name for m in mems], ["am_regs.Memory"])
271271

272272

273+
class InitializedModulesTest(unittest.TestCase):
274+
def test_only_a_memory_with_a_0_or_1_bit_is_initialized(self):
275+
def module(init):
276+
params = {"SIZE": 2, "WIDTH": 2, "RD_PORTS": 1, "WR_PORTS": 1}
277+
if init is not None:
278+
params["INIT"] = init
279+
return {"cells": {"\\m": {"type": "$mem_v2", "parameters": params}}}
280+
281+
data = {
282+
"modules": {
283+
"\\none": module(None),
284+
"\\all_x": module("xxxx"),
285+
"\\zeros": module("0000"),
286+
"\\some$top.u0": module("xx1x"),
287+
}
288+
}
289+
self.assertEqual(detect.initialized_modules(data, "top"), {"zeros", "some"})
290+
291+
273292
if __name__ == "__main__":
274293
unittest.main()

0 commit comments

Comments
 (0)