From 75c8144d42cb2f436ae0b11ab911837a990c5908 Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Thu, 30 Jul 2026 15:45:59 +0200 Subject: [PATCH 1/5] himbaechel/xilinx: emit BRAM36 width-1 features for RAMB36E1 A RAMB36E1 port configured with width 1 (true 32K x 1 mode) needs the RAMB36-level BRAM36_{READ,WRITE}_WIDTH_{A,B}_1 feature in addition to the per-half RAMB18_Yx width bits, which were the only ones emitted. In this mode the two 16K halves interleave even/odd bit addresses. Without the RAMB36-level feature both halves respond at addr>>1: every write hits the bit pair 2k/2k+1 and the address LSB is ignored, so half the memory aliases the other half. Verified against Vivado bitstreams for the same configuration and on xc7a100t hardware (32K x 1 and cascaded 64K x 1 inferred memories). Co-Authored-By: Claude Fable 5 --- himbaechel/uarch/xilinx/fasm.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/himbaechel/uarch/xilinx/fasm.cc b/himbaechel/uarch/xilinx/fasm.cc index dc70320c..b1611f69 100644 --- a/himbaechel/uarch/xilinx/fasm.cc +++ b/himbaechel/uarch/xilinx/fasm.cc @@ -1442,6 +1442,18 @@ struct FasmBackend write_bit("CASCOUT_ARD_ACTIVE", !used_rdaddrcasc.empty()); write_bit("CASCOUT_BWR_ACTIVE", !used_wraddrcasc.empty()); } + // A RAMB36E1 with a width-1 port needs the RAMB36-level BRAM36_*_WIDTH_*_1 + // feature in addition to the per-half RAMB18_Yx width bits. Without it the + // two 16K halves (which interleave even/odd bit addresses in true 32K x 1 + // mode) both respond at addr>>1: every write hits bit pairs 2k/2k+1 and + // the address LSB is ignored. + if (half == 0 && ci != nullptr && ci->type == id_RAMB36E1_RAMB36E1) { + push("RAMB36"); + for (const char *port : {"READ_WIDTH_A", "READ_WIDTH_B", "WRITE_WIDTH_A", "WRITE_WIDTH_B"}) + if (int_or_default(ci->params, ctx->id(port), 0) == 1) + write_bit(std::string("BRAM36_") + port + "_1"); + pop(); + } pop(); } From 1686fe7cb3d6fc72386ceaef067838e157018324 Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Thu, 30 Jul 2026 15:46:21 +0200 Subject: [PATCH 2/5] himbaechel/xilinx: emit RAM_EXTENSION_{A,B}_LOWER for cascaded RAMB36E1 Depth-cascaded RAMB36E1 pairs (e.g. an inferred 64K x 1 memory) set RAM_EXTENSION_A/B to LOWER on the bottom cell and UPPER on the top cell. The fasm writer ignored these parameters entirely. The UPPER value shares its encoding with NONE (feature bit clear), so only the LOWER cell needs its RAM_EXTENSION_{A,B}_LOWER bit written. Without it the cascade data path between the two cells is not configured and the memory misbehaves beyond the first 32K. Verified against Vivado bitstreams and on xc7a100t hardware (128K ROM built from cascaded pairs passes a full content fingerprint). Co-Authored-By: Claude Fable 5 --- himbaechel/uarch/xilinx/fasm.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/himbaechel/uarch/xilinx/fasm.cc b/himbaechel/uarch/xilinx/fasm.cc index b1611f69..1576d934 100644 --- a/himbaechel/uarch/xilinx/fasm.cc +++ b/himbaechel/uarch/xilinx/fasm.cc @@ -1452,6 +1452,16 @@ struct FasmBackend for (const char *port : {"READ_WIDTH_A", "READ_WIDTH_B", "WRITE_WIDTH_A", "WRITE_WIDTH_B"}) if (int_or_default(ci->params, ctx->id(port), 0) == 1) write_bit(std::string("BRAM36_") + port + "_1"); + // Depth-cascaded pairs (64K x 1): the LOWER cell needs its + // RAM_EXTENSION bit set; UPPER shares the NONE encoding (bit + // clear), so only LOWER is written. Was never emitted, which + // breaks true width-1 cascaded memories. + for (const char *port : {"A", "B"}) { + std::string ext = + str_or_default(ci->params, ctx->id(std::string("RAM_EXTENSION_") + port), "NONE"); + if (ext == "LOWER") + write_bit(std::string("RAM_EXTENSION_") + port + "_LOWER"); + } pop(); } pop(); From a652a752deaf92b375c262cfec0865bebaf2f8d6 Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Thu, 30 Jul 2026 15:47:34 +0200 Subject: [PATCH 3/5] himbaechel/xilinx: don't override driven ADDR*L15 on cascaded RAMB36E1 pack_bram unconditionally disconnected ADDRARDADDRL15/ADDRBWRADDRL15 on every RAMB36E1 and tied them to VCC. Tying high is correct for non-cascaded modes, but in a depth-cascaded pair (RAM_EXTENSION_A/B = LOWER/UPPER) these pins carry the address MSB that selects between the two 32K halves, and the port multixform has already wired ADDRARDADDR[15] onto them. Overriding that with VCC pinned both reads and writes to the upper half: address bit 15 was simply ignored. Keep the tie as a fallback for undriven pins only. Hardware-verified on xc7a100t: a 128K ROM built from cascaded RAMB36E1 pairs passes a full 32/32 content fingerprint check that failed before this change. Co-Authored-By: Claude Fable 5 --- himbaechel/uarch/xilinx/pack.cc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/himbaechel/uarch/xilinx/pack.cc b/himbaechel/uarch/xilinx/pack.cc index f363716d..df802fd8 100644 --- a/himbaechel/uarch/xilinx/pack.cc +++ b/himbaechel/uarch/xilinx/pack.cc @@ -754,14 +754,19 @@ void XC7Packer::pack_bram() ci->connectPort(p, ctx->nets[ctx->id("$PACKER_VCC_NET")].get()); } } else if (ci->type == id_RAMB36E1_RAMB36E1) { + // ADDRARDADDRL15/ADDRBWRADDRL15 must be tied high in non-cascaded + // modes, but in a depth-cascaded pair (RAM_EXTENSION_A/B = LOWER/ + // UPPER) they carry the address MSB that selects the half; the + // multixform has already wired ADDRARDADDR[15] onto them, and + // overriding that with VCC pins both reads and writes to the upper + // 32K half. Only tie when not driven. for (auto p : {id_ADDRARDADDRL15, id_ADDRBWRADDRL15}) { if (!ci->ports.count(p)) { ci->ports[p].name = p; ci->ports[p].type = PORT_IN; - } else { - ci->disconnectPort(p); } - ci->connectPort(p, ctx->nets[ctx->id("$PACKER_VCC_NET")].get()); + if (ci->getPort(p) == nullptr) + ci->connectPort(p, ctx->nets[ctx->id("$PACKER_VCC_NET")].get()); } if (int_or_default(ci->params, id_WRITE_WIDTH_A, 0) == 1) { ci->disconnectPort(id_DIADI1); From 1c85dee26329811d7a18571a68032f3664a8b4c2 Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Thu, 30 Jul 2026 15:49:25 +0200 Subject: [PATCH 4/5] himbaechel/xilinx: place cascaded RAMB36 pairs as clusters CASCADEOUTA/B -> CASCADEINA/B are dedicated wires that only reach the vertically adjacent RAMB36 site in the same column; the router has no general path for them. Without a placement constraint the two cells of a depth-cascaded pair land on arbitrary sites and routing fails. Cluster each cascade pair in pack_bram (analogous to carry chains). Plain relative (x,y,z) constraints cannot express "next RAMB36 up the column": bel z indices are not uniform across BRAM tiles. Instead, precompute a next-RAMB36-in-column map at init and resolve child bels through a getClusterPlacement override. Hardware-verified on xc7a100t: cascaded 64K x 1 memories place, route and function (128K ROM content fingerprint passes). Co-Authored-By: Claude Fable 5 --- himbaechel/uarch/xilinx/pack.cc | 36 ++++++++++++++++++++++++++ himbaechel/uarch/xilinx/xilinx.cc | 43 +++++++++++++++++++++++++++++++ himbaechel/uarch/xilinx/xilinx.h | 8 ++++++ 3 files changed, 87 insertions(+) diff --git a/himbaechel/uarch/xilinx/pack.cc b/himbaechel/uarch/xilinx/pack.cc index df802fd8..2b54bbcd 100644 --- a/himbaechel/uarch/xilinx/pack.cc +++ b/himbaechel/uarch/xilinx/pack.cc @@ -808,6 +808,42 @@ void XC7Packer::pack_bram() } } } + + // Cluster cascaded RAMB36 pairs: CASCADEOUT->CASCADEIN are dedicated + // wires that only reach the vertically adjacent RAMB36 site, so the + // pair must be placed together (cf. carry chains). BRAM tiles are 5 + // grid rows apart within a column. + int cascade_pairs = 0; + for (auto &cell : ctx->cells) { + CellInfo *lower = cell.second.get(); + if (lower->type != id_RAMB36E1_RAMB36E1) + continue; + for (auto pn : {ctx->id("CASCADEOUTA"), ctx->id("CASCADEOUTB")}) { + NetInfo *cn = lower->getPort(pn); + if (cn == nullptr) + continue; + for (auto &usr : cn->users) { + CellInfo *upper = usr.cell; + if (upper == lower || upper->type != id_RAMB36E1_RAMB36E1) + continue; + if (upper->cluster != ClusterId()) + continue; // already clustered (e.g. via the A cascade) + if (lower->cluster == ClusterId()) + lower->cluster = lower->name; + else if (lower->cluster != lower->name) + continue; // lower is a child of another cluster; unsupported + upper->cluster = lower->name; + lower->constr_children.push_back(upper); + upper->constr_x = 0; + upper->constr_y = 5; + upper->constr_z = 0; + upper->constr_abs_z = false; + ++cascade_pairs; + } + } + } + if (cascade_pairs > 0) + log_info(" Clustered %d cascaded BRAM pairs\n", cascade_pairs); } void XilinxPacker::pack_inverters() diff --git a/himbaechel/uarch/xilinx/xilinx.cc b/himbaechel/uarch/xilinx/xilinx.cc index 6e9fdb77..0f8f4772 100644 --- a/himbaechel/uarch/xilinx/xilinx.cc +++ b/himbaechel/uarch/xilinx/xilinx.cc @@ -101,6 +101,49 @@ void XilinxImpl::init(Context *ctx) auto extra_data = tile_extra_data(i); tile_status.at(i).site_variant.resize(extra_data->sites.ssize()); } + + // Build the next-RAMB36-up-the-column map for cascade placement + std::map> bram36_by_col; // x -> y -> bel + for (BelId bel : ctx->getBels()) { + if (ctx->getBelType(bel) != id_RAMB36E1_RAMB36E1) + continue; + Loc l = ctx->getBelLocation(bel); + bram36_by_col[l.x][l.y] = bel; + } + for (auto &col : bram36_by_col) { + BelId prev; + int prev_y = 0; + for (auto &entry : col.second) { + if (prev != BelId() && (entry.first - prev_y) <= 6) + next_bram36_up[entry.second] = prev; // grid Y increases downwards: 'up' = smaller y + prev = entry.second; + prev_y = entry.first; + } + } +} + +bool XilinxImpl::getClusterPlacement(ClusterId cluster, BelId root_bel, + std::vector> &placement) const +{ + CellInfo *root_cell = ctx->getClusterRootCell(cluster); + if (root_cell->type == id_RAMB36E1_RAMB36E1) { + if (ctx->getBelType(root_bel) != id_RAMB36E1_RAMB36E1) + return false; + placement.clear(); + placement.emplace_back(root_cell, root_bel); + BelId cursor = root_bel; + for (auto child : root_cell->constr_children) { + auto fnd = next_bram36_up.find(cursor); + if (fnd == next_bram36_up.end()) + return false; + cursor = fnd->second; + if (ctx->getBelType(cursor) != id_RAMB36E1_RAMB36E1) + return false; + placement.emplace_back(child, cursor); + } + return true; + } + return HimbaechelAPI::getClusterPlacement(cluster, root_bel, placement); } SiteIndex XilinxImpl::get_bel_site(BelId bel) const diff --git a/himbaechel/uarch/xilinx/xilinx.h b/himbaechel/uarch/xilinx/xilinx.h index 973ee221..140e2880 100644 --- a/himbaechel/uarch/xilinx/xilinx.h +++ b/himbaechel/uarch/xilinx/xilinx.h @@ -123,6 +123,14 @@ struct XilinxImpl : HimbaechelAPI bool isBelLocationValid(BelId bel, bool explain_invalid = false) const override; bool xc7_logic_tile_valid(IdString tileType, const LogicTileStatus <s) const; + // BRAM cascade support: CASCADEOUT->CASCADEIN only reaches the next + // RAMB36 site up the column; bel z indices are not uniform across BRAM + // tiles, so relative (x,y,z) constraints cannot express this. Precompute + // the next-RAMB36-in-column map and use it in getClusterPlacement. + dict next_bram36_up; + bool getClusterPlacement(ClusterId cluster, BelId root_bel, + std::vector> &placement) const override; + // Pips bool is_pip_unavail(PipId pip) const; bool checkPipAvail(PipId pip) const override { return !is_pip_unavail(pip); } From 840311b5ac4796413451afa5185c51a10e0df8df Mon Sep 17 00:00:00 2001 From: Joris van Zwieten Date: Thu, 30 Jul 2026 15:49:52 +0200 Subject: [PATCH 5/5] himbaechel/xilinx: give RAMB36 bels their area in the static placer RAMB36E1 bels were registered with a zero bel_area while RAMB36E1 cells claim a 1x6 cell_area. With no capacity anywhere in the RAM density map, the static placer cannot resolve RAMB36 density and BRAM-heavy designs fail to converge to a legalisable placement. Register the bel area as 1x6 to match the cell area, consistent with the RAMB18 entries above. Observed on a design using >90% of the BRAM on an xc7a100t. Co-Authored-By: Claude Fable 5 --- himbaechel/uarch/xilinx/xilinx.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/himbaechel/uarch/xilinx/xilinx.cc b/himbaechel/uarch/xilinx/xilinx.cc index 0f8f4772..7e3a037c 100644 --- a/himbaechel/uarch/xilinx/xilinx.cc +++ b/himbaechel/uarch/xilinx/xilinx.cc @@ -440,7 +440,7 @@ void XilinxImpl::configurePlacerStatic(PlacerStaticCfg &cfg) comb.cell_area[id_RAMB18E1_RAMB18E1] = StaticRect(1.0f, 3.0f); comb.bel_area[id_RAMB18E1_RAMB18E1] = StaticRect(1.0f, 3.0f); comb.cell_area[id_RAMB36E1_RAMB36E1] = StaticRect(1.0f, 6.0f); - comb.bel_area[id_RAMB36E1_RAMB36E1] = StaticRect(0.0f, 0.0f); + comb.bel_area[id_RAMB36E1_RAMB36E1] = StaticRect(1.0f, 6.0f); comb.spacer_rect = StaticRect(1.0f, 3.0f); } {