From 59b546cc1b7f6a89dc27446b47281f930f910fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 01/43] ram: generate_ram with more than one write port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_ram took exactly one read/write or one write port (RAM-0030). It now takes any number of write ports: each has its own address decoder, data bus (D0, D1, ...), write enables (we0, we1, ...) and address (addr_w0, addr_w1, ...); one write port keeps the names it had. Per word, port w writes when its row select and write enable are high; a word written by two ports in a cycle keeps the last one's data, as the behavioral model does; the word's clock gate opens for any of them. Per bit an AO22 per pair of ports, and an OR2 across pairs, picks the data. The AO22 is found by its truth table, as the AOI22 is, through findMaster4, which both now share; the OR2 by its function. Not yet with -use_latch, whose data passes a latch per column, or with -column_mux_ratio above 1, whose word select comes from the first port's address (RAM-0054, RAM-0055). Tested: src/ram/test/write_ports_nangate45.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 4 +- src/ram/include/ram/ram.h | 33 +- src/ram/src/ram.cpp | 623 ++++++++++++++++-------- src/ram/src/ram.tcl | 18 +- src/ram/test/BUILD | 7 + src/ram/test/CMakeLists.txt | 2 + src/ram/test/ram_sim.tcl | 650 +++++++++++++++++++++++++ src/ram/test/regression_tests.tcl | 1 + src/ram/test/write_ports_nangate45.tcl | 27 + 9 files changed, 1148 insertions(+), 217 deletions(-) create mode 100644 src/ram/test/ram_sim.tcl create mode 100644 src/ram/test/write_ports_nangate45.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 94c5df30369..dc03e5b6cb0 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -62,9 +62,9 @@ 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. 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. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 1a41ba0d7ba..3b9844cbae1 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -135,8 +136,19 @@ class RamGen private: void findMasters(); std::map buildPortMap(odb::dbMaster*); + // The smallest cell `match` accepts; none is an error unless not + // `required`, then nullptr. odb::dbMaster* findMaster(const std::function& match, - const char* name); + const char* name, + bool required = true); + // 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::dbNet* makeNet(const std::string& prefix, const std::string& name); odb::dbInst* makeInst( Cell* cell, @@ -159,12 +171,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 +189,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); @@ -229,6 +241,11 @@ 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}; odb::dbMaster* latch_cell_{nullptr}; odb::dbMaster* tapcell_{nullptr}; @@ -242,15 +259,19 @@ class RamGen std::map clock_gate_ports_; std::map buffer_ports_; std::map latch_ports_; + std::map or2_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 }; } // namespace ram diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 2d2c5378007..4299d906a93 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -169,14 +170,16 @@ 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); @@ -193,6 +196,57 @@ void RamGen::makeSlice(const int slice_idx, auto gclock_net = makeNet(prefix, "gclock"); auto we_net = makeNet(prefix, "we"); + auto sel_cell = std::make_unique(); + + // 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. The clock gate opens for + // any of them. + vector take(num_write); + 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) { + 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; + } + } for (int local_bit = 0; local_bit < mask_size; ++local_bit) { auto name = fmt::format("{}.bit{}", prefix, start_bit_idx + local_bit); @@ -201,19 +255,77 @@ void RamGen::makeSlice(const int slice_idx, outs[r_port] = data_output[r_port][local_bit]; } + dbNet* d = data_input[0][local_bit]; + if (num_write > 1) { + d = makeNet(name, "d"); + } + auto bit_cell = makeBit(name, r_total, gclock_net, select_b_nets, d, outs); + 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 + : 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 : 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; + } + } + 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); + ram_grid_.addCell(std::move(bit_cell), bit_col); } - auto sel_cell = std::make_unique(); - // Make clock gate makeInst(sel_cell.get(), prefix, @@ -226,8 +338,8 @@ void RamGen::makeSlice(const int slice_idx, // 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,13 +353,15 @@ 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) { + 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) { @@ -256,7 +370,7 @@ void RamGen::makeSlice(const int slice_idx, 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]}}); } } @@ -283,18 +397,24 @@ 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][slice]->getNet()); + } std::vector> slice_outputs; slice_outputs.reserve(r_total); for (int port = 0; port < r_total; ++port) { @@ -311,7 +431,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, @@ -478,7 +598,8 @@ 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* best = nullptr; float best_area = std::numeric_limits::max(); @@ -531,6 +652,9 @@ 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()); @@ -780,141 +904,176 @@ void RamGen::findMasters() // 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]; + std::array in; + aoi22_cell_ = findMaster4(0x0777, "aoi22", in, aoi22_out_); + aoi22_in_a1_ = in[0]; + aoi22_in_a2_ = in[1]; + aoi22_in_b1_ = in[2]; + aoi22_in_b2_ = in[3]; + } + + // 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) { + if (!ao22_cell_) { + // AO22 truth table: Y = A1&A2 | B1&B2 + ao22_cell_ = findMaster4( + 0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false); + } + if (!or2_cell_) { + or2_cell_ = findMaster( + [](sta::LibertyPort* port) { + if (!port->direction()->isOutput()) { + return false; } + 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"); + } + or2_ports_ = buildPortMap(or2_cell_); + } +} + +dbMaster* RamGen::findMaster4(const uint16_t table, + const char* name, + std::array& inputs_out, + std::string& output, + const bool required) +{ + // 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. + + // 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; + 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, + // 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; + 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) + }; + + return findMaster( + [&](sta::LibertyPort* out_port) -> bool { + if (!out_port->direction()->isOutput()) { + return false; } - if (evalFunc(func, inputs, vals)) { - table |= (1 << k); + auto func = out_port->function(); + if (!func) { + return false; } - } - 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) - }; - - aoi22_cell_ = findMaster( - [&](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(); - - // 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()); - } + auto cell = out_port->libertyCell(); + + // 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()); } + } - // aoi22 must have exactly 4 inputs - if (inputs.size() != 4) { - return false; - } + // 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) == 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; + // 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; - }, - "aoi22"); - } + } + return false; + }, + name, + required); } void RamGen::ramPdngen(const char* power_net_name, @@ -1095,7 +1254,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); } } @@ -1201,6 +1363,7 @@ void RamGen::generate(const int mask_size, logger_->info(RAM, 3, "Generating {}", ram_name); use_latch_ = use_latch; + num_write_ = rw_ports + w_ports; storage_cell_ = storage_cell; tristate_cell_ = tristate_cell; inv_cell_ = inv_cell; @@ -1209,6 +1372,8 @@ void RamGen::generate(const int mask_size, clock_gate_cell_ = nullptr; buffer_cell_ = nullptr; aoi22_cell_ = nullptr; + ao22_cell_ = nullptr; + or2_cell_ = nullptr; latch_cell_ = nullptr; if (use_latch_ && storage_cell_) { @@ -1270,10 +1435,17 @@ void RamGen::generate(const int mask_size, auto clock = makeBTerm("clk", dbIoType::INPUT); - 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); + // 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); + for (int slice = 0; slice < 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 @@ -1311,7 +1483,7 @@ 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 { port_prefix = fmt::format("addr_r{}", r_idx++); @@ -1370,11 +1542,18 @@ void RamGen::generate(const int mask_size, // start of input/output net creation int r_total = rw_ports + r_ports; 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)); + 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) { @@ -1451,14 +1630,13 @@ 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) { + // 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] = write_select_nets[p][row]; } - // 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] = read_select_nets[p][row]; } vector> word_output_nets(r_total); for (int port = 0; port < r_total; ++port) { @@ -1481,7 +1659,7 @@ void RamGen::generate(const int mask_size, row_selects, shared_select_b_nets, (word_idx == 0 || column_mux_ratio == 1), - use_latch_ ? mid_nets : D_nets, + use_latch_ ? vector>{mid_nets} : D_nets, word_output_nets); } } @@ -1618,16 +1796,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); @@ -1648,13 +1826,16 @@ void RamGen::generate(const int mask_size, 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]}}); + 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]}}); + } 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); @@ -1936,8 +2117,16 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, std::string module_name = fmt::format("RAM{}x{}", num_words, word_size_bit); 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"; + 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 +2137,17 @@ 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"; + for (int w = 0; w < num_write; ++w) { + port_list += ",\n " + wbus("we", w); + } // Build output declarations std::string output_declaration; @@ -1973,9 +2166,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 @@ -2005,26 +2200,45 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, addr_name); } - std::string write_addr = (rw_ports > 0) ? "addr_rw" : "addr_w"; + // Write logic, the ports in order: where two write the same word in a + // cycle, the last one's data is kept. + 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); + 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*{} +:{}] <= {}[i*{} +:{}]; + end + end +)", + slices_per_word, + wbus("we", w), + write_addr, + mask_size, + mask_size, + wbus("D", w), + mask_size, + mask_size); + } 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 +2246,13 @@ endmodule )", module_name, port_list, - word_size_bit - 1, + 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, + write_logic, read_port_logic); std::ofstream vf(filename); diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 4e58c501636..98f5e607ade 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -87,8 +87,12 @@ 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 > 1 || ($rw_ports > 0 && $w_ports > 0) } { + utl::error RAM 53 "A read/write port is the only write port." } if { $rw_ports + $r_ports < 1 } { @@ -100,6 +104,16 @@ proc generate_ram_netlist { args } { set use_latch $keys(-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 } { + utl::error RAM 54 "More than one write port is not supported with -use_latch." + } + if { $w_ports > 1 && $column_mux_ratio > 1 } { + utl::error RAM 55 "More than one write port is not supported with\ + -column_mux_ratio above 1." + } + set tapcell "" set max_tap_dist 0 if { [info exists keys(-tapcell)] } { diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 071fa2c0f0b..f8ddc273065 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -13,6 +13,7 @@ COMPULSORY_TESTS = [ ] PASSFAIL_TESTS = [ + "write_ports_nangate45", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -24,6 +25,7 @@ filegroup( # per test glob patterns below. srcs = [ "helpers.tcl", + "ram_sim.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 +53,11 @@ filegroup( test_name + ".*", ], ) + { + "write_ports_nangate45": [ + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_tech.lef", + "Nangate45/Nangate45_typ.lib", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 8545b3dbca9..4b3db6c16a0 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -5,5 +5,7 @@ or_integration_tests( # golden .defok matches the clang build used by bazel, not the gcc # build typically used by cmake. See src/ram/test/BUILD. make_7x7_nangate45 + PASSFAIL_TESTS + write_ports_nangate45 ) diff --git a/src/ram/test/ram_sim.tcl b/src/ram/test/ram_sim.tcl new file mode 100644 index 00000000000..c8de35dc035 --- /dev/null +++ b/src/ram/test/ram_sim.tcl @@ -0,0 +1,650 @@ +# 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` a single-bit enable +# 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 + set_port $we $e + if { $e } { + lappend written $a $d + } + } + cycle clk + foreach port [dict get $o writes] { + set_port [lindex $port 2] 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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index be787532f2c..a0cf7715d21 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -5,5 +5,6 @@ record_tests { make_8x8_mux4_sky130 make_8x8_2r1w_sky130 make_7x7_2r1w_nangate45 + write_ports_nangate45 make_8x8_latch_sky130 } diff --git a/src/ram/test/write_ports_nangate45.tcl b/src/ram/test/write_ports_nangate45.tcl new file mode 100644 index 00000000000..db2ddde1282 --- /dev/null +++ b/src/ram/test/write_ports_nangate45.tcl @@ -0,0 +1,27 @@ +# generate_ram_netlist 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" + +read_liberty Nangate45/Nangate45_typ.lib +read_lef Nangate45/Nangate45_tech.lef +read_lef Nangate45/Nangate45.lef + +generate_ram_netlist \ + -mask_size 7 \ + -word_size 7 \ + -num_words 7 \ + -r_ports 1 \ + -w_ports 2 \ + -storage_cell DFF_X1 + +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" From bf11c6f22ba0472fc9fe4025cd851a22c025f069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 02/43] ram: generate_ram reads through AND-OR trees without tristates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_ram's read path is a tristate bitline per bit, and a library without a tristate cell (asap7) could not generate at all (RAM-0010). -read_style and_or reads through a tree per bit column instead: an AOI22 over each pair of rows, then NAND2 and NOR2 levels by turns, each node placed in a row of the span it covers, beside its inputs. It is the default where the library has no tristate cell (RAM-0057 says so), and can be asked for where it has one; -read_style tristate insists on the bitlines. A storage flop whose output is the data inverted is taken on the AND-OR path, an inverter at the root of each tree undoing it; the tristate path refuses it (RAM-0058). buildPortMap now reckons a flop's output polarity from both the state variable a pin reads and its next_state: asap7's DFFHQNx1 stores !D in IQN and outputs IQN on QN, which it took for a true output. Cells with next_state "D" are as before. NAND2 and NOR2 are found by truth table (asap7 writes NAND2 as (!A) + (!B)), through the evaluator findMaster4 used, now shared. Tested: src/ram/test/and_or_read_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 49 +++- src/ram/src/ram.cpp | 390 ++++++++++++++++++++++++----- src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 23 +- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/and_or_read_asap7.tcl | 28 +++ src/ram/test/regression_tests.tcl | 1 + test/BUILD | 14 ++ 10 files changed, 443 insertions(+), 75 deletions(-) create mode 100644 src/ram/test/and_or_read_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index dc03e5b6cb0..b289c8104b4 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -43,6 +43,7 @@ generate_ram [-mask_size bits] [-column_mux_ratio ratio] [-storage_cell name] [-tristate_cell name] + [-read_style tristate|and_or] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -68,6 +69,7 @@ generate_ram [-mask_size bits] | `-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` | 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. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 3b9844cbae1..db535732f09 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -108,7 +109,8 @@ 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 = ""); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -135,7 +137,33 @@ 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 + }; + TreeEnd planReadTree(const std::string& prefix, + int word_idx, + int bit, + int lo, + int hi, + const std::vector& selects, + odb::dbNet* out); + bool treeInverted(int lo, int hi) const; // The smallest cell `match` accepts; none is an error unless not // `required`, then nullptr. odb::dbMaster* findMaster(const std::function& match, @@ -161,7 +189,8 @@ 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); void makeSlice(int slice_idx, int mask_size, int row_idx, @@ -246,6 +275,9 @@ class RamGen // 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}; @@ -260,6 +292,8 @@ class RamGen 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_; // every write port's data @@ -272,6 +306,15 @@ class RamGen 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 + // 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/ram.cpp b/src/ram/src/ram.cpp index 4299d906a93..73aa57fd264 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -115,11 +115,15 @@ 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) { 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(), @@ -154,6 +158,105 @@ 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); +} + +RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, + const int word_idx, + const int bit, + const int lo, + const int hi, + const vector& selects, + dbNet* out) +{ + auto add = [&](const int row, + dbMaster* master, + const std::string& name, + const vector>& conns) { + tree_nodes_[{row, word_idx, bit}].push_back({master, prefix, name, conns}); + }; + auto q + = [&](const int row) { return storage_nets_.at({row, word_idx, bit}); }; + auto net = [&](dbNet* given) { + return given ? given : makeNet(prefix, fmt::format("t{}_{}", lo, hi)); + }; + const int n = hi - lo; + if (n == 1) { + dbNet* y = net(out); + add(lo, + nand2_cell_, + fmt::format("nand_r{}", lo), + {{nand2_ports_[{PortRoleType::DataIn, 0}], selects[lo]}, + {nand2_ports_[{PortRoleType::DataIn, 1}], q(lo)}, + {nand2_ports_[{PortRoleType::DataOut, 0}], y}}); + return {y, true}; + } + if (n == 2) { + dbNet* y = net(out); + add(lo, + aoi22_cell_, + fmt::format("aoi_r{}", lo), + {{aoi22_in_a1_, selects[lo]}, + {aoi22_in_a2_, q(lo)}, + {aoi22_in_b1_, selects[lo + 1]}, + {aoi22_in_b2_, q(lo + 1)}, + {aoi22_out_, y}}); + return {y, true}; + } + int half = 1; + while (half * 2 < n) { + half *= 2; + } + const int mid = lo + half; + const int host = mid - 1; + const TreeEnd a + = planReadTree(prefix, word_idx, bit, lo, mid, selects, nullptr); + TreeEnd b = planReadTree(prefix, word_idx, bit, mid, hi, selects, nullptr); + if (b.inverted != a.inverted) { + dbNet* nb = makeNet(prefix, fmt::format("t{}_{}_inv", mid, hi)); + add(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 @@ -186,7 +289,9 @@ void RamGen::makeSlice(const int slice_idx, int r_total = rw_ports + r_ports; vector select_b_nets(r_total); - if (column_mux_ratio == 1) { + if (and_or_read_) { + // the AND-OR trees read the true selects + } else if (column_mux_ratio == 1) { for (int i = 0; i < r_total; ++i) { select_b_nets[i] = makeNet(prefix, fmt::format("select{}_b", i)); } @@ -259,7 +364,23 @@ void RamGen::makeSlice(const int slice_idx, if (num_write > 1) { d = makeNet(name, "d"); } - auto bit_cell = makeBit(name, r_total, gclock_net, select_b_nets, d, outs); + const int bit = start_bit_idx + local_bit; + auto bit_cell + = and_or_read_ + ? makeBit(name, + 0, + gclock_net, + select_b_nets, + d, + outs, + storage_nets_.at({row_idx, word_idx, bit})) + : makeBit(name, r_total, gclock_net, select_b_nets, d, outs); + 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; @@ -364,7 +485,7 @@ void RamGen::makeSlice(const int slice_idx, } // Make select inverters - if (create_select_inv) { + if (create_select_inv && !and_or_read_) { for (int i = 0; i < r_total; ++i) { makeInst(sel_cell.get(), prefix, @@ -661,8 +782,13 @@ dbMaster* RamGen::findMaster( 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; @@ -710,8 +836,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; } } } @@ -720,6 +851,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 @@ -730,6 +863,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()) { @@ -780,6 +921,75 @@ std::map RamGen::buildPortMap(dbMaster* master) return pin_map; } +// 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 (!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; + } +} + +// 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; + } + 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; +} + void RamGen::findMasters() { if (!inv_cell_) { @@ -791,18 +1001,30 @@ void RamGen::findMasters() } inv_ports_ = buildPortMap(inv_cell_); - 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 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; + } 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."); + } } - tristate_ports_ = buildPortMap(tristate_cell_); if (!and2_cell_) { and2_cell_ = findMaster( @@ -844,7 +1066,18 @@ void RamGen::findMasters() }, "storage"); } - storage_ports_ = buildPortMap(storage_cell_); + // 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 (!clock_gate_cell_) { @@ -912,6 +1145,23 @@ void RamGen::findMasters() aoi22_in_b2_ = in[3]; } + // 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) + if (!nand2_cell_) { + nand2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x7; }, + "nand2"); + } + nand2_ports_ = buildPortMap(nand2_cell_); + if (!nor2_cell_) { + nor2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x1; }, + "nor2"); + } + nor2_ports_ = buildPortMap(nor2_cell_); + } + // 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) { @@ -946,56 +1196,12 @@ dbMaster* RamGen::findMaster4(const uint16_t table, // 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. - // 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 { + = [](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); @@ -1317,7 +1523,8 @@ void RamGen::generate(const int mask_size, dbMaster* tristate_cell, dbMaster* inv_cell, dbMaster* tapcell, - int max_tap_dist) + int max_tap_dist, + const std::string& read_style) { if (num_words / column_mux_ratio == 1) { logger_->error(RAM, 42, "1-row RAMs are not supported."); @@ -1364,6 +1571,11 @@ void RamGen::generate(const int mask_size, use_latch_ = use_latch; num_write_ = rw_ports + w_ports; + read_style_ = read_style; + and_or_read_ = false; + storage_inverted_ = false; + storage_nets_.clear(); + tree_nodes_.clear(); storage_cell_ = storage_cell; tristate_cell_ = tristate_cell; inv_cell_ = inv_cell; @@ -1374,6 +1586,8 @@ void RamGen::generate(const int mask_size, aoi22_cell_ = nullptr; ao22_cell_ = nullptr; or2_cell_ = nullptr; + nand2_cell_ = nullptr; + nor2_cell_ = nullptr; latch_cell_ = nullptr; if (use_latch_ && storage_cell_) { @@ -1619,6 +1833,48 @@ 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. + // 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_) { + for (int row = 0; row < num_rows; ++row) { + for (int w = 0; w < column_mux_ratio; ++w) { + for (int bit = 0; bit < word_size; ++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"); + } + } + } + const bool flip = treeInverted(0, num_rows) != storage_inverted_; + 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) { + const std::string prefix + = fmt::format("read_w{}_p{}_b{}", w, port, bit); + dbNet* out = word_q_nets[w][port][bit]; + const TreeEnd root = planReadTree(prefix, + w, + bit, + 0, + num_rows, + read_select_nets[port], + flip ? nullptr : out); + if (flip) { + tree_nodes_[{num_rows - 1, w, 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); diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 8b225fcc890..bb90a0c4348 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -41,7 +41,8 @@ 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) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -93,7 +94,7 @@ 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); } 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 98f5e607ade..8ae362eb9e7 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -7,6 +7,7 @@ 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] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -19,7 +20,7 @@ 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 } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -133,8 +134,19 @@ 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." + } + } + 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 } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -146,6 +158,7 @@ sta::define_cmd_args "generate_ram" {[-mask_size bits] [-column_mux_ratio ratio] [-storage_cell name] [-tristate_cell name] + [-read_style tristate|and_or] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -167,7 +180,7 @@ 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 } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -204,6 +217,10 @@ 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(-inv_cell)] } { lappend ram_netlist_args -inv_cell $keys(-inv_cell) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index f8ddc273065..bd95a9a46b1 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -14,6 +14,7 @@ COMPULSORY_TESTS = [ PASSFAIL_TESTS = [ "write_ports_nangate45", + "and_or_read_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -58,6 +59,10 @@ filegroup( "Nangate45/Nangate45_tech.lef", "Nangate45/Nangate45_typ.lib", ], + "and_or_read_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 4b3db6c16a0..aa99c13cb06 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -7,5 +7,6 @@ or_integration_tests( make_7x7_nangate45 PASSFAIL_TESTS write_ports_nangate45 + and_or_read_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..9b163f21d20 --- /dev/null +++ b/src/ram/test/and_or_read_asap7.tcl @@ -0,0 +1,28 @@ +# generate_ram_netlist 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" + +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 + +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index a0cf7715d21..a84a4a0412d 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -6,5 +6,6 @@ record_tests { make_8x8_2r1w_sky130 make_7x7_2r1w_nangate45 write_ports_nangate45 + and_or_read_asap7 make_8x8_latch_sky130 } diff --git a/test/BUILD b/test/BUILD index 66146170ea0..01eac4066a4 100644 --- a/test/BUILD +++ b/test/BUILD @@ -103,6 +103,20 @@ filegroup( 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. From cf7e10eef40544ed7ad81ee03b7daf31e543f359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 03/43] ram: generate_ram -zero_word, a word that reads 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -zero_word n makes word n read 0 and drop its writes, as a RISC-V register file's x0: its row keeps its place in the grid but has no flops and no clock gate, and the AND-OR read trees are built over the other rows; where the storage flop's output is inverted the zero word stays in them, its term its own select, so the inverter at the root reads it as 0. The behavioral model reads 0 there and ignores writes. Needs -read_style and_or and -column_mux_ratio 1 (RAM-0060). Tested: src/ram/test/zero_word_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 6 +- src/ram/src/ram.cpp | 145 +++++++++++++++++++++++------- src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 17 +++- src/ram/test/BUILD | 5 ++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/zero_word_asap7.tcl | 28 ++++++ 9 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 src/ram/test/zero_word_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index b289c8104b4..6702d6699a1 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -44,6 +44,7 @@ generate_ram [-mask_size bits] [-storage_cell name] [-tristate_cell name] [-read_style tristate|and_or] + [-zero_word word] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -70,6 +71,7 @@ generate_ram [-mask_size bits] | `-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` | 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` | 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. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index db535732f09..e7fc10c1226 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -110,7 +110,8 @@ class RamGen odb::dbMaster* inv_cell, odb::dbMaster* tapcell, int max_tap_dist, - const std::string& read_style = ""); + const std::string& read_style = "", + int zero_word = -1); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -156,9 +157,11 @@ class RamGen odb::dbNet* net; bool inverted; // the net is the complement of the OR of the terms }; + // the tree over rows[lo, hi): rows lists the rows that store a word TreeEnd planReadTree(const std::string& prefix, int word_idx, int bit, + const std::vector& rows, int lo, int hi, const std::vector& selects, @@ -311,6 +314,7 @@ class RamGen 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 // 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_; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 73aa57fd264..173cd465c76 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -181,6 +181,7 @@ bool RamGen::treeInverted(const int lo, const int hi) const RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, const int word_idx, const int bit, + const vector& rows, const int lo, const int hi, const vector& selects, @@ -192,31 +193,39 @@ RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, const vector>& conns) { tree_nodes_[{row, word_idx, bit}].push_back({master, prefix, name, conns}); }; - auto q - = [&](const int row) { return storage_nets_.at({row, word_idx, bit}); }; + // 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 int row) { + return row == zero_word_ ? selects[row] + : storage_nets_.at({row, word_idx, bit}); + }; auto net = [&](dbNet* given) { return given ? given : makeNet(prefix, fmt::format("t{}_{}", lo, hi)); }; const int n = hi - lo; if (n == 1) { + const int r = rows[lo]; dbNet* y = net(out); - add(lo, + add(r, nand2_cell_, - fmt::format("nand_r{}", lo), - {{nand2_ports_[{PortRoleType::DataIn, 0}], selects[lo]}, - {nand2_ports_[{PortRoleType::DataIn, 1}], q(lo)}, + fmt::format("nand_r{}", r), + {{nand2_ports_[{PortRoleType::DataIn, 0}], selects[r]}, + {nand2_ports_[{PortRoleType::DataIn, 1}], q(r)}, {nand2_ports_[{PortRoleType::DataOut, 0}], y}}); return {y, true}; } if (n == 2) { + const int r0 = rows[lo]; + const int r1 = rows[lo + 1]; dbNet* y = net(out); - add(lo, + add(r0, aoi22_cell_, - fmt::format("aoi_r{}", lo), - {{aoi22_in_a1_, selects[lo]}, - {aoi22_in_a2_, q(lo)}, - {aoi22_in_b1_, selects[lo + 1]}, - {aoi22_in_b2_, q(lo + 1)}, + fmt::format("aoi_r{}", r0), + {{aoi22_in_a1_, selects[r0]}, + {aoi22_in_a2_, q(r0)}, + {aoi22_in_b1_, selects[r1]}, + {aoi22_in_b2_, q(r1)}, {aoi22_out_, y}}); return {y, true}; } @@ -225,13 +234,14 @@ RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, half *= 2; } const int mid = lo + half; - const int host = mid - 1; + const int host = rows[mid - 1]; const TreeEnd a - = planReadTree(prefix, word_idx, bit, lo, mid, selects, nullptr); - TreeEnd b = planReadTree(prefix, word_idx, bit, mid, hi, selects, nullptr); + = planReadTree(prefix, word_idx, bit, rows, lo, mid, selects, nullptr); + TreeEnd b + = planReadTree(prefix, word_idx, bit, rows, mid, hi, selects, nullptr); if (b.inverted != a.inverted) { dbNet* nb = makeNet(prefix, fmt::format("t{}_{}_inv", mid, hi)); - add(mid, + add(rows[mid], inv_cell_, fmt::format("inv_r{}_{}", mid, hi), {{inv_ports_[{PortRoleType::DataIn, 0}], b.net}, @@ -287,6 +297,31 @@ void RamGen::makeSlice(const int slice_idx, 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 (row_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), + slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) + + local_bit * column_mux_ratio + word_idx); + } + ram_grid_.addCell( + nullptr, + slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) + + mask_size * column_mux_ratio + word_idx); + return; + } + int r_total = rw_ports + r_ports; vector select_b_nets(r_total); if (and_or_read_) { @@ -1524,7 +1559,8 @@ void RamGen::generate(const int mask_size, dbMaster* inv_cell, dbMaster* tapcell, int max_tap_dist, - const std::string& read_style) + const std::string& read_style, + const int zero_word) { if (num_words / column_mux_ratio == 1) { logger_->error(RAM, 42, "1-row RAMs are not supported."); @@ -1572,6 +1608,7 @@ void RamGen::generate(const int mask_size, use_latch_ = use_latch; num_write_ = rw_ports + w_ports; read_style_ = read_style; + zero_word_ = zero_word; and_or_read_ = false; storage_inverted_ = false; storage_nets_.clear(); @@ -1616,6 +1653,21 @@ void RamGen::generate(const int mask_size, findMasters(); + // 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) { + 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( @@ -1838,7 +1890,19 @@ void RamGen::generate(const int mask_size, // 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 rows in the trees: where the storage output is inverted the + // final inverter assumes some row'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. + vector stored_rows; for (int row = 0; row < num_rows; ++row) { + if (row == zero_word_) { + if (storage_inverted_) { + stored_rows.push_back(row); + } + continue; + } + stored_rows.push_back(row); for (int w = 0; w < column_mux_ratio; ++w) { for (int bit = 0; bit < word_size; ++bit) { const std::string prefix = fmt::format( @@ -1848,7 +1912,8 @@ void RamGen::generate(const int mask_size, } } } - const bool flip = treeInverted(0, num_rows) != storage_inverted_; + const int stored = static_cast(stored_rows.size()); + const bool flip = treeInverted(0, stored) != storage_inverted_; 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) { @@ -1858,8 +1923,9 @@ void RamGen::generate(const int mask_size, const TreeEnd root = planReadTree(prefix, w, bit, + stored_rows, 0, - num_rows, + stored, read_select_nets[port], flip ? nullptr : out); if (flip) { @@ -2448,12 +2514,24 @@ 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); + } } // Write logic, the ports in order: where two write the same word in a @@ -2468,20 +2546,23 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, += 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 + write_logic += fmt::format( + R"( for (i = 0; i < {}; i = i + 1) begin + if ({}[i]{}) begin mem[{}][i*{} +:{}] <= {}[i*{} +:{}]; end end )", - slices_per_word, - wbus("we", w), - write_addr, - mask_size, - mask_size, - wbus("D", w), - mask_size, - mask_size); + slices_per_word, + wbus("we", w), + zero_word_ >= 0 ? fmt::format(" && {} != {}", write_addr, zero_word_) + : std::string(), + write_addr, + mask_size, + mask_size, + wbus("D", w), + mask_size, + mask_size); } std::string verilog_code = fmt::format(R"(module {} ({} diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index bb90a0c4348..35f5bf31574 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -42,7 +42,8 @@ generate_ram_netlist_cmd(int mask_size, const char* inv_cell_name, const char* tapcell_name, const int max_tap_dist, - const char* read_style) + const char* read_style, + const int zero_word) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -94,7 +95,7 @@ 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, read_style); + tapcell, max_tap_dist, read_style, zero_word); } 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 8ae362eb9e7..53540267647 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -8,6 +8,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-storage_cell name] [-tristate_cell name] [-read_style tristate|and_or] + [-zero_word word] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -20,7 +21,7 @@ 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 -read_style } flags {} + -write_behavioral_verilog -read_style -zero_word } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -144,9 +145,15 @@ proc generate_ram_netlist { args } { } } + # 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) + } + 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 \ - $read_style + $read_style $zero_word } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -180,7 +187,7 @@ 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 -read_style } flags {} + -write_behavioral_verilog -read_style -zero_word } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -221,6 +228,10 @@ proc generate_ram { args } { 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(-inv_cell)] } { lappend ram_netlist_args -inv_cell $keys(-inv_cell) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index bd95a9a46b1..2263ea1ca3e 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -15,6 +15,7 @@ COMPULSORY_TESTS = [ PASSFAIL_TESTS = [ "write_ports_nangate45", "and_or_read_asap7", + "zero_word_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -63,6 +64,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "zero_word_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index aa99c13cb06..bc908ecd4f7 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -8,5 +8,6 @@ or_integration_tests( PASSFAIL_TESTS write_ports_nangate45 and_or_read_asap7 + zero_word_asap7 ) diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index a84a4a0412d..af286e9cad4 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -7,5 +7,6 @@ record_tests { make_7x7_2r1w_nangate45 write_ports_nangate45 and_or_read_asap7 + zero_word_asap7 make_8x8_latch_sky130 } diff --git a/src/ram/test/zero_word_asap7.tcl b/src/ram/test/zero_word_asap7.tcl new file mode 100644 index 00000000000..9a193fb223b --- /dev/null +++ b/src/ram/test/zero_word_asap7.tcl @@ -0,0 +1,28 @@ +# -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" + +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 -zero_word 0 + +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" From 530b986485af4c8919bc27a6771c4974ef94f3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 04/43] ram: generate_ram -reset_value, an asynchronous reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -reset_value v resets every word to v asynchronously. Each bit's flop is cleared or preset by the reset, the other pin held off by one tie cell per word slice: the data, clear and preset pins are read from the flop's ff group, and which pin gives which value accounts for the flop's output polarity (asap7's DFFASRHQNx1 stores !D, so its `clear` sets the data to 1). The reset port takes the flop's own polarity, rst_n or rst; a parent adapts any other reset, or a synchronous one, to it. The storage cell, found or given, has both a clear and a preset. The behavioral model resets the same way. Not with -use_latch (RAM-0065). Several ORFS designs reset their register files (ibex, whose ECC and lockstep checks need valid codewords from the first cycle; cva6, ariane, swerv, coralnpu); it is an option, off by default. Tested: src/ram/test/reset_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 25 +++- src/ram/src/ram.cpp | 204 ++++++++++++++++++++++++++---- src/ram/src/ram.i | 6 +- src/ram/src/ram.tcl | 17 ++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/reset_asap7.tcl | 40 ++++++ 9 files changed, 271 insertions(+), 30 deletions(-) create mode 100644 src/ram/test/reset_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 6702d6699a1..d9f544454f8 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -45,6 +45,7 @@ generate_ram [-mask_size bits] [-tristate_cell name] [-read_style tristate|and_or] [-zero_word word] + [-reset_value value] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -72,6 +73,7 @@ generate_ram [-mask_size bits] | `-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` | 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` | 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` | Resets every word to `value` 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. A parent adapts a reset of another polarity, or a synchronous one, to it. Not with `-use_latch`. Default: no reset. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index e7fc10c1226..0f332d5097c 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -111,7 +111,8 @@ class RamGen odb::dbMaster* tapcell, int max_tap_dist, const std::string& read_style = "", - int zero_word = -1); + int zero_word = -1, + const std::string& reset_value = ""); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -167,6 +168,10 @@ class RamGen const std::vector& selects, odb::dbNet* out); 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 `match` accepts; none is an error unless not // `required`, then nullptr. odb::dbMaster* findMaster(const std::function& match, @@ -193,7 +198,9 @@ class RamGen std::vector& select, odb::dbNet* data_input, std::vector& data_output, - odb::dbNet* storage_net = nullptr); + 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, @@ -315,6 +322,20 @@ class RamGen 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 + // 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_; + uint64_t reset_bits_{0}; + odb::dbBTerm* reset_port_{nullptr}; + odb::dbMaster* tie_off_cell_{nullptr}; // drives the inactive level + std::string tie_off_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_; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 173cd465c76..be9232e55b3 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -116,7 +116,9 @@ std::unique_ptr RamGen::makeBit(const std::string& prefix, vector& select, dbNet* data_input, vector& data_output, - dbNet* storage_net) + dbNet* storage_net, + const int reset_bit, + dbNet* reset_off) { auto bit_cell = std::make_unique(); @@ -126,13 +128,19 @@ std::unique_ptr RamGen::makeBit(const std::string& prefix, } 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, @@ -338,6 +346,17 @@ void RamGen::makeSlice(const int slice_idx, auto we_net = makeNet(prefix, "we"); auto sel_cell = std::make_unique(); + // -reset_value: one tie per word slice holds the flops' unused pins off + dbNet* reset_off = nullptr; + if (reset_port_) { + 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 @@ -400,16 +419,20 @@ void RamGen::makeSlice(const int slice_idx, d = makeNet(name, "d"); } const int bit = start_bit_idx + local_bit; - auto bit_cell - = and_or_read_ - ? makeBit(name, - 0, - gclock_net, - select_b_nets, - d, - outs, - storage_nets_.at({row_idx, word_idx, bit})) - : makeBit(name, r_total, gclock_net, select_b_nets, d, outs); + const int reset_bit + = reset_port_ + ? (bit < 64 ? static_cast((reset_bits_ >> bit) & 1) : 0) + : -1; + auto bit_cell = makeBit( + name, + and_or_read_ ? 0 : r_total, + gclock_net, + select_b_nets, + d, + outs, + and_or_read_ ? storage_nets_.at({row_idx, word_idx, bit}) : nullptr, + reset_bit, + reset_off); if (and_or_read_) { for (const auto& node : tree_nodes_[{row_idx, word_idx, bit}]) { makeInst( @@ -1025,6 +1048,84 @@ static int twoInputTable(sta::LibertyPort* out) return table; } +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_ = findMaster( + [off_high](sta::LibertyPort* port) { + auto f = port->function(); + return f && port->direction()->isOutput() + && f->op() + == (off_high ? sta::FuncExpr::Op::one + : sta::FuncExpr::Op::zero); + }, + off_high ? "tie high" : "tie low"); + tie_off_pin_ = buildPortMap(tie_off_cell_)[{PortRoleType::DataOut, 0}]; + + try { + reset_bits_ = std::stoull(reset_value_, nullptr, 0); + } catch (const std::exception&) { + logger_->error(RAM, 64, "-reset_value {} is not a number.", reset_value_); + } +} + void RamGen::findMasters() { if (!inv_cell_) { @@ -1080,11 +1181,23 @@ void RamGen::findMasters() 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 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(); + } + if (!clear_preset) { + return false; + } + } // looking for DFF specifically auto cell = port->libertyCell(); auto port_iter = cell->portIterator(); @@ -1113,6 +1226,9 @@ void RamGen::findMasters() "-read_style and_or.", storage_cell_->getName()); } + if (!reset_value_.empty()) { + findResetPins(); + } } if (!clock_gate_cell_) { @@ -1560,7 +1676,8 @@ void RamGen::generate(const int mask_size, dbMaster* tapcell, int max_tap_dist, const std::string& read_style, - const int zero_word) + const int zero_word, + const std::string& reset_value) { if (num_words / column_mux_ratio == 1) { logger_->error(RAM, 42, "1-row RAMs are not supported."); @@ -1609,6 +1726,9 @@ void RamGen::generate(const int mask_size, 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(); @@ -1701,6 +1821,16 @@ void RamGen::generate(const int mask_size, auto clock = makeBTerm("clk", 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_; @@ -2446,6 +2576,12 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, }; // Build port list 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); } @@ -2565,17 +2701,37 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, mask_size); } + std::string reset_declaration; + std::string always_head = " always @(posedge clk) begin\n"; + if (!reset_name.empty()) { + 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] <= {}'d{};\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_bits_); + write_logic += " end\n"; + } + std::string verilog_code = fmt::format(R"(module {} ({} ); input clk; -{}{}{}{} +{}{}{}{}{} // memory array declaration reg [{}:0] mem[0:{}]; // write logic integer i; - always @(posedge clk) begin -{} end +{}{} end // read logic {} @@ -2583,12 +2739,14 @@ endmodule )", module_name, port_list, + reset_declaration, data_declarations, output_declaration, addr_declarations, enable_declarations, word_size_bit - 1, num_words - 1, + always_head, write_logic, read_port_logic); diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 35f5bf31574..d24bbf4966e 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -43,7 +43,8 @@ generate_ram_netlist_cmd(int mask_size, const char* tapcell_name, const int max_tap_dist, const char* read_style, - const int zero_word) + const int zero_word, + const char* reset_value) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -95,7 +96,8 @@ 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, read_style, zero_word); + tapcell, max_tap_dist, read_style, zero_word, + reset_value); } 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 53540267647..5600bd53a38 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -9,6 +9,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-tristate_cell name] [-read_style tristate|and_or] [-zero_word word] + [-reset_value value] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -21,7 +22,7 @@ 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 -read_style -zero_word } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -151,9 +152,15 @@ proc generate_ram_netlist { args } { set zero_word $keys(-zero_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) + } + 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 \ - $read_style $zero_word + $read_style $zero_word $reset_value } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -187,7 +194,7 @@ 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 -read_style -zero_word } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -232,6 +239,10 @@ proc generate_ram { args } { lappend ram_netlist_args -zero_word $keys(-zero_word) } + if { [info exists keys(-reset_value)] } { + lappend ram_netlist_args -reset_value $keys(-reset_value) + } + if { [info exists keys(-inv_cell)] } { lappend ram_netlist_args -inv_cell $keys(-inv_cell) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 2263ea1ca3e..d51e0e4591e 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -16,6 +16,7 @@ PASSFAIL_TESTS = [ "write_ports_nangate45", "and_or_read_asap7", "zero_word_asap7", + "reset_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -68,6 +69,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "reset_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index bc908ecd4f7..44f537bcb4a 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -9,5 +9,6 @@ or_integration_tests( write_ports_nangate45 and_or_read_asap7 zero_word_asap7 + reset_asap7 ) diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index af286e9cad4..41dab2d15fc 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -8,5 +8,6 @@ record_tests { write_ports_nangate45 and_or_read_asap7 zero_word_asap7 + reset_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..1a2ecdcd281 --- /dev/null +++ b/src/ram/test/reset_asap7.tcl @@ -0,0 +1,40 @@ +# -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" + +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 DFFASRHQNx1_ASAP7_75t_R -reset_value 0xa5 + +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" From 56217329221ff63ca471e3ba9e456fe828f3d0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 05/43] ram: generate_ram -banks, -bank_columns, -bank_order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -banks splits the words into banks, contiguous or interleaved in the address (-bank_order), -bank_columns of them side by side and the rest stacked, for an array's aspect ratio and for the bank counts register files use (XiangShan's 224 words in 4 interleaved banks). With banks every port decodes every word's whole address, an AND2 tree per word, so a bank need not be a power of two words and any placement of words reads and writes alike; a row holds -bank_columns words, laid out as the column mux lays out its words. A read port is one tristate bitline shared by every word, or one AND-OR tree over all of them. Without -banks, nothing changes. Not with -column_mux_ratio above 1 or -use_latch. Also: an array with fewer rows than address bits got no address inverters (ports_per_col was 0), so its decoders read floating literals; at least one port's inverters now go in each column. Tested: src/ram/test/banks_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 4 + src/ram/include/ram/ram.h | 44 +++- src/ram/src/ram.cpp | 354 +++++++++++++++++++++--------- src/ram/src/ram.i | 7 +- src/ram/src/ram.tcl | 35 ++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/banks_asap7.tcl | 35 +++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 377 insertions(+), 109 deletions(-) create mode 100644 src/ram/test/banks_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index d9f544454f8..caca31e8b55 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -46,6 +46,9 @@ generate_ram [-mask_size bits] [-read_style tristate|and_or] [-zero_word word] [-reset_value value] + [-banks count] + [-bank_columns count] + [-bank_order contiguous|interleaved] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -74,6 +77,7 @@ generate_ram [-mask_size bits] | `-read_style` | 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` | 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` | Resets every word to `value` 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. 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` | 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. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 0f332d5097c..c0d47bbf921 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -112,7 +112,10 @@ class RamGen int max_tap_dist, const std::string& read_style = "", int zero_word = -1, - const std::string& reset_value = ""); + const std::string& reset_value = "", + int banks = 1, + int bank_columns = 1, + const std::string& bank_order = "contiguous"); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -158,15 +161,24 @@ class RamGen odb::dbNet* net; bool inverted; // the net is the complement of the OR of the terms }; - // the tree over rows[lo, hi): rows lists the rows that store a word + // 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 word_idx, int bit, - const std::vector& rows, + const std::vector& terms, int lo, int hi, - const std::vector& selects, 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 @@ -246,7 +258,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, @@ -322,6 +343,17 @@ class RamGen 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}; // 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_ diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index be9232e55b3..1e0b7e09f87 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -186,54 +186,69 @@ bool RamGen::treeInverted(const int lo, const int hi) const return !treeInverted(lo, lo + half); } +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 word_idx, const int bit, - const vector& rows, + const vector& terms, const int lo, const int hi, - const vector& selects, dbNet* out) { - auto add = [&](const int row, + auto add = [&](const Term& at, dbMaster* master, const std::string& name, const vector>& conns) { - tree_nodes_[{row, word_idx, bit}].push_back({master, prefix, name, 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 int row) { - return row == zero_word_ ? selects[row] - : storage_nets_.at({row, word_idx, bit}); + 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 int r = rows[lo]; + const Term& t = terms[lo]; dbNet* y = net(out); - add(r, + add(t, nand2_cell_, - fmt::format("nand_r{}", r), - {{nand2_ports_[{PortRoleType::DataIn, 0}], selects[r]}, - {nand2_ports_[{PortRoleType::DataIn, 1}], q(r)}, + 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 int r0 = rows[lo]; - const int r1 = rows[lo + 1]; + const Term& t0 = terms[lo]; + const Term& t1 = terms[lo + 1]; dbNet* y = net(out); - add(r0, + add(t0, aoi22_cell_, - fmt::format("aoi_r{}", r0), - {{aoi22_in_a1_, selects[r0]}, - {aoi22_in_a2_, q(r0)}, - {aoi22_in_b1_, selects[r1]}, - {aoi22_in_b2_, q(r1)}, + 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}; } @@ -242,14 +257,12 @@ RamGen::TreeEnd RamGen::planReadTree(const std::string& prefix, half *= 2; } const int mid = lo + half; - const int host = rows[mid - 1]; - const TreeEnd a - = planReadTree(prefix, word_idx, bit, rows, lo, mid, selects, nullptr); - TreeEnd b - = planReadTree(prefix, word_idx, bit, rows, mid, hi, selects, nullptr); + 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(rows[mid], + add(terms[mid], inv_cell_, fmt::format("inv_r{}_{}", mid, hi), {{inv_ports_[{PortRoleType::DataIn, 0}], b.net}, @@ -307,7 +320,7 @@ void RamGen::makeSlice(const int slice_idx, // The zero word: no flops, no clock gate; its row's cells hold only // the read trees' nodes planned there. - if (row_idx == zero_word_) { + 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; @@ -334,7 +347,7 @@ void RamGen::makeSlice(const int slice_idx, vector select_b_nets(r_total); if (and_or_read_) { // the AND-OR trees read the true selects - } else if (column_mux_ratio == 1) { + } 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)); } @@ -684,23 +697,81 @@ void RamGen::makeDecoderColumn( 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) { + 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; + 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)); } 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); + 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; + // 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)); + } + ram_grid_.addLayout(std::move(decoder_layout)); +} + std::unique_ptr RamGen::makeInverterColumn( const int num_rows, const int num_row_bits, @@ -1668,7 +1739,7 @@ void RamGen::generate(const int mask_size, 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, @@ -1677,8 +1748,40 @@ void RamGen::generate(const int mask_size, int max_tap_dist, const std::string& read_style, const int zero_word, - const std::string& reset_value) + const std::string& reset_value, + const int banks, + const int bank_columns, + const std::string& bank_order) { + // 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. + banked_ = banks > 1 || bank_columns > 1; + banks_ = std::max(banks, 1); + bank_columns_ = std::max(bank_columns, 1); + bank_interleaved_ = bank_order == "interleaved"; + if (banked_) { + if (column_mux_ratio_arg != 1) { + logger_->error( + RAM, 66, "-banks is not supported with -column_mux_ratio above 1."); + } + 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, "-banks is not supported with -use_latch."); + } + } + words_per_bank_ = num_words / banks_; + // 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; if (num_words / column_mux_ratio == 1) { logger_->error(RAM, 42, "1-row RAMs are not supported."); } @@ -1686,7 +1789,8 @@ void RamGen::generate(const int mask_size, const std::string ram_name = fmt::format("RAM{}x{}", num_words, word_size); // 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 " @@ -1714,7 +1818,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 @@ -1780,7 +1885,7 @@ void RamGen::generate(const int mask_size, logger_->error( RAM, 59, "-zero_word {} is not a word of {}.", zero_word_, num_words); } - if (!and_or_read_ || column_mux_ratio != 1) { + if (!and_or_read_ || (column_mux_ratio != 1 && !banked_)) { logger_->error(RAM, 60, "-zero_word needs -read_style and_or and " @@ -1803,8 +1908,10 @@ void RamGen::generate(const int mask_size, // 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; + // 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); @@ -1896,9 +2003,26 @@ void RamGen::generate(const int mask_size, = makeNet(port_prefix, fmt::format("inv_addr[{}]", 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) { + const int word = wordAt(row, w); + for (int input = 0; input < num_inputs; ++input) { + decoder_input_nets[p][row * column_mux_ratio + w][input] + = ((word >> input) & 1) ? addr_inputs_[p][input]->getNet() + : 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) { @@ -1915,23 +2039,24 @@ void RamGen::generate(const int mask_size, } // 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 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); } } @@ -1971,10 +2096,14 @@ 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]->getNet(); + } } } @@ -1990,10 +2119,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)); } @@ -2015,25 +2144,27 @@ 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 rows in the trees: where the storage output is inverted the - // final inverter assumes some row's term is the read's, so the zero + // 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. - vector stored_rows; for (int row = 0; row < num_rows; ++row) { - if (row == zero_word_) { - if (storage_inverted_) { - stored_rows.push_back(row); - } - continue; - } - stored_rows.push_back(row); for (int w = 0; w < column_mux_ratio; ++w) { + if (wordAt(row, w) == zero_word_) { + continue; + } for (int bit = 0; bit < word_size; ++bit) { const std::string prefix = fmt::format( "storage_{}_{}_{}", row, w, (bit / mask_size) * mask_size); @@ -2042,24 +2173,39 @@ void RamGen::generate(const int mask_size, } } } - const int stored = static_cast(stored_rows.size()); - const bool flip = treeInverted(0, stored) != storage_inverted_; - for (int w = 0; w < column_mux_ratio; ++w) { + // 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; + } + terms.push_back({row, w, select_at(read_select_nets[port], row, w)}); + } + } + 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_size; ++bit) { const std::string prefix - = fmt::format("read_w{}_p{}_b{}", w, port, bit); + = 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, - w, - bit, - stored_rows, - 0, - stored, - read_select_nets[port], - flip ? nullptr : out); + const TreeEnd root + = planReadTree(prefix, bit, terms, 0, n, flip ? nullptr : out); if (flip) { - tree_nodes_[{num_rows - 1, w, bit}].push_back( + 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", @@ -2074,7 +2220,7 @@ void RamGen::generate(const int mask_size, 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) { + if (column_mux_ratio > 1 && !banked_) { for (int i = 0; i < r_total; ++i) { shared_select_b_nets[i] = makeNet(fmt::format("row{}", row), fmt::format("select{}_b", i)); @@ -2085,10 +2231,11 @@ void RamGen::generate(const int mask_size, // 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] = write_select_nets[p][row]; + row_selects[p] = select_at(write_select_nets[p], row, word_idx); } for (int p = 0; p < r_total; ++p) { - row_selects[num_write + p] = 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) { @@ -2110,15 +2257,15 @@ void RamGen::generate(const int mask_size, write_enable, row_selects, shared_select_b_nets, - (word_idx == 0 || column_mux_ratio == 1), + (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; @@ -2305,26 +2452,37 @@ void RamGen::generate(const int mask_size, 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) { @@ -2344,7 +2502,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(), @@ -2353,7 +2511,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(); } @@ -2407,9 +2565,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); @@ -2417,7 +2575,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; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index d24bbf4966e..cafbfcad189 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -44,7 +44,10 @@ generate_ram_netlist_cmd(int mask_size, const int max_tap_dist, const char* read_style, const int zero_word, - const char* reset_value) + const char* reset_value, + const int banks, + const int bank_columns, + const char* bank_order) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -97,7 +100,7 @@ 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, read_style, zero_word, - reset_value); + reset_value, banks, bank_columns, bank_order); } 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 5600bd53a38..60cad08c1a0 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -10,6 +10,9 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-read_style tristate|and_or] [-zero_word word] [-reset_value value] + [-banks count] + [-bank_columns count] + [-bank_order contiguous|interleaved] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -22,7 +25,8 @@ 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 -read_style -zero_word -reset_value } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value + -banks -bank_columns -bank_order } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -158,9 +162,27 @@ proc generate_ram_netlist { args } { 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) + } + 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." + } + } + 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 \ - $read_style $zero_word $reset_value + $read_style $zero_word $reset_value $banks $bank_columns $bank_order } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -194,7 +216,8 @@ 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 -read_style -zero_word -reset_value } flags {} + -write_behavioral_verilog -read_style -zero_word -reset_value + -banks -bank_columns -bank_order } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -243,6 +266,12 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } + foreach key {-banks -bank_columns -bank_order} { + if { [info exists keys($key)] } { + lappend ram_netlist_args $key $keys($key) + } + } + if { [info exists keys(-inv_cell)] } { lappend ram_netlist_args -inv_cell $keys(-inv_cell) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index d51e0e4591e..09ff0e119a7 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -17,6 +17,7 @@ PASSFAIL_TESTS = [ "and_or_read_asap7", "zero_word_asap7", "reset_asap7", + "banks_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -73,6 +74,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "banks_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 44f537bcb4a..7584b0b1bb8 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -10,5 +10,6 @@ or_integration_tests( and_or_read_asap7 zero_word_asap7 reset_asap7 + banks_asap7 ) diff --git a/src/ram/test/banks_asap7.tcl b/src/ram/test/banks_asap7.tcl new file mode 100644 index 00000000000..5eba3d1a4df --- /dev/null +++ b/src/ram/test/banks_asap7.tcl @@ -0,0 +1,35 @@ +# -banks 4 -bank_columns 2 -bank_order interleaved: the 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" + +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 \ + -banks 4 -bank_columns 2 -bank_order interleaved + +ram_sim::load +# two words a row: 8 words in 4 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]] == 4 } \ + "storage in [llength [lsort -unique $rows]] rows, not 4" +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 41dab2d15fc..cbd45cfff7a 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -9,5 +9,6 @@ record_tests { and_or_read_asap7 zero_word_asap7 reset_asap7 + banks_asap7 make_8x8_latch_sky130 } From ddf44b628f40b445a660cc9c54b642473b900ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 06/43] ram: generate_ram -rb_ports, banked read ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -rb_ports n adds n banked read ports to a RAM with -banks: each has an address and a data output per bank (addr_rb_b, Qrb_b), the address the word's index in its bank, every bank read independently in the same cycle, as XiangShan's integer register file reads. To the generator below, each (port, bank) is a read port of its own that decodes and reads only its bank's words: a tristate bitline or an AND-OR tree over them. The behavioral model reads the same words. Tested: src/ram/test/banked_read_ports_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 10 +- src/ram/src/ram.cpp | 149 ++++++++++++++++++++--- src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 17 ++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/banked_read_ports_asap7.tcl | 35 ++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 200 insertions(+), 25 deletions(-) create mode 100644 src/ram/test/banked_read_ports_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index caca31e8b55..a6fbf28bc98 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -49,6 +49,7 @@ generate_ram [-mask_size bits] [-banks count] [-bank_columns count] [-bank_order contiguous|interleaved] + [-rb_ports count] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -78,6 +79,7 @@ generate_ram [-mask_size bits] | `-zero_word` | 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` | Resets every word to `value` 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. 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` | 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` | 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. Default: 0. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index c0d47bbf921..6d9e6253d3a 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -115,7 +115,8 @@ class RamGen const std::string& reset_value = "", int banks = 1, int bank_columns = 1, - const std::string& bank_order = "contiguous"); + const std::string& bank_order = "contiguous", + int rb_ports = 0); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -354,6 +355,13 @@ class RamGen 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; // 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_ diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 1e0b7e09f87..9d6f37750b4 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -152,6 +152,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, @@ -186,6 +189,16 @@ bool RamGen::treeInverted(const int lo, const int hi) const 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_) { @@ -349,7 +362,9 @@ void RamGen::makeSlice(const int slice_idx, // 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; @@ -558,6 +573,9 @@ void RamGen::makeSlice(const int slice_idx, // Make select inverters if (create_select_inv && !and_or_read_) { 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), @@ -706,6 +724,9 @@ void RamGen::makeBufferColumn(const std::string& prefix, // 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, @@ -732,6 +753,9 @@ void RamGen::makeWordDecoderColumn( 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; @@ -791,6 +815,11 @@ 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(); makeInst(inv_grid_cell.get(), fmt::format("decoder_p{}", p), @@ -1751,7 +1780,8 @@ void RamGen::generate(const int mask_size, const std::string& reset_value, const int banks, const int bank_columns, - const std::string& bank_order) + const std::string& bank_order, + const int 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; @@ -1779,6 +1809,16 @@ void RamGen::generate(const int mask_size, } } words_per_bank_ = num_words / banks_; + rb_ports_ = rb_ports; + r_plain_ = rw_ports + r_ports; + if (rb_ports_ > 0 && !banked_) { + logger_->error(RAM, 70, "-rb_ports needs -banks."); + } + // 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; @@ -1907,7 +1947,7 @@ void RamGen::generate(const int mask_size, // 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 @@ -1988,15 +2028,24 @@ void RamGen::generate(const int mask_size, } else if (p < rw_ports + w_ports) { 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) + // 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(num_inputs); - inv_addr_nets[p].resize(num_inputs); + addr_inputs_[p].resize(port_inputs); + inv_addr_nets[p].resize(port_inputs); - for (int i = 0; i < num_inputs; ++i) { + 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] @@ -2010,11 +2059,18 @@ void RamGen::generate(const int mask_size, vector(num_inputs)); for (int row = 0; row < num_rows; ++row) { for (int w = 0; w < column_mux_ratio; ++w) { - const int word = wordAt(row, w); - for (int input = 0; input < num_inputs; ++input) { - decoder_input_nets[p][row * column_mux_ratio + w][input] - = ((word >> input) & 1) ? addr_inputs_[p][input]->getNet() - : inv_addr_nets[p][input]; + 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_inputs_[p][input]->getNet() + : inv_addr_nets[p][input]; } } } @@ -2058,10 +2114,21 @@ void RamGen::generate(const int mask_size, read_select_nets[r_sel_idx++] = makeSelectNets( 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); // One data bus per write port: D, or with more than one D0, D1, ... vector> data_inputs(num_write); @@ -2077,15 +2144,22 @@ void RamGen::generate(const int mask_size, } // 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 @@ -2185,7 +2259,11 @@ void RamGen::generate(const int mask_size, if (wordAt(row, w) == zero_word_ && !storage_inverted_) { continue; } - terms.push_back({row, w, select_at(read_select_nets[port], row, w)}); + 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; @@ -2248,7 +2326,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, @@ -2761,6 +2839,14 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, for (int i = 0; i < r_ports; i++) { port_list += fmt::format(",\n addr_r{}", i); } + // 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); } @@ -2795,6 +2881,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; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index cafbfcad189..5df5f55134b 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -47,7 +47,8 @@ generate_ram_netlist_cmd(int mask_size, const char* reset_value, const int banks, const int bank_columns, - const char* bank_order) + const char* bank_order, + const int rb_ports) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -100,7 +101,7 @@ 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, read_style, zero_word, - reset_value, banks, bank_columns, bank_order); + reset_value, banks, bank_columns, bank_order, rb_ports); } 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 60cad08c1a0..ab35ed2436a 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -13,6 +13,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-banks count] [-bank_columns count] [-bank_order contiguous|interleaved] + [-rb_ports count] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -26,7 +27,7 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order } flags {} + -banks -bank_columns -bank_order -rb_ports } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -102,7 +103,7 @@ proc generate_ram_netlist { args } { utl::error RAM 53 "A read/write port is the only write port." } - if { $rw_ports + $r_ports < 1 } { + if { $rw_ports + $r_ports < 1 && ![info exists keys(-rb_ports)] } { utl::error RAM 36 "Must specify at least one read output port" } @@ -172,6 +173,11 @@ proc generate_ram_netlist { args } { if { [info exists keys(-bank_columns)] } { set bank_columns $keys(-bank_columns) } + # 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) @@ -182,7 +188,8 @@ proc generate_ram_netlist { args } { 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 \ - $read_style $zero_word $reset_value $banks $bank_columns $bank_order + $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ + $rb_ports } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -217,7 +224,7 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order } flags {} + -banks -bank_columns -bank_order -rb_ports } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -266,7 +273,7 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order} { + foreach key {-banks -bank_columns -bank_order -rb_ports} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 09ff0e119a7..7dc95f61c95 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -18,6 +18,7 @@ PASSFAIL_TESTS = [ "zero_word_asap7", "reset_asap7", "banks_asap7", + "banked_read_ports_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -78,6 +79,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "banked_read_ports_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 7584b0b1bb8..085585e6284 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -11,5 +11,6 @@ or_integration_tests( zero_word_asap7 reset_asap7 banks_asap7 + banked_read_ports_asap7 ) 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..75ae0c57052 --- /dev/null +++ b/src/ram/test/banked_read_ports_asap7.tcl @@ -0,0 +1,35 @@ +# -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" + +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 16 -r_ports 0 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R \ + -banks 4 -bank_order interleaved -rb_ports 1 + +# 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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index cbd45cfff7a..ef30cd1000b 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -10,5 +10,6 @@ record_tests { zero_word_asap7 reset_asap7 banks_asap7 + banked_read_ports_asap7 make_8x8_latch_sky130 } From f3bcdede658633de1f44e8cc04dda7b0b5eac162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 07/43] ram: generate_ram -read_latency, registered read addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -read_latency 1 registers every read port's address: a plain positive-edge flop per address bit, found by its ff group, in the address inverter's cell; the data follows a cycle after the address, as XiangShan's register files read. Where the flop gives the address inverted (asap7's DFFHQNx1) the decoders take the flop's output as the complement and the inverter makes the true literal, so no cell is added. The behavioral model registers the addresses the same way. Not with -column_mux_ratio above 1 or with read/write ports (RAM-0072). Tested: src/ram/test/read_latency_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 15 ++- src/ram/src/ram.cpp | 150 ++++++++++++++++++++++++++-- src/ram/src/ram.i | 6 +- src/ram/src/ram.tcl | 15 ++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/read_latency_asap7.tcl | 37 +++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 src/ram/test/read_latency_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index a6fbf28bc98..28950270a64 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -50,6 +50,7 @@ generate_ram [-mask_size bits] [-bank_columns count] [-bank_order contiguous|interleaved] [-rb_ports count] + [-read_latency 0|1] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -80,6 +81,7 @@ generate_ram [-mask_size bits] | `-reset_value` | Resets every word to `value` 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. 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` | 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` | 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. Default: 0. | +| `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 6d9e6253d3a..fe22f91a049 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -116,7 +116,8 @@ class RamGen int banks = 1, int bank_columns = 1, const std::string& bank_order = "contiguous", - int rb_ports = 0); + int rb_ports = 0, + int read_latency = 0); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -362,6 +363,18 @@ class RamGen // 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; + // -read_latency 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 read_latency_{0}; + 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_ diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 9d6f37750b4..25cb4d9b20a 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -821,14 +821,29 @@ std::unique_ptr RamGen::makeInverterColumn( 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) { + // -read_latency 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 @@ -1781,7 +1796,8 @@ void RamGen::generate(const int mask_size, const int banks, const int bank_columns, const std::string& bank_order, - const int rb_ports) + const int rb_ports, + const int read_latency) { // 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; @@ -1810,6 +1826,18 @@ void RamGen::generate(const int mask_size, } words_per_bank_ = num_words / banks_; rb_ports_ = rb_ports; + read_latency_ = read_latency; + if (read_latency_ > 0) { + if (read_latency_ != 1) { + logger_->error(RAM, 71, "-read_latency is 0 or 1."); + } + if (column_mux_ratio_arg > 1 || rw_ports > 0) { + logger_->error(RAM, + 72, + "-read_latency 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."); @@ -1918,6 +1946,47 @@ void RamGen::generate(const int mask_size, findMasters(); + // -read_latency 1: a plain positive-edge flop for the read addresses + if (read_latency_ > 0) { + // a plain flop: positive-edge, no clear or preset + auto plain = [](const sta::LibertyCell* cell) { + for (const auto& seq : cell->sequentials()) { + if (seq.isRegister() && !seq.clear() && !seq.preset() && seq.clock() + && seq.clock()->op() == sta::FuncExpr::Op::port) { + return true; + } + } + return false; + }; + // the storage cell where it is one (findMaster passes over cells with + // two outputs, as Nangate45's flops all are), else the smallest + if (storage_cell_ + && plain(network_->libertyCell(network_->dbToSta(storage_cell_)))) { + addr_flop_cell_ = storage_cell_; + } else { + addr_flop_cell_ = findMaster( + [plain](sta::LibertyPort* port) { + return port->isRegOutput() && plain(port->libertyCell()); + }, + "address flop"); + } + 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) { @@ -1967,6 +2036,7 @@ void RamGen::generate(const int mask_size, ram_grid_.setNumLayouts(col_cell_count); auto clock = makeBTerm("clk", dbIoType::INPUT); + clock_net_ = clock->getNet(); // -reset_value: an asynchronous reset at the storage flop's own // polarity, rst_n or rst; a parent adapts any other reset to it @@ -2010,7 +2080,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 @@ -2044,12 +2117,27 @@ void RamGen::generate(const int mask_size, 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 = read_latency_ > 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: with banks, every word's whole address, one @@ -2068,9 +2156,8 @@ void RamGen::generate(const int mask_size, 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_inputs_[p][input]->getNet() - : inv_addr_nets[p][input]; + literals[input] = ((word >> input) & 1) ? addr_true_[p][input] + : inv_addr_nets[p][input]; } } } @@ -2088,7 +2175,7 @@ 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; } @@ -2943,6 +3030,51 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, } } + // -read_latency 1: every read address registered, the reads from the + // registered ones (addr_r0 -> addr_r0_q) + if (read_latency_ > 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. std::string data_declarations; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 5df5f55134b..f5b0df9f450 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -48,7 +48,8 @@ generate_ram_netlist_cmd(int mask_size, const int banks, const int bank_columns, const char* bank_order, - const int rb_ports) + const int rb_ports, + const int read_latency) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -101,7 +102,8 @@ 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, read_style, zero_word, - reset_value, banks, bank_columns, bank_order, rb_ports); + reset_value, banks, bank_columns, bank_order, rb_ports, + read_latency); } 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 ab35ed2436a..e70d6961369 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -14,6 +14,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-bank_columns count] [-bank_order contiguous|interleaved] [-rb_ports count] + [-read_latency 0|1] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -27,7 +28,7 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -173,6 +174,12 @@ proc generate_ram_netlist { args } { if { [info exists keys(-bank_columns)] } { set bank_columns $keys(-bank_columns) } + # A read latency of 1: the read addresses registered. + set read_latency 0 + if { [info exists keys(-read_latency)] } { + set read_latency $keys(-read_latency) + } + # Banked read ports: an address and a data output per bank. set rb_ports 0 if { [info exists keys(-rb_ports)] } { @@ -189,7 +196,7 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports + $rb_ports $read_latency } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -224,7 +231,7 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -273,7 +280,7 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order -rb_ports} { + foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 7dc95f61c95..f51a7c7d55d 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -19,6 +19,7 @@ PASSFAIL_TESTS = [ "reset_asap7", "banks_asap7", "banked_read_ports_asap7", + "read_latency_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -83,6 +84,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "read_latency_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 085585e6284..c2964ee6606 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -12,5 +12,6 @@ or_integration_tests( reset_asap7 banks_asap7 banked_read_ports_asap7 + read_latency_asap7 ) diff --git a/src/ram/test/read_latency_asap7.tcl b/src/ram/test/read_latency_asap7.tcl new file mode 100644 index 00000000000..9beb7e08f5b --- /dev/null +++ b/src/ram/test/read_latency_asap7.tcl @@ -0,0 +1,37 @@ +# -read_latency 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" + +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 -read_latency 1 + +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 ef30cd1000b..e07fcc614b0 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -11,5 +11,6 @@ record_tests { reset_asap7 banks_asap7 banked_read_ports_asap7 + read_latency_asap7 make_8x8_latch_sky130 } From 6324599450b9fb8e497d9e2bcc6da5366a738126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 08/43] ram: generate_ram -write_style mux, a hold mux per bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -write_style mux holds each word without a clock gate: every flop is on the clock, its D the new data where the word is written and its own value elsewhere, an AO22 per bit (the AOI22 and an inverter where the library has none) and an inverter per word for the hold; a flop whose output is inverted feeds back through an inverter per bit. The default, clock_gate, is generate_ram's clock gate per word, as before. Which is better depends on the design: a clock gate per word saves a mux per bit, a mux keeps every flop on the plain clock. Not with -use_latch. Tested: src/ram/test/write_mux_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 6 +- src/ram/src/ram.cpp | 141 ++++++++++++++++++++++++------ src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 19 +++- src/ram/test/BUILD | 5 ++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/write_mux_asap7.tcl | 33 +++++++ 9 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 src/ram/test/write_mux_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 28950270a64..d00ea4266f9 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -51,6 +51,7 @@ generate_ram [-mask_size bits] [-bank_order contiguous|interleaved] [-rb_ports count] [-read_latency 0|1] + [-write_style clock_gate|mux] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -82,6 +83,7 @@ generate_ram [-mask_size bits] | `-banks`, `-bank_columns`, `-bank_order` | 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` | 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. Default: 0. | | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | +| `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index fe22f91a049..1eb84cb2954 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -117,7 +117,8 @@ class RamGen int bank_columns = 1, const std::string& bank_order = "contiguous", int rb_ports = 0, - int read_latency = 0); + int read_latency = 0, + const std::string& write_style = "clock_gate"); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -369,6 +370,9 @@ class RamGen // where the flop's output is inverted the complement is the flop's and // the inverter makes the true one. int read_latency_{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}; odb::dbMaster* addr_flop_cell_{nullptr}; std::string addr_flop_d_, addr_flop_clk_, addr_flop_q_; bool addr_flop_inverted_{false}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 25cb4d9b20a..83191d7c154 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -370,9 +370,61 @@ void RamGen::makeSlice(const int slice_idx, select_b_nets = shared_select_b_nets; } - auto gclock_net = makeNet(prefix, "gclock"); + // the flops' clock: the word's gated clock, or with -write_style mux + // the clock itself + auto gclock_net = write_mux_ ? clock : makeNet(prefix, "gclock"); auto we_net = makeNet(prefix, "we"); auto sel_cell = std::make_unique(); + // -write_style mux: the word's hold, where it is not written + dbNet* hold = nullptr; + if (write_mux_) { + 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 = nullptr; @@ -442,25 +494,47 @@ void RamGen::makeSlice(const int slice_idx, outs[r_port] = data_output[r_port][local_bit]; } - dbNet* d = data_input[0][local_bit]; + // 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 = makeNet(name, "d"); + 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_ ? (bit < 64 ? static_cast((reset_bits_ >> bit) & 1) : 0) : -1; - auto bit_cell = makeBit( - name, - and_or_read_ ? 0 : r_total, - gclock_net, - select_b_nets, - d, - outs, - and_or_read_ ? storage_nets_.at({row_idx, word_idx, bit}) : nullptr, - reset_bit, - reset_off); + 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( @@ -473,7 +547,7 @@ void RamGen::makeSlice(const int slice_idx, for (int w = 0; w < num_write; w += 2) { const bool last = w + 2 >= num_write; dbNet* out = last && acc == nullptr - ? d + ? 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 @@ -514,7 +588,8 @@ void RamGen::makeSlice(const int slice_idx, {and2_ports_[{PortRoleType::DataOut, 0}], out}}); } if (acc != nullptr) { - dbNet* sum = last ? d : makeNet(name, fmt::format("d_or{}", w / 2)); + dbNet* sum + = last ? d_data : makeNet(name, fmt::format("d_or{}", w / 2)); makeInst(bit_cell.get(), name, fmt::format("dor{}", w / 2), @@ -533,14 +608,16 @@ void RamGen::makeSlice(const int slice_idx, 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_) { + 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}}); + } // 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. @@ -1430,12 +1507,12 @@ void RamGen::findMasters() // 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_cell_) { + // AO22 truth table: Y = A1&A2 | B1&B2 + ao22_cell_ + = findMaster4(0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false); + } if (num_write_ > 1) { - if (!ao22_cell_) { - // AO22 truth table: Y = A1&A2 | B1&B2 - ao22_cell_ = findMaster4( - 0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false); - } if (!or2_cell_) { or2_cell_ = findMaster( [](sta::LibertyPort* port) { @@ -1797,7 +1874,8 @@ void RamGen::generate(const int mask_size, const int bank_columns, const std::string& bank_order, const int rb_ports, - const int read_latency) + const int read_latency, + const std::string& write_style) { // 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; @@ -1827,6 +1905,11 @@ void RamGen::generate(const int mask_size, words_per_bank_ = num_words / banks_; rb_ports_ = rb_ports; read_latency_ = read_latency; + write_mux_ = write_style == "mux"; + if (write_mux_ && use_latch) { + logger_->error( + RAM, 73, "-write_style mux is not supported with -use_latch."); + } if (read_latency_ > 0) { if (read_latency_ != 1) { logger_->error(RAM, 71, "-read_latency is 0 or 1."); diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index f5b0df9f450..eb390477769 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -49,7 +49,8 @@ generate_ram_netlist_cmd(int mask_size, const int bank_columns, const char* bank_order, const int rb_ports, - const int read_latency) + const int read_latency, + const char* write_style) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -103,7 +104,7 @@ generate_ram_netlist_cmd(int mask_size, 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, - read_latency); + read_latency, write_style); } 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 e70d6961369..8666aacf7be 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -15,6 +15,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-bank_order contiguous|interleaved] [-rb_ports count] [-read_latency 0|1] + [-write_style clock_gate|mux] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -28,7 +29,7 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -174,6 +175,16 @@ proc generate_ram_netlist { args } { if { [info exists keys(-bank_columns)] } { set bank_columns $keys(-bank_columns) } + # 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." + } + } + # A read latency of 1: the read addresses registered. set read_latency 0 if { [info exists keys(-read_latency)] } { @@ -196,7 +207,7 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency + $rb_ports $read_latency $write_style } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -231,7 +242,7 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -280,7 +291,7 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency} { + foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index f51a7c7d55d..e7b9ed564d3 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -20,6 +20,7 @@ PASSFAIL_TESTS = [ "banks_asap7", "banked_read_ports_asap7", "read_latency_asap7", + "write_mux_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -88,6 +89,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "write_mux_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index c2964ee6606..7099d6fb617 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -13,5 +13,6 @@ or_integration_tests( banks_asap7 banked_read_ports_asap7 read_latency_asap7 + write_mux_asap7 ) diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index e07fcc614b0..95d99ebe20b 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -12,5 +12,6 @@ record_tests { banks_asap7 banked_read_ports_asap7 read_latency_asap7 + write_mux_asap7 make_8x8_latch_sky130 } diff --git a/src/ram/test/write_mux_asap7.tcl b/src/ram/test/write_mux_asap7.tcl new file mode 100644 index 00000000000..3ba7596edd9 --- /dev/null +++ b/src/ram/test/write_mux_asap7.tcl @@ -0,0 +1,33 @@ +# -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" + +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 -write_style mux + +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" From 403f14931a2fe1450f340dd635a0587410b0a6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 09/43] ram: generate_ram_netlist -periphery unplaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -periphery unplaced leaves the periphery (address inverters and registers, decoders, select buffers, each word's select cell and the input buffers) for a parent to place, size and buffer: the parent alone knows the drivers and loads outside the array. The outline is cut to what stays placed, the core, and a row above it for a parent of the other row parity, so a parent sizing its floorplan by utilization charges the core and nothing else. Each pin goes where its connections land, on -pin_layer's track grid inside the outline, for a macro placer to orient the core by before the parent dissolves it into its own rows. generate_ram_netlist only; generate_ram's flow places everything. Tested: src/ram/test/periphery_unplaced_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 1 + src/ram/include/ram/ram.h | 14 ++- src/ram/src/ram.cpp | 145 +++++++++++++++++++++- src/ram/src/ram.i | 6 +- src/ram/src/ram.tcl | 22 +++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/periphery_unplaced_asap7.tcl | 71 +++++++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 src/ram/test/periphery_unplaced_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index d00ea4266f9..9203ca06518 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -84,6 +84,7 @@ generate_ram [-mask_size bits] | `-rb_ports` | 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. Default: 0. | | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | +| `-periphery`, `-pin_layer` | `generate_ram_netlist` only. `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. Default: `placed`. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 1eb84cb2954..f751f6c63d2 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -118,7 +118,9 @@ class RamGen const std::string& bank_order = "contiguous", int rb_ports = 0, int read_latency = 0, - const std::string& write_style = "clock_gate"); + const std::string& write_style = "clock_gate", + const std::string& periphery = "placed", + const std::string& pin_layer = ""); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -373,6 +375,16 @@ class RamGen // -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}; + // -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 pin_layer_; + bool marking_periphery_{false}; + Cell* periphery_cell_{nullptr}; + std::vector periphery_insts_; + void unplacePeriphery(); odb::dbMaster* addr_flop_cell_{nullptr}; std::string addr_flop_d_, addr_flop_clk_, addr_flop_q_; bool addr_flop_inverted_{false}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 83191d7c154..890e7b53f24 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -81,6 +81,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) { @@ -375,6 +378,7 @@ void RamGen::makeSlice(const int slice_idx, auto gclock_net = write_mux_ ? clock : makeNet(prefix, "gclock"); auto we_net = 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 = nullptr; if (write_mux_) { @@ -751,6 +755,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; @@ -786,6 +791,7 @@ void RamGen::makeDecoderColumn( } decoder_layout->addCell(std::move(word_cell)); } + marking_periphery_ = false; ram_grid_.addLayout(std::move(decoder_layout)); } @@ -796,6 +802,7 @@ void RamGen::makeBufferColumn(const std::string& prefix, const int words_per_row) { auto buffer_layout = std::make_unique(odb::vertical); + marking_periphery_ = true; for (int row = 0; row < num_rows; ++row) { auto buf_cell = std::make_unique(); // a buffer per select: one per row, or with banks one per word @@ -815,6 +822,7 @@ void RamGen::makeBufferColumn(const std::string& prefix, } buffer_layout->addCell(std::move(buf_cell)); } + marking_periphery_ = false; ram_grid_.addLayout(std::move(buffer_layout)); } @@ -826,6 +834,7 @@ void RamGen::makeWordDecoderColumn( 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) { @@ -870,6 +879,7 @@ void RamGen::makeWordDecoderColumn( } decoder_layout->addCell(std::move(row_cell)); } + marking_periphery_ = false; ram_grid_.addLayout(std::move(decoder_layout)); } @@ -883,6 +893,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 @@ -929,6 +940,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) { @@ -1875,7 +1887,9 @@ void RamGen::generate(const int mask_size, const std::string& bank_order, const int rb_ports, const int read_latency, - const std::string& write_style) + const std::string& write_style, + const std::string& periphery, + const std::string& pin_layer) { // 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; @@ -1906,6 +1920,11 @@ void RamGen::generate(const int mask_size, rb_ports_ = rb_ports; read_latency_ = read_latency; write_mux_ = write_style == "mux"; + periphery_unplaced_ = periphery == "unplaced"; + 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."); @@ -2673,6 +2692,7 @@ void RamGen::generate(const int mask_size, for (int bit = 0; bit < mask_size; ++bit) { int bit_idx = bit + slice * mask_size; auto 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(), @@ -2683,6 +2703,7 @@ void RamGen::generate(const int mask_size, data_inputs[w][bit_idx]->getNet()}, {buffer_ports_[{PortRoleType::DataOut, 0}], D_nets[w][bit_idx]}}); } + marking_periphery_ = false; 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); @@ -2941,6 +2962,10 @@ 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(); + } writeBehavioralVerilog(behavioral_verilog_filename_, slices_per_word, @@ -2951,6 +2976,124 @@ void RamGen::generate(const int mask_size, w_ports); } +// -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 track grid inside the outline: these pins are for a macro +// placer to orient the core by, never routed. +void RamGen::unplacePeriphery() +{ + 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 track grid, 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; + }; + for (auto& [term, at] : pins) { + const int x = on_track(at.x() - dx, width); + const int y = on_track(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 unplaced: {} periphery instances left unplaced; " + "the core is {:.3f} x {:.3f} um.", + periphery_insts_.size(), + block_->dbuToMicrons(width), + block_->dbuToMicrons(height)); +} + void RamGen::setBehavioralVerilogFilename(const std::string& filename) { behavioral_verilog_filename_ = filename; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index eb390477769..5c34560b47e 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -50,7 +50,9 @@ generate_ram_netlist_cmd(int mask_size, const char* bank_order, const int rb_ports, const int read_latency, - const char* write_style) + const char* write_style, + const char* periphery, + const char* pin_layer) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -104,7 +106,7 @@ generate_ram_netlist_cmd(int mask_size, 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, - read_latency, write_style); + read_latency, write_style, periphery, pin_layer); } 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 8666aacf7be..ad711e88ebd 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -16,6 +16,8 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-rb_ports count] [-read_latency 0|1] [-write_style clock_gate|mux] + [-periphery placed|unplaced] + [-pin_layer layer] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -29,7 +31,8 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style + -periphery -pin_layer } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -175,6 +178,21 @@ proc generate_ram_netlist { args } { 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} } { + utl::error RAM 77 "-periphery is placed or unplaced, not $periphery." + } + } + set pin_layer "" + if { [info exists keys(-pin_layer)] } { + set pin_layer $keys(-pin_layer) + } + # 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 @@ -207,7 +225,7 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency $write_style + $rb_ports $read_latency $write_style $periphery $pin_layer } sta::define_cmd_args "generate_ram" {[-mask_size bits] diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index e7b9ed564d3..1291bb5f658 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -21,6 +21,7 @@ PASSFAIL_TESTS = [ "banked_read_ports_asap7", "read_latency_asap7", "write_mux_asap7", + "periphery_unplaced_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -93,6 +94,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "periphery_unplaced_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 7099d6fb617..7e9cb1d8030 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -14,5 +14,6 @@ or_integration_tests( banked_read_ports_asap7 read_latency_asap7 write_mux_asap7 + periphery_unplaced_asap7 ) diff --git a/src/ram/test/periphery_unplaced_asap7.tcl b/src/ram/test/periphery_unplaced_asap7.tcl new file mode 100644 index 00000000000..db93a1aaf89 --- /dev/null +++ b/src/ram/test/periphery_unplaced_asap7.tcl @@ -0,0 +1,71 @@ +# -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" +set m4 [[ord::get_db_tech] findLayer M4] +set pitch [$m4 getPitch] +set offset [$m4 getOffset] +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" + foreach c [list [expr { ([$box xMin] + [$box xMax]) / 2 }] \ + [expr { ([$box yMin] + [$box yMax]) / 2 }]] { + ram_sim::check { ($c - $offset) % $pitch == 0 } \ + "[$term getName] is off M4's tracks" + } + 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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 95d99ebe20b..b171d0334e8 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -13,5 +13,6 @@ record_tests { banked_read_ports_asap7 read_latency_asap7 write_mux_asap7 + periphery_unplaced_asap7 make_8x8_latch_sky130 } From 2a784a54f3cc753790a1bda340e5b322ec4d7e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 10/43] ram: generate_ram -cells and -name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -cells {role master ...} picks the cell for a role (inv, and2, buffer, clock_gate, aoi22, ao22, or2, nand2, nor2, tie, addr_flop) instead of the smallest the libraries have of its function. On asap7 the smallest are the xp cells a flow marks dont_use (INVxp33, AOI22xp33), and the smallest buffer is the hold buffer HB1xp67; a core the parent keeps fixed cannot be resized after. An AOI22 or AO22 given is checked by its truth table, and its pins are read from it; findMaster4 now reads them from the cell it picks, not from the last that matched in the search. -name names the block and the behavioral model, instead of RAMx, so two RAMs of one size can live in one design. Tested: src/ram/test/cells_name_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 4 + src/ram/include/ram/ram.h | 8 +- src/ram/src/ram.cpp | 213 ++++++++++++++++++++---------- src/ram/src/ram.i | 7 +- src/ram/src/ram.tcl | 23 +++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/cells_name_asap7.tcl | 42 ++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 224 insertions(+), 80 deletions(-) create mode 100644 src/ram/test/cells_name_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 9203ca06518..683015ddab5 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -52,6 +52,8 @@ generate_ram [-mask_size bits] [-rb_ports count] [-read_latency 0|1] [-write_style clock_gate|mux] + [-cells {role master ...}] + [-name name] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -85,6 +87,8 @@ generate_ram [-mask_size bits] | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | | `-periphery`, `-pin_layer` | `generate_ram_netlist` only. `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. Default: `placed`. | +| `-cells` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index f751f6c63d2..eea3100f247 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -120,7 +120,9 @@ class RamGen int read_latency = 0, const std::string& write_style = "clock_gate", const std::string& periphery = "placed", - const std::string& pin_layer = ""); + const std::string& pin_layer = "", + const std::string& cells = "", + const std::string& name = ""); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -201,7 +203,8 @@ class RamGen const char* name, std::array& inputs, std::string& output, - bool required = true); + bool required = true, + odb::dbMaster* given = nullptr); odb::dbNet* makeNet(const std::string& prefix, const std::string& name); odb::dbInst* makeInst( Cell* cell, @@ -380,6 +383,7 @@ class RamGen // 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 std::string pin_layer_; bool marking_periphery_{false}; Cell* periphery_cell_{nullptr}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 890e7b53f24..ab935f6238f 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1312,15 +1313,17 @@ void RamGen::findResetPins() // the unused pin of each flop held at its inactive level const bool off_high = reset_active_low_; - tie_off_cell_ = findMaster( - [off_high](sta::LibertyPort* port) { - auto f = port->function(); - return f && port->direction()->isOutput() - && f->op() - == (off_high ? sta::FuncExpr::Op::one - : sta::FuncExpr::Op::zero); - }, - off_high ? "tie high" : "tie low"); + if (!tie_off_cell_) { + tie_off_cell_ = findMaster( + [off_high](sta::LibertyPort* port) { + auto f = port->function(); + return f && port->direction()->isOutput() + && f->op() + == (off_high ? sta::FuncExpr::Op::one + : sta::FuncExpr::Op::zero); + }, + off_high ? "tie high" : "tie low"); + } tie_off_pin_ = buildPortMap(tie_off_cell_)[{PortRoleType::DataOut, 0}]; try { @@ -1490,10 +1493,12 @@ 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) + { + // 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_); + 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]; @@ -1519,10 +1524,10 @@ void RamGen::findMasters() // 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_cell_) { + 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_ = findMaster4( + 0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false, ao22_cell_); } if (num_write_ > 1) { if (!or2_cell_) { @@ -1546,7 +1551,8 @@ dbMaster* RamGen::findMaster4(const uint16_t table, const char* name, std::array& inputs_out, std::string& output, - const bool required) + 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. @@ -1581,60 +1587,77 @@ dbMaster* RamGen::findMaster4(const uint16_t table, {0, 3, 1, 2}, // group A=(p0,p3), group B=(p1,p2) }; - return findMaster( - [&](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(); - - // 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()); - } - } + 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) == 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; - } + // 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(); } - return false; - }, - name, - required); + 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, 79, "Cell {} is not a {} cell.", best->getName(), name); + } + } + return best; } void RamGen::ramPdngen(const char* power_net_name, @@ -1889,7 +1912,9 @@ void RamGen::generate(const int mask_size, const int read_latency, const std::string& write_style, const std::string& periphery, - const std::string& pin_layer) + const std::string& pin_layer, + const std::string& cells, + const std::string& name) { // 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; @@ -1956,7 +1981,10 @@ void RamGen::generate(const int mask_size, 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); + // the block's name: -name, or RAMx + const std::string ram_name + = name.empty() ? fmt::format("RAM{}x{}", num_words, word_size) : name; + ram_name_ = ram_name; // error checking for column_mux_ratio if (!banked_ && column_mux_ratio != 1 && column_mux_ratio != 2 @@ -2021,6 +2049,44 @@ void RamGen::generate(const int mask_size, 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); + std::string role; + std::string master_name; + 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_}}; + while (in >> role >> master_name) { + 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_); @@ -2062,8 +2128,11 @@ void RamGen::generate(const int mask_size, }; // the storage cell where it is one (findMaster passes over cells with // two outputs, as Nangate45's flops all are), else the smallest - if (storage_cell_ - && plain(network_->libertyCell(network_->dbToSta(storage_cell_)))) { + if (addr_flop_cell_) { + // given by -cells + } else if (storage_cell_ + && plain( + network_->libertyCell(network_->dbToSta(storage_cell_)))) { addr_flop_cell_ = storage_cell_; } else { addr_flop_cell_ = findMaster( @@ -3115,7 +3184,7 @@ 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; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 5c34560b47e..bc76a2c750d 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -52,7 +52,9 @@ generate_ram_netlist_cmd(int mask_size, const int read_latency, const char* write_style, const char* periphery, - const char* pin_layer) + const char* pin_layer, + const char* cells, + const char* name) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -106,7 +108,8 @@ generate_ram_netlist_cmd(int mask_size, 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, - read_latency, write_style, periphery, pin_layer); + read_latency, write_style, periphery, pin_layer, cells, + name); } 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 ad711e88ebd..93ed30dc072 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -16,6 +16,8 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-rb_ports count] [-read_latency 0|1] [-write_style clock_gate|mux] + [-cells {role master ...}] + [-name name] [-periphery placed|unplaced] [-pin_layer layer] [-inv_cell name] @@ -32,7 +34,7 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -periphery -pin_layer } flags {} + -periphery -pin_layer -cells -name } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -193,6 +195,17 @@ proc generate_ram_netlist { args } { 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) + } + # 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 @@ -225,7 +238,7 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency $write_style $periphery $pin_layer + $rb_ports $read_latency $write_style $periphery $pin_layer $cells $name } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -260,7 +273,8 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style } flags {} + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style + -cells -name } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -309,7 +323,8 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style} { + foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style + -cells -name} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 1291bb5f658..be028702f59 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -22,6 +22,7 @@ PASSFAIL_TESTS = [ "read_latency_asap7", "write_mux_asap7", "periphery_unplaced_asap7", + "cells_name_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -98,6 +99,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "cells_name_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 7e9cb1d8030..bf21a0d4d79 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -15,5 +15,6 @@ or_integration_tests( read_latency_asap7 write_mux_asap7 periphery_unplaced_asap7 + cells_name_asap7 ) diff --git a/src/ram/test/cells_name_asap7.tcl b/src/ram/test/cells_name_asap7.tcl new file mode 100644 index 00000000000..aa0e9c77e61 --- /dev/null +++ b/src/ram/test/cells_name_asap7.tcl @@ -0,0 +1,42 @@ +# -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" + +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 -name regfile \ + -cells {inv INVx2_ASAP7_75t_R buffer BUFx4_ASAP7_75t_R} + +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index b171d0334e8..db41cfc0279 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -14,5 +14,6 @@ record_tests { read_latency_asap7 write_mux_asap7 periphery_unplaced_asap7 + cells_name_asap7 make_8x8_latch_sky130 } From 795772336cacb102a9308cf6ee651456987e9b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 11/43] ram: generate_ram -decoder word, a balanced tree per word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -decoder word decodes each word's whole address through a balanced AND2 tree, the banks' decoders (the -banks commit) without banks. The default, row, is generate_ram's decoder per row, an AND2 chain over the row address: four AND2 deep for 32 rows where a tree is three. On riscv32i's register file the chain is about 60 ps of the read path's 220. Tested: src/ram/test/decoder_word_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 ++ src/ram/include/ram/ram.h | 3 ++- src/ram/src/ram.cpp | 7 +++++-- src/ram/src/ram.i | 5 +++-- src/ram/src/ram.tcl | 21 +++++++++++++++---- src/ram/test/BUILD | 5 +++++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/decoder_word_asap7.tcl | 32 +++++++++++++++++++++++++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/ram/test/decoder_word_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 683015ddab5..4e17cb11673 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -54,6 +54,7 @@ generate_ram [-mask_size bits] [-write_style clock_gate|mux] [-cells {role master ...}] [-name name] + [-decoder row|word] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -89,6 +90,7 @@ generate_ram [-mask_size bits] | `-periphery`, `-pin_layer` | `generate_ram_netlist` only. `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. Default: `placed`. | | `-cells` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | +| `-decoder` | `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`. | | `-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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index eea3100f247..d253b238188 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -122,7 +122,8 @@ class RamGen const std::string& periphery = "placed", const std::string& pin_layer = "", const std::string& cells = "", - const std::string& name = ""); + const std::string& name = "", + const std::string& decoder = "row"); void ramPdngen(const char* power_net_name, const char* ground_net_name, diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index ab935f6238f..cf0f024bda3 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1914,12 +1914,15 @@ void RamGen::generate(const int mask_size, const std::string& periphery, const std::string& pin_layer, const std::string& cells, - const std::string& name) + const std::string& name, + const std::string& decoder) { // 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. - banked_ = banks > 1 || bank_columns > 1; + // -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_ = std::max(banks, 1); bank_columns_ = std::max(bank_columns, 1); bank_interleaved_ = bank_order == "interleaved"; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index bc76a2c750d..41f4ec7c5c1 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -54,7 +54,8 @@ generate_ram_netlist_cmd(int mask_size, const char* periphery, const char* pin_layer, const char* cells, - const char* name) + const char* name, + const char* decoder) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -109,7 +110,7 @@ generate_ram_netlist_cmd(int mask_size, tapcell, max_tap_dist, read_style, zero_word, reset_value, banks, bank_columns, bank_order, rb_ports, read_latency, write_style, periphery, pin_layer, cells, - name); + name, decoder); } 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 93ed30dc072..61a0297a925 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -18,6 +18,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-write_style clock_gate|mux] [-cells {role master ...}] [-name name] + [-decoder row|word] [-periphery placed|unplaced] [-pin_layer layer] [-inv_cell name] @@ -34,7 +35,7 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -periphery -pin_layer -cells -name } flags {} + -periphery -pin_layer -cells -name -decoder } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -206,6 +207,17 @@ proc generate_ram_netlist { args } { 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. + 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 @@ -238,7 +250,8 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency $write_style $periphery $pin_layer $cells $name + $rb_ports $read_latency $write_style $periphery $pin_layer $cells $name \ + $decoder } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -274,7 +287,7 @@ proc generate_ram { args } { -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -cells -name } flags {} + -cells -name -decoder } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -324,7 +337,7 @@ proc generate_ram { args } { } foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -cells -name} { + -cells -name -decoder} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index be028702f59..3286ae0a670 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -23,6 +23,7 @@ PASSFAIL_TESTS = [ "write_mux_asap7", "periphery_unplaced_asap7", "cells_name_asap7", + "decoder_word_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -103,6 +104,10 @@ filegroup( "//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", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index bf21a0d4d79..3916b7070c0 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -16,5 +16,6 @@ or_integration_tests( write_mux_asap7 periphery_unplaced_asap7 cells_name_asap7 + decoder_word_asap7 ) diff --git a/src/ram/test/decoder_word_asap7.tcl b/src/ram/test/decoder_word_asap7.tcl new file mode 100644 index 00000000000..c248846671d --- /dev/null +++ b/src/ram/test/decoder_word_asap7.tcl @@ -0,0 +1,32 @@ +# -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" + +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 4 -word_size 4 -num_words 32 -r_ports 1 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -decoder word + +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index db41cfc0279..5843f8aaa88 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -15,5 +15,6 @@ record_tests { write_mux_asap7 periphery_unplaced_asap7 cells_name_asap7 + decoder_word_asap7 make_8x8_latch_sky130 } From 654faadca14ce1989e294872d88c73a0fe041688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 12/43] ram: generate_ram -bit_folds, a word's slices in rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -bit_folds k lays a word's mask slices (-mask_size, word/k rounded up) in k rows instead of side by side; where k does not divide the word the last fold is short, its missing bits empty columns (XiangShan's FtqMetaQueueRedirect, 211 bits in 4 folds). generate_ram puts a word in one row, a bit's cells side by side, so a wide word with many ports is a long thin array: XiangShan's integer register file, 64 bits, 12 read and 9 write ports, came out 1458 x 31 um. With folds the decoder, select-buffer and address-inverter columns take k rows a word, and a column's input buffers stack a row a fold above the words, so a wide word's buffers do not set the column's width. A folded word is written whole: one write enable a port, and the word's write logic (enables, priority, hold, clock gate, reset tie, select inverters) built in its first fold and shared by the others. Not with -use_latch or the row decoder's column mux. Tested: src/ram/test/bit_folds_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 26 ++++- src/ram/src/ram.cpp | 167 +++++++++++++++++++++++------- src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 18 +++- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/bit_folds_asap7.tcl | 34 ++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 213 insertions(+), 46 deletions(-) create mode 100644 src/ram/test/bit_folds_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 4e17cb11673..21c00d74575 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -55,6 +55,7 @@ generate_ram [-mask_size bits] [-cells {role master ...}] [-name name] [-decoder row|word] + [-bit_folds count] [-inv_cell name] [-power_net_name name] [-ground_net_name name] @@ -91,6 +92,7 @@ generate_ram [-mask_size bits] | `-cells` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | | `-decoder` | `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` | 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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index d253b238188..276af99c4f8 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -123,7 +123,8 @@ class RamGen const std::string& pin_layer = "", const std::string& cells = "", const std::string& name = "", - const std::string& decoder = "row"); + const std::string& decoder = "row", + int bit_folds = 1); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -385,6 +386,29 @@ class RamGen // 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_; + } std::string pin_layer_; bool marking_periphery_{false}; Cell* periphery_cell_{nullptr}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index cf0f024bda3..d4355802b16 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -350,19 +350,24 @@ void RamGen::makeSlice(const int slice_idx, } ram_grid_.addCell( std::move(cell), - slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) - + local_bit * column_mux_ratio + word_idx); + sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx); } ram_grid_.addCell( nullptr, - slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) - + mask_size * column_mux_ratio + word_idx); + sliceOffset(slice_idx) + mask_size * column_mux_ratio + word_idx); 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 (and_or_read_) { + 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) { @@ -376,13 +381,15 @@ void RamGen::makeSlice(const int slice_idx, // the flops' clock: the word's gated clock, or with -write_style mux // the clock itself - auto gclock_net = write_mux_ ? clock : makeNet(prefix, "gclock"); - auto we_net = makeNet(prefix, "we"); + 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 = nullptr; - if (write_mux_) { + dbNet* hold = shared ? word_write.hold : nullptr; + if (write_mux_ && !shared) { hold = makeNet(prefix, "hold"); makeInst(sel_cell.get(), prefix, @@ -432,8 +439,8 @@ void RamGen::makeSlice(const int slice_idx, }; // -reset_value: one tie per word slice holds the flops' unused pins off - dbNet* reset_off = nullptr; - if (reset_port_) { + dbNet* reset_off = shared ? word_write.reset_off : nullptr; + if (reset_port_ && !shared) { reset_off = makeNet(prefix, "reset_off"); makeInst(sel_cell.get(), prefix, @@ -448,7 +455,9 @@ void RamGen::makeSlice(const int slice_idx, // last port wins, as in the behavioral model. The clock gate opens for // any of them. vector take(num_write); - if (num_write > 1) { + 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)); @@ -492,7 +501,18 @@ void RamGen::makeSlice(const int slice_idx, } } + 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, + sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx); + 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) { @@ -608,13 +628,13 @@ void RamGen::makeSlice(const int slice_idx, } } - int bit_col = slice_idx * (mask_size * column_mux_ratio + column_mux_ratio) - + local_bit * column_mux_ratio + word_idx; + int bit_col + = sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx; ram_grid_.addCell(std::move(bit_cell), bit_col); } // Make clock gate (with -write_style mux, none: the flops hold by mux) - if (!write_mux_) { + if (!write_mux_ && !shared) { makeInst(sel_cell.get(), prefix, "cg", @@ -642,7 +662,7 @@ 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 - if (num_write == 1) { + if (num_write == 1 && !shared) { makeInst(sel_cell.get(), prefix, "gcand", @@ -653,7 +673,7 @@ void RamGen::makeSlice(const int slice_idx, } // Make select inverters - if (create_select_inv && !and_or_read_) { + 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 @@ -667,9 +687,10 @@ void RamGen::makeSlice(const int slice_idx, } } - 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 + = sliceOffset(slice_idx) + mask_size * column_mux_ratio + word_idx; + // 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 @@ -705,7 +726,8 @@ void RamGen::makeWord(const int slices_per_word, 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][slice]->getNet()); + slice_enables.push_back( + write_enable[w][bit_folds_ > 1 ? 0 : slice]->getNet()); } std::vector> slice_outputs; slice_outputs.reserve(r_total); @@ -791,6 +813,9 @@ 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)); @@ -822,6 +847,9 @@ void RamGen::makeBufferColumn(const std::string& prefix, {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)); @@ -879,6 +907,9 @@ void RamGen::makeWordDecoderColumn( } } 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)); @@ -1890,7 +1921,7 @@ 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, @@ -1915,7 +1946,8 @@ void RamGen::generate(const int mask_size, const std::string& pin_layer, const std::string& cells, const std::string& name, - const std::string& decoder) + const std::string& decoder, + const int bit_folds) { // 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; @@ -1980,13 +2012,42 @@ void RamGen::generate(const int mask_size, // 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; + bit_folds_ = std::max(bit_folds, 1); + 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; // the block's name: -name, or RAMx const std::string ram_name - = name.empty() ? fmt::format("RAM{}x{}", num_words, word_size) : name; + = name.empty() ? fmt::format("RAM{}x{}", num_words, word_bits) : name; ram_name_ = ram_name; // error checking for column_mux_ratio @@ -2039,6 +2100,7 @@ void RamGen::generate(const int mask_size, storage_inverted_ = false; storage_nets_.clear(); tree_nodes_.clear(); + word_write_.clear(); storage_cell_ = storage_cell; tristate_cell_ = tristate_cell; inv_cell_ = inv_cell; @@ -2205,8 +2267,8 @@ 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); @@ -2229,7 +2291,9 @@ void RamGen::generate(const int mask_size, 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); - for (int slice = 0; slice < slices_per_word; ++slice) { + // 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); } @@ -2395,6 +2459,16 @@ void RamGen::generate(const int mask_size, vector> data_inputs(num_write); vector> D_nets(num_write, vector(word_size)); for (int bit = 0; bit < word_size; ++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( @@ -2437,7 +2511,9 @@ void RamGen::generate(const int mask_size, 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]->getNet(); + word_q_nets[w][port][bit] = q_outputs_[port][bit] + ? q_outputs_[port][bit]->getNet() + : nullptr; } } } @@ -2500,7 +2576,7 @@ void RamGen::generate(const int mask_size, if (wordAt(row, w) == zero_word_) { continue; } - for (int bit = 0; bit < word_size; ++bit) { + 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}] @@ -2534,7 +2610,7 @@ void RamGen::generate(const int mask_size, 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_size; ++bit) { + 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); @@ -2760,10 +2836,22 @@ 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(); + int buf_col = sliceOffset(slice) + bit * column_mux_ratio; + 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( @@ -2776,11 +2864,11 @@ void RamGen::generate(const int mask_size, {buffer_ports_[{PortRoleType::DataOut, 0}], D_nets[w][bit_idx]}}); } marking_periphery_ = false; - 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); } } + for (auto& [key, cell] : buffer_cells) { + ram_grid_.addCell(std::move(cell), key.first); + } // indices for creating buffers int r_buf_idx = 0; @@ -2831,7 +2919,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, @@ -2930,7 +3018,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 @@ -3039,9 +3129,10 @@ void RamGen::generate(const int mask_size, 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, diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 41f4ec7c5c1..78d76a9488c 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -55,7 +55,8 @@ generate_ram_netlist_cmd(int mask_size, const char* pin_layer, const char* cells, const char* name, - const char* decoder) + const char* decoder, + const int bit_folds) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -110,7 +111,7 @@ generate_ram_netlist_cmd(int mask_size, tapcell, max_tap_dist, read_style, zero_word, reset_value, banks, bank_columns, bank_order, rb_ports, read_latency, write_style, periphery, pin_layer, cells, - name, decoder); + name, decoder, bit_folds); } 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 61a0297a925..fc5921e9355 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -19,6 +19,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-cells {role master ...}] [-name name] [-decoder row|word] + [-bit_folds count] [-periphery placed|unplaced] [-pin_layer layer] [-inv_cell name] @@ -35,7 +36,7 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -periphery -pin_layer -cells -name -decoder } flags {} + -periphery -pin_layer -cells -name -decoder -bit_folds } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -54,7 +55,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)." } @@ -210,6 +213,11 @@ proc generate_ram_netlist { args } { # 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. + 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) @@ -251,7 +259,7 @@ proc generate_ram_netlist { args } { $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 $read_latency $write_style $periphery $pin_layer $cells $name \ - $decoder + $decoder $bit_folds } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -287,7 +295,7 @@ proc generate_ram { args } { -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -cells -name -decoder } flags {} + -cells -name -decoder -bit_folds } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -337,7 +345,7 @@ proc generate_ram { args } { } foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -cells -name -decoder} { + -cells -name -decoder -bit_folds} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 3286ae0a670..c4d9a455322 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -24,6 +24,7 @@ PASSFAIL_TESTS = [ "periphery_unplaced_asap7", "cells_name_asap7", "decoder_word_asap7", + "bit_folds_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -108,6 +109,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "bit_folds_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 3916b7070c0..6549d2cc86e 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -17,5 +17,6 @@ or_integration_tests( periphery_unplaced_asap7 cells_name_asap7 decoder_word_asap7 + bit_folds_asap7 ) diff --git a/src/ram/test/bit_folds_asap7.tcl b/src/ram/test/bit_folds_asap7.tcl new file mode 100644 index 00000000000..0f5425599fd --- /dev/null +++ b/src/ram/test/bit_folds_asap7.tcl @@ -0,0 +1,34 @@ +# -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" + +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 3 -word_size 7 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -decoder word -bit_folds 3 + +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 5843f8aaa88..da84aab2bd4 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -16,5 +16,6 @@ record_tests { periphery_unplaced_asap7 cells_name_asap7 decoder_word_asap7 + bit_folds_asap7 make_8x8_latch_sky130 } From 61b46c9a92e1a810049c0d391a62f7686ecf5210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 13/43] ram: generate_ram_netlist -periphery verilog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -periphery verilog makes the block the core alone, _core, and writes the periphery to -wrapper_verilog as a module around an instance of it: the periphery's logic cells as assigns of their liberty functions, for synthesis to map with the parent's logic, and its other cells (clock gates, flops, ties) as instances. A net the core shares with the periphery becomes a core port; a port only the periphery uses leaves the core. The core is then outlined and pinned as with -periphery unplaced. An option beside generator-built periphery, not a replacement: which is better depends on how the array is integrated. Tested: src/ram/test/periphery_verilog_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 +- src/ram/include/ram/ram.h | 12 +- src/ram/src/ram.cpp | 242 ++++++++++++++++++++++- src/ram/src/ram.i | 5 +- src/ram/src/ram.tcl | 15 +- src/ram/test/BUILD | 5 + src/ram/test/CMakeLists.txt | 1 + src/ram/test/periphery_verilog_asap7.tcl | 38 ++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 309 insertions(+), 12 deletions(-) create mode 100644 src/ram/test/periphery_verilog_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 21c00d74575..ca5af6c2dde 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -88,7 +88,7 @@ generate_ram [-mask_size bits] | `-rb_ports` | 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. Default: 0. | | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | -| `-periphery`, `-pin_layer` | `generate_ram_netlist` only. `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. Default: `placed`. | +| `-periphery`, `-pin_layer`, `-wrapper_verilog` | `generate_ram_netlist` only. `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | | `-decoder` | `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`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 276af99c4f8..598a0b4a8ee 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -124,7 +124,8 @@ class RamGen const std::string& cells = "", const std::string& name = "", const std::string& decoder = "row", - int bit_folds = 1); + int bit_folds = 1, + const std::string& wrapper_verilog = ""); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -414,6 +415,15 @@ class RamGen 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}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index d4355802b16..6315d6b63c3 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1947,7 +1948,8 @@ void RamGen::generate(const int mask_size, const std::string& cells, const std::string& name, const std::string& decoder, - const int bit_folds) + const int bit_folds, + const std::string& wrapper_verilog) { // 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; @@ -1980,7 +1982,13 @@ void RamGen::generate(const int mask_size, rb_ports_ = rb_ports; read_latency_ = read_latency; write_mux_ = write_style == "mux"; - periphery_unplaced_ = periphery == "unplaced"; + 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."); + } pin_layer_ = pin_layer; periphery_insts_.clear(); marking_periphery_ = false; @@ -2246,7 +2254,10 @@ 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, @@ -3146,8 +3157,233 @@ void RamGen::generate(const int mask_size, // (those of the periphery too, while they are still placed), on the pin // layer's track grid inside the outline: these pins are for a macro // placer to orient the core by, never routed. +// A liberty function as a Verilog expression, its pins by their nets. +static std::string verilogExpr(const sta::FuncExpr* f, + const std::map& nets) +{ + if (f == nullptr) { + return "1'b0"; + } + switch (f->op()) { + case sta::FuncExpr::Op::port: { + auto it = nets.find(f->port()->name()); + return it == nets.end() ? "1'b0" : it->second; + } + case sta::FuncExpr::Op::not_: + return "~(" + verilogExpr(f->left(), nets) + ")"; + case sta::FuncExpr::Op::and_: + return "(" + verilogExpr(f->left(), nets) + " & " + + verilogExpr(f->right(), nets) + ")"; + case sta::FuncExpr::Op::or_: + return "(" + verilogExpr(f->left(), nets) + " | " + + verilogExpr(f->right(), nets) + ")"; + case sta::FuncExpr::Op::xor_: + return "(" + verilogExpr(f->left(), nets) + " ^ " + + verilogExpr(f->right(), nets) + ")"; + case sta::FuncExpr::Op::one: + return "1'b1"; + default: + return "1'b0"; + } +} + +// 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())) { + body << " assign " << nets[out->name()] << " = " + << verilogExpr(out->function(), nets) << "; // " << 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); + } + 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"; +} + void RamGen::unplacePeriphery() { + if (periphery_verilog_) { + splitPeriphery(); + } odb::dbTechLayer* layer = db_->getTech()->findLayer(pin_layer_.c_str()); if (layer == nullptr) { logger_->error(RAM, diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 78d76a9488c..0040503bfd7 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -56,7 +56,8 @@ generate_ram_netlist_cmd(int mask_size, const char* cells, const char* name, const char* decoder, - const int bit_folds) + const int bit_folds, + const char* wrapper_verilog) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -111,7 +112,7 @@ generate_ram_netlist_cmd(int mask_size, tapcell, max_tap_dist, read_style, zero_word, reset_value, banks, bank_columns, bank_order, rb_ports, read_latency, write_style, periphery, pin_layer, cells, - name, decoder, bit_folds); + name, decoder, bit_folds, wrapper_verilog); } 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 fc5921e9355..87eaf2694e8 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -20,7 +20,8 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-name name] [-decoder row|word] [-bit_folds count] - [-periphery placed|unplaced] + [-periphery placed|unplaced|verilog] + [-wrapper_verilog file] [-pin_layer layer] [-inv_cell name] [-rw_ports count] @@ -36,7 +37,7 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style - -periphery -pin_layer -cells -name -decoder -bit_folds } flags {} + -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -190,10 +191,14 @@ proc generate_ram_netlist { args } { set periphery placed if { [info exists keys(-periphery)] } { set periphery $keys(-periphery) - if { $periphery ni {placed unplaced} } { - utl::error RAM 77 "-periphery is placed or unplaced, not $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) @@ -259,7 +264,7 @@ proc generate_ram_netlist { args } { $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 $read_latency $write_style $periphery $pin_layer $cells $name \ - $decoder $bit_folds + $decoder $bit_folds $wrapper_verilog } sta::define_cmd_args "generate_ram" {[-mask_size bits] diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index c4d9a455322..24234b211d0 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -25,6 +25,7 @@ PASSFAIL_TESTS = [ "cells_name_asap7", "decoder_word_asap7", "bit_folds_asap7", + "periphery_verilog_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -113,6 +114,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "periphery_verilog_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 6549d2cc86e..a0c299f333e 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -18,5 +18,6 @@ or_integration_tests( cells_name_asap7 decoder_word_asap7 bit_folds_asap7 + periphery_verilog_asap7 ) diff --git a/src/ram/test/periphery_verilog_asap7.tcl b/src/ram/test/periphery_verilog_asap7.tcl new file mode 100644 index 00000000000..a99497949ff --- /dev/null +++ b/src/ram/test/periphery_verilog_asap7.tcl @@ -0,0 +1,38 @@ +# -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" + +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 + + +set wrapper [make_result_file periphery_verilog_asap7_wrapper.v] +generate_ram_netlist \ + -mask_size 8 -word_size 8 -num_words 8 -r_ports 2 -w_ports 1 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -name rf -pin_layer M4 \ + -periphery verilog -wrapper_verilog $wrapper + +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/regression_tests.tcl b/src/ram/test/regression_tests.tcl index da84aab2bd4..790c7e959d2 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -17,5 +17,6 @@ record_tests { cells_name_asap7 decoder_word_asap7 bit_folds_asap7 + periphery_verilog_asap7 make_8x8_latch_sky130 } From a296682e3d451d36e059015b6547151d423f4685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 14/43] ram: generate_ram -write_priority or, colliding writes OR'd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -write_priority says what a word takes when two write ports write it in one cycle: last, the last port's data, as before; or or, the OR of every writing port's data. firtool's register files (XiangShan's IntRegFile, FpRegFile, VfRegFile) merge their writes with an OR, so a generated register file in their place is equivalent to the RTL only with or; it also drops each word's priority logic, an inverter and an AND2 per port but the last. The behavioral model follows the option. The fourteenth of the generate_regfile features carried onto generate_ram (generate_regfile's default); see OpenROAD #11669. Not upstreamed. Tested: src/ram/test/write_priority_or_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 + src/ram/include/ram/ram.h | 4 ++ src/ram/src/ram.cpp | 64 ++++++++++++++++-------- src/ram/src/ram.i | 3 +- src/ram/src/ram.tcl | 19 +++++-- src/ram/test/BUILD | 5 ++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/write_priority_or_asap7.tcl | 26 ++++++++++ 9 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 src/ram/test/write_priority_or_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index ca5af6c2dde..bf440797734 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -52,6 +52,7 @@ generate_ram [-mask_size bits] [-rb_ports count] [-read_latency 0|1] [-write_style clock_gate|mux] + [-write_priority last|or] [-cells {role master ...}] [-name name] [-decoder row|word] @@ -88,6 +89,7 @@ generate_ram [-mask_size bits] | `-rb_ports` | 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. Default: 0. | | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | +| `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | | `-periphery`, `-pin_layer`, `-wrapper_verilog` | `generate_ram_netlist` only. `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 598a0b4a8ee..e3ce1cdfd74 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -119,6 +119,7 @@ class RamGen int rb_ports = 0, int read_latency = 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 = "", @@ -381,6 +382,9 @@ class RamGen // -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 diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 6315d6b63c3..bed043448e6 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -453,8 +453,9 @@ void RamGen::makeSlice(const int slice_idx, // 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. The clock gate opens for - // any of them. + // 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; @@ -473,21 +474,25 @@ void RamGen::makeSlice(const int slice_idx, take[num_write - 1] = we[num_write - 1]; dbNet* later = we[num_write - 1]; for (int w = num_write - 2; w >= 0; --w) { - 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]}}); + 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)); @@ -1943,6 +1948,7 @@ void RamGen::generate(const int mask_size, const int rb_ports, const int read_latency, const std::string& write_style, + const std::string& write_priority, const std::string& periphery, const std::string& pin_layer, const std::string& cells, @@ -1982,6 +1988,7 @@ void RamGen::generate(const int mask_size, rb_ports_ = rb_ports; read_latency_ = read_latency; write_mux_ = write_style == "mux"; + write_or_ = write_priority == "or"; periphery_verilog_ = periphery == "verilog"; periphery_unplaced_ = periphery == "unplaced" || periphery_verilog_; wrapper_verilog_ = wrapper_verilog; @@ -3701,13 +3708,28 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, } // Write logic, the ports in order: where two write the same word in a - // cycle, the last one's data is kept. + // 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( @@ -3715,7 +3737,7 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, write_logic += fmt::format( R"( for (i = 0; i < {}; i = i + 1) begin if ({}[i]{}) begin - mem[{}][i*{} +:{}] <= {}[i*{} +:{}]; + mem[{}][i*{} +:{}] <= {}; end end )", @@ -3726,9 +3748,7 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, write_addr, mask_size, mask_size, - wbus("D", w), - mask_size, - mask_size); + write_data); } std::string reset_declaration; diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 0040503bfd7..429ae8b7632 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -51,6 +51,7 @@ generate_ram_netlist_cmd(int mask_size, const int rb_ports, const int read_latency, const char* write_style, + const char* write_priority, const char* periphery, const char* pin_layer, const char* cells, @@ -111,7 +112,7 @@ generate_ram_netlist_cmd(int mask_size, 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, - read_latency, write_style, periphery, pin_layer, cells, + read_latency, write_style, write_priority, periphery, pin_layer, cells, name, decoder, bit_folds, wrapper_verilog); } diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 87eaf2694e8..25be620cbeb 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -16,6 +16,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-rb_ports count] [-read_latency 0|1] [-write_style clock_gate|mux] + [-write_priority last|or] [-cells {role master ...}] [-name name] [-decoder row|word] @@ -36,7 +37,7 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog } flags {} set column_mux_ratio 1 @@ -241,6 +242,16 @@ proc generate_ram_netlist { args } { } } + # 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." + } + } + # A read latency of 1: the read addresses registered. set read_latency 0 if { [info exists keys(-read_latency)] } { @@ -263,7 +274,7 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency $write_style $periphery $pin_layer $cells $name \ + $rb_ports $read_latency $write_style $write_priority $periphery $pin_layer $cells $name \ $decoder $bit_folds $wrapper_verilog } @@ -299,7 +310,7 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style + -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority -cells -name -decoder -bit_folds } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -349,7 +360,7 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style + foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority -cells -name -decoder -bit_folds} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 24234b211d0..aff5750bbe1 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -26,6 +26,7 @@ PASSFAIL_TESTS = [ "decoder_word_asap7", "bit_folds_asap7", "periphery_verilog_asap7", + "write_priority_or_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -118,6 +119,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "write_priority_or_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index a0c299f333e..4cabed6372a 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -19,5 +19,6 @@ or_integration_tests( decoder_word_asap7 bit_folds_asap7 periphery_verilog_asap7 + write_priority_or_asap7 ) diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 790c7e959d2..9a8d75ef735 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -18,5 +18,6 @@ record_tests { decoder_word_asap7 bit_folds_asap7 periphery_verilog_asap7 + write_priority_or_asap7 make_8x8_latch_sky130 } 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..4e76a7a52da --- /dev/null +++ b/src/ram/test/write_priority_or_asap7.tcl @@ -0,0 +1,26 @@ +# -write_priority or: where both write ports write a word in one cycle it +# takes the OR of their data, as firtool's register files describe it. +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 2 \ + -storage_cell DFFHQNx1_ASAP7_75t_R -write_style mux -write_priority or + +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" From 6adb39e034dbe0bc3244ad05bb1953883dbeb3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 15/43] ram: generate_ram -service_columns, room in the array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -service_columns {every sites} puts an empty column `sites` sites wide after every `every` storage columns. An array of placed standard cells is full: a parent flow that buffers the long nets crossing it (repair inserts wire buffers on the word selects and read bitlines) has no site to put them on, and legalization fails. XiangShan's RobEntryFile, 352 words of 21 bits with 16 read and 8 write ports, dissolved into a parent, left 1,230 such buffers overlapping without these columns. The columns are inside the core, so -periphery unplaced keeps them in the outline. Default: none, as before. Tested: src/ram/test/service_columns_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 ++ src/ram/include/ram/layout.h | 3 ++ src/ram/include/ram/ram.h | 4 ++- src/ram/src/layout.cpp | 5 +++ src/ram/src/ram.cpp | 20 ++++++++++- src/ram/src/ram.i | 7 ++-- src/ram/src/ram.tcl | 20 ++++++++--- src/ram/test/BUILD | 5 +++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/service_columns_asap7.tcl | 49 ++++++++++++++++++++++++++ 11 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 src/ram/test/service_columns_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index bf440797734..82a25c997ba 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -53,6 +53,7 @@ generate_ram [-mask_size bits] [-read_latency 0|1] [-write_style clock_gate|mux] [-write_priority last|or] + [-service_columns {every sites}] [-cells {role master ...}] [-name name] [-decoder row|word] @@ -90,6 +91,7 @@ generate_ram [-mask_size bits] | `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | | `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | +| `-service_columns {every sites}` | An empty column `sites` sites wide after every `every` storage columns: 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. | | `-periphery`, `-pin_layer`, `-wrapper_verilog` | `generate_ram_netlist` only. `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | 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 e3ce1cdfd74..32ce0252904 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -126,7 +126,9 @@ class RamGen const std::string& name = "", const std::string& decoder = "row", int bit_folds = 1, - const std::string& wrapper_verilog = ""); + const std::string& wrapper_verilog = "", + int service_every = 0, + int service_sites = 0); void ramPdngen(const char* power_net_name, const char* ground_net_name, 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 bed043448e6..d991e942b53 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1955,7 +1955,9 @@ void RamGen::generate(const int mask_size, const std::string& name, const std::string& decoder, const int bit_folds, - const std::string& wrapper_verilog) + const std::string& wrapper_verilog, + const int service_every, + const int service_sites) { // 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; @@ -3075,6 +3077,22 @@ void RamGen::generate(const int mask_size, ram_grid_.setOrigin(ram_origin); ram_grid_.gridInit(); + // -service_columns {every sites}: after every `every` storage 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) { diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 429ae8b7632..b70e571e2f8 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -58,7 +58,9 @@ generate_ram_netlist_cmd(int mask_size, const char* name, const char* decoder, const int bit_folds, - const char* wrapper_verilog) + const char* wrapper_verilog, + const int service_every, + const int service_sites) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -113,7 +115,8 @@ generate_ram_netlist_cmd(int mask_size, tapcell, max_tap_dist, read_style, zero_word, reset_value, banks, bank_columns, bank_order, rb_ports, read_latency, write_style, write_priority, periphery, pin_layer, cells, - name, decoder, bit_folds, wrapper_verilog); + name, decoder, bit_folds, wrapper_verilog, + service_every, service_sites); } 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 25be620cbeb..cf56d3b2e78 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -24,6 +24,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-periphery placed|unplaced|verilog] [-wrapper_verilog file] [-pin_layer layer] + [-service_columns {every sites}] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -38,7 +39,7 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog } flags {} + -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog -service_columns } flags {} set column_mux_ratio 1 if { [info exists keys(-column_mux_ratio)] } { @@ -220,6 +221,17 @@ proc generate_ram_netlist { args } { # 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 + } + set bit_folds 1 if { [info exists keys(-bit_folds)] } { set bit_folds $keys(-bit_folds) @@ -275,7 +287,7 @@ proc generate_ram_netlist { args } { $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 $read_latency $write_style $write_priority $periphery $pin_layer $cells $name \ - $decoder $bit_folds $wrapper_verilog + $decoder $bit_folds $wrapper_verilog $service_every $service_sites } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -311,7 +323,7 @@ proc generate_ram { args } { -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -cells -name -decoder -bit_folds } flags {} + -cells -name -decoder -bit_folds -service_columns } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -361,7 +373,7 @@ proc generate_ram { args } { } foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -cells -name -decoder -bit_folds} { + -cells -name -decoder -bit_folds -service_columns} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index aff5750bbe1..b93398ec49e 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -27,6 +27,7 @@ PASSFAIL_TESTS = [ "bit_folds_asap7", "periphery_verilog_asap7", "write_priority_or_asap7", + "service_columns_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -123,6 +124,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "service_columns_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 4cabed6372a..b439e920428 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -20,5 +20,6 @@ or_integration_tests( bit_folds_asap7 periphery_verilog_asap7 write_priority_or_asap7 + service_columns_asap7 ) diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 9a8d75ef735..e3f33616eee 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -19,5 +19,6 @@ record_tests { bit_folds_asap7 periphery_verilog_asap7 write_priority_or_asap7 + service_columns_asap7 make_8x8_latch_sky130 } diff --git a/src/ram/test/service_columns_asap7.tcl b/src/ram/test/service_columns_asap7.tcl new file mode 100644 index 00000000000..66dab3c934f --- /dev/null +++ b/src/ram/test/service_columns_asap7.tcl @@ -0,0 +1,49 @@ +# -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" + +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 -service_columns {4 10} + +set block [ord::get_db_block] +set site [[lindex [[lindex [[ord::get_db] getLibs] 0] getSites] 0] getWidth] +set spans {} +foreach inst [$block getInsts] { + if { [$inst isPlaced] } { + 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" From a9bc37680ba95d8465a99e71c52f1b7c425e66fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:12:48 +0200 Subject: [PATCH 16/43] ram: generate_ram -bank_column_layout, a word's bits contiguous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With banks a row holds -bank_columns words, laid out as the column mux lays out its words: bit by bit, interleaved. Each word is decoded whole, so nothing reads the row through a mux, and the interleave only costs: a word's selects and write enables run the whole row to reach its bits. -bank_column_layout contiguous puts each word's bits side by side with its select column after them; the per-bit read trees cross the row instead. XiangShan's RobEntryFile (352 words of 21 bits, 16 read and 8 write ports, 4 words a row) had rows 278 um wide: dissolved into a parent, the resizer found 6,483 slew violations on those nets and put 6,043 buffers inside the full array, which detailed placement could not legalize. Default: interleaved, as before. Tested: src/ram/test/bank_column_layout_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 ++ src/ram/include/ram/ram.h | 21 +++++++++++- src/ram/src/ram.cpp | 33 ++++++++++--------- src/ram/src/ram.i | 5 +-- src/ram/src/ram.tcl | 23 ++++++++++--- src/ram/test/BUILD | 5 +++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/bank_column_layout_asap7.tcl | 40 +++++++++++++++++++++++ src/ram/test/regression_tests.tcl | 1 + 9 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 src/ram/test/bank_column_layout_asap7.tcl diff --git a/src/ram/README.md b/src/ram/README.md index 82a25c997ba..bf0abbcc094 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -54,6 +54,7 @@ generate_ram [-mask_size bits] [-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] @@ -92,6 +93,7 @@ generate_ram [-mask_size bits] | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | | `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | | `-service_columns {every sites}` | An empty column `sites` sites wide after every `every` storage columns: 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` | 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 then cross the row instead. Default: `interleaved`. | | `-periphery`, `-pin_layer`, `-wrapper_verilog` | `generate_ram_netlist` only. `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 32ce0252904..eca32b997c9 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -128,7 +128,8 @@ class RamGen int bit_folds = 1, const std::string& wrapper_verilog = "", int service_every = 0, - int service_sites = 0); + int service_sites = 0, + const std::string& bank_column_layout = "interleaved"); void ramPdngen(const char* power_net_name, const char* ground_net_name, @@ -416,6 +417,24 @@ class RamGen { 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 bitColumn(int slice, int bit, int word, int mask_size) const + { + return sliceOffset(slice) + + (words_contiguous_ ? word * (mask_size + 1) + bit + : bit * words_per_row_ + word); + } + int selColumn(int slice, int word, int mask_size) const + { + return sliceOffset(slice) + + (words_contiguous_ ? word * (mask_size + 1) + mask_size + : mask_size * words_per_row_ + word); + } std::string pin_layer_; bool marking_periphery_{false}; Cell* periphery_cell_{nullptr}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index d991e942b53..f9f91311042 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -349,13 +349,10 @@ void RamGen::makeSlice(const int slice_idx, makeInst(cell.get(), node.prefix, node.name, node.master, node.conns); } } - ram_grid_.addCell( - std::move(cell), - sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx); + ram_grid_.addCell(std::move(cell), + bitColumn(slice_idx, local_bit, word_idx, mask_size)); } - ram_grid_.addCell( - nullptr, - sliceOffset(slice_idx) + mask_size * column_mux_ratio + word_idx); + ram_grid_.addCell(nullptr, selColumn(slice_idx, word_idx, mask_size)); return; } @@ -514,9 +511,8 @@ void RamGen::makeSlice(const int slice_idx, 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, - sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx); + 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); @@ -634,8 +630,7 @@ void RamGen::makeSlice(const int slice_idx, } } - int bit_col - = sliceOffset(slice_idx) + local_bit * column_mux_ratio + word_idx; + int bit_col = bitColumn(slice_idx, local_bit, word_idx, mask_size); ram_grid_.addCell(std::move(bit_cell), bit_col); } @@ -693,8 +688,7 @@ void RamGen::makeSlice(const int slice_idx, } } - int sel_col - = sliceOffset(slice_idx) + mask_size * column_mux_ratio + word_idx; + 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); } @@ -1957,7 +1951,8 @@ void RamGen::generate(const int mask_size, const int bit_folds, const std::string& wrapper_verilog, const int service_every, - const int service_sites) + const int service_sites, + const std::string& bank_column_layout) { // 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; @@ -2029,6 +2024,14 @@ void RamGen::generate(const int mask_size, // 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_ = std::max(bit_folds, 1); 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 @@ -2863,7 +2866,7 @@ void RamGen::generate(const int mask_size, for (int slice = 0; slice < slices_per_word; ++slice) { for (int bit = 0; bit < mask_size; ++bit) { int bit_idx = bit + slice * mask_size; - int buf_col = sliceOffset(slice) + bit * column_mux_ratio; + 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_) { diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index b70e571e2f8..969aef4d08a 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -60,7 +60,8 @@ generate_ram_netlist_cmd(int mask_size, const int bit_folds, const char* wrapper_verilog, const int service_every, - const int service_sites) + const int service_sites, + const char* bank_column_layout) { RamGen* ram_gen = ord::getRamGen(); odb::dbDatabase* db = ord::getDb(); @@ -116,7 +117,7 @@ generate_ram_netlist_cmd(int mask_size, reset_value, banks, bank_columns, bank_order, rb_ports, read_latency, write_style, write_priority, periphery, pin_layer, cells, name, decoder, bit_folds, wrapper_verilog, - service_every, service_sites); + 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 cf56d3b2e78..84044c935c1 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -25,6 +25,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-wrapper_verilog file] [-pin_layer layer] [-service_columns {every sites}] + [-bank_column_layout interleaved|contiguous] [-inv_cell name] [-rw_ports count] [-r_ports count] @@ -39,7 +40,8 @@ proc generate_ram_netlist { args } { -rw_ports -r_ports -w_ports -use_latch -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -periphery -pin_layer -cells -name -decoder -bit_folds -wrapper_verilog -service_columns } flags {} + -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)] } { @@ -232,6 +234,16 @@ proc generate_ram_netlist { args } { lassign $keys(-service_columns) service_every service_sites } + # 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) @@ -287,7 +299,8 @@ proc generate_ram_netlist { args } { $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 $read_latency $write_style $write_priority $periphery $pin_layer $cells $name \ - $decoder $bit_folds $wrapper_verilog $service_every $service_sites + $decoder $bit_folds $wrapper_verilog $service_every $service_sites \ + $bank_column_layout } sta::define_cmd_args "generate_ram" {[-mask_size bits] @@ -323,7 +336,8 @@ proc generate_ram { args } { -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -cells -name -decoder -bit_folds -service_columns } flags {} + -cells -name -decoder -bit_folds -service_columns + -bank_column_layout } flags {} sta::check_argc_eq0 "generate_ram" $args @@ -373,7 +387,8 @@ proc generate_ram { args } { } foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority - -cells -name -decoder -bit_folds -service_columns} { + -cells -name -decoder -bit_folds -service_columns + -bank_column_layout} { if { [info exists keys($key)] } { lappend ram_netlist_args $key $keys($key) } diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index b93398ec49e..589df221fd1 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -28,6 +28,7 @@ PASSFAIL_TESTS = [ "periphery_verilog_asap7", "write_priority_or_asap7", "service_columns_asap7", + "bank_column_layout_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -128,6 +129,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "bank_column_layout_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index b439e920428..3fa985a9880 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -21,5 +21,6 @@ or_integration_tests( periphery_verilog_asap7 write_priority_or_asap7 service_columns_asap7 + bank_column_layout_asap7 ) 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..e88a162b5ed --- /dev/null +++ b/src/ram/test/bank_column_layout_asap7.tcl @@ -0,0 +1,40 @@ +# -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. + +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 -banks 4 -bank_columns 2 \ + -bank_column_layout contiguous + +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 8 bits 8 + writes {{addr_w D {we[0]}}} + reads {{addr_r0 Q0} {addr_r1 Q1}} +} + +puts "pass" diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index e3f33616eee..38fac14593e 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -20,5 +20,6 @@ record_tests { periphery_verilog_asap7 write_priority_or_asap7 service_columns_asap7 + bank_column_layout_asap7 make_8x8_latch_sky130 } From 00e99e3357da9b6df10796c5cb522111530bcbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:20:25 +0200 Subject: [PATCH 17/43] ram: stop on out-of-range option values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A value out of range stopped nothing: -banks, -bank_columns and -bit_folds below 1 were clamped to 1, a negative -read_latency read as 0, a negative -zero_word as no zero word, -service_columns below 1 was ignored, and a negative -rb_ports went into the read port count. -rb_ports 0 with no other read port also passed the check for at least one read port. Each now stops with its own message (RAM-0036, RAM-0071, RAM-0092 to RAM-0096). Tested: src/ram/test/option_errors_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 25 +++++++++++++----- src/ram/src/ram.tcl | 14 +++++++++- src/ram/test/BUILD | 5 ++++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/option_errors_asap7.tcl | 38 ++++++++++++++++++++++++++++ src/ram/test/regression_tests.tcl | 1 + 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/ram/test/option_errors_asap7.tcl diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index f9f91311042..f442b4fc4f4 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1954,14 +1954,27 @@ void RamGen::generate(const int mask_size, 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_ = std::max(banks, 1); - bank_columns_ = std::max(bank_columns, 1); + banks_ = banks; + bank_columns_ = bank_columns; bank_interleaved_ = bank_order == "interleaved"; if (banked_) { if (column_mux_ratio_arg != 1) { @@ -2001,10 +2014,10 @@ void RamGen::generate(const int mask_size, logger_->error( RAM, 73, "-write_style mux is not supported with -use_latch."); } + if (read_latency_ != 0 && read_latency_ != 1) { + logger_->error(RAM, 71, "-read_latency is 0 or 1."); + } if (read_latency_ > 0) { - if (read_latency_ != 1) { - logger_->error(RAM, 71, "-read_latency is 0 or 1."); - } if (column_mux_ratio_arg > 1 || rw_ports > 0) { logger_->error(RAM, 72, @@ -2032,7 +2045,7 @@ void RamGen::generate(const int mask_size, "or -decoder word: the column mux reads a row's words " "interleaved."); } - bit_folds_ = std::max(bit_folds, 1); + 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 diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 84044c935c1..56b221e732d 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -119,7 +119,12 @@ proc generate_ram_netlist { args } { utl::error RAM 53 "A read/write port is the only write port." } - if { $rw_ports + $r_ports < 1 && ![info exists keys(-rb_ports)] } { + # 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" } @@ -171,6 +176,9 @@ proc generate_ram_netlist { args } { 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. @@ -232,6 +240,10 @@ proc generate_ram_netlist { args } { 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 diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 589df221fd1..4e292a3f20c 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -29,6 +29,7 @@ PASSFAIL_TESTS = [ "write_priority_or_asap7", "service_columns_asap7", "bank_column_layout_asap7", + "option_errors_asap7", ] ALL_TESTS = COMPULSORY_TESTS + PASSFAIL_TESTS @@ -133,6 +134,10 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], + "option_errors_asap7": [ + "//test:asap7_data", + "//test:asap7_nldm_data", + ], "make_7x7_2r1w_nangate45": [ "make_7x7_2r1w_nangate45_behavioral.vok", "Nangate45/Nangate45.lef", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 3fa985a9880..70ea44f7b60 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -22,5 +22,6 @@ or_integration_tests( write_priority_or_asap7 service_columns_asap7 bank_column_layout_asap7 + option_errors_asap7 ) diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl new file mode 100644 index 00000000000..40a0d9504d5 --- /dev/null +++ b/src/ram/test/option_errors_asap7.tcl @@ -0,0 +1,38 @@ +# Out-of-range options stop generation, each with its own message. + +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 + + +set base {-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} + 92 {-banks 0} + 92 {-bank_columns -1} + 95 {-service_columns {0 10}} + 93 {-bit_folds 0} + 71 {-read_latency -1} + 94 {-banks 2 -rb_ports -1} +} { + set args [dict merge $base $options] + if { ![catch { generate_ram_netlist {*}$args } message] } { + error "generate_ram_netlist $options did not stop" + } + if { ![string match "*RAM-00$id*" $message] } { + error "generate_ram_netlist $options stopped with: $message, not RAM-00$id" + } + puts "$options: RAM-00$id" +} + +puts "pass" diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 38fac14593e..0787bafd143 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -21,5 +21,6 @@ record_tests { write_priority_or_asap7 service_columns_asap7 bank_column_layout_asap7 + option_errors_asap7 make_8x8_latch_sky130 } From 546f5d38c67c49cb28036137a211bada2b2dd69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 13 Oct 2026 17:24:04 +0200 Subject: [PATCH 18/43] ram: test BUILD's per-test data in sorted order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buildifier's unsorted-dict-items lint: the new tests' entries in the per-test data dictionary are sorted by name. No change to what any test reads. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/test/BUILD | 62 +++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 4e292a3f20c..7c006674135 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -69,20 +69,15 @@ filegroup( test_name + ".*", ], ) + { - "write_ports_nangate45": [ - "Nangate45/Nangate45.lef", - "Nangate45/Nangate45_tech.lef", - "Nangate45/Nangate45_typ.lib", - ], "and_or_read_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "zero_word_asap7": [ + "bank_column_layout_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "reset_asap7": [ + "banked_read_ports_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], @@ -90,31 +85,37 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], - "banked_read_ports_asap7": [ + "bit_folds_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "read_latency_asap7": [ + "cells_name_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "write_mux_asap7": [ + "decoder_word_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "periphery_unplaced_asap7": [ - "//test:asap7_data", - "//test:asap7_nldm_data", + "make_7x7_2r1w_nangate45": [ + "make_7x7_2r1w_nangate45_behavioral.vok", + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_tech.lef", + "Nangate45/Nangate45_typ.lib", ], - "cells_name_asap7": [ - "//test:asap7_data", - "//test:asap7_nldm_data", + "make_7x7_nangate45": [ + "make_7x7_nangate45_behavioral.vok", + "Nangate45/Nangate45.lef", + "Nangate45/Nangate45_tech.lef", + "Nangate45/Nangate45_typ.lib", ], - "decoder_word_asap7": [ + "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", ], - "bit_folds_asap7": [ + "periphery_unplaced_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], @@ -122,36 +123,35 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], - "write_priority_or_asap7": [ + "read_latency_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "service_columns_asap7": [ + "reset_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "bank_column_layout_asap7": [ + "service_columns_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "option_errors_asap7": [ + "write_mux_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], - "make_7x7_2r1w_nangate45": [ - "make_7x7_2r1w_nangate45_behavioral.vok", + "write_ports_nangate45": [ "Nangate45/Nangate45.lef", "Nangate45/Nangate45_tech.lef", "Nangate45/Nangate45_typ.lib", ], - "make_7x7_nangate45": [ - "make_7x7_nangate45_behavioral.vok", - "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", ], - "make_8x8_2r1w_sky130": ["make_8x8_2r1w_sky130_behavioral.vok"], - "make_8x8_sky130": ["make_8x8_sky130_behavioral.vok"], }.get(test_name, []), ) for test_name in ALL_TESTS From dd7088604cb860e58135f5258af6ce70cfff299b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 14 Oct 2026 10:32:52 +0200 Subject: [PATCH 19/43] ram: -read_latency is -register_read_address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The option registers each read port's address: the decoders and read trees come after the flop, and the data follows a cycle after the address. -read_latency read as a register on the data, a different circuit. 0 and 1 only; more than one cycle stops with RAM-0071. Tested: src/ram/test/register_read_address_asap7.tcl, src/ram/test/option_errors_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 4 +-- src/ram/include/ram/ram.h | 6 ++-- src/ram/src/ram.cpp | 36 +++++++++++-------- src/ram/src/ram.i | 4 +-- src/ram/src/ram.tcl | 19 +++++----- src/ram/test/BUILD | 4 +-- src/ram/test/CMakeLists.txt | 2 +- src/ram/test/option_errors_asap7.tcl | 2 +- ...p7.tcl => register_read_address_asap7.tcl} | 4 +-- src/ram/test/regression_tests.tcl | 2 +- 10 files changed, 46 insertions(+), 37 deletions(-) rename src/ram/test/{read_latency_asap7.tcl => register_read_address_asap7.tcl} (89%) diff --git a/src/ram/README.md b/src/ram/README.md index bf0abbcc094..e5a52fa91bc 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -50,7 +50,7 @@ generate_ram [-mask_size bits] [-bank_columns count] [-bank_order contiguous|interleaved] [-rb_ports count] - [-read_latency 0|1] + [-register_read_address 0|1] [-write_style clock_gate|mux] [-write_priority last|or] [-service_columns {every sites}] @@ -89,7 +89,7 @@ generate_ram [-mask_size bits] | `-reset_value` | Resets every word to `value` 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. 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` | 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` | 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. Default: 0. | -| `-read_latency` | `1` registers every read port's address, a plain positive-edge flop per address bit, so the data follows a cycle after the address. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | +| `-register_read_address` | `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. 0 and 1 only. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | | `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | | `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | | `-service_columns {every sites}` | An empty column `sites` sites wide after every `every` storage columns: 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. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index eca32b997c9..e329341edb2 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -117,7 +117,7 @@ class RamGen int bank_columns = 1, const std::string& bank_order = "contiguous", int rb_ports = 0, - int read_latency = 0, + int register_read_address = 0, const std::string& write_style = "clock_gate", const std::string& write_priority = "last", const std::string& periphery = "placed", @@ -376,12 +376,12 @@ class RamGen // 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; - // -read_latency 1: each read port's address through a flop (a plain + // -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 read_latency_{0}; + 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}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index f442b4fc4f4..fcadc384d45 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -944,7 +944,8 @@ std::unique_ptr RamGen::makeInverterColumn( const int a = num_word_bits + bit; dbNet* flop_out = addr_flop_out_[p][a]; if (flop_out) { - // -read_latency 1: the address through a flop, beside its inverter + // -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), @@ -1940,7 +1941,7 @@ void RamGen::generate(const int mask_size, const int bank_columns, const std::string& bank_order, const int rb_ports, - const int read_latency, + const int register_read_address, const std::string& write_style, const std::string& write_priority, const std::string& periphery, @@ -1996,7 +1997,7 @@ void RamGen::generate(const int mask_size, } words_per_bank_ = num_words / banks_; rb_ports_ = rb_ports; - read_latency_ = read_latency; + register_read_address_ = register_read_address; write_mux_ = write_style == "mux"; write_or_ = write_priority == "or"; periphery_verilog_ = periphery == "verilog"; @@ -2014,15 +2015,19 @@ void RamGen::generate(const int mask_size, logger_->error( RAM, 73, "-write_style mux is not supported with -use_latch."); } - if (read_latency_ != 0 && read_latency_ != 1) { - logger_->error(RAM, 71, "-read_latency is 0 or 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 (read_latency_ > 0) { + if (register_read_address_ > 0) { if (column_mux_ratio_arg > 1 || rw_ports > 0) { - logger_->error(RAM, - 72, - "-read_latency 1 is not supported with -column_mux_ratio " - "above 1 or with read/write ports."); + 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; @@ -2212,8 +2217,8 @@ void RamGen::generate(const int mask_size, findMasters(); - // -read_latency 1: a plain positive-edge flop for the read addresses - if (read_latency_ > 0) { + // -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 auto plain = [](const sta::LibertyCell* cell) { for (const auto& seq : cell->sequentials()) { @@ -2393,7 +2398,8 @@ void RamGen::generate(const int mask_size, inv_addr_nets[p].resize(port_inputs); addr_true_[p].resize(port_inputs); addr_flop_out_[p].assign(port_inputs, nullptr); - const bool registered = read_latency_ > 0 && p >= rw_ports + w_ports; + const bool registered + = register_read_address_ > 0 && p >= rw_ports + w_ports; for (int i = 0; i < port_inputs; ++i) { addr_inputs_[p][i] @@ -3696,9 +3702,9 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, } } - // -read_latency 1: every read address registered, the reads from the + // -register_read_address 1: every read address registered, the reads from the // registered ones (addr_r0 -> addr_r0_q) - if (read_latency_ > 0) { + if (register_read_address_ > 0) { std::string registers; auto registered = [&](const std::string& name, int width) { registers += fmt::format( diff --git a/src/ram/src/ram.i b/src/ram/src/ram.i index 969aef4d08a..10729eaa8e9 100644 --- a/src/ram/src/ram.i +++ b/src/ram/src/ram.i @@ -49,7 +49,7 @@ generate_ram_netlist_cmd(int mask_size, const int bank_columns, const char* bank_order, const int rb_ports, - const int read_latency, + const int register_read_address, const char* write_style, const char* write_priority, const char* periphery, @@ -115,7 +115,7 @@ generate_ram_netlist_cmd(int mask_size, 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, - read_latency, write_style, write_priority, periphery, pin_layer, cells, + register_read_address, write_style, write_priority, periphery, pin_layer, cells, name, decoder, bit_folds, wrapper_verilog, service_every, service_sites, bank_column_layout); } diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 56b221e732d..ce5f5aa8cc3 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -14,7 +14,7 @@ sta::define_cmd_args "generate_ram_netlist" {[-mask_size bits] [-bank_columns count] [-bank_order contiguous|interleaved] [-rb_ports count] - [-read_latency 0|1] + [-register_read_address 0|1] [-write_style clock_gate|mux] [-write_priority last|or] [-cells {role master ...}] @@ -39,7 +39,7 @@ proc 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 -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority + -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 {} @@ -289,9 +289,9 @@ proc generate_ram_netlist { args } { } # A read latency of 1: the read addresses registered. - set read_latency 0 - if { [info exists keys(-read_latency)] } { - set read_latency $keys(-read_latency) + 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. @@ -310,7 +310,8 @@ proc generate_ram_netlist { args } { 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ - $rb_ports $read_latency $write_style $write_priority $periphery $pin_layer $cells $name \ + $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 } @@ -347,7 +348,8 @@ proc generate_ram { args } { -ground_net_name -routing_layer -ver_layer -hor_layer -filler_cells -tapcell -max_tap_dist -write_behavioral_verilog -read_style -zero_word -reset_value - -banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority + -banks -bank_columns -bank_order -rb_ports -register_read_address + -write_style -write_priority -cells -name -decoder -bit_folds -service_columns -bank_column_layout } flags {} @@ -398,7 +400,8 @@ proc generate_ram { args } { lappend ram_netlist_args -reset_value $keys(-reset_value) } - foreach key {-banks -bank_columns -bank_order -rb_ports -read_latency -write_style -write_priority + 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} { if { [info exists keys($key)] } { diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 7c006674135..c5eb9e1fc7c 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -19,7 +19,7 @@ PASSFAIL_TESTS = [ "reset_asap7", "banks_asap7", "banked_read_ports_asap7", - "read_latency_asap7", + "register_read_address_asap7", "write_mux_asap7", "periphery_unplaced_asap7", "cells_name_asap7", @@ -123,7 +123,7 @@ filegroup( "//test:asap7_data", "//test:asap7_nldm_data", ], - "read_latency_asap7": [ + "register_read_address_asap7": [ "//test:asap7_data", "//test:asap7_nldm_data", ], diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 70ea44f7b60..7e421c74d60 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -12,7 +12,7 @@ or_integration_tests( reset_asap7 banks_asap7 banked_read_ports_asap7 - read_latency_asap7 + register_read_address_asap7 write_mux_asap7 periphery_unplaced_asap7 cells_name_asap7 diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index 40a0d9504d5..c0a27c777a4 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -22,7 +22,7 @@ foreach {id options} { 92 {-bank_columns -1} 95 {-service_columns {0 10}} 93 {-bit_folds 0} - 71 {-read_latency -1} + 71 {-register_read_address -1} 94 {-banks 2 -rb_ports -1} } { set args [dict merge $base $options] diff --git a/src/ram/test/read_latency_asap7.tcl b/src/ram/test/register_read_address_asap7.tcl similarity index 89% rename from src/ram/test/read_latency_asap7.tcl rename to src/ram/test/register_read_address_asap7.tcl index 9beb7e08f5b..51a38da5673 100644 --- a/src/ram/test/read_latency_asap7.tcl +++ b/src/ram/test/register_read_address_asap7.tcl @@ -1,4 +1,4 @@ -# -read_latency 1: a read's data follows a clock after its address, and +# -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" @@ -14,7 +14,7 @@ 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 -read_latency 1 + -storage_cell DFFHQNx1_ASAP7_75t_R -register_read_address 1 ram_sim::load set mem [ram_sim::exercise { diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index 0787bafd143..a7e91d8247c 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -11,7 +11,7 @@ record_tests { reset_asap7 banks_asap7 banked_read_ports_asap7 - read_latency_asap7 + register_read_address_asap7 write_mux_asap7 periphery_unplaced_asap7 cells_name_asap7 From 460fe9a52786fd770e092000a09eb47f72272fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 14 Oct 2026 10:33:14 +0200 Subject: [PATCH 20/43] ram: generate_ram routes on asap7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things kept generate_ram from routing an asap7 block. -tracks: generate_ram made its tracks from the technology LEF's offsets, and asap7's M2 offset is negative (IFP-0039). -tracks names a file of make_tracks commands to source instead, as asap7's flows do; without it the tracks are made as before. A file that cannot be read stops with RAM-0097. Pins off the power straps: the pin placer did not know where generate_ram's own straps run, and put pins on the tracks under them. On asap7 the M4 straps start at the block's bottom edge, its first track 12 nm above it, and detailed routing could not reach the pin (DRT-0255). Each strap's span, with its layer's spacing, is now excluded from the edges that layer's pins go on. The existing sky130 and Nangate45 blocks keep their pins where they were. The asap7 tracks file joins //test:asap7_data. Tested: the asap7 tests in src/ram/test, which route their blocks through generate_ram in the commit that follows. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 1 + src/ram/src/ram.cpp | 36 ++++++++++++++++++++++++++++++++++-- src/ram/src/ram.tcl | 13 +++++++++++-- test/BUILD | 1 + 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/ram/README.md b/src/ram/README.md index e5a52fa91bc..3a36bd2cabb 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -110,6 +110,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` | `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 diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index fcadc384d45..1c292a6a83f 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1885,8 +1885,40 @@ 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; + } + const odb::Rect r = box->getBox(); + if (box->getTechLayer() == hor_layer) { + const int s = hor_layer->getSpacing(); + 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(); + 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(); } diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index ce5f5aa8cc3..bd1f4e1948f 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -337,6 +337,7 @@ sta::define_cmd_args "generate_ram" {[-mask_size bits] [-max_tap_dist value] [-use_latch value] [-write_behavioral_verilog filename] + [-tracks file] } # user arguments for generate ram arguments @@ -351,10 +352,14 @@ proc generate_ram { args } { -banks -bank_columns -bank_order -rb_ports -register_read_address -write_style -write_priority -cells -name -decoder -bit_folds -service_columns - -bank_column_layout } flags {} + -bank_column_layout -tracks } flags {} sta::check_argc_eq0 "generate_ram" $args + 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" @@ -509,7 +514,11 @@ 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)] } { + source $keys(-tracks) + } else { + make_tracks -x_offset 0 -y_offset 0 + } ram::ram_pinplacer $ver_name $hor_name diff --git a/test/BUILD b/test/BUILD index 01eac4066a4..d9a1dcea702 100644 --- a/test/BUILD +++ b/test/BUILD @@ -97,6 +97,7 @@ filegroup( filegroup( name = "asap7_data", srcs = [ + "asap7/asap7.tracks.tcl", "asap7/asap7_tech_1x_201209.lef", "asap7/asap7sc7p5t_28_R_1x_220121a.lef", ], From 3cb6b6d184ce40a7bfac1de3c8a01bb179c29a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 14 Oct 2026 10:33:41 +0200 Subject: [PATCH 21/43] ram: generate_ram -periphery verilog, the core routed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_ram takes -periphery verilog and -wrapper_verilog: the routed block is the core, _core, and the periphery is written as a Verilog module, , around it, for a parent's synthesis to map and place with its own logic. The netlist puts each of the core's pins where its connections land, for a macro placer to orient the core by; generate_ram drops those and places the pins on the core's edges as it does for any block, so the core routes. -periphery unplaced, which leaves the periphery unplaced inside the block, stays generate_ram_netlist's, for a flow that places it; generate_ram stops on it with RAM-0098. Tested: src/ram/test/periphery_verilog_asap7.tcl, which routes the core in the commit that follows. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 +- src/ram/src/ram.tcl | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ram/README.md b/src/ram/README.md index 3a36bd2cabb..75ac7f63799 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -94,7 +94,7 @@ generate_ram [-mask_size bits] | `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | | `-service_columns {every sites}` | An empty column `sites` sites wide after every `every` storage columns: 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` | 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 then cross the row instead. Default: `interleaved`. | -| `-periphery`, `-pin_layer`, `-wrapper_verilog` | `generate_ram_netlist` only. `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`. | +| `-periphery`, `-pin_layer`, `-wrapper_verilog` | `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | | `-decoder` | `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`. | diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index bd1f4e1948f..6532bbe5723 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -337,6 +337,8 @@ 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] } @@ -352,10 +354,15 @@ proc generate_ram { args } { -banks -bank_columns -bank_order -rb_ports -register_read_address -write_style -write_priority -cells -name -decoder -bit_folds -service_columns - -bank_column_layout -tracks } flags {} + -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." } @@ -408,11 +415,19 @@ proc generate_ram { args } { 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} { + -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) @@ -520,6 +535,16 @@ proc generate_ram { args } { 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 From 446e0dbe5a99cc41fd1ae98dfdd4a48236234ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 14 Oct 2026 10:50:31 +0200 Subject: [PATCH 22/43] ram: the option tests route their blocks through generate_ram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every option test now builds its block with generate_ram, so it is placed, given its power grid, pins and fillers, and routed, and then checks what the option computes on the routed block. src/ram/test/routed.tcl holds generate_ram's arguments for asap7 and Nangate45, and routed::check: every signal net with two or more pins has a wire, and detailed routing ran and left no violations. What changed besides the command: - banks and bank_column_layout use 32 words: at 8 words in four banks, two side by side, the block was a strip 36 um wide and 1.35 um tall whose east edge had six pin slots for nine address pins. - service_columns counts fillers and taps as empty sites: in a routed block they fill the columns the option leaves. - periphery_unplaced stays on generate_ram_netlist: generate_ram does not take -periphery unplaced. - option_errors runs generate_ram, and adds -zero_word past the last word (RAM-0059), -register_read_address 2 (RAM-0071), an unreadable -tracks file (RAM-0097) and -periphery unplaced (RAM-0098). Tested: src/ram/test, all 21 tests. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/test/BUILD | 1 + src/ram/test/and_or_read_asap7.tcl | 15 ++-- src/ram/test/bank_column_layout_asap7.tcl | 18 ++--- src/ram/test/banked_read_ports_asap7.tcl | 13 ++-- src/ram/test/banks_asap7.tcl | 25 +++---- src/ram/test/bit_folds_asap7.tcl | 14 ++-- src/ram/test/cells_name_asap7.tcl | 14 ++-- src/ram/test/decoder_word_asap7.tcl | 14 ++-- src/ram/test/option_errors_asap7.tcl | 26 +++---- src/ram/test/periphery_verilog_asap7.tcl | 16 ++--- src/ram/test/register_read_address_asap7.tcl | 13 ++-- src/ram/test/reset_asap7.tcl | 13 ++-- src/ram/test/routed.tcl | 72 ++++++++++++++++++++ src/ram/test/service_columns_asap7.tcl | 18 ++--- src/ram/test/write_mux_asap7.tcl | 13 ++-- src/ram/test/write_ports_nangate45.tcl | 10 +-- src/ram/test/write_priority_or_asap7.tcl | 13 ++-- src/ram/test/zero_word_asap7.tcl | 13 ++-- 18 files changed, 158 insertions(+), 163 deletions(-) create mode 100644 src/ram/test/routed.tcl diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index c5eb9e1fc7c..2477ee689e1 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -42,6 +42,7 @@ filegroup( 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", diff --git a/src/ram/test/and_or_read_asap7.tcl b/src/ram/test/and_or_read_asap7.tcl index 9b163f21d20..a98530916ee 100644 --- a/src/ram/test/and_or_read_asap7.tcl +++ b/src/ram/test/and_or_read_asap7.tcl @@ -1,21 +1,16 @@ -# generate_ram_netlist on asap7, which has no tristate cell: the read ports +# 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" -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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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" diff --git a/src/ram/test/bank_column_layout_asap7.tcl b/src/ram/test/bank_column_layout_asap7.tcl index e88a162b5ed..8cea0db794e 100644 --- a/src/ram/test/bank_column_layout_asap7.tcl +++ b/src/ram/test/bank_column_layout_asap7.tcl @@ -3,21 +3,15 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -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 \ +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_column_layout contiguous +routed::check ram_sim::load set ranges [dict create] @@ -32,7 +26,7 @@ dict for {row rs} $ranges { "row $row: the words' bits are interleaved ($a, $b)" } ram_sim::exercise { - words 8 bits 8 + words 32 bits 8 writes {{addr_w D {we[0]}}} reads {{addr_r0 Q0} {addr_r1 Q1}} } diff --git a/src/ram/test/banked_read_ports_asap7.tcl b/src/ram/test/banked_read_ports_asap7.tcl index 75ae0c57052..2347a73f405 100644 --- a/src/ram/test/banked_read_ports_asap7.tcl +++ b/src/ram/test/banked_read_ports_asap7.tcl @@ -3,20 +3,15 @@ # i * 4 + b. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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 } { diff --git a/src/ram/test/banks_asap7.tcl b/src/ram/test/banks_asap7.tcl index 5eba3d1a4df..b9dd6b8ea74 100644 --- a/src/ram/test/banks_asap7.tcl +++ b/src/ram/test/banks_asap7.tcl @@ -1,33 +1,28 @@ -# -banks 4 -bank_columns 2 -bank_order interleaved: the words in four +# -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" -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 +routed::read_asap7 -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 \ +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: 8 words in 4 rows of storage +# 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]] == 4 } \ - "storage in [llength [lsort -unique $rows]] rows, not 4" +ram_sim::check { [llength [lsort -unique $rows]] == 16 } \ + "storage in [llength [lsort -unique $rows]] rows, not 16" ram_sim::exercise { - words 8 bits 8 + words 32 bits 8 writes {{addr_w D {we[0]}}} reads {{addr_r0 Q0} {addr_r1 Q1}} } diff --git a/src/ram/test/bit_folds_asap7.tcl b/src/ram/test/bit_folds_asap7.tcl index 0f5425599fd..4092fcc506d 100644 --- a/src/ram/test/bit_folds_asap7.tcl +++ b/src/ram/test/bit_folds_asap7.tcl @@ -3,20 +3,14 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - - -generate_ram_netlist \ +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 } \ diff --git a/src/ram/test/cells_name_asap7.tcl b/src/ram/test/cells_name_asap7.tcl index aa0e9c77e61..6ff39db8211 100644 --- a/src/ram/test/cells_name_asap7.tcl +++ b/src/ram/test/cells_name_asap7.tcl @@ -3,21 +3,15 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - - -generate_ram_netlist \ +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]" diff --git a/src/ram/test/decoder_word_asap7.tcl b/src/ram/test/decoder_word_asap7.tcl index c248846671d..b4ac887d9db 100644 --- a/src/ram/test/decoder_word_asap7.tcl +++ b/src/ram/test/decoder_word_asap7.tcl @@ -5,20 +5,14 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - - -generate_ram_netlist \ +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*] diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index c0a27c777a4..50796aa5eab 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -1,36 +1,32 @@ # Out-of-range options stop generation, each with its own message. source "helpers.tcl" -source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - - -set base {-mask_size 8 -word_size 8 -num_words 8 -r_ports 1 -w_ports 1 - -storage_cell DFFHQNx1_ASAP7_75t_R} +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} } { set args [dict merge $base $options] - if { ![catch { generate_ram_netlist {*}$args } message] } { - error "generate_ram_netlist $options did not stop" + if { ![catch { generate_ram {*}$args } message] } { + error "generate_ram $options did not stop" } if { ![string match "*RAM-00$id*" $message] } { - error "generate_ram_netlist $options stopped with: $message, not RAM-00$id" + error "generate_ram $options stopped with: $message, not RAM-00$id" } puts "$options: RAM-00$id" } diff --git a/src/ram/test/periphery_verilog_asap7.tcl b/src/ram/test/periphery_verilog_asap7.tcl index a99497949ff..c87d35835c2 100644 --- a/src/ram/test/periphery_verilog_asap7.tcl +++ b/src/ram/test/periphery_verilog_asap7.tcl @@ -4,22 +4,16 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 - +routed::read_asap7 set wrapper [make_result_file periphery_verilog_asap7_wrapper.v] -generate_ram_netlist \ +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 -pin_layer M4 \ + -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" diff --git a/src/ram/test/register_read_address_asap7.tcl b/src/ram/test/register_read_address_asap7.tcl index 51a38da5673..435cda96c7e 100644 --- a/src/ram/test/register_read_address_asap7.tcl +++ b/src/ram/test/register_read_address_asap7.tcl @@ -2,19 +2,14 @@ # does not follow an address change before that clock. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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 { diff --git a/src/ram/test/reset_asap7.tcl b/src/ram/test/reset_asap7.tcl index 1a2ecdcd281..32e6a96fa22 100644 --- a/src/ram/test/reset_asap7.tcl +++ b/src/ram/test/reset_asap7.tcl @@ -3,19 +3,14 @@ # the words are written and read as usual after it. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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 diff --git a/src/ram/test/routed.tcl b/src/ram/test/routed.tcl new file mode 100644 index 00000000000..c45976ca754 --- /dev/null +++ b/src/ram/test/routed.tcl @@ -0,0 +1,72 @@ +# 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, and detailed routing +# left no violations. +proc check { } { + set block [ord::get_db_block] + 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 index 66dab3c934f..8199529ab3b 100644 --- a/src/ram/test/service_columns_asap7.tcl +++ b/src/ram/test/service_columns_asap7.tcl @@ -4,26 +4,22 @@ source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - - -generate_ram_netlist \ +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] { - if { [$inst isPlaced] } { + 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]] } diff --git a/src/ram/test/write_mux_asap7.tcl b/src/ram/test/write_mux_asap7.tcl index 3ba7596edd9..b90b728df50 100644 --- a/src/ram/test/write_mux_asap7.tcl +++ b/src/ram/test/write_mux_asap7.tcl @@ -2,19 +2,14 @@ # a word not written keeps its value through a mux per bit. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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" diff --git a/src/ram/test/write_ports_nangate45.tcl b/src/ram/test/write_ports_nangate45.tcl index db2ddde1282..d26ebc02c02 100644 --- a/src/ram/test/write_ports_nangate45.tcl +++ b/src/ram/test/write_ports_nangate45.tcl @@ -1,20 +1,20 @@ -# generate_ram_netlist with two write ports: each port writes the word it +# 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" -read_liberty Nangate45/Nangate45_typ.lib -read_lef Nangate45/Nangate45_tech.lef -read_lef Nangate45/Nangate45.lef +routed::read_nangate45 -generate_ram_netlist \ +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 { diff --git a/src/ram/test/write_priority_or_asap7.tcl b/src/ram/test/write_priority_or_asap7.tcl index 4e76a7a52da..00950252bf4 100644 --- a/src/ram/test/write_priority_or_asap7.tcl +++ b/src/ram/test/write_priority_or_asap7.tcl @@ -2,19 +2,14 @@ # takes the OR of their data, as firtool's register files describe it. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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 { diff --git a/src/ram/test/zero_word_asap7.tcl b/src/ram/test/zero_word_asap7.tcl index 9a193fb223b..6374f553593 100644 --- a/src/ram/test/zero_word_asap7.tcl +++ b/src/ram/test/zero_word_asap7.tcl @@ -2,19 +2,14 @@ # register file's x0, and holds no flops; every other word reads back. source "helpers.tcl" source "ram_sim.tcl" +source "routed.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 +routed::read_asap7 -read_lef ../../../test/asap7/asap7_tech_1x_201209.lef -read_lef ../../../test/asap7/asap7sc7p5t_28_R_1x_220121a.lef - -generate_ram_netlist \ +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 } \ From d6bb185eceaf7f30a68ed2f46ad11559f0a14ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 14 Oct 2026 10:53:42 +0200 Subject: [PATCH 23/43] ram: generate_ram's help lists its options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_ram takes the register-file options and passes them to generate_ram_netlist, but its help listed none of them, so `help generate_ram` did not show what the command takes. The README's synopsis lacked -periphery, -wrapper_verilog and -tracks. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 3 +++ src/ram/src/ram.tcl | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ram/README.md b/src/ram/README.md index 75ac7f63799..d187fd9e070 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -69,6 +69,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 diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 6532bbe5723..d0a2ecf9bc4 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -326,6 +326,21 @@ sta::define_cmd_args "generate_ram" {[-mask_size bits] [-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] From e36b6de45e5434a8356bc0bb5b1b8b71b770dab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:01:09 +0200 Subject: [PATCH 24/43] ram: -periphery verilog keeps no destroyed pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit splitPeriphery destroys the pins only the periphery used, the data and address inputs among them, but left them in data_inputs_ and addr_inputs_. generate_ram's pin placer then constrained those freed pins. The test passed because odb keeps a freed object's slot. The destroyed pins now leave the pin lists first. Tested: src/ram/test/periphery_verilog_asap7.tcl. The use-after-free has no test of its own; it shows only under ASan or when a freed slot is reused. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 1c292a6a83f..7cf23ee0851 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -3410,6 +3410,22 @@ void RamGen::splitPeriphery() 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); } From 33cdb7f8214aca5e64cfca129ddbc012bc512305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:04:33 +0200 Subject: [PATCH 25/43] ram: -cells checks every role's function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -cells took the master given for a role without looking at it, except for aoi22 and ao22, which findMaster4 checks by truth table. An OR2 given as and2, or a NOR2 as nand2, built without a word and read wrong data. A list with an odd number of words dropped its last role. findMaster takes the cell given, as findMaster4 does, and checks it with the role's own match on any of its outputs: RAM-0079 where it does not match. That covers inv, and2, buffer, clock_gate, or2, nand2, nor2, tie and addr_flop. A list with an odd number of words stops with RAM-0099. -inv_cell sets the same role as -cells inv, so it is checked too. Tested: src/ram/test/option_errors_asap7.tcl, an OR2 as and2, a NOR2 as nand2 and a role without a cell. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/include/ram/ram.h | 6 +- src/ram/src/ram.cpp | 173 +++++++++++++++------------ src/ram/test/option_errors_asap7.tcl | 10 +- 3 files changed, 109 insertions(+), 80 deletions(-) diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index e329341edb2..45e2973a27f 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -199,10 +199,12 @@ class RamGen // holds the unused pin off void findResetPins(); // The smallest cell `match` accepts; none is an error unless not - // `required`, then nullptr. + // `required`, then nullptr. A cell `given` is checked by `match` + // instead, on any of its outputs. odb::dbMaster* findMaster(const std::function& match, const char* name, - bool required = true); + bool required = true, + odb::dbMaster* given = nullptr); // 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. diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 7cf23ee0851..824106b4b92 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -1015,8 +1016,26 @@ std::vector RamGen::makeSelectNets(const std::string& prefix, dbMaster* RamGen::findMaster( const std::function& match, const char* name, - const bool required) + const bool required, + dbMaster* given) { + 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(); @@ -1345,17 +1364,17 @@ void RamGen::findResetPins() // the unused pin of each flop held at its inactive level const bool off_high = reset_active_low_; - if (!tie_off_cell_) { - tie_off_cell_ = findMaster( - [off_high](sta::LibertyPort* port) { - auto f = port->function(); - return f && port->direction()->isOutput() - && f->op() - == (off_high ? sta::FuncExpr::Op::one - : sta::FuncExpr::Op::zero); - }, - off_high ? "tie high" : "tie low"); - } + tie_off_cell_ = findMaster( + [off_high](sta::LibertyPort* port) { + auto f = port->function(); + return f && port->direction()->isOutput() + && f->op() + == (off_high ? sta::FuncExpr::Op::one + : sta::FuncExpr::Op::zero); + }, + off_high ? "tie high" : "tie low", + /*required=*/true, + tie_off_cell_); tie_off_pin_ = buildPortMap(tie_off_cell_)[{PortRoleType::DataOut, 0}]; try { @@ -1367,13 +1386,11 @@ void RamGen::findResetPins() void RamGen::findMasters() { - if (!inv_cell_) { - inv_cell_ = findMaster( - [](sta::LibertyPort* port) { - return port->libertyCell()->isInverter(); - }, - "inverter"); - } + 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 @@ -1401,19 +1418,19 @@ void RamGen::findMasters() } } - 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"); - } + 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_) { @@ -1470,21 +1487,19 @@ void RamGen::findMasters() } } - if (!clock_gate_cell_) { - clock_gate_cell_ = findMaster( - [](sta::LibertyPort* port) { - return port->libertyCell()->isClockGate(); - }, - "clock gate"); - } + clock_gate_cell_ = findMaster( + [](sta::LibertyPort* port) { return port->libertyCell()->isClockGate(); }, + "clock gate", + /*required=*/true, + clock_gate_cell_); 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 @@ -1540,17 +1555,17 @@ void RamGen::findMasters() // 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) - if (!nand2_cell_) { - nand2_cell_ = findMaster( - [](sta::LibertyPort* port) { return twoInputTable(port) == 0x7; }, - "nand2"); - } + nand2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x7; }, + "nand2", + /*required=*/true, + nand2_cell_); nand2_ports_ = buildPortMap(nand2_cell_); - if (!nor2_cell_) { - nor2_cell_ = findMaster( - [](sta::LibertyPort* port) { return twoInputTable(port) == 0x1; }, - "nor2"); - } + nor2_cell_ = findMaster( + [](sta::LibertyPort* port) { return twoInputTable(port) == 0x1; }, + "nor2", + /*required=*/true, + nor2_cell_); nor2_ports_ = buildPortMap(nor2_cell_); } @@ -1562,19 +1577,19 @@ void RamGen::findMasters() 0xF888, "ao22", ao22_in_, ao22_out_, /*required=*/false, ao22_cell_); } if (num_write_ > 1) { - if (!or2_cell_) { - or2_cell_ = findMaster( - [](sta::LibertyPort* port) { - if (!port->direction()->isOutput()) { - return false; - } - 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"); - } + or2_cell_ = findMaster( + [](sta::LibertyPort* port) { + if (!port->direction()->isOutput()) { + return false; + } + 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_); } } @@ -2191,8 +2206,6 @@ void RamGen::generate(const int mask_size, // smallest the libraries have of its function { std::istringstream in(cells); - std::string role; - std::string master_name; const std::map roles = {{"inv", &inv_cell_}, {"and2", &and2_cell_}, @@ -2205,7 +2218,17 @@ void RamGen::generate(const int mask_size, {"nor2", &nor2_cell_}, {"tie", &tie_off_cell_}, {"addr_flop", &addr_flop_cell_}}; - while (in >> role >> master_name) { + 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, @@ -2263,18 +2286,18 @@ void RamGen::generate(const int mask_size, }; // the storage cell where it is one (findMaster passes over cells with // two outputs, as Nangate45's flops all are), else the smallest + 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( - [plain](sta::LibertyPort* port) { - return port->isRegOutput() && plain(port->libertyCell()); - }, - "address flop"); + addr_flop_cell_ = findMaster(flop, "address flop"); } auto ports = buildPortMap(addr_flop_cell_, &addr_flop_inverted_); auto liberty = network_->libertyCell(network_->dbToSta(addr_flop_cell_)); diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index 50796aa5eab..292dbb34367 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -20,15 +20,19 @@ foreach {id options} { 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}} } { set args [dict merge $base $options] if { ![catch { generate_ram {*}$args } message] } { error "generate_ram $options did not stop" } - if { ![string match "*RAM-00$id*" $message] } { - error "generate_ram $options stopped with: $message, not RAM-00$id" + set want [format "RAM-%04d" $id] + if { ![string match "*$want*" $message] } { + error "generate_ram $options stopped with: $message, not $want" } - puts "$options: RAM-00$id" + puts "$options: $want" } puts "pass" From cc26e72ee588569b2e69f3a9fd99bfbd5695367d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:04:41 +0200 Subject: [PATCH 26/43] ram: -reset_value is parsed strictly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -reset_value went through std::stoull with base 0, which takes the digits it can and drops the rest: 12abc read as 12, -1 as every bit set, and 0x1ff on an 8-bit word built 0xff while the behavioral model said 8'd511. Bits from 64 up were always 0. The value is now read as a word of the RAM's width, in the bases stoull took (decimal, 0x hexadecimal, 0 octal): anything else stops with RAM-0064, a value wider than the word with RAM-0100. Words wider than 64 bits take their whole value, and the behavioral model writes it in binary, at the word's width. Tested: src/ram/test/option_errors_asap7.tcl, trailing junk, a negative value, a bare 0x and 0x1ff on an 8-bit word; src/ram/test/reset_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 2 +- src/ram/include/ram/ram.h | 2 +- src/ram/src/ram.cpp | 89 +++++++++++++++++++++++++--- src/ram/test/option_errors_asap7.tcl | 4 ++ 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/ram/README.md b/src/ram/README.md index d187fd9e070..f663704ca2c 100644 --- a/src/ram/README.md +++ b/src/ram/README.md @@ -89,7 +89,7 @@ generate_ram [-mask_size bits] | `-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` | 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` | 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` | Resets every word to `value` 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. A parent adapts a reset of another polarity, or a synchronous one, to it. Not with `-use_latch`. Default: no reset. | +| `-reset_value` | 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. 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` | 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` | 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. Default: 0. | | `-register_read_address` | `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. 0 and 1 only. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index 45e2973a27f..ba9d4f31299 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -462,7 +462,7 @@ class RamGen // 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_; - uint64_t reset_bits_{0}; + 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_; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 824106b4b92..77ee3ae0b1c 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -536,9 +536,7 @@ void RamGen::makeSlice(const int slice_idx, storage = makeNet(name, "storage"); } const int reset_bit - = reset_port_ - ? (bit < 64 ? static_cast((reset_bits_ >> bit) & 1) : 0) - : -1; + = reset_port_ ? static_cast(reset_bits_.at(bit)) : -1; auto bit_cell = makeBit(name, and_or_read_ ? 0 : r_total, gclock_net, @@ -1304,6 +1302,64 @@ static int twoInputTable(sta::LibertyPort* out) return table; } +// `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_)); @@ -1377,10 +1433,21 @@ void RamGen::findResetPins() tie_off_cell_); tie_off_pin_ = buildPortMap(tie_off_cell_)[{PortRoleType::DataOut, 0}]; - try { - reset_bits_ = std::stoull(reset_value_, nullptr, 0); - } catch (const std::exception&) { - logger_->error(RAM, 64, "-reset_value {} is not a number.", reset_value_); + 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_); } } @@ -3865,12 +3932,16 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, 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] <= {}'d{};\n" + " mem[i] <= {}'b{};\n" " end\n" " end else begin\n", reset_active_low_ ? "neg" : "pos", @@ -3879,7 +3950,7 @@ void RamGen::writeBehavioralVerilog(const std::string& filename, reset_name, num_words, word_size_bit, - reset_bits_); + reset_word); write_logic += " end\n"; } diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index 292dbb34367..a847d2bd24b 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -23,6 +23,10 @@ foreach {id options} { 79 {-cells {and2 OR2x2_ASAP7_75t_R}} 79 {-cells {nand2 NOR2xp33_ASAP7_75t_R}} 99 {-cells {and2 AND2x2_ASAP7_75t_R inv}} + 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] } { From 78888381a0c0dbf9ff96639405e6469964593203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:06:16 +0200 Subject: [PATCH 27/43] ram: reset and the read address find their flops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With -reset_value and no -storage_cell, generate_ram looks for a flop with an asynchronous clear and preset; with -register_read_address 1 and a storage flop that has a clear or preset, it looks for a plain flop for the address. findMaster passes over every cell with more than one output, and every such flop on Nangate45 and sky130 drives Q and QN, so both searches stopped with RAM-0010, "Can't find storage cell", on libraries that have the cell. These two searches take a flop whose two outputs are its Q and QN; buildPortMap already wires Q and leaves QN open. The reset's storage flop must also be positive-edge, as every storage flop is: sky130's smallest flop with a clear and preset, dfbbn_1, is negative-edge. The search for a storage flop without a reset is unchanged, so the default output is too. Tested: src/ram/test/reset_nangate45.tcl, new: Nangate45 with -reset_value and -register_read_address 1 and no -storage_cell, every word read after reset, then written and read; it stops with RAM-0010 without this change. On sky130hd, generate_ram_netlist with -reset_value selects dfbbp_1. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/include/ram/ram.h | 7 ++++-- src/ram/src/ram.cpp | 39 ++++++++++++++++++++----------- src/ram/test/BUILD | 6 +++++ src/ram/test/CMakeLists.txt | 1 + src/ram/test/regression_tests.tcl | 1 + src/ram/test/reset_nangate45.tcl | 34 +++++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 src/ram/test/reset_nangate45.tcl diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index ba9d4f31299..aaf426d0e9d 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -200,11 +200,14 @@ class RamGen void findResetPins(); // 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. + // 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, bool required = true, - odb::dbMaster* given = nullptr); + 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. diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 77ee3ae0b1c..03f54851422 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1015,7 +1015,8 @@ dbMaster* RamGen::findMaster( const std::function& match, const char* name, const bool required, - dbMaster* given) + dbMaster* given, + const bool q_and_qn) { if (given) { auto liberty = network_->libertyCell(network_->dbToSta(given)); @@ -1047,7 +1048,6 @@ dbMaster* RamGen::findMaster( if (!liberty) { continue; } - if (!match) { if (liberty->portCount() == 0) { best_area = liberty->area(); @@ -1060,18 +1060,22 @@ 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; @@ -1510,12 +1514,14 @@ void RamGen::findMasters() if (!port->isRegOutput()) { return false; } - // with a reset, a flop with both an asynchronous clear and - // preset, so any bit can take either value + // 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(); + clear_preset |= seq.isRegister() && seq.clear() && seq.preset() + && seq.clock() + && seq.clock()->op() == sta::FuncExpr::Op::port; } if (!clear_preset) { return false; @@ -1535,7 +1541,10 @@ 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 @@ -2351,8 +2360,8 @@ void RamGen::generate(const int mask_size, } return false; }; - // the storage cell where it is one (findMaster passes over cells with - // two outputs, as Nangate45's flops all are), else the smallest + // 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()); }; @@ -2364,7 +2373,11 @@ void RamGen::generate(const int mask_size, network_->libertyCell(network_->dbToSta(storage_cell_)))) { addr_flop_cell_ = storage_cell_; } else { - addr_flop_cell_ = findMaster(flop, "address flop"); + 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_)); diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 2477ee689e1..0e5efe1f4fd 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -17,6 +17,7 @@ PASSFAIL_TESTS = [ "and_or_read_asap7", "zero_word_asap7", "reset_asap7", + "reset_nangate45", "banks_asap7", "banked_read_ports_asap7", "register_read_address_asap7", @@ -132,6 +133,11 @@ filegroup( "//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", diff --git a/src/ram/test/CMakeLists.txt b/src/ram/test/CMakeLists.txt index 7e421c74d60..6fd75e94a11 100644 --- a/src/ram/test/CMakeLists.txt +++ b/src/ram/test/CMakeLists.txt @@ -10,6 +10,7 @@ or_integration_tests( and_or_read_asap7 zero_word_asap7 reset_asap7 + reset_nangate45 banks_asap7 banked_read_ports_asap7 register_read_address_asap7 diff --git a/src/ram/test/regression_tests.tcl b/src/ram/test/regression_tests.tcl index a7e91d8247c..cdf0c6462e9 100644 --- a/src/ram/test/regression_tests.tcl +++ b/src/ram/test/regression_tests.tcl @@ -9,6 +9,7 @@ record_tests { and_or_read_asap7 zero_word_asap7 reset_asap7 + reset_nangate45 banks_asap7 banked_read_ports_asap7 register_read_address_asap7 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" From 4cdb0577bb57fa2d59b7f764fb3fd5eca40d33ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:06:28 +0200 Subject: [PATCH 28/43] ram: cell search skips dont_use, isolation and level-shifter cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findMaster took the smallest cell whose function matched, whatever the library said about it. On sky130hd the OR2 search for several write ports picked sky130_fd_sc_hd__lpflow_inputiso1p_1, an isolation cell, and the AND2, NAND2 and NOR2 searches had the same gap. The search passes over isolation cells, level shifters and cells the library marks dont_use. A tie cell is taken although it is dont_use: libraries mark their tie cells so to keep them from synthesis, as Nangate45 does its LOGIC0_X1 and LOGIC1_X1. -cells still takes any cell given by name. Tested: the cells generate_ram selects on sky130hd with two write ports, before and after; src/ram/test, the golden tests unchanged. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 03f54851422..d138452dca3 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1048,6 +1048,12 @@ 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) { best_area = liberty->area(); @@ -1080,6 +1086,16 @@ dbMaster* RamGen::findMaster( if (!out || !match(out->libertyPort())) { continue; } + // not a cell the library keeps from use, but for a tie cell, which + // libraries mark dont_use to keep it from synthesis (Nangate45's + // LOGIC0_X1 and LOGIC1_X1) + auto function = out->libertyPort()->function(); + const bool tie = function + && (function->op() == sta::FuncExpr::Op::zero + || function->op() == sta::FuncExpr::Op::one); + if (liberty->dontUse() && !tie) { + continue; + } if (liberty->area() < best_area) { best_area = liberty->area(); From 968effd3ace932ed6403579fe4ab12acd4be292b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:21:23 +0200 Subject: [PATCH 29/43] ram: a tie cell may drive both levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -reset_value holds each flop's unused clear or preset pin off with a tie cell, and the clock gate's test enable is tied low. The tie was found by findMaster, which passes over cells with two outputs, and its pin read by buildPortMap. sky130's only tie cell, conb_1, drives HI and LO, so -reset_value on sky130hd stopped with RAM-0010, "Can't find tie high cell". findTie looks for the smallest cell with an output at the level wanted, and takes that output's pin, whatever other outputs the cell has. It takes a tie cell marked dont_use, which libraries do to keep it from synthesis (Nangate45's LOGIC0_X1 and LOGIC1_X1), so findMaster no longer makes an exception for ties. A cell given by -cells tie is checked the same way. Tested: generate_ram_netlist on sky130hd with -reset_value and no -storage_cell, which selects dfbbp_1 and conb_1; src/ram/test/reset_nangate45.tcl and reset_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/include/ram/ram.h | 4 +++ src/ram/src/ram.cpp | 76 ++++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index aaf426d0e9d..ccbee99c0c3 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -198,6 +198,10 @@ class RamGen // 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 diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index d138452dca3..16fcc6ccb58 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1086,14 +1086,8 @@ dbMaster* RamGen::findMaster( if (!out || !match(out->libertyPort())) { continue; } - // not a cell the library keeps from use, but for a tie cell, which - // libraries mark dont_use to keep it from synthesis (Nangate45's - // LOGIC0_X1 and LOGIC1_X1) - auto function = out->libertyPort()->function(); - const bool tie = function - && (function->op() == sta::FuncExpr::Op::zero - || function->op() == sta::FuncExpr::Op::one); - if (liberty->dontUse() && !tie) { + // not a cell the library keeps from use (a tie: findTie) + if (liberty->dontUse()) { continue; } @@ -1322,6 +1316,59 @@ static int twoInputTable(sta::LibertyPort* out) return table; } +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, 79, "Cell {} is not a {} cell.", given->getName(), name); + } + logger_->error(RAM, 10, "Can't find {} cell", name); + } + if (!given) { + logger_->info(RAM, 16, "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` @@ -1440,18 +1487,7 @@ void RamGen::findResetPins() // the unused pin of each flop held at its inactive level const bool off_high = reset_active_low_; - tie_off_cell_ = findMaster( - [off_high](sta::LibertyPort* port) { - auto f = port->function(); - return f && port->direction()->isOutput() - && f->op() - == (off_high ? sta::FuncExpr::Op::one - : sta::FuncExpr::Op::zero); - }, - off_high ? "tie high" : "tie low", - /*required=*/true, - tie_off_cell_); - tie_off_pin_ = buildPortMap(tie_off_cell_)[{PortRoleType::DataOut, 0}]; + tie_off_cell_ = findTie(off_high, tie_off_cell_, tie_off_pin_); bool fits = true; reset_bits_ = parseWord(reset_value_, word_bits_, fits); From cdd12c1cba142c52234934d7e98637b5a6b572a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:07:29 +0200 Subject: [PATCH 30/43] ram: -write_style mux needs no clock-gate cell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -write_style mux holds each flop with a mux so that a word needs no clock gate, but findMasters still looked one up and stopped with RAM-0010 where the library has none: the style meant for such a library could not be used with it. The clock gate is looked up only for -write_style clock_gate. Tested: src/ram/test/write_mux_asap7.tcl. asap7 has a clock gate, so a library without one is checked by reading: no cell instantiates clock_gate_cell_ under -write_style mux. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 16fcc6ccb58..e3fa7c241ed 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1615,12 +1615,17 @@ void RamGen::findMasters() } } - clock_gate_cell_ = findMaster( - [](sta::LibertyPort* port) { return port->libertyCell()->isClockGate(); }, - "clock gate", - /*required=*/true, - clock_gate_cell_); - clock_gate_ports_ = buildPortMap(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", + /*required=*/true, + clock_gate_cell_); + clock_gate_ports_ = buildPortMap(clock_gate_cell_); + } // for input buffers buffer_cell_ = findMaster( From 92c17756c05f361815f301e0db1bf63250203c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:08:28 +0200 Subject: [PATCH 31/43] ram: a clock gate's test enable is tied low MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asap7's integrated clock gate, ICGx1, has a test enable, SE, beside its enable. Each word's clock gate was connected to its clock, enable and gated clock only, so SE was left open in every asap7 block, and in the wrapper -periphery verilog writes. buildPortMap also took the enable as the first input in the liberty's order, which is ENA only because asap7 lists it before SE. The enable is now the pin the liberty marks clock_gate_enable_pin. Where it marks one, every other input is a test enable and is tied low by a tie cell in the word's select cell. A clock gate with no test enable, as on Nangate45 and sky130, is built as before. routed::check now also stops on a cell with a signal input left open, for every test that routes its block. Tested: src/ram/test, every asap7 test through routed::check. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/include/ram/ram.h | 5 ++++ src/ram/src/ram.cpp | 51 +++++++++++++++++++++++++++++++++------ src/ram/test/routed.tcl | 11 +++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index ccbee99c0c3..f244a5138aa 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -473,6 +473,11 @@ class RamGen 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_; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index e3fa7c241ed..7340590993a 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -635,13 +635,22 @@ void RamGen::makeSlice(const int slice_idx, // Make clock gate (with -write_style mux, none: the flops hold by mux) if (!write_mux_ && !shared) { - 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}}); + 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 @@ -1625,6 +1634,34 @@ void RamGen::findMasters() /*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_); + } } // for input buffers diff --git a/src/ram/test/routed.tcl b/src/ram/test/routed.tcl index c45976ca754..f44228d2c2f 100644 --- a/src/ram/test/routed.tcl +++ b/src/ram/test/routed.tcl @@ -41,10 +41,17 @@ variable nangate45 { -max_tap_dist 10 } -# Every signal net with two or more pins has a wire, and detailed routing -# left no violations. +# 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" } { From ca74189f26b4fa451ac5d81b710b8a2f65fc92a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:08:57 +0200 Subject: [PATCH 32/43] ram: -periphery verilog stops on logic it cannot write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapper -periphery verilog writes gives each logic cell as an assign of its liberty function. verilogExpr wrote 1'b0 for an input pin with no net and for a missing part of a function, so the wrapper could compute something else than the cell, without a word. verilogExpr returns nothing in those cases, and the wrapper stops with RAM-0101, naming the instance and its cell. Every operator a liberty function has is written as before. Tested: src/ram/test/periphery_verilog_asap7.tcl. No cell of the tests' libraries reaches RAM-0101; it is checked by reading. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 60 ++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 7340590993a..c40f3735ec4 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -3433,34 +3434,51 @@ void RamGen::generate(const int mask_size, // (those of the periphery too, while they are still placed), on the pin // layer's track grid inside the outline: these pins are for a macro // placer to orient the core by, never routed. -// A liberty function as a Verilog expression, its pins by their nets. -static std::string verilogExpr(const sta::FuncExpr* f, - const std::map& nets) +// 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 "1'b0"; + 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()); - return it == nets.end() ? "1'b0" : it->second; + 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::not_: - return "~(" + verilogExpr(f->left(), nets) + ")"; case sta::FuncExpr::Op::and_: - return "(" + verilogExpr(f->left(), nets) + " & " - + verilogExpr(f->right(), nets) + ")"; + return both(f, " & "); case sta::FuncExpr::Op::or_: - return "(" + verilogExpr(f->left(), nets) + " | " - + verilogExpr(f->right(), nets) + ")"; + return both(f, " | "); case sta::FuncExpr::Op::xor_: - return "(" + verilogExpr(f->left(), nets) + " ^ " - + verilogExpr(f->right(), nets) + ")"; + return both(f, " ^ "); case sta::FuncExpr::Op::one: return "1'b1"; - default: + case sta::FuncExpr::Op::zero: return "1'b0"; } + return std::nullopt; } // Ports as Verilog declarations, `name[i]` grouped into buses. @@ -3572,9 +3590,17 @@ void RamGen::splitPeriphery() } if (outputs == 1 && out->function() && !out->direction()->isTristate() && nets.count(out->name())) { - body << " assign " << nets[out->name()] << " = " - << verilogExpr(out->function(), nets) << "; // " << inst->getName() - << "\n"; + 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() From 59f33497b668c4c6628b99db12d21d35f93af454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:09:32 +0200 Subject: [PATCH 33/43] ram: several write ports are checked in generate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More than one write port with -use_latch, or with -column_mux_ratio above 1, stopped with RAM-0054 and RAM-0055 in generate_ram_netlist's Tcl only. RamGen::generate, which SWIG exposes, built past the end of its per-port data in either case when called another way. The two checks move into generate, with the same messages. Tested: src/ram/test/option_errors_asap7.tcl, both combinations. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 12 ++++++++++++ src/ram/src/ram.tcl | 10 ---------- src/ram/test/option_errors_asap7.tcl | 2 ++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index c40f3735ec4..507a04c8ba2 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2233,6 +2233,18 @@ void RamGen::generate(const int mask_size, 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, diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index d0a2ecf9bc4..b16024fe2b7 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -133,16 +133,6 @@ proc generate_ram_netlist { args } { set use_latch $keys(-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 } { - utl::error RAM 54 "More than one write port is not supported with -use_latch." - } - if { $w_ports > 1 && $column_mux_ratio > 1 } { - utl::error RAM 55 "More than one write port is not supported with\ - -column_mux_ratio above 1." - } - set tapcell "" set max_tap_dist 0 if { [info exists keys(-tapcell)] } { diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index a847d2bd24b..24930192fe2 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -23,6 +23,8 @@ foreach {id options} { 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} 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} From 8d8a755c4dc462adb00491a3b3291890f446a751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:12:33 +0200 Subject: [PATCH 34/43] ram: a scan flop is not a plain address flop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -register_read_address 1 takes the storage cell for the address registers where it is a plain flop, and reads its D pin from the flop's next state. A scan flop's next state is a mux of D and the scan input, with no single pin, so a scan flop as the storage cell dereferenced a null port. A plain flop's data must now be a pin, D or !D. A scan storage cell leaves the address registers to the smallest plain flop, and a scan flop given by -cells addr_flop stops with RAM-0079. Tested: src/ram/test/option_errors_asap7.tcl, asap7's SDFHx1 as -cells addr_flop. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 10 ++++++++-- src/ram/test/option_errors_asap7.tcl | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 507a04c8ba2..81ff8f00285 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2457,11 +2457,17 @@ void RamGen::generate(const int mask_size, // -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 + // 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) { + && seq.clock()->op() == sta::FuncExpr::Op::port && d + && d->op() == sta::FuncExpr::Op::port) { return true; } } diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index 24930192fe2..b761e328b03 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -25,6 +25,7 @@ foreach {id options} { 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}} 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} From dfe0d0bcb1272588c0baa42edf4fea3c6d6649c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:12:59 +0200 Subject: [PATCH 35/43] ram: -decoder word's errors name -decoder word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -decoder word builds the banks' decoders by setting the same flag as -banks, so its errors named -banks: -decoder word -column_mux_ratio 2 stopped with "-banks is not supported with -column_mux_ratio above 1", an option that was never given. RAM-0070 said -rb_ports needs -banks, though -decoder word is enough. RAM-0066 and RAM-0068 name the option given, -banks or -decoder word, and RAM-0070 names both. What each combination does is unchanged. Tested: src/ram/test/option_errors_asap7.tcl, -decoder word with -column_mux_ratio 2. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 13 +++++++++---- src/ram/test/option_errors_asap7.tcl | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 81ff8f00285..f77c80b3b79 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2196,9 +2196,14 @@ void RamGen::generate(const int mask_size, 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, "-banks is not supported with -column_mux_ratio above 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, @@ -2210,7 +2215,7 @@ void RamGen::generate(const int mask_size, bank_columns_); } if (use_latch) { - logger_->error(RAM, 68, "-banks is not supported with -use_latch."); + logger_->error(RAM, 68, "{} is not supported with -use_latch.", asked); } } words_per_bank_ = num_words / banks_; @@ -2262,7 +2267,7 @@ void RamGen::generate(const int mask_size, } r_plain_ = rw_ports + r_ports; if (rb_ports_ > 0 && !banked_) { - logger_->error(RAM, 70, "-rb_ports needs -banks."); + 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_; diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index b761e328b03..0c4a23178c7 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -26,6 +26,7 @@ foreach {id options} { 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} 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} From cc15070f38786053982ec118b9c0f073a71faa72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:14:00 +0200 Subject: [PATCH 36/43] ram: -bank_column_layout contiguous keeps a word's slices together MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -bank_column_layout contiguous puts each word's bits side by side, so its selects and write enables span its own bits. The columns were still laid out slice by slice: with -mask_size below -word_size, a row held slice 0 of every word, then slice 1 of every word, and a word was contiguous only within a slice. A contiguous row now holds a word's slices side by side, each with its select column, then the next word's. The row's width is unchanged. ram_sim::exercise takes a write port's enable as a list of pins, every mask slice's, set together. Tested: src/ram/test/bank_column_layout_asap7.tcl, now with two mask slices a word, which fails on the slice-by-slice layout. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/include/ram/ram.h | 19 +++++++++++++------ src/ram/src/ram.cpp | 1 + src/ram/test/bank_column_layout_asap7.tcl | 7 ++++--- src/ram/test/ram_sim.tcl | 11 ++++++++--- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/ram/include/ram/ram.h b/src/ram/include/ram/ram.h index f244a5138aa..5c4fbeb2202 100644 --- a/src/ram/include/ram/ram.h +++ b/src/ram/include/ram/ram.h @@ -432,17 +432,24 @@ class RamGen // 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 sliceOffset(slice) - + (words_contiguous_ ? word * (mask_size + 1) + bit - : bit * words_per_row_ + word); + 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 sliceOffset(slice) - + (words_contiguous_ ? word * (mask_size + 1) + mask_size - : mask_size * words_per_row_ + word); + 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}; diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index f77c80b3b79..15b74ac9727 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2318,6 +2318,7 @@ void RamGen::generate(const int mask_size, logger_->error(RAM, 42, "1-row RAMs are not supported."); } const int slices_per_word = word_size / mask_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; diff --git a/src/ram/test/bank_column_layout_asap7.tcl b/src/ram/test/bank_column_layout_asap7.tcl index 8cea0db794e..df60819e34a 100644 --- a/src/ram/test/bank_column_layout_asap7.tcl +++ b/src/ram/test/bank_column_layout_asap7.tcl @@ -1,5 +1,6 @@ # -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. +# 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" @@ -8,7 +9,7 @@ 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 \ + -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 @@ -27,7 +28,7 @@ dict for {row rs} $ranges { } ram_sim::exercise { words 32 bits 8 - writes {{addr_w D {we[0]}}} + writes {{addr_w D {{we[0]} {we[1]}}}} reads {{addr_r0 Q0} {addr_r1 Q1}} } diff --git a/src/ram/test/ram_sim.tcl b/src/ram/test/ram_sim.tcl index c8de35dc035..ed15be91079 100644 --- a/src/ram/test/ram_sim.tcl +++ b/src/ram/test/ram_sim.tcl @@ -382,7 +382,8 @@ proc ram_sim::check { condition 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` a single-bit enable +# 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` @@ -416,14 +417,18 @@ proc ram_sim::exercise { options } { set e [expr { rand() < 0.7 }] set_bus $addr $abits $a set_bus $data $bits $d - set_port $we $e + foreach pin $we { + set_port $pin $e + } if { $e } { lappend written $a $d } } cycle clk foreach port [dict get $o writes] { - set_port [lindex $port 2] 0 + foreach pin [lindex $port 2] { + set_port $pin 0 + } } set next $mem set hit [dict create] From 89a7ee5c3c1f3120518fc4d554e0545faffa8fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:15:20 +0200 Subject: [PATCH 37/43] ram: -periphery unplaced puts pins on tracks across the layer only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -periphery unplaced puts each pin where its connections land, on -pin_layer's tracks. It snapped x and y alike to the layer's pitch and offset, but a layer's tracks run in its preferred direction only: along it there is no track, and the pin moved up to half a pitch from its connections for nothing. The test checked both coordinates the same way, so it held the mistake in place. A pin now sits on a track across the layer's direction and on the manufacturing grid along it. Tested: src/ram/test/periphery_unplaced_asap7.tcl, which checks M4's pins with y on a track and x on the manufacturing grid. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 23 ++++++++++++++++++++--- src/ram/test/periphery_unplaced_asap7.tcl | 14 +++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 15b74ac9727..d90803d6df7 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -3805,7 +3805,7 @@ void RamGen::unplacePeriphery() } block_->setDieArea(odb::Rect(0, 0, width, height)); block_->setCoreArea(block_->computeCoreArea()); - // each pin on the layer's track grid, inside the outline + // 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); @@ -3819,9 +3819,26 @@ void RamGen::unplacePeriphery() } 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 = on_track(at.x() - dx, width); - const int y = on_track(at.y() - dy, height); + 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); diff --git a/src/ram/test/periphery_unplaced_asap7.tcl b/src/ram/test/periphery_unplaced_asap7.tcl index db93a1aaf89..6d4da85c527 100644 --- a/src/ram/test/periphery_unplaced_asap7.tcl +++ b/src/ram/test/periphery_unplaced_asap7.tcl @@ -44,18 +44,22 @@ ram_sim::check { [$die xMin] <= $x0 && [$die yMin] <= $y0 "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" - foreach c [list [expr { ([$box xMin] + [$box xMax]) / 2 }] \ - [expr { ([$box yMin] + [$box yMax]) / 2 }]] { - ram_sim::check { ($c - $offset) % $pitch == 0 } \ - "[$term getName] is off M4's tracks" - } + 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" } From c2b9f6d08f11d14dc934fab499095b6cd4ee3573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:15:50 +0200 Subject: [PATCH 38/43] ram: pins keep a pin's width from the power straps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_ram keeps pins off the power straps of their own layer by excluding, on each edge, the strap's extent and the layer's spacing. The pin placer reads an excluded interval as pin centres, so a pin centred just outside it reached half its width into the spacing. The interval now also holds half the layer's width on either side. Tested: src/ram/test, every block routed with no DRC violations. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index d90803d6df7..803db6ad92d 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2088,15 +2088,17 @@ void RamGen::ramPinplacer(const char* ver_name, const char* hor_name) 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(); + 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(); + 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( From 16c1e3fc0338a59bf3a32a24fb8eeceef9dfbc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:16:27 +0200 Subject: [PATCH 39/43] ram: options that change nothing say so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some option combinations were taken without a word and did nothing: - -wrapper_verilog without -periphery verilog wrote no file. It now stops with RAM-0102. - -write_priority or with one write port has no writes to merge. It warns with RAM-0103. - -tristate_cell with -read_style and_or names a cell the AND-OR read path does not use. It warns with RAM-0104. RAM-0076 said "0 periphery instances left unplaced" under -periphery verilog, where the periphery is written to the wrapper and none is left in the block. It now counts the periphery before the split and says where it went. Tested: src/ram/test/option_errors_asap7.tcl, -wrapper_verilog alone; src/ram/test/periphery_verilog_asap7.tcl. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 27 ++++++++++++++++++++++++--- src/ram/test/option_errors_asap7.tcl | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 803db6ad92d..22b4d0bca86 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1530,6 +1530,13 @@ void RamGen::findMasters() // 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( @@ -2232,6 +2239,16 @@ void RamGen::generate(const int mask_size, 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; @@ -3725,6 +3742,7 @@ void RamGen::splitPeriphery() void RamGen::unplacePeriphery() { + const size_t periphery_count = periphery_insts_.size(); if (periphery_verilog_) { splitPeriphery(); } @@ -3847,9 +3865,12 @@ void RamGen::unplacePeriphery() } logger_->info(RAM, 76, - "-periphery unplaced: {} periphery instances left unplaced; " - "the core is {:.3f} x {:.3f} um.", - periphery_insts_.size(), + "-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)); } diff --git a/src/ram/test/option_errors_asap7.tcl b/src/ram/test/option_errors_asap7.tcl index 0c4a23178c7..2cdeab158ca 100644 --- a/src/ram/test/option_errors_asap7.tcl +++ b/src/ram/test/option_errors_asap7.tcl @@ -27,6 +27,7 @@ foreach {id options} { 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} From 2653a2e580a952c05a2a0fadf6c2c98119709f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:16:57 +0200 Subject: [PATCH 40/43] ram: stale comments and empty select nets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - With the AND-OR read path and -column_mux_ratio above 1, each row still made the shared select{i}_b nets the tristate path uses, and left them empty in the block. They are made for tristates only. - Two comments still said a RAM has one write-capable port, and generate_ram_netlist's Tcl still called -register_read_address a read latency. - The comment on -periphery unplaced sat above verilogExpr; it moves to unplacePeriphery. Tested: src/ram/test. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 22 ++++++++++++---------- src/ram/src/ram.tcl | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 22b4d0bca86..ac9242054bd 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -2739,7 +2739,8 @@ void RamGen::generate(const int mask_size, decoder_output_nets[p] = 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_selects, RamPortType::ReadWrite); @@ -2950,7 +2951,8 @@ void RamGen::generate(const int mask_size, 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 && !banked_) { + // (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)); @@ -3191,7 +3193,7 @@ 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, @@ -3470,13 +3472,6 @@ void RamGen::generate(const int mask_size, w_ports); } -// -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 track grid inside the outline: these pins are for a macro -// placer to orient the core by, never routed. // 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( @@ -3740,6 +3735,13 @@ void RamGen::splitPeriphery() 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(); diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index b16024fe2b7..5474ef8df00 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -278,7 +278,7 @@ proc generate_ram_netlist { args } { } } - # A read latency of 1: the read addresses registered. + # -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) From 1170b35ae905d21f3fd1774fedb65af1d662a435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 23 Oct 2026 08:18:47 +0200 Subject: [PATCH 41/43] ram: generate_ram's register-file options are experimental MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The options added for register files are a baseline: it gets them under test, here in OpenROAD and in integration tests in ORFS, and lets the flows that need register files use generate_ram, so that how each option should behave can be settled on real designs. Until it is, their interface may change, and they may move to a command of their own, such as a generate_register, with the generate_ram options retired once flows use it. The README marks each of them Experimental, and more than one write port. generate_ram_netlist logs the experimental options a run uses as RAM-0105, generate_ram -tracks as RAM-0106. The README also says how the baseline behaves where it did not: banked read ports leave the bank select to the parent; a registered read address reads the word written on the same clock as its new value; -write_style mux clocks every flop every cycle; -bank_column_layout contiguous runs the write data across the row; -reset_value needs a clear and a preset also for 0; and -service_columns counts every column of the array. It drops the attribution of -write_priority or to firtool. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/README.md | 38 +++++++++++++----------- src/ram/src/ram.cpp | 2 +- src/ram/src/ram.tcl | 20 +++++++++++++ src/ram/test/write_priority_or_asap7.tcl | 2 +- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/ram/README.md b/src/ram/README.md index f663704ca2c..124171b29bc 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 @@ -83,25 +85,25 @@ generate_ram [-mask_size bits] | `-num_words` | Number of words in the array. | | `-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. 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. Not yet with `-use_latch` or `-column_mux_ratio` above 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` | 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` | 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` | 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. 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` | 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` | 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. Default: 0. | -| `-register_read_address` | `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. 0 and 1 only. Not with `-column_mux_ratio` above 1 or read/write ports. Default: 0. | -| `-write_style` | 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. Not with `-use_latch`. Default: `clock_gate`. | -| `-write_priority` | 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 (firtool's register files). `or` drops a word's priority logic, an inverter and an AND2 per port but the last. Default: `last`. | -| `-service_columns {every sites}` | An empty column `sites` sites wide after every `every` storage columns: 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` | 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 then cross the row instead. Default: `interleaved`. | -| `-periphery`, `-pin_layer`, `-wrapper_verilog` | `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` | 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` | The block's and the behavioral model's name. Default: `RAMx`. | -| `-decoder` | `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` | 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. | +| `-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`. | @@ -113,7 +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` | `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. | +| `-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 @@ -131,7 +133,7 @@ Simply run the following script: ## 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/src/ram.cpp b/src/ram/src/ram.cpp index ac9242054bd..1072ff8cf41 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -3374,7 +3374,7 @@ void RamGen::generate(const int mask_size, ram_grid_.setOrigin(ram_origin); ram_grid_.gridInit(); - // -service_columns {every sites}: after every `every` storage columns + // -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) { diff --git a/src/ram/src/ram.tcl b/src/ram/src/ram.tcl index 5474ef8df00..ead7122d1aa 100644 --- a/src/ram/src/ram.tcl +++ b/src/ram/src/ram.tcl @@ -297,6 +297,25 @@ proc generate_ram_netlist { args } { } } + # 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 \ $read_style $zero_word $reset_value $banks $bank_columns $bank_order \ @@ -535,6 +554,7 @@ proc generate_ram { args } { $ver_name $ver_width $ver_pitch $hor_name $hor_width $hor_pitch 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 diff --git a/src/ram/test/write_priority_or_asap7.tcl b/src/ram/test/write_priority_or_asap7.tcl index 00950252bf4..42dbe97d1d7 100644 --- a/src/ram/test/write_priority_or_asap7.tcl +++ b/src/ram/test/write_priority_or_asap7.tcl @@ -1,5 +1,5 @@ # -write_priority or: where both write ports write a word in one cycle it -# takes the OR of their data, as firtool's register files describe it. +# takes the OR of their data. source "helpers.tcl" source "ram_sim.tcl" source "routed.tcl" From 8686893f8c9f13d35f638b7e3577cf7bfc01e389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sat, 24 Oct 2026 22:21:38 +0200 Subject: [PATCH 42/43] ram: a message ID per call site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findTie and the AOI22 cell check reused RAM 10, 16 and 79 from findMaster, which the messages check rejects. They get 107-110. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/src/ram.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ram/src/ram.cpp b/src/ram/src/ram.cpp index 1072ff8cf41..ef590bc6f7f 100644 --- a/src/ram/src/ram.cpp +++ b/src/ram/src/ram.cpp @@ -1368,12 +1368,12 @@ dbMaster* RamGen::findTie(const bool one, dbMaster* given, std::string& pin) if (!best) { if (given) { logger_->error( - RAM, 79, "Cell {} is not a {} cell.", given->getName(), name); + RAM, 107, "Cell {} is not a {} cell.", given->getName(), name); } - logger_->error(RAM, 10, "Can't find {} cell", name); + logger_->error(RAM, 108, "Can't find {} cell", name); } if (!given) { - logger_->info(RAM, 16, "Selected {} cell {}", name, best->getName()); + logger_->info(RAM, 109, "Selected {} cell {}", name, best->getName()); } pin = best_port->name(); return best; @@ -1879,7 +1879,7 @@ dbMaster* RamGen::findMaster4(const uint16_t table, } if (!matched) { logger_->error( - RAM, 79, "Cell {} is not a {} cell.", best->getName(), name); + RAM, 110, "Cell {} is not a {} cell.", best->getName(), name); } } return best; From d10fb8427627031b6df80dc48c6adc968e5ec48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sat, 24 Oct 2026 22:48:54 +0200 Subject: [PATCH 43/43] ram: the slowest register-file tests are manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit banks_asap7, bank_column_layout_asap7 and write_priority_or_asap7 take minutes each; they are local tests, too slow to run in CI. Run them by name. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- src/ram/test/BUILD | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ram/test/BUILD b/src/ram/test/BUILD index 0e5efe1f4fd..2586cd39e12 100644 --- a/src/ram/test/BUILD +++ b/src/ram/test/BUILD @@ -35,6 +35,13 @@ PASSFAIL_TESTS = [ 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, @@ -171,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