Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/user/AutoMemories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions flow/scripts/memories/gen_memories_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
55 changes: 55 additions & 0 deletions flow/scripts/memories/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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}
Expand Down
45 changes: 45 additions & 0 deletions flow/scripts/memories/schema_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions tools/FakeRAM2.0/orfs_asap7/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Comment thread
oharboe marked this conversation as resolved.
Resolved
}
mem_config = MemoryConfig.from_json(sram_dict)
ram = MemoryFactory.create(mem_config, "RAM", "SP", process, timing_data)
Expand Down
23 changes: 23 additions & 0 deletions tools/FakeRAM2.0/test/spsram_mask_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions tools/FakeRAM2.0/utils/class_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions tools/FakeRAM2.0/utils/memory_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(
additional_height,
pin_names=None,
mask_lanes=0,
mask_lane_width=0,
):
"""Initializer"""

Expand All @@ -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)
)
Comment thread
oharboe marked this conversation as resolved.
Resolved

def set_name(self, name):
Expand Down Expand Up @@ -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(
Expand All @@ -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),
)
28 changes: 22 additions & 6 deletions tools/FakeRAM2.0/utils/single_port_ram_verilog_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down