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:
maksyuki committed 2026-09-20 08:52:23 +08:00
1 parent 95ee570a44
commit d3ef9ae27f
31 files changed
+4943 -408

No files matched your search

+243
View File
@@ -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())
+43 -1
View File
@@ -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,