diff --git a/src/ram/README.md b/src/ram/README.md index 2d7eb2e7058..8845be57b21 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -33,6 +33,8 @@ The `generate_ram` command is an all-in-one command to place and route a RAM. Standard cell libraries must be loaded before running this command. The module will create a new design, therefore a design should not be loaded before running the command. +The options marked **Experimental.** below, and more than one write port, were added for register files. They are a baseline to build on: their interface may change, and they may move to a command of their own, such as a `generate_register`, with these `generate_ram` options retired once flows use it. A run that uses any of them names them in RAM-0105, and `-tracks` in RAM-0106. + ```tcl generate_ram [-mask_size bits] -word_size bits @@ -43,6 +45,22 @@ generate_ram [-mask_size bits] [-column_mux_ratio ratio] [-storage_cell name] [-tristate_cell name] + [-read_style tristate|and_or] + [-zero_word word] + [-reset_value value] + [-banks count] + [-bank_columns count] + [-bank_order contiguous|interleaved] + [-rb_ports count] + [-register_read_address 0|1] + [-write_style clock_gate|mux] + [-write_priority last|or] + [-service_columns {every sites}] + [-bank_column_layout interleaved|contiguous] + [-cells {role master ...}] + [-name name] + [-decoder row|word] + [-bit_folds count] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -53,6 +71,9 @@ generate_ram [-mask_size bits] [-tapcell name] [-max_tap_dist value] [-write_behavioral_verilog filename] + [-periphery placed|verilog] + [-wrapper_verilog file] + [-tracks file] ``` #### Options @@ -62,12 +83,27 @@ generate_ram [-mask_size bits] | `-mask_size` | Determines the number of bits which are grouped together for masking during writes. For example, a mask size of `8` will enable each 8 bits of the word to be masked when writing (commonly known as byte masking). A mask size of `1` will enable each bit to be individually masked. The write enable signal for each port will be `(word_size / mask_size)` bits wide. The word size must be a multiple of the mask size. Default: equal to `-word_size`. | | `-word_size` | Size of each word in bits. | | `-num_words` | Number of words in the array. | -| `-rw_ports` | Number of read-write ports for the array. Sum of read-write ports and write ports must equal 1. Default: 1. | +| `-rw_ports` | Number of read-write ports for the array: 0 or 1, and a read-write port is the only write port. Default: 1. | | `-r_ports` | Number of read ports for the array. Default: 0. | -| `-w_ports` | Number of write ports for the array. Sum of read-write ports and write ports must equal 1. Default: 0. | +| `-w_ports` | Number of write ports for the array. With more than one, each has its own address decoder, data (`D0`, `D1`, ...), write enables (`we0`, ...) and address (`addr_w0`, ...); a word two ports write in the same cycle keeps the last port's data. More than one write port is **experimental**. Not yet with `-use_latch` or `-column_mux_ratio` above 1. Default: 0. | | `-column_mux_ratio` | Number of words sharing a physical column. Must be `1`, `2`, or `4`. The number of words must be divisible by the column mux ratio. Default: `1` (no column muxing). | | `-storage_cell` | Name of the master to use for the storage device (i.e. a flip-flop). Must be positive-edge triggered. Default: auto-select from the loaded cell library. | | `-tristate_cell` | Name of the master to use for the tristate device (i.e. a tristate inverter). It is currently assumed that the device is inverting. Default: auto-select from the loaded cell library. | +| `-read_style` | **Experimental.** The read path: `tristate`, a tristate bitline per bit, or `and_or`, a tree per bit column of an AOI22 over each pair of rows and NAND2 and NOR2 levels above, for a library without a tristate cell. `and_or` also takes a storage flop whose only output is inverted (QN). Default: `tristate` where the library has a tristate cell, `and_or` where it has none. | +| `-zero_word` | **Experimental.** A word that reads 0 and drops its writes, as a RISC-V register file's x0: its row has no flops or clock gate, and the read trees leave it out. Needs `-read_style and_or` and `-column_mux_ratio 1`. Default: none. | +| `-reset_value` | **Experimental.** Resets every word to `value`, decimal, `0x` hexadecimal or `0` octal and no wider than the word, asynchronously, through a reset port at the storage flop's own polarity (`rst_n` or `rst`); each bit's flop is cleared or preset, the other pin held off. The storage cell has both an asynchronous clear and preset, also for a value of 0. A parent adapts a reset of another polarity, or a synchronous one, to it. Not with `-use_latch`. Default: no reset. | +| `-banks`, `-bank_columns`, `-bank_order` | **Experimental.** The words in `banks` banks, contiguous or interleaved in the address, `bank_columns` of them side by side and the rest stacked. With banks each port decodes every word's whole address, so a bank need not be a power of two words. Not with `-column_mux_ratio` above 1 or `-use_latch`. Default: one bank. | +| `-rb_ports` | **Experimental.** Banked read ports, with `-banks`: each an address and a data output per bank (`addr_rb_b`, `Qrb_b`), the address the word's index in its bank, every bank read independently. The block has no mux across the banks: selecting a bank's data, and registering the selection, is the parent's. Default: 0. | +| `-register_read_address` | **Experimental.** `1` registers every read port's address, a plain positive-edge flop per address bit, so the decoders and read trees come after the flop and the data follows a cycle after the address. This is the read address register, not a register on the read data. A read whose address is registered on the clock that writes its word returns the new data (write-first). 0 and 1 only. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | +| `-write_style` | **Experimental.** How a word holds its value between writes: `clock_gate`, a clock gate per word; or `mux`, every flop on the clock and a mux per bit taking the new data or the flop's own value. A clock gate saves a mux per bit; a mux keeps every flop on the plain clock, so every flop is clocked every cycle and clock power grows with the block's bits, not the words written. Not with `-use_latch`. Default: `clock_gate`. | +| `-write_priority` | **Experimental.** Where two write ports write the same word in one cycle: `last`, the last port's data; or `or`, the OR of every writing port's data, as RTL that merges its writes with an OR describes it. `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | +| `-service_columns {every sites}` | **Experimental.** An empty column `sites` sites wide after every `every` columns of the array, its bits' and selects' alike: room inside the array for the buffers a parent flow inserts on the long nets that cross it, where the array is otherwise full. Default: none. | +| `-bank_column_layout interleaved\|contiguous` | **Experimental.** With banks, how a row's words share its columns: `interleaved`, bit by bit as the column mux lays them out; or `contiguous`, each word's bits side by side with its select column after them. Contiguous keeps a word's selects and write enables as short as its own bits, where interleaved runs them the whole row; the read trees and each write port's data nets then cross the row instead. Default: `interleaved`. | +| `-periphery`, `-pin_layer`, `-wrapper_verilog` | **Experimental.** `unplaced` and `-pin_layer` are `generate_ram_netlist`'s only, for a flow that places the periphery itself; `generate_ram` takes `placed` and `verilog`, routes the core and puts its pins on its edges. `unplaced` leaves the periphery (address inverters and registers, decoders, select buffers, each word's select cell, the input buffers) for a parent to place, size and buffer; the outline is the placed core and a row, and each pin sits where its connections land, on `-pin_layer`'s tracks, for a macro placer before the parent dissolves the core into its rows. `verilog` makes the core a block of its own, `_core`, and writes the periphery to `-wrapper_verilog` as a Verilog module, ``, around it: assigns of the cells' functions, for synthesis to map with the parent. Default: `placed`. | +| `-cells` | **Experimental.** The cell for a role (`inv`, `and2`, `buffer`, `clock_gate`, `aoi22`, `ao22`, `or2`, `nand2`, `nor2`, `tie`, `addr_flop`), instead of the smallest the libraries have of its function. An AOI22 or AO22 given is checked by its truth table. Default: the smallest of each. | +| `-name` | **Experimental.** The block's and the behavioral model's name. Default: `RAMx`. | +| `-decoder` | **Experimental.** `row`: a decoder per row, an AND2 chain over the row address, the column mux selecting within the row. `word`: a balanced AND2 tree over each word's whole address, as with banks; shallower for the read path. Default: `row`. | +| `-bit_folds` | **Experimental.** A word in `count` rows: its bits in slices of `-mask_size` (the word divided by `count`, rounded up; the last slice may be short), one slice a row, so a wide word with many ports is not a long thin strip. A folded word is written whole, one write enable a port, its write logic built once and shared by its rows. Not with `-use_latch` or the row decoder's `-column_mux_ratio` above 1. Default: 1. | | `-inv_cell` | Name of the master to use for inverters. Default: auto-select from the loaded cell library. | | `-routing_layer` | A list of the metal layer and metal width (in microns) for generating standard cell power tracks (followpins). Example: `{met1 0.48}`. | | `-power_net_name` | Name of the power net to create. Default: `VDD`. | @@ -79,6 +115,7 @@ generate_ram [-mask_size bits] | `-max_tap_dist` | The distance (in microns) that tapcells should be placed apart. Requires `-tapcell`. | | `-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. | +| `-tracks` | **Experimental.** `generate_ram` only. A Tcl file of `make_tracks` commands, sourced in place of `make_tracks` with the technology LEF's offsets, for a technology whose offsets do not fit the block (asap7's M2 offset is negative). Default: the LEF's tracks. | ## Example scripts @@ -96,7 +133,7 @@ bazelisk test //src/ram/... ## Limitations -This is an experimental module and many basic features may not work yet. Please see [#9392](https://github.com/The-OpenROAD-Project/OpenROAD/issues/9392) for ongoing and future work. +This is an experimental module and many basic features may not work yet. The register-file options are experimental on their own account, see [RAM Generation](#ram-generation). Please see [#9392](https://github.com/The-OpenROAD-Project/OpenROAD/issues/9392) for ongoing and future work. ## Authors diff --git a/src/ram/include/ram/layout.h b/src/ram/include/ram/layout.h index bbaf4b7abdb..b8a5cf072e1 100644 --- a/src/ram/include/ram/layout.h +++ b/src/ram/include/ram/layout.h @@ -65,6 +65,9 @@ class Layout int getWidth() const; + // a floor on the width, for a column kept empty + void setMinWidth(int width); + private: odb::Orientation2D orientation_; odb::Point origin_; diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 1a41ba0d7ba..5c4fbeb2202 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -4,11 +4,13 @@ #pragma once #include +#include #include #include #include #include #include +#include #include #include @@ -107,7 +109,27 @@ class RamGen odb::dbMaster* tristate_cell, odb::dbMaster* inv_cell, odb::dbMaster* tapcell, - int max_tap_dist); + int max_tap_dist, + const std::string& read_style = "", + int zero_word = -1, + const std::string& reset_value = "", + int banks = 1, + int bank_columns = 1, + const std::string& bank_order = "contiguous", + int rb_ports = 0, + int register_read_address = 0, + const std::string& write_style = "clock_gate", + const std::string& write_priority = "last", + const std::string& periphery = "placed", + const std::string& pin_layer = "", + const std::string& cells = "", + const std::string& name = "", + const std::string& decoder = "row", + int bit_folds = 1, + const std::string& wrapper_verilog = "", + int service_every = 0, + int service_sites = 0, + const std::string& bank_column_layout = "interleaved"); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -134,9 +156,71 @@ class RamGen private: void findMasters(); - std::map buildPortMap(odb::dbMaster*); + // `inverted`, if given, is set when a flop's only data output is its + // inverted one (QN), which then stands as DataOut. + std::map buildPortMap(odb::dbMaster*, + bool* inverted = nullptr); + // The AND-OR read path: each read port's bitline a tree over the rows + // of a bit column, planned before the rows are built (planReadTree) and + // placed row by row with the bits (treeNodes_). + struct TreeNode + { + odb::dbMaster* master; + std::string prefix; + std::string name; + std::vector> conns; + }; + struct TreeEnd + { + odb::dbNet* net; + bool inverted; // the net is the complement of the OR of the terms + }; + // one term of a read tree: the word at (row, word in the row) and its + // read select + struct Term + { + int row; + int word_idx; + odb::dbNet* select; + }; + // the tree over terms[lo, hi), each node in the bit cell of a term it + // covers + TreeEnd planReadTree(const std::string& prefix, + int bit, + const std::vector& terms, + int lo, + int hi, + odb::dbNet* out); + // the word at (row, word in the row) + int wordAt(int row, int word_idx) const; + bool treeInverted(int lo, int hi) const; + // -reset_value: the storage flop's data, clear and preset pins, the + // reset's polarity, what a cleared flop reads, and the tie cell that + // holds the unused pin off + void findResetPins(); + // The smallest cell with an output at the constant `one` (or 0), or the + // cell `given`, checked; `pin` gets that output. A cell may drive both + // levels on two pins, as sky130's conb_1 does. + odb::dbMaster* findTie(bool one, odb::dbMaster* given, std::string& pin); + // The smallest cell `match` accepts; none is an error unless not + // `required`, then nullptr. A cell `given` is checked by `match` + // instead, on any of its outputs. Cells with more than one output are + // passed over, except, with `q_and_qn`, a flop whose outputs are its + // Q and QN. odb::dbMaster* findMaster(const std::function& match, - const char* name); + const char* name, + bool required = true, + odb::dbMaster* given = nullptr, + bool q_and_qn = false); + // The smallest four-input cell whose function is `table` (bit k the + // output for inputs k in binary, in the order of `inputs`): `inputs` + // gets its input pins in that order, `output` its output pin. + odb::dbMaster* findMaster4(uint16_t table, + const char* name, + std::array& inputs, + std::string& output, + bool required = true, + odb::dbMaster* given = nullptr); odb::dbNet* makeNet(const std::string& prefix, const std::string& name); odb::dbInst* makeInst( Cell* cell, @@ -149,7 +233,10 @@ class RamGen odb::dbNet* clock, std::vector& select, odb::dbNet* data_input, - std::vector& data_output); + std::vector& data_output, + odb::dbNet* storage_net = nullptr, + int reset_bit = -1, + odb::dbNet* reset_off = nullptr); void makeSlice(int slice_idx, int mask_size, int row_idx, @@ -159,12 +246,12 @@ class RamGen int word_idx, int column_mux_ratio, odb::dbNet* clock, - odb::dbNet* write_enable, + const std::vector& write_enable, odb::dbNet* word_select, const std::vector& selects, const std::vector& shared_select_b_nets, bool create_select_inv, - const std::vector& data_input, + const std::vector>& data_input, const std::vector>& data_output); void makeWord(int slices_per_word, @@ -177,11 +264,11 @@ class RamGen int column_mux_ratio, odb::dbNet* clock, odb::dbNet* word_select, - std::vector& write_enable, + std::vector>& write_enable, const std::vector& selects, const std::vector& shared_select_b_nets, bool create_select_inv, - const std::vector& data_input, + const std::vector>& data_input, const std::vector>& data_output); odb::dbBTerm* makeBTerm(const std::string& name, odb::dbIoType io_type); @@ -195,7 +282,16 @@ class RamGen void makeBufferColumn(const std::string& prefix, int num_rows, const std::vector& decoder_output_nets, - const std::vector& select_nets); + const std::vector& select_nets, + int words_per_row = 1); + // banks: a decoder per word, an AND2 tree over all its address + // literals, the words of a row in that row's cell + void makeWordDecoderColumn( + const std::string& prefix, + int num_rows, + int words_per_row, + const std::vector>& literals, + const std::vector& decoder_output_nets); std::unique_ptr makeInverterColumn( int num_rows, @@ -229,6 +325,14 @@ class RamGen odb::dbMaster* clock_gate_cell_{nullptr}; odb::dbMaster* buffer_cell_{nullptr}; odb::dbMaster* aoi22_cell_{nullptr}; + // with more than one write port: the write data mux (an AO22, or where + // the library has none the AOI22 and an inverter) and the write + // enables' OR + odb::dbMaster* ao22_cell_{nullptr}; + odb::dbMaster* or2_cell_{nullptr}; + // the AND-OR read path's tree + odb::dbMaster* nand2_cell_{nullptr}; + odb::dbMaster* nor2_cell_{nullptr}; odb::dbMaster* latch_cell_{nullptr}; odb::dbMaster* tapcell_{nullptr}; @@ -242,15 +346,154 @@ class RamGen std::map clock_gate_ports_; std::map buffer_ports_; std::map latch_ports_; + std::map or2_ports_; + std::map nand2_ports_; + std::map nor2_ports_; std::vector> addr_inputs_; - std::vector data_inputs_; + std::vector data_inputs_; // every write port's data std::vector> q_outputs_; std::string behavioral_verilog_filename_; std::string aoi22_in_a1_, aoi22_in_a2_, aoi22_in_b1_, aoi22_in_b2_, aoi22_out_; + std::array ao22_in_; + std::string ao22_out_; Grid ram_grid_; bool use_latch_{false}; + int num_write_{1}; // write and read/write ports + // Reads through AND-OR trees instead of tristate bitlines: asked for, + // or the library has no tristate cell. + bool and_or_read_{false}; + std::string read_style_; + bool storage_inverted_{false}; // the storage flop's output is QN + int zero_word_{-1}; // the word that reads 0 and is not stored, or -1 + // Banks: the words in `banks_` banks (contiguous or interleaved + // addresses), bank_columns_ of them side by side, the rest stacked. + // Each port decodes every word's whole address, so any placement of + // words reads and writes alike; a row holds bank_columns_ words, its + // bits interleaved as the column mux's. + bool banked_{false}; + int banks_{1}; + int bank_columns_{1}; + bool bank_interleaved_{false}; + int words_per_bank_{0}; + int words_per_row_{1}; + // Banked read ports: each an address and a data output per bank, read + // independently (rb_ports_ of them, after the plain read ports) + int rb_ports_{0}; + int r_plain_{0}; // the plain read ports, read/write ones included + // the bank of the word at (row, word in the row), and its index there + int bankAt(int row, int word_idx) const; + int indexInBank(int row) const; + // -register_read_address 1: each read port's address through a flop (a plain + // positive-edge one) in the address inverter's cell. The decoders read + // addr_true_ (the address, or the flop's output) and its complement; + // where the flop's output is inverted the complement is the flop's and + // the inverter makes the true one. + int register_read_address_{0}; + // -write_style mux: no clock gate; each bit's flop on the clock, its D + // the new data where the word is written and its own value elsewhere + bool write_mux_{false}; + // -write_priority or: where ports write the same word in a cycle, the + // word takes the OR of their data, not the last port's + bool write_or_{false}; + // -periphery unplaced: the periphery (address inverters and registers, + // decoders, select buffers, each word's select cell, the input + // buffers) left for a parent to place; the outline the placed core and + // a row; each pin where its connections land, on pin_layer_'s tracks + bool periphery_unplaced_{false}; + std::string ram_name_; // the block's and the behavioral model's name + // -bit_folds k: a word's mask slices in k rows instead of side by side, + // so a wide word with many ports is as tall as it is wide; every + // column is k rows a word + int bit_folds_{1}; + int word_bits_{0}; // the word's bits, short of the folds' pads + int slice_width_{0}; // a slice's columns: its bits, and its select + // with -bit_folds a word is written whole: its write logic (enables, + // priority, hold, clock gate, reset tie, select inverters) built in its + // first fold and shared by the others, keyed (row, word in the row) + struct WordWrite + { + odb::dbNet* we{nullptr}; + odb::dbNet* hold{nullptr}; + odb::dbNet* gclock{nullptr}; + odb::dbNet* reset_off{nullptr}; + std::vector take; + std::vector select_b; + }; + std::map, WordWrite> word_write_; + int sliceOffset(int slice) const + { + return bit_folds_ > 1 ? 0 : slice * slice_width_; + } + // -bank_column_layout: a row's words interleaved bit by bit, as the + // column mux lays them out (bit b of every word, then bit b+1), or + // contiguous, each word's bits side by side and its select column + // after them, so a word's selects and write enables span its own bits + // and not the row + bool words_contiguous_{false}; + int slices_per_row_{1}; // a word's slices in a row: 1 with -bit_folds + // contiguous: a word's slices side by side, each its bits and then its + // select column, the row's next word after them + int contiguousColumn(int slice, int word, int mask_size) const + { + return (word * slices_per_row_ + (bit_folds_ > 1 ? 0 : slice)) + * (mask_size + 1); + } + int bitColumn(int slice, int bit, int word, int mask_size) const + { + return words_contiguous_ ? contiguousColumn(slice, word, mask_size) + bit + : sliceOffset(slice) + bit * words_per_row_ + word; + } + int selColumn(int slice, int word, int mask_size) const + { + return words_contiguous_ + ? contiguousColumn(slice, word, mask_size) + mask_size + : sliceOffset(slice) + mask_size * words_per_row_ + word; + } + std::string pin_layer_; + bool marking_periphery_{false}; + Cell* periphery_cell_{nullptr}; + std::vector periphery_insts_; + void unplacePeriphery(); + // -periphery verilog: the periphery leaves the block, which becomes the + // core alone (_core), for a wrapper module in Verilog written to + // wrapper_verilog_: the periphery's logic cells as assigns of their + // liberty functions, its other cells (clock gates, flops, ties) as + // instances, around an instance of the core. A net the core shares + // with the periphery becomes a core port. + bool periphery_verilog_{false}; + std::string wrapper_verilog_; + void splitPeriphery(); + odb::dbMaster* addr_flop_cell_{nullptr}; + std::string addr_flop_d_, addr_flop_clk_, addr_flop_q_; + bool addr_flop_inverted_{false}; + odb::dbNet* clock_net_{nullptr}; + std::vector> addr_true_; + std::vector> addr_flop_out_; // or nullptr + // An asynchronous reset to reset_value_ (a word's value, every word): + // each bit's flop is cleared or preset by the reset port, the other + // pin held off. The pins come from the flop's ff group; clear_reads_ + // is the data a cleared flop reads back, after any inversion. + std::string reset_value_; + std::vector reset_bits_; // a word, least significant bit first + odb::dbBTerm* reset_port_{nullptr}; + odb::dbMaster* tie_off_cell_{nullptr}; // drives the inactive level + std::string tie_off_pin_; + // a clock gate's inputs besides its clock and enable (a test enable), + // each tied low by tie_low_cell_ + std::vector clock_gate_test_pins_; + odb::dbMaster* tie_low_cell_{nullptr}; + std::string tie_low_pin_; + std::string storage_data_pin_; + std::string storage_clear_pin_; + std::string storage_preset_pin_; + bool reset_active_low_{true}; + int clear_reads_{0}; + // per (row, word in the row, bit): the storage net, and the tree + // cells placed in that bit's cell + std::map, odb::dbNet*> storage_nets_; + std::map, std::vector> tree_nodes_; }; } // namespace ram diff --git a/src/ram/src/layout.cpp b/src/ram/src/layout.cpp index 4a75649c5ad..41b3ea57bfa 100644 --- a/src/ram/src/layout.cpp +++ b/src/ram/src/layout.cpp @@ -148,6 +148,11 @@ int Layout::getWidth() const return cell_width_; } +void Layout::setMinWidth(const int width) +{ + cell_width_ = std::max(cell_width_, width); +} + ////////////////////////////////////////////////////////////// Grid::Grid(odb::Orientation2D orientation) : orientation_(orientation) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 2d2c5378007..ef590bc6f7f 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -5,13 +5,18 @@ #include #include +#include #include #include #include #include +#include #include #include #include +#include +#include +#include #include #include #include @@ -80,6 +85,9 @@ dbInst* RamGen::makeInst( { const auto inst_name = fmt::format("{}.{}", prefix, name); auto inst = dbInst::create(block_, master, inst_name.c_str()); + if (marking_periphery_ || (cell && cell == periphery_cell_)) { + periphery_insts_.push_back(inst); + } for (auto& [mterm_name, net] : connections) { auto mterm = master->findMTerm(mterm_name.c_str()); if (!mterm) { @@ -114,20 +122,32 @@ std::unique_ptr RamGen::makeBit(const std::string& prefix, dbNet* clock, vector& select, dbNet* data_input, - vector& data_output) + vector& data_output, + dbNet* storage_net, + const int reset_bit, + dbNet* reset_off) { auto bit_cell = std::make_unique(); - auto storage_net = makeNet(prefix, "storage"); + // the AND-OR read path names the storage net ahead, for its trees + if (storage_net == nullptr) { + storage_net = makeNet(prefix, "storage"); + } if (!use_latch_) { - makeInst(bit_cell.get(), - prefix, - "bit", - storage_cell_, - {{storage_ports_[{PortRoleType::Clock, 0}], clock}, - {storage_ports_[{PortRoleType::DataIn, 0}], data_input}, - {storage_ports_[{PortRoleType::DataOut, 0}], storage_net}}); + vector> conns + = {{storage_ports_[{PortRoleType::Clock, 0}], clock}, + {storage_ports_[{PortRoleType::DataIn, 0}], data_input}, + {storage_ports_[{PortRoleType::DataOut, 0}], storage_net}}; + if (reset_bit >= 0) { + // the reset clears the flop where a cleared flop reads the bit's + // value, else presets it; the other pin held off + dbNet* reset = reset_port_->getNet(); + const bool clear = reset_bit == clear_reads_; + conns.emplace_back(storage_clear_pin_, clear ? reset : reset_off); + conns.emplace_back(storage_preset_pin_, clear ? reset_off : reset); + } + makeInst(bit_cell.get(), prefix, "bit", storage_cell_, conns); } else { makeInst(bit_cell.get(), prefix, @@ -139,6 +159,9 @@ std::unique_ptr RamGen::makeBit(const std::string& prefix, } for (int read_port = 0; read_port < read_ports; ++read_port) { + if (select[read_port] == nullptr) { + continue; // a banked read port's other banks + } makeInst( bit_cell.get(), prefix, @@ -153,6 +176,138 @@ std::unique_ptr RamGen::makeBit(const std::string& prefix, return bit_cell; } +// The AND-OR read path: one read port's bitline of one bit column, rows +// [lo, hi), as a tree: an AOI22 over each pair of rows (a NAND2 over a +// lone row), then NAND2 and NOR2 levels by turns, each node in the last +// row of its left half, beside its inputs. Planned before the rows are +// built; each row's bit cell takes its nodes from tree_nodes_. +bool RamGen::treeInverted(const int lo, const int hi) const +{ + const int n = hi - lo; + if (n <= 2) { + return true; // AOI22 or NAND2: the complement of the terms' OR + } + int half = 1; + while (half * 2 < n) { + half *= 2; + } + // the right half is brought to the left's polarity, and the node + // (NAND2 of complements, NOR2 of trues) flips it + return !treeInverted(lo, lo + half); +} + +int RamGen::bankAt(const int row, const int word_idx) const +{ + return (row / words_per_bank_) * bank_columns_ + word_idx; +} + +int RamGen::indexInBank(const int row) const +{ + return row % words_per_bank_; +} + +int RamGen::wordAt(const int row, const int word_idx) const +{ + if (!banked_) { + return row * words_per_row_ + word_idx; + } + const int bank = (row / words_per_bank_) * bank_columns_ + word_idx; + const int i = row % words_per_bank_; + return bank_interleaved_ ? i * banks_ + bank : bank * words_per_bank_ + i; +} + +RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, + const int bit, + const vector& terms, + const int lo, + const int hi, + dbNet* out) +{ + auto add = [&](const Term& at, + dbMaster* master, + const std::string& name, + const vector>& conns) { + tree_nodes_[{at.row, at.word_idx, bit}].push_back( + {master, prefix, name, conns}); + }; + // the zero word's term, where the trees read inverted data: its own + // select, the inverted form of a stored 0 (the zero word is in the + // tree only then) + auto q = [&](const Term& t) { + return wordAt(t.row, t.word_idx) == zero_word_ + ? t.select + : storage_nets_.at({t.row, t.word_idx, bit}); + }; + // a leaf's name: its row, and with banks its word in the row too + auto leaf = [&](const char* kind, const Term& t) { + return banked_ ? fmt::format("{}_r{}_w{}", kind, t.row, t.word_idx) + : fmt::format("{}_r{}", kind, t.row); + }; + auto net = [&](dbNet* given) { + return given ? given : makeNet(prefix, fmt::format("t{}_{}", lo, hi)); + }; + const int n = hi - lo; + if (n == 1) { + const Term& t = terms[lo]; + dbNet* y = net(out); + add(t, + nand2_cell_, + leaf("nand", t), + {{nand2_ports_[{PortRoleType::DataIn, 0}], t.select}, + {nand2_ports_[{PortRoleType::DataIn, 1}], q(t)}, + {nand2_ports_[{PortRoleType::DataOut, 0}], y}}); + return {y, true}; + } + if (n == 2) { + const Term& t0 = terms[lo]; + const Term& t1 = terms[lo + 1]; + dbNet* y = net(out); + add(t0, + aoi22_cell_, + leaf("aoi", t0), + {{aoi22_in_a1_, t0.select}, + {aoi22_in_a2_, q(t0)}, + {aoi22_in_b1_, t1.select}, + {aoi22_in_b2_, q(t1)}, + {aoi22_out_, y}}); + return {y, true}; + } + int half = 1; + while (half * 2 < n) { + half *= 2; + } + const int mid = lo + half; + const Term& host = terms[mid - 1]; + const TreeEnd a = planReadTree(prefix, bit, terms, lo, mid, nullptr); + TreeEnd b = planReadTree(prefix, bit, terms, mid, hi, nullptr); + if (b.inverted != a.inverted) { + dbNet* nb = makeNet(prefix, fmt::format("t{}_{}_inv", mid, hi)); + add(terms[mid], + inv_cell_, + fmt::format("inv_r{}_{}", mid, hi), + {{inv_ports_[{PortRoleType::DataIn, 0}], b.net}, + {inv_ports_[{PortRoleType::DataOut, 0}], nb}}); + b = {nb, a.inverted}; + } + dbNet* y = net(out); + if (a.inverted) { + add(host, + nand2_cell_, + fmt::format("nand_r{}_{}", lo, hi), + {{nand2_ports_[{PortRoleType::DataIn, 0}], a.net}, + {nand2_ports_[{PortRoleType::DataIn, 1}], b.net}, + {nand2_ports_[{PortRoleType::DataOut, 0}], y}}); + return {y, false}; + } + add(host, + nor2_cell_, + fmt::format("nor_r{}_{}", lo, hi), + {{nor2_ports_[{PortRoleType::DataIn, 0}], a.net}, + {nor2_ports_[{PortRoleType::DataIn, 1}], b.net}, + {nor2_ports_[{PortRoleType::DataOut, 0}], y}}); + return {y, true}; +} + // Note: For column_mux_ratio > 1, a single shared select_b net and select_inv // gate are reused across all words in the same physical row (passed via // shared_select_b_nets). This avoids creating redundant nets and gates per @@ -169,65 +324,341 @@ void RamGen::makeSlice(const int slice_idx, const int word_idx, const int column_mux_ratio, dbNet* clock, - dbNet* write_enable, + const vector& write_enable, dbNet* word_select, const vector& selects, const vector& shared_select_b_nets, const bool create_select_inv, - const vector& data_input, + const vector>& data_input, const vector>& data_output) { + // selects: the write ports' row selects, then the read ports' + const int num_write = rw_ports + w_ports; const int start_bit_idx = slice_idx * mask_size; std::string prefix = fmt::format("storage_{}_{}_{}", row_idx, word_idx, start_bit_idx); + // The zero word: no flops, no clock gate; its row's cells hold only + // the read trees' nodes planned there. + if (wordAt(row_idx, word_idx) == zero_word_) { + for (int local_bit = 0; local_bit < mask_size; ++local_bit) { + const int bit = start_bit_idx + local_bit; + std::unique_ptr cell; + auto nodes = tree_nodes_.find({row_idx, word_idx, bit}); + if (nodes != tree_nodes_.end() && !nodes->second.empty()) { + cell = std::make_unique(); + for (const auto& node : nodes->second) { + makeInst(cell.get(), node.prefix, node.name, node.master, node.conns); + } + } + ram_grid_.addCell(std::move(cell), + bitColumn(slice_idx, local_bit, word_idx, mask_size)); + } + ram_grid_.addCell(nullptr, selColumn(slice_idx, word_idx, mask_size)); + return; + } + int r_total = rw_ports + r_ports; + // -bit_folds: the word's first fold builds its write logic, the others + // share it + const bool shared = bit_folds_ > 1 && slice_idx > 0; + WordWrite& word_write = word_write_[{row_idx, word_idx}]; + vector select_b_nets(r_total); - if (column_mux_ratio == 1) { + if (shared) { + select_b_nets = word_write.select_b; + } else if (and_or_read_) { + // the AND-OR trees read the true selects + } else if (column_mux_ratio == 1 || banked_) { for (int i = 0; i < r_total; ++i) { - select_b_nets[i] = makeNet(prefix, fmt::format("select{}_b", i)); + if (selects[num_write + i] != nullptr) { + select_b_nets[i] = makeNet(prefix, fmt::format("select{}_b", i)); + } } } else { select_b_nets = shared_select_b_nets; } - auto gclock_net = makeNet(prefix, "gclock"); - auto we_net = makeNet(prefix, "we"); + // the flops' clock: the word's gated clock, or with -write_style mux + // the clock itself + auto gclock_net = shared ? word_write.gclock + : write_mux_ ? clock + : makeNet(prefix, "gclock"); + auto we_net = shared ? word_write.we : makeNet(prefix, "we"); + auto sel_cell = std::make_unique(); + periphery_cell_ = sel_cell.get(); // each word's select cell + // -write_style mux: the word's hold, where it is not written + dbNet* hold = shared ? word_write.hold : nullptr; + if (write_mux_ && !shared) { + hold = makeNet(prefix, "hold"); + makeInst(sel_cell.get(), + prefix, + "hold_inv", + inv_cell_, + {{inv_ports_[{PortRoleType::DataIn, 0}], we_net}, + {inv_ports_[{PortRoleType::DataOut, 0}], hold}}); + } + // (sel0 & a) | (sel1 & b): an AO22, or the AOI22 and an inverter where + // the library has no AO22 + auto ao22 = [&](Cell* cell, + const std::string& name, + const std::string& inst, + dbNet* a1, + dbNet* a2, + dbNet* b1, + dbNet* b2, + dbNet* y) { + if (ao22_cell_) { + makeInst(cell, + name, + inst, + ao22_cell_, + {{ao22_in_[0], a1}, + {ao22_in_[1], a2}, + {ao22_in_[2], b1}, + {ao22_in_[3], b2}, + {ao22_out_, y}}); + return; + } + auto y_b = makeNet(name, inst + "_b"); + makeInst(cell, + name, + inst, + aoi22_cell_, + {{aoi22_in_a1_, a1}, + {aoi22_in_a2_, a2}, + {aoi22_in_b1_, b1}, + {aoi22_in_b2_, b2}, + {aoi22_out_, y_b}}); + makeInst(cell, + name, + inst + "_inv", + inv_cell_, + {{inv_ports_[{PortRoleType::DataIn, 0}], y_b}, + {inv_ports_[{PortRoleType::DataOut, 0}], y}}); + }; + + // -reset_value: one tie per word slice holds the flops' unused pins off + dbNet* reset_off = shared ? word_write.reset_off : nullptr; + if (reset_port_ && !shared) { + reset_off = makeNet(prefix, "reset_off"); + makeInst(sel_cell.get(), + prefix, + "reset_tie", + tie_off_cell_, + {{tie_off_pin_, reset_off}}); + } + + // More than one write port: port w writes this word when its row select + // and its write enable are both high (we{w}), and its data is taken + // unless a later port writes the word in the same cycle (take{w}): the + // last port wins, as in the behavioral model. With -write_priority or + // every writing port's data is taken (take{w} is we{w}) and the word + // gets their OR. The clock gate opens for any of them. + vector take(num_write); + if (shared) { + take = word_write.take; + } else if (num_write > 1) { + vector we(num_write); + for (int w = 0; w < num_write; ++w) { + we[w] = makeNet(prefix, fmt::format("we{}", w)); + makeInst(sel_cell.get(), + prefix, + fmt::format("gcand{}", w), + and2_cell_, + {{and2_ports_[{PortRoleType::DataIn, 0}], selects[w]}, + {and2_ports_[{PortRoleType::DataIn, 1}], write_enable[w]}, + {and2_ports_[{PortRoleType::DataOut, 0}], we[w]}}); + } + take[num_write - 1] = we[num_write - 1]; + dbNet* later = we[num_write - 1]; + for (int w = num_write - 2; w >= 0; --w) { + if (write_or_) { + take[w] = we[w]; + } else { + auto later_b = makeNet(prefix, fmt::format("later{}_b", w)); + makeInst(sel_cell.get(), + prefix, + fmt::format("later_inv{}", w), + inv_cell_, + {{inv_ports_[{PortRoleType::DataIn, 0}], later}, + {inv_ports_[{PortRoleType::DataOut, 0}], later_b}}); + take[w] = makeNet(prefix, fmt::format("take{}", w)); + makeInst(sel_cell.get(), + prefix, + fmt::format("take_and{}", w), + and2_cell_, + {{and2_ports_[{PortRoleType::DataIn, 0}], we[w]}, + {and2_ports_[{PortRoleType::DataIn, 1}], later_b}, + {and2_ports_[{PortRoleType::DataOut, 0}], take[w]}}); + } + // any port from w on writes: the next earlier port's later, and at + // port 0 the clock gate's enable + auto any = w == 0 ? we_net : makeNet(prefix, fmt::format("later{}", w)); + makeInst(sel_cell.get(), + prefix, + fmt::format("any_or{}", w), + or2_cell_, + {{or2_ports_[{PortRoleType::DataIn, 0}], later}, + {or2_ports_[{PortRoleType::DataIn, 1}], we[w]}, + {or2_ports_[{PortRoleType::DataOut, 0}], any}}); + later = any; + } + } + + if (!shared) { + word_write = {we_net, hold, gclock_net, reset_off, take, select_b_nets}; + } for (int local_bit = 0; local_bit < mask_size; ++local_bit) { + if (start_bit_idx + local_bit >= word_bits_) { + // a short fold's pad: its column empty in this row + ram_grid_.addCell(nullptr, + bitColumn(slice_idx, local_bit, word_idx, mask_size)); + continue; + } auto name = fmt::format("{}.bit{}", prefix, start_bit_idx + local_bit); vector outs(r_total); for (int r_port = 0; r_port < r_total; ++r_port) { outs[r_port] = data_output[r_port][local_bit]; } - int bit_col = slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) - + local_bit * column_mux_ratio + word_idx; - ram_grid_.addCell(makeBit(name, - r_total, - gclock_net, - select_b_nets, - data_input[local_bit], - outs), - bit_col); - } + // the written data, and the flop's D: the written data, or with + // -write_style mux the hold mux's output + dbNet* d_data = data_input[0][local_bit]; + if (num_write > 1) { + d_data = makeNet(name, "d"); + } + dbNet* d = write_mux_ ? makeNet(name, "dhold") : d_data; + const int bit = start_bit_idx + local_bit; + dbNet* storage + = and_or_read_ ? storage_nets_.at({row_idx, word_idx, bit}) : nullptr; + if (write_mux_ && storage == nullptr) { + storage = makeNet(name, "storage"); + } + const int reset_bit + = reset_port_ ? static_cast(reset_bits_.at(bit)) : -1; + auto bit_cell = makeBit(name, + and_or_read_ ? 0 : r_total, + gclock_net, + select_b_nets, + d, + outs, + storage, + reset_bit, + reset_off); + if (write_mux_) { + // the word's own value back where it is not written: the flop's + // output, through an inverter where that output is inverted + dbNet* old = storage; + if (storage_inverted_) { + old = makeNet(name, "q"); + makeInst(bit_cell.get(), + name, + "qinv", + inv_cell_, + {{inv_ports_[{PortRoleType::DataIn, 0}], storage}, + {inv_ports_[{PortRoleType::DataOut, 0}], old}}); + } + ao22(bit_cell.get(), name, "hold_mux", we_net, d_data, hold, old, d); + } + if (and_or_read_) { + for (const auto& node : tree_nodes_[{row_idx, word_idx, bit}]) { + makeInst( + bit_cell.get(), node.prefix, node.name, node.master, node.conns); + } + } + if (num_write > 1) { + // the taken port's data: AO22 over pairs of ports, OR2 across pairs + dbNet* acc = nullptr; + for (int w = 0; w < num_write; w += 2) { + const bool last = w + 2 >= num_write; + dbNet* out = last && acc == nullptr + ? d_data + : makeNet(name, fmt::format("d_pair{}", w / 2)); + if (w + 1 < num_write && !ao22_cell_) { + // no AO22 in the library: the AOI22 and an inverter + auto out_b = makeNet(name, fmt::format("d_pair{}_b", w / 2)); + makeInst(bit_cell.get(), + name, + fmt::format("dmux{}", w / 2), + aoi22_cell_, + {{aoi22_in_a1_, take[w]}, + {aoi22_in_a2_, data_input[w][local_bit]}, + {aoi22_in_b1_, take[w + 1]}, + {aoi22_in_b2_, data_input[w + 1][local_bit]}, + {aoi22_out_, out_b}}); + makeInst(bit_cell.get(), + name, + fmt::format("dmux{}_inv", w / 2), + inv_cell_, + {{inv_ports_[{PortRoleType::DataIn, 0}], out_b}, + {inv_ports_[{PortRoleType::DataOut, 0}], out}}); + } else if (w + 1 < num_write) { + makeInst(bit_cell.get(), + name, + fmt::format("dmux{}", w / 2), + ao22_cell_, + {{ao22_in_[0], take[w]}, + {ao22_in_[1], data_input[w][local_bit]}, + {ao22_in_[2], take[w + 1]}, + {ao22_in_[3], data_input[w + 1][local_bit]}, + {ao22_out_, out}}); + } else { + makeInst(bit_cell.get(), + name, + fmt::format("dmux{}", w / 2), + and2_cell_, + {{and2_ports_[{PortRoleType::DataIn, 0}], take[w]}, + {and2_ports_[{PortRoleType::DataIn, 1}], + data_input[w][local_bit]}, + {and2_ports_[{PortRoleType::DataOut, 0}], out}}); + } + if (acc != nullptr) { + dbNet* sum + = last ? d_data : makeNet(name, fmt::format("d_or{}", w / 2)); + makeInst(bit_cell.get(), + name, + fmt::format("dor{}", w / 2), + or2_cell_, + {{or2_ports_[{PortRoleType::DataIn, 0}], acc}, + {or2_ports_[{PortRoleType::DataIn, 1}], out}, + {or2_ports_[{PortRoleType::DataOut, 0}], sum}}); + out = sum; + } + acc = out; + } + } - auto sel_cell = std::make_unique(); + int bit_col = bitColumn(slice_idx, local_bit, word_idx, mask_size); + ram_grid_.addCell(std::move(bit_cell), bit_col); + } - // Make clock gate - makeInst(sel_cell.get(), - prefix, - "cg", - clock_gate_cell_, - {{clock_gate_ports_[{PortRoleType::Clock, 0}], clock}, - {clock_gate_ports_[{PortRoleType::DataIn, 0}], we_net}, - {clock_gate_ports_[{PortRoleType::DataOut, 0}], gclock_net}}); + // Make clock gate (with -write_style mux, none: the flops hold by mux) + if (!write_mux_ && !shared) { + std::vector> connections + = {{clock_gate_ports_[{PortRoleType::Clock, 0}], clock}, + {clock_gate_ports_[{PortRoleType::DataIn, 0}], we_net}, + {clock_gate_ports_[{PortRoleType::DataOut, 0}], gclock_net}}; + if (!clock_gate_test_pins_.empty()) { + dbNet* test_off = makeNet(prefix, "cg_test_off"); + makeInst(sel_cell.get(), + prefix, + "cg_tie", + tie_low_cell_, + {{tie_low_pin_, test_off}}); + for (const std::string& pin : clock_gate_test_pins_) { + connections.emplace_back(pin, test_off); + } + } + makeInst(sel_cell.get(), prefix, "cg", clock_gate_cell_, connections); + } // Write path: this net is row_select AND with word_select so clock gate only // fires for the addressed word within the row. Read path handled by AOI mux. // word_select is nullptr when column_mux_ratio=1 (no mux needed). - dbNet* write_sel = selects[0]; - if (word_select) { + dbNet* write_sel = num_write > 1 ? nullptr : selects[0]; + if (word_select && num_write == 1) { write_sel = makeNet(prefix, "write_sel"); makeInst(sel_cell.get(), prefix, @@ -241,29 +672,34 @@ void RamGen::makeSlice(const int slice_idx, // Make clock and // this AND gate needs to be fed a net created by a decoder // adding any net will automatically connect with any port - makeInst(sel_cell.get(), - prefix, - "gcand", - and2_cell_, - {{and2_ports_[{PortRoleType::DataIn, 0}], write_sel}, - {and2_ports_[{PortRoleType::DataIn, 1}], write_enable}, - {and2_ports_[{PortRoleType::DataOut, 0}], we_net}}); + if (num_write == 1 && !shared) { + makeInst(sel_cell.get(), + prefix, + "gcand", + and2_cell_, + {{and2_ports_[{PortRoleType::DataIn, 0}], write_sel}, + {and2_ports_[{PortRoleType::DataIn, 1}], write_enable[0]}, + {and2_ports_[{PortRoleType::DataOut, 0}], we_net}}); + } // Make select inverters - if (create_select_inv) { + if (create_select_inv && !and_or_read_ && !shared) { for (int i = 0; i < r_total; ++i) { + if (selects[num_write + i] == nullptr) { + continue; // a banked read port's other banks + } makeInst(sel_cell.get(), prefix, fmt::format("select_inv_{}", i), inv_cell_, - {{inv_ports_[{PortRoleType::DataIn, 0}], selects[i + 1]}, + {{inv_ports_[{PortRoleType::DataIn, 0}], selects[num_write + i]}, {inv_ports_[{PortRoleType::DataOut, 0}], select_b_nets[i]}}); } } - int sel_col = slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) - + mask_size * column_mux_ratio + word_idx; - ram_grid_.addCell(std::move(sel_cell), sel_col); + int sel_col = selColumn(slice_idx, word_idx, mask_size); + // a fold sharing its word's logic has an empty select cell + ram_grid_.addCell(shared ? nullptr : std::move(sel_cell), sel_col); } // Note: For column_mux_ratio > 1, creates one shared select_b net per row and @@ -283,18 +719,25 @@ void RamGen::makeWord(const int slices_per_word, const int column_mux_ratio, dbNet* clock, dbNet* word_select, - vector& write_enable, + vector>& write_enable, const vector& selects, const vector& shared_select_b_nets, const bool create_select_inv, - const vector& data_input, + const vector>& data_input, const vector>& data_output) { int r_total = rw_ports + r_ports; for (int slice = 0; slice < slices_per_word; ++slice) { int start_idx = slice * mask_size; - vector slice_inputs(data_input.begin() + start_idx, - data_input.begin() + start_idx + mask_size); + // per write port: its data bits and its write enable for this slice + vector> slice_inputs; + vector slice_enables; + for (size_t w = 0; w < data_input.size(); ++w) { + slice_inputs.emplace_back(data_input[w].begin() + start_idx, + data_input[w].begin() + start_idx + mask_size); + slice_enables.push_back( + write_enable[w][bit_folds_ > 1 ? 0 : slice]->getNet()); + } std::vector> slice_outputs; slice_outputs.reserve(r_total); for (int port = 0; port < r_total; ++port) { @@ -311,7 +754,7 @@ void RamGen::makeWord(const int slices_per_word, word_idx, column_mux_ratio, clock, - write_enable[slice]->getNet(), + slice_enables, word_select, selects, shared_select_b_nets, @@ -344,6 +787,7 @@ void RamGen::makeDecoderColumn( const std::vector& decoder_output_nets) { auto decoder_layout = std::make_unique(odb::vertical); + marking_periphery_ = true; const int layers = std::ceil(std::log2(num_rows)) - 1; @@ -378,30 +822,108 @@ void RamGen::makeDecoderColumn( } } decoder_layout->addCell(std::move(word_cell)); + for (int f = 1; f < bit_folds_; ++f) { + decoder_layout->addCell(nullptr); // the word's other folds + } } + marking_periphery_ = false; ram_grid_.addLayout(std::move(decoder_layout)); } void RamGen::makeBufferColumn(const std::string& prefix, const int num_rows, const std::vector& decoder_output_nets, - const std::vector& select_nets) + const std::vector& select_nets, + const int words_per_row) { auto buffer_layout = std::make_unique(odb::vertical); - for (int word = 0; word < num_rows; ++word) { + marking_periphery_ = true; + for (int row = 0; row < num_rows; ++row) { auto buf_cell = std::make_unique(); - makeInst( - buf_cell.get(), - prefix, - fmt::format("sel_buf_{}", word), - buffer_cell_, - {{buffer_ports_[{PortRoleType::DataIn, 0}], decoder_output_nets[word]}, - {buffer_ports_[{PortRoleType::DataOut, 0}], select_nets[word]}}); + // a buffer per select: one per row, or with banks one per word + for (int k = 0; k < words_per_row; ++k) { + const int word = row * words_per_row + k; + if (decoder_output_nets[word] == nullptr) { + continue; // a banked read port's other banks + } + makeInst( + buf_cell.get(), + prefix, + fmt::format("sel_buf_{}", word), + buffer_cell_, + {{buffer_ports_[{PortRoleType::DataIn, 0}], + decoder_output_nets[word]}, + {buffer_ports_[{PortRoleType::DataOut, 0}], select_nets[word]}}); + } buffer_layout->addCell(std::move(buf_cell)); + for (int f = 1; f < bit_folds_; ++f) { + buffer_layout->addCell(nullptr); // the word's other folds + } } + marking_periphery_ = false; ram_grid_.addLayout(std::move(buffer_layout)); } +void RamGen::makeWordDecoderColumn( + const std::string& prefix, + const int num_rows, + const int words_per_row, + const std::vector>& literals, + const std::vector& decoder_output_nets) +{ + auto decoder_layout = std::make_unique(odb::vertical); + marking_periphery_ = true; + for (int row = 0; row < num_rows; ++row) { + auto row_cell = std::make_unique(); + for (int k = 0; k < words_per_row; ++k) { + const int word = row * words_per_row + k; + if (literals[word].empty()) { + continue; // a banked read port's other banks + } + // a balanced AND2 tree over the word's address literals + vector level = literals[word]; + int node = 0; + while (level.size() > 1) { + vector next; + for (size_t i = 0; i + 1 < level.size(); i += 2) { + dbNet* y + = level.size() == 2 + ? decoder_output_nets[word] + : makeNet(prefix, fmt::format("y{}_word{}", node, word)); + makeInst(row_cell.get(), + prefix, + fmt::format("and{}_word{}", node++, word), + and2_cell_, + {{and2_ports_[{PortRoleType::DataIn, 0}], level[i]}, + {and2_ports_[{PortRoleType::DataIn, 1}], level[i + 1]}, + {and2_ports_[{PortRoleType::DataOut, 0}], y}}); + next.push_back(y); + } + if (level.size() % 2 == 1) { + next.push_back(level.back()); + } + level = std::move(next); + } + if (literals[word].size() == 1) { + // a single literal: buffered to the decoder's output + makeInst(row_cell.get(), + prefix, + fmt::format("buf_word{}", word), + buffer_cell_, + {{buffer_ports_[{PortRoleType::DataIn, 0}], level[0]}, + {buffer_ports_[{PortRoleType::DataOut, 0}], + decoder_output_nets[word]}}); + } + } + decoder_layout->addCell(std::move(row_cell)); + for (int f = 1; f < bit_folds_; ++f) { + decoder_layout->addCell(nullptr); // the word's other folds + } + } + marking_periphery_ = false; + ram_grid_.addLayout(std::move(decoder_layout)); +} + std::unique_ptr RamGen::makeInverterColumn( const int num_rows, const int num_row_bits, @@ -412,6 +934,7 @@ std::unique_ptr RamGen::makeInverterColumn( { auto cell_inv_layout = std::make_unique(odb::vertical); int ports_in_col = end_port - start_port; + marking_periphery_ = true; // number of inverters per bit group (one per port in this column) // fillers evenly space each bit group within the column @@ -421,15 +944,36 @@ std::unique_ptr RamGen::makeInverterColumn( for (int bit = num_row_bits - 1; bit >= 0; --bit) { for (int p = start_port; p < end_port; ++p) { + // a banked read port's address is narrower than the others + if (num_word_bits + bit >= static_cast(addr_inputs_[p].size())) { + cell_inv_layout->addCell(nullptr); + continue; + } auto inv_grid_cell = std::make_unique(); + const int a = num_word_bits + bit; + dbNet* flop_out = addr_flop_out_[p][a]; + if (flop_out) { + // -register_read_address 1: the address through a flop, beside its + // inverter + makeInst(inv_grid_cell.get(), + fmt::format("decoder_p{}", p), + fmt::format("addr_reg_p{}_a{}", p, bit), + addr_flop_cell_, + {{addr_flop_d_, addr_inputs_[p][a]->getNet()}, + {addr_flop_clk_, clock_net_}, + {addr_flop_q_, flop_out}}); + } + // the inverter: the address (or the flop's) in, its complement out; + // from a flop holding the complement, the address out + const bool from_complement = flop_out && addr_flop_inverted_; makeInst(inv_grid_cell.get(), fmt::format("decoder_p{}", p), fmt::format("inv_p{}_a{}", p, bit), inv_cell_, {{inv_ports_[{PortRoleType::DataIn, 0}], - addr_inputs_[p][num_word_bits + bit]->getNet()}, + from_complement ? inv_addr_nets[p][a] : addr_true_[p][a]}, {inv_ports_[{PortRoleType::DataOut, 0}], - inv_addr_nets[p][num_word_bits + bit]}}); + from_complement ? addr_true_[p][a] : inv_addr_nets[p][a]}}); cell_inv_layout->addCell(std::move(inv_grid_cell)); } // add fillers between bit groups @@ -438,6 +982,7 @@ std::unique_ptr RamGen::makeInverterColumn( } } + marking_periphery_ = false; // pad any remaining rows int total_placed = (ports_in_col + fillers_per_group) * num_row_bits; for (int f = total_placed; f < num_rows; ++f) { @@ -478,8 +1023,28 @@ std::vector RamGen::makeSelectNets(const std::string& prefix, dbMaster* RamGen::findMaster( const std::function& match, - const char* name) + const char* name, + const bool required, + dbMaster* given, + const bool q_and_qn) { + if (given) { + auto liberty = network_->libertyCell(network_->dbToSta(given)); + bool matched = false; + if (liberty && match) { + std::unique_ptr ports( + liberty->portIterator()); + while (ports->hasNext() && !matched) { + auto port = ports->next()->libertyPort(); + matched = port && port->direction()->isAnyOutput() && match(port); + } + } + if (!matched) { + logger_->error( + RAM, 79, "Cell {} is not a {} cell.", given->getName(), name); + } + return given; + } dbMaster* best = nullptr; float best_area = std::numeric_limits::max(); @@ -493,6 +1058,11 @@ dbMaster* RamGen::findMaster( if (!liberty) { continue; } + // not a power domain's isolation cell or level shifter, whose + // functions look like logic + if (liberty->isIsolationCell() || liberty->isLevelShifter()) { + continue; + } if (!match) { if (liberty->portCount() == 0) { @@ -506,22 +1076,30 @@ dbMaster* RamGen::findMaster( liberty->portIterator()); sta::ConcretePort* out = nullptr; + int outputs = 0; + bool flop_outputs = true; // every output a flop's Q or QN while (port_iter->hasNext()) { auto lib_port = port_iter->next(); auto dir = lib_port->direction(); if (dir->isAnyOutput()) { - if (!out) { + ++outputs; + flop_outputs &= lib_port->libertyPort()->isRegOutput(); + if (!out || (q_and_qn && !match(out->libertyPort()))) { out = lib_port; - } else { - out = nullptr; // no multi-output gates - break; } } } + if (outputs > 1 && !(q_and_qn && outputs == 2 && flop_outputs)) { + continue; // no multi-output gates + } if (!out || !match(out->libertyPort())) { continue; } + // not a cell the library keeps from use (a tie: findTie) + if (liberty->dontUse()) { + continue; + } if (liberty->area() < best_area) { best_area = liberty->area(); @@ -531,14 +1109,22 @@ dbMaster* RamGen::findMaster( } if (!best) { + if (!required) { + return nullptr; + } logger_->error(RAM, 10, "Can't find {} cell", name); } logger_->info(RAM, 16, "Selected {} cell {}", name, best->getName()); return best; } -std::map RamGen::buildPortMap(dbMaster* master) +std::map RamGen::buildPortMap(dbMaster* master, + bool* inverted) { + if (inverted) { + *inverted = false; + } + std::string inverted_out; // a flop's inverted output (QN) auto sta_cell = network_->dbToSta(master); auto liberty = network_->libertyCell(sta_cell); std::map pin_map; @@ -586,8 +1172,13 @@ std::map RamGen::buildPortMap(dbMaster* master) auto seq = lib_cell->outputPortSequential(internal_port); if (seq) { is_seq_output = true; - // outputInv() is the negative state variable (IQN) - is_seq_inverted = (seq->outputInv() == internal_port); + // outputInv() is the negative state variable (IQN); a state + // that stores the data inverted (next_state "!D", asap7's + // DFFHQNx1) inverts it again + const bool stores_inverted + = seq->data() && seq->data()->op() == sta::FuncExpr::Op::not_; + is_seq_inverted + = (seq->outputInv() == internal_port) != stores_inverted; } } } @@ -596,6 +1187,8 @@ std::map RamGen::buildPortMap(dbMaster* master) // only assign if this is the non-inverted output (Q and not Qn) if (!is_seq_inverted) { pin_map[{PortRoleType::DataOut, 0}] = lib_port->name(); + } else { + inverted_out = lib_port->name(); } } else { // non-sequential cell (AND2, INV etc) assign as DataOut @@ -606,6 +1199,14 @@ std::map RamGen::buildPortMap(dbMaster* master) } } + // A flop whose only data output is QN: QN stands as DataOut, for a + // caller that says it can take the inversion. + if (inverted && !inverted_out.empty() + && pin_map.find({PortRoleType::DataOut, 0}) == pin_map.end()) { + pin_map[{PortRoleType::DataOut, 0}] = inverted_out; + *inverted = true; + } + // second pass for to assign tristate enable correct role // first pass can not classify without a dedicated tristate flag if (!tri_enable_name.empty()) { @@ -656,54 +1257,345 @@ std::map RamGen::buildPortMap(dbMaster* master) return pin_map; } -void RamGen::findMasters() +// Evaluates a liberty function expression given its input ports' values. +static bool evalFunc(const sta::FuncExpr* expr, + const std::vector& ports, + const std::vector& vals) { - if (!inv_cell_) { - inv_cell_ = findMaster( - [](sta::LibertyPort* port) { - return port->libertyCell()->isInverter(); - }, - "inverter"); + if (!expr) { + return false; } - inv_ports_ = buildPortMap(inv_cell_); + switch (expr->op()) { + case sta::FuncExpr::Op::port: { + // look up this port's value in the input vector + auto p = expr->port(); + for (int i = 0; i < (int) ports.size(); ++i) { + if (ports[i] == p) { + return vals[i]; + } + } + return false; + } + case sta::FuncExpr::Op::not_: + return !evalFunc(expr->left(), ports, vals); + case sta::FuncExpr::Op::and_: + return evalFunc(expr->left(), ports, vals) + && evalFunc(expr->right(), ports, vals); + case sta::FuncExpr::Op::or_: + return evalFunc(expr->left(), ports, vals) + || evalFunc(expr->right(), ports, vals); + case sta::FuncExpr::Op::xor_: + return evalFunc(expr->left(), ports, vals) + != evalFunc(expr->right(), ports, vals); + case sta::FuncExpr::Op::one: + return true; + case sta::FuncExpr::Op::zero: + return false; + default: + return false; + } +} - if (!tristate_cell_) { - tristate_cell_ = findMaster( - [](sta::LibertyPort* port) { - if (!port->direction()->isTristate()) { - return false; - } - auto function = port->function(); - return function->op() != sta::FuncExpr::Op::not_; - }, - "tristate"); +// The truth table of a two-input single-output cell's output, bit k its +// value for inputs k in binary; -1 for any other cell. Symmetric +// functions only (AND, OR, NAND, NOR), so the inputs' order is moot. +static int twoInputTable(sta::LibertyPort* out) +{ + auto func = out->function(); + if (!func || !out->direction()->isOutput()) { + return -1; } - tristate_ports_ = buildPortMap(tristate_cell_); + std::vector inputs; + std::unique_ptr port_iter( + out->libertyCell()->portIterator()); + while (port_iter->hasNext()) { + auto p = static_cast(port_iter->next()); + if (p->direction()->isInput()) { + inputs.push_back(p->libertyPort()); + } + } + if (inputs.size() != 2) { + return -1; + } + int table = 0; + for (int k = 0; k < 4; ++k) { + if (evalFunc(func, inputs, {(k & 1) != 0, (k & 2) != 0})) { + table |= 1 << k; + } + } + return table; +} - if (!and2_cell_) { - and2_cell_ = findMaster( - [](sta::LibertyPort* port) { - if (!port->direction()->isOutput()) { - return false; - } - auto function = port->function(); - return function && function->op() == sta::FuncExpr::Op::and_ - && function->left()->op() == sta::FuncExpr::Op::port - && function->right()->op() == sta::FuncExpr::Op::port; - }, - "and2"); +dbMaster* RamGen::findTie(const bool one, dbMaster* given, std::string& pin) +{ + const auto level = one ? sta::FuncExpr::Op::one : sta::FuncExpr::Op::zero; + const char* name = one ? "tie high" : "tie low"; + // the cell's output at `level`, or none + auto output = [level](sta::LibertyCell* liberty) -> sta::LibertyPort* { + std::unique_ptr ports( + liberty->portIterator()); + while (ports->hasNext()) { + auto port = ports->next()->libertyPort(); + if (port && port->direction()->isOutput() && port->function() + && port->function()->op() == level) { + return port; + } + } + return nullptr; + }; + dbMaster* best = nullptr; + sta::LibertyPort* best_port = nullptr; + float best_area = std::numeric_limits::max(); + for (auto lib : db_->getLibs()) { + for (auto master : lib->getMasters()) { + if (given && master != given) { + continue; + } + auto liberty = network_->libertyCell(network_->dbToSta(master)); + if (!liberty || liberty->isIsolationCell() || liberty->isLevelShifter()) { + continue; + } + // a tie cell may be dont_use, which keeps it from synthesis + // (Nangate45's LOGIC0_X1 and LOGIC1_X1); it is the cell to tie with + auto port = output(liberty); + if (port && liberty->area() < best_area) { + best_area = liberty->area(); + best = master; + best_port = port; + } + } } + if (!best) { + if (given) { + logger_->error( + RAM, 107, "Cell {} is not a {} cell.", given->getName(), name); + } + logger_->error(RAM, 108, "Can't find {} cell", name); + } + if (!given) { + logger_->info(RAM, 109, "Selected {} cell {}", name, best->getName()); + } + pin = best_port->name(); + return best; +} + +// `text` as a word of `width` bits, least significant first: decimal, +// hexadecimal after 0x or octal after 0, as C writes them. Empty where +// `text` is not such a number or needs more than `width` bits; `fits` +// says which. +static std::vector parseWord(const std::string& text, + const int width, + bool& fits) +{ + fits = true; + int base = 10; + size_t i = 0; + if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + base = 16; + i = 2; + } else if (text.size() > 1 && text[0] == '0') { + base = 8; + i = 1; + } + if (i >= text.size()) { + return {}; + } + std::vector limbs{0}; // least significant first + for (; i < text.size(); ++i) { + const char c + = static_cast(std::tolower(static_cast(text[i]))); + int digit = base; + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } + if (digit >= base) { + return {}; + } + uint64_t carry = digit; + for (uint32_t& limb : limbs) { + const uint64_t v = static_cast(limb) * base + carry; + limb = static_cast(v); + carry = v >> 32; + } + if (carry) { + limbs.push_back(static_cast(carry)); + } + } + std::vector bits(width); + for (size_t b = 0; b < limbs.size() * 32; ++b) { + const bool one = (limbs[b / 32] >> (b % 32)) & 1; + if (one && b >= static_cast(width)) { + fits = false; + return {}; + } + if (b < static_cast(width)) { + bits[b] = one; + } + } + return bits; +} + +void RamGen::findResetPins() +{ + auto liberty = network_->libertyCell(network_->dbToSta(storage_cell_)); + const sta::Sequential* seq = nullptr; + for (const auto& s : liberty->sequentials()) { + if (s.isRegister() && s.clear() && s.preset()) { + seq = &s; + break; + } + } + if (!seq) { + logger_->error(RAM, + 61, + "Storage cell {} has no asynchronous clear and preset for " + "-reset_value.", + storage_cell_->getName()); + } + // a pin of the ff group, and whether it acts low: "!RN" or "RN" + auto pin = [&](const sta::FuncExpr* e, bool& low) -> std::string { + low = e->op() == sta::FuncExpr::Op::not_; + const sta::FuncExpr* p = low ? e->left() : e; + if (p->op() != sta::FuncExpr::Op::port) { + logger_->error(RAM, + 62, + "Storage cell {}: a clear, preset or data that is not " + "one pin is not supported.", + storage_cell_->getName()); + } + return p->port()->name(); + }; + bool clear_low = false; + bool preset_low = false; + bool data_inverted = false; + storage_clear_pin_ = pin(seq->clear(), clear_low); + storage_preset_pin_ = pin(seq->preset(), preset_low); + storage_data_pin_ = pin(seq->data(), data_inverted); + if (clear_low != preset_low) { + logger_->error(RAM, + 63, + "Storage cell {}: clear and preset act on opposite levels.", + storage_cell_->getName()); + } + reset_active_low_ = clear_low; + storage_ports_[{PortRoleType::DataIn, 0}] = storage_data_pin_; + + // A clear sets the ff group's first state variable to 0; the output + // pin reads it or its complement, and the data read back is that, + // inverted again where the storage output is (asap7's DFFASRHQNx1 + // stores !D: its `clear` sets the data to 1). + auto out = liberty->findLibertyPort( + storage_ports_[{PortRoleType::DataOut, 0}].c_str()); + bool reads_complement = false; + if (out && out->function() + && out->function()->op() == sta::FuncExpr::Op::port) { + reads_complement = seq->outputInv() == out->function()->port(); + } + clear_reads_ = (reads_complement ? 1 : 0) ^ (storage_inverted_ ? 1 : 0); + + // the unused pin of each flop held at its inactive level + const bool off_high = reset_active_low_; + tie_off_cell_ = findTie(off_high, tie_off_cell_, tie_off_pin_); + + bool fits = true; + reset_bits_ = parseWord(reset_value_, word_bits_, fits); + if (!fits) { + logger_->error(RAM, + 100, + "-reset_value {} does not fit a word of {} bits.", + reset_value_, + word_bits_); + } + if (reset_bits_.empty()) { + logger_->error(RAM, + 64, + "-reset_value {} is not a number: decimal, 0x hexadecimal " + "or 0 octal.", + reset_value_); + } +} + +void RamGen::findMasters() +{ + inv_cell_ = findMaster( + [](sta::LibertyPort* port) { return port->libertyCell()->isInverter(); }, + "inverter", + /*required=*/true, + inv_cell_); + inv_ports_ = buildPortMap(inv_cell_); + + // The read path: tristate bitlines where the library has a tristate + // cell, AND-OR trees where it has none, or as -read_style says. + if (read_style_ == "and_or") { + and_or_read_ = true; + if (tristate_cell_) { + logger_->warn(RAM, + 104, + "-tristate_cell {} is not used: -read_style and_or reads " + "without tristates.", + tristate_cell_->getName()); + } + } else { + if (!tristate_cell_) { + tristate_cell_ = findMaster( + [](sta::LibertyPort* port) { + if (!port->direction()->isTristate()) { + return false; + } + auto function = port->function(); + return function->op() != sta::FuncExpr::Op::not_; + }, + "tristate", + /*required=*/read_style_ == "tristate"); + } + if (tristate_cell_) { + tristate_ports_ = buildPortMap(tristate_cell_); + } else { + and_or_read_ = true; + logger_->info(RAM, 57, "No tristate cell: reads through AND-OR trees."); + } + } + + and2_cell_ = findMaster( + [](sta::LibertyPort* port) { + if (!port->direction()->isOutput()) { + return false; + } + auto function = port->function(); + return function && function->op() == sta::FuncExpr::Op::and_ + && function->left()->op() == sta::FuncExpr::Op::port + && function->right()->op() == sta::FuncExpr::Op::port; + }, + "and2", + /*required=*/true, + and2_cell_); and2_ports_ = buildPortMap(and2_cell_); if (!use_latch_) { if (!storage_cell_) { // FIXME // Still needs changes to get right type of flip-flop + const bool reset = !reset_value_.empty(); storage_cell_ = findMaster( - [](sta::LibertyPort* port) { + [reset](sta::LibertyPort* port) { if (!port->isRegOutput()) { return false; } + // with a reset, a positive-edge flop with both an asynchronous + // clear and preset, so any bit can take either value + if (reset) { + bool clear_preset = false; + for (const auto& seq : port->libertyCell()->sequentials()) { + clear_preset |= seq.isRegister() && seq.clear() && seq.preset() + && seq.clock() + && seq.clock()->op() == sta::FuncExpr::Op::port; + } + if (!clear_preset) { + return false; + } + } // looking for DFF specifically auto cell = port->libertyCell(); auto port_iter = cell->portIterator(); @@ -718,26 +1610,74 @@ void RamGen::findMasters() delete port_iter; return true; }, - "storage"); + "storage", + /*required=*/true, + /*given=*/nullptr, + /*q_and_qn=*/reset); + } + // the AND-OR read path takes a flop whose only output is QN; its + // trees undo the inversion + storage_ports_ = buildPortMap(storage_cell_, + and_or_read_ ? &storage_inverted_ : nullptr); + if (storage_ports_.find({PortRoleType::DataOut, 0}) + == storage_ports_.end()) { + logger_->error(RAM, + 58, + "Storage cell {} has no non-inverted output; use " + "-read_style and_or.", + storage_cell_->getName()); + } + if (!reset_value_.empty()) { + findResetPins(); } - storage_ports_ = buildPortMap(storage_cell_); } - if (!clock_gate_cell_) { + // -write_style mux holds every flop by a mux, with no clock gate + if (!write_mux_) { clock_gate_cell_ = findMaster( [](sta::LibertyPort* port) { return port->libertyCell()->isClockGate(); }, - "clock gate"); + "clock gate", + /*required=*/true, + clock_gate_cell_); + clock_gate_ports_ = buildPortMap(clock_gate_cell_); + // The enable by its liberty role: an integrated clock gate's enable + // and test enable (asap7's SE) are both inputs, in no set order. The + // test enable is tied low, so the gate opens on the enable alone. + clock_gate_test_pins_.clear(); + auto liberty = network_->libertyCell(network_->dbToSta(clock_gate_cell_)); + std::string enable; + std::vector others; + std::unique_ptr ports( + liberty->portIterator()); + while (ports->hasNext()) { + auto port = ports->next()->libertyPort(); + if (port == nullptr || port->isPwrGnd() || !port->direction()->isInput() + || port->isClockGateClock() || port->isClock()) { + continue; + } + if (port->isClockGateEnable()) { + enable = port->name(); + } else { + others.emplace_back(port->name()); + } + } + if (!enable.empty()) { + clock_gate_ports_[{PortRoleType::DataIn, 0}] = enable; + clock_gate_test_pins_ = others; + } + if (!clock_gate_test_pins_.empty()) { + tie_low_cell_ = findTie(/*one=*/false, nullptr, tie_low_pin_); + } } - clock_gate_ports_ = buildPortMap(clock_gate_cell_); // for input buffers - if (!buffer_cell_) { - buffer_cell_ = findMaster( - [](sta::LibertyPort* port) { return port->libertyCell()->isBuffer(); }, - "buffer"); - } + buffer_cell_ = findMaster( + [](sta::LibertyPort* port) { return port->libertyCell()->isBuffer(); }, + "buffer", + /*required=*/true, + buffer_cell_); buffer_ports_ = buildPortMap(buffer_cell_); // latch cells for latch-based ram: find neg/pos latch cell @@ -778,143 +1718,171 @@ void RamGen::findMasters() // aoi cells used for column mux functionality when column_mux_ratio > 1 // uses truth table simulation to identify AOI22 and discover port names // to work for all PDKs, avoiding hardcoding PDK-specific expression parsing - if (!aoi22_cell_) { - // AOI22 truth table: Y = !(A1&A2 | B1&B2) - // for 4 inputs ordered (A1,A2,B1,B2) mapped to bits (0,1,2,3), - // bit k of the table = output when inputs = k in binary. - // kAoi22Table holds the expected output values column for aoi22 truth table - static const uint16_t kAoi22Table = 0x0777; - - // recursively evaluate a liberty function expression given input port - // values - std::function&, - const std::vector&)> - evalFunc; - - evalFunc = [&evalFunc](const sta::FuncExpr* expr, - const std::vector& ports, - const std::vector& vals) -> bool { - if (!expr) { - return false; - } - switch (expr->op()) { - case sta::FuncExpr::Op::port: { - // look up this port's value in the input vector - auto p = expr->port(); - for (int i = 0; i < (int) ports.size(); ++i) { - if (ports[i] == p) { - return vals[i]; - } - } - return false; - } - case sta::FuncExpr::Op::not_: - return !evalFunc(expr->left(), ports, vals); - case sta::FuncExpr::Op::and_: - return evalFunc(expr->left(), ports, vals) - && evalFunc(expr->right(), ports, vals); - case sta::FuncExpr::Op::or_: - return evalFunc(expr->left(), ports, vals) - || evalFunc(expr->right(), ports, vals); - case sta::FuncExpr::Op::xor_: - return evalFunc(expr->left(), ports, vals) - != evalFunc(expr->right(), ports, vals); - case sta::FuncExpr::Op::one: - return true; - case sta::FuncExpr::Op::zero: - return false; - default: - return false; - } - }; - - // compute 16-entry truth table for a function given 4 ordered input ports. - // each bit k of the result = output when inputs = k in binary. (i.e. k= 2 = - // 10) - auto computeTable = - [&evalFunc](const sta::FuncExpr* func, - const std::vector& inputs) -> uint16_t { - uint16_t table = 0; - for (int k = 0; k < 16; ++k) { - std::vector vals(4); - for (int i = 0; i < 4; ++i) { - vals[i] = (k >> i) & 1; - } - if (evalFunc(func, inputs, vals)) { - table |= (1 << k); - } - } - return table; - }; + { + // AOI22 truth table: Y = !(A1&A2 | B1&B2); a given cell's pins read + // the same way + std::array in; + aoi22_cell_ = findMaster4( + 0x0777, "aoi22", in, aoi22_out_, /*required=*/true, aoi22_cell_); + aoi22_in_a1_ = in[0]; + aoi22_in_a2_ = in[1]; + aoi22_in_b1_ = in[2]; + aoi22_in_b2_ = in[3]; + } - // there are exactly 3 unique ways to split 4 ports into 2 pairs for AOI22, - // each pairing assigns (inputs[0],inputs[1]) to group A and - // (inputs[2],inputs[3]) to group B. Try all 3 and check which pattern - // pairing produces the correct AOI22 truth table, which is the correct port - // mapping - static const int kPairings[3][4] = { - {0, 1, 2, 3}, // group A=(p0,p1), group B=(p2,p3) - {0, 2, 1, 3}, // group A=(p0,p2), group B=(p1,p3) - {0, 3, 1, 2}, // group A=(p0,p3), group B=(p1,p2) - }; + // The AND-OR read path's tree: NAND2 and NOR2 levels over AOI22 leaves. + if (and_or_read_) { + // by truth table: asap7 writes its NAND2 as (!A) + (!B) + nand2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x7; }, + "nand2", + /*required=*/true, + nand2_cell_); + nand2_ports_ = buildPortMap(nand2_cell_); + nor2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x1; }, + "nor2", + /*required=*/true, + nor2_cell_); + nor2_ports_ = buildPortMap(nor2_cell_); + } - aoi22_cell_ = findMaster( - [&](sta::LibertyPort* out_port) -> bool { - if (!out_port->direction()->isOutput()) { - return false; - } - auto func = out_port->function(); - if (!func) { + // More than one write port: the write data mux, an AO22 per pair of + // ports, and an OR2 for the write enables and across pairs. + if (num_write_ > 1 || write_mux_) { + // AO22 truth table: Y = A1&A2 | B1&B2 + ao22_cell_ = findMaster4( + 0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false, ao22_cell_); + } + if (num_write_ > 1) { + or2_cell_ = findMaster( + [](sta::LibertyPort* port) { + if (!port->direction()->isOutput()) { return false; } - auto cell = out_port->libertyCell(); + auto function = port->function(); + return function && function->op() == sta::FuncExpr::Op::or_ + && function->left()->op() == sta::FuncExpr::Op::port + && function->right()->op() == sta::FuncExpr::Op::port; + }, + "or2", + /*required=*/true, + or2_cell_); + or2_ports_ = buildPortMap(or2_cell_); + } +} - // collect all input ports for this cell - std::vector inputs; - std::unique_ptr port_iter( - cell->portIterator()); - while (port_iter->hasNext()) { - auto p = static_cast(port_iter->next()); - if (p->direction()->isInput()) { - inputs.push_back(p->libertyPort()); - } - } +dbMaster* RamGen::findMaster4(const uint16_t table, + const char* name, + std::array& inputs_out, + std::string& output, + const bool required, + dbMaster* given) +{ + // for 4 inputs ordered (A1,A2,B1,B2) mapped to bits (0,1,2,3), + // bit k of the table = output when inputs = k in binary. + + // compute 16-entry truth table for a function given 4 ordered input ports. + // each bit k of the result = output when inputs = k in binary. (i.e. k= 2 = + // 10) + auto computeTable + = [](const sta::FuncExpr* func, + const std::vector& inputs) -> uint16_t { + uint16_t table = 0; + for (int k = 0; k < 16; ++k) { + std::vector vals(4); + for (int i = 0; i < 4; ++i) { + vals[i] = (k >> i) & 1; + } + if (evalFunc(func, inputs, vals)) { + table |= (1 << k); + } + } + return table; + }; + + // there are exactly 3 unique ways to split 4 ports into 2 pairs for AOI22, + // each pairing assigns (inputs[0],inputs[1]) to group A and + // (inputs[2],inputs[3]) to group B. Try all 3 and check which pattern + // pairing produces the correct AOI22 truth table, which is the correct port + // mapping + static const int kPairings[3][4] = { + {0, 1, 2, 3}, // group A=(p0,p1), group B=(p2,p3) + {0, 2, 1, 3}, // group A=(p0,p2), group B=(p1,p3) + {0, 3, 1, 2}, // group A=(p0,p3), group B=(p1,p2) + }; + + auto match = [&](sta::LibertyPort* out_port) -> bool { + if (!out_port->direction()->isOutput()) { + return false; + } + auto func = out_port->function(); + if (!func) { + return false; + } + auto cell = out_port->libertyCell(); - // aoi22 must have exactly 4 inputs - if (inputs.size() != 4) { - return false; - } + // collect all input ports for this cell + std::vector inputs; + std::unique_ptr port_iter( + cell->portIterator()); + while (port_iter->hasNext()) { + auto p = static_cast(port_iter->next()); + if (p->direction()->isInput()) { + inputs.push_back(p->libertyPort()); + } + } - // sort ports by name for deterministic ordering across builds - std::ranges::sort( - inputs, [](sta::LibertyPort* a, sta::LibertyPort* b) { - return std::string(a->name()) < std::string(b->name()); - }); - - // try each pairing, if truth table matches, record port names - for (auto& pairing : kPairings) { - std::vector ordered = {inputs[pairing[0]], - inputs[pairing[1]], - inputs[pairing[2]], - inputs[pairing[3]]}; - if (computeTable(func, ordered) == kAoi22Table) { - // found matching pairing so this is the correct port mapping, - // store port names for later use - // ordered[0] = A1, ordered[1] = A2, ordered[2] = B1, ordered[3] = - // B2 - aoi22_in_a1_ = ordered[0]->name(); - aoi22_in_a2_ = ordered[1]->name(); - aoi22_in_b1_ = ordered[2]->name(); - aoi22_in_b2_ = ordered[3]->name(); - aoi22_out_ = out_port->name(); - return true; - } - } - return false; - }, - "aoi22"); + // aoi22 must have exactly 4 inputs + if (inputs.size() != 4) { + return false; + } + + // sort ports by name for deterministic ordering across builds + std::ranges::sort(inputs, [](sta::LibertyPort* a, sta::LibertyPort* b) { + return std::string(a->name()) < std::string(b->name()); + }); + + // try each pairing, if truth table matches, record port names + for (auto& pairing : kPairings) { + std::vector ordered = {inputs[pairing[0]], + inputs[pairing[1]], + inputs[pairing[2]], + inputs[pairing[3]]}; + if (computeTable(func, ordered) == table) { + // found matching pairing so this is the correct port mapping, + // store port names for later use + // ordered[0] = A1, ordered[1] = A2, ordered[2] = B1, ordered[3] = + // B2 + for (int i = 0; i < 4; ++i) { + inputs_out[i] = ordered[i]->name(); + } + output = out_port->name(); + return true; + } + } + return false; + }; + // the cell given, or the smallest that matches; its pins are read from + // it, whichever matched last in the search + dbMaster* best = given ? given : findMaster(match, name, required); + if (best) { + bool matched = false; + auto liberty = network_->libertyCell(network_->dbToSta(best)); + std::unique_ptr ports( + liberty->portIterator()); + while (ports->hasNext() && !matched) { + auto port = ports->next()->libertyPort(); + if (port && port->direction()->isOutput()) { + matched = match(port); + } + } + if (!matched) { + logger_->error( + RAM, 110, "Cell {} is not a {} cell.", best->getName(), name); + } } + return best; } void RamGen::ramPdngen(const char* power_net_name, @@ -1095,7 +2063,10 @@ void RamGen::ramPinplacer(const char* ver_name, const char* hor_name) std::string name = bterm->getName(); if (name == "clk") { clk_pins.push_back(bterm); - } else if (name.starts_with("we[")) { + } else if (name.starts_with("we") && name.find('[') != std::string::npos + && std::all_of(name.begin() + 2, + name.begin() + name.find('['), + [](char c) { return std::isdigit(c); })) { we_pins.push_back(bterm); } } @@ -1107,8 +2078,42 @@ void RamGen::ramPinplacer(const char* ver_name, const char* hor_name) } auto pin_tech = block_->getDb()->getTech(); - io_placer_->addHorLayer(pin_tech->findLayer(hor_name)); - io_placer_->addVerLayer(pin_tech->findLayer(ver_name)); + odb::dbTechLayer* hor_layer = pin_tech->findLayer(hor_name); + odb::dbTechLayer* ver_layer = pin_tech->findLayer(ver_name); + io_placer_->addHorLayer(hor_layer); + io_placer_->addVerLayer(ver_layer); + + // no pin on a power strap of its own layer: where a strap reaches an + // edge, a pin on the tracks under it cannot be reached (asap7's M4 + // straps start on the bottom edge, its first track 12 nm above it) + for (dbNet* net : block_->getNets()) { + if (!net->getSigType().isSupply()) { + continue; + } + for (odb::dbSWire* swire : net->getSWires()) { + for (odb::dbSBox* box : swire->getWires()) { + if (box->isVia()) { + continue; + } + // the interval holds pin centres: a pin's half width and the + // layer's spacing from the strap on either side + const odb::Rect r = box->getBox(); + if (box->getTechLayer() == hor_layer) { + const int s = hor_layer->getSpacing() + hor_layer->getWidth() / 2; + io_placer_->excludeInterval( + ppl::Edge::right, r.yMin() - s, r.yMax() + s); + io_placer_->excludeInterval( + ppl::Edge::left, r.yMin() - s, r.yMax() + s); + } else if (box->getTechLayer() == ver_layer) { + const int s = ver_layer->getSpacing() + ver_layer->getWidth() / 2; + io_placer_->excludeInterval( + ppl::Edge::top, r.xMin() - s, r.xMax() + s); + io_placer_->excludeInterval( + ppl::Edge::bottom, r.xMin() - s, r.xMax() + s); + } + } + } + } io_placer_->getParameters()->setCornerAvoidance(0); io_placer_->runHungarianMatching(); } @@ -1144,27 +2149,203 @@ void RamGen::ramRouting(int thread_count) } void RamGen::generate(const int mask_size, - const int word_size, + const int word_bits, const int num_words, const int rw_ports, const int r_ports, const int w_ports, - const int column_mux_ratio, + const int column_mux_ratio_arg, const bool use_latch, dbMaster* storage_cell, dbMaster* tristate_cell, dbMaster* inv_cell, dbMaster* tapcell, - int max_tap_dist) + int max_tap_dist, + const std::string& read_style, + const int zero_word, + const std::string& reset_value, + const int banks, + const int bank_columns, + const std::string& bank_order, + const int rb_ports, + const int register_read_address, + const std::string& write_style, + const std::string& write_priority, + const std::string& periphery, + const std::string& pin_layer, + const std::string& cells, + const std::string& name, + const std::string& decoder, + const int bit_folds, + const std::string& wrapper_verilog, + const int service_every, + const int service_sites, + const std::string& bank_column_layout) { + if (banks < 1 || bank_columns < 1) { + logger_->error(RAM, + 92, + "-banks {} and -bank_columns {} must be at least 1.", + banks, + bank_columns); + } + if (bit_folds < 1) { + logger_->error(RAM, 93, "-bit_folds {} must be at least 1.", bit_folds); + } + if (rb_ports < 0) { + logger_->error(RAM, 94, "-rb_ports {} must not be negative.", rb_ports); + } + // Banks: every port decodes every word's whole address, and a row holds + // bank_columns words, laid out as the column mux lays out its words; + // without banks, the column mux as it was. + // -decoder word: each port decodes every word's whole address through + // a balanced AND2 tree per word, the banks' decoders, banks or none + banked_ = banks > 1 || bank_columns > 1 || decoder == "word"; + banks_ = banks; + bank_columns_ = bank_columns; + bank_interleaved_ = bank_order == "interleaved"; + if (banked_) { + // the option that asked for the banks' decoders, for the messages + const std::string asked + = banks > 1 || bank_columns > 1 ? "-banks" : "-decoder word"; + if (column_mux_ratio_arg != 1) { + logger_->error(RAM, + 66, + "{} is not supported with -column_mux_ratio above 1.", + asked); + } + if (num_words % banks_ != 0 || banks_ % bank_columns_ != 0) { + logger_->error(RAM, + 67, + "-num_words {} must divide into -banks {}, and the banks " + "into -bank_columns {}.", + num_words, + banks_, + bank_columns_); + } + if (use_latch) { + logger_->error(RAM, 68, "{} is not supported with -use_latch.", asked); + } + } + words_per_bank_ = num_words / banks_; + rb_ports_ = rb_ports; + register_read_address_ = register_read_address; + write_mux_ = write_style == "mux"; + write_or_ = write_priority == "or"; + periphery_verilog_ = periphery == "verilog"; + periphery_unplaced_ = periphery == "unplaced" || periphery_verilog_; + wrapper_verilog_ = wrapper_verilog; + if (periphery_verilog_ && wrapper_verilog_.empty()) { + logger_->error( + RAM, 85, "-periphery verilog needs -wrapper_verilog, its file."); + } + if (!periphery_verilog_ && !wrapper_verilog_.empty()) { + logger_->error( + RAM, 102, "-wrapper_verilog is written by -periphery verilog only."); + } + if (write_or_ && rw_ports + w_ports < 2) { + logger_->warn(RAM, + 103, + "-write_priority or with one write port: no two writes " + "collide, so it changes nothing."); + } + pin_layer_ = pin_layer; + periphery_insts_.clear(); + marking_periphery_ = false; + periphery_cell_ = nullptr; + if (write_mux_ && use_latch) { + logger_->error( + RAM, 73, "-write_style mux is not supported with -use_latch."); + } + // More than one write port: each has its own address decoder and its + // own data, a word written by two in a cycle keeps the last one's. + if (w_ports > 1 && use_latch) { + logger_->error( + RAM, 54, "More than one write port is not supported with -use_latch."); + } + if (w_ports > 1 && column_mux_ratio_arg > 1) { + logger_->error(RAM, + 55, + "More than one write port is not supported with " + "-column_mux_ratio above 1."); + } + if (register_read_address_ != 0 && register_read_address_ != 1) { + logger_->error(RAM, + 71, + "-register_read_address is 0 or 1; more than one cycle is " + "not supported."); + } + if (register_read_address_ > 0) { + if (column_mux_ratio_arg > 1 || rw_ports > 0) { + logger_->error( + RAM, + 72, + "-register_read_address 1 is not supported with -column_mux_ratio " + "above 1 or with read/write ports."); + } + } + r_plain_ = rw_ports + r_ports; + if (rb_ports_ > 0 && !banked_) { + logger_->error(RAM, 70, "-rb_ports needs -banks or -decoder word."); + } + // a banked read port is a read port per bank to the generator below + const int r_ports_all = r_ports + rb_ports_ * banks_; + // the address bits of a banked read port's per-bank address + const int bank_inputs + = std::max(1, static_cast(std::ceil(std::log2(words_per_bank_)))); + // the words in a row: the column mux's, or the banks side by side + const int column_mux_ratio = banked_ ? bank_columns_ : column_mux_ratio_arg; + words_per_row_ = column_mux_ratio; + words_contiguous_ = bank_column_layout == "contiguous"; + if (words_contiguous_ && !banked_) { + logger_->error(RAM, + 90, + "-bank_column_layout contiguous needs -banks, -bank_columns " + "or -decoder word: the column mux reads a row's words " + "interleaved."); + } + bit_folds_ = bit_folds; + slice_width_ = mask_size * column_mux_ratio + column_mux_ratio; + // the word's own bits; with -bit_folds the last fold may be short, its + // missing bits pads: columns with no port, flop or read tree + word_bits_ = word_bits; + const int word_size = bit_folds_ > 1 ? mask_size * bit_folds_ : word_bits; + if (bit_folds_ > 1) { + const int fold = (word_bits + bit_folds_ - 1) / bit_folds_; + if (mask_size != fold) { + logger_->error(RAM, + 83, + "-bit_folds {} needs -mask_size {}: a fold is a slice.", + bit_folds_, + fold); + } + if (word_size - word_bits >= mask_size) { + logger_->error(RAM, + 88, + "-bit_folds {} leaves a fold of -word_size {} empty.", + bit_folds_, + word_bits); + } + if (use_latch || (!banked_ && column_mux_ratio > 1)) { + logger_->error(RAM, + 84, + "-bit_folds is not supported with -use_latch or the row " + "decoder's -column_mux_ratio above 1."); + } + } if (num_words / column_mux_ratio == 1) { logger_->error(RAM, 42, "1-row RAMs are not supported."); } const int slices_per_word = word_size / mask_size; - const std::string ram_name = fmt::format("RAM{}x{}", num_words, word_size); + slices_per_row_ = bit_folds_ > 1 ? 1 : slices_per_word; + // the block's name: -name, or RAMx + const std::string ram_name + = name.empty() ? fmt::format("RAM{}x{}", num_words, word_bits) : name; + ram_name_ = ram_name; // error checking for column_mux_ratio - if (column_mux_ratio != 1 && column_mux_ratio != 2 && column_mux_ratio != 4) { + if (!banked_ && column_mux_ratio != 1 && column_mux_ratio != 2 + && column_mux_ratio != 4) { logger_->error(RAM, 33, "The ram generator currently only supports column_mux_ratio " @@ -1192,7 +2373,8 @@ void RamGen::generate(const int mask_size, const int num_rows = num_words / column_mux_ratio; // if column mux ratio > 1, then the lower log2(column_mux_ratio) bits are // used to select the word within a row - const int num_word_bits = (column_mux_ratio > 1) + // (with banks, none: each port decodes every word's whole address) + const int num_word_bits = (column_mux_ratio > 1 && !banked_) ? static_cast(std::log2(column_mux_ratio)) : 0; // the remaining upper bits are used to select the row @@ -1201,6 +2383,17 @@ void RamGen::generate(const int mask_size, logger_->info(RAM, 3, "Generating {}", ram_name); use_latch_ = use_latch; + num_write_ = rw_ports + w_ports; + read_style_ = read_style; + zero_word_ = zero_word; + reset_value_ = reset_value; + reset_port_ = nullptr; + tie_off_cell_ = nullptr; + and_or_read_ = false; + storage_inverted_ = false; + storage_nets_.clear(); + tree_nodes_.clear(); + word_write_.clear(); storage_cell_ = storage_cell; tristate_cell_ = tristate_cell; inv_cell_ = inv_cell; @@ -1209,7 +2402,57 @@ void RamGen::generate(const int mask_size, clock_gate_cell_ = nullptr; buffer_cell_ = nullptr; aoi22_cell_ = nullptr; + ao22_cell_ = nullptr; + or2_cell_ = nullptr; + nand2_cell_ = nullptr; + nor2_cell_ = nullptr; latch_cell_ = nullptr; + addr_flop_cell_ = nullptr; + tie_off_cell_ = nullptr; + + // -cells {role master ...}: the cell for a role, instead of the + // smallest the libraries have of its function + { + std::istringstream in(cells); + const std::map roles + = {{"inv", &inv_cell_}, + {"and2", &and2_cell_}, + {"buffer", &buffer_cell_}, + {"clock_gate", &clock_gate_cell_}, + {"aoi22", &aoi22_cell_}, + {"ao22", &ao22_cell_}, + {"or2", &or2_cell_}, + {"nand2", &nand2_cell_}, + {"nor2", &nor2_cell_}, + {"tie", &tie_off_cell_}, + {"addr_flop", &addr_flop_cell_}}; + std::vector words{std::istream_iterator(in), + std::istream_iterator()}; + if (words.size() % 2 != 0) { + logger_->error(RAM, + 99, + "-cells takes pairs of a role and a cell; {} has no cell.", + words.back()); + } + for (size_t i = 0; i < words.size(); i += 2) { + const std::string& role = words[i]; + const std::string& master_name = words[i + 1]; + auto slot = roles.find(role); + if (slot == roles.end()) { + logger_->error(RAM, + 80, + "-cells: no role {}; the roles are inv, and2, buffer, " + "clock_gate, aoi22, ao22, or2, nand2, nor2, tie and " + "addr_flop.", + role); + } + dbMaster* master = db_->findMaster(master_name.c_str()); + if (master == nullptr) { + logger_->error(RAM, 81, "-cells: {} can't be found.", master_name); + } + *slot->second = master; + } + } if (use_latch_ && storage_cell_) { auto sta_cell = network_->dbToSta(storage_cell_); @@ -1237,6 +2480,75 @@ void RamGen::generate(const int mask_size, findMasters(); + // -register_read_address 1: a plain positive-edge flop for the read addresses + if (register_read_address_ > 0) { + // a plain flop: positive-edge, no clear or preset, its data a pin + // (D or !D: a scan flop's is a mux) + auto plain = [](const sta::LibertyCell* cell) { + for (const auto& seq : cell->sequentials()) { + const sta::FuncExpr* d = seq.data(); + if (d && d->op() == sta::FuncExpr::Op::not_) { + d = d->left(); + } + if (seq.isRegister() && !seq.clear() && !seq.preset() && seq.clock() + && seq.clock()->op() == sta::FuncExpr::Op::port && d + && d->op() == sta::FuncExpr::Op::port) { + return true; + } + } + return false; + }; + // the storage cell where it is one, else the smallest (Nangate45's + // flops all have Q and QN) + auto flop = [plain](sta::LibertyPort* port) { + return port->isRegOutput() && plain(port->libertyCell()); + }; + if (addr_flop_cell_) { + // given by -cells + findMaster(flop, "address flop", /*required=*/true, addr_flop_cell_); + } else if (storage_cell_ + && plain( + network_->libertyCell(network_->dbToSta(storage_cell_)))) { + addr_flop_cell_ = storage_cell_; + } else { + addr_flop_cell_ = findMaster(flop, + "address flop", + /*required=*/true, + /*given=*/nullptr, + /*q_and_qn=*/true); + } + auto ports = buildPortMap(addr_flop_cell_, &addr_flop_inverted_); + auto liberty = network_->libertyCell(network_->dbToSta(addr_flop_cell_)); + for (const auto& seq : liberty->sequentials()) { + if (seq.isRegister()) { + // the D pin (buildPortMap's polarity already counts a "!D") + const sta::FuncExpr* d = seq.data(); + if (d->op() == sta::FuncExpr::Op::not_) { + d = d->left(); + } + addr_flop_d_ = d->port()->name(); + break; + } + } + addr_flop_clk_ = ports[{PortRoleType::Clock, 0}]; + addr_flop_q_ = ports[{PortRoleType::DataOut, 0}]; + } + + // A zero word reads 0 and drops its writes, as a RISC-V x0: its row + // has no flops, and the read trees leave it out. + if (zero_word_ >= 0) { + if (zero_word_ >= num_words) { + logger_->error( + RAM, 59, "-zero_word {} is not a word of {}.", zero_word_, num_words); + } + if (!and_or_read_ || (column_mux_ratio != 1 && !banked_)) { + logger_->error(RAM, + 60, + "-zero_word needs -read_style and_or and " + "-column_mux_ratio 1."); + } + } + auto chip = db_->getChip(); if (!chip) { chip = odb::dbChip::create( @@ -1245,15 +2557,20 @@ void RamGen::generate(const int mask_size, block_ = chip->getBlock(); if (!block_) { - block_ = odb::dbBlock::create(chip, ram_name.c_str()); + // -periphery verilog: the block is the core, the wrapper the name + const std::string block_name + = periphery_verilog_ ? ram_name + "_core" : ram_name; + block_ = odb::dbBlock::create(chip, block_name.c_str()); } // One column per bit plus one select/control column per slice, // Each read/write port gets a buffer col and decoder col, // plus inverter columns shared across ports - const int total_ports = rw_ports + r_ports + w_ports; + const int total_ports = rw_ports + r_ports_all + w_ports; + // at least one port's inverters a column: an array with fewer rows than + // address bits (four banks side by side, say) otherwise got none const int ports_per_col - = (num_row_bits > 0) ? (num_rows / num_row_bits) : total_ports; + = (num_row_bits > 0) ? std::max(1, num_rows / num_row_bits) : total_ports; const int inv_col_count = std::ceil(static_cast(total_ports) / ports_per_col); @@ -1264,16 +2581,36 @@ void RamGen::generate(const int mask_size, // one extra decoder column at far right shared across all row // mux layout columns + per-port buffer/decoder columns + inverter columns - int col_cell_count - = slices_per_word * (mask_size * column_mux_ratio + column_mux_ratio); + int col_cell_count = (bit_folds_ > 1 ? 1 : slices_per_word) + * (mask_size * column_mux_ratio + column_mux_ratio); ram_grid_.setNumLayouts(col_cell_count); auto clock = makeBTerm("clk", dbIoType::INPUT); + clock_net_ = clock->getNet(); - vector write_enable(slices_per_word, nullptr); - for (int slice = 0; slice < slices_per_word; ++slice) { - auto in_name = fmt::format("we[{}]", slice); - write_enable[slice] = makeBTerm(in_name, dbIoType::INPUT); + // -reset_value: an asynchronous reset at the storage flop's own + // polarity, rst_n or rst; a parent adapts any other reset to it + if (!reset_value_.empty()) { + if (use_latch_) { + logger_->error(RAM, 65, "-reset_value is not supported with -use_latch."); + } + reset_port_ + = makeBTerm(reset_active_low_ ? "rst_n" : "rst", dbIoType::INPUT); + } + + // One write enable bus per write port: we, or with more than one write + // port we0, we1, ... + const int num_write = num_write_; + vector> write_enable( + num_write, vector(slices_per_word, nullptr)); + for (int w = 0; w < num_write; ++w) { + const std::string we = num_write == 1 ? "we" : fmt::format("we{}", w); + // with -bit_folds a word is written whole: one enable a port + for (int slice = 0; slice < (bit_folds_ > 1 ? 1 : slices_per_word); + ++slice) { + write_enable[w][slice] + = makeBTerm(fmt::format("{}[{}]", we, slice), dbIoType::INPUT); + } } // When column_mux_ratio > 1: word_sel_nets[word_idx] is high/active when @@ -1295,7 +2632,10 @@ void RamGen::generate(const int mask_size, int r_sel_idx = 0; int w_sel_idx = 0; + addr_inputs_.clear(); addr_inputs_.resize(total_ports); + addr_true_.assign(total_ports, {}); + addr_flop_out_.assign(total_ports, {}); vector> inv_addr_nets(total_ports); // for column muxing, deocder input nets uses only the upper address bits // (num_row_bits) to determine row @@ -1311,26 +2651,74 @@ void RamGen::generate(const int mask_size, port_prefix = "addr_rw"; rw_idx++; } else if (p < rw_ports + w_ports) { - port_prefix = "addr_w"; + port_prefix = num_write == 1 ? "addr_w" : fmt::format("addr_w{}", w_idx); w_idx++; - } else { + } else if (r_idx < r_ports) { port_prefix = fmt::format("addr_r{}", r_idx++); + } else { + // a banked read port's address for one bank + const int kb = r_idx++ - r_ports; + port_prefix = fmt::format("addr_rb{}_b{}", kb / banks_, kb % banks_); } // NOLINTEND(misc-include-cleaner) - - addr_inputs_[p].resize(num_inputs); - inv_addr_nets[p].resize(num_inputs); - - for (int i = 0; i < num_inputs; ++i) { + // a banked read port's bank, or -1 + const int port_bank = p >= rw_ports + w_ports + r_ports + ? (p - rw_ports - w_ports - r_ports) % banks_ + : -1; + const int port_inputs = port_bank >= 0 ? bank_inputs : num_inputs; + + addr_inputs_[p].resize(port_inputs); + inv_addr_nets[p].resize(port_inputs); + addr_true_[p].resize(port_inputs); + addr_flop_out_[p].assign(port_inputs, nullptr); + const bool registered + = register_read_address_ > 0 && p >= rw_ports + w_ports; + + for (int i = 0; i < port_inputs; ++i) { addr_inputs_[p][i] = makeBTerm(fmt::format("{}[{}]", port_prefix, i), dbIoType::INPUT); - inv_addr_nets[p][i] - = makeNet(port_prefix, fmt::format("inv_addr[{}]", i)); + if (registered && addr_flop_inverted_) { + // the flop holds the complement; the inverter makes the address + addr_flop_out_[p][i] = inv_addr_nets[p][i] + = makeNet(port_prefix, fmt::format("inv_addr[{}]", i)); + addr_true_[p][i] = makeNet(port_prefix, fmt::format("addr_q[{}]", i)); + } else { + inv_addr_nets[p][i] + = makeNet(port_prefix, fmt::format("inv_addr[{}]", i)); + addr_true_[p][i] = addr_inputs_[p][i]->getNet(); + if (registered) { + addr_flop_out_[p][i] = addr_true_[p][i] + = makeNet(port_prefix, fmt::format("addr_q[{}]", i)); + } + } } - // inputs to decoders - decoder_input_nets[p].assign(num_rows, vector(num_row_bits)); - for (int row = 0; row < num_rows; ++row) { + // inputs to decoders: with banks, every word's whole address, one + // decoder per word (slot row * words per row + word in the row) + if (banked_) { + decoder_input_nets[p].assign(num_rows * column_mux_ratio, + vector(num_inputs)); + for (int row = 0; row < num_rows; ++row) { + for (int w = 0; w < column_mux_ratio; ++w) { + auto& literals = decoder_input_nets[p][row * column_mux_ratio + w]; + if (port_bank >= 0 && bankAt(row, w) != port_bank) { + literals.clear(); // a word of another bank: no decoder + continue; + } + // a banked read port decodes the word's index in its bank + const int word = port_bank >= 0 ? indexInBank(row) : wordAt(row, w); + literals.resize(port_inputs); + for (int input = 0; input < port_inputs; ++input) { + literals[input] = ((word >> input) & 1) ? addr_true_[p][input] + : inv_addr_nets[p][input]; + } + } + } + } + if (!banked_) { + decoder_input_nets[p].assign(num_rows, vector(num_row_bits)); + } + for (int row = 0; row < num_rows && !banked_; ++row) { int row_num = row; // start at right most bit for (int input = 0; input < num_row_bits; ++input) { @@ -1340,52 +2728,89 @@ void RamGen::generate(const int mask_size, = inv_addr_nets[p][num_word_bits + input]; } else { // puts original input in invert nets decoder_input_nets[p][row][input] - = addr_inputs_[p][num_word_bits + input]->getNet(); + = addr_true_[p][num_word_bits + input]; } row_num /= 2; } } // output nets of each port decoder + const int num_selects = banked_ ? num_rows * column_mux_ratio : num_rows; decoder_output_nets[p] - = makeDecoderOutputNets(fmt::format("decoder_p{}", p), num_rows); + = makeDecoderOutputNets(fmt::format("decoder_p{}", p), num_selects); // output nets of each decoder buffers - // write-capable ports to index 0 since there can only be 1 + // the write-capable ports first: the read-write port, then the + // write ports if (p < rw_ports) { write_select_nets[w_sel_idx] = makeSelectNets( - fmt::format("rw_sel_p{}", p), num_rows, RamPortType::ReadWrite); + fmt::format("rw_sel_p{}", p), num_selects, RamPortType::ReadWrite); read_select_nets[r_sel_idx++] = write_select_nets[w_sel_idx]; ++w_sel_idx; } else if (p < rw_ports + w_ports) { write_select_nets[w_sel_idx] = makeSelectNets( - fmt::format("w_sel_p{}", p), num_rows, RamPortType::Write); + fmt::format("w_sel_p{}", p), num_selects, RamPortType::Write); ++w_sel_idx; } if (p >= rw_ports + w_ports) { read_select_nets[r_sel_idx++] = makeSelectNets( - fmt::format("r_sel_p{}", p), num_rows, RamPortType::Read); + fmt::format("r_sel_p{}", p), num_selects, RamPortType::Read); + } + // a banked read port selects only its bank's words + if (port_bank >= 0) { + for (int slot = 0; slot < num_selects; ++slot) { + if (decoder_input_nets[p][slot].empty()) { + dbNet::destroy(decoder_output_nets[p][slot]); + dbNet::destroy(read_select_nets[r_sel_idx - 1][slot]); + decoder_output_nets[p][slot] = nullptr; + read_select_nets[r_sel_idx - 1][slot] = nullptr; + } + } } } // start of input/output net creation - int r_total = rw_ports + r_ports; + int r_total = rw_ports + r_ports_all; q_outputs_.resize(r_total); - vector D_nets(word_size); + // One data bus per write port: D, or with more than one D0, D1, ... + vector> data_inputs(num_write); + vector> D_nets(num_write, vector(word_size)); for (int bit = 0; bit < word_size; ++bit) { - data_inputs_.push_back( - makeBTerm(fmt::format("D[{}]", bit), dbIoType::INPUT)); - D_nets[bit] = makeNet("D_nets", fmt::format("b{}", bit)); + if (bit >= word_bits_) { + // a short fold's pad: no ports + for (int w = 0; w < num_write; ++w) { + data_inputs[w].push_back(nullptr); + } + for (auto& q : q_outputs_) { + q.push_back(nullptr); + } + continue; + } + for (int w = 0; w < num_write; ++w) { + const std::string d = num_write == 1 ? "D" : fmt::format("D{}", w); + data_inputs[w].push_back( + makeBTerm(fmt::format("{}[{}]", d, bit), dbIoType::INPUT)); + data_inputs_.push_back(data_inputs[w].back()); + D_nets[w][bit] = makeNet(num_write == 1 ? "D_nets" : d + "_nets", + fmt::format("b{}", bit)); + } // if total read ports == 1, only have Q outputs - if (r_total == 1) { + if (r_plain_ == 1) { auto out_name = fmt::format("Q[{}]", bit); q_outputs_[0].push_back(makeBTerm(out_name, dbIoType::OUTPUT)); } else { - for (int port = 0; port < r_total; ++port) { + for (int port = 0; port < r_plain_; ++port) { auto out_name = fmt::format("Q{}[{}]", port, bit); q_outputs_[port].push_back(makeBTerm(out_name, dbIoType::OUTPUT)); } } + // a banked read port's data, one bus per bank + for (int port = r_plain_; port < r_total; ++port) { + const int kb = port - r_plain_; + auto out_name + = fmt::format("Qrb{}_b{}[{}]", kb / banks_, kb % banks_, bit); + q_outputs_[port].push_back(makeBTerm(out_name, dbIoType::OUTPUT)); + } } // Intermediate nets between tristate outputs and Q BTerms @@ -1396,10 +2821,16 @@ void RamGen::generate(const int mask_size, vector>> word_q_nets( column_mux_ratio, vector>(r_total, vector(word_size))); - if (column_mux_ratio == 1) { - for (int port = 0; port < r_total; ++port) { - for (int bit = 0; bit < word_size; ++bit) { - word_q_nets[0][port][bit] = q_outputs_[port][bit]->getNet(); + // with banks every word drives the read port's own net: a tristate + // bitline shared by every word, or one AND-OR tree over all of them + if (column_mux_ratio == 1 || banked_) { + for (int w = 0; w < column_mux_ratio; ++w) { + for (int port = 0; port < r_total; ++port) { + for (int bit = 0; bit < word_size; ++bit) { + word_q_nets[w][port][bit] = q_outputs_[port][bit] + ? q_outputs_[port][bit]->getNet() + : nullptr; + } } } @@ -1415,10 +2846,10 @@ void RamGen::generate(const int mask_size, } // Word selects for column mux, placed before word creation - if (column_mux_ratio == 2) { + if (column_mux_ratio == 2 && !banked_) { word_sel_nets[0] = inv_addr_nets[0][0]; word_sel_nets[1] = addr_inputs_[0][0]->getNet(); - } else if (column_mux_ratio == 4) { + } else if (column_mux_ratio == 4 && !banked_) { for (int c = 0; c < 4; ++c) { word_sel_nets[c] = makeNet("word_sel", fmt::format("{}", c)); } @@ -1440,10 +2871,88 @@ void RamGen::generate(const int mask_size, // gate — all other word_idx reuse the existing net, reducing cell count // and horizontal routing congestion in storage rows. + // a port's select of the word at (row, word in the row): with banks a + // select per word, else per row + auto select_at + = [&](const vector& selects, const int row, const int w) { + return banked_ ? selects[row * column_mux_ratio + w] : selects[row]; + }; + + // The AND-OR read path: every storage net named ahead, and every + // bitline's tree planned, so each row's bit cells take their nodes. + // Where the storage flop's output is QN, or the tree's root is + // inverted, but not both, an inverter at the bottom row puts it right. + if (and_or_read_) { + // The words in the trees: where the storage output is inverted the + // final inverter assumes some word's term is the read's, so the zero + // word stays, its term a 1; otherwise its term would be a 0, and it + // is left out. + for (int row = 0; row < num_rows; ++row) { + for (int w = 0; w < column_mux_ratio; ++w) { + if (wordAt(row, w) == zero_word_) { + continue; + } + for (int bit = 0; bit < word_bits_; ++bit) { + const std::string prefix = fmt::format( + "storage_{}_{}_{}", row, w, (bit / mask_size) * mask_size); + storage_nets_[{row, w, bit}] + = makeNet(fmt::format("{}.bit{}", prefix, bit), "storage"); + } + } + } + // a read port's terms: with banks every word, a column at a time; else + // the words of column w + auto terms_of = [&](const int port, const int only_w) { + vector terms; + for (int w = 0; w < column_mux_ratio; ++w) { + if (!banked_ && w != only_w) { + continue; + } + for (int row = 0; row < num_rows; ++row) { + if (wordAt(row, w) == zero_word_ && !storage_inverted_) { + continue; + } + dbNet* select = select_at(read_select_nets[port], row, w); + if (select == nullptr) { + continue; // a banked read port's other banks + } + terms.push_back({row, w, select}); + } + } + return terms; + }; + for (int w = 0; w < (banked_ ? 1 : column_mux_ratio); ++w) { + for (int port = 0; port < r_total; ++port) { + const vector terms = terms_of(port, w); + const int n = static_cast(terms.size()); + const bool flip = treeInverted(0, n) != storage_inverted_; + for (int bit = 0; bit < word_bits_; ++bit) { + const std::string prefix + = banked_ ? fmt::format("read_p{}_b{}", port, bit) + : fmt::format("read_w{}_p{}_b{}", w, port, bit); + dbNet* out = word_q_nets[w][port][bit]; + const TreeEnd root + = planReadTree(prefix, bit, terms, 0, n, flip ? nullptr : out); + if (flip) { + const Term& at + = banked_ ? terms.back() : Term{num_rows - 1, w, nullptr}; + tree_nodes_[{at.row, at.word_idx, bit}].push_back( + {inv_cell_, + prefix, + "out_inv", + {{inv_ports_[{PortRoleType::DataIn, 0}], root.net}, + {inv_ports_[{PortRoleType::DataOut, 0}], out}}}); + } + } + } + } + } + for (int row = 0; row < num_rows; ++row) { // one select_b_net per row for column_mux_ratio > 1 vector shared_select_b_nets(r_total); - if (column_mux_ratio > 1) { + // (the AND-OR trees read the true selects: none) + if (column_mux_ratio > 1 && !banked_ && !and_or_read_) { for (int i = 0; i < r_total; ++i) { shared_select_b_nets[i] = makeNet(fmt::format("row{}", row), fmt::format("select{}_b", i)); @@ -1451,14 +2960,14 @@ void RamGen::generate(const int mask_size, } for (int word_idx = 0; word_idx < column_mux_ratio; ++word_idx) { - // size of row_selects is write port and total num read ports - vector row_selects(r_total + 1); - for (int p = 0; p < rw_ports + w_ports; ++p) { - row_selects[p] = write_select_nets[p][row]; + // the write ports' row selects, then the read ports' + vector row_selects(num_write + r_total); + for (int p = 0; p < num_write; ++p) { + row_selects[p] = select_at(write_select_nets[p], row, word_idx); } - // read selects start at index 1 (always have one write port) for (int p = 0; p < r_total; ++p) { - row_selects[p + 1] = read_select_nets[p][row]; + row_selects[num_write + p] + = select_at(read_select_nets[p], row, word_idx); } vector> word_output_nets(r_total); for (int port = 0; port < r_total; ++port) { @@ -1471,7 +2980,7 @@ void RamGen::generate(const int mask_size, mask_size, row, rw_ports, - r_ports, + r_ports_all, w_ports, word_idx, column_mux_ratio, @@ -1480,15 +2989,15 @@ void RamGen::generate(const int mask_size, write_enable, row_selects, shared_select_b_nets, - (word_idx == 0 || column_mux_ratio == 1), - use_latch_ ? mid_nets : D_nets, + (word_idx == 0 || column_mux_ratio == 1 || banked_), + use_latch_ ? vector>{mid_nets} : D_nets, word_output_nets); } } // cell placement for mux made of AOI22 (and inverter if needed) for column - // muxing - if (column_mux_ratio > 1) { + // muxing (with banks each port's tree or bitline spans every word) + if (column_mux_ratio > 1 && !banked_) { for (int slice = 0; slice < slices_per_word; ++slice) { for (int bit = 0; bit < mask_size; ++bit) { const int global_bit = slice * mask_size + bit; @@ -1618,16 +3127,16 @@ void RamGen::generate(const int mask_size, for (int c = group_start; c < group_start + column_mux_ratio; ++c) { if (c == nl_col) { auto nl_cell = std::make_unique(); - makeInst( - nl_cell.get(), - // NOLINTNEXTLINE(misc-include-cleaner) - fmt::format("neg_lat_b{}", global_bit), - "nlat", - latch_cell_, - {{latch_ports_[{PortRoleType::Clock, 0}], clk_b_net}, - {latch_ports_[{PortRoleType::DataIn, 0}], D_nets[global_bit]}, - {latch_ports_[{PortRoleType::DataOut, 0}], - mid_nets[global_bit]}}); + makeInst(nl_cell.get(), + // NOLINTNEXTLINE(misc-include-cleaner) + fmt::format("neg_lat_b{}", global_bit), + "nlat", + latch_cell_, + {{latch_ports_[{PortRoleType::Clock, 0}], clk_b_net}, + {latch_ports_[{PortRoleType::DataIn, 0}], + D_nets[0][global_bit]}, + {latch_ports_[{PortRoleType::DataOut, 0}], + mid_nets[global_bit]}}); ram_grid_.addCell(std::move(nl_cell), c); } else { ram_grid_.addCell(nullptr, c); @@ -1644,22 +3153,39 @@ void RamGen::generate(const int mask_size, } } + // the input buffers, a cell a column and slice: with -bit_folds a + // column's folds stack theirs in k rows, so the buffers of a wide word + // do not set the column's width + std::map, std::unique_ptr> buffer_cells; for (int slice = 0; slice < slices_per_word; ++slice) { for (int bit = 0; bit < mask_size; ++bit) { int bit_idx = bit + slice * mask_size; - auto buffer_grid_cell = std::make_unique(); - makeInst(buffer_grid_cell.get(), - "buffer", - fmt::format("in[{}]", bit_idx), - buffer_cell_, - {{buffer_ports_[{PortRoleType::DataIn, 0}], - data_inputs_[bit_idx]->getNet()}, - {buffer_ports_[{PortRoleType::DataOut, 0}], D_nets[bit_idx]}}); - int buf_col = slice * (mask_size * column_mux_ratio + column_mux_ratio) - + bit * column_mux_ratio; - ram_grid_.addCell(std::move(buffer_grid_cell), buf_col); + int buf_col = bitColumn(slice, bit, 0, mask_size); + auto& buffer_grid_cell + = buffer_cells[{buf_col, bit_folds_ > 1 ? slice : 0}]; + if (bit_idx >= word_bits_) { + continue; // a short fold's pad: an empty cell + } + if (!buffer_grid_cell) { + buffer_grid_cell = std::make_unique(); + } + marking_periphery_ = true; // the input buffers + for (int w = 0; w < num_write; ++w) { + makeInst( + buffer_grid_cell.get(), + num_write == 1 ? "buffer" : fmt::format("buffer{}", w), + fmt::format("in[{}]", bit_idx), + buffer_cell_, + {{buffer_ports_[{PortRoleType::DataIn, 0}], + data_inputs[w][bit_idx]->getNet()}, + {buffer_ports_[{PortRoleType::DataOut, 0}], D_nets[w][bit_idx]}}); + } + marking_periphery_ = false; } } + for (auto& [key, cell] : buffer_cells) { + ram_grid_.addCell(std::move(cell), key.first); + } // indices for creating buffers int r_buf_idx = 0; @@ -1667,31 +3193,42 @@ void RamGen::generate(const int mask_size, // append buffer, decoder, and inverter columns per port for (int p = 0; p < total_ports; ++p) { // buffer column - // keeping convention of write-capable ports at index 0 + // the write-capable ports first, as above if (p < rw_ports) { makeBufferColumn(fmt::format("sel_buf_rw_p{}", p), num_rows, decoder_output_nets[p], - write_select_nets[w_buf_idx++]); + write_select_nets[w_buf_idx++], + banked_ ? column_mux_ratio : 1); ++r_buf_idx; // so that next read port doesn't use the rw_select } else if (p < rw_ports + w_ports) { makeBufferColumn(fmt::format("sel_buf_w_p{}", p), num_rows, decoder_output_nets[p], - write_select_nets[w_buf_idx++]); + write_select_nets[w_buf_idx++], + banked_ ? column_mux_ratio : 1); } if (p >= rw_ports + w_ports) { makeBufferColumn(fmt::format("sel_buf_r_p{}", p), num_rows, decoder_output_nets[p], - read_select_nets[r_buf_idx++]); + read_select_nets[r_buf_idx++], + banked_ ? column_mux_ratio : 1); } // decoder column creation - makeDecoderColumn(fmt::format("decoder_p{}", p), - num_rows, - decoder_input_nets[p], - decoder_output_nets[p]); + if (banked_) { + makeWordDecoderColumn(fmt::format("decoder_p{}", p), + num_rows, + column_mux_ratio, + decoder_input_nets[p], + decoder_output_nets[p]); + } else { + makeDecoderColumn(fmt::format("decoder_p{}", p), + num_rows, + decoder_input_nets[p], + decoder_output_nets[p]); + } } if (num_row_bits > 0) { @@ -1699,7 +3236,7 @@ void RamGen::generate(const int mask_size, for (int col = 0; col < inv_col_count; ++col) { int start_port = col * ports_per_col; int end_port = std::min(start_port + ports_per_col, total_ports); - ram_grid_.addLayout(makeInverterColumn(num_rows, + ram_grid_.addLayout(makeInverterColumn(num_rows * bit_folds_, num_row_bits, start_port, end_port, @@ -1711,7 +3248,7 @@ void RamGen::generate(const int mask_size, std::unique_ptr inv_sel_cell = std::make_unique(); std::vector> word_sel_cells(column_mux_ratio); - if (column_mux_ratio == 2) { + if (column_mux_ratio == 2 && !banked_) { // place inv_addr[0] inverter in sel column makeInst( inv_sel_cell.get(), @@ -1720,7 +3257,7 @@ void RamGen::generate(const int mask_size, inv_cell_, {{inv_ports_[{PortRoleType::DataIn, 0}], addr_inputs_[0][0]->getNet()}, {inv_ports_[{PortRoleType::DataOut, 0}], inv_addr_nets[0][0]}}); - } else if (column_mux_ratio == 4) { + } else if (column_mux_ratio == 4 && !banked_) { for (int c = 0; c < 4; ++c) { word_sel_cells[c] = std::make_unique(); } @@ -1774,9 +3311,9 @@ void RamGen::generate(const int mask_size, // moving column_mux select cells to the top row to be closer to connected // nets also removes extra column that sits to the right of decoders - if (column_mux_ratio == 2) { + if (column_mux_ratio == 2 && !banked_) { ram_grid_.addCell(std::move(inv_sel_cell), mask_size * column_mux_ratio); - } else if (column_mux_ratio == 4) { + } else if (column_mux_ratio == 4 && !banked_) { for (int c = 0; c < 4; ++c) { int sel_col = mask_size * column_mux_ratio + c; ram_grid_.addCell(std::move(word_sel_cells[c]), sel_col); @@ -1784,7 +3321,7 @@ void RamGen::generate(const int mask_size, } // adding filler cells to ease congestion for multi-port - if (column_mux_ratio > 1 && total_ports > 1) { + if (column_mux_ratio > 1 && total_ports > 1 && !banked_) { int filler_spacing = num_rows / (column_mux_ratio - 1); if (filler_spacing > 0) { int filler_count = 0; @@ -1798,7 +3335,9 @@ void RamGen::generate(const int mask_size, } // // One extra row at the top for placing input buffers, one extra row for // // negative latches if use latch-based ram - const int num_rows_grid = num_rows + 1 + (use_latch_ ? 1 : 0); + // the words' rows, then the input buffers' (a row, or one a fold) + const int num_rows_grid + = num_rows * bit_folds_ + bit_folds_ + (use_latch_ ? 1 : 0); // auto cell_inv_layout = std::make_unique(odb::vertical); // // check for AND gate, specific case for 2 words @@ -1835,6 +3374,22 @@ void RamGen::generate(const int mask_size, ram_grid_.setOrigin(ram_origin); ram_grid_.gridInit(); + // -service_columns {every sites}: after every `every` array columns + // an empty column `sites` sites wide, room a parent's buffers can take + // inside the array + if (service_every > 0 && service_sites > 0) { + const int site_width + = (*(*db_->getLibs().begin())->getSites().begin())->getWidth(); + for (int col = (col_cell_count - 1) / service_every * service_every; + col > 0; + col -= service_every) { + auto service = std::make_unique(odb::vertical); + service->setMinWidth(service_sites * site_width); + ram_grid_.insertLayout(std::move(service), col); + } + ram_grid_.gridInit(); + } + if (tapcell_) { // max tap distance specified is greater than the length of ram if (ram_grid_.getRowWidth() <= max_tap_dist) { @@ -1902,16 +3457,426 @@ void RamGen::generate(const int mask_size, block_->setDieArea(odb::Rect(0, 0, max_x_coord, max_y_coord)); block_->setCoreArea(block_->computeCoreArea()); + periphery_cell_ = nullptr; + if (periphery_unplaced_) { + unplacePeriphery(); + } + // with -bit_folds a word is written whole: one slice to the model writeBehavioralVerilog(behavioral_verilog_filename_, - slices_per_word, - mask_size, + bit_folds_ > 1 ? 1 : slices_per_word, + bit_folds_ > 1 ? word_bits_ : mask_size, num_words, rw_ports, r_ports, w_ports); } +// A liberty function as a Verilog expression, its pins by their nets; +// none where a pin has no net or the function is incomplete. +static std::optional verilogExpr( + const sta::FuncExpr* f, + const std::map& nets) +{ + if (f == nullptr) { + return std::nullopt; + } + auto both = [&nets](const sta::FuncExpr* f, + const char* op) -> std::optional { + auto left = verilogExpr(f->left(), nets); + auto right = verilogExpr(f->right(), nets); + if (!left || !right) { + return std::nullopt; + } + return "(" + *left + op + *right + ")"; + }; + switch (f->op()) { + case sta::FuncExpr::Op::port: { + auto it = nets.find(f->port()->name()); + if (it == nets.end()) { + return std::nullopt; + } + return it->second; + } + case sta::FuncExpr::Op::not_: { + auto left = verilogExpr(f->left(), nets); + if (!left) { + return std::nullopt; + } + return "~(" + *left + ")"; + } + case sta::FuncExpr::Op::and_: + return both(f, " & "); + case sta::FuncExpr::Op::or_: + return both(f, " | "); + case sta::FuncExpr::Op::xor_: + return both(f, " ^ "); + case sta::FuncExpr::Op::one: + return "1'b1"; + case sta::FuncExpr::Op::zero: + return "1'b0"; + } + return std::nullopt; +} + +// Ports as Verilog declarations, `name[i]` grouped into buses. +static std::vector> verilogPorts( + const std::vector>& ports) +{ + std::map> buses; + std::vector order; + for (const auto& [name, io] : ports) { + std::string base = name; + int index = -1; + const auto lb = name.rfind('['); + if (lb != std::string::npos && name.back() == ']') { + base = name.substr(0, lb); + index = std::stoi(name.substr(lb + 1, name.size() - lb - 2)); + } + if (buses.find(base) == buses.end()) { + order.push_back(base); + buses[base] = {io == odb::dbIoType::INPUT ? "input" : "output", -1}; + } + buses[base].second = std::max(buses[base].second, index); + } + std::vector> decls; + for (const auto& base : order) { + const auto& [dir, hi] = buses[base]; + decls.emplace_back( + base, + hi < 0 ? dir + " " + base : fmt::format("{} [{}:0] {}", dir, hi, base)); + } + return decls; +} + +void RamGen::splitPeriphery() +{ + std::set periphery; // by id: odb objects have no order + for (dbInst* inst : periphery_insts_) { + periphery.insert(inst->getId()); + } + auto ref = [](dbNet* n) { + return n->getBTerms().begin() != n->getBTerms().end() + ? std::string((*n->getBTerms().begin())->getName()) + : "\\" + n->getName() + " "; + }; + std::vector> ports; + for (dbBTerm* t : block_->getBTerms()) { + ports.emplace_back(t->getName(), t->getIoType()); + } + std::vector wires; + std::vector dropped; + std::vector> shared; + for (dbNet* n : block_->getNets()) { + if (n->getSigType() == odb::dbSigType::POWER + || n->getSigType() == odb::dbSigType::GROUND) { + continue; + } + bool in_periphery = false; + bool in_core = false; + bool periphery_drives = false; + for (odb::dbITerm* it : n->getITerms()) { + const bool p = periphery.count(it->getInst()->getId()) > 0; + in_periphery |= p; + in_core |= !p; + periphery_drives |= p && it->getIoType() == odb::dbIoType::OUTPUT; + } + if (n->getBTerms().begin() != n->getBTerms().end()) { + if (!in_core) { + for (dbBTerm* t : n->getBTerms()) { + dropped.push_back(t); + } + } + continue; + } + if (in_periphery) { + wires.push_back(ref(n)); + } + if (in_periphery && in_core) { + shared.emplace_back( + n, periphery_drives ? odb::dbIoType::INPUT : odb::dbIoType::OUTPUT); + } + } + // the wrapper: the periphery's cells, before they go + std::ostringstream body; + for (dbInst* inst : periphery_insts_) { + std::map nets; + std::vector> conns; + for (odb::dbITerm* it : inst->getITerms()) { + dbNet* n = it->getNet(); + if (n == nullptr || n->getSigType() == odb::dbSigType::POWER + || n->getSigType() == odb::dbSigType::GROUND) { + continue; + } + nets[it->getMTerm()->getName()] = ref(n); + conns.emplace_back(it->getMTerm()->getName(), ref(n)); + } + // a logic cell, one output with a function, as an assign + auto liberty = network_->libertyCell(network_->dbToSta(inst->getMaster())); + sta::LibertyPort* out = nullptr; + int outputs = 0; + if (liberty && !liberty->hasSequentials() && !liberty->isClockGate()) { + std::unique_ptr it( + liberty->portIterator()); + while (it->hasNext()) { + auto port = it->next()->libertyPort(); + if (port && port->direction()->isAnyOutput()) { + ++outputs; + out = port; + } + } + } + if (outputs == 1 && out->function() && !out->direction()->isTristate() + && nets.count(out->name())) { + auto expr = verilogExpr(out->function(), nets); + if (!expr) { + logger_->error(RAM, + 101, + "-periphery verilog: {} ({}) has an input with no net, " + "or a function that can't be written as Verilog.", + inst->getName(), + inst->getMaster()->getName()); + } + body << " assign " << nets[out->name()] << " = " << *expr << "; // " + << inst->getName() << "\n"; + continue; + } + body << " " << inst->getMaster()->getName() << " \\" << inst->getName() + << " ("; + for (size_t i = 0; i < conns.size(); ++i) { + body << (i ? ", " : "") << "." << conns[i].first << "(" << conns[i].second + << ")"; + } + body << ");\n"; + } + // the core: its own ports kept, the shared nets new ones, named as a + // Verilog and liberty identifier (a net's name has dots and brackets) + std::map shared_ref; // core port -> wrapper net + for (auto& [n, io] : shared) { + std::string name = n->getName(); + for (char& c : name) { + if (!std::isalnum(static_cast(c)) && c != '_') { + c = '_'; + } + } + while ( + block_->findBTerm(name.c_str()) != nullptr + || (name != n->getName() && block_->findNet(name.c_str()) != nullptr)) { + name += "_"; + } + shared_ref[name] = ref(n); + // a port's net goes by the port's name, as the netlist writes it + n->rename(name.c_str()); + dbBTerm* t = dbBTerm::create(n, name.c_str()); + t->setIoType(io); + t->setSigType(odb::dbSigType::SIGNAL); + } + // the pin placer constrains what is left of the pin lists + std::set gone; // by id: odb objects have no order + for (dbBTerm* t : dropped) { + gone.insert(t->getId()); + } + auto forget = [&gone](std::vector& terms) { + std::erase_if(terms, + [&gone](dbBTerm* t) { return gone.count(t->getId()) > 0; }); + }; + forget(data_inputs_); + for (auto& terms : addr_inputs_) { + forget(terms); + } + for (auto& terms : q_outputs_) { + forget(terms); + } + for (dbBTerm* t : dropped) { + odb::dbBTerm::destroy(t); + } + for (dbInst* inst : periphery_insts_) { + dbInst::destroy(inst); + } + periphery_insts_.clear(); + std::vector empty; + for (dbNet* n : block_->getNets()) { + if (n->getITerms().empty() && n->getBTerms().empty()) { + empty.push_back(n); + } + } + for (dbNet* n : empty) { + dbNet::destroy(n); + } + std::vector> core_ports; + for (dbBTerm* t : block_->getBTerms()) { + core_ports.emplace_back(t->getName(), t->getIoType()); + } + std::ofstream out(wrapper_verilog_); + if (!out) { + logger_->error(RAM, 86, "Unable to open file {}", wrapper_verilog_); + } + const auto decls = verilogPorts(ports); + out << "// generate_ram's periphery around " << block_->getName() + << ", as Verilog\nmodule " << ram_name_ << " (\n"; + for (size_t i = 0; i < decls.size(); ++i) { + out << " " << decls[i].second << (i + 1 < decls.size() ? ",\n" : "\n"); + } + out << ");\n"; + for (const auto& w : wires) { + out << " wire " << w << ";\n"; + } + out << body.str(); + // the core by its ports: a bus of the module's own, a shared net bit by + // bit, by its name + const auto core_decls = verilogPorts(core_ports); + out << " " << block_->getName() << " core ("; + for (size_t i = 0; i < core_decls.size(); ++i) { + const std::string& p = core_decls[i].first; + auto net = shared_ref.find(p); + out << (i ? ", " : "") << "." << p << "(" + << (net == shared_ref.end() ? p : net->second) << ")"; + } + out << ");\nendmodule\n"; +} + +// -periphery unplaced: the periphery left for a parent to place and the +// outline cut to what stays, the placed core, so a parent sizing its +// floorplan by utilization charges the core and nothing else. Each pin +// goes where its connections land, the centroid of the cells on its net +// (those of the periphery too, while they are still placed), on the pin +// layer's tracks inside the outline: these pins are for a macro +// placer to orient the core by, never routed. +void RamGen::unplacePeriphery() +{ + const size_t periphery_count = periphery_insts_.size(); + if (periphery_verilog_) { + splitPeriphery(); + } + odb::dbTechLayer* layer = db_->getTech()->findLayer(pin_layer_.c_str()); + if (layer == nullptr) { + logger_->error(RAM, + 75, + "-periphery unplaced needs -pin_layer, a routing layer; " + "{} is not one.", + pin_layer_); + } + // the pins' centres, before anything moves + std::vector> pins; + for (dbBTerm* term : block_->getBTerms()) { + dbNet* net = term->getNet(); + if (net == nullptr) { + continue; + } + int64_t sx = 0; + int64_t 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) { + pins.emplace_back(term, odb::Point(sx / n, sy / n)); + } + } + for (odb::dbInst* inst : periphery_insts_) { + inst->setPlacementStatus(odb::dbPlacementStatus::UNPLACED); + } + // the placed core's bounding box, and a row above it: a parent whose + // rows are of the other parity there moves the core up a row + odb::Rect core; + core.mergeInit(); + for (odb::dbInst* inst : block_->getInsts()) { + if (inst->getPlacementStatus().isPlaced()) { + core.merge(inst->getBBox()->getBox()); + } + } + odb::dbRow* first = *block_->getRows().begin(); + const int row_h = first->getBBox().dy(); + odb::dbSite* site = first->getSite(); + const int site_w = site->getWidth(); + const int dx = core.xMin(); + const int dy = core.yMin(); + const int first_row = dy / row_h; + const int num_rows = core.dy() / row_h + 1; + const int width = core.dx(); + const int height = num_rows * row_h; + for (odb::dbInst* inst : block_->getInsts()) { + if (inst->getPlacementStatus().isPlaced()) { + const odb::Point at = inst->getLocation(); + const odb::dbPlacementStatus status = inst->getPlacementStatus(); + // a FIRM cell's origin is fixed (ODB-0359) + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + inst->setLocation(at.x() - dx, at.y() - dy); + inst->setPlacementStatus(status); + } + } + std::vector rows(block_->getRows().begin(), + block_->getRows().end()); + for (odb::dbRow* row : rows) { + odb::dbRow::destroy(row); + } + for (int r = 0; r < num_rows; ++r) { + dbRow::create(block_, + fmt::format("RAM_ROW{}", r).c_str(), + site, + 0, + r * row_h, + (first_row + r) % 2 == 0 ? odb::dbOrientType::R0 + : odb::dbOrientType::MX, + odb::dbRowDir::HORIZONTAL, + width / site_w, + site_w); + } + block_->setDieArea(odb::Rect(0, 0, width, height)); + block_->setCoreArea(block_->computeCoreArea()); + // each pin on the layer's tracks, inside the outline + const int pitch = std::max(1, static_cast(layer->getPitch())); + const int offset = layer->getOffset(); + const int half = std::max(1, static_cast(layer->getWidth()) / 2); + auto on_track = [&](int v, int hi) { + int t = offset + std::max(0, (v - offset + pitch / 2) / pitch) * pitch; + while (t > hi - half && t - pitch >= half) { + t -= pitch; + } + while (t < half) { + t += pitch; + } + return t; + }; + // the tracks run in the layer's preferred direction: across it a pin + // sits on a track, along it on the manufacturing grid + odb::dbTech* tech = db_->getTech(); + const int grid + = tech->hasManufacturingGrid() ? tech->getManufacturingGrid() : 1; + auto on_grid = [&](int v, int hi) { + int t = std::clamp(v, half, std::max(half, hi - half)); + t -= t % grid; + while (t < half) { + t += grid; + } + return t; + }; + const bool horizontal + = layer->getDirection() == odb::dbTechLayerDir::HORIZONTAL; + for (auto& [term, at] : pins) { + const int x = horizontal ? on_grid(at.x() - dx, width) + : on_track(at.x() - dx, width); + const int y = horizontal ? on_track(at.y() - dy, height) + : on_grid(at.y() - dy, height); + odb::dbBPin* pin = odb::dbBPin::create(term); + odb::dbBox::create(pin, layer, x - half, y - half, x + half, y + half); + pin->setPlacementStatus(odb::dbPlacementStatus::FIRM); + } + logger_->info(RAM, + 76, + "-periphery {}: {} periphery instances {}; the core is {:.3f} " + "x {:.3f} um.", + periphery_verilog_ ? "verilog" : "unplaced", + periphery_count, + periphery_verilog_ ? "written to " + wrapper_verilog_ + : std::string("left unplaced"), + block_->dbuToMicrons(width), + block_->dbuToMicrons(height)); +} + void RamGen::setBehavioralVerilogFilename(const std::string& filename) { behavioral_verilog_filename_ = filename; @@ -1933,11 +3898,25 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, const int address_width = (num_words <= 1) ? 1 : std::ceil(std::log2(num_words)); - std::string module_name = fmt::format("RAM{}x{}", num_words, word_size_bit); + std::string module_name = ram_name_; int r_total = rw_ports + r_ports; + const int num_write = rw_ports + w_ports; + // a write port's bus: D, we, addr_w, or with more than one D0, we0, ... + auto wbus = [num_write](const char* base, int w) { + return num_write == 1 ? std::string(base) : fmt::format("{}{}", base, w); + }; // Build port list - std::string port_list = "\n clk,\n D"; + std::string port_list = "\n clk"; + // -reset_value: the reset port, at the storage flop's polarity + const std::string reset_name + = reset_value_.empty() ? "" : (reset_active_low_ ? "rst_n" : "rst"); + if (!reset_name.empty()) { + port_list += ",\n " + reset_name; + } + for (int w = 0; w < num_write; ++w) { + port_list += ",\n " + wbus("D", w); + } for (int i = 0; i < r_total; i++) { if (r_total == 1) { port_list += ",\n Q"; @@ -1948,13 +3927,25 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, // address ports if (rw_ports > 0) { port_list += ",\n addr_rw"; - } else if (w_ports > 0) { - port_list += ",\n addr_w"; + } else { + for (int w = 0; w < w_ports; ++w) { + port_list += ",\n " + wbus("addr_w", w); + } } for (int i = 0; i < r_ports; i++) { port_list += fmt::format(",\n addr_r{}", i); } - port_list += ",\n we"; + // banked read ports: an address and a data output per bank + const int bank_bits = std::max( + 1, static_cast(std::ceil(std::log2(std::max(words_per_bank_, 1))))); + for (int k = 0; k < rb_ports_; ++k) { + for (int b = 0; b < banks_; ++b) { + port_list += fmt::format(",\n addr_rb{}_b{}", k, b); + } + } + for (int w = 0; w < num_write; ++w) { + port_list += ",\n " + wbus("we", w); + } // Build output declarations std::string output_declaration; @@ -1973,9 +3964,11 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, if (rw_ports > 0) { addr_declarations += fmt::format(" input [{}:0] addr_rw;\n", address_width - 1); - } else if (w_ports > 0) { - addr_declarations - += fmt::format(" input [{}:0] addr_w;\n", address_width - 1); + } else { + for (int w = 0; w < w_ports; ++w) { + addr_declarations += fmt::format( + " input [{}:0] {};\n", address_width - 1, wbus("addr_w", w)); + } } for (int i = 0; i < r_ports; i++) { addr_declarations @@ -1984,6 +3977,35 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, // Build read port logic std::string read_port_logic; + for (int k = 0; k < rb_ports_; ++k) { + for (int b = 0; b < banks_; ++b) { + port_list += fmt::format(",\n Qrb{}_b{}", k, b); + output_declaration += fmt::format( + " output reg [{}:0] Qrb{}_b{};\n", word_size_bit - 1, k, b); + addr_declarations += fmt::format( + " input [{}:0] addr_rb{}_b{};\n", bank_bits - 1, k, b); + // the word at index addr in bank b + const std::string word + = bank_interleaved_ + ? fmt::format("addr_rb{}_b{} * {} + {}", k, b, banks_, b) + : fmt::format("{} + addr_rb{}_b{}", b * words_per_bank_, k, b); + const std::string value + = zero_word_ >= 0 ? fmt::format("({}) == {} ? {}'d0 : mem[{}]", + word, + zero_word_, + word_size_bit, + word) + : fmt::format("mem[{}]", word); + read_port_logic += fmt::format(R"( always @(*) begin + Qrb{}_b{} = {}; + end + +)", + k, + b, + value); + } + } for (int i = 0; i < r_total; i++) { std::string port_name = (r_total == 1) ? "Q" : fmt::format("Q{}", i); std::string addr_name; @@ -1997,34 +4019,150 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, if (i > 0) { read_port_logic += "\n"; } - read_port_logic += fmt::format(R"( always @(*) begin + if (zero_word_ >= 0) { + read_port_logic += fmt::format(R"( always @(*) begin + {} = {} == {} ? {}'d0 : mem[{}]; + end +)", + port_name, + addr_name, + zero_word_, + word_size_bit, + addr_name); + } else { + read_port_logic += fmt::format(R"( always @(*) begin {} = mem[{}]; end )", - port_name, - addr_name); + port_name, + addr_name); + } + } + + // -register_read_address 1: every read address registered, the reads from the + // registered ones (addr_r0 -> addr_r0_q) + if (register_read_address_ > 0) { + std::string registers; + auto registered = [&](const std::string& name, int width) { + registers += fmt::format( + " reg [{}:0] {}_q;\n always @(posedge clk) {}_q <= {};\n", + width - 1, + name, + name, + name); + // whole words only: addr_r1 is not part of addr_r10 + std::string out; + size_t at = 0; + while (true) { + size_t hit = read_port_logic.find(name, at); + if (hit == std::string::npos) { + out += read_port_logic.substr(at); + break; + } + const size_t end = hit + name.size(); + const bool whole + = end >= read_port_logic.size() + || !( + std::isalnum(static_cast(read_port_logic[end])) + || read_port_logic[end] == '_'); + out += read_port_logic.substr(at, end - at); + if (whole) { + out += "_q"; + } + at = end; + } + read_port_logic = out; + }; + for (int i = 0; i < r_ports; ++i) { + registered(fmt::format("addr_r{}", i), address_width); + } + for (int k = 0; k < rb_ports_; ++k) { + for (int b = 0; b < banks_; ++b) { + registered(fmt::format("addr_rb{}_b{}", k, b), bank_bits); + } + } + read_port_logic = registers + "\n" + read_port_logic; + } + + // Write logic, the ports in order: where two write the same word in a + // cycle, the last one's data is kept. With -write_priority or each + // port also writes the data of every earlier port writing the same + // slice, so the last one writes the OR of them all. + std::string data_declarations; + std::string enable_declarations; + std::string write_logic; + for (int w = 0; w < num_write; ++w) { + const std::string write_addr + = rw_ports > 0 ? std::string("addr_rw") : wbus("addr_w", w); + std::string write_data + = fmt::format("{}[i*{} +:{}]", wbus("D", w), mask_size, mask_size); + for (int v = 0; write_or_ && v < w; ++v) { + write_data += fmt::format( + "\n | ({{{}{{{}[i] && {} == {}}}}} & {}[i*{} +:{}])", + mask_size, + wbus("we", v), + wbus("addr_w", v), + write_addr, + wbus("D", v), + mask_size, + mask_size); + } + data_declarations + += fmt::format(" input [{}:0] {};\n", word_size_bit - 1, wbus("D", w)); + enable_declarations += fmt::format( + " input [{}:0] {};\n", slices_per_word - 1, wbus("we", w)); + write_logic += fmt::format( + R"( for (i = 0; i < {}; i = i + 1) begin + if ({}[i]{}) begin + mem[{}][i*{} +:{}] <= {}; + end + end +)", + slices_per_word, + wbus("we", w), + zero_word_ >= 0 ? fmt::format(" && {} != {}", write_addr, zero_word_) + : std::string(), + write_addr, + mask_size, + mask_size, + write_data); } - std::string write_addr = (rw_ports > 0) ? "addr_rw" : "addr_w"; + std::string reset_declaration; + std::string always_head = " always @(posedge clk) begin\n"; + if (!reset_name.empty()) { + std::string reset_word; // most significant bit first + for (int bit = word_size_bit - 1; bit >= 0; --bit) { + reset_word += reset_bits_.at(bit) ? '1' : '0'; + } + reset_declaration = fmt::format(" input {};\n", reset_name); + always_head = fmt::format( + " always @(posedge clk or {}edge {}) begin\n" + " if ({}{}) begin\n" + " for (i = 0; i < {}; i = i + 1) begin\n" + " mem[i] <= {}'b{};\n" + " end\n" + " end else begin\n", + reset_active_low_ ? "neg" : "pos", + reset_name, + reset_active_low_ ? "!" : "", + reset_name, + num_words, + word_size_bit, + reset_word); + write_logic += " end\n"; + } std::string verilog_code = fmt::format(R"(module {} ({} ); input clk; - input [{}:0] D; -{}{} input [{}:0] we; - +{}{}{}{}{} // memory array declaration reg [{}:0] mem[0:{}]; // write logic integer i; - always @(posedge clk) begin - for (i = 0; i < {}; i = i + 1) begin - if (we[i]) begin - mem[{}][i*{} +:{}] <= D[i*{} +:{}]; - end - end - end +{}{} end // read logic {} @@ -2032,18 +4170,15 @@ endmodule )", module_name, port_list, - word_size_bit - 1, + reset_declaration, + data_declarations, output_declaration, addr_declarations, - slices_per_word - 1, + enable_declarations, word_size_bit - 1, num_words - 1, - slices_per_word, - write_addr, - mask_size, - mask_size, - mask_size, - mask_size, + always_head, + write_logic, read_port_logic); std::ofstream vf(filename); diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 8b225fcc890..10729eaa8e9 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -41,7 +41,27 @@ generate_ram_netlist_cmd(int mask_size, const char* tristate_cell_name, const char* inv_cell_name, const char* tapcell_name, - const int max_tap_dist) + const int max_tap_dist, + const char* read_style, + const int zero_word, + const char* reset_value, + const int banks, + const int bank_columns, + const char* bank_order, + const int rb_ports, + const int register_read_address, + const char* write_style, + const char* write_priority, + const char* periphery, + const char* pin_layer, + const char* cells, + const char* name, + const char* decoder, + const int bit_folds, + const char* wrapper_verilog, + const int service_every, + const int service_sites, + const char* bank_column_layout) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -93,7 +113,11 @@ generate_ram_netlist_cmd(int mask_size, ram_gen->generate(mask_size, word_size, num_words, rw_ports, r_ports, w_ports, column_mux_ratio, use_latch, storage_cell, tristate_cell, inv_cell, - tapcell, max_tap_dist); + tapcell, max_tap_dist, read_style, zero_word, + reset_value, banks, bank_columns, bank_order, rb_ports, + register_read_address, write_style, write_priority, periphery, pin_layer, cells, + name, decoder, bit_folds, wrapper_verilog, + service_every, service_sites, bank_column_layout); } void ram_pdngen(const char* power_net_name, const char* ground_net_name, diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 4e58c501636..ead7122d1aa 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -7,6 +7,25 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-column_mux_ratio ratio] [-storage_cell name] [-tristate_cell name] + [-read_style tristate|and_or] + [-zero_word word] + [-reset_value value] + [-banks count] + [-bank_columns count] + [-bank_order contiguous|interleaved] + [-rb_ports count] + [-register_read_address 0|1] + [-write_style clock_gate|mux] + [-write_priority last|or] + [-cells {role master ...}] + [-name name] + [-decoder row|word] + [-bit_folds count] + [-periphery placed|unplaced|verilog] + [-wrapper_verilog file] + [-pin_layer layer] + [-service_columns {every sites}] + [-bank_column_layout interleaved|contiguous] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -19,7 +38,10 @@ proc generate_ram_netlist { args } { sta::parse_key_args "generate_ram_netlist" args \ keys { -mask_size -word_size -num_words -column_mux_ratio -storage_cell -tristate_cell -inv_cell -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist - -write_behavioral_verilog } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value + -banks -bank_columns -bank_order -rb_ports -register_read_address -write_style -write_priority + -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog -service_columns + -bank_column_layout } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -38,7 +60,9 @@ proc generate_ram_netlist { args } { set mask_size $word_size } - if { $word_size % $mask_size != 0 } { + # with -bit_folds the last fold may be short + set folded [expr { [info exists keys(-bit_folds)] && $keys(-bit_folds) > 1 }] + if { $word_size % $mask_size != 0 && !$folded } { utl::error RAM 26 "The -word_size ($word_size) must be divisible by -mask_size ($mask_size)." } @@ -87,11 +111,20 @@ proc generate_ram_netlist { args } { set rw_ports 1 } - if { $rw_ports + $w_ports != 1 } { - utl::error RAM 30 "Must have exactly one read/write port or one write port." + if { $rw_ports + $w_ports < 1 } { + utl::error RAM 30 "Must have a read/write port or a write port." } - if { $rw_ports + $r_ports < 1 } { + if { $rw_ports > 1 || ($rw_ports > 0 && $w_ports > 0) } { + utl::error RAM 53 "A read/write port is the only write port." + } + + # banked read ports count as read ports + set rb_given 0 + if { [info exists keys(-rb_ports)] } { + set rb_given $keys(-rb_ports) + } + if { $rw_ports + $r_ports + max($rb_given, 0) < 1 } { utl::error RAM 36 "Must specify at least one read output port" } @@ -119,8 +152,177 @@ proc generate_ram_netlist { args } { ram::set_behavioral_verilog_filename $keys(-write_behavioral_verilog) } + # The read path: tristate bitlines, or AND-OR trees (and_or). By default + # tristate where the library has a tristate cell, and_or where not. + set read_style "" + if { [info exists keys(-read_style)] } { + set read_style $keys(-read_style) + if { $read_style ni {tristate and_or} } { + utl::error RAM 56 "-read_style is tristate or and_or, not $read_style." + } + } + + # A word that reads 0 and drops its writes, as a RISC-V x0. + set zero_word -1 + if { [info exists keys(-zero_word)] } { + set zero_word $keys(-zero_word) + if { $zero_word < 0 } { + utl::error RAM 96 "-zero_word $zero_word is not a word." + } + } + + # An asynchronous reset of every word to this value. + set reset_value "" + if { [info exists keys(-reset_value)] } { + set reset_value $keys(-reset_value) + } + + # Banks: the words in banks, contiguous or interleaved addresses, + # bank_columns of them side by side and the rest stacked. + set banks 1 + if { [info exists keys(-banks)] } { + set banks $keys(-banks) + } + set bank_columns 1 + if { [info exists keys(-bank_columns)] } { + set bank_columns $keys(-bank_columns) + } + # The periphery placed with the array (placed), or left for a parent to + # place, the outline then the core alone and the pins on -pin_layer + # where their connections land (unplaced). + set periphery placed + if { [info exists keys(-periphery)] } { + set periphery $keys(-periphery) + if { $periphery ni {placed unplaced verilog} } { + utl::error RAM 77 "-periphery is placed, unplaced or verilog, not $periphery." + } + } + set wrapper_verilog "" + if { [info exists keys(-wrapper_verilog)] } { + set wrapper_verilog $keys(-wrapper_verilog) + } + set pin_layer "" + if { [info exists keys(-pin_layer)] } { + set pin_layer $keys(-pin_layer) + } + + # {role master ...}: the cell for a role, instead of the smallest of its + # function; and the block's name, instead of RAMx + set cells "" + if { [info exists keys(-cells)] } { + set cells [join $keys(-cells) " "] + } + set name "" + if { [info exists keys(-name)] } { + set name $keys(-name) + } + + # The address decoders: row, a decoder per row (an AND2 chain), the + # column mux selecting within it; or word, a balanced AND2 tree over + # each word's whole address. + # A word's mask slices in this many rows instead of side by side. + # Empty columns in the array for a parent's buffers: one `sites` sites + # wide after every `every` storage columns. + set service_every 0 + set service_sites 0 + if { [info exists keys(-service_columns)] } { + if { [llength $keys(-service_columns)] != 2 } { + utl::error RAM 89 "-service_columns is {every sites}." + } + lassign $keys(-service_columns) service_every service_sites + if { $service_every < 1 || $service_sites < 1 } { + utl::error RAM 95 "-service_columns {$service_every $service_sites} needs\ + two counts of at least 1." + } + } + + # A row's words interleaved bit by bit (the column mux's order) or each + # word's bits side by side. + set bank_column_layout interleaved + if { [info exists keys(-bank_column_layout)] } { + set bank_column_layout $keys(-bank_column_layout) + if { $bank_column_layout ni {interleaved contiguous} } { + utl::error RAM 91 "-bank_column_layout is interleaved or contiguous, not $bank_column_layout." + } + } + + set bit_folds 1 + if { [info exists keys(-bit_folds)] } { + set bit_folds $keys(-bit_folds) + } + set decoder row + if { [info exists keys(-decoder)] } { + set decoder $keys(-decoder) + if { $decoder ni {row word} } { + utl::error RAM 82 "-decoder is row or word, not $decoder." + } + } + + # How a word holds its value between writes: a clock gate per word + # (clock_gate), or a mux per bit (mux). + set write_style clock_gate + if { [info exists keys(-write_style)] } { + set write_style $keys(-write_style) + if { $write_style ni {clock_gate mux} } { + utl::error RAM 74 "-write_style is clock_gate or mux, not $write_style." + } + } + + # Where ports write the same word in a cycle: the last port's data + # (last), or the OR of every writing port's data (or). + set write_priority last + if { [info exists keys(-write_priority)] } { + set write_priority $keys(-write_priority) + if { $write_priority ni {last or} } { + utl::error RAM 87 "-write_priority is last or or, not $write_priority." + } + } + + # -register_read_address 1: the read addresses registered. + set register_read_address 0 + if { [info exists keys(-register_read_address)] } { + set register_read_address $keys(-register_read_address) + } + + # Banked read ports: an address and a data output per bank. + set rb_ports 0 + if { [info exists keys(-rb_ports)] } { + set rb_ports $keys(-rb_ports) + } + set bank_order contiguous + if { [info exists keys(-bank_order)] } { + set bank_order $keys(-bank_order) + if { $bank_order ni {contiguous interleaved} } { + utl::error RAM 69 "-bank_order is contiguous or interleaved, not $bank_order." + } + } + + # The options added for register files are experimental: their + # interface may change, or move to a command of their own. + set experimental {} + foreach key {-read_style -zero_word -reset_value -banks -bank_columns -bank_order + -rb_ports -register_read_address -write_style -write_priority -cells -name + -decoder -bit_folds -periphery -pin_layer -wrapper_verilog -service_columns + -bank_column_layout} { + if { [info exists keys($key)] } { + lappend experimental $key + } + } + if { $rw_ports + $w_ports > 1 } { + lappend experimental "more than one write port" + } + if { $experimental != {} } { + utl::info RAM 105 "Experimental: [join $experimental {, }]; the interface\ + may change." + } + ram::generate_ram_netlist_cmd $mask_size $word_size $num_words $rw_ports $r_ports $w_ports \ - $column_mux_ratio $use_latch $storage_cell $tristate_cell $inv_cell $tapcell $max_tap_dist + $column_mux_ratio $use_latch $storage_cell $tristate_cell $inv_cell $tapcell $max_tap_dist \ + $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ + $rb_ports $register_read_address $write_style $write_priority $periphery $pin_layer \ + $cells $name \ + $decoder $bit_folds $wrapper_verilog $service_every $service_sites \ + $bank_column_layout } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -132,6 +334,22 @@ sta::define_cmd_args "generate_ram" {[-mask_size bits] [-column_mux_ratio ratio] [-storage_cell name] [-tristate_cell name] + [-read_style tristate|and_or] + [-zero_word word] + [-reset_value value] + [-banks count] + [-bank_columns count] + [-bank_order contiguous|interleaved] + [-rb_ports count] + [-register_read_address 0|1] + [-write_style clock_gate|mux] + [-write_priority last|or] + [-cells {role master ...}] + [-name name] + [-decoder row|word] + [-bit_folds count] + [-service_columns {every sites}] + [-bank_column_layout interleaved|contiguous] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -143,6 +361,9 @@ sta::define_cmd_args "generate_ram" {[-mask_size bits] [-max_tap_dist value] [-use_latch value] [-write_behavioral_verilog filename] + [-periphery placed|verilog] + [-wrapper_verilog file] + [-tracks file] } # user arguments for generate ram arguments @@ -153,10 +374,23 @@ proc generate_ram { args } { -r_ports -w_ports -use_latch -power_net_name -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist - -write_behavioral_verilog } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value + -banks -bank_columns -bank_order -rb_ports -register_read_address + -write_style -write_priority + -cells -name -decoder -bit_folds -service_columns + -bank_column_layout -periphery -wrapper_verilog -tracks } flags {} sta::check_argc_eq0 "generate_ram" $args + if { [info exists keys(-periphery)] && $keys(-periphery) == "unplaced" } { + utl::error RAM 98 "generate_ram routes the block; -periphery unplaced\ + is generate_ram_netlist's, for a flow that places the periphery." + } + + if { [info exists keys(-tracks)] && ![file readable $keys(-tracks)] } { + utl::error RAM 97 "-tracks file $keys(-tracks) is not readable." + } + # Check for valid design if { [ord::get_db_block] != "NULL" } { utl::error RAM 20 "A design is already loaded. Cannot generate RAM" @@ -190,6 +424,35 @@ proc generate_ram { args } { lappend ram_netlist_args -tristate_cell $keys(-tristate_cell) } + if { [info exists keys(-read_style)] } { + lappend ram_netlist_args -read_style $keys(-read_style) + } + + if { [info exists keys(-zero_word)] } { + lappend ram_netlist_args -zero_word $keys(-zero_word) + } + + if { [info exists keys(-reset_value)] } { + lappend ram_netlist_args -reset_value $keys(-reset_value) + } + + foreach key {-banks -bank_columns -bank_order -rb_ports -register_read_address + -write_style -write_priority + -cells -name -decoder -bit_folds -service_columns + -bank_column_layout -periphery -wrapper_verilog} { + if { [info exists keys($key)] } { + lappend ram_netlist_args $key $keys($key) + } + } + # the netlist places a Verilog periphery's core pins on a layer of its + # own; generate_ram places them again on its edges + if { + [info exists keys(-periphery)] && $keys(-periphery) == "verilog" + && [info exists keys(-hor_layer)] + } { + lappend ram_netlist_args -pin_layer [lindex $keys(-hor_layer) 0] + } + if { [info exists keys(-inv_cell)] } { lappend ram_netlist_args -inv_cell $keys(-inv_cell) } @@ -290,8 +553,23 @@ proc generate_ram { args } { $route_name $route_width \ $ver_name $ver_width $ver_pitch $hor_name $hor_width $hor_pitch - make_tracks -x_offset 0 -y_offset 0 + if { [info exists keys(-tracks)] } { + utl::info RAM 106 "Experimental: -tracks; the interface may change." + source $keys(-tracks) + } else { + make_tracks -x_offset 0 -y_offset 0 + } + # -periphery verilog: the netlist put each of the core's pins where + # its connections land, for a macro placer to orient the core by; + # a routed core takes its pins on its edges instead + if { [info exists keys(-periphery)] && $keys(-periphery) == "verilog" } { + foreach bterm [[ord::get_db_block] getBTerms] { + foreach bpin [$bterm getBPins] { + odb::dbBPin_destroy $bpin + } + } + } ram::ram_pinplacer $ver_name $hor_name ram::ram_filler $filler_cells diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 071fa2c0f0b..2586cd39e12 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -13,10 +13,35 @@ COMPULSORY_TESTS = [ ] PASSFAIL_TESTS = [ + "write_ports_nangate45", + "and_or_read_asap7", + "zero_word_asap7", + "reset_asap7", + "reset_nangate45", + "banks_asap7", + "banked_read_ports_asap7", + "register_read_address_asap7", + "write_mux_asap7", + "periphery_unplaced_asap7", + "cells_name_asap7", + "decoder_word_asap7", + "bit_folds_asap7", + "periphery_verilog_asap7", + "write_priority_or_asap7", + "service_columns_asap7", + "bank_column_layout_asap7", + "option_errors_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS +# Local tests, too slow to run in CI (minutes each); run them by name. +MANUAL_TESTS = [ + "banks_asap7", + "bank_column_layout_asap7", + "write_priority_or_asap7", +] + filegroup( name = "regression_resources", # Dependencies could be specified more narrowly per test case, @@ -24,6 +49,8 @@ filegroup( # per test glob patterns below. srcs = [ "helpers.tcl", + "ram_sim.tcl", + "routed.tcl", "sky130hd/sky130_fd_sc_hd__ff_n40C_1v95.lib", "sky130hd/sky130_fd_sc_hd__ss_n40C_1v40.lib", "sky130hd/sky130_fd_sc_hd__tt_025C_1v80.lib", @@ -51,6 +78,34 @@ filegroup( test_name + ".*", ], ) + { + "and_or_read_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "bank_column_layout_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "banked_read_ports_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "banks_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "bit_folds_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "cells_name_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "decoder_word_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", @@ -65,6 +120,52 @@ filegroup( ], "make_8x8_2r1w_sky130": ["make_8x8_2r1w_sky130_behavioral.vok"], "make_8x8_sky130": ["make_8x8_sky130_behavioral.vok"], + "option_errors_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "periphery_unplaced_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "periphery_verilog_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "register_read_address_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "reset_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "reset_nangate45": [ + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_tech.lef", + "Nangate45/Nangate45_typ.lib", + ], + "service_columns_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "write_mux_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "write_ports_nangate45": [ + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_tech.lef", + "Nangate45/Nangate45_typ.lib", + ], + "write_priority_or_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], + "zero_word_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], }.get(test_name, []), ) for test_name in ALL_TESTS @@ -77,6 +178,7 @@ filegroup( check_log = False if test_name in PASSFAIL_TESTS else True, check_passfail = True if test_name in PASSFAIL_TESTS else False, data = [":" + test_name + "_resources"], + tags = ["manual"] if test_name in MANUAL_TESTS else [], visibility = ["//visibility:public"], ) for test_name in ALL_TESTS diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 8545b3dbca9..6fd75e94a11 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -5,5 +5,24 @@ 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 + write_ports_nangate45 + and_or_read_asap7 + zero_word_asap7 + reset_asap7 + reset_nangate45 + banks_asap7 + banked_read_ports_asap7 + register_read_address_asap7 + write_mux_asap7 + periphery_unplaced_asap7 + cells_name_asap7 + decoder_word_asap7 + bit_folds_asap7 + periphery_verilog_asap7 + write_priority_or_asap7 + service_columns_asap7 + bank_column_layout_asap7 + option_errors_asap7 ) diff --git a/src/ram/test/and_or_read_asap7.tcl b/src/ram/test/and_or_read_asap7.tcl new file mode 100644 index 00000000000..a98530916ee --- /dev/null +++ b/src/ram/test/and_or_read_asap7.tcl @@ -0,0 +1,23 @@ +# generate_ram on asap7, which has no tristate cell: the read ports +# are AND-OR trees, not tristate bitlines, and every word reads back what +# was written, through a flop whose only output is its data inverted. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R +routed::check + +ram_sim::load +ram_sim::check { [ram_sim::tristate_count] == 0 } "a read path has a tristate" +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/bank_column_layout_asap7.tcl b/src/ram/test/bank_column_layout_asap7.tcl new file mode 100644 index 00000000000..df60819e34a --- /dev/null +++ b/src/ram/test/bank_column_layout_asap7.tcl @@ -0,0 +1,35 @@ +# -bank_column_layout contiguous: with two words a row, each word's bits +# sit side by side, the other word's to one side, not interleaved, and +# a word written in two mask slices keeps both slices together. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 4 -word_size 8 -num_words 32 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -banks 4 -bank_columns 2 \ + -bank_column_layout contiguous +routed::check + +ram_sim::load +set ranges [dict create] +dict for {word insts} [ram_sim::words_of_flops] { + set xs [lmap i $insts { lindex [$i getLocation] 0 }] + set range [list [tcl::mathfunc::min {*}$xs] [tcl::mathfunc::max {*}$xs]] + dict lappend ranges [lindex $word 0] $range +} +dict for {row rs} $ranges { + lassign [lsort -integer -index 0 $rs] a b + ram_sim::check { [lindex $a 1] < [lindex $b 0] } \ + "row $row: the words' bits are interleaved ($a, $b)" +} +ram_sim::exercise { + words 32 bits 8 + writes {{addr_w D {{we[0]} {we[1]}}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/banked_read_ports_asap7.tcl b/src/ram/test/banked_read_ports_asap7.tcl new file mode 100644 index 00000000000..2347a73f405 --- /dev/null +++ b/src/ram/test/banked_read_ports_asap7.tcl @@ -0,0 +1,30 @@ +# -rb_ports 1 with 4 interleaved banks: a read port with an address and +# data per bank (addr_rb0_b, Qrb0_b); bank b's address i reads word +# i * 4 + b. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 16 -r_ports 0 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R \ + -banks 4 -bank_order interleaved -rb_ports 1 +routed::check + +# bank b's address i holds word i * 4 + b +proc interleaved { bank i } { + return [expr { $i * 4 + $bank }] +} + +ram_sim::load +ram_sim::exercise { + words 16 bits 8 + writes {{addr_w D {we[0]}}} + banked_reads {{addr_rb0_b0 Qrb0_b0 0} {addr_rb0_b1 Qrb0_b1 1} + {addr_rb0_b2 Qrb0_b2 2} {addr_rb0_b3 Qrb0_b3 3}} + bank_word interleaved +} + +puts "pass" diff --git a/src/ram/test/banks_asap7.tcl b/src/ram/test/banks_asap7.tcl new file mode 100644 index 00000000000..b9dd6b8ea74 --- /dev/null +++ b/src/ram/test/banks_asap7.tcl @@ -0,0 +1,30 @@ +# -banks 4 -bank_columns 2 -bank_order interleaved: 32 words in four +# banks, word w in bank w % 4, two banks side by side in a row; every word +# is decoded whole and reads back what was written. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 32 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R \ + -banks 4 -bank_columns 2 -bank_order interleaved +routed::check + +ram_sim::load +# two words a row: 32 words in 16 rows of storage +set rows {} +foreach f $ram_sim::flops { + lappend rows [lindex [[lindex $f 0] getLocation] 1] +} +ram_sim::check { [llength [lsort -unique $rows]] == 16 } \ + "storage in [llength [lsort -unique $rows]] rows, not 16" +ram_sim::exercise { + words 32 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/bit_folds_asap7.tcl b/src/ram/test/bit_folds_asap7.tcl new file mode 100644 index 00000000000..4092fcc506d --- /dev/null +++ b/src/ram/test/bit_folds_asap7.tcl @@ -0,0 +1,28 @@ +# -bit_folds 3 on a 7-bit word: each word in three rows of 3, 3 and 1 +# bits, written whole and read back. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 3 -word_size 7 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -decoder word -bit_folds 3 +routed::check + +ram_sim::load +ram_sim::check { [llength $ram_sim::flops] == 8 * 7 } \ + "[llength $ram_sim::flops] storage flops, not 8 words of 7" +dict for {word insts} [ram_sim::words_of_flops] { + set ys [lsort -unique [lmap i $insts { lindex [$i getLocation] 1 }]] + ram_sim::check { [llength $ys] == 3 } "word $word is in [llength $ys] rows, not 3" +} +ram_sim::exercise { + words 8 bits 7 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/cells_name_asap7.tcl b/src/ram/test/cells_name_asap7.tcl new file mode 100644 index 00000000000..6ff39db8211 --- /dev/null +++ b/src/ram/test/cells_name_asap7.tcl @@ -0,0 +1,36 @@ +# -cells and -name: the cell given for a role is the one used, and the +# block takes the name given. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -name regfile \ + -cells {inv INVx2_ASAP7_75t_R buffer BUFx4_ASAP7_75t_R} +routed::check + +ram_sim::check { [[ord::get_db_block] getName] eq "regfile" } \ + "the block is [[ord::get_db_block] getName]" +foreach inst [[ord::get_db_block] getInsts] { + set master [[$inst getMaster] getName] + if { [string match INV* $master] } { + ram_sim::check { $master eq "INVx2_ASAP7_75t_R" } "[$inst getName] is $master" + } + if { [string match BUF* $master] } { + ram_sim::check { $master eq "BUFx4_ASAP7_75t_R" } "[$inst getName] is $master" + } +} +ram_sim::check { [llength [ram_sim::insts_of INVx2_ASAP7_75t_R]] > 0 } "no INVx2" +ram_sim::check { [llength [ram_sim::insts_of BUFx4_ASAP7_75t_R]] > 0 } "no BUFx4" +ram_sim::load +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/decoder_word_asap7.tcl b/src/ram/test/decoder_word_asap7.tcl new file mode 100644 index 00000000000..b4ac887d9db --- /dev/null +++ b/src/ram/test/decoder_word_asap7.tcl @@ -0,0 +1,26 @@ +# -decoder word: each word's select is a balanced tree of AND2s over its +# whole address, ceil(log2(5)) = 3 deep for 32 words, and one more where +# the select meets the write enable: 4, where the row decoder's chain of +# AND2s makes 5. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 4 -word_size 4 -num_words 32 -r_ports 1 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -decoder word +routed::check + +ram_sim::load +set d [ram_sim::depth AND2*] +ram_sim::check { $d <= 4 } "the decode is $d AND2s deep, not 4" +ram_sim::exercise { + words 32 bits 4 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q}} +} + +puts "pass" diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl new file mode 100644 index 00000000000..2cdeab158ca --- /dev/null +++ b/src/ram/test/option_errors_asap7.tcl @@ -0,0 +1,47 @@ +# Out-of-range options stop generation, each with its own message. + +source "helpers.tcl" +source "routed.tcl" + +routed::read_asap7 + +set base [concat $routed::asap7 {-mask_size 8 -word_size 8 -num_words 8 -r_ports 1 -w_ports 1 + -storage_cell DFFHQNx1_ASAP7_75t_R}] +foreach {id options} { + 36 {-r_ports 0 -rb_ports 0} + 96 {-zero_word -2} + 59 {-zero_word 8} + 92 {-banks 0} + 92 {-bank_columns -1} + 95 {-service_columns {0 10}} + 93 {-bit_folds 0} + 71 {-register_read_address -1} + 71 {-register_read_address 2} + 94 {-banks 2 -rb_ports -1} + 97 {-tracks no_such_tracks.tcl} + 98 {-periphery unplaced} + 79 {-cells {and2 OR2x2_ASAP7_75t_R}} + 79 {-cells {nand2 NOR2xp33_ASAP7_75t_R}} + 99 {-cells {and2 AND2x2_ASAP7_75t_R inv}} + 54 {-w_ports 2 -use_latch 1} + 55 {-w_ports 2 -column_mux_ratio 2} + 79 {-register_read_address 1 -cells {addr_flop SDFHx1_ASAP7_75t_R}} + 66 {-decoder word -column_mux_ratio 2} + 102 {-wrapper_verilog unused.v} + 64 {-storage_cell DFFASRHQNx1_ASAP7_75t_R -reset_value 12abc} + 64 {-storage_cell DFFASRHQNx1_ASAP7_75t_R -reset_value -1} + 64 {-storage_cell DFFASRHQNx1_ASAP7_75t_R -reset_value 0x} + 100 {-storage_cell DFFASRHQNx1_ASAP7_75t_R -reset_value 0x1ff} +} { + set args [dict merge $base $options] + if { ![catch { generate_ram {*}$args } message] } { + error "generate_ram $options did not stop" + } + set want [format "RAM-%04d" $id] + if { ![string match "*$want*" $message] } { + error "generate_ram $options stopped with: $message, not $want" + } + puts "$options: $want" +} + +puts "pass" diff --git a/src/ram/test/periphery_unplaced_asap7.tcl b/src/ram/test/periphery_unplaced_asap7.tcl new file mode 100644 index 00000000000..6d4da85c527 --- /dev/null +++ b/src/ram/test/periphery_unplaced_asap7.tcl @@ -0,0 +1,75 @@ +# -periphery unplaced: the core (storage and read trees) is placed, the +# periphery (decoders, select and input buffers) is left for a parent to +# place; the outline is the placed core and one row, and every pin sits +# inside it on a track of -pin_layer. + +source "helpers.tcl" +source "ram_sim.tcl" + +read_liberty ../../../test/asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz +read_liberty ../../../test/asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz +read_liberty ../../../test/asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz +read_liberty ../../../test/asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib +read_liberty ../../../test/asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz + +read_lef ../../../test/asap7/asap7_tech_1x_201209.lef +read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef + + +generate_ram_netlist \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -periphery unplaced -pin_layer M4 + +set block [ord::get_db_block] +set x0 1e18; set y0 1e18; set x1 -1e18; set y1 -1e18 +set row_h 0 +foreach inst [$block getInsts] { + set name [$inst getName] + if { [string match decoder* $name] || [string match *sel_buf* $name] } { + ram_sim::check { ![$inst isPlaced] } "periphery $name is placed" + } + if { [string match storage_* $name] && [string match DFF* [[$inst getMaster] getName]] } { + ram_sim::check { [$inst isPlaced] } "storage $name is not placed" + } + if { [$inst isPlaced] } { + set b [$inst getBBox] + set x0 [expr { min($x0, [$b xMin]) }]; set y0 [expr { min($y0, [$b yMin]) }] + set x1 [expr { max($x1, [$b xMax]) }]; set y1 [expr { max($y1, [$b yMax]) }] + set row_h [[$inst getMaster] getHeight] + } +} +set die [$block getDieArea] +ram_sim::check { [$die xMin] <= $x0 && [$die yMin] <= $y0 + && [$die xMax] >= $x1 && [$die yMax] >= $y1 } \ + "a placed cell is outside the outline" +ram_sim::check { [$die dy] - ($y1 - $y0) <= $row_h } \ + "the outline is [$die dy] tall for a core of [expr { $y1 - $y0 }]: more than a row over" +# M4 is horizontal: its tracks are rows, a pin's y on one of them and +# its x on the manufacturing grid +set m4 [[ord::get_db_tech] findLayer M4] +set pitch [$m4 getPitch] +set offset [$m4 getOffset] +set grid [[ord::get_db_tech] getManufacturingGrid] +foreach term [$block getBTerms] { + foreach pin [$term getBPins] { + foreach box [$pin getBoxes] { + ram_sim::check { [[$box getTechLayer] getName] eq "M4" } "[$term getName] is not on M4" + set x [expr { ([$box xMin] + [$box xMax]) / 2 }] + set y [expr { ([$box yMin] + [$box yMax]) / 2 }] + ram_sim::check { ($y - $offset) % $pitch == 0 } \ + "[$term getName] is off M4's tracks" + ram_sim::check { $x % $grid == 0 } \ + "[$term getName] is off the manufacturing grid" + ram_sim::check { [$box xMin] >= [$die xMin] && [$box xMax] <= [$die xMax] } \ + "[$term getName] is outside the outline" + } + } +} +ram_sim::load +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/periphery_verilog_asap7.tcl b/src/ram/test/periphery_verilog_asap7.tcl new file mode 100644 index 00000000000..c87d35835c2 --- /dev/null +++ b/src/ram/test/periphery_verilog_asap7.tcl @@ -0,0 +1,32 @@ +# -periphery verilog: the block is the core alone, _core, and the +# periphery a Verilog module, , around it. Simulated together, the +# wrapper and the core read back every word written. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +set wrapper [make_result_file periphery_verilog_asap7_wrapper.v] +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -name rf \ + -periphery verilog -wrapper_verilog $wrapper +routed::check + +ram_sim::check { [[ord::get_db_block] getName] eq "rf_core" } \ + "the block is [[ord::get_db_block] getName], not rf_core" +foreach inst [[ord::get_db_block] getInsts] { + ram_sim::check { ![string match decoder* [$inst getName]] } \ + "the core holds the periphery's [$inst getName]" +} +ram_sim::load +ram_sim::add_wrapper $wrapper +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/ram_sim.tcl b/src/ram/test/ram_sim.tcl new file mode 100644 index 00000000000..ed15be91079 --- /dev/null +++ b/src/ram/test/ram_sim.tcl @@ -0,0 +1,655 @@ +# A cycle simulator of the generated block, for tests that check what an +# option is meant to do (a word written reads back, a reset clears every +# word, ...) rather than what the generator happens to emit. +# +# Combinational cells are evaluated from their Liberty functions. Flops +# and clock gates have no function OpenSTA exposes to Tcl, so the few the +# tests use are modelled below, one line each, from their datasheets. + +namespace eval ram_sim { +# master -> {clock data {output_pin polarity ...} set set_active reset reset_active} +# polarity: q (the stored bit) or qn (its complement); set/reset "" +# when the flop has none; active 0 or 1. +variable flop_models +array set flop_models { + DFFHQNx1_ASAP7_75t_R {CLK D {QN qn} "" 0 "" 0} + DFFASRHQNx1_ASAP7_75t_R {CLK D {QN qn} SETN 0 RESETN 0} + DFF_X1 {CK D {Q q QN qn} "" 0 "" 0} + DFFRS_X1 {CK D {Q q QN qn} SN 0 RN 0} + sky130_fd_sc_hd__dfxtp_1 {CLK D {Q q} "" 0 "" 0} + } +# master -> {clock enable test_enable gated_clock}: the enable is +# latched while the clock is low +variable gate_models +array set gate_models { + ICGx1_ASAP7_75t_R {CLK ENA SE GCLK} + CLKGATETST_X1 {CK E SE GCK} + CLKGATE_X1 {CK E "" GCK} + sky130_fd_sc_hd__dlclkp_1 {CLK GATE "" GCLK} + } + +variable value +variable net_ids +variable comb_order +variable comb_all +variable port_ids +variable flops +variable gates +variable state +variable latched +variable clock_net +} + +# Nets by integer id: names carry brackets, which Tcl would substitute +# inside an expression. +proc ram_sim::net_name { net } { + variable net_ids + set name [$net getName] + if { ![info exists net_ids($name)] } { + set net_ids($name) [array size net_ids] + } + return $net_ids($name) +} + +# Liberty function syntax to a Tcl expression over value(). +proc ram_sim::to_expr { function pins } { + set e $function + regsub -all {\&} $e {*} e + regsub -all {\|} $e {+} e + # tokens: identifiers become net lookups + set out "" + set i 0 + set n [string length $e] + while { $i < $n } { + set c [string index $e $i] + if { [regexp {[A-Za-z_]} $c] } { + regexp -start $i {[A-Za-z_][A-Za-z0-9_\[\]]*} $e tok + if { [dict exists $pins $tok] } { + append out "\$::ram_sim::value([dict get $pins $tok])" + } else { + error "ram_sim: function $function names $tok, not a pin" + } + incr i [string length $tok] + continue + } + switch -- $c { + "*" { append out " && " } + "+" { append out " || " } + "^" { append out " ^ " } + "!" { append out "!" } + "'" { + error "ram_sim: postfix complement in $function is not supported" + } + default { append out $c } + } + incr i + } + return $out +} + +# Build the model of the current block. +proc ram_sim::load { } { + variable flop_models + variable gate_models + variable value + variable comb_order + variable flops + variable gates + variable state + variable latched + array unset value + array unset net_ids + array unset port_ids + array unset state + array unset latched + set flops {} + set gates {} + set block [ord::get_db_block] + foreach term [$block getBTerms] { + set net [$term getNet] + if { $net ne "NULL" } { + set value([net_name $net]) 0 + } + } + # combinational instances: output net -> expression, inputs + set comb {} + foreach inst [$block getInsts] { + set master [[$inst getMaster] getName] + set pins [dict create] + foreach it [$inst getITerms] { + set net [$it getNet] + if { $net eq "NULL" || [$net getSigType] in {POWER GROUND} } { + continue + } + set name [net_name $net] + dict set pins [[$it getMTerm] getName] $name + if { ![info exists value($name)] } { + set value($name) 0 + } + } + if { [info exists flop_models($master)] } { + lassign $flop_models($master) clk d outs set set_on reset reset_on + lappend flops [list $inst $pins $clk $d $outs $set $set_on $reset $reset_on] + set state($inst) 0 + continue + } + if { [info exists gate_models($master)] } { + lappend gates [list $inst $pins {*}$gate_models($master)] + set latched($inst) 0 + continue + } + if { [dict size $pins] == 0 } { + continue + } + foreach it [$inst getITerms] { + set mterm [$it getMTerm] + if { [$mterm getIoType] ne "OUTPUT" } { + continue + } + set pin [$mterm getName] + if { ![dict exists $pins $pin] } { + continue + } + set lib_pin [get_lib_pins -quiet "$master/$pin"] + if { $lib_pin eq "" } { + error "ram_sim: no Liberty pin $master/$pin" + } + set function [$lib_pin function] + if { $function eq "" } { + error "ram_sim: $master/$pin has no function and no model" + } + set enable [$lib_pin tristate_enable] + set inputs [dict values [dict remove $pins $pin]] + lappend comb [list [dict get $pins $pin] [to_expr $function $pins] \ + [expr { $enable eq "" ? "" : [to_expr $enable $pins] }] \ + $inputs $master] + } + } + variable comb_all $comb + order +} + +# Order the combinational outputs: each after every output its inputs read. +proc ram_sim::order { } { + variable comb_all + variable comb_order + set comb $comb_all + set driven [dict create] + foreach c $comb { + dict lappend driven [lindex $c 0] $c + } + set comb_order {} + set done [dict create] + set visiting [dict create] + foreach c $comb { + visit $c $driven done visiting + } + # a net driven by several tristates: one entry, resolved together after + # the last of its drivers, whose inputs are then all settled + set last [dict create] + set i 0 + foreach c $comb_order { + dict set last [lindex $c 0] $i + incr i + } + set ordered {} + set i 0 + foreach c $comb_order { + set net [lindex $c 0] + if { [dict get $last $net] == $i } { + lappend ordered [list $net [dict get $driven $net]] + } + incr i + } + set comb_order $ordered +} + +proc ram_sim::visit { c driven done_var visiting_var } { + upvar $done_var done + upvar $visiting_var visiting + variable comb_order + set key [lindex $c 0]:[lindex $c 1] + if { [dict exists $done $key] } { + return + } + if { [dict exists $visiting $key] } { + error "ram_sim: combinational loop through [lindex $c 0]" + } + dict set visiting $key 1 + foreach input [lindex $c 3] { + if { [dict exists $driven $input] } { + foreach d [dict get $driven $input] { + visit $d $driven done visiting + } + } + } + dict set done $key 1 + lappend comb_order $c +} + +# Evaluate every combinational output from the inputs and the flops. +proc ram_sim::settle { } { + variable value + variable comb_order + variable flops + variable state + variable gates + variable latched + foreach f $flops { + lassign $f inst pins clk d outs + foreach {pin polarity} $outs { + if { [dict exists $pins $pin] } { + set value([dict get $pins $pin]) \ + [expr { $polarity eq "q" ? $state($inst) : !$state($inst) }] + } + } + } + # gated clocks follow their clock and latched enable + foreach g $gates { + lassign $g inst pins clk en se gclk + if { [dict exists $pins $gclk] } { + set value([dict get $pins $gclk]) \ + [expr { $value([dict get $pins $clk]) && $latched($inst) }] + } + } + foreach entry $comb_order { + lassign $entry net drivers + set driving {} + foreach c $drivers { + lassign $c - expression enable + if { $enable ne "" } { + set on [expr $enable] + if { !$on } { + continue + } + } + set bit [expr $expression] + lappend driving [expr { $bit ? 1 : 0 }] + } + if { [llength [lsort -unique $driving]] > 1 } { + error "ram_sim: $net driven both ways" + } + if { [llength $driving] == 1 } { + set value($net) [lindex $driving 0] + } + } +} + +# Asynchronous set and reset take effect at once. +proc ram_sim::apply_async { } { + variable value + variable flops + variable state + foreach f $flops { + lassign $f inst pins clk d outs set set_on reset reset_on + if { + $reset ne "" && [dict exists $pins $reset] + && $value([dict get $pins $reset]) == $reset_on + } { + set state($inst) 0 + } elseif { + $set ne "" && [dict exists $pins $set] + && $value([dict get $pins $set]) == $set_on + } { + set state($inst) 1 + } + } +} + +# One clock cycle on `clock`: low (gates latch their enables), then a +# rising edge on which every flop whose clock rose samples its data. +proc ram_sim::cycle { clock } { + variable value + variable flops + variable state + variable gates + variable latched + set clock [net_of_port $clock] + set value($clock) 0 + apply_async + settle + foreach g $gates { + lassign $g inst pins clk en se + set on [expr { $value([dict get $pins $en]) }] + if { $se ne "" && [dict exists $pins $se] } { + set on [expr { $on || $value([dict get $pins $se]) }] + } + set latched($inst) $on + } + settle + set before [array get value] + set value($clock) 1 + settle + set sampled {} + array set was $before + foreach f $flops { + lassign $f inst pins clk d + set c [dict get $pins $clk] + if { !$was($c) && $value($c) } { + lappend sampled $inst $was([dict get $pins $d]) + } + } + foreach {inst bit} $sampled { + set state($inst) $bit + } + apply_async + settle +} + +proc ram_sim::net_of_port { name } { + variable port_ids + if { [info exists port_ids($name)] } { + return $port_ids($name) + } + set term [[ord::get_db_block] findBTerm $name] + if { $term eq "NULL" } { + error "ram_sim: no port $name" + } + return [net_name [$term getNet]] +} + +proc ram_sim::set_port { name bit } { + variable value + set value([net_of_port $name]) $bit +} + +proc ram_sim::get_port { name } { + variable value + return $value([net_of_port $name]) +} + +# A bus `base`[0..width-1] to an integer value and back. +proc ram_sim::set_bus { base width int } { + for { set i 0 } { $i < $width } { incr i } { + set_port "$base\[$i\]" [expr { ($int >> $i) & 1 }] + } +} + +proc ram_sim::get_bus { base width } { + set int 0 + for { set i 0 } { $i < $width } { incr i } { + set int [expr { $int | ([get_port "$base\[$i\]"] << $i) }] + } + return $int +} + +proc ram_sim::check { condition message } { + if { ![uplevel 1 [list expr $condition]] } { + error "ram_sim: $message" + } +} + +# Random writes and reads against a model of what the RAM is meant to +# hold. Options (a dict): +# words, bits the RAM's shape +# writes ports: {addr data we}, `we` the enable pins, every +# mask slice's, set together: a whole word written +# reads ports: {addr data} +# banked_reads ports: {addr data bank}, the word that address in +# that bank holds given by `bank_word` +# bank_word a command: bank index -> word +# zero_word a word that reads 0 and drops writes, or -1 +# latency 0, or 1 for a read one clock after its address +# priority last (the last port writing a word wins) or or +# mem the words' contents to start from +# cycles how many +proc ram_sim::exercise { options } { + set o [dict merge {writes {} reads {} banked_reads {} bank_word {} + zero_word -1 latency 0 priority last mem {} + cycles 200} $options] + set words [dict get $o words] + set bits [dict get $o bits] + set abits [expr { max(1, int(ceil(log($words) / log(2)))) }] + set mem [dict get $o mem] + if { $mem eq "" } { + set mem [lrepeat $words 0] + } + set zero [dict get $o zero_word] + expr { srand(1) } + for { set cycle 0 } { $cycle < [dict get $o cycles] } { incr cycle } { + # writes: a third of the time every port writes the same word + set same [expr { int(rand() * $words) }] + set written {} + foreach port [dict get $o writes] { + lassign $port addr data we + set a [expr { rand() < 0.33 ? $same : int(rand() * $words) }] + set d [expr { int(rand() * (1 << $bits)) }] + set e [expr { rand() < 0.7 }] + set_bus $addr $abits $a + set_bus $data $bits $d + foreach pin $we { + set_port $pin $e + } + if { $e } { + lappend written $a $d + } + } + cycle clk + foreach port [dict get $o writes] { + foreach pin [lindex $port 2] { + set_port $pin 0 + } + } + set next $mem + set hit [dict create] + foreach {a d} $written { + if { $a == $zero } { + continue + } + if { [dict get $o priority] eq "or" && [dict exists $hit $a] } { + lset next $a [expr { [lindex $next $a] | $d }] + } else { + lset next $a $d + } + dict set hit $a 1 + } + set mem $next + # reads + set expect {} + foreach port [dict get $o reads] { + lassign $port addr data + set a [expr { int(rand() * $words) }] + set_bus $addr $abits $a + lappend expect $data $a $a + } + foreach port [dict get $o banked_reads] { + lassign $port addr data bank + set per_bank [expr { $words / [llength [dict get $o banked_reads]] }] + set i [expr { int(rand() * $per_bank) }] + set_bus $addr [expr { max(1, int(ceil(log($per_bank) / log(2)))) }] $i + lappend expect $data [{*}[dict get $o bank_word] $bank $i] "$bank/$i" + } + if { [dict get $o latency] } { + cycle clk + } else { + settle + } + foreach {data a where} $expect { + set q [get_bus $data $bits] + set want [expr { $a == $zero ? 0 : [lindex $mem $a] }] + check { $q == $want } \ + "cycle $cycle: $data at $where (word $a) reads $q, expected $want" + } + } + return $mem +} + +# Instances whose master is `pattern` (glob). +proc ram_sim::insts_of { pattern } { + set out {} + foreach inst [[ord::get_db_block] getInsts] { + if { [string match $pattern [[$inst getMaster] getName]] } { + lappend out $inst + } + } + return $out +} + +# Outputs driven through a tristate enable. +proc ram_sim::tristate_count { } { + variable comb_order + set n 0 + foreach entry $comb_order { + foreach c [lindex $entry 1] { + if { [lindex $c 2] ne "" } { + incr n + } + } + } + return $n +} + +# A -periphery verilog wrapper around the loaded core: its assigns, its +# clock gates and its instance of the core, joined to the core through +# the core's ports. The wrapper's own ports then stand for the RAM's. +proc ram_sim::add_wrapper { file } { + variable comb_all + variable gates + variable gate_models + variable latched + variable value + variable port_ids + set text [read [set f [open $file r]]] + close $f + regsub -all {//[^\n]*} $text {} text + # wrapper net name -> id; a net joined to a core port is that port's net + set ids [dict create] + # the core instance: .port(net) pairs + if { ![regexp {\n\s*(\S+)\s+core\s*\(([^;]*)\);} $text - core_master pairs] } { + error "ram_sim: $file instantiates no core" + } + foreach {- port conn} [regexp -all -inline {\.(\w+)\(([^)]*)\)} $pairs] { + set conn [unescape $conn] + set term [[ord::get_db_block] findBTerm $port] + if { $term ne "NULL" } { + dict set ids $conn [net_name [$term getNet]] + continue + } + # a bus: each bit of the core's port to the same bit of the net + foreach term [[ord::get_db_block] getBTerms] { + if { + [regexp {^(.+)\[(\d+)\]$} [$term getName] - base bit] + && $base eq $port + } { + dict set ids "$conn\[$bit\]" [net_name [$term getNet]] + } + } + } + # the wrapper's ports + foreach {- dir range name} [regexp -all -inline {(input|output)\s*(\[\d+:\d+\])?\s*(\w+)} $text] { + if { $range eq "" } { + set port_ids($name) [wrapper_net ids $name] + } else { + regexp {\[(\d+):(\d+)\]} $range - hi lo + for { set b $lo } { $b <= $hi } { incr b } { + set port_ids($name\[$b\]) [wrapper_net ids "$name\[$b\]"] + } + } + } + # assigns + foreach {- lhs rhs} [regexp -all -inline {assign\s+([^=]*?)\s*=\s*([^;]*);} $text] { + set out [wrapper_net ids [unescape $lhs]] + set expr "" + set inputs {} + set rest $rhs + while { [string length [string trim $rest]] } { + set rest [string trimleft $rest] + if { + [regexp {^\\(\S+)\s(.*)$} $rest - tok rest] || + [regexp {^([A-Za-z_][\w.]*(?:\[\d+\])?)(.*)$} $rest - tok rest] + } { + set id [wrapper_net ids $tok] + lappend inputs $id + append expr "\$::ram_sim::value($id)" + } elseif { [regexp {^1'b([01])(.*)$} $rest - bit rest] } { + append expr $bit + } else { + set c [string index $rest 0] + set rest [string range $rest 1 end] + append expr [string map {~ ! & " && " | " || " ^ " ^ "} $c] + } + } + lappend comb_all [list $out $expr "" $inputs assign] + } + # clock gates + set cell {\n\s*(\w+)\s+(\\\S+\s|\w+)\s*\(([^;]*)\);} + foreach {- master name pins} [regexp -all -inline $cell $text] { + if { ![info exists gate_models($master)] } { + continue + } + set p [dict create] + foreach {- pin conn} [regexp -all -inline {\.(\w+)\(([^)]*)\)} $pins] { + dict set p $pin [wrapper_net ids [unescape $conn]] + } + set inst "wrapper:$name" + lappend gates [list $inst $p {*}$gate_models($master)] + set latched($inst) 0 + } + foreach id [dict values $ids] { + if { ![info exists value($id)] } { + set value($id) 0 + } + } + order +} + +# An escaped Verilog identifier (\\name followed by a space) as a name. +proc ram_sim::unescape { s } { + return [string trim [regsub {^\\} [string trim $s] {}]] +} + +# The id of a wrapper net, new unless the net is joined to a core port. +proc ram_sim::wrapper_net { ids_var name } { + upvar $ids_var ids + if { ![dict exists $ids $name] } { + dict set ids $name [fresh "w:$name"] + } + return [dict get $ids $name] +} + +proc ram_sim::fresh { name } { + variable net_ids + if { ![info exists net_ids($name)] } { + set net_ids($name) [array size net_ids] + } + return $net_ids($name) +} + +# The most cells matching `pattern` (glob, by master) on any path from a +# flop or port to `net`: a decoder's depth in AND2s, say. +proc ram_sim::depth { pattern } { + variable comb_order + set d [dict create] + set most 0 + foreach entry $comb_order { + lassign $entry net drivers + set here 0 + foreach c $drivers { + lassign $c - - - inputs master + set in 0 + foreach i $inputs { + if { [dict exists $d $i] } { + set in [expr { max($in, [dict get $d $i]) }] + } + } + set here [expr { max($here, $in + [string match $pattern $master]) }] + } + dict set d $net $here + set most [expr { max($most, $here) }] + } + return $most +} + +# Storage flops by word: {row word} -> flops, from their names +# (storage___.bit). +proc ram_sim::words_of_flops { } { + variable flops + set out [dict create] + foreach f $flops { + set inst [lindex $f 0] + if { [regexp {^storage_(\d+)_(\d+)_\d+\.} [$inst getName] - row word] } { + dict lappend out [list $row $word] $inst + } + } + return $out +} diff --git a/src/ram/test/register_read_address_asap7.tcl b/src/ram/test/register_read_address_asap7.tcl new file mode 100644 index 00000000000..435cda96c7e --- /dev/null +++ b/src/ram/test/register_read_address_asap7.tcl @@ -0,0 +1,32 @@ +# -register_read_address 1: a read's data follows a clock after its address, and +# does not follow an address change before that clock. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -register_read_address 1 +routed::check + +ram_sim::load +set mem [ram_sim::exercise { + words 8 bits 8 latency 1 cycles 100 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +}] +# the address registered: a new address without a clock changes nothing +ram_sim::set_bus addr_r0 3 0 +ram_sim::cycle clk +set before [ram_sim::get_bus Q0 8] +ram_sim::set_bus addr_r0 3 5 +ram_sim::settle +ram_sim::check { [ram_sim::get_bus Q0 8] == $before } \ + "Q0 followed its address before the clock" +ram_sim::cycle clk +ram_sim::check { [ram_sim::get_bus Q0 8] == [lindex $mem 5] } \ + "Q0 does not read word 5 a clock after its address" + +puts "pass" diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index be787532f2c..cdf0c6462e9 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -5,5 +5,23 @@ record_tests { make_8x8_mux4_sky130 make_8x8_2r1w_sky130 make_7x7_2r1w_nangate45 + write_ports_nangate45 + and_or_read_asap7 + zero_word_asap7 + reset_asap7 + reset_nangate45 + banks_asap7 + banked_read_ports_asap7 + register_read_address_asap7 + write_mux_asap7 + periphery_unplaced_asap7 + cells_name_asap7 + decoder_word_asap7 + bit_folds_asap7 + periphery_verilog_asap7 + write_priority_or_asap7 + service_columns_asap7 + bank_column_layout_asap7 + option_errors_asap7 make_8x8_latch_sky130 } diff --git a/src/ram/test/reset_asap7.tcl b/src/ram/test/reset_asap7.tcl new file mode 100644 index 00000000000..32e6a96fa22 --- /dev/null +++ b/src/ram/test/reset_asap7.tcl @@ -0,0 +1,35 @@ +# -reset_value 0xa5: an asynchronous reset at the storage flop's own +# polarity (rst_n for asap7's DFFASRHQNx1) sets every word to 0xa5, and +# the words are written and read as usual after it. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFASRHQNx1_ASAP7_75t_R -reset_value 0xa5 +routed::check + +ram_sim::load +# scramble the flops, then reset +foreach f $ram_sim::flops { + set ram_sim::state([lindex $f 0]) [expr { int(rand() * 2) }] +} +ram_sim::set_port rst_n 0 +ram_sim::cycle clk +for { set a 0 } { $a < 8 } { incr a } { + ram_sim::set_bus addr_r0 3 $a + ram_sim::settle + set q [ram_sim::get_bus Q0 8] + ram_sim::check { $q == 0xa5 } "word $a reads $q after reset, not 0xa5" +} +ram_sim::set_port rst_n 1 +ram_sim::exercise { + words 8 bits 8 mem {165 165 165 165 165 165 165 165} + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/reset_nangate45.tcl b/src/ram/test/reset_nangate45.tcl new file mode 100644 index 00000000000..06dcd420e21 --- /dev/null +++ b/src/ram/test/reset_nangate45.tcl @@ -0,0 +1,34 @@ +# -reset_value on Nangate45 with no -storage_cell: generate_ram picks a +# flop with an asynchronous clear and preset, and a plain flop for the +# registered read address, though every flop Nangate45 has drives both +# Q and QN. Every word reads 0x5a after reset. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_nangate45 + +generate_ram {*}$routed::nangate45 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 1 -w_ports 1 \ + -reset_value 0x5a -register_read_address 1 +routed::check + +ram_sim::load +foreach f $ram_sim::flops { + set ram_sim::state([lindex $f 0]) [expr { int(rand() * 2) }] +} +ram_sim::set_port rst_n 0 +for { set a 0 } { $a < 8 } { incr a } { + ram_sim::set_bus addr_r0 3 $a + ram_sim::cycle clk + set q [ram_sim::get_bus Q 8] + ram_sim::check { $q == 0x5a } "word $a reads $q after reset, not 0x5a" +} +ram_sim::set_port rst_n 1 +ram_sim::exercise { + words 8 bits 8 latency 1 mem {90 90 90 90 90 90 90 90} + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q}} +} + +puts "pass" diff --git a/src/ram/test/routed.tcl b/src/ram/test/routed.tcl new file mode 100644 index 00000000000..f44228d2c2f --- /dev/null +++ b/src/ram/test/routed.tcl @@ -0,0 +1,79 @@ +# generate_ram's arguments for a routed block on the tests' technologies, +# and the check that the block came out routed. + +namespace eval routed { +# asap7: the libraries the asap7 tests read, and its strap widths, which +# must be ones the technology allows. The technology LEF's M2 track offset +# is negative, so the tracks come from the file asap7's flows use. +proc read_asap7 { } { + read_liberty ../../../test/asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz + read_liberty ../../../test/asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz + read_liberty ../../../test/asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz + read_liberty ../../../test/asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib + read_liberty ../../../test/asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz + + read_lef ../../../test/asap7/asap7_tech_1x_201209.lef + read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef +} + +variable asap7 { + -routing_layer {M1 0.018} + -ver_layer {M3 0.090 2.16} + -hor_layer {M4 0.120 1.08} + -filler_cells {FILLERxp5_ASAP7_75t_R FILLER_ASAP7_75t_R} + -tapcell TAPCELL_ASAP7_75t_R + -max_tap_dist 25 + -tracks ../../../test/asap7/asap7.tracks.tcl +} + +proc read_nangate45 { } { + read_liberty Nangate45/Nangate45_typ.lib + read_lef Nangate45/Nangate45_tech.lef + read_lef Nangate45/Nangate45.lef +} + +variable nangate45 { + -routing_layer {metal1 0.08} + -ver_layer {metal4 0.14 9} + -hor_layer {metal3 0.08 8} + -filler_cells {FILLCELL_X1 FILLCELL_X2 FILLCELL_X4 FILLCELL_X8} + -tapcell TAPCELL_X1 + -max_tap_dist 10 +} + +# Every signal net with two or more pins has a wire, detailed routing +# left no violations, and no cell has a signal input left open. +proc check { } { + set block [ord::get_db_block] + foreach inst [$block getInsts] { + foreach iterm [$inst getITerms] { + if { [$iterm getNet] == "NULL" && [$iterm isInputSignal] } { + error "[$inst getName]/[[$iterm getMTerm] getName] is not connected" + } + } + } + set nets 0 + foreach net [$block getNets] { + if { [$net getSigType] != "SIGNAL" } { + continue + } + if { [llength [$net getITerms]] + [llength [$net getBTerms]] < 2 } { + continue + } + incr nets + if { [$net getWire] == "NULL" } { + error "net [$net getName] is not routed" + } + } + if { $nets == 0 } { + error "no signal nets to route" + } + set drc [$block findMarkerCategory "DRC"] + if { $drc == "NULL" } { + error "no DRC markers: detailed routing did not run" + } + if { [$drc getMarkerCount] > 0 } { + error "[$drc getMarkerCount] DRC violations" + } +} +} diff --git a/src/ram/test/service_columns_asap7.tcl b/src/ram/test/service_columns_asap7.tcl new file mode 100644 index 00000000000..8199529ab3b --- /dev/null +++ b/src/ram/test/service_columns_asap7.tcl @@ -0,0 +1,45 @@ +# -service_columns {4 10}: an empty column 10 sites wide after every 4 +# storage columns (two in an 8-bit word with its select column), every +# row clear there. + +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -service_columns {4 10} +routed::check + +set block [ord::get_db_block] +set site [[lindex [[lindex [[ord::get_db] getLibs] 0] getSites] 0] getWidth] +set spans {} +# fillers and taps fill the routed block's empty columns +foreach inst [$block getInsts] { + set type [[$inst getMaster] getType] + if { [$inst isPlaced] && $type != "CORE_SPACER" && $type != "CORE_WELLTAP" } { + set b [$inst getBBox] + lappend spans [list [$b xMin] [$b xMax]] + } +} +set gaps {} +set edge [[$block getDieArea] xMin] +foreach s [lsort -integer -index 0 $spans] { + lassign $s a b + if { $a - $edge >= 10 * $site } { + lappend gaps [expr { ($a - $edge) / $site }] + } + set edge [expr { max($edge, $b) }] +} +ram_sim::check { [llength $gaps] == 2 && [lsort -unique $gaps] eq {10} } \ + "empty columns of $gaps sites, not two of 10" +ram_sim::load +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/write_mux_asap7.tcl b/src/ram/test/write_mux_asap7.tcl new file mode 100644 index 00000000000..b90b728df50 --- /dev/null +++ b/src/ram/test/write_mux_asap7.tcl @@ -0,0 +1,28 @@ +# -write_style mux: no clock gate; every storage flop is on the clock, and +# a word not written keeps its value through a mux per bit. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -write_style mux +routed::check + +ram_sim::load +ram_sim::check { [llength $ram_sim::gates] == 0 } "a clock gate is left" +set clk [ram_sim::net_of_port clk] +foreach f $ram_sim::flops { + set pins [lindex $f 1] + ram_sim::check { [dict get $pins CLK] == $clk } \ + "[[lindex $f 0] getName] is not on the clock" +} +ram_sim::exercise { + words 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/write_ports_nangate45.tcl b/src/ram/test/write_ports_nangate45.tcl new file mode 100644 index 00000000000..d26ebc02c02 --- /dev/null +++ b/src/ram/test/write_ports_nangate45.tcl @@ -0,0 +1,27 @@ +# generate_ram with two write ports: each port writes the word it +# addresses, both may write in one cycle, and where both write the same +# word the last port's data is kept. Checked by simulating the block. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_nangate45 + +generate_ram {*}$routed::nangate45 \ + -mask_size 7 \ + -word_size 7 \ + -num_words 7 \ + -r_ports 1 \ + -w_ports 2 \ + -storage_cell DFF_X1 +routed::check + +ram_sim::load +ram_sim::exercise { + words 7 bits 7 + writes {{addr_w0 D0 {we0[0]}} {addr_w1 D1 {we1[0]}}} + reads {{addr_r0 Q}} + cycles 300 +} + +puts "pass" diff --git a/src/ram/test/write_priority_or_asap7.tcl b/src/ram/test/write_priority_or_asap7.tcl new file mode 100644 index 00000000000..42dbe97d1d7 --- /dev/null +++ b/src/ram/test/write_priority_or_asap7.tcl @@ -0,0 +1,21 @@ +# -write_priority or: where both write ports write a word in one cycle it +# takes the OR of their data. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 2 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -write_style mux -write_priority or +routed::check + +ram_sim::load +ram_sim::exercise { + words 8 bits 8 priority or cycles 300 + writes {{addr_w0 D0 {we0[0]}} {addr_w1 D1 {we1[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/zero_word_asap7.tcl b/src/ram/test/zero_word_asap7.tcl new file mode 100644 index 00000000000..6374f553593 --- /dev/null +++ b/src/ram/test/zero_word_asap7.tcl @@ -0,0 +1,23 @@ +# -zero_word 0: word 0 reads 0 whatever is written to it, as a RISC-V +# register file's x0, and holds no flops; every other word reads back. +source "helpers.tcl" +source "ram_sim.tcl" +source "routed.tcl" + +routed::read_asap7 + +generate_ram {*}$routed::asap7 \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -zero_word 0 +routed::check + +ram_sim::load +ram_sim::check { [llength $ram_sim::flops] == 7 * 8 } \ + "[llength $ram_sim::flops] storage flops, not 7 words of 8" +ram_sim::exercise { + words 8 bits 8 zero_word 0 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/test/BUILD b/test/BUILD index b7637bd3e8a..0496466246c 100644 --- a/test/BUILD +++ b/test/BUILD @@ -97,12 +97,27 @@ filegroup( filegroup( name = "asap7_data", srcs = [ + "asap7/asap7.tracks.tcl", "asap7/asap7_tech_1x_201209.lef", "asap7/asap7sc7p5t_28_R_1x_220121a.lef", ], visibility = ["//visibility:public"], ) +# asap7's NLDM liberty, for the cells' functions: the ram module's asap7 +# tests find their cells by them. +filegroup( + name = "asap7_nldm_data", + srcs = [ + "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", + ], + visibility = ["//visibility:public"], +) + # CCS (current source model) liberty. Required by delay calculators that need # output current waveforms, e.g. prima; the asap7 libs above are NLDM only. # Holds four cells: AND2x2, BUFx2, DFFHQx4, INVx2.