Skip to content

ram: register-file options for generate_ram - #11723

Open
oharboe wants to merge 23 commits into
The-OpenROAD-Project:masterfrom
oharboe:ram-regfile-options
Open

oharboe wants to merge 23 commits into
The-OpenROAD-Project:masterfrom
oharboe:ram-regfile-options

Conversation

@oharboe

@oharboe oharboe commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

This replaces #11669.

This PR adds the options a register file needs to generate_ram and generate_ram_netlist, one commit per option, so each can be reviewed on its own. Every option is off by default: with none given, generate_ram builds exactly what it builds today, and the existing ram tests are unchanged.

Each option comes from a register file in a real design: the ORFS designs (riscv32i, ibex, cva6, coralnpu, swerv) and XiangShan's (IntRegFile has 12 read and 9 write ports, banked reads and an x0 word). The commits are in dependency order, and each commit message says what its option does and why.

# Commit Test
1 ram: generate_ram with more than one write port write_ports_nangate45
2 ram: generate_ram reads through AND-OR trees without tristates and_or_read_asap7
3 ram: generate_ram -zero_word, a word that reads 0 zero_word_asap7
4 ram: generate_ram -reset_value, an asynchronous reset reset_asap7
5 ram: generate_ram -banks, -bank_columns, -bank_order banks_asap7
6 ram: generate_ram -rb_ports, banked read ports banked_read_ports_asap7
7 ram: generate_ram -read_latency, registered read addresses register_read_address_asap7 (renamed in 19)
8 ram: generate_ram -write_style mux, a hold mux per bit write_mux_asap7
9 ram: generate_ram_netlist -periphery unplaced periphery_unplaced_asap7
10 ram: generate_ram -cells and -name cells_name_asap7
11 ram: generate_ram -decoder word, a balanced tree per word decoder_word_asap7
12 ram: generate_ram -bit_folds, a word's slices in rows bit_folds_asap7
13 ram: generate_ram_netlist -periphery verilog periphery_verilog_asap7
14 ram: generate_ram -write_priority or, colliding writes OR'd write_priority_or_asap7
15 ram: generate_ram -service_columns, room in the array service_columns_asap7
16 ram: generate_ram -bank_column_layout, a word's bits contiguous bank_column_layout_asap7
17 ram: stop on out-of-range option values option_errors_asap7
18 ram: test BUILD's per-test data in sorted order
19 ram: -read_latency is -register_read_address register_read_address_asap7
20 ram: generate_ram routes on asap7 the asap7 tests, routed
21 ram: generate_ram -periphery verilog, the core routed periphery_verilog_asap7
22 ram: the option tests route their blocks through generate_ram all 21
23 ram: generate_ram's help lists its options

Testing. Each option has a test of what it is meant to do, not a snapshot of the netlist. Since commit 22 each test builds its block with generate_ram, so the block is placed and routed, and checks that every net is routed with no DRC violations before it simulates the routed block. src/ram/test/ram_sim.tcl simulates the generated block cycle by cycle: combinational cells from their Liberty functions, and the few flops and clock gates the tests use from a one-line model each. The tests then write and read random words against a model of what the RAM should hold. Examples: word 0 reads 0 with -zero_word 0; every word reads the reset value after -reset_value; colliding writes give the last port's data, or their OR with -write_priority or; with -register_read_address 1 the data follows a clock after its address. Layout options also check placement: rows per word, empty service columns, contiguous words, pins on the pin layer's tracks. Each test was checked to fail when its option is removed or reversed. The existing tests are unchanged.

The commits are split one per option for reading. They were built and tested together, at the last commit, not one by one.

The integration test is The-OpenROAD-Project/bazel-orfs#1239 (before it, The-OpenROAD-Project/bazel-orfs#1234): it carries these commits and the ORFS side, and runs register files from ORFS designs through simulation, equivalence checks and placement. It is being extended to XiangShan. The ORFS changes that use these options (AUTO_MEMORIES_REGFILES through generate_ram, The-OpenROAD-Project/OpenROAD-flow-scripts#4651) are carried as patches in bazel-orfs until OpenROAD has this support and ORFS is bumped onto it.

🤖 Generated with Claude Code

oharboe and others added 16 commits October 13, 2026 17:12
generate_ram took exactly one read/write or one write port (RAM-0030).
It now takes any number of write ports: each has its own address
decoder, data bus (D0, D1, ...), write enables (we0, we1, ...) and
address (addr_w0, addr_w1, ...); one write port keeps the names it had.
Per word, port w writes when its row select and write enable are high;
a word written by two ports in a cycle keeps the last one's data, as
the behavioral model does; the word's clock gate opens for any of them.
Per bit an AO22 per pair of ports, and an OR2 across pairs, picks the
data. The AO22 is found by its truth table, as the AOI22 is, through
findMaster4, which both now share; the OR2 by its function.

Not yet with -use_latch, whose data passes a latch per column, or with
-column_mux_ratio above 1, whose word select comes from the first
port's address (RAM-0054, RAM-0055).

Tested: src/ram/test/write_ports_nangate45.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
generate_ram's read path is a tristate bitline per bit, and a library
without a tristate cell (asap7) could not generate at all (RAM-0010).
-read_style and_or reads through a tree per bit column instead: an
AOI22 over each pair of rows, then NAND2 and NOR2 levels by turns, each
node placed in a row of the span it covers, beside its inputs. It is the
default where the library has no tristate cell (RAM-0057 says so), and
can be asked for where it has one; -read_style tristate insists on the
bitlines.

A storage flop whose output is the data inverted is taken on the
AND-OR path, an inverter at the root of each tree undoing it; the
tristate path refuses it (RAM-0058). buildPortMap now reckons a flop's
output polarity from both the state variable a pin reads and its
next_state: asap7's DFFHQNx1 stores !D in IQN and outputs IQN on QN,
which it took for a true output. Cells with next_state "D" are as
before. NAND2 and NOR2 are found by truth table (asap7 writes NAND2 as
(!A) + (!B)), through the evaluator findMaster4 used, now shared.

Tested: src/ram/test/and_or_read_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-zero_word n makes word n read 0 and drop its writes, as a RISC-V
register file's x0: its row keeps its place in the grid but has no
flops and no clock gate, and the AND-OR read trees are built over the
other rows; where the storage flop's output is inverted the zero word
stays in them, its term its own select, so the inverter at the root
reads it as 0. The behavioral model reads 0 there and ignores writes.
Needs -read_style and_or and -column_mux_ratio 1 (RAM-0060).

Tested: src/ram/test/zero_word_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-reset_value v resets every word to v asynchronously. Each bit's flop is
cleared or preset by the reset, the other pin held off by one tie cell
per word slice: the data, clear and preset pins are read from the flop's
ff group, and which pin gives which value accounts for the flop's
output polarity (asap7's DFFASRHQNx1 stores !D, so its `clear` sets the
data to 1). The reset port takes the flop's own polarity, rst_n or rst;
a parent adapts any other reset, or a synchronous one, to it. The
storage cell, found or given, has both a clear and a preset. The
behavioral model resets the same way. Not with -use_latch (RAM-0065).

Several ORFS designs reset their register files (ibex, whose ECC and
lockstep checks need valid codewords from the first cycle; cva6,
ariane, swerv, coralnpu); it is an option, off by default.

Tested: src/ram/test/reset_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-banks splits the words into banks, contiguous or interleaved in the
address (-bank_order), -bank_columns of them side by side and the rest
stacked, for an array's aspect ratio and for the bank counts register
files use (XiangShan's 224 words in 4 interleaved banks). With banks
every port decodes every word's whole address, an AND2 tree per word,
so a bank need not be a power of two words and any placement of words
reads and writes alike; a row holds -bank_columns words, laid out as the
column mux lays out its words. A read port is one tristate bitline
shared by every word, or one AND-OR tree over all of them. Without
-banks, nothing changes. Not with -column_mux_ratio above 1 or
-use_latch.

Also: an array with fewer rows than address bits got no address
inverters (ports_per_col was 0), so its decoders read floating
literals; at least one port's inverters now go in each column.

Tested: src/ram/test/banks_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-rb_ports n adds n banked read ports to a RAM with -banks: each has an
address and a data output per bank (addr_rb<k>_b<b>, Qrb<k>_b<b>), the
address the word's index in its bank, every bank read independently in
the same cycle, as XiangShan's integer register file reads. To the
generator below, each (port, bank) is a read port of its own that
decodes and reads only its bank's words: a tristate bitline or an
AND-OR tree over them. The behavioral model reads the same words.

Tested: src/ram/test/banked_read_ports_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-read_latency 1 registers every read port's address: a plain
positive-edge flop per address bit, found by its ff group, in the
address inverter's cell; the data follows a cycle after the address,
as XiangShan's register files read. Where the flop gives the address
inverted (asap7's DFFHQNx1) the decoders take the flop's output as the
complement and the inverter makes the true literal, so no cell is
added. The behavioral model registers the addresses the same way. Not
with -column_mux_ratio above 1 or with read/write ports (RAM-0072).

Tested: src/ram/test/read_latency_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-write_style mux holds each word without a clock gate: every flop is
on the clock, its D the new data where the word is written and its own
value elsewhere, an AO22 per bit (the AOI22 and an inverter where the
library has none) and an inverter per word for the hold; a flop whose
output is inverted feeds back through an inverter per bit. The default,
clock_gate, is generate_ram's clock gate per word, as before. Which is
better depends on the design: a clock gate per word saves a mux per
bit, a mux keeps every flop on the plain clock. Not with -use_latch.

Tested: src/ram/test/write_mux_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-periphery unplaced leaves the periphery (address inverters and
registers, decoders, select buffers, each word's select cell and the
input buffers) for a parent to place, size and buffer: the parent alone
knows the drivers and loads outside the array. The outline is cut to
what stays placed, the core, and a row above it for a parent of the
other row parity, so a parent sizing its floorplan by utilization
charges the core and nothing else. Each pin goes where its connections
land, on -pin_layer's track grid inside the outline, for a macro
placer to orient the core by before the parent dissolves it into its
own rows. generate_ram_netlist only; generate_ram's flow places
everything.

Tested: src/ram/test/periphery_unplaced_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-cells {role master ...} picks the cell for a role (inv, and2, buffer,
clock_gate, aoi22, ao22, or2, nand2, nor2, tie, addr_flop) instead of
the smallest the libraries have of its function. On asap7 the smallest
are the xp cells a flow marks dont_use (INVxp33, AOI22xp33), and the
smallest buffer is the hold buffer HB1xp67; a core the parent keeps
fixed cannot be resized after. An AOI22 or AO22 given is checked by its
truth table, and its pins are read from it; findMaster4 now reads them
from the cell it picks, not from the last that matched in the search.

-name names the block and the behavioral model, instead of
RAM<words>x<bits>, so two RAMs of one size can live in one design.

Tested: src/ram/test/cells_name_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-decoder word decodes each word's whole address through a balanced
AND2 tree, the banks' decoders (the -banks commit) without banks. The default,
row, is generate_ram's decoder per row, an AND2 chain over the row
address: four AND2 deep for 32 rows where a tree is three. On riscv32i's
register file the chain is about 60 ps of the read path's 220.

Tested: src/ram/test/decoder_word_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-bit_folds k lays a word's mask slices (-mask_size, word/k rounded up)
in k rows instead of side by side; where k does not divide the word the
last fold is short, its missing bits empty columns (XiangShan's
FtqMetaQueueRedirect, 211 bits in 4 folds). generate_ram puts a word in one row, a bit's
cells side by side, so a wide word with many ports is a long thin
array: XiangShan's integer register file, 64 bits, 12 read and 9 write
ports, came out 1458 x 31 um. With folds the decoder, select-buffer and
address-inverter columns take k rows a word, and a column's input
buffers stack a row a fold above the words, so a wide word's buffers do
not set the column's width. A folded word is written whole: one write
enable a port, and the word's write logic (enables, priority, hold,
clock gate, reset tie, select inverters) built in its first fold and
shared by the others. Not with -use_latch or the row decoder's column
mux.

Tested: src/ram/test/bit_folds_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-periphery verilog makes the block the core alone, <name>_core, and
writes the periphery to -wrapper_verilog as a module <name> around an
instance of it: the periphery's logic cells as assigns of their liberty
functions, for synthesis to map with the parent's logic, and its other
cells (clock gates, flops, ties) as instances. A net the core shares
with the periphery becomes a core port; a port only the periphery uses
leaves the core. The core is then outlined and pinned as with
-periphery unplaced. An option beside generator-built periphery, not a
replacement: which is better depends on how the array is integrated.

Tested: src/ram/test/periphery_verilog_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-write_priority says what a word takes when two write ports write it in
one cycle: last, the last port's data, as before; or or, the OR of every
writing port's data. firtool's register files (XiangShan's IntRegFile,
FpRegFile, VfRegFile) merge their writes with an OR, so a generated
register file in their place is equivalent to the RTL only with or; it
also drops each word's priority logic, an inverter and an AND2 per port
but the last. The behavioral model follows the option.

The fourteenth of the generate_regfile features carried onto
generate_ram (generate_regfile's default); see OpenROAD The-OpenROAD-Project#11669. Not
upstreamed.

Tested: src/ram/test/write_priority_or_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
-service_columns {every sites} puts an empty column `sites` sites wide
after every `every` storage columns. An array of placed standard cells
is full: a parent flow that buffers the long nets crossing it (repair
inserts wire buffers on the word selects and read bitlines) has no site
to put them on, and legalization fails. XiangShan's RobEntryFile,
352 words of 21 bits with 16 read and 8 write ports, dissolved into a
parent, left 1,230 such buffers overlapping without these columns. The
columns are inside the core, so -periphery unplaced keeps them in the
outline. Default: none, as before.

Tested: src/ram/test/service_columns_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
With banks a row holds -bank_columns words, laid out as the column mux
lays out its words: bit by bit, interleaved. Each word is decoded whole,
so nothing reads the row through a mux, and the interleave only costs:
a word's selects and write enables run the whole row to reach its bits.
-bank_column_layout contiguous puts each word's bits side by side with
its select column after them; the per-bit read trees cross the row
instead. XiangShan's RobEntryFile (352 words of 21 bits, 16 read and 8
write ports, 4 words a row) had rows 278 um wide: dissolved into a
parent, the resizer found 6,483 slew violations on those nets and put
6,043 buffers inside the full array, which detailed placement could not
legalize. Default: interleaved, as before.

Tested: src/ram/test/bank_column_layout_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe
oharboe requested a review from a team as a code owner October 13, 2026 15:13
@oharboe
oharboe requested a review from maliberty October 13, 2026 15:13
@github-actions

Copy link
Copy Markdown
Contributor

This PR exceeds the recommended size of 1000 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly expands the RAM generator's capabilities by introducing several new configuration options, such as read styles, banked configurations, asynchronous resets, custom decoders, bit folding, and Verilog wrapper generation. It also adds a cycle simulator and extensive test cases to verify these features. The code review feedback is highly constructive, suggesting robust input validation for the newly introduced parameters (such as -zero_word, -banks, -bank_columns, -service_columns, -bit_folds, -read_latency, and -rb_ports) to prevent potential runtime errors like division-by-zero, out-of-bounds access, or infinite loops, as well as addressing a potential check bypass when -rb_ports is explicitly set to 0.

Comment thread src/ram/src/ram.tcl Outdated
Comment thread src/ram/src/ram.tcl
Comment thread src/ram/src/ram.tcl
Comment thread src/ram/src/ram.tcl
Comment thread src/ram/src/ram.tcl
Comment thread src/ram/src/ram.tcl Outdated
Comment thread src/ram/src/ram.tcl
@oharboe

oharboe commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

@rovinski FYI

oharboe and others added 2 commits October 13, 2026 17:20
A value out of range stopped nothing: -banks, -bank_columns and
-bit_folds below 1 were clamped to 1, a negative -read_latency read as
0, a negative -zero_word as no zero word, -service_columns below 1 was
ignored, and a negative -rb_ports went into the read port count.
-rb_ports 0 with no other read port also passed the check for at least
one read port. Each now stops with its own message (RAM-0036, RAM-0071,
RAM-0092 to RAM-0096).

Tested: src/ram/test/option_errors_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Buildifier's unsorted-dict-items lint: the new tests' entries in the
per-test data dictionary are sorted by name. No change to what any test
reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe

oharboe commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

@maliberty @rovinski pr-merge green, if pr-head fails then that's not important.

@oharboe

oharboe commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Mmm.... green. 😌

@rovinski

Copy link
Copy Markdown
Collaborator

None of the tests actually check that the rams are routable. generate_ram_netlist should be avoided as it is an internal command - only generate_ram should be used.

@rovinski

Copy link
Copy Markdown
Collaborator

I have a lot of concerns and I don't have time to review a 4k line PR.

You should really take a look at this paper as it covers several strategies for high performance RAMs that we are looking to implement.

ram: generate_ram reads through AND-OR trees without tristates

Use De Morgan's Law and convert to NAND-NOR tree for reduced delay and size

ram: generate_ram -zero_word, a word that reads 0

I would want to see an experiment showing that this is better than implementing the functionality in RTL outside of the RAM. I am not convinced that putting in a 0 row in the RAM is the best methodology.

ram: generate_ram -reset_value, an asynchronous reset

For reasons mentioned in #11669 I am opposed to implementing this feature. I have not seen any convincing use case that this is a good feature.

ram: generate_ram -read_latency, registered read addresses

The feature is ok but I don't like the naming. Either the output should be registered or unregistered. There is not really a reason why the latency would be anything other than 0 or 1. So maybe change the flag to -register_outputs (default unregistered)

See #11669 for other comments

It would be very helpful to see screenshots of the layout(s) generated to review.

@oharboe

oharboe commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@rovinski thank you for taking the time to go through this, here and on #11669. You're under no obligation to, and a little direction from you goes a long way.

(This continues the discussion from #11669 (comment).)

Before I roll up my sleeves, here is how I read your comments and what I plan to do about each one.

First, I apologize for my ignorance on memories. I'm not trying to become an expert. As my open PRs show, I'm trying to get the whole flow in place, from RTL to XiangShan-sized designs, so that experts can work on the individual pieces one concern at a time, from an OpenROAD+ORFS baseline, warts and all.

That's what this PR is: a new baseline of features. Some will be replaced by something better, some made automatic, some removed. None of that can be tried against OpenROAD, ORFS and real designs until the features are in master; out of tree, the turnaround is too long. So I'll keep this as one PR and address the concerns in it.

Routing and generate_ram_netlist. I'll switch the tests to generate_ram, so every test routes the block it checks. OpenROAD used to run flows, but that required bazel-orfs and didn't add enough value to break ORFS's separation of concerns. The compromise is that an OpenROAD bump can break a flow in ORFS, and in exchange OpenROAD CI doesn't run every ORFS flow. Full-flow integration tests on tiny designs, one for every generate_ram option, will go into The-OpenROAD-Project/bazel-orfs#1234.

Screenshots. I'll commit them in bazel-orfs#1234 and embed them here.

NAND/NOR read tree. I'll measure it against the AND-OR tree and post delay and area.

-read_latency will become -register_read_address (0 or 1). It registers the read address, as a flow-through synchronous SRAM does, and the decoder and read tree come after the flop. A register on the output data, as in a pipelined SRAM, is a different circuit and would be its own feature.

-write_style mux. Each bit has a mux that feeds back the stored value unless that word is written, instead of a clock gate per word. I'll make the commit message say so.

-zero_word and -reset_value stay in the baseline. The designs ask for them: x0 is RISC-V, and ibex, cva6, coralnpu and swerv reset their register files for their own reasons. We take the RTL as it is. Whether a zero row beats tie-lo cells, or ibex's ECC bits are better split from the data bits, is the experiment you describe. With the baseline in master, anyone can run it against these designs.

On swerv, you're right, I worded that badly: I meant the flop cell swerv's RTL instantiates, which always has a reset, not a PDK.

-decoder word. Choosing the strategy automatically is the goal. The explicit option is the baseline an automatic choice gets measured against.

12 read and 9 write ports. Register files like that are normal in high-performance CPUs, and XiangShan is where this is headed. Synthesized flops are what the generated register file is compared against.

The paper. I couldn't get it. IEEE has it behind a paywall, and the open-access listing for the journal version (ACM TODAES 2016) at EPFL has no PDF attached. What in it do you think applies here? That's exactly the kind of direction that goes a long way. It's also another reason to put a baseline into OpenROAD and ORFS master: people who have that knowledge can then improve the generator against real designs, one concern at a time.

@oharboe

oharboe commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@rovinski on the bigger question this PR raises: you asked for these features to go into generate_ram rather than sit beside it, and that's what #11723 does. Looking at the result, do you still think it should be one command?

generate_ram sounded more general than it was. It solved one problem, a routed RAM block, in one particular way. With the options added here it does several different things. The code shows it: RamGen::generate is a single function of about 1,300 lines that handles every option, and the parts around it hardly depend on each other:

Part What it does Shared with
Cell selection (findMasters, ~330 lines) finds the cells for each role in the libraries everything
Array construction (makeBit, makeSlice, makeWord, decoders, read trees) word rows by bit columns, placed by construction RAM and register file
Register-file logic several write ports and write priority, zero word, reset, banks and banked read ports, registered read address, write mux, word decoder, bit folds, service columns register file only
Physical finish (ramPdngen, ramPinplacer, ramFiller, ramRouting, ~260 lines) power grid, pins, filler, routing any placed block
Periphery for a parent flow (splitPeriphery, unplacePeriphery, ~280 lines) leaves the periphery to the parent's placer, or writes it as Verilog for the parent's synthesis a flow that places the block itself (ORFS)
Behavioral Verilog the model RAM and register file

OpenROAD already splits commands where the steps are separate: macro placement and detailed routing are two commands. Here a list is less obvious. A split could look like this, with cleaner syntax per command and the code above shared behind them:

  • generate_ram: what it was, a routed RAM block, with its own options and its existing tests unchanged.
  • generate_register_file: the register-file options, on the same array construction and cell selection.
  • A finishing command (power grid, pins, filler, routing) that both of them call, and that anything else that builds a placed block could call too.
  • The periphery handling as its own step, for a flow like ORFS that places and optimizes the periphery with the parent.

What I found on XiangShan is that the details of the RAM weren't the problem. Global placement was: long wires. If the array isn't placed where it needs to be, there's nothing the RAM can do about it. To help global placement, this PR separates the periphery from the regular structure, and it also lets a flow take the netlist only and leave the placement to global placement. Which is best depends on the context; no single option is best everywhere. By its own QoR function, generate_ram builds an optimal block, and you become what you measure. I think that QoR function is a good one, and in many cases it will also turn out to be optimal in the system.

Memories are one case of a wider pattern: a repeating structure the general flow doesn't handle well. CAMs are another, and so are design-specific structures. XiangShan's reorder buffer is hundreds of copies of one entry, each decoding the same wide bus locally. That's a very large number of broadcast pins and thousands of cells to hold a few dozen bits of state. Synthesis can't share that decode across entries, and global placement can't spread it. The aim is a large cut in synthesis time along with better QoR.

Those structures don't belong in generate_ram. They don't have to be in the OpenROAD binary either, at least not yet. I'm tinkering with them as a separate binary in bazel-orfs, built against OpenROAD's database, with the user injecting the netlist of one tile: tools/structured_gen. I'd be surprised, and flattered, if anyone is interested in it at this point. It could become part of OpenROAD later, reusing the array construction, cell selection and finishing step above, but not until there are interesting results and I've learned more.

I don't think we'll get the split right on the first try. Reworking the code for a split is cheap now. The real cost is the cognitive load on users each time the API changes.

These features are experimental until they're stable. OpenROAD has no releases, and anyone can see from the git history how mature a feature is. A note in the docs saying "experimental" would be out of date as soon as it was written, and less true than the history.

A cross-cutting change such as read_db -hier needs an explicit experimental toggle. A separate command doesn't, because the command is the toggle. The options in this PR work the same way: each is off by default.

My main concern is a baseline that supports the features in this PR, so contributors can work on it one concern at a time, as their interest and resources allow. Given that, I'm flexible on the split: one command now, the split above, or another you prefer.

@oharboe

oharboe commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@rovinski about that paper: I never read it, I never saw it, and I don't know anybody who has. But I may have overheard some people talking about it, and then I happened to be looking at the code.

Word is it describes how to build a small standard-cell memory with controlled placement:

  • latches as storage,
  • a NAND/NOR tree for the read path,
  • registered read addresses,
  • a clock gate per word on the write path,
  • gaps left in the array for buffers and for the synthesized decoders and clock tree.

It places those cells by construction, then lets the standard flow finish. The way I hear it, the scope is low-power, low-voltage, two-port (one read, one write) memories from 32×32 to 128×32 in 28 nm FD-SOI. Each block is measured on its own: area, wirelength, power and runtime, against the same RTL placed without control. Beyond about 256 rows it goes to banks.

If that's right, it's a clear result within that scope, and much of it is already in generate_ram. The and_or read path is that tree: an AOI22 over each pair of rows, then NAND2 and NOR2 levels. -register_read_address is the read address register. -service_columns gives the buffer gaps, the default write style is a clock gate per word, and -banks is the banking.

From what I'm told, the paper puts the whole flow into making a macro. That's the right choice for what it measures, but it can't show what the general flow would do with the same cells in context, or whether that would be the better solution there. I want to keep that signal. So this PR keeps both paths: a block placed by construction, and a netlist, or a placed core with its periphery, that the parent's flow places and optimizes with its surroundings. On XiangShan the comparison between the two is the interesting part.

The latch storage, I hear, relies on its latch-to-output path having 1.5 cycles. In XiangShan, the only multicycle path I know of is on L2's data SRAMs, which the RTL reads over two cycles. A 1.5-cycle path would fit inside that on timing. But an L2 data array is far beyond the sizes the paper targets, so it isn't a strong fit.

This PR is aimed at the cases outside that scope:

  • register files with many read and write ports, as in high-performance CPUs;
  • flops with what RTL register files carry: reset, a zero word, write priority between ports, banked reads;
  • the block placed in the context of its parent, where the periphery can be flattened and optimized with its surroundings.

oharboe and others added 5 commits October 14, 2026 10:32
The option registers each read port's address: the decoders and read
trees come after the flop, and the data follows a cycle after the
address. -read_latency read as a register on the data, a different
circuit. 0 and 1 only; more than one cycle stops with RAM-0071.

Tested: src/ram/test/register_read_address_asap7.tcl,
src/ram/test/option_errors_asap7.tcl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Two things kept generate_ram from routing an asap7 block.

-tracks: generate_ram made its tracks from the technology LEF's
offsets, and asap7's M2 offset is negative (IFP-0039). -tracks names a
file of make_tracks commands to source instead, as asap7's flows do;
without it the tracks are made as before. A file that cannot be read
stops with RAM-0097.

Pins off the power straps: the pin placer did not know where
generate_ram's own straps run, and put pins on the tracks under them.
On asap7 the M4 straps start at the block's bottom edge, its first
track 12 nm above it, and detailed routing could not reach the pin
(DRT-0255). Each strap's span, with its layer's spacing, is now
excluded from the edges that layer's pins go on. The existing sky130
and Nangate45 blocks keep their pins where they were.

The asap7 tracks file joins //test:asap7_data.

Tested: the asap7 tests in src/ram/test, which route their blocks
through generate_ram in the commit that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
generate_ram takes -periphery verilog and -wrapper_verilog: the routed
block is the core, <name>_core, and the periphery is written as a
Verilog module, <name>, around it, for a parent's synthesis to map and
place with its own logic. The netlist puts each of the core's pins
where its connections land, for a macro placer to orient the core by;
generate_ram drops those and places the pins on the core's edges as it
does for any block, so the core routes. -periphery unplaced, which
leaves the periphery unplaced inside the block, stays
generate_ram_netlist's, for a flow that places it; generate_ram stops
on it with RAM-0098.

Tested: src/ram/test/periphery_verilog_asap7.tcl, which routes the core
in the commit that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Every option test now builds its block with generate_ram, so it is
placed, given its power grid, pins and fillers, and routed, and then
checks what the option computes on the routed block.
src/ram/test/routed.tcl holds generate_ram's arguments for asap7 and
Nangate45, and routed::check: every signal net with two or more pins
has a wire, and detailed routing ran and left no violations.

What changed besides the command:

- banks and bank_column_layout use 32 words: at 8 words in four banks,
  two side by side, the block was a strip 36 um wide and 1.35 um tall
  whose east edge had six pin slots for nine address pins.
- service_columns counts fillers and taps as empty sites: in a routed
  block they fill the columns the option leaves.
- periphery_unplaced stays on generate_ram_netlist: generate_ram does
  not take -periphery unplaced.
- option_errors runs generate_ram, and adds -zero_word past the last
  word (RAM-0059), -register_read_address 2 (RAM-0071), an unreadable
  -tracks file (RAM-0097) and -periphery unplaced (RAM-0098).

Tested: src/ram/test, all 21 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
generate_ram takes the register-file options and passes them to
generate_ram_netlist, but its help listed none of them, so `help
generate_ram` did not show what the command takes. The README's
synopsis lacked -periphery, -wrapper_verilog and -tracks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe oharboe mentioned this pull request Oct 14, 2026
1 of 25 tasks
@oharboe

oharboe commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@rovinski five new commits on top, no force-push, so the review history stays.

Commit What
ram: -read_latency is -register_read_address The option registers the read address. The decoders and read trees come after the flop. 0 and 1 only.
ram: generate_ram routes on asap7 -tracks <file>: asap7's LEF M2 track offset is negative (IFP-0039), so the tracks come from the file asap7's flows use. Pins are kept off generate_ram's own power straps: on asap7 the M4 straps start at the bottom edge and covered the first pin track (DRT-0255). The existing sky130 and Nangate45 blocks keep their pins where they were.
ram: generate_ram -periphery verilog, the core routed generate_ram takes -periphery verilog: the routed core, with its pins on its edges, and the periphery as Verilog for the parent's synthesis. -periphery unplaced stays generate_ram_netlist's, for a flow that places the periphery itself: the "structured placement group" item in the ram feature tracker, #9392.
ram: the option tests route their blocks through generate_ram Every option test now builds with generate_ram, checks that every net is routed with zero DRC violations, then simulates the routed block.
ram: generate_ram's help lists its options help generate_ram now lists the register-file options.

Routing. Routing the tests found the two asap7 problems above. A thin banked strip, 8 words in four banks two wide (36 × 1.35 µm), can't be routed: its East edge has six pin slots for nine address pins. It fails loudly (PPL-0111/DRT-0255), and the bank tests use 32 words. On one thread, three tests spend 5–12 minutes in detailed route (banks, bank_column_layout, write_priority_or). Changing the power straps or limiting the routing layers didn't shorten it. The rest take seconds to two minutes.

NAND/NOR read tree. It's already that: -read_style and_or builds an AOI22 over each pair of rows, then NAND2 and NOR2 levels by turns. The commit title "AND-OR trees" named the function, not the gates.

Integration test: The-OpenROAD-Project/bazel-orfs#1239. It carries these 23 commits and runs each option through the ORFS flow to detailed route on a small design (12 designs, all routed clean). Routed blocks across asap7, Nangate45 and sky130hd, a sample of the options:

asap7, two write ports, write mux, zero word
asap7, banks, banked read port, read address register
Nangate45, bit folds, service columns, reset, word decoder
sky130hd, bit folds, write mux, reset, word decoder

All 14, with the options each shows: https://github.com/The-OpenROAD-Project/bazel-orfs/blob/c2481240914d97010eef8d89a23b040e38decb90/test/regfile/images/README.md

@oharboe

oharboe commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

The insight behind generate_ram, and behind the paper referenced earlier in this thread, is that a repeating structure is generated faster and better by explicit code than by general-purpose synthesis and placement. To reach real designs, that structure has to match the abstractions the RTL already uses.

For design space exploration, a memory gets used when it fits the flow people already trust. That means formal equivalence with open-source tools, static timing (flip-flops are better supported there than latches), a match to the RTL as written, and an SDC users can write. Asking RTL authors to write for a new memory works when a tapeout pays for it. For small memories like register files, where a memory compiler is too expensive or not available, the memory has to come to the RTL. That is where generate_ram shines.

This study maps that reach for the designs in ORFS: which register files yosys plus Verilog surgery can hand to generate_ram today, and what QoR that buys.

  • Already matching by shape: picorv32's picorv32_regs and tinyRocket's register file are both 31×32 2r1w with x0 handled outside, the shape generate_ram listed. On asap7, which has no tristate cell, they needed a read path built from AND-OR gates.
  • Needing this PR's options: riscv32i (x0 inside the module) and ibex (asynchronous reset). swerv, cva6 and coralnpu (more ports, reset, write priority) are prepared, with numbers to follow.
  • Surgery, on the books: where the flow cannot reach a register file, a script beside the design's config moves it into a module of its own. That is either a change the RTL might make for a tapeout, or a stand-in for a feature yosys does not have yet. It is checked and never changes function. For coralnpu's FRegfile, a simulation of 20k cycles against the original showed 0 mismatches.

Results through detailed route on asap7 with RCX extraction, best shape against flip-flops. Min period = clock period − WNS; reg→reg is the same for register-to-register paths alone:

design shape Δ min period Δ reg→reg Δ std-cell area Δ power
tinyRocket verilog periphery −5.8 % −5.8 % −7.0 % −15.4 %
riscv32i netlist −3.1 % −2.0 % −27.7 % −0.3 %
ibex netlist −2.2 % −2.2 % −13.4 % −23.4 %
picorv32 verilog periphery +3.8 % +0.8 % −11.0 % −43.6 %
  • tinyRocket: every shape is faster, smaller and lower in power: period −3.8 to −5.8 %, area −3 to −7 %, power −12 to −22 %.
  • ibex and riscv32i: a shape is better or within noise on all three.
  • picorv32: register-to-register paths are within noise in every shape, area falls 4–12 % and power 19–47 %. Its full period is set by flop-to-mem_la_addr output paths that do not pass through the register file.
  • Run time: the netlist and Verilog-periphery shapes roughly halve the flow's run time at the ORFS clock.

One run per arm. Name and hierarchy perturbation alone moves results by more than 7 % (Kahng and Mantik, ISQED 2002), and routed wirelength by more than 10 % (Chan, Kahng and Woo, SLIP 2020). Differences of a few percent are within noise.

Full tables, stage times and the harness: The-OpenROAD-Project/bazel-orfs#1250

Try it, from bazel-orfs with that PR:

bazelisk run @orfs//flow/designs/asap7/ibex-regfile:ibex_core_route web_route

ibex-regfile routed, in the web viewer

Claude made the changes and stared at paint drying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants