Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/user/AutoMemories.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,134 @@ the left edge, alternating horizontal M4 `VDD`/`VSS` straps across the
macro (which the platform's PDN macro grid connects to M5), and an `OBS`
covering M1 to M4.

## Register files

A register file, many read and write ports over a few words, is no
SRAM: FakeRAM's single-port model does not describe it, and synthesised
to flops its mux trees are placed by a placer that does not know they
are an array. `AUTO_MEMORIES_REGFILES` lists spec files, one per RTL
module, and OpenROAD's `generate_ram` builds each as an array of placed
standard cells: a flop per bit, its write logic beside it, the read
trees in columns. The spec names the module, its words and bits, its
read and write ports by the module's own port names, and the cells:

```
module RegFile
mode netlist
words 16
bits 8
clock clock
read io_r0_addr io_r0_data
write io_w0_addr io_w0_data io_w0_en
cell flop DFFHQNx1_ASAP7_75t_R
...
```

`memories/regfile_ram.py` translates the spec to `generate_ram_netlist`
options (ports, banks, folds, the zero word, reset, read latency, write
style and priority, cells) and checks it against the module's ports,
from synthesis: a port in one and not the other, or a data port of
another width, stops the build rather than miswire the parent. The RAM
is a block `<m>_ram`, and a wrapper takes the module's place,
connecting the module's ports to the RAM's (an active-high reset
inverted for an active-low flop, an address narrowed or widened). What
happens next depends on `AUTO_MEMORIES_MACRO_PLACE`, the register files
to place as macros:

- **Not listed (the default): inlined.** Synthesis reads the wrapper
and the RAM's netlist in the module's place; from there on their cells
are placed, sized and buffered with the rest of the design. No macro,
no abstract, no dissolve. The better shape for a small design, where a
macro's outline and channels cost more core than the array saves.
- **Listed: a macro.** `<m>_ram` is a macro to synthesis and macro
placement: its abstract `<m>_ram.lef` and a liberty of its area and
pins land beside the FakeRAM views and it joins `blackboxes.txt`. In
`mode netlist` (the only one) `generate_ram` places only the array's
core and leaves its periphery (decode, select and input buffers) to
the parent, and the macro dissolves into its cells at the end of macro
placement (`regfile_dissolve.tcl`): the core lands FIRM where the
macro was placed, flipped as the macro was, on the parent's rows; the
periphery is left to global placement and the resizer, and dead logic
is eliminated as at synthesis. A large design whose floorplan plans
its register files lists them.
- **A macro of its own.** A register file wanted as a macro to the end
is a design of its own: run its module through the flow, its spec
listed and macro-placed so it dissolves there, and give the parent
its abstract as a macro. Its clock tree is then built and its timing
model is written from it. `mode macro` stops with that advice: a
timing model written from `generate_ram`'s netlist before any clock
tree would have the whole unbuffered clock net as one pin.

A word holds its value between writes either through a hold mux per
bit (`write_style mux`, the default here) or through one integrated
clock gate per word, enabled by its write selects (`write_style
clock_gate`, `generate_ram`'s own default). Where a row holds more than
one word (`bank_columns`), each word's bits sit side by side, so its
selects and write enables are no longer than its bits; and a word whose
bits would run more than 150 um is folded into more rows. `tap_columns`
and `service_sites` leave empty columns in the array for the buffers a
parent puts on the nets that cross it.

Listed register files appear in `memories.json` with kind `regfile`;
the rest have kind `fakeram` or `flops`.

Moving the dissolve into OpenROAD is left for later: it would make it
faster, testable at unit level and maintained with the database it
edits.

### Designs to measure it on

What a register file of placed cells buys is a shorter minimum clock
period (the read mux trees are columns, not a cloud) and a shorter
build (fewer cells for synthesis, placement and the resizer to work
on). The ORFS designs whose register file is flip-flops today and its
own module, the seam a spec needs, measured as flops against
`AUTO_MEMORIES_REGFILES`:

| Design | Module | Today | Shape | Ready? |
|---|---|---|---|---|
| asap7/riscv32i | `regfile` | flip-flops: `reg [31:0] rf[31:0]`, mapped to flops (no AUTO_MEMORIES) | 32 x 32, 2R1W, x0 reads 0 | Yes: the ports are the spec's (`zero_word 0`). Also exercises `OPENROAD_HIERARCHICAL=1` beside two FakeRAM macros. |
| asap7/ibex | `ibex_register_file_ff` | flip-flops: `RegFile = RegFileFF`, the default | 32 x 32, 2R1W, x0 reads 0 | Yes: `unused` names `test_en_i` and `dummy_instr_id_i`, ports the array does not use, and `async_reset rst_ni low` resets the flops to 0 as ibex does; simulated against ibex's RTL. |
| asap7/cva6 | `ariane_regfile` (`ariane_regfile_ff.sv`) | flip-flops: cv32a65x has `FpgaEn` 0 | 32 x 32, 2R1W | Not yet: the ports are packed arrays (`raddr_i[1:0][4:0]`), one port per slice, which a spec cannot yet address; asynchronous reset as ibex. |
| asap7/swerv_wrapper | `dec_gpr_ctl_*` | flip-flops | 32 x 32, 4R3W | Not yet: read enables, bank ids and scan ports around the array need a seam in the RTL. |

coralnpu's `Regfile` (8R4W) and `FRegfile` (3R2W) carry their
scoreboard and bypass logic in the same module, and picorv32 and
tinyRocket keep their register file inside the CPU module: those need a
seam in their RTL first.

### Status: at parity on riscv32i, on a smaller core

riscv32i with its register file generated and inlined (a spec in
`AUTO_MEMORIES_REGFILES`, `zero_word 0`), at riscv32i's own
utilisation, against `designs/asap7/riscv32i`. To global route, 950 ps
clock, minimum period from placement parasitics on the global-route
database:

| | flops | register file |
|---|---|---|
| minimum period | 950 ps | 943 ps |
| core area | 4 012 um2 | 3 503 um2 |

Both are limited by the same single-cycle path: the instruction into
the register file's read, the ALU, the data memory and back into the
register file's write. The register file's share of it, decode and read
tree, is about 260 ps in either.

What did not pay, measured on the same design:

- the register file as a macro dissolved after macro placement: it does
not fit riscv32i's 62 % beside the four FakeRAMs, and at 55 % it is
959 ps on a 4 644 um2 core;
- a hold mux per bit instead of the clock gate: 994 ps inlined, every
array endpoint carrying the mux;
- predecoded read selects: 960 ps;
- a tree of 2:1 muxes steered by the address bits instead of a decoded
one-hot read: 959 ps, the address bits' fanout (about 1 000 inputs a
port) needs as many buffer levels as the decode it replaces;
- four-input gates in the read tree, stronger cells: 950 to 954 ps,
within what any change to the placement moves.

## Platform support

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

- [AUTO_MEMORIES](FlowVariables.md#AUTO_MEMORIES)
- [ADDITIONAL_MEMORIES](FlowVariables.md#ADDITIONAL_MEMORIES)
- [AUTO_MEMORIES_MACRO_PLACE](FlowVariables.md#AUTO_MEMORIES_MACRO_PLACE)
- [AUTO_MEMORIES_REGFILES](FlowVariables.md#AUTO_MEMORIES_REGFILES)
4 changes: 4 additions & 0 deletions docs/user/FlowVariables.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ configuration file.
| <a name="ADDITIONAL_SITES"></a>ADDITIONAL_SITES| Passed as -additional_sites to initialize_floorplan.| |
| <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|
| <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|
| <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.| |
| <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.| |
| <a name="BALANCE_ROWS"></a>BALANCE_ROWS| Balance rows during placement.| 0|
| <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.| |
| <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.| |
Expand Down Expand Up @@ -351,6 +353,8 @@ configuration file.
- [ABC_SCRIPT](#ABC_SCRIPT)
- [ADDER_MAP_FILE](#ADDER_MAP_FILE)
- [ADDITIONAL_MEMORIES](#ADDITIONAL_MEMORIES)
- [AUTO_MEMORIES_MACRO_PLACE](#AUTO_MEMORIES_MACRO_PLACE)
- [AUTO_MEMORIES_REGFILES](#AUTO_MEMORIES_REGFILES)
- [CACHED_REPORTS](#CACHED_REPORTS)
- [CLKGATE_MAP_FILE](#CLKGATE_MAP_FILE)
- [DFF_LIB_FILE](#DFF_LIB_FILE)
Expand Down
8 changes: 6 additions & 2 deletions flow/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,14 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json
--yosys-json $(RESULTS_DIR)/memories_inferred.json \
--out-dir $(RESULTS_DIR)/memories \
--json $(RESULTS_DIR)/memories.json \
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file))
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) \
$(foreach file,$(AUTO_MEMORIES_REGFILES),--regfile-spec $(file)) \
$(foreach m,$(AUTO_MEMORIES_MACRO_PLACE),--macro-place $(m)) \
$(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \
$(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file))

$(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \
$(wildcard $(SCRIPTS_DIR)/memories/*.py)
$(AUTO_MEMORIES_REGFILES) $(wildcard $(SCRIPTS_DIR)/memories/*.py)
$(UNSET_AND_MAKE) do-auto-memories

yosys-dependencies: $(RESULTS_DIR)/memories.json
Expand Down
28 changes: 28 additions & 0 deletions flow/scripts/macro_place.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,38 @@ erase_non_stage_variables floorplan
load_design 2_1_floorplan.odb 2_1_floorplan.sdc
source_step_tcl PRE MACRO_PLACE

# A register file in `mode netlist` dissolves onto the parent's rows
# after macro placement, and the rows are then cut MACRO_ROWS_HALO_X/Y
# around every other macro: a halo that wide keeps them off its core,
# which fills its outline to the edge.
set regfile_list "$::env(RESULTS_DIR)/memories/regfiles.txt"
if { [env_var_equals AUTO_MEMORIES 1] && [file exists $regfile_list] } {
set f [open $regfile_list r]
set regfile_masters [split [string trim [read $f]] "\n"]
close $f
set dbu [[ord::get_db_tech] getDbUnitsPerMicron]
set halo {}
foreach v {MACRO_ROWS_HALO_X MACRO_ROWS_HALO_Y} {
set um [expr { [env_var_exists_and_non_empty $v] ? $::env($v) : 0 }]
lappend halo [expr { round($um * $dbu) }]
}
lassign $halo x y
foreach inst [[ord::get_db_block] getInsts] {
if { [[$inst getMaster] getName] in $regfile_masters } {
# the instance's own halo, left bottom right top, which the macro
# placer keeps clear of other macros
odb::dbBox_create $inst $x $y $x $y
}
}
}

source $::env(SCRIPTS_DIR)/macro_place_util.tcl

source_step_tcl POST MACRO_PLACE

# register files in `mode netlist` become their cells
source $::env(SCRIPTS_DIR)/regfile_dissolve.tcl

report_design_area

orfs_write_db $::env(RESULTS_DIR)/2_2_floorplan_macro.odb
Expand Down
17 changes: 17 additions & 0 deletions flow/scripts/memories/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,23 @@ def scan_yosys_json(
return dedupe(out)


def initialized_modules(data: dict | str | Path, top: str | None = None) -> set[str]:
"""Modules holding a memory with an initial value: a $mem_v2 whose INIT
has a 0 or 1 bit (yosys writes an uninitialized memory's INIT as x)."""
if isinstance(data, (str, Path)):
data = json.loads(Path(data).read_text())
out = set()
for mod_name, mod_info in data.get("modules", {}).items():
name = mod_name[1:] if mod_name.startswith("\\") else mod_name
for cell in mod_info.get("cells", {}).values():
if cell.get("type", "") not in ("$mem_v2", "$mem"):
continue
init = str(cell.get("parameters", {}).get("INIT", ""))
Comment thread
oharboe marked this conversation as resolved.
Resolved
if "0" in init or "1" in init:
out.add(definition_name(name, top))
return out


def definition_name(module: str, top: str | None) -> str:
"""`array_64x114$top.u0` -> `array_64x114` for top module `top`.

Expand Down
19 changes: 19 additions & 0 deletions flow/scripts/memories/detect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,24 @@ def test_inline_array_with_other_logic_is_named_per_cell(self):
self.assertEqual([m.name for m in mems], ["am_regs.Memory"])


class InitializedModulesTest(unittest.TestCase):
def test_only_a_memory_with_a_0_or_1_bit_is_initialized(self):
def module(init):
params = {"SIZE": 2, "WIDTH": 2, "RD_PORTS": 1, "WR_PORTS": 1}
if init is not None:
params["INIT"] = init
return {"cells": {"\\m": {"type": "$mem_v2", "parameters": params}}}

data = {
"modules": {
"\\none": module(None),
"\\all_x": module("xxxx"),
"\\zeros": module("0000"),
"\\some$top.u0": module("xx1x"),
}
}
self.assertEqual(detect.initialized_modules(data, "top"), {"zeros", "some"})


if __name__ == "__main__":
unittest.main()
Loading