diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index d6c9b7aa47..d60698a92c 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -94,6 +94,44 @@ 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", + "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 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 754ae8373c..a63dccf02c 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,53 @@ 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 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: @@ -207,6 +260,8 @@ def validate_emittable(mem: Memory) -> None: f"memory {mem.name}: rows ({mem.rows}) and bits ({mem.bits}) " f"must be >= 1" ) + 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 31775f1b01..bd93147967 100644 --- a/flow/scripts/memories/schema_test.py +++ b/flow/scripts/memories/schema_test.py @@ -115,6 +115,51 @@ 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_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) + [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 a002f4ac9e..7e877dba10 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -155,6 +155,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..2555f796da 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,23 @@ 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: + raise Exception("mask_lane_width without mask_lanes (%s)" % name) + 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 +79,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 +93,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")