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..034eb02a14c 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -80,9 +80,70 @@ 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. 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. | +| `-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; 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. | +| `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. | +| `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`; 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. | +| `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..fb4b113bda1 --- /dev/null +++ b/src/ram/src/regfile.cpp @@ -0,0 +1,1642 @@ +// 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 + // 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"}; + 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* store_ = nullptr; // the storage flop: flop_, or flop_r + dbMaster* tie_hi_ = 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); + } + 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(); + 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, + // 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); + 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( + 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. + 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({store_->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(store_->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()) + + (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)); + 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]); + } + 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 + ? 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]}}); + } + // 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) { + 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; + const std::string ff_name + = s.store_name.empty() + ? tn + "_ff" + : 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_, + 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 (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) { + 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); + if (!s.reset.empty()) { + Refuse(path + ":" + std::to_string(lineno) + + ": 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) + + ": `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()) { + 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{.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) + + ": `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{.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]; + 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") { + 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 == "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") { + 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"); + } + 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; +} + +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..649ac3fe814 --- /dev/null +++ b/src/ram/src/regfile.h @@ -0,0 +1,182 @@ +// 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) + // 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"; + 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 + // `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; + // `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; + 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"; + // 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, + // {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..d90332ba559 --- /dev/null +++ b/src/ram/src/regfile_cmd.cpp @@ -0,0 +1,93 @@ +// 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); + // 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, + 52, + "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); + } + 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..1fe0e3bba55 --- /dev/null +++ b/src/ram/src/views.cpp @@ -0,0 +1,662 @@ +// 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 +#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); +} + +// 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; + 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"; + std::vector quiet = spec.unused; + if (!spec.reset.empty()) { + quiet.insert(quiet.begin(), spec.reset); + } + for (const auto& name : quiet) { + o << " pin(" << name << ") {\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*(#\\s*)?\\("); + std::smatch mh; + if (!std::regex_search(text, mh, head)) { + throw std::runtime_error("module " + module + " is not in " + verilog); + } + // 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 = 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. 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|var|signed)\\s+)*" + "((?:\\[[^\\]]*\\]\\s*)*)" + "([A-Za-z_][A-Za-z_0-9$]*)\\s*$"); + std::regex dim("\\[([^:\\]]+):([^\\]]+)\\]"); + bool input = true; + int width = 1; + 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)) { + throw std::runtime_error("module " + module + + ": cannot read the port declaration `" + entry + + "`"); + } + if (m[1].matched) { + input = m[1].str() != "output"; + width = 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[3].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}; + } + 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) { + 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..9a834644a3e 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -13,10 +13,32 @@ 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", + "generate_regfile_write_priority_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 +73,28 @@ 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", + ], + "generate_regfile_modes_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", + ], + "generate_regfile_sv_ports_asap7": REGFILE_TEST_DATA + [ + "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 8545b3dbca9..a02f25bf641 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -5,5 +5,12 @@ 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 + generate_regfile_modes_asap7 + 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/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..2e26e14c4d8 --- /dev/null +++ b/src/ram/test/generate_regfile_asap7.tcl @@ -0,0 +1,87 @@ +# 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] +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] + +# 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-0052*mode netlist*" $gen_log } 1 + +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/generate_regfile_modes_asap7.tcl b/src/ram/test/generate_regfile_modes_asap7.tcl new file mode 100644 index 00000000000..6ac445d0755 --- /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-0052*" $gen_log } 0 + +exit_summary 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 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 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 diff --git a/src/ram/test/regfile_checks.tcl b/src/ram/test/regfile_checks.tcl new file mode 100644 index 00000000000..56b5ae3ce8a --- /dev/null +++ b/src/ram/test/regfile_checks.tcl @@ -0,0 +1,136 @@ +# 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 +} + +# 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 +}