From 2e98135b640dcc7cae268824eac8f5f64a5ba889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 6 Oct 2026 15:07:02 +0200 Subject: [PATCH 1/8] ram: generate_regfile, a multi-port register file as placed standard cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_regfile builds a register file with many read and write ports over few words as an array of placed standard cells, from a spec that names the module, its words and bits, its read and write ports by the RTL's own port names, and the cells to use: generate_regfile -spec rf.regfile -check_ports rf.v \ -verilog rf_cells.v -def rf.def -lef rf.lef -liberty rf.lib The array is laid out as a bitcell array is: word rows by bit columns, each tile a flop, its write select and an AND per read port; the read word lines are decoded in a header column, the read bit lines are OR trees in a footer row. Every cell is placed by construction. Banks, bit folds, a registered read and a word that reads zero are spec options. It writes the cells as structural Verilog, their placement as DEF, an abstract LEF and a timing model (.lib, and a _pre_layout.lib). In `mode netlist` the address decode is left unplaced for the parent to place and size, and the abstract's pins sit where their connections land in the array, so a macro placer that flips the block sees where its wires go. -check_ports refuses a spec whose ports are not the module's. A flow can use it as a macro, or place the macro and then replace it with the cells, as ORFS's AUTO_MEMORIES_REGFILES does. Tests check what the generated file computes, not how it is laid out: case analysis on its inputs and storage, and OpenSTA's constant propagation through its cells (src/ram/test/regfile_checks.tcl). generate_regfile_asap7 (an 8 x 4, two-bank, 2R2W file, word 0 reading zero): every read port reads every word, a write loads only the word it addresses and every other word holds, the abstract and the timing model name every port, and a spec whose ports are not the RTL module's is refused. generate_regfile_clock_gate_asap7: a write opens the clock gate of the word it addresses, and only that one. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/BUILD | 8 + src/ram/CMakeLists.txt | 3 + src/ram/README.md | 60 +- src/ram/include/ram/regfile.h | 33 + src/ram/src/ram.i | 12 + src/ram/src/ram.tcl | 25 + src/ram/src/regfile.cpp | 1506 +++++++++++++++++ src/ram/src/regfile.h | 164 ++ src/ram/src/regfile_cmd.cpp | 72 + src/ram/src/views.cpp | 441 +++++ src/ram/src/views.h | 67 + src/ram/test/BUILD | 23 + src/ram/test/CMakeLists.txt | 4 +- src/ram/test/asap7 | 1 + src/ram/test/generate_regfile_asap7.regfile | 22 + src/ram/test/generate_regfile_asap7.tcl | 76 + src/ram/test/generate_regfile_asap7_rtl.v | 8 + .../test/generate_regfile_asap7_rtl_extra.v | 9 + .../generate_regfile_clock_gate_asap7.tcl | 50 + src/ram/test/regfile_checks.tcl | 113 ++ 20 files changed, 2695 insertions(+), 2 deletions(-) create mode 100644 src/ram/include/ram/regfile.h create mode 100644 src/ram/src/regfile.cpp create mode 100644 src/ram/src/regfile.h create mode 100644 src/ram/src/regfile_cmd.cpp create mode 100644 src/ram/src/views.cpp create mode 100644 src/ram/src/views.h create mode 120000 src/ram/test/asap7 create mode 100644 src/ram/test/generate_regfile_asap7.regfile create mode 100644 src/ram/test/generate_regfile_asap7.tcl create mode 100644 src/ram/test/generate_regfile_asap7_rtl.v create mode 100644 src/ram/test/generate_regfile_asap7_rtl_extra.v create mode 100644 src/ram/test/generate_regfile_clock_gate_asap7.tcl create mode 100644 src/ram/test/regfile_checks.tcl diff --git a/src/ram/BUILD b/src/ram/BUILD index 2cb8a34b5e9..fa9f8c8ce2e 100644 --- a/src/ram/BUILD +++ b/src/ram/BUILD @@ -18,6 +18,11 @@ cc_library( "src/MakeRam.cpp", "src/layout.cpp", "src/ram.cpp", + "src/regfile.cpp", + "src/regfile.h", + "src/regfile_cmd.cpp", + "src/views.cpp", + "src/views.h", ":swig", ":tcl", ], @@ -25,6 +30,7 @@ cc_library( "include/ram/MakeRam.h", "include/ram/layout.h", "include/ram/ram.h", + "include/ram/regfile.h", ], includes = [ "include", @@ -36,6 +42,8 @@ cc_library( "//src/drt", "//src/grt", "//src/odb/src/db", + "//src/odb/src/defout", + "//src/odb/src/lefout", "//src/pdn", "//src/ppl", "//src/sta:opensta_lib", diff --git a/src/ram/CMakeLists.txt b/src/ram/CMakeLists.txt index d0008e8664d..81aaa4ab9a4 100644 --- a/src/ram/CMakeLists.txt +++ b/src/ram/CMakeLists.txt @@ -15,6 +15,9 @@ target_sources(ram src/ram.cpp src/layout.cpp src/MakeRam.cpp + src/regfile.cpp + src/regfile_cmd.cpp + src/views.cpp ) target_include_directories(ram diff --git a/src/ram/README.md b/src/ram/README.md index 94c5df30369..8e466547e4e 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -80,9 +80,67 @@ generate_ram [-mask_size bits] | `-use_latch` | If set to `1`, uses a two-phase latch-based design instead of flip-flops for the storage elements. Functionally equivalent to flip-flop design with smaller storage cell area. Default: `0`. | | `-write_behavioral_verilog` | Write a behavioral Verilog model of the RAM array to the specified file. | +### Register File Generation + +The `generate_regfile` command builds a register file, many read and +write ports over few words, as an array of placed standard cells: word +rows by bit columns, each tile a flop, its write select and an AND per +read port, the read word lines decoded in a header column and the read +bit lines as OR trees in a footer row. Every cell is placed by +construction. The block is built into the current database, whose +libraries must hold the cells the spec names. + +```tcl +generate_regfile -spec file + [-check_ports verilog_file] + [-verilog file] + [-def file] + [-lef file] + [-liberty file] +``` + +#### Options + +| Switch Name | Description | +| ---------------------- | -------------------------------------- | +| `-spec` | The spec file, below. | +| `-check_ports` | A Verilog file declaring the spec's module. The command stops if the module's ports are not the ports the spec names, at the widths the spec implies. | +| `-verilog` | Write the cells as structural Verilog. | +| `-def` | Write the placed block as DEF. | +| `-lef` | Write an abstract LEF of the block. | +| `-liberty` | Write a timing model; a `_pre_layout.lib` beside it has ideal clocks. | + +#### Spec + +One key per line, `#` comments: + +| Key | Description | +| ---------------------- | -------------------------------------- | +| `module name` | The module and block name. | +| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent. `netlist`: the parent places the cells; the address decode is left unplaced for the parent's placer and resizer, and the abstract's pins sit where their connections land in the array. | +| `words n`, `bits n` | Depth and width. | +| `clock port` | The clock port. | +| `reset port` | A reset port the RTL has and the array ignores. | +| `read addr data` | A read port, by the module's port names. Repeat per port. | +| `read_banked addr0 data0 addr1 data1 ...` | A read port with an address and a data port per bank. | +| `write addr data [en]` | A write port. Repeat per port. | +| `cell role master` | The cells: `flop`, `and2`, `or2`, `ao22`, `inv`, `tap`. | +| `pins role pin...` | The pin names of a cell role (`flop`, `and2`, `or2`, `ao22`, `inv`), for a library whose names differ from the defaults. | +| `flop_output Q\|QN` | Whether the flop's output is inverted. | +| `pin_layer layer`, `pin_layer_v layer` | The layers of the pins on the left and right edges, and on the top and bottom. | +| `pin_track offset pitch` | The track grid the pins are centred on, in microns. | +| `banks n`, `bank_columns n`, `bank_order contiguous\|interleaved` | Word columns side by side, how many of them stand in a row, and how words fall into them. | +| `bit_folds n` | The word split into `n` bit bands stacked one above the other. | +| `read_latency 0\|1` | `1` registers each read address on the clock first. | +| `zero_word n` | A word with no storage that reads 0 and drops writes (RISC-V x0). | +| `tap_columns n`, `service_sites n` | A tap column every `n` bit columns, and free sites beside each for the clock tree. | +| `store_name pattern`, `read_reg_name pattern` | Instance names of the flops, from `{word}`, `{bit}`, `{port}` and `{bank}`. | +| `lib knob value` | The timing model's parameters: `gate_delay_ps`, `wire_factor`, `input_load_ff`, `clock_load_ff`, `leakage_nw_per_cell`, `output_max_cap_ff`, `hold_ps`. | + ## Example scripts -See [test/make_8x8_sky130.tcl](test/make_8x8_sky130.tcl). +See [test/make_8x8_sky130.tcl](test/make_8x8_sky130.tcl) and +[test/generate_regfile_asap7.tcl](test/generate_regfile_asap7.tcl). ## Regression tests diff --git a/src/ram/include/ram/regfile.h b/src/ram/include/ram/regfile.h new file mode 100644 index 00000000000..663887e0ec4 --- /dev/null +++ b/src/ram/include/ram/regfile.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include + +namespace odb { +class dbDatabase; +} + +namespace utl { +class Logger; +} + +namespace ram { + +// A multi-port register file as placed standard cells: word rows by bit +// columns, every cell of its core placed by construction (see +// src/regfile.h). Reads the spec file, builds the block into `db`, whose +// libraries must hold the spec's cells, and writes each view whose path +// is not empty. `check_ports` names a Verilog file whose module the spec +// must match port for port before anything is built. +void generateRegfile(odb::dbDatabase* db, + utl::Logger* logger, + const std::string& spec_path, + const std::string& verilog_path, + const std::string& def_path, + const std::string& lef_path, + const std::string& liberty_path, + const std::string& check_ports); + +} // namespace ram diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 8b225fcc890..c4584484fa3 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -8,6 +8,7 @@ #include "ord/OpenRoad.hh" #include "ram/ram.h" +#include "ram/regfile.h" #include "utl/Logger.h" namespace ord { @@ -96,6 +97,17 @@ generate_ram_netlist_cmd(int mask_size, tapcell, max_tap_dist); } +void generate_regfile_cmd(const char* spec_path, + const char* verilog_path, + const char* def_path, + const char* lef_path, + const char* liberty_path, + const char* check_ports) +{ + generateRegfile(ord::getDb(), ord::getLogger(), spec_path, verilog_path, + def_path, lef_path, liberty_path, check_ports); +} + void ram_pdngen(const char* power_net_name, const char* ground_net_name, const char* route_name, int route_width, const char* ver_name, int ver_width, int ver_pitch, diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 4e58c501636..505b551342a 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -298,3 +298,28 @@ proc generate_ram { args } { ram::ram_routing } + +sta::define_cmd_args "generate_regfile" {-spec file + [-verilog file] + [-def file] + [-lef file] + [-liberty file] + [-check_ports verilog_file]} + +# A multi-port register file as placed standard cells, from a spec file +# (src/regfile.h): the block is built into the current database, whose +# libraries must hold the spec's cells, and each view asked for is written. +proc generate_regfile { args } { + sta::parse_key_args "generate_regfile" args \ + keys { -spec -verilog -def -lef -liberty -check_ports } flags {} + sta::check_argc_eq0 "generate_regfile" $args + if { ![info exists keys(-spec)] } { + utl::error RAM 51 "generate_regfile needs -spec." + } + set views {} + foreach k { -verilog -def -lef -liberty -check_ports } { + lappend views [expr { [info exists keys($k)] ? $keys($k) : "" }] + } + ram::generate_regfile_cmd $keys(-spec) {*}$views + ord::design_created +} diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp new file mode 100644 index 00000000000..4d606de5f55 --- /dev/null +++ b/src/ram/src/regfile.cpp @@ -0,0 +1,1506 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// generate_regfile: a multi-port register file as a placed standard-cell +// macro, built directly in odb. See regfile.h and README.md. +#include "regfile.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "odb/db.h" +#include "odb/dbTypes.h" +#include "odb/geom.h" +#include "utl/Logger.h" + +namespace ram::regfile { + +namespace { + +using odb::dbBlock; +using odb::dbBTerm; +using odb::dbInst; +using odb::dbMaster; +using odb::dbNet; + +std::string Bit(const std::string& bus, int i) +{ + return bus + "[" + std::to_string(i) + "]"; +} + +int AddrBits(int words) +{ + int a = 0; + while ((1 << a) < words) { + ++a; + } + return std::max(a, 1); +} + +[[noreturn]] void Refuse(const std::string& what) +{ + throw std::runtime_error(what); +} + +// `pattern` with each `{key}` replaced by its value. +std::string Fmt(std::string pattern, + const std::vector>& values) +{ + for (const auto& [key, value] : values) { + const std::string k = "{" + key + "}"; + for (size_t at = pattern.find(k); at != std::string::npos; + at = pattern.find(k, at)) { + pattern.replace(at, k.size(), std::to_string(value)); + } + } + return pattern; +} + +// The pins of each cell kind, asap7's names by default; a spec may +// override them with `pins ...` in the same order. +struct Pins +{ + std::vector flop = {"D", "CLK", "QN"}; // data, clock, out + std::vector and2 = {"A", "B", "Y"}; + std::vector or2 = {"A", "B", "Y"}; + std::vector ao22 = {"A1", "A2", "B1", "B2", "Y"}; + std::vector aoi22 = {"A1", "A2", "B1", "B2", "Y"}; + std::vector nand2 = {"A", "B", "Y"}; + std::vector nor2 = {"A", "B", "Y"}; + std::vector inv = {"A", "Y"}; + std::vector tie_lo = {"L"}; + std::vector icg = {"ENA", "SE", "CLK", "GCLK"}; + bool flop_inverted = true; // the flop's output is QN +}; + +Pins g_pins; // set by ReadSpec, read by Generate + +// Everything the builder needs while it lays cells into rows. +class Builder +{ + public: + Builder(odb::dbDatabase* db, utl::Logger* logger, const Spec& spec) + : db_(db), logger_(logger), spec_(spec) + { + } + + dbBlock* Run(); + + private: + dbMaster* Master(const std::string& name, const char* role) + { + dbMaster* m = db_->findMaster(name.c_str()); + if (m == nullptr) { + Refuse(std::string("cell for ") + role + + " not in the loaded LEF: " + name); + } + return m; + } + void CheckPins(dbMaster* m, const std::vector& pins) + { + for (const auto& p : pins) { + if (m->findMTerm(p.c_str()) == nullptr) { + Refuse("cell " + m->getName() + " has no pin " + p + + "; give its pins with `pins ...` in the spec"); + } + } + } + dbNet* Net(const std::string& name) + { + dbNet* n = block_->findNet(name.c_str()); + if (n == nullptr) { + n = dbNet::create(block_, name.c_str()); + } + return n; + } + dbBTerm* Input(const std::string& name) + { + dbNet* n = Net(name); + dbBTerm* t = dbBTerm::create(n, name.c_str()); + t->setIoType(odb::dbIoType::INPUT); + t->setSigType(odb::dbSigType::SIGNAL); + return t; + } + dbBTerm* Output(const std::string& name, dbNet* net) + { + dbBTerm* t = dbBTerm::create(net, name.c_str()); + t->setIoType(odb::dbIoType::OUTPUT); + t->setSigType(odb::dbSigType::SIGNAL); + return t; + } + // A cell dropped into a row cursor: placed left to right, FIRM. + struct Cursor + { + int row = 0; + int x = 0; + }; + dbInst* Place(Cursor& c, + dbMaster* m, + const std::string& name, + const std::vector>& conns) + { + // Cells are named after the net they drive, with a suffix: Verilog + // has one namespace for wires and instances, and yosys refuses a + // netlist where a cell and a wire share a name. + std::string inst_name = name; + if (block_->findNet(name.c_str()) != nullptr) { + inst_name += "_g"; + } + dbInst* inst = dbInst::create(block_, m, inst_name.c_str()); + for (const auto& [pin, net] : conns) { + odb::dbITerm* it = inst->findITerm(pin.c_str()); + if (it == nullptr) { + Refuse("cell " + m->getName() + " has no pin " + pin); + } + it->connect(net); + } + inst->setOrient(c.row % 2 == 0 ? odb::dbOrientType::R0 + : odb::dbOrientType::MX); + inst->setLocation(core_x0_ + c.x, core_y0_ + c.row * row_h_); + inst->setPlacementStatus(odb::dbPlacementStatus::FIRM); + if (periphery_) { + periphery_insts_.push_back(inst); + } + c.x += static_cast(m->getWidth()); + max_x_ = std::max(max_x_, c.x); + return inst; + } + // A one-hot select for `bits` address literals and an enable: an AND2 + // tree over the literals, placed at the cursor. + dbNet* Decode(Cursor& c, + const std::string& prefix, + const std::vector& literals) + { + std::vector level = literals; + int k = 0; + while (level.size() > 1) { + std::vector next; + for (size_t i = 0; i + 1 < level.size(); i += 2) { + dbNet* y = Net(prefix + "_a" + std::to_string(k++)); + Place(c, + and2_, + y->getName(), + {{g_pins.and2[0], level[i]}, + {g_pins.and2[1], level[i + 1]}, + {g_pins.and2[2], y}}); + next.push_back(y); + } + if (level.size() % 2 == 1) { + next.push_back(level.back()); + } + level = next; + } + return level.front(); + } + // OR tree over `leaves`, each node placed by a caller-supplied hook so + // the tree can run down a column rather than sit in a footer. + dbNet* OrTree(const std::string& prefix, + const std::vector& leaves, + const std::function& where) + { + std::function rec + = [&](int lo, int hi, int& k) -> dbNet* { + if (hi - lo == 1) { + return leaves[lo]; + } + int mid = (lo + hi) / 2; + dbNet* a = rec(lo, mid, k); + dbNet* b = rec(mid, hi, k); + dbNet* y = Net(prefix + "_o" + std::to_string(k++)); + Place(where(lo, hi), + or2_, + y->getName(), + {{g_pins.or2[0], a}, {g_pins.or2[1], b}, {g_pins.or2[2], y}}); + return y; + }; + int k = 0; + return rec(0, static_cast(leaves.size()), k); + } + // A read bitline: OR over words of (sel & q), in inverting gates. A + // word pair is one AOI22, ~(sel_a & q_a | sel_b & q_b), in the pair's + // first tile; an odd word out is a NAND2. Over those active-low leaves + // a balanced tree alternates NAND2 (low in, high out) and NOR2 (high + // in, low out), each node in the second tile of its middle pair, and + // the root takes two high inputs with an OR2, so the bitline leaves on + // the library's OR2 drive. Where a split leaves the two halves at + // different polarities an inverter evens them. One gate level per + // pair of words instead of an AND2 and an OR2 per level. + struct Polar + { + dbNet* net; + bool low; + }; + dbNet* Bitline(const std::string& prefix, + const std::vector& sel, + const std::vector& q, + const std::function& tile, + const std::vector& words) + { + const int n = static_cast(q.size()); + const int L = (n + 1) / 2; + int k = 0; + auto first = [&](int l) { return words[2 * l]; }; + auto second = [&](int l) { + return 2 * l + 1 < n ? words[2 * l + 1] : words[2 * l]; + }; + std::vector leaf(L); + for (int l = 0; l < L; ++l) { + dbNet* y = Net(prefix + "_l" + std::to_string(l)); + if (2 * l + 1 < n) { + Place(tile(first(l)), + aoi22_, + y->getName(), + {{g_pins.aoi22[0], sel[2 * l]}, + {g_pins.aoi22[1], q[2 * l]}, + {g_pins.aoi22[2], sel[2 * l + 1]}, + {g_pins.aoi22[3], q[2 * l + 1]}, + {g_pins.aoi22[4], y}}); + } else { + Place(tile(first(l)), + nand2_, + y->getName(), + {{g_pins.nand2[0], sel[2 * l]}, + {g_pins.nand2[1], q[2 * l]}, + {g_pins.nand2[2], y}}); + } + leaf[l] = Polar{y, true}; + } + auto invert = [&](Polar p, Cursor& c) { + dbNet* y = Net(prefix + "_i" + std::to_string(k++)); + Place( + c, inv_, y->getName(), {{g_pins.inv[0], p.net}, {g_pins.inv[1], y}}); + return Polar{y, !p.low}; + }; + std::function rec + = [&](int lo, int hi, bool root) -> Polar { + if (hi - lo == 1) { + return leaf[lo]; + } + const int mid = (lo + hi) / 2; + Polar a = rec(lo, mid, false); + Polar b = rec(mid, hi, false); + Cursor& c = tile(second(mid)); + if (a.low != b.low) { + // the shallower half is the one a level behind + if (mid - lo < hi - mid) { + a = invert(a, c); + } else { + b = invert(b, c); + } + } + dbNet* y = Net(prefix + "_o" + std::to_string(k++)); + dbMaster* m = a.low ? nand2_ : (root ? or2_ : nor2_); + const std::vector& p + = a.low ? g_pins.nand2 : (root ? g_pins.or2 : g_pins.nor2); + Place(c, m, y->getName(), {{p[0], a.net}, {p[1], b.net}, {p[2], y}}); + return Polar{y, !a.low && !root}; + }; + Polar out = rec(0, L, true); + if (out.low) { + out = invert(out, tile(second(0))); + } + return out.net; + } + // The row of a tile or header with the least in it so far: cells of + // different widths pack evenly, which a round robin does not do. + static Cursor& Least(std::vector& rows) + { + return *std::min_element( + rows.begin(), rows.end(), [](const Cursor& a, const Cursor& b) { + return a.x < b.x; + }); + } + void Pin(dbBTerm* t, odb::dbTechLayer* layer, int x, int y) + { + odb::dbBPin* bp = odb::dbBPin::create(t); + int w = std::max(static_cast(layer->getWidth()), 1); + odb::dbBox::create(bp, layer, x - w / 2, y - w / 2, x + w / 2, y + w / 2); + bp->setPlacementStatus(odb::dbPlacementStatus::FIRM); + } + + odb::dbDatabase* db_; + utl::Logger* logger_; + const Spec& spec_; + dbBlock* block_ = nullptr; + odb::dbSite* site_ = nullptr; + odb::dbTechLayer* layer_h_ = nullptr; // left/right edge pins + odb::dbTechLayer* layer_v_ = nullptr; // top/bottom edge pins + dbMaster* flop_ = nullptr; + dbMaster* and2_ = nullptr; + dbMaster* or2_ = nullptr; + dbMaster* ao22_ = nullptr; + dbMaster* aoi22_ = nullptr; + dbMaster* nand2_ = nullptr; + dbMaster* nor2_ = nullptr; + dbMaster* inv_ = nullptr; + dbMaster* tap_ = nullptr; + dbMaster* tie_lo_ = nullptr; + dbMaster* icg_ = nullptr; + int dbu_ = 1000; + int site_w_ = 0; + int row_h_ = 0; + int core_x0_ = 0; + int core_y0_ = 0; + int pin_w_ = 0; + int max_x_ = 0; + // One entry per (word, bit) tile in row-major order: its row cursors, + // which the read OR trees keep filling after the tile's own cells, and + // the x where the tile ends, checked once the trees are in. + struct Tile + { + std::vector rows; + int x_end = 0; + }; + std::vector tiles_; + // The periphery: the address band and each word's decode. In netlist + // mode these are left unplaced (see Run). + bool periphery_ = false; + std::vector periphery_insts_; +}; + +dbBlock* Builder::Run() +{ + const Spec& s = spec_; + if (s.words < 2 || s.bits < 1 || s.read.empty() || s.write.empty()) { + Refuse( + "a register file needs at least 2 words, 1 bit, 1 read and 1 " + "write port"); + } + flop_ = Master(s.cells.flop, "flop"); + and2_ = Master(s.cells.and2, "and2"); + or2_ = Master(s.cells.or2, "or2"); + ao22_ = Master(s.cells.ao22, "ao22"); + aoi22_ = Master(s.cells.aoi22, "aoi22"); + nand2_ = Master(s.cells.nand2, "nand2"); + nor2_ = Master(s.cells.nor2, "nor2"); + inv_ = Master(s.cells.inv, "inv"); + tap_ = s.cells.tap.empty() ? nullptr : Master(s.cells.tap, "tap"); + tie_lo_ = s.cells.tie_lo.empty() ? nullptr : Master(s.cells.tie_lo, "tie_lo"); + CheckPins(flop_, g_pins.flop); + CheckPins(and2_, g_pins.and2); + CheckPins(or2_, g_pins.or2); + CheckPins(ao22_, g_pins.ao22); + CheckPins(aoi22_, g_pins.aoi22); + CheckPins(nand2_, g_pins.nand2); + CheckPins(nor2_, g_pins.nor2); + CheckPins(inv_, g_pins.inv); + const bool gated = s.write_style == "clock_gate"; + if (gated) { + icg_ = Master(s.cells.icg, "icg"); + CheckPins(icg_, g_pins.icg); + } + + odb::dbTech* tech = db_->getTech(); + site_ = flop_->getSite(); + if (site_ == nullptr) { + Refuse("flop " + flop_->getName() + " has no site"); + } + site_w_ = site_->getWidth(); + row_h_ = site_->getHeight(); + layer_h_ = tech->findLayer(s.pin_layer_h.c_str()); + layer_v_ = tech->findLayer(s.pin_layer_v.c_str()); + if (layer_h_ == nullptr || layer_v_ == nullptr) { + Refuse("pin layers not in the tech LEF: " + s.pin_layer_h + " " + + s.pin_layer_v); + } + pin_w_ = std::max( + static_cast(std::max(layer_h_->getWidth(), layer_v_->getWidth())), + 1); + + odb::dbChip* chip = db_->getChip(); + if (chip == nullptr) { + chip = odb::dbChip::create(db_, tech, s.module.c_str()); + } + if (chip->getBlock() != nullptr) { + Refuse("the database already has a top block (" + + chip->getBlock()->getName() + "); one generation per database"); + } + block_ = dbBlock::create(chip, s.module.c_str()); + if (block_ == nullptr) { + Refuse("could not create block " + s.module); + } + block_->setDefUnits(tech->getLefUnits()); + dbu_ = tech->getLefUnits(); + + const int W = static_cast(s.write.size()); + const int R = static_cast(s.read.size()); + const int A = AddrBits(s.words); + + // ---- ports and their nets ------------------------------------------ + dbNet* clock = Net(s.clock); + Input(s.clock); + if (!s.reset.empty()) { + // Chisel gives every module a reset; a register file without one + // still has the port. It exists on the macro, connected to nothing. + Input(s.reset); + } + // Banks first: a banked read port has one address per bank, of the + // bank-local width. + const int banks = std::max(s.banks, 1); + if (s.words % banks != 0) { + Refuse("words (" + std::to_string(s.words) + + ") is not a multiple of banks (" + std::to_string(banks) + ")"); + } + const int words_per_bank = s.words / banks; + const int A_bank = AddrBits(words_per_bank); + const bool interleaved = s.bank_order == "interleaved"; + auto bank_of + = [&](int n) { return interleaved ? n % banks : n / words_per_bank; }; + auto local_of + = [&](int n) { return interleaved ? n / banks : n % words_per_bank; }; + if (s.zero_word >= s.words || (s.zero_word >= 0 && words_per_bank < 2)) { + Refuse("zero_word " + std::to_string(s.zero_word) + + " is not a word of a bank that keeps another"); + } + // Banks in a grid: bank k stands in column k % bank_cols, bank row + // k / bank_cols, bank rows stacked upward from the footer. + const int bank_cols = s.bank_columns > 0 ? s.bank_columns : banks; + if (bank_cols > banks || banks % bank_cols != 0) { + Refuse("bank_columns (" + std::to_string(bank_cols) + + ") does not divide banks (" + std::to_string(banks) + ")"); + } + const int bank_rows = banks / bank_cols; + // Bit folds: band f holds bits [f * bits_per_fold, (f + 1) * bits_per_fold) + // of every word, stacked above band f - 1 inside the bank; the last + // band is short when the folds do not divide the bits. + const int folds = std::max(s.bit_folds, 1); + if (folds > s.bits) { + Refuse("bit_folds (" + std::to_string(folds) + ") exceeds bits (" + + std::to_string(s.bits) + ")"); + } + const int bits_per_fold = (s.bits + folds - 1) / folds; + for (int r = 0; r < R; ++r) { + if (s.read[r].banked() + && static_cast(s.read[r].bank_addr.size()) != banks) { + Refuse("read port " + std::to_string(r) + " names " + + std::to_string(s.read[r].bank_addr.size()) + + " banks, the array has " + std::to_string(banks)); + } + } + // raddr[r][k] is bank k's literals for a banked port; raddr[r][0] the + // one address of a plain port. + std::vector>> raddr(R); + std::vector> waddr(W), wdata(W); + std::vector wen(W, nullptr); + for (int r = 0; r < R; ++r) { + if (s.read[r].banked()) { + raddr[r].resize(banks); + for (int k = 0; k < banks; ++k) { + for (int i = 0; i < A_bank; ++i) { + raddr[r][k].push_back(Net(Bit(s.read[r].bank_addr[k], i))); + Input(Bit(s.read[r].bank_addr[k], i)); + } + } + } else { + raddr[r].resize(1); + for (int i = 0; i < A; ++i) { + raddr[r][0].push_back(Net(Bit(s.read[r].addr, i))); + Input(Bit(s.read[r].addr, i)); + } + } + } + for (int w = 0; w < W; ++w) { + for (int i = 0; i < A; ++i) { + waddr[w].push_back(Net(Bit(s.write[w].addr, i))); + Input(Bit(s.write[w].addr, i)); + } + for (int b = 0; b < s.bits; ++b) { + wdata[w].push_back(Net(Bit(s.write[w].data, b))); + Input(Bit(s.write[w].data, b)); + } + if (!s.write[w].en.empty()) { + wen[w] = Net(s.write[w].en); + Input(s.write[w].en); + } + } + dbNet* vdd = Net("VDD"); + vdd->setSigType(odb::dbSigType::POWER); + vdd->setSpecial(); + dbNet* vss = Net("VSS"); + vss->setSigType(odb::dbSigType::GROUND); + vss->setSpecial(); + + // ---- geometry -------------------------------------------------------- + // A word is `rows_per_word` standard rows tall; a bit column holds one + // tile per word. The header column to the left holds the decode for + // that word: R read selects, W write selects, the hold term. + // + // Cells per tile: flop, inverter (Q from QN), W-way write mux as AO22 + // pairs and an OR2 tree, and per read port one cell of that port's + // bitline: a word pair's AOI22 leaf in the pair's first tile, a node + // of the NAND2/NOR2 tree over the leaves in its second (see the read + // bitlines below). A fix-up inverter rides with a node. + // W + 1 terms (the writes and the hold) in pairs: AO22s, an odd last + // term an AND2 + // Clock-gated, the hold term is gone; one write port leaves no mux. + const int write_terms = W + (gated ? 0 : 1); + const int write_pairs = write_terms > 1 ? (write_terms + 1) / 2 : 0; + const int read_node_w = static_cast( + std::max({nand2_->getWidth(), nor2_->getWidth(), or2_->getWidth()}) + + inv_->getWidth()); + // A tile holds a leaf or a node per port; the odd word out of a bank + // with an odd count holds its NAND2 leaf and a node. + const int read_cell_w + = std::max({static_cast(aoi22_->getWidth()), + read_node_w, + static_cast(nand2_->getWidth() + + std::max({nand2_->getWidth(), + nor2_->getWidth(), + or2_->getWidth()}))}); + const int tile_cells_w = static_cast( + flop_->getWidth() + inv_->getWidth() + write_pairs * ao22_->getWidth() + + std::max(write_pairs - 1, 0) * or2_->getWidth() + R * read_cell_w); + int rows_per_word = 1; + // Keep a tile near square-ish: at most ~40 sites wide. + while (tile_cells_w / rows_per_word > 40 * site_w_ && rows_per_word < 8) { + rows_per_word *= 2; + } + // Rows are filled least-first, so a row is at most one widest cell over + // the average; the header carries that slack. + const int widest = static_cast(std::max({flop_->getWidth(), + ao22_->getWidth(), + aoi22_->getWidth(), + or2_->getWidth(), + and2_->getWidth()})); + // A tile is as wide as its fullest row once its cells are in, in the + // order the builder drops them: the flop, Q's inverter, the write mux, + // then a bitline cell per port. The flop alone is most of a row, so + // an average-plus-widest-cell estimate leaves a third of the array empty. + int tile_w = 0; + { + std::vector fill(rows_per_word, 0); + auto drop + = [&](int w) { *std::min_element(fill.begin(), fill.end()) += w; }; + drop(static_cast(flop_->getWidth())); + if (g_pins.flop_inverted) { + drop(static_cast(inv_->getWidth())); + } + for (int i = 0; i < write_pairs; ++i) { + drop(static_cast(ao22_->getWidth())); + } + for (int i = 0; i + 1 < write_pairs; ++i) { + drop(static_cast(or2_->getWidth())); + } + for (int r = 0; r < R; ++r) { + drop(read_cell_w); + } + tile_w = (*std::max_element(fill.begin(), fill.end()) + site_w_ - 1) + / site_w_ * site_w_; + } + // Header: per word, (R+W) decodes of (A+1) literals as AND2 trees, the + // address inverters live above the array. hold_n = ~(OR of write + // selects), or the word's clock gate enabled by that OR. + const int header_cells_w = static_cast( + (R + W) * A * and2_->getWidth() + (W - 1) * or2_->getWidth() + + (gated ? icg_->getWidth() : inv_->getWidth())); + const int header_w = ((header_cells_w + rows_per_word - 1) / rows_per_word + + widest + site_w_ - 1) + / site_w_ * site_w_; + // A service column every `tap_columns` bit columns: the tap cell and + // then `service_sites` empty sites on every row. The placement is + // legal without them; what needs the room is what comes after -- the + // clock tree's buffers and any repair -- and the slack inside a tile + // is a few sites at a time, which a BUFx24 cannot use. Contiguous free + // sites next to the flops are where a clock buffer wants to be. + const int tap_w = (tap_ ? static_cast(tap_->getWidth()) : 0) + + std::max(s.service_sites, 0) * site_w_; + const int tap_every = std::max(s.tap_columns, 1); + const int tap_cols = (tap_ || s.service_sites > 0) + ? (bits_per_fold + tap_every - 1) / tap_every + : 0; + // Banks: the word column folded into `banks` columns, `bank_cols` of + // them side by side and the rest stacked, each with its own header and + // service columns, so a 256-word file is not ten times taller than it + // is wide and a 128-bit one not ten times wider than tall. A plain read + // port's bitline is one OR tree per bank and a final OR across banks in + // a footer band; a banked port's bank trees are its outputs and need no + // footer. + const int bank_w = header_w + bits_per_fold * tile_w + tap_cols * tap_w; + // A bank is `folds` bands tall, each band every word of the bank. + const int band_h_rows = words_per_bank * rows_per_word; + const int bank_h_rows = folds * band_h_rows; + int plain_reads = 0; + for (int r = 0; r < R; ++r) { + plain_reads += s.read[r].banked() ? 0 : 1; + } + // Footer: (banks-1) OR2 per read port per bit, as many rows as it takes. + const int core_w = bank_cols * bank_w; + const int footer_cells_w = banks > 1 + ? (banks - 1) * plain_reads * s.bits + * static_cast(or2_->getWidth()) + : 0; + const int footer_rows + = footer_cells_w > 0 ? (footer_cells_w + core_w - 1) / core_w + 1 : 0; + // The address band above the array: an inverter per address literal, + // and with a read latency a flop ahead of each read one, each cell with + // a spare site after it; as many rows as that takes. + int read_addr_bits = 0; + for (int r = 0; r < R; ++r) { + read_addr_bits += s.read[r].banked() ? banks * A_bank : A; + } + const int inv_cell_w = static_cast(inv_->getWidth()) + site_w_; + const int reg_cell_w + = s.read_latency > 0 ? static_cast(flop_->getWidth()) + site_w_ : 0; + const int band_w + = read_addr_bits * (inv_cell_w + reg_cell_w) + W * A * inv_cell_w; + const int inv_rows = std::max(1, (band_w + core_w - 1) / core_w); + const int total_rows = footer_rows + bank_rows * bank_h_rows + inv_rows; + + // Core at a site multiple in from the die so a parent's ring fits. + core_x0_ = 10 * site_w_; + core_y0_ = 2 * row_h_; + const int die_w = core_w + 20 * site_w_; + const int die_h = total_rows * row_h_ + 4 * row_h_; + block_->setDieArea(odb::Rect(0, 0, die_w, die_h)); + for (int r = 0; r < total_rows; ++r) { + std::string rn = "ROW_" + std::to_string(r); + odb::dbRow::create( + block_, + rn.c_str(), + site_, + core_x0_, + core_y0_ + r * row_h_, + r % 2 == 0 ? odb::dbOrientType::R0 : odb::dbOrientType::MX, + odb::dbRowDir::HORIZONTAL, + core_w / site_w_, + site_w_); + } + + // ---- address inverters, top band ------------------------------------ + // Periphery: their inputs are the address ports, their outputs the + // widest nets of the block. In netlist mode they are left unplaced for + // the parent (see the end of Run). In a macro, the macro's own repair + // may upsize one, and a cell that may be resized is one that may move: + // PLACED, not FIRM, with a spare site after each for INVx1 to INVx2. + std::vector>> raddr_n(R); + std::vector> waddr_n(W); + periphery_ = true; + { + Cursor c{total_rows - 1, 0}; + // A cell and its spare site, on the next band row down if this one + // is full. + auto band = [&](dbMaster* m, + const std::string& name, + const std::vector>& conns) { + if (c.x + static_cast(m->getWidth()) > core_w) { + c = Cursor{c.row - 1, 0}; + } + Place(c, m, name, conns) + ->setPlacementStatus(odb::dbPlacementStatus::PLACED); + c.x += site_w_; + }; + for (int r = 0; r < R; ++r) { + raddr_n[r].resize(raddr[r].size()); + for (size_t k = 0; k < raddr[r].size(); ++k) { + for (size_t i = 0; i < raddr[r][k].size(); ++i) { + const std::string base + = "rd" + std::to_string(r) + "_k" + std::to_string(k); + if (s.read_latency > 0) { + // The registered address: the flop's QN is the inverted + // literal, one inverter gives the true one (the other way + // round for a Q flop). The decode reads only these. + const std::string reg = s.read_reg_name.empty() + ? base + "_areg" + std::to_string(i) + : Fmt(s.read_reg_name, + {{"port", r}, + {"bank", static_cast(k)}, + {"bit", static_cast(i)}}); + dbNet* out = Net(base + "_aq" + std::to_string(i)); + band(flop_, + reg, + {{g_pins.flop[0], raddr[r][k][i]}, + {g_pins.flop[1], clock}, + {g_pins.flop[2], out}}); + dbNet* other = Net(base + "_ai" + std::to_string(i)); + band(inv_, + other->getName(), + {{g_pins.inv[0], out}, {g_pins.inv[1], other}}); + raddr[r][k][i] = g_pins.flop_inverted ? other : out; + raddr_n[r][k].push_back(g_pins.flop_inverted ? out : other); + continue; + } + dbNet* y = Net(base + "_na" + std::to_string(i)); + band(inv_, + y->getName(), + {{g_pins.inv[0], raddr[r][k][i]}, {g_pins.inv[1], y}}); + raddr_n[r][k].push_back(y); + } + } + } + for (int w = 0; w < W; ++w) { + for (int i = 0; i < A; ++i) { + dbNet* y = Net("wr" + std::to_string(w) + "_na" + std::to_string(i)); + band(inv_, + y->getName(), + {{g_pins.inv[0], waddr[w][i]}, {g_pins.inv[1], y}}); + waddr_n[w].push_back(y); + } + } + if (c.row < total_rows - inv_rows) { + Refuse("address band overflow: widen the band estimate"); + } + } + periphery_ = false; + + // ---- per band and word: header decode, then tiles across the bits ---- + // The decode is per (band, word): a band is its own rows, so the word + // selects it needs are made again beside it. Cheap next to the tiles. + std::vector> rsel(R, std::vector(s.words)); + std::vector> wsel(W, std::vector(s.words)); + std::vector hold(s.words); + std::vector gclk(s.words, nullptr); + // Read bitline inputs per (r, b), down the column: each word's select + // and stored value, paired into AOI22 leaves once all tiles are in. + std::vector>> leaves( + R, std::vector>(s.bits)); + std::vector>> leaf_sel( + R, std::vector>(s.bits)); + // The word each leaf reads, for its bank and the tile its OR node sits in. + std::vector>> leaf_word( + R, std::vector>(s.bits)); + tiles_.assign(static_cast(s.words) * s.bits, Tile{}); + + auto literals = + [&](const std::vector& a, const std::vector& an, int n) { + std::vector l; + for (size_t i = 0; i < a.size(); ++i) { + l.push_back(((n >> i) & 1) ? a[i] : an[i]); + } + return l; + }; + + for (int f = 0; f < folds; ++f) { + for (int n = 0; n < s.words; ++n) { + if (n == s.zero_word) { + continue; // no storage, no selects: it reads 0, a write is dropped + } + const int bank = bank_of(n); + const int row0 = footer_rows + (bank / bank_cols) * bank_h_rows + + f * band_h_rows + local_of(n) * rows_per_word; + const int bank_x0 = (bank % bank_cols) * bank_w; + // Header column, spread over this word's rows. + std::vector hc; + for (int k = 0; k < rows_per_word; ++k) { + hc.push_back(Cursor{row0 + k, bank_x0}); + } + auto hcur = [&]() -> Cursor& { return Least(hc); }; + periphery_ = true; + std::string wn = "w" + std::to_string(n) + + (folds > 1 ? "_f" + std::to_string(f) : ""); + for (int r = 0; r < R; ++r) { + const bool banked = s.read[r].banked(); + rsel[r][n] = Decode(hcur(), + wn + "_rsel" + std::to_string(r), + literals(raddr[r][banked ? bank : 0], + raddr_n[r][banked ? bank : 0], + banked ? local_of(n) : n)); + } + std::vector wsels; + for (int w = 0; w < W; ++w) { + std::vector l = literals(waddr[w], waddr_n[w], n); + if (wen[w] != nullptr) { + l.push_back(wen[w]); + } + wsel[w][n] = Decode(hcur(), wn + "_wsel" + std::to_string(w), l); + wsels.push_back(wsel[w][n]); + } + // hold = ~(wsel0 | wsel1 | ...): the word keeps its value. + dbNet* any_write + = wsels.size() == 1 + ? wsels[0] + : OrTree(wn + "_anyw", wsels, [&](int, int) -> Cursor& { + return hcur(); + }); + if (gated) { + // The word's clock runs only on a write. SE is the latch's other + // enable (ENA | SE); tied to the same select it changes nothing. + gclk[n] = Net(wn + "_gclk"); + Place(hcur(), + icg_, + wn + "_icg", + {{g_pins.icg[0], any_write}, + {g_pins.icg[1], any_write}, + {g_pins.icg[2], clock}, + {g_pins.icg[3], gclk[n]}}); + } else { + hold[n] = Net(wn + "_hold"); + Place(hcur(), + inv_, + hold[n]->getName(), + {{g_pins.inv[0], any_write}, {g_pins.inv[1], hold[n]}}); + } + periphery_ = false; + for (auto& c : hc) { + if (c.x > bank_x0 + header_w) { + Refuse("header column overflow at word " + std::to_string(n) + + ": widen the header estimate"); + } + } + + // Tiles, this band's bits. + int x = bank_x0 + header_w; + for (int bl = 0; bl < bits_per_fold && f * bits_per_fold + bl < s.bits; + ++bl) { + const int b = f * bits_per_fold + bl; + if (tap_cols > 0 && bl % tap_every == 0) { + if (tap_) { + for (int k = 0; k < rows_per_word; ++k) { + Cursor tc{row0 + k, x}; + Place(tc, + tap_, + "tap_" + wn + "_c" + std::to_string(bl / tap_every) + "_r" + + std::to_string(k), + {}); + } + } + x += tap_w; // the rest of the service column stays empty + } + std::string tn = wn + "_b" + std::to_string(b); + std::vector tc; + for (int k = 0; k < rows_per_word; ++k) { + tc.push_back(Cursor{row0 + k, x}); + } + auto tcur = [&]() -> Cursor& { return Least(tc); }; + + // Storage. The flop's QN holds ~value; Q is recovered by an inverter. + // One gated write port: D is the write data bit. + dbNet* d = gated && W == 1 ? wdata[0][b] : Net(tn + "_d"); + dbNet* qn = Net(tn + "_qn"); + dbNet* q = g_pins.flop_inverted ? Net(tn + "_q") : qn; + Place(tcur(), + flop_, + s.store_name.empty() + ? tn + "_ff" + : Fmt(s.store_name, {{"word", n}, {"bit", b}}), + {{g_pins.flop[0], d}, + {g_pins.flop[1], gated ? gclk[n] : clock}, + {g_pins.flop[2], qn}}); + if (g_pins.flop_inverted) { + Place(tcur(), + inv_, + tn + "_qinv", + {{g_pins.inv[0], qn}, {g_pins.inv[1], q}}); + } + // Write mux: d = OR_w (wsel_w & wdata_w[b]) | (hold & q), as AO22 + // pairs then an OR2 tree. + std::vector> terms; + for (int w = 0; w < W; ++w) { + terms.emplace_back(wsel[w][n], wdata[w][b]); + } + if (!gated) { + terms.emplace_back(hold[n], q); + } + std::vector partial; + for (size_t i = 0; write_pairs > 0 && i < terms.size(); i += 2) { + dbNet* y = Net(tn + "_wm" + std::to_string(i / 2)); + if (i + 1 < terms.size()) { + Place(tcur(), + ao22_, + y->getName(), + {{g_pins.ao22[0], terms[i].first}, + {g_pins.ao22[1], terms[i].second}, + {g_pins.ao22[2], terms[i + 1].first}, + {g_pins.ao22[3], terms[i + 1].second}, + {g_pins.ao22[4], y}}); + } else { + Place(tcur(), + and2_, + y->getName(), + {{g_pins.and2[0], terms[i].first}, + {g_pins.and2[1], terms[i].second}, + {g_pins.and2[2], y}}); + } + partial.push_back(y); + } + dbNet* mux = partial.empty() ? d + : partial.size() == 1 + ? partial[0] + : OrTree(tn + "_wo", + partial, + [&](int, int) -> Cursor& { return tcur(); }); + // The OR tree's root must be the flop's D net: alias by re-connecting. + if (mux != d) { + // Move every iterm on `mux` to `d`. + std::vector its(mux->getITerms().begin(), + mux->getITerms().end()); + for (auto* it : its) { + it->disconnect(); + it->connect(d); + } + dbNet::destroy(mux); + } + // This word's inputs to each port's bitline. + for (int r = 0; r < R; ++r) { + leaves[r][b].push_back(q); + leaf_sel[r][b].push_back(rsel[r][n]); + leaf_word[r][b].push_back(n); + } + // Keep the row cursors: the read OR trees fill the tile's leftover. + tiles_[static_cast(n) * s.bits + b] = Tile{tc, x + tile_w}; + x += tile_w; + } + } + } + + // ---- read bitlines: OR trees down each column ------------------------ + // A node covering words [lo, hi) is dropped into the tile of word + // (lo+hi)/2 in that bit column, round-robin over its rows. + std::vector> rdata(R); + std::vector footer; + for (int k = 0; k < footer_rows; ++k) { + footer.push_back(Cursor{k, 0}); + } + for (int r = 0; r < R; ++r) { + for (int b = 0; b < s.bits; ++b) { + std::string prefix = "rd" + std::to_string(r) + "_b" + std::to_string(b); + std::vector bank_roots; + for (int k = 0; k < banks; ++k) { + // The bank's words in entry order; ascending words keep it. + std::vector bank_q, bank_sel; + std::vector bank_words; + for (size_t i = 0; i < leaves[r][b].size(); ++i) { + if (bank_of(leaf_word[r][b][i]) == k) { + bank_q.push_back(leaves[r][b][i]); + bank_sel.push_back(leaf_sel[r][b][i]); + bank_words.push_back(leaf_word[r][b][i]); + } + } + bank_roots.push_back(Bitline( + prefix + "_k" + std::to_string(k), + bank_sel, + bank_q, + [&](int word) -> Cursor& { + return Least(tiles_[word * s.bits + b].rows); + }, + bank_words)); + } + // Each root becomes an output net with the RTL's name: one per + // bank for a banked port, one after the footer OR for a plain one. + auto emit = [&](dbNet* root, const std::string& name) { + std::vector its(root->getITerms().begin(), + root->getITerms().end()); + dbNet* out = Net(name); + for (auto* it : its) { + it->disconnect(); + it->connect(out); + } + dbNet::destroy(root); + Output(name, out); + rdata[r].push_back(out); + }; + if (s.read[r].banked()) { + for (int k = 0; k < banks; ++k) { + emit(bank_roots[k], Bit(s.read[r].bank_data[k], b)); + } + } else { + dbNet* root + = banks == 1 + ? bank_roots[0] + : OrTree(prefix + "_f", bank_roots, [&](int, int) -> Cursor& { + return Least(footer); + }); + emit(root, Bit(s.read[r].data, b)); + } + } + } + for (size_t i = 0; i < tiles_.size(); ++i) { + for (const auto& c : tiles_[i].rows) { + if (c.x > tiles_[i].x_end) { + Refuse( + "tile " + std::to_string(i) + " overflowed by " + + std::to_string(c.x - tiles_[i].x_end) + + " dbu once its read OR trees were in: widen the tile estimate"); + } + } + } + for (const auto& c : footer) { + if (c.x > core_w) { + Refuse("footer overflow: widen the footer estimate"); + } + } + if (max_x_ > core_w) { + Refuse("placement ran past the core: " + std::to_string(max_x_) + " > " + + std::to_string(core_w)); + } + + // ---- pins on the die edge -------------------------------------------- + // Read data along the bottom and write data along the top, on the + // vertical layer; addresses, enables and the clock on the left, on the + // horizontal layer. Centres sit on the platform's track grid for that + // layer so a parent's macro placer can align them (MPL-0005 otherwise). + const int track_off = static_cast(s.pin_track_offset_um * dbu_); + const int track_pitch + = std::max(static_cast(s.pin_track_pitch_um * dbu_), 1); + auto on_track = [&](int v) { + int k = (v - track_off + track_pitch - 1) / track_pitch; + return track_off + std::max(k, 0) * track_pitch; + }; + const int edge = pin_w_; // pin centre this far in from the die edge + int px = on_track(core_x0_ + track_pitch); + auto bottom = [&](const std::string& name) { + Pin(block_->findBTerm(name.c_str()), layer_v_, px, edge); + px += track_pitch; + }; + for (int r = 0; r < R; ++r) { + for (int b = 0; b < s.bits; ++b) { + if (s.read[r].banked()) { + for (int k = 0; k < banks; ++k) { + bottom(Bit(s.read[r].bank_data[k], b)); + } + } else { + bottom(Bit(s.read[r].data, b)); + } + } + } + const int px_bottom_end = px; + px = on_track(core_x0_ + track_pitch); + for (int w = 0; w < W; ++w) { + for (int b = 0; b < s.bits; ++b) { + Pin(block_->findBTerm(Bit(s.write[w].data, b).c_str()), + layer_v_, + px, + die_h - edge); + px += track_pitch; + } + } + int py = on_track(core_y0_ + track_pitch); + auto left = [&](const std::string& name) { + Pin(block_->findBTerm(name.c_str()), layer_h_, edge, py); + py += track_pitch; + }; + left(s.clock); + if (!s.reset.empty()) { + left(s.reset); + } + for (int r = 0; r < R; ++r) { + for (size_t k = 0; k < raddr[r].size(); ++k) { + for (size_t i = 0; i < raddr[r][k].size(); ++i) { + left(s.read[r].banked() + ? Bit(s.read[r].bank_addr[k], static_cast(i)) + : Bit(s.read[r].addr, static_cast(i))); + } + } + } + for (int w = 0; w < W; ++w) { + for (int i = 0; i < A; ++i) { + left(Bit(s.write[w].addr, i)); + } + if (wen[w] != nullptr) { + left(s.write[w].en); + } + } + if (std::max(px, px_bottom_end) > die_w || py > die_h) { + Refuse( + "more pins than the die edge holds at the track pitch; the array " + "is too narrow for its ports (widen with fewer banks or more bits)"); + } + + logger_->report( + "generate_regfile: {} {}x{} {}R{}W in {} bank(s) as {} x {}, {} bit " + "fold(s): " + "{} rows of {} sites, die {} x {} um, {} instances", + s.module, + s.words, + s.bits, + R, + W, + banks, + bank_cols, + bank_rows, + folds, + total_rows, + core_w / site_w_, + die_w / static_cast(tech->getLefUnits()), + die_h / static_cast(tech->getLefUnits()), + block_->getInsts().size()); + // In netlist mode the core alone is the generator's: the tiles and the + // read bitlines, FIRM. The periphery -- address inverters, read address + // registers, each word's decode, hold -- is left unplaced, so the parent's + // flow places, sizes and buffers it like any logic: it alone knows the + // drivers and loads outside the array. Fixed by the generator, an + // address inverter drove its literal into every word's decode at a + // 3.7 ns slew on XiangShan's ROB that nothing was allowed to repair. The + // header column and the address band stay as free sites beside the core. + // A macro is the generator's to the last cell and keeps its periphery. + if (s.mode == "netlist") { + // The abstract a parent's macro placer sees before the block dissolves + // into its rows: each port's pin where its connections land, at the + // centroid of the cells on its net -- a write data bit over its + // column's write muxes, a read data bit at its bitline's root, an + // address among its inverters and decode. Edge pins would tell the + // placer's flips nothing about where the wires go; these pins are + // never routed, the block dissolves first. + std::vector>> moves; + for (dbBTerm* t : block_->getBTerms()) { + dbNet* net = t->getNet(); + if (net == nullptr) { + continue; + } + long long sx = 0, sy = 0; + int n = 0; + for (odb::dbITerm* it : net->getITerms()) { + const odb::Rect b = it->getInst()->getBBox()->getBox(); + sx += b.xCenter(); + sy += b.yCenter(); + ++n; + } + if (n > 0) { + // on the track grid, as the edge pins are (MPL-0005 otherwise) + moves.push_back({t, + {on_track(static_cast(sx / n)), + on_track(static_cast(sy / n))}}); + } + } + for (auto& [t, xy] : moves) { + std::vector old(t->getBPins().begin(), t->getBPins().end()); + for (odb::dbBPin* bp : old) { + odb::dbBPin::destroy(bp); + } + Pin(t, layer_h_, xy.first, xy.second); + } + for (dbInst* inst : periphery_insts_) { + inst->setPlacementStatus(odb::dbPlacementStatus::UNPLACED); + } + } + return block_; +} + +} // namespace + +Spec ReadSpec(const std::string& path) +{ + std::ifstream in(path); + if (!in) { + Refuse("cannot read spec " + path); + } + Spec s; + g_pins = Pins(); + std::string line; + int lineno = 0; + while (std::getline(in, line)) { + ++lineno; + auto hash = line.find('#'); + if (hash != std::string::npos) { + line = line.substr(0, hash); + } + std::istringstream ss(line); + std::string key; + if (!(ss >> key)) { + continue; + } + std::vector v; + for (std::string t; ss >> t;) { + v.push_back(t); + } + auto need = [&](size_t n) { + if (v.size() != n) { + Refuse(path + ":" + std::to_string(lineno) + ": `" + key + "` takes " + + std::to_string(n) + " value(s)"); + } + }; + if (key == "module") { + need(1); + s.module = v[0]; + } else if (key == "mode") { + need(1); + if (v[0] != "macro" && v[0] != "netlist") { + Refuse(path + ":" + std::to_string(lineno) + + ": mode is macro or netlist, not `" + v[0] + "`"); + } + s.mode = v[0]; + } else if (key == "words") { + need(1); + s.words = std::stoi(v[0]); + } else if (key == "bits") { + need(1); + s.bits = std::stoi(v[0]); + } else if (key == "clock") { + need(1); + s.clock = v[0]; + } else if (key == "reset") { + need(1); + s.reset = v[0]; + } else if (key == "read") { + need(2); + s.read.push_back(Port{v[0], v[1], ""}); + } else if (key == "read_banked") { + if (v.size() < 2 || v.size() % 2 != 0) { + Refuse(path + ":" + std::to_string(lineno) + + ": `read_banked` takes addr data pairs, one per bank"); + } + Port port; + for (size_t i = 0; i < v.size(); i += 2) { + port.bank_addr.push_back(v[i]); + port.bank_data.push_back(v[i + 1]); + } + s.read.push_back(port); + } else if (key == "write") { + if (v.size() != 2 && v.size() != 3) { + Refuse(path + ":" + std::to_string(lineno) + + ": `write` takes addr data [en]"); + } + s.write.push_back(Port{v[0], v[1], v.size() == 3 ? v[2] : ""}); + } else if (key == "cell") { + need(2); + const std::string& kind = v[0]; + if (kind == "flop") { + s.cells.flop = v[1]; + } else if (kind == "and2") { + s.cells.and2 = v[1]; + } else if (kind == "or2") { + s.cells.or2 = v[1]; + } else if (kind == "ao22") { + s.cells.ao22 = v[1]; + } else if (kind == "aoi22") { + s.cells.aoi22 = v[1]; + } else if (kind == "nand2") { + s.cells.nand2 = v[1]; + } else if (kind == "nor2") { + s.cells.nor2 = v[1]; + } else if (kind == "inv") { + s.cells.inv = v[1]; + } else if (kind == "tap") { + s.cells.tap = v[1]; + } else if (kind == "tie_lo") { + s.cells.tie_lo = v[1]; + } else if (kind == "icg") { + s.cells.icg = v[1]; + } else { + Refuse(path + ":" + std::to_string(lineno) + ": unknown cell kind " + + kind); + } + } else if (key == "pins") { + if (v.size() < 2) { + Refuse(path + ":" + std::to_string(lineno) + + ": `pins ...`"); + } + std::vector pins(v.begin() + 1, v.end()); + const std::string& kind = v[0]; + auto set = [&](std::vector& dst, size_t n) { + if (pins.size() != n) { + Refuse(path + ":" + std::to_string(lineno) + ": `pins " + kind + + "` takes " + std::to_string(n) + " pin names"); + } + dst = pins; + }; + if (kind == "flop") { + set(g_pins.flop, 3); + } else if (kind == "and2") { + set(g_pins.and2, 3); + } else if (kind == "or2") { + set(g_pins.or2, 3); + } else if (kind == "ao22") { + set(g_pins.ao22, 5); + } else if (kind == "aoi22") { + set(g_pins.aoi22, 5); + } else if (kind == "nand2") { + set(g_pins.nand2, 3); + } else if (kind == "nor2") { + set(g_pins.nor2, 3); + } else if (kind == "inv") { + set(g_pins.inv, 2); + } else if (kind == "icg") { + set(g_pins.icg, 4); + } else { + Refuse(path + ":" + std::to_string(lineno) + ": unknown pins kind " + + kind); + } + } else if (key == "write_style") { + need(1); + if (v[0] != "mux" && v[0] != "clock_gate") { + Refuse(path + ":" + std::to_string(lineno) + + ": write_style is mux or clock_gate"); + } + s.write_style = v[0]; + } else if (key == "flop_output") { + need(1); + if (v[0] == "QN") { + g_pins.flop_inverted = true; + } else if (v[0] == "Q") { + g_pins.flop_inverted = false; + } else { + Refuse(path + ":" + std::to_string(lineno) + + ": flop_output is Q or QN"); + } + } else if (key == "pin_layer" || key == "pin_layer_h") { + need(1); + s.pin_layer_h = v[0]; + } else if (key == "pin_layer_v") { + need(1); + s.pin_layer_v = v[0]; + } else if (key == "pin_track") { + need(2); + s.pin_track_offset_um = std::stod(v[0]); + s.pin_track_pitch_um = std::stod(v[1]); + } else if (key == "tap_columns") { + need(1); + s.tap_columns = std::stoi(v[0]); + } else if (key == "service_sites") { + need(1); + s.service_sites = std::stoi(v[0]); + } else if (key == "banks") { + need(1); + s.banks = std::stoi(v[0]); + } else if (key == "bank_columns") { + need(1); + s.bank_columns = std::stoi(v[0]); + } else if (key == "bit_folds") { + need(1); + s.bit_folds = std::stoi(v[0]); + } else if (key == "read_latency") { + need(1); + if (v[0] != "0" && v[0] != "1") { + Refuse(path + ":" + std::to_string(lineno) + + ": read_latency is 0 or 1"); + } + s.read_latency = std::stoi(v[0]); + } else if (key == "bank_order") { + need(1); + if (v[0] != "contiguous" && v[0] != "interleaved") { + Refuse(path + ":" + std::to_string(lineno) + + ": bank_order is contiguous or interleaved"); + } + s.bank_order = v[0]; + } else if (key == "zero_word") { + need(1); + s.zero_word = std::stoi(v[0]); + } else if (key == "store_name") { + need(1); + s.store_name = v[0]; + } else if (key == "read_reg_name") { + need(1); + s.read_reg_name = v[0]; + } else if (key == "lib") { + need(2); + double x = std::stod(v[1]); + const std::string& knob = v[0]; + if (knob == "gate_delay_ps") { + s.lib.gate_delay_ps = x; + } else if (knob == "wire_factor") { + s.lib.wire_factor = x; + } else if (knob == "input_load_ff") { + s.lib.input_load_ff = x; + } else if (knob == "clock_load_ff") { + s.lib.clock_load_ff = x; + } else if (knob == "leakage_nw_per_cell") { + s.lib.leakage_nw_per_cell = x; + } else if (knob == "output_max_cap_ff") { + s.lib.output_max_cap_ff = x; + } else if (knob == "hold_ps") { + s.lib.hold_ps = x; + } else { + Refuse(path + ":" + std::to_string(lineno) + ": unknown lib knob " + + knob); + } + } else { + Refuse(path + ":" + std::to_string(lineno) + ": unknown key `" + key + + "`"); + } + } + if (s.module.empty()) { + Refuse(path + ": no `module`"); + } + if (s.cells.flop.empty() || s.cells.and2.empty() || s.cells.or2.empty() + || s.cells.ao22.empty() || s.cells.inv.empty()) { + Refuse(path + ": cells flop, and2, or2, ao22 and inv are all required"); + } + return s; +} + +odb::dbBlock* Generate(odb::dbDatabase* db, + utl::Logger* logger, + const Spec& spec) +{ + Builder b(db, logger, spec); + return b.Run(); +} + +void WriteVerilog(odb::dbBlock* block, const std::string& path) +{ + std::ofstream out(path); + if (!out) { + Refuse("cannot write " + path); + } + // Group bterms into buses by `name[i]`. + struct Bus + { + odb::dbIoType io; + int hi = -1; + bool scalar = false; + }; + std::map buses; + std::vector order; + for (odb::dbBTerm* t : block->getBTerms()) { + std::string n = t->getName(); + if (t->getSigType() != odb::dbSigType::SIGNAL) { + continue; + } + auto lb = n.rfind('['); + std::string base = n; + int idx = -1; + if (lb != std::string::npos && n.back() == ']') { + base = n.substr(0, lb); + idx = std::stoi(n.substr(lb + 1, n.size() - lb - 2)); + } + auto it = buses.find(base); + if (it == buses.end()) { + order.push_back(base); + it = buses.emplace(base, Bus{t->getIoType(), -1, idx < 0}).first; + } + it->second.hi = std::max(it->second.hi, idx); + } + out << "// Generated by generate_regfile. Structural: every cell placed.\n"; + out << "module " << block->getName() << "(\n"; + for (size_t i = 0; i < order.size(); ++i) { + const Bus& b = buses[order[i]]; + out << " " << (b.io == odb::dbIoType::INPUT ? "input" : "output"); + if (!b.scalar) { + out << " [" << b.hi << ":0]"; + } + out << " " << order[i] << (i + 1 < order.size() ? ",\n" : "\n"); + } + out << ");\n"; + for (odb::dbNet* n : block->getNets()) { + if (n->getBTerms().begin() != n->getBTerms().end()) { + continue; // a port + } + if (n->getSigType() == odb::dbSigType::POWER + || n->getSigType() == odb::dbSigType::GROUND) { + continue; + } + out << " wire \\" << n->getName() << " ;\n"; + } + for (odb::dbInst* inst : block->getInsts()) { + out << " " << inst->getMaster()->getName() << " \\" << inst->getName() + << " ("; + bool first = true; + for (odb::dbITerm* it : inst->getITerms()) { + odb::dbNet* n = it->getNet(); + if (n == nullptr) { + continue; + } + if (n->getSigType() == odb::dbSigType::POWER + || n->getSigType() == odb::dbSigType::GROUND) { + continue; + } + out << (first ? "" : ", ") << "." << it->getMTerm()->getName() << "("; + // A port net is written by its port name, buses with [i] intact. + out << (n->getBTerms().begin() != n->getBTerms().end() + ? n->getName() + : "\\" + n->getName() + " ") + << ")"; + first = false; + } + out << ");\n"; + } + out << "endmodule\n"; +} + +} // namespace ram::regfile diff --git a/src/ram/src/regfile.h b/src/ram/src/regfile.h new file mode 100644 index 00000000000..c3605197aaa --- /dev/null +++ b/src/ram/src/regfile.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// generate_regfile: a multi-port register file as a placed standard-cell +// macro, built directly in odb. +// +// A register file synthesised to flops is a flop per bit, a W-way write +// mux on each flop's D and an R-way read mux tree per bit: dense, and the +// legaliser cannot spread it. Here it is laid out the way a bitcell array +// is: word rows by bit columns, one tile per (word, bit) holding the flop, +// its write select and R read AND gates; the read wordlines run along the +// rows and are decoded in a header column, the read bitlines are OR trees +// in a footer row. Every cell is placed by construction; nothing is left +// to a placer. +#pragma once + +#include +#include + +namespace odb { +class dbDatabase; +class dbBlock; +class dbMaster; +class dbNet; +class dbInst; +} // namespace odb + +namespace utl { +class Logger; +} + +namespace ram::regfile { + +// One read or write port, with the RTL's own pin names so the macro drops +// in for the module it replaces. +struct Port +{ + std::string addr; // bus name; bits are [i] + std::string data; // bus name; bits are [i] + std::string en; // write ports only; empty means always enabled + // A banked read port (Chisel RegfileBank): one address and one data bus + // per bank, the bank mux outside. addr/data above are unused then. + std::vector bank_addr; + std::vector bank_data; + bool banked() const { return !bank_addr.empty(); } +}; + +// The cells the array is built from, by name in the loaded LEF. +struct Cells +{ + std::string flop; // D flip-flop, Q output (QN accepted, see below) + std::string and2; // read select AND + std::string or2; // bitline OR tree node + std::string ao22; // write mux node: (sel & new) | (hold & old) + // read bitline: AOI22 leaf per word pair, NAND2/NOR2 tree over them + std::string aoi22 = "AOI22xp5_ASAP7_75t_R"; + std::string nand2 = "NAND2xp5_ASAP7_75t_R"; + std::string nor2 = "NOR2xp33_ASAP7_75t_R"; + std::string inv; // decoder inverters + std::string tap; // well tap, one column per tap_columns + // integrated clock gate, one per word with `write_style clock_gate` + std::string icg = "ICGx1_ASAP7_75t_R"; + std::string tie_lo; // for unused inputs +}; + +// Knobs of the model liberty (views.h), asap7-shaped defaults, set in +// the spec with `lib `. Picoseconds, femtofarads, +// nanowatts, so a spec reads in plain units. +struct LibModel +{ + double gate_delay_ps = 25.0; // one loaded gate level + double wire_factor = 1.3; // on top of the gate levels + double input_load_ff = 0.6; // per driven cell input + double clock_load_ff = 10.0; // the block's clock pin as the parent sees it + double leakage_nw_per_cell = 1.0; + double output_max_cap_ff = 50.0; + double hold_ps = 0.0; +}; + +struct Spec +{ + // "macro" (an abstract and a model liberty for a parent to place) or + // "netlist" (the placed cells dropped into the parent's rows by the + // flow); the generator builds the same block either way. The flow's + // memories step reads this key too, and decides which views to ask for. + std::string mode = "macro"; + std::string module; // generated module and block name + int words = 0; + int bits = 0; + std::string clock = "clock"; + std::string reset; // optional: a port the RTL has and the array ignores + std::vector read; + std::vector write; + Cells cells; + int tap_columns = 8; // a tap column every N bit columns + int service_sites = 40; // free sites beside each tap, for the clock tree + int banks = 1; // word columns side by side; words % banks == 0 + // How many of the banks stand side by side; the rest stack below them. + // 0 means all of them, one row of banks. The outline is bank_columns + // bank widths wide and banks/bank_columns bank heights tall: a wide + // word (128 bits) wants its banks stacked, a narrow one wants them + // side by side. banks % bank_columns == 0. + int bank_columns = 0; + // The word split into this many bit bands, stacked: a 564-bit word + // is one band 564 tiles wide, or four bands of 141 one above the + // other, each with its own copy of the word decode; the last band is + // short when the folds do not divide. Banks fold words; this folds bits. + int bit_folds = 1; + // Read latency in cycles: 0 reads combinationally from the address + // ports; 1 registers each read address on the clock first, the way + // firtool writes a Chisel register file read through RegNext(addr) + // (XiangShan's IntRegFile, FpRegFile and VfRegFile). The macro + // replaces the whole module, so it has to carry that register. + int read_latency = 0; + // How words fall into banks: "contiguous" (bank n / words_per_bank, + // the default) or "interleaved" (bank n % banks, entry n / banks, as + // XiangShan's IntRegFile banks its read: bank k's entry l is word + // l * banks + k). + std::string bank_order = "contiguous"; + // A word that is the constant zero and has no storage (RISC-V x0): it + // reads 0 and a write to it is dropped. -1 for none. + int zero_word = -1; + // How a word keeps its value between writes: "mux" (the default), a + // free-running clock and a hold term in every bit's write mux; or + // "clock_gate", one integrated clock gate per word enabled by any of + // its write selects, as generate_ram writes, so a bit's D is the write + // data itself with one write port and a select mux of the writes with + // more. + std::string write_style = "mux"; + // Instance names of the flops, as patterns, so a flop carries the name + // of the RTL register bit it implements (an equivalence check matches + // sequential instances by name). {word} {bit} for the storage, + // {port} {bank} {bit} for a read address register. Empty keeps the + // generator's own names. + std::string store_name; + std::string read_reg_name; + LibModel lib; + // Pins: left and right edges on the horizontal layer, top and bottom on + // the vertical one, centred on that layer's track grid as the platform + // makes it, so a parent's macro placer can align them. + std::string pin_layer_h = "M4"; + std::string pin_layer_v = "M5"; + double pin_track_offset_um = 0.012; + double pin_track_pitch_um = 0.048; +}; + +// Reads a spec from a small `key value` text file (see README.md). +// Throws std::runtime_error with the offending line on anything it does +// not understand: a spec is either complete or refused. +Spec ReadSpec(const std::string& path); + +// Builds the netlist and placement of `spec` into a new block of `db`, +// whose tech and libs must already hold the cells named in the spec. +// Returns the block. Throws std::runtime_error naming the cell or pin +// when the library does not fit the spec. +odb::dbBlock* Generate(odb::dbDatabase* db, + utl::Logger* logger, + const Spec& spec); + +// Writes the block as a structural Verilog module, cells as instances, +// for simulation and for the flow's netlist view. +void WriteVerilog(odb::dbBlock* block, const std::string& path); + +} // namespace ram::regfile diff --git a/src/ram/src/regfile_cmd.cpp b/src/ram/src/regfile_cmd.cpp new file mode 100644 index 00000000000..6b600efab36 --- /dev/null +++ b/src/ram/src/regfile_cmd.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include +#include +#include + +#include "odb/db.h" +#include "odb/defout.h" +#include "ram/regfile.h" +#include "regfile.h" +#include "utl/Logger.h" +#include "views.h" + +namespace ram { + +void generateRegfile(odb::dbDatabase* db, + utl::Logger* logger, + const std::string& spec_path, + const std::string& verilog_path, + const std::string& def_path, + const std::string& lef_path, + const std::string& liberty_path, + const std::string& check_ports) +{ + try { + regfile::Spec spec = regfile::ReadSpec(spec_path); + if (!check_ports.empty()) { + // The spec against the RTL it stands in for, before anything is + // built: a block whose ports are not the module's is a silent + // miswire at the parent. + auto ports = regfile::ReadModulePorts(check_ports, spec.module); + auto problems = regfile::CheckPorts(spec, ports); + if (!problems.empty()) { + std::string all; + for (const auto& p : problems) { + all += "\n " + p; + } + throw std::runtime_error("spec does not match module " + spec.module + + " in " + check_ports + ":" + all); + } + } + odb::dbBlock* block = regfile::Generate(db, logger, spec); + if (!verilog_path.empty()) { + regfile::WriteVerilog(block, verilog_path); + } + if (!def_path.empty()) { + odb::DefOut writer(logger); + if (!writer.writeBlock(block, def_path.c_str())) { + throw std::runtime_error("cannot write " + def_path); + } + } + if (!lef_path.empty()) { + regfile::WriteLef(block, logger, lef_path); + } + if (!liberty_path.empty()) { + // A flow's memories directory holds .lib and _pre_layout.lib; + // the model is the same file twice, said so in its comment. + regfile::WriteLiberty(block, spec, spec.lib, liberty_path, false); + std::string pre = liberty_path; + auto dot = pre.rfind(".lib"); + if (dot != std::string::npos) { + pre = pre.substr(0, dot) + "_pre_layout.lib"; + regfile::WriteLiberty(block, spec, spec.lib, pre, true); + } + } + } catch (const std::exception& e) { + logger->error(utl::RAM, 50, "generate_regfile: {}", e.what()); + } +} + +} // namespace ram diff --git a/src/ram/src/views.cpp b/src/ram/src/views.cpp new file mode 100644 index 00000000000..6c609674453 --- /dev/null +++ b/src/ram/src/views.cpp @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// generate_regfile: abstract LEF, model liberty, and the spec-against-RTL +// port check. See views.h. +#include "views.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "odb/db.h" +#include "odb/lefout.h" +#include "regfile.h" +#include "utl/Logger.h" + +namespace ram::regfile { + +namespace { + +int Log2Ceil(int n) +{ + int k = 0; + while ((1 << k) < n) { + ++k; + } + return k; +} + +int AddrWidth(int words) +{ + return std::max(Log2Ceil(words), 1); +} + +std::string F(double v) +{ + std::ostringstream s; + s.precision(6); + s << std::fixed << v; + return s.str(); +} + +// The two-point tables FakeRAM's libs use: constant across slew and load, +// which is what a model is. +void Table2(std::ostream& o, + const char* kind, + const std::string& tmpl, + double value_ns, + int indent) +{ + std::string pad(indent, ' '); + o << pad << kind << "(" << tmpl << ") {\n" + << pad << " index_1 (\"0.009, 0.227\");\n" + << pad << " index_2 (\"0.005, 0.500\");\n" + << pad << " values ( \\\n" + << pad << " \"" << F(value_ns) << ", " << F(value_ns) << "\", \\\n" + << pad << " \"" << F(value_ns) << ", " << F(value_ns) << "\" \\\n" + << pad << " )\n" + << pad << "}\n"; +} + +void Constraint(std::ostream& o, + const std::string& cell, + const std::string& clk, + double setup_ns, + double hold_ns) +{ + o << " timing() {\n" + << " related_pin : " << clk << ";\n" + << " timing_type : setup_rising ;\n"; + Table2(o, "rise_constraint", cell + "_constraint_template", setup_ns, 12); + Table2(o, "fall_constraint", cell + "_constraint_template", setup_ns, 12); + o << " }\n" + << " timing() {\n" + << " related_pin : " << clk << ";\n" + << " timing_type : hold_rising ;\n"; + Table2(o, "rise_constraint", cell + "_constraint_template", hold_ns, 12); + Table2(o, "fall_constraint", cell + "_constraint_template", hold_ns, 12); + o << " }\n"; +} + +} // namespace + +void WriteLef(odb::dbBlock* block, utl::Logger* logger, const std::string& path) +{ + std::ofstream out(path); + if (!out) { + throw std::runtime_error("cannot write " + path); + } + odb::lefout writer(logger, out); + writer.setBloatOccupiedLayers(true); + writer.writeAbstractLef(block); +} + +void WriteLiberty(odb::dbBlock* block, + const Spec& spec, + const LibModel& m, + const std::string& path, + bool pre_layout) +{ + std::ofstream o(path); + if (!o) { + throw std::runtime_error("cannot write " + path); + } + const std::string cell = spec.module; + const int A = AddrWidth(spec.words); + const int R = static_cast(spec.read.size()); + const int W = static_cast(spec.write.size()); + const int banks = std::max(spec.banks, 1); + const int words_per_bank = spec.words / banks; + + // Gate levels, from the structure the generator built (regfile.cc). + const int read_levels + = 1 + Log2Ceil(A) + 1 + Log2Ceil(words_per_bank) + Log2Ceil(banks); + const int write_levels = 1 + Log2Ceil(A + 1) + 1 + Log2Ceil((W + 2) / 2); + const double read_ns = read_levels * m.gate_delay_ps * m.wire_factor / 1000.0; + const double setup_ns + = write_levels * m.gate_delay_ps * m.wire_factor / 1000.0; + const double hold_ns = m.hold_ps / 1000.0; + // Loads, from fanout: an address literal reaches one inverter and half + // the words' decodes; a data bit reaches every word's write mux; an + // enable reaches every word's decode. + const double addr_pf = (spec.words / 2 + 1) * m.input_load_ff / 1000.0; + const double data_pf = spec.words * m.input_load_ff / 1000.0; + const double en_pf = spec.words * m.input_load_ff / 1000.0; + const double clk_pf = m.clock_load_ff / 1000.0; + const odb::Rect die = block->getDieArea(); + const double dbu = block->getDbUnitsPerMicron(); + const double area_um2 = die.dx() / dbu * (die.dy() / dbu); + const double leakage_uw + = block->getInsts().size() * m.leakage_nw_per_cell / 1000.0; + + o << "library(" << cell << ") {\n" + << " technology (cmos);\n" + << " delay_model : table_lookup;\n" + << " revision : 1.0;\n" + << " comment : \"generate_regfile model" + << (pre_layout ? ", pre-layout" : "") + << ": loads from fanout, arcs from gate depth; replaced by the routed " + "block's abstract for a measured run\";\n" + << " time_unit : \"1ns\";\n" + << " voltage_unit : \"1V\";\n" + << " current_unit : \"1uA\";\n" + << " leakage_power_unit : \"1uW\";\n" + << " nom_process : 1;\n" + << " nom_temperature : 25.000;\n" + << " nom_voltage : 0.700;\n" + << " capacitive_load_unit (1,pf);\n" + << " pulling_resistance_unit : \"1kohm\";\n" + << " operating_conditions(tt_1.0_25.0) {\n" + << " process : 1;\n" + << " temperature : 25.000;\n" + << " voltage : 0.700;\n" + << " tree_type : balanced_tree;\n" + << " }\n" + << " default_cell_leakage_power : 0;\n" + << " default_fanout_load : 1;\n" + << " default_inout_pin_cap : 0.0;\n" + << " default_input_pin_cap : 0.0;\n" + << " default_output_pin_cap : 0.0;\n" + << " default_max_transition : 0.227;\n" + << " default_operating_conditions : tt_1.0_25.0;\n" + << " default_leakage_power_density : 0.0;\n" + << " slew_derate_from_library : 1.000;\n" + << " slew_lower_threshold_pct_fall : 20.000;\n" + << " slew_upper_threshold_pct_fall : 80.000;\n" + << " slew_lower_threshold_pct_rise : 20.000;\n" + << " slew_upper_threshold_pct_rise : 80.000;\n" + << " input_threshold_pct_fall : 50.000;\n" + << " input_threshold_pct_rise : 50.000;\n" + << " output_threshold_pct_fall : 50.000;\n" + << " output_threshold_pct_rise : 50.000;\n" + << " lu_table_template(" << cell << "_delay_template) {\n" + << " variable_1 : input_net_transition;\n" + << " variable_2 : total_output_net_capacitance;\n" + << " index_1 (\"1000, 1001\");\n" + << " index_2 (\"1000, 1001\");\n" + << " }\n" + << " lu_table_template(" << cell << "_slew_template) {\n" + << " variable_1 : total_output_net_capacitance;\n" + << " index_1 (\"1000, 1001\");\n" + << " }\n" + << " lu_table_template(" << cell << "_constraint_template) {\n" + << " variable_1 : related_pin_transition;\n" + << " variable_2 : constrained_pin_transition;\n" + << " index_1 (\"1000, 1001\");\n" + << " index_2 (\"1000, 1001\");\n" + << " }\n" + << " library_features(report_delay_calculation);\n"; + const int A_bank = AddrWidth(words_per_bank); + // One bus type per width in use. + std::set widths = {A, spec.bits, A_bank}; + for (int w : widths) { + o << " type (" << cell << "_bus_" << w << ") {\n" + << " base_type : array ;\n" + << " data_type : bit ;\n" + << " bit_width : " << w << ";\n" + << " bit_from : " << w - 1 << ";\n" + << " bit_to : 0 ;\n" + << " downto : true ;\n" + << " }\n"; + } + o << "cell(" << cell << ") {\n" + << " area : " << F(area_um2) << ";\n" + << " is_macro_cell : true;\n" + << " interface_timing : true;\n"; + // Clock. + o << " pin(" << spec.clock << ") {\n" + << " direction : input;\n" + << " capacitance : " << F(clk_pf) << ";\n" + << " clock : true;\n" + << " }\n"; + if (!spec.reset.empty()) { + o << " pin(" << spec.reset << ") {\n" + << " direction : input;\n" + << " capacitance : 0.001000;\n" + << " }\n"; + } + // Read ports: address in, data out combinational from that address; with + // a read latency the address is a flop's D (setup and hold to the clock) + // and the data comes from the clock, one flop level more. A banked port + // is one such pair per bank, with the bank-local width. + const bool registered = spec.read_latency > 0; + const double flop_ns = m.gate_delay_ps * m.wire_factor / 1000.0; + auto read_pair = [&](const std::string& addr, + const std::string& data, + int a_width, + double read_ns_here) { + o << " bus(" << addr << ") {\n" + << " bus_type : " << cell << "_bus_" << a_width << ";\n" + << " direction : input;\n" + << " capacitance : " + << F(registered ? m.input_load_ff / 1000.0 : addr_pf) << ";\n"; + if (registered) { + Constraint(o, cell, spec.clock, flop_ns, hold_ns); + } + o << " }\n" + << " bus(" << data << ") {\n" + << " bus_type : " << cell << "_bus_" << spec.bits << ";\n" + << " direction : output;\n" + << " max_capacitance : " << F(m.output_max_cap_ff / 1000.0) + << ";\n"; + // The arc per data bit, related to every address bit: a bus-to-bus + // arc between an A-bit address and a B-bit data would be read as + // bitwise pairs of different sizes (STA-1216). Every data bit + // depends on every address bit here. + std::string addr_bits; + for (int i = 0; i < a_width; ++i) { + addr_bits += (i ? " " : "") + addr + "[" + std::to_string(i) + "]"; + } + for (int b = 0; b < spec.bits; ++b) { + o << " pin(" << data << "[" << b << "]) {\n" + << " direction : output;\n" + << " timing() {\n"; + if (registered) { + o << " related_pin : " << spec.clock << " ;\n" + << " timing_type : rising_edge;\n"; + } else { + o << " related_pin : \"" << addr_bits << "\" ;\n" + << " timing_type : combinational;\n" + << " timing_sense : non_unate;\n"; + } + const double arc_ns = read_ns_here + (registered ? flop_ns : 0.0); + Table2(o, "cell_rise", cell + "_delay_template", arc_ns, 16); + Table2(o, "cell_fall", cell + "_delay_template", arc_ns, 16); + o << " rise_transition(" << cell << "_slew_template) {\n" + << " index_1 (\"0.005, 0.500\");\n" + << " values (\"0.009, 0.227\")\n" + << " }\n" + << " fall_transition(" << cell << "_slew_template) {\n" + << " index_1 (\"0.005, 0.500\");\n" + << " values (\"0.009, 0.227\")\n" + << " }\n" + << " }\n" + << " }\n"; + } + o << " }\n"; + }; + // A banked port's read has no footer level. + const int bank_read_levels + = 1 + Log2Ceil(A_bank) + 1 + Log2Ceil(words_per_bank); + const double bank_read_ns + = bank_read_levels * m.gate_delay_ps * m.wire_factor / 1000.0; + for (int r = 0; r < R; ++r) { + if (spec.read[r].banked()) { + for (size_t k = 0; k < spec.read[r].bank_addr.size(); ++k) { + read_pair(spec.read[r].bank_addr[k], + spec.read[r].bank_data[k], + A_bank, + bank_read_ns); + } + } else { + read_pair(spec.read[r].addr, spec.read[r].data, A, read_ns); + } + } + // Write ports: everything constrained to the clock. + for (int w = 0; w < W; ++w) { + o << " bus(" << spec.write[w].addr << ") {\n" + << " bus_type : " << cell << "_bus_" << A << ";\n" + << " direction : input;\n" + << " capacitance : " << F(addr_pf) << ";\n"; + Constraint(o, cell, spec.clock, setup_ns, hold_ns); + o << " }\n" + << " bus(" << spec.write[w].data << ") {\n" + << " bus_type : " << cell << "_bus_" << spec.bits << ";\n" + << " direction : input;\n" + << " capacitance : " << F(data_pf) << ";\n"; + Constraint(o, cell, spec.clock, setup_ns, hold_ns); + o << " }\n"; + if (!spec.write[w].en.empty()) { + o << " pin(" << spec.write[w].en << ") {\n" + << " direction : input;\n" + << " capacitance : " << F(en_pf) << ";\n"; + Constraint(o, cell, spec.clock, setup_ns, hold_ns); + o << " }\n"; + } + } + o << " cell_leakage_power : " << F(leakage_uw) << ";\n" + << "}\n" + << "}\n"; +} + +std::vector ReadModulePorts(const std::string& verilog, + const std::string& module) +{ + std::ifstream in(verilog); + if (!in) { + throw std::runtime_error("cannot read " + verilog); + } + std::string text((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + std::regex head("\\bmodule\\s+" + module + "\\s*\\("); + std::smatch mh; + if (!std::regex_search(text, mh, head)) { + throw std::runtime_error("module " + module + " is not in " + verilog); + } + size_t start = mh.position(0) + mh.length(0); + size_t end = text.find(");", start); + if (end == std::string::npos) { + throw std::runtime_error("module " + module + ": header never closes"); + } + std::string header = text.substr(start, end - start); + // Strip comments. + header = std::regex_replace(header, std::regex("//[^\n]*"), ""); + header = std::regex_replace(header, std::regex("/\\*[\\s\\S]*?\\*/"), ""); + // ANSI ports, one entry per comma: `input [4:0] a, b` declares two, the + // second inheriting the first's direction and width, whether it is on + // the same line or the next. + std::vector ports; + std::regex decl( + "^\\s*(input|output|inout)?\\s*(?:wire|reg|logic)?\\s*" + "(\\[\\s*(\\d+)\\s*:\\s*(\\d+)\\s*\\])?\\s*" + "([A-Za-z_][A-Za-z_0-9$]*)\\s*$"); + bool input = true; + int width = 1; + std::istringstream entries(header); + std::string entry; + while (std::getline(entries, entry, ',')) { + std::smatch m; + if (!std::regex_match(entry, m, decl)) { + continue; + } + if (m[1].matched) { + input = m[1].str() != "output"; + width = 1; + } + if (m[2].matched) { + width = std::abs(std::stoi(m[3].str()) - std::stoi(m[4].str())) + 1; + } + ports.push_back(RtlPort{m[5].str(), width, input}); + } + return ports; +} + +std::vector CheckPorts(const Spec& spec, + const std::vector& ports) +{ + const int A = AddrWidth(spec.words); + std::map want; + want[spec.clock] = RtlPort{spec.clock, 1, true}; + if (!spec.reset.empty()) { + want[spec.reset] = RtlPort{spec.reset, 1, true}; + } + const int banks = std::max(spec.banks, 1); + const int A_bank = AddrWidth(spec.words / banks); + for (const auto& r : spec.read) { + if (r.banked()) { + for (size_t k = 0; k < r.bank_addr.size(); ++k) { + want[r.bank_addr[k]] = RtlPort{r.bank_addr[k], A_bank, true}; + want[r.bank_data[k]] = RtlPort{r.bank_data[k], spec.bits, false}; + } + } else { + want[r.addr] = RtlPort{r.addr, A, true}; + want[r.data] = RtlPort{r.data, spec.bits, false}; + } + } + for (const auto& w : spec.write) { + want[w.addr] = RtlPort{w.addr, A, true}; + want[w.data] = RtlPort{w.data, spec.bits, true}; + if (!w.en.empty()) { + want[w.en] = RtlPort{w.en, 1, true}; + } + } + std::map have; + for (const auto& p : ports) { + have[p.name] = p; + } + std::vector problems; + for (const auto& [name, w] : want) { + auto it = have.find(name); + if (it == have.end()) { + problems.push_back("spec names port " + name + ", the module has none"); + continue; + } + if (it->second.width != w.width) { + problems.push_back( + "port " + name + " is " + std::to_string(it->second.width) + + " bits in the module, " + std::to_string(w.width) + " in the spec"); + } + if (it->second.input != w.input) { + problems.push_back("port " + name + " is " + + (it->second.input ? "an input" : "an output") + + " in the module and the other way in the spec"); + } + } + for (const auto& [name, p] : have) { + if (want.find(name) == want.end()) { + problems.push_back("module port " + name + " is not in the spec"); + } + } + return problems; +} + +} // namespace ram::regfile diff --git a/src/ram/src/views.h b/src/ram/src/views.h new file mode 100644 index 00000000000..4b312577e3b --- /dev/null +++ b/src/ram/src/views.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// generate_regfile: the views a parent's flow consumes before the block is +// routed -- an abstract LEF from the placed block and a model liberty -- +// and the check that a spec's ports are the RTL module's. +// +// The lib is a model, the way FakeRAM's is, so it lands in seconds inside +// the parent's synthesis: pin loads from fanout, arcs from the known gate +// depth of the read and write paths, a constant leakage per cell. It is +// replaced by the routed block's own abstract for a measured run; the +// numbers here are written so that swap changes as little as possible. +#pragma once + +#include +#include + +#include "regfile.h" + +namespace odb { +class dbBlock; +} + +namespace utl { +class Logger; +} + +namespace ram::regfile { + +// Abstract LEF of the placed block: outline, pins, obstruction on every +// layer the cells occupy. Bloats the occupied layers into one obstruction, +// as write_abstract_lef -bloat_occupied_layers does. +void WriteLef(odb::dbBlock* block, + utl::Logger* logger, + const std::string& path); + +// Model liberty for the generated block, see LibModel. `pre_layout` only +// changes the comment: the model has no clock tree to be ideal about. +void WriteLiberty(odb::dbBlock* block, + const Spec& spec, + const LibModel& model, + const std::string& path, + bool pre_layout); + +// One RTL port as declared: name, width in bits (1 for a scalar), input? +struct RtlPort +{ + std::string name; + int width = 1; + bool input = true; +}; + +// The ports of `module` in a Verilog file, firtool's declaration style: +// ANSI header, and consecutive same-direction ports packed on bare +// continuation lines that inherit direction and width. Throws +// std::runtime_error when the module is not in the file. +std::vector ReadModulePorts(const std::string& verilog, + const std::string& module); + +// Refuses, with every difference named, a spec whose ports are not the +// module's: a port the spec names that the module lacks, a width that +// differs, a direction that differs, or a module port the spec never +// mentions. Returns the list of problems; empty means they agree. +std::vector CheckPorts(const Spec& spec, + const std::vector& ports); + +} // namespace ram::regfile diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 071fa2c0f0b..30865b3f0b0 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -13,10 +13,27 @@ COMPULSORY_TESTS = [ ] PASSFAIL_TESTS = [ + "generate_regfile_asap7", + "generate_regfile_clock_gate_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS +# What every generate_regfile test reads: the asap7 cells, their +# liberty for constant propagation, the base spec's module. Every test +# but generate_regfile_asap7 (whose own glob has it) adds the spec. +REGFILE_TEST_DATA = [ + "asap7/asap7_tech_1x_201209.lef", + "asap7/asap7sc7p5t_28_R_1x_220121a.lef", + "asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz", + "asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz", + "asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz", + "asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib", + "asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz", + "generate_regfile_asap7_rtl.v", + "regfile_checks.tcl", +] + filegroup( name = "regression_resources", # Dependencies could be specified more narrowly per test case, @@ -51,6 +68,12 @@ filegroup( test_name + ".*", ], ) + { + "generate_regfile_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7_rtl_extra.v", + ], + "generate_regfile_clock_gate_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 8545b3dbca9..d7bc6afa34d 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -5,5 +5,7 @@ or_integration_tests( # golden .defok matches the clang build used by bazel, not the gcc # build typically used by cmake. See src/ram/test/BUILD. make_7x7_nangate45 + PASSFAIL_TESTS + generate_regfile_asap7 + generate_regfile_clock_gate_asap7 ) - diff --git a/src/ram/test/asap7 b/src/ram/test/asap7 new file mode 120000 index 00000000000..16dbc1fd7b7 --- /dev/null +++ b/src/ram/test/asap7 @@ -0,0 +1 @@ +../../../test/asap7 \ No newline at end of file diff --git a/src/ram/test/generate_regfile_asap7.regfile b/src/ram/test/generate_regfile_asap7.regfile new file mode 100644 index 00000000000..ad31829371c --- /dev/null +++ b/src/ram/test/generate_regfile_asap7.regfile @@ -0,0 +1,22 @@ +# An 8 x 4 register file, two read and two write ports, in two banks, +# with word 0 reading zero, laid out for a parent that places its decode +# (mode netlist). +module rf8x4 +mode netlist +words 8 +bits 4 +banks 2 +clock clock +read r0_addr r0_data +read r1_addr r1_data +write w0_addr w0_data w0_en +write w1_addr w1_data w1_en +zero_word 0 +cell flop DFFHQNx1_ASAP7_75t_R +cell and2 AND2x2_ASAP7_75t_R +cell or2 OR2x2_ASAP7_75t_R +cell ao22 AO22x2_ASAP7_75t_R +cell inv INVx1_ASAP7_75t_R +cell tap TAPCELL_ASAP7_75t_R +pin_layer M4 +tap_columns 2 diff --git a/src/ram/test/generate_regfile_asap7.tcl b/src/ram/test/generate_regfile_asap7.tcl new file mode 100644 index 00000000000..270a90ad85c --- /dev/null +++ b/src/ram/test/generate_regfile_asap7.tcl @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_asap7 +# generate_regfile builds the register file its spec describes: every +# read port reads every word, word 0 reads zero, a write port loads only +# the word it addresses and every other word holds, and the abstract and +# the timing model name every port. A spec whose ports are not the RTL +# module's is refused. Checked by constant propagation through the +# generated cells, not against a golden netlist. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries + +set failed [catch { + tee -quiet -variable log { + generate_regfile -spec generate_regfile_asap7.regfile \ + -check_ports generate_regfile_asap7_rtl_extra.v + } +} message] +check "a port the spec does not name is refused" { set failed } 1 +check "the refusal is RAM-0050" { string match "*RAM-0050*" $message } 1 +check "the refusal names the port" \ + { string match "*module port w2_en is not in the spec*" $log } 1 + +set lef_file [make_result_file generate_regfile_asap7.lef] +set lib_file [make_result_file generate_regfile_asap7.lib] +generate_regfile -spec generate_regfile_asap7.regfile \ + -check_ports generate_regfile_asap7_rtl.v \ + -lef $lef_file -liberty $lib_file + +set words 8 +set bits 4 +set abits 3 +for { set w 1 } { $w < $words } { incr w } { + rf_store $w $bits [rf_word_value $w $bits] +} + +foreach port { r0 r1 } { + for { set a 0 } { $a < $words } { incr a } { + rf_drive ${port}_addr $abits $a + set expected [expr { $a == 0 ? 0 : [rf_word_value $a $bits] }] + check "$port reads word $a" { rf_read ${port}_data $bits } $expected + } +} + +# With a hold term per bit (write_style mux, the default) a word's flops +# load the write data when the word is written and their own value +# otherwise. +proc check_writes { port other } { + rf_drive_bit ${other}_en 0 + rf_drive_bit ${port}_en 1 + foreach a { 1 3 6 } { + set data [expr { (~[rf_word_value $a 4]) & 15 }] + rf_drive ${port}_addr 3 $a + rf_drive ${port}_data 4 $data + for { set w 1 } { $w < 8 } { incr w } { + set expected [expr { $w == $a ? $data : [rf_word_value $w 4] }] + check "$port writing word $a: word $w loads" { rf_next $w 4 } $expected + } + } + rf_drive_bit ${port}_en 0 + for { set w 1 } { $w < 8 } { incr w } { + check "no write: word $w holds" { rf_next $w 4 } [rf_word_value $w 4] + } +} +check_writes w0 w1 +check_writes w1 w0 + +check "the abstract names every port" { rf_ports_missing_from $lef_file lef } {} +check "the timing model names every port" \ + { rf_ports_missing_from $lib_file lib } {} + +exit_summary diff --git a/src/ram/test/generate_regfile_asap7_rtl.v b/src/ram/test/generate_regfile_asap7_rtl.v new file mode 100644 index 00000000000..36a3e3a9993 --- /dev/null +++ b/src/ram/test/generate_regfile_asap7_rtl.v @@ -0,0 +1,8 @@ +// The module generate_regfile_asap7.regfile describes, its ports +// declared several to a line, as hand-written RTL often does. +module rf8x4 (input clock, + input [2:0] r0_addr, r1_addr, + output [3:0] r0_data, r1_data, + input [2:0] w0_addr, input [3:0] w0_data, input w0_en, + input [2:0] w1_addr, input [3:0] w1_data, input w1_en); +endmodule diff --git a/src/ram/test/generate_regfile_asap7_rtl_extra.v b/src/ram/test/generate_regfile_asap7_rtl_extra.v new file mode 100644 index 00000000000..688dd3ac99c --- /dev/null +++ b/src/ram/test/generate_regfile_asap7_rtl_extra.v @@ -0,0 +1,9 @@ +// generate_regfile_asap7_rtl.v with a port the spec does not name +// (w2_en): the port check must refuse the spec. +module rf8x4 (input clock, + input [2:0] r0_addr, r1_addr, + output [3:0] r0_data, r1_data, + input [2:0] w0_addr, input [3:0] w0_data, input w0_en, + input [2:0] w1_addr, input [3:0] w1_data, input w1_en, + input w2_en); +endmodule diff --git a/src/ram/test/generate_regfile_clock_gate_asap7.tcl b/src/ram/test/generate_regfile_clock_gate_asap7.tcl new file mode 100644 index 00000000000..3707bb41402 --- /dev/null +++ b/src/ram/test/generate_regfile_clock_gate_asap7.tcl @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_clock_gate_asap7 +# generate_regfile with a clock gate per word (write_style clock_gate): +# every read port reads every word, and a write opens the clock gate of +# the word it addresses, and only that one, with the write data on its +# flops' D. Checked by constant propagation through the generated cells. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries +set spec [rf_spec generate_regfile_asap7.regfile \ + generate_regfile_clock_gate_asap7.regfile {"write_style clock_gate"}] +generate_regfile -spec $spec -check_ports generate_regfile_asap7_rtl.v + +for { set w 1 } { $w < 8 } { incr w } { + rf_store $w 4 [rf_word_value $w 4] +} +foreach port { r0 r1 } { + for { set a 0 } { $a < 8 } { incr a } { + rf_drive ${port}_addr 3 $a + set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] + check "$port reads word $a" { rf_read ${port}_data 4 } $expected + } +} + +proc check_writes { port other } { + rf_drive_bit ${other}_en 0 + rf_drive_bit ${port}_en 1 + foreach a { 1 3 6 } { + set data [expr { (~[rf_word_value $a 4]) & 15 }] + rf_drive ${port}_addr 3 $a + rf_drive ${port}_data 4 $data + for { set w 1 } { $w < 8 } { incr w } { + check "$port writing word $a: word $w's clock gate" \ + { rf_value w${w}_icg/ENA } [expr { $w == $a ? 1 : 0 }] + } + check "$port writing word $a: its flops load the data" { rf_next $a 4 } $data + } + rf_drive_bit ${port}_en 0 + for { set w 1 } { $w < 8 } { incr w } { + check "no write: word $w's clock gate is shut" { rf_value w${w}_icg/ENA } 0 + } +} +check_writes w0 w1 +check_writes w1 w0 + +exit_summary diff --git a/src/ram/test/regfile_checks.tcl b/src/ram/test/regfile_checks.tcl new file mode 100644 index 00000000000..777bce5aabd --- /dev/null +++ b/src/ram/test/regfile_checks.tcl @@ -0,0 +1,113 @@ +# Checks of a generated register file by what it computes, not by how it +# is laid out: case analysis on its inputs and on its storage, and +# OpenSTA's constant propagation through its cells. Sourced by the +# generate_regfile tests after helpers.tcl. + +proc rf_read_libraries { } { + read_lef asap7/asap7_tech_1x_201209.lef + read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef + foreach kind { AO OA SIMPLE } { + read_liberty asap7/asap7sc7p5t_${kind}_RVT_FF_nldm_211120.lib.gz + } + read_liberty asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz + read_liberty asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib +} + +# A spec file: the base spec with `lines` appended, in the results dir. +proc rf_spec { base name lines } { + set spec [make_result_file $name] + set out [open $spec w] + set in [open $base r] + puts -nonewline $out [read $in] + close $in + foreach line $lines { + puts $out $line + } + close $out + return $spec +} + +# The propagated value of a port (by name) or an instance pin. +proc rf_value { name } { + set port [get_ports -quiet $name] + if { $port ne "" } { + return [sta::pin_sim_logic_value [sta::get_port_pin $port]] + } + return [sta::pin_sim_logic_value [get_pins $name]] +} + +proc rf_drive_bit { name value } { + set_case_analysis $value [get_ports $name] +} + +proc rf_drive { bus width value } { + for { set i 0 } { $i < $width } { incr i } { + rf_drive_bit "$bus\[$i\]" [expr { ($value >> $i) & 1 }] + } +} + +# The integer an output bus carries, or X if any bit is not a constant. +proc rf_read { bus width } { + set v 0 + for { set i 0 } { $i < $width } { incr i } { + set b [rf_value "$bus\[$i\]"] + if { $b ne "0" && $b ne "1" } { + return X + } + set v [expr { $v | ($b << $i) }] + } + return $v +} + +# Word `word` holding `value`: the storage flop of each bit holds the +# inverse on QN, which is where the value is set. +proc rf_store { word bits value } { + for { set b 0 } { $b < $bits } { incr b } { + set_case_analysis [expr { 1 - (($value >> $b) & 1) }] \ + [get_pins w${word}_b${b}_ff/QN] + } +} + +# The integer word `word`'s flops would load: their D inputs. +proc rf_next { word bits } { + set v 0 + for { set b 0 } { $b < $bits } { incr b } { + set d [rf_value w${word}_b${b}_ff/D] + if { $d ne "0" && $d ne "1" } { + return X + } + set v [expr { $v | ($d << $b) }] + } + return $v +} + +# A value per word, distinct across words, for the storage. +proc rf_word_value { word bits } { + return [expr { ($word * 5 + 3) & ((1 << $bits) - 1) }] +} + +# The ports of the generated block missing from a view: the abstract +# (`PIN `) or the timing model (`pin()`, or a `bus()` +# for a bus bit). +proc rf_ports_missing_from { file kind } { + set in [open $file r] + set text [read $in] + close $in + set missing {} + foreach bterm [[ord::get_db_block] getBTerms] { + set name [$bterm getName] + set base [lindex [split $name {[}] 0] + if { $kind eq "lef" } { + set found [expr { [string first "PIN $name\n" $text] >= 0 }] + } else { + set found [expr { + [string first "pin($name)" $text] >= 0 + || [string first "bus($base)" $text] >= 0 + }] + } + if { !$found } { + lappend missing $name + } + } + return $missing +} From f6517431a43a09f32833aea5f198a48a57bd1b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 7 Oct 2026 20:46:56 +0200 Subject: [PATCH 2/8] ram: generate_regfile async_reset, a register file that resets to zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `async_reset low|high` clears every stored word to zero asynchronously, as an RTL register file written `always @(posedge clk or negedge rst_n)` does: ibex's, cva6's, swerv's and coralnpu's all do. The storage flops are `cell flop_r` (an async reset/set flop, asap7 DFFASRHQNx1) with the set tied off by `cell tie_hi`; one tie, and for an active-high reset one inverter, per word in its header column, so no reset or tie net fans out past one word's bits. `reset` keeps its meaning, a port the array ignores. Read-address registers (read_latency) are not reset. Signed-off-by: Øyvind Harboe Tests: generate_regfile_reset_asap7 and generate_regfile_reset_high_asap7, one per polarity: every storage flop is the reset flop, its reset is asserted exactly while the reset port is active, its set is tied off, and the file reads its words once reset is released. Co-Authored-By: Claude Opus 5.5 --- src/ram/README.md | 5 +- src/ram/src/regfile.cpp | 98 ++++++++++++++++--- src/ram/src/regfile.h | 17 +++- src/ram/test/BUILD | 8 ++ src/ram/test/CMakeLists.txt | 2 + src/ram/test/generate_regfile_reset_asap7.tcl | 55 +++++++++++ .../generate_regfile_reset_high_asap7.tcl | 55 +++++++++++ 7 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 src/ram/test/generate_regfile_reset_asap7.tcl create mode 100644 src/ram/test/generate_regfile_reset_high_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 8e466547e4e..a2ad03f65b4 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -121,11 +121,12 @@ One key per line, `#` comments: | `words n`, `bits n` | Depth and width. | | `clock port` | The clock port. | | `reset port` | A reset port the RTL has and the array ignores. | +| `async_reset port low\|high` | The reset clears every stored word to zero asynchronously, active low or high; the storage flops are `cell flop_r` with their set tied off by `cell tie_hi`, one tie (and for an active-high reset one inverter) per word. Read-address registers are not reset. One of `reset` or `async_reset`. | | `read addr data` | A read port, by the module's port names. Repeat per port. | | `read_banked addr0 data0 addr1 data1 ...` | A read port with an address and a data port per bank. | | `write addr data [en]` | A write port. Repeat per port. | -| `cell role master` | The cells: `flop`, `and2`, `or2`, `ao22`, `inv`, `tap`. | -| `pins role pin...` | The pin names of a cell role (`flop`, `and2`, `or2`, `ao22`, `inv`), for a library whose names differ from the defaults. | +| `cell role master` | The cells: `flop`, `and2`, `or2`, `ao22`, `inv`, `tap`; with `async_reset` also `flop_r` (pins D, CLK, RESETN, SETN, QN by default) and `tie_hi` (pin H). | +| `pins role pin...` | The pin names of a cell role (`flop`, `flop_r`, `tie_hi`, `and2`, `or2`, `ao22`, `inv`), for a library whose names differ from the defaults. | | `flop_output Q\|QN` | Whether the flop's output is inverted. | | `pin_layer layer`, `pin_layer_v layer` | The layers of the pins on the left and right edges, and on the top and bottom. | | `pin_track offset pitch` | The track grid the pins are centred on, in microns. | diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp index 4d606de5f55..7ddb2ba6881 100644 --- a/src/ram/src/regfile.cpp +++ b/src/ram/src/regfile.cpp @@ -68,6 +68,9 @@ std::string Fmt(std::string pattern, struct Pins { std::vector flop = {"D", "CLK", "QN"}; // data, clock, out + // data, clock, reset, set (both active low), out + std::vector flop_r = {"D", "CLK", "RESETN", "SETN", "QN"}; + std::vector tie_hi = {"H"}; std::vector and2 = {"A", "B", "Y"}; std::vector or2 = {"A", "B", "Y"}; std::vector ao22 = {"A1", "A2", "B1", "B2", "Y"}; @@ -333,6 +336,8 @@ class Builder odb::dbTechLayer* layer_h_ = nullptr; // left/right edge pins odb::dbTechLayer* layer_v_ = nullptr; // top/bottom edge pins dbMaster* flop_ = nullptr; + dbMaster* store_ = nullptr; // the storage flop: flop_, or flop_r + dbMaster* tie_hi_ = nullptr; dbMaster* and2_ = nullptr; dbMaster* or2_ = nullptr; dbMaster* ao22_ = nullptr; @@ -396,6 +401,13 @@ dbBlock* Builder::Run() icg_ = Master(s.cells.icg, "icg"); CheckPins(icg_, g_pins.icg); } + store_ = flop_; + if (s.async_reset) { + store_ = Master(s.cells.flop_r, "flop_r"); + CheckPins(store_, g_pins.flop_r); + tie_hi_ = Master(s.cells.tie_hi, "tie_hi"); + CheckPins(tie_hi_, g_pins.tie_hi); + } odb::dbTech* tech = db_->getTech(); site_ = flop_->getSite(); @@ -438,7 +450,8 @@ dbBlock* Builder::Run() Input(s.clock); if (!s.reset.empty()) { // Chisel gives every module a reset; a register file without one - // still has the port. It exists on the macro, connected to nothing. + // still has the port. It exists on the macro, connected to nothing, + // unless `async_reset` makes it the storage flops' reset. Input(s.reset); } // Banks first: a banked read port has one address per bank, of the @@ -555,7 +568,7 @@ dbBlock* Builder::Run() nor2_->getWidth(), or2_->getWidth()}))}); const int tile_cells_w = static_cast( - flop_->getWidth() + inv_->getWidth() + write_pairs * ao22_->getWidth() + store_->getWidth() + inv_->getWidth() + write_pairs * ao22_->getWidth() + std::max(write_pairs - 1, 0) * or2_->getWidth() + R * read_cell_w); int rows_per_word = 1; // Keep a tile near square-ish: at most ~40 sites wide. @@ -564,7 +577,7 @@ dbBlock* Builder::Run() } // Rows are filled least-first, so a row is at most one widest cell over // the average; the header carries that slack. - const int widest = static_cast(std::max({flop_->getWidth(), + const int widest = static_cast(std::max({store_->getWidth(), ao22_->getWidth(), aoi22_->getWidth(), or2_->getWidth(), @@ -578,7 +591,7 @@ dbBlock* Builder::Run() std::vector fill(rows_per_word, 0); auto drop = [&](int w) { *std::min_element(fill.begin(), fill.end()) += w; }; - drop(static_cast(flop_->getWidth())); + drop(static_cast(store_->getWidth())); if (g_pins.flop_inverted) { drop(static_cast(inv_->getWidth())); } @@ -599,7 +612,10 @@ dbBlock* Builder::Run() // selects), or the word's clock gate enabled by that OR. const int header_cells_w = static_cast( (R + W) * A * and2_->getWidth() + (W - 1) * or2_->getWidth() - + (gated ? icg_->getWidth() : inv_->getWidth())); + + (gated ? icg_->getWidth() : inv_->getWidth()) + + (s.async_reset + ? tie_hi_->getWidth() + (s.reset_active_low ? 0 : inv_->getWidth()) + : 0)); const int header_w = ((header_cells_w + rows_per_word - 1) / rows_per_word + widest + site_w_ - 1) / site_w_ * site_w_; @@ -836,6 +852,23 @@ dbBlock* Builder::Run() hold[n]->getName(), {{g_pins.inv[0], any_write}, {g_pins.inv[1], hold[n]}}); } + // The word's reset and set, for `async_reset`: an active-high reset + // inverted here, and the set tied off, one of each per word so no + // net fans out past one word's bits. + dbNet* resetn = nullptr; + dbNet* setn = nullptr; + if (s.async_reset) { + resetn = Net(s.reset); + if (!s.reset_active_low) { + resetn = Net(wn + "_resetn"); + Place(hcur(), + inv_, + resetn->getName(), + {{g_pins.inv[0], Net(s.reset)}, {g_pins.inv[1], resetn}}); + } + setn = Net(wn + "_setn"); + Place(hcur(), tie_hi_, setn->getName(), {{g_pins.tie_hi[0], setn}}); + } periphery_ = false; for (auto& c : hc) { if (c.x > bank_x0 + header_w) { @@ -874,14 +907,27 @@ dbBlock* Builder::Run() dbNet* d = gated && W == 1 ? wdata[0][b] : Net(tn + "_d"); dbNet* qn = Net(tn + "_qn"); dbNet* q = g_pins.flop_inverted ? Net(tn + "_q") : qn; - Place(tcur(), - flop_, - s.store_name.empty() + const std::string ff_name + = s.store_name.empty() ? tn + "_ff" - : Fmt(s.store_name, {{"word", n}, {"bit", b}}), - {{g_pins.flop[0], d}, - {g_pins.flop[1], gated ? gclk[n] : clock}, - {g_pins.flop[2], qn}}); + : Fmt(s.store_name, {{"word", n}, {"bit", b}}); + if (s.async_reset) { + Place(tcur(), + store_, + ff_name, + {{g_pins.flop_r[0], d}, + {g_pins.flop_r[1], gated ? gclk[n] : clock}, + {g_pins.flop_r[2], resetn}, + {g_pins.flop_r[3], setn}, + {g_pins.flop_r[4], qn}}); + } else { + Place(tcur(), + flop_, + ff_name, + {{g_pins.flop[0], d}, + {g_pins.flop[1], gated ? gclk[n] : clock}, + {g_pins.flop[2], qn}}); + } if (g_pins.flop_inverted) { Place(tcur(), inv_, @@ -1226,7 +1272,24 @@ Spec ReadSpec(const std::string& path) s.clock = v[0]; } else if (key == "reset") { need(1); + if (!s.reset.empty()) { + Refuse(path + ":" + std::to_string(lineno) + + ": one of `reset` or `async_reset`, once"); + } + s.reset = v[0]; + } else if (key == "async_reset") { + need(2); + if (!s.reset.empty()) { + Refuse(path + ":" + std::to_string(lineno) + + ": one of `reset` or `async_reset`, once"); + } + if (v[1] != "low" && v[1] != "high") { + Refuse(path + ":" + std::to_string(lineno) + + ": `async_reset low|high`"); + } s.reset = v[0]; + s.async_reset = true; + s.reset_active_low = v[1] == "low"; } else if (key == "read") { need(2); s.read.push_back(Port{v[0], v[1], ""}); @@ -1252,6 +1315,10 @@ Spec ReadSpec(const std::string& path) const std::string& kind = v[0]; if (kind == "flop") { s.cells.flop = v[1]; + } else if (kind == "flop_r") { + s.cells.flop_r = v[1]; + } else if (kind == "tie_hi") { + s.cells.tie_hi = v[1]; } else if (kind == "and2") { s.cells.and2 = v[1]; } else if (kind == "or2") { @@ -1292,6 +1359,10 @@ Spec ReadSpec(const std::string& path) }; if (kind == "flop") { set(g_pins.flop, 3); + } else if (kind == "flop_r") { + set(g_pins.flop_r, 5); + } else if (kind == "tie_hi") { + set(g_pins.tie_hi, 1); } else if (kind == "and2") { set(g_pins.and2, 3); } else if (kind == "or2") { @@ -1411,6 +1482,9 @@ Spec ReadSpec(const std::string& path) || s.cells.ao22.empty() || s.cells.inv.empty()) { Refuse(path + ": cells flop, and2, or2, ao22 and inv are all required"); } + if (s.async_reset && (s.cells.flop_r.empty() || s.cells.tie_hi.empty())) { + Refuse(path + ": `async_reset` needs `cell flop_r` and `cell tie_hi`"); + } return s; } diff --git a/src/ram/src/regfile.h b/src/ram/src/regfile.h index c3605197aaa..9252ccdc37c 100644 --- a/src/ram/src/regfile.h +++ b/src/ram/src/regfile.h @@ -49,9 +49,13 @@ struct Port struct Cells { std::string flop; // D flip-flop, Q output (QN accepted, see below) - std::string and2; // read select AND - std::string or2; // bitline OR tree node - std::string ao22; // write mux node: (sel & new) | (hold & old) + // the storage flop with `async_reset`: an asynchronous reset (RESETN) + // and set (SETN, tied off) flop + std::string flop_r; + std::string tie_hi; // ties flop_r's set off, one per word + std::string and2; // read select AND + std::string or2; // bitline OR tree node + std::string ao22; // write mux node: (sel & new) | (hold & old) // read bitline: AOI22 leaf per word pair, NAND2/NOR2 tree over them std::string aoi22 = "AOI22xp5_ASAP7_75t_R"; std::string nand2 = "NAND2xp5_ASAP7_75t_R"; @@ -89,6 +93,13 @@ struct Spec int bits = 0; std::string clock = "clock"; std::string reset; // optional: a port the RTL has and the array ignores + // `async_reset low|high`: the reset clears every stored word to + // zero asynchronously, as an RTL `always @(posedge clk or negedge + // rst_n)` register file does; the storage flops are `cell flop_r`. The + // port is `reset` above. Read-address registers (read_latency) are not + // reset. + bool async_reset = false; + bool reset_active_low = true; std::vector read; std::vector write; Cells cells; diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 30865b3f0b0..81cc32514f9 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -15,6 +15,8 @@ COMPULSORY_TESTS = [ PASSFAIL_TESTS = [ "generate_regfile_asap7", "generate_regfile_clock_gate_asap7", + "generate_regfile_reset_asap7", + "generate_regfile_reset_high_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -74,6 +76,12 @@ filegroup( "generate_regfile_clock_gate_asap7": REGFILE_TEST_DATA + [ "generate_regfile_asap7.regfile", ], + "generate_regfile_reset_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + ], + "generate_regfile_reset_high_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index d7bc6afa34d..a7e9c00b96c 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -8,4 +8,6 @@ or_integration_tests( PASSFAIL_TESTS generate_regfile_asap7 generate_regfile_clock_gate_asap7 + generate_regfile_reset_asap7 + generate_regfile_reset_high_asap7 ) diff --git a/src/ram/test/generate_regfile_reset_asap7.tcl b/src/ram/test/generate_regfile_reset_asap7.tcl new file mode 100644 index 00000000000..0830ae2680f --- /dev/null +++ b/src/ram/test/generate_regfile_reset_asap7.tcl @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_reset_asap7 +# generate_regfile with an asynchronous reset, active low (async_reset +# rst_n low): every storage flop is the spec's reset flop, its reset +# (RESETN, which clears the stored value) is asserted exactly while rst_n +# is active, its set is tied off, and the file reads its words once reset +# is released. Checked by constant propagation through the generated +# cells. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries +set spec [rf_spec generate_regfile_asap7.regfile generate_regfile_reset_asap7.regfile { + "async_reset rst_n low" + "cell flop_r DFFASRHQNx1_ASAP7_75t_R" + "cell tie_hi TIEHIx1_ASAP7_75t_R" +}] +generate_regfile -spec $spec + +set flops {} +for { set w 1 } { $w < 8 } { incr w } { + for { set b 0 } { $b < 4 } { incr b } { + lappend flops w${w}_b${b}_ff + } +} +set flop_r [[ord::get_db] findMaster DFFASRHQNx1_ASAP7_75t_R] +foreach ff $flops { + check "$ff is the reset flop" \ + { [[ord::get_db_block] findInst $ff] getMaster } $flop_r +} + +proc check_reset { flops state value } { + foreach ff $flops { + check "rst_n $state: $ff RESETN" { rf_value $ff/RESETN } $value + check "rst_n $state: $ff SETN is tied off" { rf_value $ff/SETN } 1 + } +} +rf_drive_bit rst_n 0 +check_reset $flops active 0 +rf_drive_bit rst_n 1 +check_reset $flops released 1 + +for { set w 1 } { $w < 8 } { incr w } { + rf_store $w 4 [rf_word_value $w 4] +} +for { set a 0 } { $a < 8 } { incr a } { + rf_drive r0_addr 3 $a + set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] + check "released: r0 reads word $a" { rf_read r0_data 4 } $expected +} + +exit_summary diff --git a/src/ram/test/generate_regfile_reset_high_asap7.tcl b/src/ram/test/generate_regfile_reset_high_asap7.tcl new file mode 100644 index 00000000000..da87418cd4a --- /dev/null +++ b/src/ram/test/generate_regfile_reset_high_asap7.tcl @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_reset_high_asap7 +# generate_regfile with an asynchronous reset, active high (async_reset +# rst high): every storage flop is the spec's reset flop, its reset +# (RESETN, which clears the stored value) is asserted exactly while rst +# is active, its set is tied off, and the file reads its words once reset +# is released. Checked by constant propagation through the generated +# cells. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries +set spec [rf_spec generate_regfile_asap7.regfile generate_regfile_reset_high_asap7.regfile { + "async_reset rst high" + "cell flop_r DFFASRHQNx1_ASAP7_75t_R" + "cell tie_hi TIEHIx1_ASAP7_75t_R" +}] +generate_regfile -spec $spec + +set flops {} +for { set w 1 } { $w < 8 } { incr w } { + for { set b 0 } { $b < 4 } { incr b } { + lappend flops w${w}_b${b}_ff + } +} +set flop_r [[ord::get_db] findMaster DFFASRHQNx1_ASAP7_75t_R] +foreach ff $flops { + check "$ff is the reset flop" \ + { [[ord::get_db_block] findInst $ff] getMaster } $flop_r +} + +proc check_reset { flops state value } { + foreach ff $flops { + check "rst $state: $ff RESETN" { rf_value $ff/RESETN } $value + check "rst $state: $ff SETN is tied off" { rf_value $ff/SETN } 1 + } +} +rf_drive_bit rst 1 +check_reset $flops active 0 +rf_drive_bit rst 0 +check_reset $flops released 1 + +for { set w 1 } { $w < 8 } { incr w } { + rf_store $w 4 [rf_word_value $w 4] +} +for { set a 0 } { $a < 8 } { incr a } { + rf_drive r0_addr 3 $a + set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] + check "released: r0 reads word $a" { rf_read r0_data 4 } $expected +} + +exit_summary From ec318b0bb758475e40652fa8e407f0995e83e1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 7 Oct 2026 20:51:46 +0200 Subject: [PATCH 3/8] ram: generate_regfile port check reads SystemVerilog headers, unused ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -check_ports read only firtool's headers: `module m(` with numeric widths, and it skipped any port declaration it could not read. ibex's register file is `module ibex_register_file_ff #(parameter ...) (` with `[DataWidth-1:0]` widths, so the check could not find it, and a skipped port is a check that passes without checking. The check now reads a `#(...)` parameter list, evaluates widths in its parameters' integer defaults (+ - * / and parentheses), multiplies packed dimensions, and stops on a declaration it cannot read. Comments are stripped by a linear scan from the header on, not std::regex, which recurses per character on megabyte netlists. `unused ...` names one-bit inputs the module has and the array ignores (ibex's test_en_i and, with DummyInstructions off, dummy_instr_id_i): the block gets the port, connected to nothing. Signed-off-by: Øyvind Harboe Test: generate_regfile_sv_ports_asap7: a parameterized module header with an unused input is accepted; the same module with a wider parameter is refused, naming the port and both widths; a typed declaration is refused, quoted. Co-Authored-By: Claude Opus 5.5 --- src/ram/README.md | 3 +- src/ram/src/regfile.cpp | 12 + src/ram/src/regfile.h | 3 + src/ram/src/views.cpp | 261 ++++++++++++++++-- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + .../test/generate_regfile_sv_ports_asap7.tcl | 64 +++++ .../generate_regfile_sv_ports_asap7_rtl.sv | 20 ++ 8 files changed, 348 insertions(+), 21 deletions(-) create mode 100644 src/ram/test/generate_regfile_sv_ports_asap7.tcl create mode 100644 src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv diff --git a/src/ram/README.md b/src/ram/README.md index a2ad03f65b4..aeffef0a82d 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -104,7 +104,7 @@ generate_regfile -spec file | Switch Name | Description | | ---------------------- | -------------------------------------- | | `-spec` | The spec file, below. | -| `-check_ports` | A Verilog file declaring the spec's module. The command stops if the module's ports are not the ports the spec names, at the widths the spec implies. | +| `-check_ports` | A Verilog file declaring the spec's module. The command stops if the module's ports are not the ports the spec names, at the widths the spec implies. It reads ANSI headers, a SystemVerilog `#(...)` parameter list and widths written in its parameters' defaults, packed dimensions multiplied; a port declaration it cannot read stops it too. | | `-verilog` | Write the cells as structural Verilog. | | `-def` | Write the placed block as DEF. | | `-lef` | Write an abstract LEF of the block. | @@ -121,6 +121,7 @@ One key per line, `#` comments: | `words n`, `bits n` | Depth and width. | | `clock port` | The clock port. | | `reset port` | A reset port the RTL has and the array ignores. | +| `unused port...` | One-bit inputs the RTL module has and the array ignores (ibex's `test_en_i`), so the block keeps the module's ports. | | `async_reset port low\|high` | The reset clears every stored word to zero asynchronously, active low or high; the storage flops are `cell flop_r` with their set tied off by `cell tie_hi`, one tie (and for an active-high reset one inverter) per word. Read-address registers are not reset. One of `reset` or `async_reset`. | | `read addr data` | A read port, by the module's port names. Repeat per port. | | `read_banked addr0 data0 addr1 data1 ...` | A read port with an address and a data port per bank. | diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp index 7ddb2ba6881..f33d4c68380 100644 --- a/src/ram/src/regfile.cpp +++ b/src/ram/src/regfile.cpp @@ -454,6 +454,9 @@ dbBlock* Builder::Run() // unless `async_reset` makes it the storage flops' reset. Input(s.reset); } + for (const auto& u : s.unused) { + Input(u); + } // Banks first: a banked read port has one address per bank, of the // bank-local width. const int banks = std::max(s.banks, 1); @@ -1125,6 +1128,9 @@ dbBlock* Builder::Run() if (!s.reset.empty()) { left(s.reset); } + for (const auto& u : s.unused) { + left(u); + } for (int r = 0; r < R; ++r) { for (size_t k = 0; k < raddr[r].size(); ++k) { for (size_t i = 0; i < raddr[r][k].size(); ++i) { @@ -1277,6 +1283,12 @@ Spec ReadSpec(const std::string& path) + ": one of `reset` or `async_reset`, once"); } s.reset = v[0]; + } else if (key == "unused") { + if (v.empty()) { + Refuse(path + ":" + std::to_string(lineno) + + ": `unused ...` names at least one port"); + } + s.unused.insert(s.unused.end(), v.begin(), v.end()); } else if (key == "async_reset") { need(2); if (!s.reset.empty()) { diff --git a/src/ram/src/regfile.h b/src/ram/src/regfile.h index 9252ccdc37c..fc7109a413a 100644 --- a/src/ram/src/regfile.h +++ b/src/ram/src/regfile.h @@ -100,6 +100,9 @@ struct Spec // reset. bool async_reset = false; bool reset_active_low = true; + // `unused `: one-bit inputs the RTL module has and the array + // ignores (ibex's test_en_i), so the block has the module's ports. + std::vector unused; std::vector read; std::vector write; Cells cells; diff --git a/src/ram/src/views.cpp b/src/ram/src/views.cpp index 6c609674453..1fe0e3bba55 100644 --- a/src/ram/src/views.cpp +++ b/src/ram/src/views.cpp @@ -6,7 +6,9 @@ #include "views.h" #include +#include #include +#include #include #include #include @@ -39,6 +41,179 @@ int AddrWidth(int words) return std::max(Log2Ceil(words), 1); } +// An integer constant expression of a module header: decimal numbers, +// parameters by name, + - * / and parentheses. Throws naming what it +// cannot evaluate: a width the check cannot read is a refusal, not a +// port to skip. +class ConstExpr +{ + public: + ConstExpr(const std::string& text, const std::map& params) + : text_(text), params_(params) + { + } + long Eval() + { + long v = Sum(); + Space(); + if (pos_ != text_.size()) { + Fail(); + } + return v; + } + + private: + void Space() + { + while (pos_ < text_.size() + && std::isspace(static_cast(text_[pos_]))) { + ++pos_; + } + } + [[noreturn]] void Fail() + { + throw std::runtime_error("cannot evaluate `" + text_ + + "` in a module header"); + } + long Sum() + { + long v = Product(); + for (;;) { + Space(); + if (pos_ < text_.size() && (text_[pos_] == '+' || text_[pos_] == '-')) { + const char op = text_[pos_++]; + const long r = Product(); + v = op == '+' ? v + r : v - r; + } else { + return v; + } + } + } + long Product() + { + long v = Atom(); + for (;;) { + Space(); + if (pos_ < text_.size() && (text_[pos_] == '*' || text_[pos_] == '/')) { + const char op = text_[pos_++]; + const long r = Atom(); + if (op == '/' && r == 0) { + Fail(); + } + v = op == '*' ? v * r : v / r; + } else { + return v; + } + } + } + long Atom() + { + Space(); + if (pos_ >= text_.size()) { + Fail(); + } + if (text_[pos_] == '(') { + ++pos_; + const long v = Sum(); + Space(); + if (pos_ >= text_.size() || text_[pos_] != ')') { + Fail(); + } + ++pos_; + return v; + } + if (text_[pos_] == '-') { + ++pos_; + return -Atom(); + } + if (std::isdigit(static_cast(text_[pos_]))) { + long v = 0; + while (pos_ < text_.size() + && std::isdigit(static_cast(text_[pos_]))) { + v = v * 10 + (text_[pos_++] - '0'); + } + return v; + } + size_t start = pos_; + while (pos_ < text_.size() + && (std::isalnum(static_cast(text_[pos_])) + || text_[pos_] == '_')) { + ++pos_; + } + auto it = params_.find(text_.substr(start, pos_ - start)); + if (start == pos_ || it == params_.end()) { + Fail(); + } + return it->second; + } + std::string text_; + const std::map& params_; + size_t pos_ = 0; +}; + +// `text` without its // and /* */ comments. +std::string StripComments(const std::string& text) +{ + std::string out; + out.reserve(text.size()); + for (size_t i = 0; i < text.size(); ++i) { + if (text.compare(i, 2, "//") == 0) { + i = text.find('\n', i); + if (i == std::string::npos) { + break; + } + out += '\n'; + } else if (text.compare(i, 2, "/*") == 0) { + i = text.find("*/", i + 2); + if (i == std::string::npos) { + break; + } + ++i; + } else { + out += text[i]; + } + } + return out; +} + +// The text between the parenthesis at `open` and its match. +std::string Balanced(const std::string& text, size_t open, size_t* end) +{ + int depth = 0; + for (size_t i = open; i < text.size(); ++i) { + if (text[i] == '(') { + ++depth; + } else if (text[i] == ')' && --depth == 0) { + *end = i; + return text.substr(open + 1, i - open - 1); + } + } + throw std::runtime_error("unbalanced parentheses in a module header"); +} + +// Splits at the commas outside brackets and parentheses. +std::vector TopLevelCommas(const std::string& text) +{ + std::vector out; + int depth = 0; + std::string cur; + for (char c : text) { + if (c == '(' || c == '[' || c == '{') { + ++depth; + } else if (c == ')' || c == ']' || c == '}') { + --depth; + } + if (c == ',' && depth == 0) { + out.push_back(cur); + cur.clear(); + } else { + cur += c; + } + } + out.push_back(cur); + return out; +} + std::string F(double v) { std::ostringstream s; @@ -217,8 +392,12 @@ void WriteLiberty(odb::dbBlock* block, << " capacitance : " << F(clk_pf) << ";\n" << " clock : true;\n" << " }\n"; + std::vector quiet = spec.unused; if (!spec.reset.empty()) { - o << " pin(" << spec.reset << ") {\n" + quiet.insert(quiet.begin(), spec.reset); + } + for (const auto& name : quiet) { + o << " pin(" << name << ") {\n" << " direction : input;\n" << " capacitance : 0.001000;\n" << " }\n"; @@ -336,45 +515,84 @@ std::vector ReadModulePorts(const std::string& verilog, } std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); - std::regex head("\\bmodule\\s+" + module + "\\s*\\("); + std::regex head("\\bmodule\\s+" + module + "\\s*(#\\s*)?\\("); std::smatch mh; if (!std::regex_search(text, mh, head)) { throw std::runtime_error("module " + module + " is not in " + verilog); } - size_t start = mh.position(0) + mh.length(0); - size_t end = text.find(");", start); - if (end == std::string::npos) { - throw std::runtime_error("module " + module + ": header never closes"); + // From the header on, without comments, which may hold parentheses: a + // linear scan, as std::regex recurses per character and a generated + // core's Verilog runs to megabytes. + text = StripComments(text.substr(mh.position(0))); + if (!std::regex_search(text, mh, head) || mh.position(0) != 0) { + throw std::runtime_error("module " + module + ": cannot read its header"); + } + size_t open = mh.length(0) - 1; + size_t close = 0; + // A SystemVerilog parameter list, `#(parameter int W = 32, ...)`: its + // defaults size the ports. + std::map params; + if (mh[1].matched) { + std::string plist = Balanced(text, open, &close); + std::regex param( + "^\\s*(?:parameter|localparam)?\\s*(?:[A-Za-z_][A-Za-z_0-9:]*\\s+)*" + "(?:\\[[^\\]]*\\]\\s*)?([A-Za-z_][A-Za-z_0-9]*)\\s*=\\s*([\\s\\S]+?)" + "\\s*$"); + for (const auto& p : TopLevelCommas(plist)) { + std::smatch m; + if (std::regex_match(p, m, param)) { + try { + params[m[1].str()] = ConstExpr(m[2].str(), params).Eval(); + } catch (const std::runtime_error&) { + // A type or string parameter: no port width can use it here, + // and one that does fails below, naming it. + } + } + } + open = text.find('(', close + 1); + if (open == std::string::npos) { + throw std::runtime_error("module " + module + ": header never closes"); + } } - std::string header = text.substr(start, end - start); - // Strip comments. - header = std::regex_replace(header, std::regex("//[^\n]*"), ""); - header = std::regex_replace(header, std::regex("/\\*[\\s\\S]*?\\*/"), ""); + std::string header = Balanced(text, open, &close); // ANSI ports, one entry per comma: `input [4:0] a, b` declares two, the // second inheriting the first's direction and width, whether it is on - // the same line or the next. + // the same line or the next. Packed dimensions multiply. An entry that + // is not a port declaration this can read is an error, never skipped. std::vector ports; std::regex decl( - "^\\s*(input|output|inout)?\\s*(?:wire|reg|logic)?\\s*" - "(\\[\\s*(\\d+)\\s*:\\s*(\\d+)\\s*\\])?\\s*" + "^\\s*(input|output|inout)?\\s*(?:(?:wire|reg|logic|var|signed)\\s+)*" + "((?:\\[[^\\]]*\\]\\s*)*)" "([A-Za-z_][A-Za-z_0-9$]*)\\s*$"); + std::regex dim("\\[([^:\\]]+):([^\\]]+)\\]"); bool input = true; int width = 1; - std::istringstream entries(header); - std::string entry; - while (std::getline(entries, entry, ',')) { + for (const auto& entry : TopLevelCommas(header)) { + if (entry.find_first_not_of(" \t\r\n") == std::string::npos) { + continue; + } std::smatch m; if (!std::regex_match(entry, m, decl)) { - continue; + throw std::runtime_error("module " + module + + ": cannot read the port declaration `" + entry + + "`"); } if (m[1].matched) { input = m[1].str() != "output"; width = 1; } - if (m[2].matched) { - width = std::abs(std::stoi(m[3].str()) - std::stoi(m[4].str())) + 1; + const std::string dims = m[2].str(); + if (!dims.empty() || m[1].matched) { + width = 1; + for (std::sregex_iterator it(dims.begin(), dims.end(), dim), done; + it != done; + ++it) { + const long hi = ConstExpr((*it)[1].str(), params).Eval(); + const long lo = ConstExpr((*it)[2].str(), params).Eval(); + width *= static_cast(std::labs(hi - lo) + 1); + } } - ports.push_back(RtlPort{m[5].str(), width, input}); + ports.push_back(RtlPort{m[3].str(), width, input}); } return ports; } @@ -388,6 +606,9 @@ std::vector CheckPorts(const Spec& spec, if (!spec.reset.empty()) { want[spec.reset] = RtlPort{spec.reset, 1, true}; } + for (const auto& u : spec.unused) { + want[u] = RtlPort{u, 1, true}; + } const int banks = std::max(spec.banks, 1); const int A_bank = AddrWidth(spec.words / banks); for (const auto& r : spec.read) { diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 81cc32514f9..445db267ab4 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -17,6 +17,7 @@ PASSFAIL_TESTS = [ "generate_regfile_clock_gate_asap7", "generate_regfile_reset_asap7", "generate_regfile_reset_high_asap7", + "generate_regfile_sv_ports_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -82,6 +83,10 @@ filegroup( "generate_regfile_reset_high_asap7": REGFILE_TEST_DATA + [ "generate_regfile_asap7.regfile", ], + "generate_regfile_sv_ports_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + "generate_regfile_sv_ports_asap7_rtl.sv", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index a7e9c00b96c..1162a902fd7 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -10,4 +10,5 @@ or_integration_tests( generate_regfile_clock_gate_asap7 generate_regfile_reset_asap7 generate_regfile_reset_high_asap7 + generate_regfile_sv_ports_asap7 ) diff --git a/src/ram/test/generate_regfile_sv_ports_asap7.tcl b/src/ram/test/generate_regfile_sv_ports_asap7.tcl new file mode 100644 index 00000000000..ef43a8c13b7 --- /dev/null +++ b/src/ram/test/generate_regfile_sv_ports_asap7.tcl @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_sv_ports_asap7 +# The port check reads a SystemVerilog module header: a #(...) parameter +# list, port widths written in the parameters' defaults, and an input the +# register file does not use (`unused`). It refuses a width that differs +# once the parameters are evaluated, and a declaration it cannot read, +# rather than skipping it. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries +set spec [rf_spec generate_regfile_asap7.regfile \ + generate_regfile_sv_ports_asap7.regfile {"unused test_en_i"}] + +# The same module with other text: a copy of the RTL in the results dir. +proc rtl_variant { name from to } { + set in [open generate_regfile_sv_ports_asap7_rtl.sv r] + set text [read $in] + close $in + set file [make_result_file $name] + set out [open $file w] + puts -nonewline $out [string map [list $from $to] $text] + close $out + return $file +} + +# generate_regfile's error and its log for `rtl`; tee runs its script +# unsubstituted, so the command is built first. +proc refusal { rtl } { + global spec + set cmd [list generate_regfile -spec $spec -check_ports $rtl] + set failed [catch { tee -quiet -variable log $cmd } message] + return [list $failed $message $log] +} + +lassign [refusal [rtl_variant wide.sv "W = 4" "W = 8"]] failed message log +check "a width that differs once W is evaluated is refused" { set failed } 1 +check "the refusal is RAM-0050" { string match "*RAM-0050*" $message } 1 +check "the refusal names the port and both widths" \ + { string match "*port r0_data is 8 bits in the module, 4 in the spec*" $log } 1 + +lassign [refusal [rtl_variant typed.sv "logic \[A-1:0\] r1_addr" \ + "rf_pkg::addr_t r1_addr"]] failed message log +check "a declaration the check cannot read is refused" { set failed } 1 +check "the refusal quotes it" \ + { string match "*cannot read the port declaration*rf_pkg::addr_t r1_addr*" $log } 1 + +generate_regfile -spec $spec -check_ports generate_regfile_sv_ports_asap7_rtl.sv +check "the unused input is a port of the register file" \ + { expr { [[ord::get_db_block] findBTerm test_en_i] ne "NULL" } } 1 + +for { set w 1 } { $w < 8 } { incr w } { + rf_store $w 4 [rf_word_value $w 4] +} +for { set a 0 } { $a < 8 } { incr a } { + rf_drive r1_addr 3 $a + set expected [expr { $a == 0 ? 0 : [rf_word_value $a 4] }] + check "r1 reads word $a" { rf_read r1_data 4 } $expected +} + +exit_summary diff --git a/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv b/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv new file mode 100644 index 00000000000..bfda9bdd337 --- /dev/null +++ b/src/ram/test/generate_regfile_sv_ports_asap7_rtl.sv @@ -0,0 +1,20 @@ +// rf8x4 as a SystemVerilog module: a parameter list, port widths written +// in its parameters, and an input the register file does not use. +module rf8x4 #( + parameter int unsigned W = 4, + parameter int unsigned A = 3 +) ( + input logic clock, + input logic test_en_i, + input logic [A-1:0] r0_addr, + output logic [W-1:0] r0_data, + input logic [A-1:0] r1_addr, + output logic [W-1:0] r1_data, + input logic [A-1:0] w0_addr, + input logic [W-1:0] w0_data, + input logic w0_en, + input logic [A-1:0] w1_addr, + input logic [W-1:0] w1_data, + input logic w1_en +); +endmodule From 96dd5bae4c85fe30f314c1c74916012cbb7e607b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 7 Oct 2026 21:06:42 +0200 Subject: [PATCH 4/8] ram: generate_regfile write_priority, the later write port wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two write ports writing one word in one cycle had their data ORed: right for an RTL that never does it (swerv asserts so), wrong for one that does. cva6's superscalar commit can retire two instructions writing one register, and its register file lets the later commit port win. `write_priority last` masks each write select by the later ports' (an inverter, an AND and an OR per port per word, in the word's header); the clock gate still opens on any write. `or` stays the default. Signed-off-by: Øyvind Harboe Test: generate_regfile_write_priority_asap7: both ports writing one word load the later port's data; writes to different words load their own. Co-Authored-By: Claude Opus 5.5 --- src/ram/README.md | 1 + src/ram/src/regfile.cpp | 42 +++++++++++++++++++ src/ram/src/regfile.h | 4 ++ src/ram/test/BUILD | 4 ++ src/ram/test/CMakeLists.txt | 1 + .../generate_regfile_write_priority_asap7.tcl | 36 ++++++++++++++++ 6 files changed, 88 insertions(+) create mode 100644 src/ram/test/generate_regfile_write_priority_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index aeffef0a82d..f8a2d2be250 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -121,6 +121,7 @@ One key per line, `#` comments: | `words n`, `bits n` | Depth and width. | | `clock port` | The clock port. | | `reset port` | A reset port the RTL has and the array ignores. | +| `write_priority or\|last` | Two ports writing one word in one cycle: `or` (the default) ORs their data, for an RTL that never does it; `last`, the later write port in the spec wins, as an RTL's later assignment does. | | `unused port...` | One-bit inputs the RTL module has and the array ignores (ibex's `test_en_i`), so the block keeps the module's ports. | | `async_reset port low\|high` | The reset clears every stored word to zero asynchronously, active low or high; the storage flops are `cell flop_r` with their set tied off by `cell tie_hi`, one tie (and for an active-high reset one inverter) per word. Read-address registers are not reset. One of `reset` or `async_reset`. | | `read addr data` | A read port, by the module's port names. Repeat per port. | diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp index f33d4c68380..ce32a72618b 100644 --- a/src/ram/src/regfile.cpp +++ b/src/ram/src/regfile.cpp @@ -616,6 +616,10 @@ dbBlock* Builder::Run() const int header_cells_w = static_cast( (R + W) * A * and2_->getWidth() + (W - 1) * or2_->getWidth() + (gated ? icg_->getWidth() : inv_->getWidth()) + + (s.write_priority == "last" + ? (W - 1) + * (inv_->getWidth() + and2_->getWidth() + or2_->getWidth()) + : 0) + (s.async_reset ? tie_hi_->getWidth() + (s.reset_active_low ? 0 : inv_->getWidth()) : 0)); @@ -830,6 +834,37 @@ dbBlock* Builder::Run() wsel[w][n] = Decode(hcur(), wn + "_wsel" + std::to_string(w), l); wsels.push_back(wsel[w][n]); } + if (s.write_priority == "last" && W > 1) { + // A later port wins: each write select masked by the later ones'. + // The clock gate and hold still see every write (wsels). + dbNet* later = wsel[W - 1][n]; + for (int w = W - 2; w >= 0; --w) { + const std::string k = wn + "_wp" + std::to_string(w); + dbNet* later_n = Net(k + "_ln"); + Place(hcur(), + inv_, + later_n->getName(), + {{g_pins.inv[0], later}, {g_pins.inv[1], later_n}}); + dbNet* masked = Net(k + "_sel"); + Place(hcur(), + and2_, + masked->getName(), + {{g_pins.and2[0], wsel[w][n]}, + {g_pins.and2[1], later_n}, + {g_pins.and2[2], masked}}); + if (w > 0) { + dbNet* any_later = Net(k + "_l"); + Place(hcur(), + or2_, + any_later->getName(), + {{g_pins.or2[0], later}, + {g_pins.or2[1], wsel[w][n]}, + {g_pins.or2[2], any_later}}); + later = any_later; + } + wsel[w][n] = masked; + } + } // hold = ~(wsel0 | wsel1 | ...): the word keeps its value. dbNet* any_write = wsels.size() == 1 @@ -1283,6 +1318,13 @@ Spec ReadSpec(const std::string& path) + ": one of `reset` or `async_reset`, once"); } s.reset = v[0]; + } else if (key == "write_priority") { + need(1); + if (v[0] != "or" && v[0] != "last") { + Refuse(path + ":" + std::to_string(lineno) + + ": `write_priority or|last`"); + } + s.write_priority = v[0]; } else if (key == "unused") { if (v.empty()) { Refuse(path + ":" + std::to_string(lineno) diff --git a/src/ram/src/regfile.h b/src/ram/src/regfile.h index fc7109a413a..649ac3fe814 100644 --- a/src/ram/src/regfile.h +++ b/src/ram/src/regfile.h @@ -141,6 +141,10 @@ struct Spec // data itself with one write port and a select mux of the writes with // more. std::string write_style = "mux"; + // Two ports writing one word in one cycle: "or" (the default) ORs their + // data, for an RTL that never does it; "last", the later port in the + // spec wins, as an RTL's later assignment does (cva6's commit ports). + std::string write_priority = "or"; // Instance names of the flops, as patterns, so a flop carries the name // of the RTL register bit it implements (an equivalence check matches // sequential instances by name). {word} {bit} for the storage, diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 445db267ab4..a8f309af26a 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -18,6 +18,7 @@ PASSFAIL_TESTS = [ "generate_regfile_reset_asap7", "generate_regfile_reset_high_asap7", "generate_regfile_sv_ports_asap7", + "generate_regfile_write_priority_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -87,6 +88,9 @@ filegroup( "generate_regfile_asap7.regfile", "generate_regfile_sv_ports_asap7_rtl.sv", ], + "generate_regfile_write_priority_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 1162a902fd7..52145c0038f 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -11,4 +11,5 @@ or_integration_tests( generate_regfile_reset_asap7 generate_regfile_reset_high_asap7 generate_regfile_sv_ports_asap7 + generate_regfile_write_priority_asap7 ) diff --git a/src/ram/test/generate_regfile_write_priority_asap7.tcl b/src/ram/test/generate_regfile_write_priority_asap7.tcl new file mode 100644 index 00000000000..39374756281 --- /dev/null +++ b/src/ram/test/generate_regfile_write_priority_asap7.tcl @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_write_priority_asap7 +# generate_regfile with `write_priority last`: when both write ports +# write one word in one cycle, the word loads the later port's data; +# writes to different words each load their own. Checked by constant +# propagation through the generated cells. + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries +set spec [rf_spec generate_regfile_asap7.regfile \ + generate_regfile_write_priority_asap7.regfile {"write_priority last"}] +generate_regfile -spec $spec -check_ports generate_regfile_asap7_rtl.v + +for { set w 1 } { $w < 8 } { incr w } { + rf_store $w 4 [rf_word_value $w 4] +} +rf_drive_bit w0_en 1 +rf_drive_bit w1_en 1 +foreach a { 1 4 7 } { + rf_drive w0_addr 3 $a + rf_drive w1_addr 3 $a + rf_drive w0_data 4 5 + rf_drive w1_data 4 10 + check "both ports write word $a: it loads w1's data" { rf_next $a 4 } 10 +} +rf_drive w0_addr 3 2 +rf_drive w1_addr 3 6 +check "w0 writes word 2: it loads w0's data" { rf_next 2 4 } 5 +check "w1 writes word 6: it loads w1's data" { rf_next 6 4 } 10 +check "word 3 holds" { rf_next 3 4 } [rf_word_value 3 4] + +exit_summary From 16d446a9680715a598e7ab258d4457415d146d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 11:09:11 +0200 Subject: [PATCH 5/8] ram: generate_regfile, name Port fields when reading the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port gained bank_addr and bank_data for read_banked, and the positional Port{addr, data, en} initializers in ReadSpec now trip -Wmissing-field-initializers under -Werror in the bazel build. Designated initializers name the fields and leave the bank vectors empty, so a new field does not break them again. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/regfile.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp index ce32a72618b..4474d1883ac 100644 --- a/src/ram/src/regfile.cpp +++ b/src/ram/src/regfile.cpp @@ -1346,7 +1346,7 @@ Spec ReadSpec(const std::string& path) s.reset_active_low = v[1] == "low"; } else if (key == "read") { need(2); - s.read.push_back(Port{v[0], v[1], ""}); + s.read.push_back(Port{.addr = v[0], .data = v[1]}); } else if (key == "read_banked") { if (v.size() < 2 || v.size() % 2 != 0) { Refuse(path + ":" + std::to_string(lineno) @@ -1363,7 +1363,8 @@ Spec ReadSpec(const std::string& path) Refuse(path + ":" + std::to_string(lineno) + ": `write` takes addr data [en]"); } - s.write.push_back(Port{v[0], v[1], v.size() == 3 ? v[2] : ""}); + s.write.push_back( + Port{.addr = v[0], .data = v[1], .en = v.size() == 3 ? v[2] : ""}); } else if (key == "cell") { need(2); const std::string& kind = v[0]; From 5975b4e2c8f5ba0aa147e516493efa28e2882b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 14:25:35 +0200 Subject: [PATCH 6/8] ram: generate_regfile, initialize every Port field by name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bazel build's clang fails with -Wmissing-designated-field-initializers under -Werror when a designated initializer leaves out fields of Port (en, bank_addr, bank_data). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/regfile.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ram/src/regfile.cpp b/src/ram/src/regfile.cpp index 4474d1883ac..fb4b113bda1 100644 --- a/src/ram/src/regfile.cpp +++ b/src/ram/src/regfile.cpp @@ -1346,7 +1346,11 @@ Spec ReadSpec(const std::string& path) s.reset_active_low = v[1] == "low"; } else if (key == "read") { need(2); - s.read.push_back(Port{.addr = v[0], .data = v[1]}); + s.read.push_back(Port{.addr = v[0], + .data = v[1], + .en = "", + .bank_addr = {}, + .bank_data = {}}); } else if (key == "read_banked") { if (v.size() < 2 || v.size() % 2 != 0) { Refuse(path + ":" + std::to_string(lineno) @@ -1363,8 +1367,11 @@ Spec ReadSpec(const std::string& path) Refuse(path + ":" + std::to_string(lineno) + ": `write` takes addr data [en]"); } - s.write.push_back( - Port{.addr = v[0], .data = v[1], .en = v.size() == 3 ? v[2] : ""}); + s.write.push_back(Port{.addr = v[0], + .data = v[1], + .en = v.size() == 3 ? v[2] : "", + .bank_addr = {}, + .bank_data = {}}); } else if (key == "cell") { need(2); const std::string& kind = v[0]; From 8b5610825d3853b9b1f00e63e7dc2c6f6b30205b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:39:08 +0200 Subject: [PATCH 7/8] ram: generate_regfile says what mode netlist leaves unplaced, and tests it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In mode netlist the generator fixes the core (storage flops and read bitlines) and leaves the periphery (address inverters, decode, hold) unplaced for the parent's placer and resizer, which alone know the drivers and loads outside the array. On its own, such a block shows those cells unplaced and said nothing about why. RAM-0051 now reports how many it left and how many it fixed, the README says what each mode places, and the tests check it: generate_regfile_asap7 (mode netlist) that every storage flop is fixed and the periphery left unplaced, and generate_regfile_modes_asap7 that mode macro places every instance. Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 +- src/ram/src/regfile_cmd.cpp | 21 ++++++++++++++ src/ram/test/BUILD | 4 +++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/generate_regfile_asap7.tcl | 15 ++++++++-- src/ram/test/generate_regfile_modes_asap7.tcl | 28 +++++++++++++++++++ src/ram/test/regfile_checks.tcl | 23 +++++++++++++++ 7 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/ram/test/generate_regfile_modes_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index f8a2d2be250..708288cc596 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -117,7 +117,7 @@ One key per line, `#` comments: | Key | Description | | ---------------------- | -------------------------------------- | | `module name` | The module and block name. | -| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent. `netlist`: the parent places the cells; the address decode is left unplaced for the parent's placer and resizer, and the abstract's pins sit where their connections land in the array. | +| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent; every cell is placed, the address inverters placed but movable so the macro's own repair can size them. `netlist`: the block is placed as a macro and then dissolves into its parent's cells: the core (storage flops and read bitlines) is fixed where it lies, and the periphery (address inverters, decode, hold) is left unplaced, RAM-0051 says how much, for the parent's placer and resizer, which alone know the drivers and loads outside the array; the abstract's pins sit where their connections land in the array. Which shape is better, and whether the periphery should be unplaced or placed but movable, is still being measured. | | `words n`, `bits n` | Depth and width. | | `clock port` | The clock port. | | `reset port` | A reset port the RTL has and the array ignores. | diff --git a/src/ram/src/regfile_cmd.cpp b/src/ram/src/regfile_cmd.cpp index 6b600efab36..e2eae8bccb8 100644 --- a/src/ram/src/regfile_cmd.cpp +++ b/src/ram/src/regfile_cmd.cpp @@ -41,6 +41,27 @@ void generateRegfile(odb::dbDatabase* db, } } odb::dbBlock* block = regfile::Generate(db, logger, spec); + // In mode netlist the generator fixes the core and leaves the + // periphery to the parent, which alone knows the drivers and loads + // outside the array; the block on its own shows those cells unplaced. + int fixed = 0; + int unplaced = 0; + for (odb::dbInst* inst : block->getInsts()) { + if (!inst->getPlacementStatus().isPlaced()) { + ++unplaced; + } else if (inst->getPlacementStatus().isFixed()) { + ++fixed; + } + } + if (unplaced > 0) { + logger->info(utl::RAM, + 51, + "mode {}: {} periphery instances left unplaced for the " + "parent to place, size and buffer; {} fixed.", + spec.mode, + unplaced, + fixed); + } if (!verilog_path.empty()) { regfile::WriteVerilog(block, verilog_path); } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index a8f309af26a..9a834644a3e 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -15,6 +15,7 @@ COMPULSORY_TESTS = [ PASSFAIL_TESTS = [ "generate_regfile_asap7", "generate_regfile_clock_gate_asap7", + "generate_regfile_modes_asap7", "generate_regfile_reset_asap7", "generate_regfile_reset_high_asap7", "generate_regfile_sv_ports_asap7", @@ -78,6 +79,9 @@ filegroup( "generate_regfile_clock_gate_asap7": REGFILE_TEST_DATA + [ "generate_regfile_asap7.regfile", ], + "generate_regfile_modes_asap7": REGFILE_TEST_DATA + [ + "generate_regfile_asap7.regfile", + ], "generate_regfile_reset_asap7": REGFILE_TEST_DATA + [ "generate_regfile_asap7.regfile", ], diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 52145c0038f..a02f25bf641 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -8,6 +8,7 @@ or_integration_tests( PASSFAIL_TESTS generate_regfile_asap7 generate_regfile_clock_gate_asap7 + generate_regfile_modes_asap7 generate_regfile_reset_asap7 generate_regfile_reset_high_asap7 generate_regfile_sv_ports_asap7 diff --git a/src/ram/test/generate_regfile_asap7.tcl b/src/ram/test/generate_regfile_asap7.tcl index 270a90ad85c..8f6f84dac76 100644 --- a/src/ram/test/generate_regfile_asap7.tcl +++ b/src/ram/test/generate_regfile_asap7.tcl @@ -27,9 +27,20 @@ check "the refusal names the port" \ set lef_file [make_result_file generate_regfile_asap7.lef] set lib_file [make_result_file generate_regfile_asap7.lib] -generate_regfile -spec generate_regfile_asap7.regfile \ +tee -quiet -variable gen_log [list generate_regfile \ + -spec generate_regfile_asap7.regfile \ -check_ports generate_regfile_asap7_rtl.v \ - -lef $lef_file -liberty $lib_file + -lef $lef_file -liberty $lib_file] + +# mode netlist: the generator fixes the core and leaves the periphery +# (address inverters, decode, hold) to the parent, which places, sizes +# and buffers it with the drivers and loads outside the array. +set placement [rf_placement] +check "mode netlist fixes every storage flop" \ + { dict get $placement flops_not_fixed } 0 +check "mode netlist leaves the periphery unplaced" \ + { expr { [dict get $placement unplaced] > 0 } } 1 +check "and says so" { string match "*RAM-0051*mode netlist*" $gen_log } 1 set words 8 set bits 4 diff --git a/src/ram/test/generate_regfile_modes_asap7.tcl b/src/ram/test/generate_regfile_modes_asap7.tcl new file mode 100644 index 00000000000..c9f895c1482 --- /dev/null +++ b/src/ram/test/generate_regfile_modes_asap7.tcl @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors +# +# Test: ram/generate_regfile_modes_asap7 +# mode macro: the register file is a macro to its parent, every instance +# placed by the generator, none left to the parent, and nothing to say +# about it. (mode netlist, which leaves the periphery to the parent, is +# checked by generate_regfile_asap7.) + +source "helpers.tcl" +source "regfile_checks.tcl" + +rf_read_libraries + +set spec [rf_spec generate_regfile_asap7.regfile \ + generate_regfile_modes_asap7.regfile {"mode macro"}] +tee -quiet -variable gen_log [list generate_regfile \ + -spec $spec -check_ports generate_regfile_asap7_rtl.v] + +set placement [rf_placement] +check "mode macro places every instance" \ + { dict get $placement unplaced } 0 +check "mode macro fixes every storage flop" \ + { dict get $placement flops_not_fixed } 0 +check "mode macro reports nothing left unplaced" \ + { string match "*RAM-0051*" $gen_log } 0 + +exit_summary diff --git a/src/ram/test/regfile_checks.tcl b/src/ram/test/regfile_checks.tcl index 777bce5aabd..56b5ae3ce8a 100644 --- a/src/ram/test/regfile_checks.tcl +++ b/src/ram/test/regfile_checks.tcl @@ -111,3 +111,26 @@ proc rf_ports_missing_from { file kind } { } return $missing } + +# The block's instances by placement: fixed, placed (movable) and +# unplaced, and how many of the storage flops (DFF*) are not fixed. +proc rf_placement { } { + set n [dict create fixed 0 placed 0 unplaced 0 flops_not_fixed 0] + foreach inst [[ord::get_db_block] getInsts] { + set status [$inst getPlacementStatus] + if { $status == "FIRM" || $status == "LOCKED" || $status == "COVER" } { + dict incr n fixed + } elseif { $status == "PLACED" || $status == "SUGGESTED" } { + dict incr n placed + } else { + dict incr n unplaced + } + if { + [string match "DFF*" [[$inst getMaster] getName]] + && $status != "FIRM" + } { + dict incr n flops_not_fixed + } + } + return $n +} From 691d8e15184d6d7baf40944536dc5980f8b9a685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 09:31:16 +0200 Subject: [PATCH 8/8] ram: generate_regfile, unplaced-periphery info is RAM-0052 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAM-0051 is generate_regfile's "needs -spec." error in ram.tcl, so the mode netlist info message that reports the unplaced periphery takes the next free id. Fixes the messages_txt duplicate-id check. Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 +- src/ram/src/regfile_cmd.cpp | 2 +- src/ram/test/generate_regfile_asap7.tcl | 2 +- src/ram/test/generate_regfile_modes_asap7.tcl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ram/README.md b/src/ram/README.md index 708288cc596..034eb02a14c 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -117,7 +117,7 @@ One key per line, `#` comments: | Key | Description | | ---------------------- | -------------------------------------- | | `module name` | The module and block name. | -| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent; every cell is placed, the address inverters placed but movable so the macro's own repair can size them. `netlist`: the block is placed as a macro and then dissolves into its parent's cells: the core (storage flops and read bitlines) is fixed where it lies, and the periphery (address inverters, decode, hold) is left unplaced, RAM-0051 says how much, for the parent's placer and resizer, which alone know the drivers and loads outside the array; the abstract's pins sit where their connections land in the array. Which shape is better, and whether the periphery should be unplaced or placed but movable, is still being measured. | +| `mode macro\|netlist` | `macro` (default): the block is a macro to its parent; every cell is placed, the address inverters placed but movable so the macro's own repair can size them. `netlist`: the block is placed as a macro and then dissolves into its parent's cells: the core (storage flops and read bitlines) is fixed where it lies, and the periphery (address inverters, decode, hold) is left unplaced, RAM-0052 says how much, for the parent's placer and resizer, which alone know the drivers and loads outside the array; the abstract's pins sit where their connections land in the array. Which shape is better, and whether the periphery should be unplaced or placed but movable, is still being measured. | | `words n`, `bits n` | Depth and width. | | `clock port` | The clock port. | | `reset port` | A reset port the RTL has and the array ignores. | diff --git a/src/ram/src/regfile_cmd.cpp b/src/ram/src/regfile_cmd.cpp index e2eae8bccb8..d90332ba559 100644 --- a/src/ram/src/regfile_cmd.cpp +++ b/src/ram/src/regfile_cmd.cpp @@ -55,7 +55,7 @@ void generateRegfile(odb::dbDatabase* db, } if (unplaced > 0) { logger->info(utl::RAM, - 51, + 52, "mode {}: {} periphery instances left unplaced for the " "parent to place, size and buffer; {} fixed.", spec.mode, diff --git a/src/ram/test/generate_regfile_asap7.tcl b/src/ram/test/generate_regfile_asap7.tcl index 8f6f84dac76..2e26e14c4d8 100644 --- a/src/ram/test/generate_regfile_asap7.tcl +++ b/src/ram/test/generate_regfile_asap7.tcl @@ -40,7 +40,7 @@ check "mode netlist fixes every storage flop" \ { dict get $placement flops_not_fixed } 0 check "mode netlist leaves the periphery unplaced" \ { expr { [dict get $placement unplaced] > 0 } } 1 -check "and says so" { string match "*RAM-0051*mode netlist*" $gen_log } 1 +check "and says so" { string match "*RAM-0052*mode netlist*" $gen_log } 1 set words 8 set bits 4 diff --git a/src/ram/test/generate_regfile_modes_asap7.tcl b/src/ram/test/generate_regfile_modes_asap7.tcl index c9f895c1482..6ac445d0755 100644 --- a/src/ram/test/generate_regfile_modes_asap7.tcl +++ b/src/ram/test/generate_regfile_modes_asap7.tcl @@ -23,6 +23,6 @@ check "mode macro places every instance" \ check "mode macro fixes every storage flop" \ { dict get $placement flops_not_fixed } 0 check "mode macro reports nothing left unplaced" \ - { string match "*RAM-0051*" $gen_log } 0 + { string match "*RAM-0052*" $gen_log } 0 exit_summary