mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: enhance GA2D and Resource Controller Test Coverage
- 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.
This commit is contained in:
31 files changed
+4943
-408
No files matched your search
@@ -0,0 +1,243 @@
|
||||
#!/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())
|
||||
@@ -7,6 +7,7 @@ import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -30,6 +31,7 @@ REQUIRED_FIELDS = {
|
||||
"jobs",
|
||||
"cpu_cycles",
|
||||
"cpu_hz",
|
||||
"pclk_hz",
|
||||
"ga2d_features",
|
||||
"ga2d_limits",
|
||||
"ga2d_formats",
|
||||
@@ -37,13 +39,45 @@ REQUIRED_FIELDS = {
|
||||
"ga2d_read_bytes",
|
||||
"ga2d_write_bytes",
|
||||
}
|
||||
OPTIONAL_FIELDS = ("width", "height", "pitch")
|
||||
TOKEN = re.compile(r"([a-z0-9_]+)=([^\s]+)")
|
||||
QUANTUM = Decimal("0.001")
|
||||
|
||||
|
||||
def parse_number(value: str) -> int:
|
||||
return int(value, 0)
|
||||
|
||||
|
||||
def ratio_string(numerator: int, denominator: int) -> str:
|
||||
value = (Decimal(numerator) / Decimal(denominator)).quantize(
|
||||
QUANTUM, rounding=ROUND_HALF_UP
|
||||
)
|
||||
return str(value)
|
||||
|
||||
|
||||
def derived_ga2d_metrics(fields: dict[str, str]) -> dict[str, str]:
|
||||
ga2d_cycles = parse_number(fields["ga2d_cycles"])
|
||||
cpu_cycles = parse_number(fields["cpu_cycles"])
|
||||
pixels = parse_number(fields["pixels"])
|
||||
workload_bytes = parse_number(fields["workload_bytes"])
|
||||
cpu_hz = parse_number(fields["cpu_hz"])
|
||||
pclk_hz = parse_number(fields["pclk_hz"])
|
||||
derived: dict[str, str] = {}
|
||||
if ga2d_cycles <= 0 or pclk_hz <= 0:
|
||||
return derived
|
||||
derived["ga2d_job_latency_us"] = ratio_string(ga2d_cycles * 1_000_000, pclk_hz)
|
||||
derived["ga2d_effective_mb_per_s"] = ratio_string(
|
||||
workload_bytes * pclk_hz, ga2d_cycles * 1_000_000
|
||||
)
|
||||
if pixels > 0:
|
||||
derived["ga2d_pixels_per_s"] = ratio_string(pixels * pclk_hz, ga2d_cycles)
|
||||
if cpu_hz > 0:
|
||||
derived["lp_cpu_us"] = ratio_string(cpu_cycles * 1_000_000, cpu_hz)
|
||||
if pixels > 0:
|
||||
derived["lp_cpu_cycles_per_pixel"] = ratio_string(cpu_cycles, pixels)
|
||||
return derived
|
||||
|
||||
|
||||
def parse_log(content: str) -> dict[str, object]:
|
||||
samples: list[dict[str, object]] = []
|
||||
for line in content.splitlines():
|
||||
@@ -68,11 +102,18 @@ def parse_log(content: str) -> dict[str, object]:
|
||||
"ga2d_write_bytes": parse_number(fields["ga2d_write_bytes"]),
|
||||
"configuration": {
|
||||
"cpu_hz": parse_number(fields["cpu_hz"]),
|
||||
"pclk_hz": parse_number(fields["pclk_hz"]),
|
||||
"ga2d_features": parse_number(fields["ga2d_features"]),
|
||||
"ga2d_limits": parse_number(fields["ga2d_limits"]),
|
||||
"ga2d_formats": parse_number(fields["ga2d_formats"]),
|
||||
},
|
||||
}
|
||||
for optional in OPTIONAL_FIELDS:
|
||||
if optional in fields:
|
||||
sample[optional] = parse_number(fields[optional])
|
||||
derived = derived_ga2d_metrics(fields)
|
||||
if derived:
|
||||
sample["derived"] = derived
|
||||
for counter in sorted(
|
||||
REQUIRED_FIELDS
|
||||
- {
|
||||
@@ -86,6 +127,7 @@ def parse_log(content: str) -> dict[str, object]:
|
||||
"jobs",
|
||||
"cpu_cycles",
|
||||
"cpu_hz",
|
||||
"pclk_hz",
|
||||
"ga2d_features",
|
||||
"ga2d_limits",
|
||||
"ga2d_formats",
|
||||
@@ -99,7 +141,7 @@ def parse_log(content: str) -> dict[str, object]:
|
||||
passed = PASS_MARKER in content
|
||||
failure_marker = any(line.startswith(FAIL_PREFIX) for line in content.splitlines())
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"schema_version": 3,
|
||||
"status": "passed" if passed and samples and not failure_marker else "failed",
|
||||
"failure_marker": failure_marker,
|
||||
"pass_marker": passed,
|
||||
|
||||
Reference in new issue
Block a user