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
1 change: 1 addition & 0 deletions flow/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json
mkdir -p $(RESULTS_DIR)
$(PYTHON_EXE) $(SCRIPTS_DIR)/memories/gen_memories.py \
--platform $(PLATFORM) \
--top $(DESIGN_NAME) \
--yosys-json $(RESULTS_DIR)/memories_inferred.json \
--out-dir $(RESULTS_DIR)/memories \
--json $(RESULTS_DIR)/memories.json \
Expand Down
71 changes: 68 additions & 3 deletions flow/scripts/memories/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,14 @@ def _clock_enable_bits(val, ports: int) -> str:
return val


def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]:
"""Extract memory modules from a Yosys netlist JSON output ($mem_v2 cells)."""
def scan_yosys_json(
data: dict | str | Path, top: str | None = None
) -> list[schema.Memory]:
"""Extract memory modules from a Yosys netlist JSON output ($mem_v2 cells).

`top` is the design's top module; slang's per-instance module names
are cut back to their definition with it (see definition_name).
"""
if isinstance(data, (str, Path)):
data = json.loads(Path(data).read_text())

Expand All @@ -122,6 +128,13 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]:

for mod_name, mod_info in modules.items():
clean_mod_name = mod_name[1:] if mod_name.startswith("\\") else mod_name
# slang's --keep-hierarchy elaborates one module per instance and
# names it `<definition>$<instance path>`; the flow blackboxes by
# definition (`--blackboxed-module <definition>` is what the
# frontend honours, and the black box it imports has the
# definition's type), so the memory is named for the definition
# and its copies collapse into one entry below.
clean_mod_name = definition_name(clean_mod_name, top)
cells = mod_info.get("cells", {})
mem_cells = [
n for n, c in cells.items() if c.get("type", "") in ("$mem_v2", "$mem")
Expand Down Expand Up @@ -289,4 +302,56 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]:
)
out.append(mem)

return out
return dedupe(out)


def definition_name(module: str, top: str | None) -> str:
"""`array_64x114$top.u0` -> `array_64x114` for top module `top`.

slang's instance path starts at the top module, so the name is cut
where `$<top>.` begins, not at the first `$`: a definition may have a
`$` of its own (`my$ram$top.u0` is `my$ram`). Any other name, yosys's
own `$paramod\\...` names among them, is returned unchanged.
"""
if top:
i = module.find(f"${top}.")
if i > 0:
return module[:i]
return module


def dedupe(memories: list[schema.Memory]) -> list[schema.Memory]:
"""One entry per name; copies must agree on shape and pins.

The copies are one definition elaborated per instance, so they agree
unless the definition is parameterised on the memory's shape -- and
then one liberty view per definition cannot serve them, which is an
error rather than a silent pick.
"""

def shape(m):
return (
m.rows,
m.bits,
m.read_ports,
m.write_ports,
m.rw_ports,
m.mask_lanes,
m.comb_read_ports,
m.port_convention,
tuple(m.pins),
)

by_name: dict[str, schema.Memory] = {}
for m in memories:
first = by_name.get(m.name)
if first is None:
by_name[m.name] = m
elif shape(first) != shape(m):
raise ValueError(
f"memory {m.name}: two elaborations differ in shape "
f"({first.rows}x{first.bits} against {m.rows}x{m.bits}, or in "
"ports); a definition parameterised on its memory cannot be "
"one macro"
)
return list(by_name.values())
38 changes: 38 additions & 0 deletions flow/scripts/memories/detect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,44 @@ def test_one_memory_per_module_is_named_after_the_module(self):
mems = detect.scan_yosys_json(yosys_json)
self.assertEqual(sorted(m.name for m in mems), ["array_256x66", "array_64x114"])

def test_slang_uniquified_copies_collapse_to_the_definition(self):
# slang --keep-hierarchy names a module per instance; the flow
# blackboxes the definition, so that is the memory's name and its
# copies are one entry
yosys_json = {
"modules": {
"\\array_64x114$top.t0.u0": _firtool_rw_module(),
"\\array_64x114$top.t0.u1": _firtool_rw_module(),
"\\array_256x66$top.t1.u0": _firtool_rw_module(256, 66, 1),
}
}
mems = detect.scan_yosys_json(yosys_json, top="top")
self.assertEqual(sorted(m.name for m in mems), ["array_256x66", "array_64x114"])
by = {m.name: m for m in mems}
self.assertEqual(by["array_64x114"].behavioral_model["module"], "array_64x114")

def test_definition_name_is_cut_where_the_instance_path_starts(self):
name = detect.definition_name
# read_slang names modules this way: a definition with a `$` of its
# own, an escaped one starting with a digit, a single instance
self.assertEqual(name("my$ram$top2.u2", "top2"), "my$ram")
self.assertEqual(name("1d_ram$top2.u0", "top2"), "1d_ram")
self.assertEqual(name("ram$top.u0", "top"), "ram")
# not a slang instance name: unchanged
self.assertEqual(name("$paramod\\foo\\W=8", "top"), "$paramod\\foo\\W=8")
self.assertEqual(name("my$ram", "top"), "my$ram")
self.assertEqual(name("ram$top.u0", None), "ram$top.u0")

def test_copies_of_one_definition_must_agree(self):
yosys_json = {
"modules": {
"\\array_64x114$top.t0.u0": _firtool_rw_module(),
"\\array_64x114$top.t0.u1": _firtool_rw_module(64, 114, 1),
}
}
with self.assertRaises(ValueError):
detect.scan_yosys_json(yosys_json, top="top")

def test_rw_port_pins_are_the_module_ports(self):
(m,) = detect.scan_yosys_json(
{"modules": {"\\array_64x114": _firtool_rw_module()}}
Expand Down
9 changes: 8 additions & 1 deletion flow/scripts/memories/gen_memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ def run(
platform: str,
out_dir: Path,
json_path: Path,
top: str | None = None,
) -> int:
if platform != "asap7":
sys.stderr.write(f"gen_memories: unsupported platform {platform}\n")
return 1

found = detect.scan_yosys_json(yosys_json)
found = detect.scan_yosys_json(yosys_json, top)

idiomatic.apply(found)

Expand Down Expand Up @@ -114,13 +115,19 @@ def main(argv: list[str] | None = None) -> int:
type=Path,
help="Path of the memories.json inventory to write.",
)
p.add_argument(
"--top",
help="The design's top module, which slang's per-instance module "
"names carry.",
)
args = p.parse_args(argv)
return run(
args.yosys_json,
args.memories,
args.platform,
args.out_dir,
args.json,
args.top,
)


Expand Down
Loading