From fcb61a7d845c271872627375f4b5caec57f067b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sun, 1 Nov 2026 09:50:54 +0100 Subject: [PATCH 1/5] memories: a write mask with a ragged last lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A memory's write mask lanes had to split its word evenly, so pulp's tc_sram, whose byte enables cover a word that is not whole bytes, could not become a macro: cva6's I-cache tag RAM is 25 bits with 4 byte enables, 8, 8, 8 and 1 bit wide. A memory may now declare mask_lane_width: every lane is that wide but the last, which holds the rest (lanes * width >= bits > (lanes - 1) * width). FakeRAM builds the mask bus as before, one pin per lane, and its Verilog model writes each lane's own bit range. Without a width the lanes split the word evenly, as before, and an uneven split is refused as before. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/schema.py | 33 +++++++++++++++++++ flow/scripts/memories/schema_test.py | 26 +++++++++++++++ tools/FakeRAM2.0/orfs_asap7/generate.py | 1 + tools/FakeRAM2.0/test/spsram_mask_test.py | 23 +++++++++++++ tools/FakeRAM2.0/utils/class_memory.py | 5 +++ tools/FakeRAM2.0/utils/memory_config.py | 23 +++++++++++-- .../utils/single_port_ram_verilog_exporter.py | 28 ++++++++++++---- 7 files changed, 130 insertions(+), 9 deletions(-) diff --git a/flow/scripts/memories/schema.py b/flow/scripts/memories/schema.py index 754ae8373c..6aaada0f5c 100644 --- a/flow/scripts/memories/schema.py +++ b/flow/scripts/memories/schema.py @@ -49,6 +49,10 @@ class Memory: # Total width of the per-port write mask, 0 if unmasked. Subword-split # mask pins (`W0_mask_0..3`) contribute one lane each. mask_lanes: int = 0 + # Bits per mask lane, 0 for the even split (bits / mask_lanes). A + # ragged last lane needs it: tc_sram's byte enables over a 25-bit + # word are 8, 8, 8 and 1 bit wide (mask_lanes 4, mask_lane_width 8). + mask_lane_width: int = 0 # Read ports yosys inferred without a clock ($mem_v2 RD_CLK_ENABLE=0). # An SRAM macro reads on a clock edge, so one of these rules the memory # out of conversion; see idiomatic.py. @@ -115,6 +119,7 @@ def memory_from_dict(d: dict) -> Memory: "write_ports", "rw_ports", "mask_lanes", + "mask_lane_width", "comb_read_ports", ): if key in d: @@ -182,6 +187,7 @@ def merge(detected: list[Memory], overrides: list[Memory]) -> list[Memory]: "write_ports", "rw_ports", "mask_lanes", + "mask_lane_width", "comb_read_ports", ): value = getattr(o, key) @@ -198,6 +204,31 @@ def merge(detected: list[Memory], overrides: list[Memory]) -> list[Memory]: return list(by_name.values()) +def lane_width_check(name: str, bits: int, lanes: int, width: int) -> None: + """Raise SchemaError unless `lanes` mask lanes cover `bits` bits. + + Every lane is `width` bits wide but the last, which holds the rest: + lanes * width >= bits > (lanes - 1) * width. Without a width the lanes + split the word evenly. + """ + if not lanes: + if width: + raise SchemaError(f"memory {name}: mask_lane_width without mask_lanes") + return + if not width: + if bits % lanes: + raise SchemaError( + f"memory {name}: {bits} bits are not {lanes} even mask lanes; " + f"give mask_lane_width for a ragged last lane" + ) + return + if not lanes * width >= bits > (lanes - 1) * width: + raise SchemaError( + f"memory {name}: {lanes} mask lanes of {width} bits do not cover " + f"{bits} bits with a last lane of 1 to {width}" + ) + + def validate_emittable(mem: Memory) -> None: """Raise SchemaError unless `mem` carries enough to emit .lib/.lef.""" if not mem.pins: @@ -207,6 +238,8 @@ def validate_emittable(mem: Memory) -> None: f"memory {mem.name}: rows ({mem.rows}) and bits ({mem.bits}) " f"must be >= 1" ) + if mem.mask_lane_width: + lane_width_check(mem.name, mem.bits, mem.mask_lanes, mem.mask_lane_width) port_ids = {p.port_id for p in mem.pins} for port_id in port_ids: functions = {p.function for p in mem.pins if p.port_id == port_id} diff --git a/flow/scripts/memories/schema_test.py b/flow/scripts/memories/schema_test.py index 31775f1b01..43aa7b4701 100644 --- a/flow/scripts/memories/schema_test.py +++ b/flow/scripts/memories/schema_test.py @@ -115,6 +115,32 @@ def test_validate_emittable(self): with self.assertRaises(schema.SchemaError): schema.validate_emittable(dataless) + def test_mask_lanes_cover_the_word(self): + # even split, ragged last lane (tc_sram's bytes over 25 bits), none + schema.lane_width_check("m", 64, 8, 0) + schema.lane_width_check("m", 25, 4, 8) + schema.lane_width_check("m", 25, 0, 0) + for bits, lanes, width in ( + (25, 4, 0), # not even, and no width given + (25, 4, 6), # 24 bits of lanes for 25 + (25, 4, 9), # the last lane would be empty + (25, 0, 8), # a width with no lanes + ): + with self.assertRaises(schema.SchemaError): + schema.lane_width_check("m", bits, lanes, width) + + def test_mask_lane_width_survives_merge_and_dump(self): + base = sample_memory() + override = schema.Memory(name=base.name, mask_lanes=4, mask_lane_width=8) + [merged] = schema.merge([base], [override]) + self.assertEqual(merged.mask_lane_width, 8) + with tempfile.TemporaryDirectory() as d: + path = Path(d) / "memories.json" + schema.dump([merged], path, "asap7") + self.assertEqual( + json.loads(path.read_text())["memories"][0]["mask_lane_width"], 8 + ) + if __name__ == "__main__": unittest.main() diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index 1e39732d99..d1236d929a 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -146,6 +146,7 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): "additional_height": additional_height_for_pins(process, bits, rows, lanes), "pin_names": pin_names, "mask_lanes": lanes, + "mask_lane_width": int(m.get("mask_lane_width", 0)), } mem_config = MemoryConfig.from_json(sram_dict) ram = MemoryFactory.create(mem_config, "RAM", "SP", process, timing_data) diff --git a/tools/FakeRAM2.0/test/spsram_mask_test.py b/tools/FakeRAM2.0/test/spsram_mask_test.py index 1aa7ea1fe6..a04a9b3910 100755 --- a/tools/FakeRAM2.0/test/spsram_mask_test.py +++ b/tools/FakeRAM2.0/test/spsram_mask_test.py @@ -22,6 +22,7 @@ from timing_data import TimingData from ram_liberty_exporter import RAMLibertyExporter from lef_exporter import LefExporter +from single_port_ram_verilog_exporter import SinglePortRAMVerilogExporter from test_utils import TestUtils from orfs_asap7.generate import ( ASAP7_TIMING_CONFIG, @@ -132,6 +133,28 @@ def test_mask_must_divide_the_word(self): with self.assertRaises(Exception): MemoryConfig("m", 114, 64, 1, 0, RW0_NAMES, 5) + def test_ragged_last_lane(self): + # tc_sram's byte enables over a 25-bit word: 8, 8, 8 and 1 bits + cfg = MemoryConfig("tag_64x25", 25, 64, 1, 0, RW0_NAMES, 4, 8) + self.assertEqual(cfg.get_mask_lane_width(), 8) + ram = MemoryFactory.create(cfg, "RAM", "SP", self.process, self.timing) + out = io.StringIO() + SinglePortRAMVerilogExporter(ram).export_module(out) + v = out.getvalue() + self.assertIn("mem[RW0_addr][23:16] <= RW0_wdata[23:16];", v) + self.assertIn("if (RW0_wmask[3])", v) + self.assertIn("mem[RW0_addr][24:24] <= RW0_wdata[24:24];", v) + + def test_lane_width_must_leave_a_last_lane(self): + for width in (6, 9): # 4 x 6 < 25; 3 x 9 >= 25 leaves lane 4 empty + with self.assertRaises(Exception): + MemoryConfig("m", 25, 64, 1, 0, RW0_NAMES, 4, width) + + def test_even_split_is_unchanged(self): + self.assertEqual( + MemoryConfig("m", 114, 64, 1, 0, RW0_NAMES, 2).get_mask_lane_width(), 57 + ) + class SinglePortPinsTest(unittest.TestCase): def test_reads_names_and_lanes(self): diff --git a/tools/FakeRAM2.0/utils/class_memory.py b/tools/FakeRAM2.0/utils/class_memory.py index 1cb9046dbc..1ba7f85db7 100644 --- a/tools/FakeRAM2.0/utils/class_memory.py +++ b/tools/FakeRAM2.0/utils/class_memory.py @@ -37,6 +37,7 @@ def __init__(self, mem_config, process, timing_data): self.total_size = self.width_in_bytes * self.depth self.additional_height = mem_config.get_additional_height() self.mask_lanes = mem_config.get_mask_lanes() + self.mask_lane_width = mem_config.get_mask_lane_width() self.timing_data = timing_data self.physical = PhysicalData() if self.process.calc_dimensions(): @@ -96,6 +97,10 @@ def get_width(self): """Returns the width in bits""" return self.width_in_bits + def get_mask_lane_width(self): + """Returns the bits per write mask lane; the last holds the rest""" + return self.mask_lane_width + def get_num_banks(self): """Returns the number of banks""" return self.num_banks diff --git a/tools/FakeRAM2.0/utils/memory_config.py b/tools/FakeRAM2.0/utils/memory_config.py index db202c76d7..411559a1dd 100644 --- a/tools/FakeRAM2.0/utils/memory_config.py +++ b/tools/FakeRAM2.0/utils/memory_config.py @@ -13,6 +13,7 @@ def __init__( additional_height, pin_names=None, mask_lanes=0, + mask_lane_width=0, ): """Initializer""" @@ -27,10 +28,21 @@ def __init__( # and writes whole words. self._pin_names = dict(pin_names) if pin_names else {} self._mask_lanes = int(mask_lanes or 0) - if self._mask_lanes and width_in_bits % self._mask_lanes: + # Bits per lane; the last lane holds the rest (tc_sram's byte + # enables over a word that is not whole bytes). 0: an even split. + self._mask_lane_width = int(mask_lane_width or 0) + lanes, width = self._mask_lanes, self._mask_lane_width + if lanes and not width: + if width_in_bits % lanes: + raise Exception( + "Width %d is not a whole number of %d mask lanes (%s)" + % (width_in_bits, lanes, name) + ) + self._mask_lane_width = width_in_bits // lanes + elif width and not (lanes * width >= width_in_bits > (lanes - 1) * width): raise Exception( - "Width %d is not a whole number of %d mask lanes (%s)" - % (width_in_bits, self._mask_lanes, name) + "%d mask lanes of %d bits do not cover width %d (%s)" + % (lanes, width, width_in_bits, name) ) def set_name(self, name): @@ -65,6 +77,10 @@ def get_mask_lanes(self): """Returns the number of write mask lanes, 0 when unmasked""" return self._mask_lanes + def get_mask_lane_width(self): + """Returns the bits per mask lane; the last lane holds the rest""" + return self._mask_lane_width + @staticmethod def from_json(json_data): return MemoryConfig( @@ -75,4 +91,5 @@ def from_json(json_data): json_data.get("additional_height", 0), json_data.get("pin_names"), json_data.get("mask_lanes", 0), + json_data.get("mask_lane_width", 0), ) diff --git a/tools/FakeRAM2.0/utils/single_port_ram_verilog_exporter.py b/tools/FakeRAM2.0/utils/single_port_ram_verilog_exporter.py index e5c1a84157..8e246e9ebf 100644 --- a/tools/FakeRAM2.0/utils/single_port_ram_verilog_exporter.py +++ b/tools/FakeRAM2.0/utils/single_port_ram_verilog_exporter.py @@ -32,6 +32,14 @@ def _get_mask(self): rw_port_group.get_write_mask_lanes(), ) + def _lane_range(self, j): + """Bit range [msb:lsb] of mask lane j; the last lane holds the rest""" + mem = self.get_memory() + width = mem.get_mask_lane_width() + lsb = j * width + msb = min(lsb + width, mem.get_width()) - 1 + return f"[{msb}:{lsb}]" + def export_module(self, out_fh): """Exports the verilog module to the output stream""" @@ -69,11 +77,19 @@ def export_module(self, out_fh): out_fh.write(" begin\n") mask_bus, lanes = self._get_mask() if mask_bus: - out_fh.write(" for (j = 0; j < MASK_LANES; j = j + 1)\n") - out_fh.write(f" if ({mask_bus}[j])\n") - out_fh.write( - f" mem[{addr_bus}][j*LANE_WIDTH +: LANE_WIDTH] <= {din_bus}[j*LANE_WIDTH +: LANE_WIDTH];\n" - ) + if lanes * mem.get_mask_lane_width() == mem.get_width(): + out_fh.write(" for (j = 0; j < MASK_LANES; j = j + 1)\n") + out_fh.write(f" if ({mask_bus}[j])\n") + out_fh.write( + f" mem[{addr_bus}][j*LANE_WIDTH +: LANE_WIDTH] <= {din_bus}[j*LANE_WIDTH +: LANE_WIDTH];\n" + ) + else: + for j in range(lanes): + lane = self._lane_range(j) + out_fh.write(f" if ({mask_bus}[{j}])\n") + out_fh.write( + f" mem[{addr_bus}]{lane} <= {din_bus}{lane};\n" + ) else: out_fh.write(f" mem[{addr_bus}] <= {din_bus};\n") out_fh.write(" end\n") @@ -114,7 +130,7 @@ def write_module_ports(self, out_fh): out_fh.write(f" parameter corrupt_mem_on_X_p = 1;\n") if mask_bus: out_fh.write(f" parameter MASK_LANES = {lanes};\n") - out_fh.write(f" parameter LANE_WIDTH = {mem.get_width() // lanes};\n") + out_fh.write(f" parameter LANE_WIDTH = {mem.get_mask_lane_width()};\n") out_fh.write("\n") out_fh.write(f" output reg [BITS-1:0] {dout_bus};\n") out_fh.write(f" input [ADDR_WIDTH-1:0] {addr_bus};\n") From 3785274e67ea195226cb119fc50686910beed83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sun, 1 Nov 2026 10:10:59 +0100 Subject: [PATCH 2/5] memories: refuse an uneven mask split where it is declared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate_emittable checked the lanes only when mask_lane_width was given, so a memory whose bits do not split evenly over mask_lanes got past the schema and failed later in FakeRAM. It is now refused in the schema, naming the memory. FakeRAM names a mask_lane_width given without mask_lanes, instead of reporting zero lanes that do not cover the word. gen_memories_test's 512 x 17 memory kept the 4 write enable bits and 32 data bits of the 128 x 32 it was made from; it now has one of each per bit, as yosys writes it. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/gen_memories_test.py | 5 +++++ flow/scripts/memories/schema.py | 2 +- tools/FakeRAM2.0/utils/memory_config.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index e71aed7844..78d43f8b9e 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -157,6 +157,11 @@ def test_deep_memory_is_folded_square(self): netlist = json.loads(json.dumps(SAMPLE_YOSYS_NETLIST)) mem = netlist["modules"].pop("\\mem_128x32") mem["cells"]["\\$mem_0"]["parameters"].update(SIZE=512, WIDTH=17) + # as yosys writes it: a data bit and a write enable bit per bit + conn = mem["cells"]["\\$mem_0"]["connections"] + for port in ("RD_DATA", "WR_DATA"): + conn[port] = conn[port][:17] + conn["WR_EN"] = conn["WR_EN"][:1] * 17 netlist["modules"] = {"\\mem_512x17": mem} d = Path(self.tmp.name) (d / "netlist.json").write_text(json.dumps(netlist)) diff --git a/flow/scripts/memories/schema.py b/flow/scripts/memories/schema.py index 6aaada0f5c..a962c3a96f 100644 --- a/flow/scripts/memories/schema.py +++ b/flow/scripts/memories/schema.py @@ -238,7 +238,7 @@ def validate_emittable(mem: Memory) -> None: f"memory {mem.name}: rows ({mem.rows}) and bits ({mem.bits}) " f"must be >= 1" ) - if mem.mask_lane_width: + if mem.mask_lanes or mem.mask_lane_width: lane_width_check(mem.name, mem.bits, mem.mask_lanes, mem.mask_lane_width) port_ids = {p.port_id for p in mem.pins} for port_id in port_ids: diff --git a/tools/FakeRAM2.0/utils/memory_config.py b/tools/FakeRAM2.0/utils/memory_config.py index 411559a1dd..2555f796da 100644 --- a/tools/FakeRAM2.0/utils/memory_config.py +++ b/tools/FakeRAM2.0/utils/memory_config.py @@ -39,6 +39,8 @@ def __init__( % (width_in_bits, lanes, name) ) self._mask_lane_width = width_in_bits // lanes + elif width and not lanes: + raise Exception("mask_lane_width without mask_lanes (%s)" % name) elif width and not (lanes * width >= width_in_bits > (lanes - 1) * width): raise Exception( "%d mask lanes of %d bits do not cover width %d (%s)" From 7118dcaad75680a74e81eee0fecb5327e1244a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sun, 1 Nov 2026 15:09:47 +0100 Subject: [PATCH 3/5] docs: AutoMemories' mask_lane_width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An override entry may set mask_lane_width for a word that is not a whole number of write-mask lanes; say so where the overrides are documented. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index d6c9b7aa47..4c3c7b5d2f 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -94,6 +94,19 @@ carries win, everything else (geometry, pins) is kept from detection. A `.memories` entry naming a module the scanner never found is taken whole — it must then describe its pins itself. +Write-mask lanes split the word evenly. A word that is not a whole +number of lanes, such as pulp's `tc_sram` with byte enables over a +25-bit word, needs `mask_lane_width` in its entry. Every lane is that +wide except the last, which holds the remaining bits: + +```json +{ "name": "tag_ram", "mask_lanes": 4, "mask_lane_width": 8 } +``` + +The lanes must cover the word with a last lane of 1 to +`mask_lane_width` bits; anything else is refused, as is an uneven split +without `mask_lane_width`. + ## Generated views The views come from FakeRAM2.0's asap7 backend From d70413da273cda7344c959a70cce8a35898d6e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Mon, 2 Nov 2026 18:26:52 +0100 Subject: [PATCH 4/5] memories: refuse a mask pin that is not mask_lanes wide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FakeRAM takes a memory's lane count from its mask pin's width and never reads mask_lanes, and merge keeps the detected pins unless an override brings its own. An override that set mask_lanes on a detected memory passed the schema and then failed in FakeRAM with an error that did not point at the cause, such as "mask_lane_width without mask_lanes" when mask_lanes was set. validate_emittable now refuses a mask pin whose width is not mask_lanes, and mask_lanes on a memory with no mask pin, naming the memory. Detected memories already agree: yosys's mask pin is mask_lanes wide, and a firtool memory takes mask_lanes from its pin. The guard before lane_width_check goes; that check returns by itself when there are no lanes and no width. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/schema.py | 26 ++++++++++++++++++++++++-- flow/scripts/memories/schema_test.py | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/flow/scripts/memories/schema.py b/flow/scripts/memories/schema.py index a962c3a96f..a63dccf02c 100644 --- a/flow/scripts/memories/schema.py +++ b/flow/scripts/memories/schema.py @@ -229,6 +229,28 @@ def lane_width_check(name: str, bits: int, lanes: int, width: int) -> None: ) +def mask_check(mem: Memory) -> None: + """Raise SchemaError unless every mask pin is `mask_lanes` wide. + + FakeRAM counts the lanes from the mask pin's width, never from + mask_lanes, and an override merged onto a detected memory keeps the + detected pins: an override that changes mask_lanes must bring a + mask pin to match. + """ + masks = [p for p in mem.pins if p.function == "mask"] + if mem.mask_lanes and not masks: + raise SchemaError( + f"memory {mem.name}: mask_lanes is {mem.mask_lanes} but no pin " + f"has function mask" + ) + for p in masks: + if p.width != mem.mask_lanes: + raise SchemaError( + f"memory {mem.name}: mask pin {p.name} is {p.width} bits wide " + f"but mask_lanes is {mem.mask_lanes}" + ) + + def validate_emittable(mem: Memory) -> None: """Raise SchemaError unless `mem` carries enough to emit .lib/.lef.""" if not mem.pins: @@ -238,8 +260,8 @@ def validate_emittable(mem: Memory) -> None: f"memory {mem.name}: rows ({mem.rows}) and bits ({mem.bits}) " f"must be >= 1" ) - if mem.mask_lanes or mem.mask_lane_width: - lane_width_check(mem.name, mem.bits, mem.mask_lanes, mem.mask_lane_width) + lane_width_check(mem.name, mem.bits, mem.mask_lanes, mem.mask_lane_width) + mask_check(mem) port_ids = {p.port_id for p in mem.pins} for port_id in port_ids: functions = {p.function for p in mem.pins if p.port_id == port_id} diff --git a/flow/scripts/memories/schema_test.py b/flow/scripts/memories/schema_test.py index 43aa7b4701..bd93147967 100644 --- a/flow/scripts/memories/schema_test.py +++ b/flow/scripts/memories/schema_test.py @@ -129,6 +129,25 @@ def test_mask_lanes_cover_the_word(self): with self.assertRaises(schema.SchemaError): schema.lane_width_check("m", bits, lanes, width) + def test_mask_pin_matches_mask_lanes(self): + masked = sample_memory() + masked.mask_lanes = 4 + masked.pins.append(schema.Pin("W0_mask", "input", 4, "W0", "mask")) + schema.validate_emittable(masked) # no raise + # the override changed mask_lanes, the detected pin is per bit + narrow = sample_memory() + narrow.mask_lanes = 4 + narrow.pins.append(schema.Pin("W0_mask", "input", 32, "W0", "mask")) + # mask_lanes on a memory with no mask pin + pinless = sample_memory() + pinless.mask_lanes = 4 + # a mask pin with no mask_lanes + laneless = sample_memory() + laneless.pins.append(schema.Pin("W0_mask", "input", 4, "W0", "mask")) + for mem in (narrow, pinless, laneless): + with self.assertRaisesRegex(schema.SchemaError, mem.name): + schema.validate_emittable(mem) + def test_mask_lane_width_survives_merge_and_dump(self): base = sample_memory() override = schema.Memory(name=base.name, mask_lanes=4, mask_lane_width=8) From 59a9a4da406d39ae64bf2c42787c495face3756d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Mon, 2 Nov 2026 18:26:52 +0100 Subject: [PATCH 5/5] docs: an AutoMemories mask override brings its mask pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mask_lane_width example was a bare override, which on a detected memory keeps the detected pins and is now refused. Show the whole entry, pins with a mask pin mask_lanes wide among them, and say that an override changing mask_lanes must bring its pins. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 4c3c7b5d2f..d60698a92c 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -100,13 +100,38 @@ number of lanes, such as pulp's `tc_sram` with byte enables over a wide except the last, which holds the remaining bits: ```json -{ "name": "tag_ram", "mask_lanes": 4, "mask_lane_width": 8 } +{ + "name": "tag_ram", + "rows": 64, + "bits": 25, + "addr_w": 6, + "rw_ports": 1, + "mask_lanes": 4, + "mask_lane_width": 8, + "idiomatic": true, + "reason": "one read-write port, byte enables, the last 1 bit", + "pins": [ + {"name": "clk", "direction": "input", "width": 1, "port_id": "RW0", "function": "clk"}, + {"name": "cs", "direction": "input", "width": 1, "port_id": "RW0", "function": "en"}, + {"name": "we", "direction": "input", "width": 1, "port_id": "RW0", "function": "wmode"}, + {"name": "addr", "direction": "input", "width": 6, "port_id": "RW0", "function": "addr"}, + {"name": "wdata", "direction": "input", "width": 25, "port_id": "RW0", "function": "data_in"}, + {"name": "be", "direction": "input", "width": 4, "port_id": "RW0", "function": "mask"}, + {"name": "rdata", "direction": "output", "width": 25, "port_id": "RW0", "function": "data_out"} + ] +} ``` The lanes must cover the word with a last lane of 1 to `mask_lane_width` bits; anything else is refused, as is an uneven split without `mask_lane_width`. +The macro takes its lane count from the mask pin's width, so every +mask pin must be `mask_lanes` wide, and a memory with `mask_lanes` must +have one; anything else is refused, naming the memory. An override +merged onto a detected memory keeps the detected pins: one that changes +`mask_lanes` must bring its pins, with a mask pin to match. + ## Generated views The views come from FakeRAM2.0's asap7 backend