mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- Added validation checks for GA2D soft reset and error handling in . - Improved resource controller tests in to cover IRQ routing and ownership scenarios. - Updated performance log tests in to reflect changes in GA2D features and formats. - Introduced a new netlist transaction test for GA2D in to ensure synthesized block functionality. - Modified publication software tests in to account for new GA2D stages. - Adjusted script tools tests in for updated configuration values. - Added print statements for better traceability in GA2D reference campaign tests.
244 lines
9.9 KiB
Python
244 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate and summarize the isolated apb4_ga2d block synthesis evidence.
|
|
|
|
Reads the reports produced by the GA2D-scoped yosys run (physical/smoke/syn/
|
|
yosys/ga2d_block.mk), fails loudly on inferred latches or non-PDK
|
|
(black-box/unmapped) cells, and writes ga2d_block_summary.json next to the
|
|
area report for the Phase 6 evidence section.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
from scripts.setup_helpers import atomic_write # noqa: E402
|
|
|
|
FIFO_INSTANCES = ("u_foreground_fifo", "u_background_fifo", "u_output_fifo")
|
|
LATCH_INFERRED = re.compile(r"latch inferred", re.IGNORECASE)
|
|
CHECK_PROBLEMS = re.compile(r"found and reported (\d+) problems?", re.IGNORECASE)
|
|
# Generic (pre-technology-mapping) cell types that must never survive the
|
|
# coarse synthesis passes: unresolved memories and inferred latches.
|
|
GENERIC_FORBIDDEN = ("$mem", "$dlatch", "$_dlatch")
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--log", type=Path, required=True, help="yosys flow log")
|
|
parser.add_argument("--check-report", type=Path, required=True, help="yosys check report")
|
|
parser.add_argument("--area-json", type=Path, required=True, help="yosys stat -json report")
|
|
parser.add_argument(
|
|
"--registers-report", type=Path, required=True, help="pre-map DFF selection report"
|
|
)
|
|
parser.add_argument(
|
|
"--generic-report", type=Path, required=True, help="pre-flatten cmos stat report"
|
|
)
|
|
parser.add_argument("--top", required=True, help="synthesized top module name")
|
|
parser.add_argument("--pdk", required=True)
|
|
parser.add_argument("--recipe", required=True)
|
|
parser.add_argument("--period-ps", type=int, required=True)
|
|
parser.add_argument(
|
|
"--cell-prefix",
|
|
required=True,
|
|
help="required prefix for every mapped cell type (PDK standard cells)",
|
|
)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def read_text(path: Path, label: str) -> str:
|
|
try:
|
|
return path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError as error:
|
|
raise ValueError(f"cannot read {label}: {error}") from error
|
|
|
|
|
|
def load_area(path: Path, top: str) -> dict:
|
|
try:
|
|
document = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
raise ValueError(f"cannot parse area report {path}: {error}") from error
|
|
modules = document.get("modules")
|
|
if not isinstance(modules, dict):
|
|
raise ValueError(f"area report {path} has no modules object")
|
|
entry = modules.get(top)
|
|
if entry is None:
|
|
# yosys writes Verilog-escaped module names with a leading backslash.
|
|
entry = modules.get(f"\\{top}")
|
|
if not isinstance(entry, dict):
|
|
raise ValueError(f"area report {path} has no modules entry for top {top}")
|
|
if not isinstance(entry.get("num_cells"), int):
|
|
raise ValueError(f"area report {path} is missing num_cells for top {top}")
|
|
if not isinstance(entry.get("num_cells_by_type"), dict):
|
|
raise ValueError(f"area report {path} is missing num_cells_by_type for top {top}")
|
|
if not isinstance(entry.get("area"), (int, float)):
|
|
raise ValueError(f"area report {path} is missing area for top {top}")
|
|
return entry
|
|
|
|
|
|
def classify_cells(
|
|
cells_by_type: dict, cell_prefix: str
|
|
) -> tuple[dict[str, int], dict[str, int], dict[str, int]]:
|
|
dff_types: dict[str, int] = {}
|
|
latch_types: dict[str, int] = {}
|
|
foreign_types: dict[str, int] = {}
|
|
for cell_type, count in sorted(cells_by_type.items()):
|
|
if not isinstance(count, int) or count < 0:
|
|
raise ValueError(f"invalid cell count for {cell_type}: {count}")
|
|
if not cell_type.startswith(cell_prefix):
|
|
foreign_types[cell_type] = count
|
|
elif re.match(rf"^{re.escape(cell_prefix)}dl[hl]", cell_type):
|
|
# IHP130 level-sensitive latches: dlhq/dlhr/dlhrq/dllr/dllrq. The
|
|
# dlygate* delay cells share only the "dl" prefix and stay excluded.
|
|
latch_types[cell_type] = count
|
|
elif re.match(rf"^{re.escape(cell_prefix)}s?df", cell_type):
|
|
dff_types[cell_type] = count
|
|
return dff_types, latch_types, foreign_types
|
|
|
|
|
|
def count_register_lines(report: str) -> tuple[int, dict[str, dict[str, int]]]:
|
|
lines = [line.strip() for line in report.splitlines() if line.strip()]
|
|
fifo: dict[str, dict[str, int]] = {}
|
|
for instance in FIFO_INSTANCES:
|
|
instance_lines = [line for line in lines if instance in line]
|
|
storage_lines = [line for line in instance_lines if "r_storage" in line]
|
|
fifo[instance] = {
|
|
"dff_cells": len(instance_lines),
|
|
"storage_dff_cells": len(storage_lines),
|
|
}
|
|
return len(lines), fifo
|
|
|
|
|
|
def parse_generic_fifo(report: str) -> dict[str, dict[str, int]]:
|
|
# The slang frontend specializes parameterized modules per instance, so
|
|
# each GA2D FIFO appears as its own section: fifo$<hierarchical path>.
|
|
sections = re.split(r"^===\s+", report, flags=re.MULTILINE)
|
|
mapping: dict[str, dict[str, int]] = {}
|
|
for instance in FIFO_INSTANCES:
|
|
body = next(
|
|
(
|
|
section
|
|
for section in sections
|
|
if section.startswith("fifo$") and instance in section.split("===", 1)[0]
|
|
),
|
|
None,
|
|
)
|
|
if body is None:
|
|
raise ValueError(f"generic report has no fifo section for {instance}")
|
|
cells = re.search(r"^\s+(\d+)\s+cells\s*$", body, re.MULTILINE)
|
|
dffe_bits = {
|
|
name: int(match.group(1))
|
|
for name in ("$_DFFE_PP_", "$_DFFE_PN0P_")
|
|
if (match := re.search(rf"^\s+(\d+)\s+{re.escape(name)}\s*$", body, re.MULTILINE))
|
|
is not None
|
|
}
|
|
if cells is None or not dffe_bits:
|
|
raise ValueError(f"generic report fifo section for {instance} lacks DFF counts")
|
|
mapping[instance] = {
|
|
"cells": int(cells.group(1)),
|
|
"dff_cells": sum(dffe_bits.values()),
|
|
"storage_dff_cells": dffe_bits.get("$_DFFE_PP_", 0),
|
|
}
|
|
return mapping
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
problems: list[str] = []
|
|
try:
|
|
log_text = read_text(args.log, "yosys log")
|
|
check_text = read_text(args.check_report, "check report")
|
|
registers_text = read_text(args.registers_report, "registers report")
|
|
generic_text = read_text(args.generic_report, "generic report")
|
|
area = load_area(args.area_json, args.top)
|
|
except ValueError as error:
|
|
print(f"ga2d_block_summary: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
inferred_latches = LATCH_INFERRED.findall(log_text)
|
|
if inferred_latches:
|
|
problems.append(f"yosys log reports {len(inferred_latches)} inferred-latch message(s)")
|
|
|
|
problems_match = CHECK_PROBLEMS.search(check_text)
|
|
check_problems = int(problems_match.group(1)) if problems_match else -1
|
|
if problems_match is None:
|
|
problems.append("check report does not end with a problem count")
|
|
elif check_problems != 0:
|
|
problems.append(f"yosys check reported {check_problems} problem(s)")
|
|
|
|
generic_forbidden = sorted({token for token in GENERIC_FORBIDDEN if token in generic_text})
|
|
if generic_forbidden:
|
|
problems.append(
|
|
"generic report retains forbidden cell type(s): " + ", ".join(generic_forbidden)
|
|
)
|
|
|
|
cells_by_type = area["num_cells_by_type"]
|
|
dff_types, latch_types, foreign_types = classify_cells(cells_by_type, args.cell_prefix)
|
|
if latch_types:
|
|
problems.append(f"mapped latch cell(s): {latch_types}")
|
|
if foreign_types:
|
|
problems.append(f"black-box or unmapped cell type(s): {foreign_types}")
|
|
|
|
pre_map_dff, fifo = count_register_lines(registers_text)
|
|
try:
|
|
generic_fifo = parse_generic_fifo(generic_text)
|
|
except ValueError as error:
|
|
problems.append(str(error))
|
|
generic_fifo = {}
|
|
for instance, counts in fifo.items():
|
|
if counts["dff_cells"] == 0:
|
|
problems.append(f"no DFF cells found for FIFO instance {instance}")
|
|
for instance, counts in generic_fifo.items():
|
|
# The Common fifo storage is expected to map to 32 x 64 DFF bits.
|
|
if counts["storage_dff_cells"] != 32 * 64:
|
|
problems.append(
|
|
f"FIFO instance {instance} storage mapped to "
|
|
f"{counts['storage_dff_cells']} DFF bit(s), expected {32 * 64}"
|
|
)
|
|
|
|
if problems:
|
|
for problem in problems:
|
|
print(f"ga2d_block_summary: FAIL: {problem}", file=sys.stderr)
|
|
return 1
|
|
|
|
fifo_mapping = {
|
|
instance: {
|
|
"pre_map_dff_cells": counts["dff_cells"],
|
|
"pre_map_storage_dff_cells": counts["storage_dff_cells"],
|
|
**generic_fifo.get(instance, {}),
|
|
}
|
|
for instance, counts in fifo.items()
|
|
}
|
|
summary = {
|
|
"top": args.top,
|
|
"pdk": args.pdk,
|
|
"synth_recipe": args.recipe,
|
|
"target_period_ps": args.period_ps,
|
|
"total_cells": area["num_cells"],
|
|
"area": area["area"],
|
|
"dff_count": sum(dff_types.values()),
|
|
"dff_cells_by_type": dff_types,
|
|
"pre_map_dff_count": pre_map_dff,
|
|
"latch_count": sum(latch_types.values()),
|
|
"black_box_cells": sum(foreign_types.values()),
|
|
"check_problems": check_problems,
|
|
"fifo_mapping": fifo_mapping,
|
|
}
|
|
atomic_write(args.output, json.dumps(summary, indent=2, sort_keys=True) + "\n")
|
|
print(
|
|
"ga2d_block_summary: PASS: "
|
|
f"cells={summary['total_cells']} area={summary['area']} "
|
|
f"dff={summary['dff_count']} latches=0 black_boxes=0 "
|
|
f"fifo_dff={ {name: counts['dff_cells'] for name, counts in fifo.items()} }"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|