mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat(spi): implement SPI-P4 sub-stage A display acceptance
Add the frozen display path on top of the committed SPI-P3 integration: the project-owned ST7789VI four-wire adapter in app/board (held-CS command/data chains, D/C at clean boundaries with readback, bounded workspaces), the TINY_SPI_P4_ACCEPTANCE ci_smoke image (discovery, panel init ordering, color bars, gradient, odd rectangles, rotation variant, abort/recover/release/reacquire case, measurements), the pin-independent panel oracle rtl/tiny/dv/tiny_st7789_panel.sv, the golden geometry/pixel checker scripts/spi_p4.py, pytest contracts tests/test_spi_p4.py and the st7789vi host tests. The runbook is docs/ip/spi-p4.md. Validation: Verilator acceptance passes on both Tiny profiles at SIM_TEST_PASS Tiny cycles=43019427 (build/ics55-tiny-2026-10-23-01-37-106e35060a8e/, build/ihp130-tiny-2026-10-23-01-38-616e0604657d/) and Icarus behavioral passes on both profiles with the documented QUICK delay/geometry variant at cycles=2710932 (build/ics55-tiny-2026-10-23-01-34-106e35060a8e/, build/ihp130-tiny-2026-10-23-01-39-616e0604657d/); the independent golden checker reports zero problems on all four logs (8/8 windows, pixel counts and FNV-1a checksums, violations=0); full Pytest 2102 passed, 0 failed, 1 skipped; C format/policy/host and RTL format gates clean. Sub-stage B (PSRAM-source display, sequential capture/verify/SD save, SRAM-vs-PSRAM performance) remains gated on R2-P8/R2-P9 human acceptance. SPI-P5 remains deferred; phase_accepted stays false. Recorded in docs/ip/spi-verification.md.
This commit is contained in:
15 files changed
+1258
-2
No files matched your search
@@ -56,6 +56,7 @@ TEST_SOURCES = (
|
||||
"rtl/managed/clusterip/rtc/sw/src/rtc.c",
|
||||
"app/media/src/video_player.c",
|
||||
"app/media/src/wav_audio.c",
|
||||
"app/board/src/st7789vi_math.c",
|
||||
"app/benchmark/npu/npu_p6_reference.c",
|
||||
"tests/c/test_runtime.c",
|
||||
)
|
||||
@@ -141,6 +142,8 @@ def main() -> int:
|
||||
"-I",
|
||||
str(root / "app/media/include"),
|
||||
"-I",
|
||||
str(root / "app/board/include"),
|
||||
"-I",
|
||||
str(root / "app/benchmark/npu"),
|
||||
"-o",
|
||||
str(executable),
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Source-bound focused SPI-P4 Tiny display checks and panel-oracle validation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.setup_helpers import atomic_write # noqa: E402
|
||||
|
||||
STAGE_GEOMETRY = {
|
||||
1: (0, 0, 319, 239),
|
||||
2: (0, 0, 319, 239),
|
||||
3: (13, 17, 45, 29),
|
||||
4: (319, 0, 319, 239),
|
||||
5: (200, 239, 260, 239),
|
||||
6: (5, 7, 45, 29),
|
||||
7: (100, 100, 116, 108),
|
||||
}
|
||||
BARS = [0xFFFF, 0xFFE0, 0x07FF, 0x07E0, 0xF81F, 0xF800, 0x001F, 0x0000]
|
||||
|
||||
|
||||
def golden_pixel(stage: int, row: int, col: int) -> int:
|
||||
if stage == 1:
|
||||
return BARS[min(col // 40, 7)]
|
||||
if stage in (2, 8):
|
||||
red5 = min(col >> 3, 31)
|
||||
green6 = min(row >> 2, 63)
|
||||
blue5 = min((row + col) >> 4, 31)
|
||||
return (red5 << 11) | (green6 << 5) | blue5
|
||||
if stage == 3:
|
||||
return 0xFFFF if (row + col) & 1 else 0x0000
|
||||
if stage == 4:
|
||||
return 0x07E0
|
||||
if stage == 5:
|
||||
return 0xFFE0
|
||||
if stage == 6:
|
||||
return ((col & 31) << 11) | ((row & 63) << 5) | ((row ^ col) & 31)
|
||||
if stage == 7:
|
||||
return 0xFFFF
|
||||
raise ValueError(f"unknown stage {stage}")
|
||||
|
||||
|
||||
def fnv1a(data: bytes) -> int:
|
||||
value = 2166136261
|
||||
for byte in data:
|
||||
value = ((value ^ byte) * 16777619) & 0xFFFFFFFF
|
||||
return value
|
||||
|
||||
|
||||
def window_stream(stage: int, x0: int, y0: int, x1: int, y1: int) -> bytes:
|
||||
out = bytearray()
|
||||
for row in range(y0, y1 + 1):
|
||||
for col in range(x0, x1 + 1):
|
||||
pixel = golden_pixel(stage, row, col)
|
||||
out.append((pixel >> 8) & 0xFF)
|
||||
out.append(pixel & 0xFF)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def configuration_digest(manifest: dict) -> str:
|
||||
payload = json.dumps(manifest["configuration"], sort_keys=True).encode("utf-8")
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def focused(output: Path, stage: str) -> int:
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
command = [sys.executable, "-m", "pytest", "-q", "tests/test_spi_p4.py",
|
||||
"tests/test_spi_p3.py", "tests/test_spi_register_parity.py"]
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
|
||||
atomic_write(output / "pytest.log", result.stdout + result.stderr)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def check_log(log: str, quick: bool = False) -> list[str]:
|
||||
problems = []
|
||||
windows = []
|
||||
violations = 0
|
||||
panel = None
|
||||
commands = 0
|
||||
saw_slpout = False
|
||||
saw_dispon = False
|
||||
for line in log.splitlines():
|
||||
if "SPI_P4_VIOLATION" in line:
|
||||
violations += 1
|
||||
elif "SPI_P4_CMD " in line:
|
||||
commands += 1
|
||||
fields = dict(re.findall(r"(\w+)=([0-9a-f]+)", line))
|
||||
code = int(fields["code"], 16)
|
||||
if code == 0x11:
|
||||
saw_slpout = True
|
||||
if code == 0x29:
|
||||
saw_dispon = True
|
||||
elif "SPI_P4_WINDOW " in line:
|
||||
fields = dict(re.findall(r"(\w+)=([0-9a-f]+)", line))
|
||||
windows.append(fields)
|
||||
elif "SPI_P4_PANEL " in line:
|
||||
panel = dict(re.findall(r"(\w+)=([0-9a-f]+)", line))
|
||||
if violations != 0:
|
||||
problems.append(f"{violations} panel violations recorded")
|
||||
if not saw_slpout or not saw_dispon:
|
||||
problems.append("init ordering missing SLPOUT or DISPON")
|
||||
full_geometry = (319, 239) if not quick else (39, 23)
|
||||
expected = [((0, 0) + full_geometry), ((0, 0) + full_geometry), (13, 17, 45, 29),
|
||||
(319, 0, 319, 239), (200, 239, 260, 239), (5, 7, 45, 29),
|
||||
((0, 0) + full_geometry), (100, 100, 116, 108)]
|
||||
checksum_stage = {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 8: 7}
|
||||
if len(windows) != len(expected):
|
||||
problems.append(f"expected {len(expected)} windows, saw {len(windows)}")
|
||||
return problems
|
||||
for index, (fields, geometry) in enumerate(zip(windows, expected)):
|
||||
seq = index + 1
|
||||
if int(fields["seq"], 16) != seq:
|
||||
problems.append(f"window {seq}: seq mismatch {fields['seq']}")
|
||||
got = tuple(int(fields[name], 16) for name in ("x0", "y0", "x1", "y1"))
|
||||
if got != geometry:
|
||||
problems.append(f"window {seq}: geometry {got} != {geometry}")
|
||||
continue
|
||||
pixels = int(fields["pixels"], 16)
|
||||
aborted = int(fields["aborted"], 16)
|
||||
want_pixels = (geometry[2] - geometry[0] + 1) * (geometry[3] - geometry[1] + 1)
|
||||
if seq == 7:
|
||||
if aborted != 1 or not (0 < pixels < want_pixels):
|
||||
problems.append(f"window 7: abort semantics wrong pixels={pixels} aborted={aborted}")
|
||||
continue
|
||||
if aborted != 0:
|
||||
problems.append(f"window {seq}: unexpected abort")
|
||||
if pixels != want_pixels:
|
||||
problems.append(f"window {seq}: pixels {pixels} != {want_pixels}")
|
||||
continue
|
||||
golden = fnv1a(window_stream(checksum_stage[seq], *geometry))
|
||||
if int(fields["fnv1a"], 16) != golden:
|
||||
problems.append(f"window {seq}: pixel checksum mismatch")
|
||||
if panel is None:
|
||||
problems.append("missing SPI_P4_PANEL summary")
|
||||
else:
|
||||
if int(panel["violations"], 16) != 0:
|
||||
problems.append("panel summary reports violations")
|
||||
if int(panel["pixels"], 16) != sum(int(w["pixels"], 16) for w in windows):
|
||||
problems.append("panel pixel total mismatch")
|
||||
if commands < 10:
|
||||
problems.append(f"too few commands observed: {commands}")
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("stage", choices=("lint", "rtl", "report", "check"))
|
||||
parser.add_argument("--variant-root", type=Path, required=True)
|
||||
parser.add_argument("--pdk", choices=("ICS55", "IHP130"), required=True)
|
||||
parser.add_argument("--log", type=Path)
|
||||
parser.add_argument("--quick", action="store_true")
|
||||
args = parser.parse_args()
|
||||
variant = args.variant_root.resolve()
|
||||
variant.relative_to(ROOT / "build")
|
||||
manifest = json.loads((variant / "meta/manifest.json").read_text())
|
||||
if manifest["configuration"]["SOC"] != "TINY" or manifest["configuration"]["PDK"] != args.pdk:
|
||||
raise ValueError("profile/PDK mismatch")
|
||||
output = variant / "meta/spi-p4" / args.stage
|
||||
if args.stage == "check":
|
||||
if args.log is None or not args.log.is_file():
|
||||
raise ValueError("check requires an existing --log sim.log path")
|
||||
problems = check_log(args.log.read_text(errors="replace"), quick=args.quick)
|
||||
status = "passed" if not problems else "failed"
|
||||
else:
|
||||
problems = []
|
||||
status = "passed" if focused(output, args.stage) == 0 else "failed"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
record = {"phase": "SPI-P4", "stage": args.stage, "status": status,
|
||||
"problems": problems, "configuration_digest": configuration_digest(manifest),
|
||||
"phase_accepted": False}
|
||||
atomic_write(output / "record.json", json.dumps(record, indent=2, sort_keys=True) + "\n")
|
||||
for problem in problems:
|
||||
print(problem, file=sys.stderr)
|
||||
return int(bool(problems)) if args.stage == "check" else (0 if status == "passed" else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user