feat(tiny): implement R2-P9 DVP capture and recovery

Connect DVP to Tiny DMA2/request 11, IRQ15 and the camera pad/reset lifecycle.
Harden shared capture CDC, frame statistics, per-row size validation and HAL
cleanup while preserving the Mini DMA3 binding and existing register ABI.

Add deterministic capture/recovery firmware, source-bound evidence tooling,
focused RTL/host tests and compact netlist acceptance. Preserve SAFE24,
128 KiB main SRAM, private Crypto storage and the locked dependencies.

Retained validation: focused RTL, host/policy checks, Mini firmware, and
ICS55/IHP130 RECOVERY, QVGA, synthesis and boot/DVP netlist checks pass.
Full-system VGA remains historical; long full Icarus and broad regressions
remain deferred. Global publication/format gaps and negative observational
STA are retained, not reported as passing qualification.

Specification: docs/ip/tiny-soc.md, TINY-R2-P9.
Evidence: docs/ip/tiny-soc-verification.md.
This commit is contained in:
maksyuki committed 2026-10-17 15:32:59 +08:00
1 parent fd03776a36
commit 8f5630fb41
61 files changed
+6038 -410

No files matched your search

+1 -1
View File
@@ -140,7 +140,7 @@ def validate(document_path: Path, root: Path) -> None:
raise ValueError("unrecognized generated APB crossing contract")
expected = {"aon": ["clint", "archinfo"], "memory": ["xpi"],
"peripheral": ["gpio", "uart0", "timer0", "timer1", "i2c0", "dma",
"pwm", "rtc", "wdg", "rng", "crc", "ws2812", "crypto", "i2s"]}
"pwm", "rtc", "wdg", "rng", "crc", "ws2812", "crypto", "i2s", "dvp"]}
if (generated.get("destinations") != expected or
generated.get("same_clock_isolation") != ["sdio"] or
generated.get("native_system") != ["sysctrl", "sram"] or
+15 -10
View File
@@ -18,15 +18,15 @@ from scripts.setup_helpers import atomic_write # noqa: E402
TINY = ROOT / "rtl/tiny"
RCU_TARGETS = {"gpio": 1, "uart0": 2, "i2c0": 3, "timer0": 4, "timer1": 5,
"pwm": 6, "i2s": 7, "sdio": 8, "dma": 9, "ws2812": 10,
"rng": 11, "crc": 12, "crypto": 13}
"rng": 11, "crc": 12, "crypto": 13, "dvp": 14}
def read_topology(path: Path, memory_map: Path) -> dict:
document = json.loads(path.read_text())
regions = {r["symbol"] for r in json.loads(memory_map.read_text())["regions"]}
if (document.get("schema_version") != 1 or document.get("dma_channels") != 8
or document.get("dma_request_mask") != "0x31ff"):
raise ValueError("Tiny P6 requires eight DMA channels and request mask 0x31ff")
or document.get("dma_request_mask") != "0x39ff"):
raise ValueError("Tiny P9 requires eight DMA channels and request mask 0x39ff")
if document.get("cpu") != {"module": "hazard3_cpu_2port", "local_ports": 2,
"data_outstanding": 1}:
raise ValueError("Tiny P4 requires independent I/D and one ordered data operation")
@@ -43,9 +43,9 @@ def read_topology(path: Path, memory_map: Path) -> dict:
"cpu_combined_outstanding": 1, "dma_direction_outstanding": 1,
"sdio_direction_outstanding": 1, "target_combined_outstanding": 1,
"sram_targets": [0, 1, 2, 3], "xpi_target": 4, "apb_target_base": 5,
"decode_error_target": 25, "protocol_error_target": 26, "target_count": 27,
"decode_error_target": 26, "protocol_error_target": 27, "target_count": 28,
}:
raise ValueError("Tiny P6 requires three active owners and 27 targets")
raise ValueError("Tiny P9 requires three active owners and 28 targets")
if document.get("irq_vector_width") != 32 or document.get("external_irq_count") != 30:
raise ValueError("Tiny requires a 32-bit IRQ vector and 30 external inputs")
names: set[str] = set()
@@ -65,10 +65,10 @@ def read_topology(path: Path, memory_map: Path) -> dict:
assigned.add(region)
if assigned != {r for r in regions if r.startswith("APB4_")}:
raise ValueError("Tiny topology must route every APB region")
if len(document["apb_targets"]) != 20:
raise ValueError("Tiny P6 requires twenty independent APB targets")
if len(document["apb_targets"]) != 21:
raise ValueError("Tiny P9 requires twenty-one independent APB targets")
target_names = [target["name"] for target in document["apb_targets"]]
if len(set(target_names)) != 20 or any(re.fullmatch(r"[a-z][a-z0-9_]*", name) is None
if len(set(target_names)) != 21 or any(re.fullmatch(r"[a-z][a-z0-9_]*", name) is None
for name in target_names):
raise ValueError("Tiny APB target names must be unique identifiers")
if sorted(item["pin"] for item in document["gpio_alt_functions"]) != list(range(32)):
@@ -140,14 +140,15 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
"#define RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RS_SOC_TINY 1\n#define RS_SOC_HAS_PSRAM 0U\n"
"#define RS_SOC_DMA_CHANNELS 8U\n#define RS_SOC_ID 0x54494E59U\n"
"#define RS_SOC_DMA_REQUEST_MASK 0x31FFU\n"
f"#define RS_SOC_DMA_REQUEST_MASK {document['dma_request_mask']}U\n"
"#define RS_SOC_HAS_I2C1 0U\n#define RS_SOC_HAS_SDIO1 0U\n"
"#define RS_SOC_HAS_UART0 1U\n#define RS_SOC_HAS_UART1 0U\n"
"#define RS_SOC_HAS_I2C0 1U\n"
"#define RS_SOC_HAS_RNG 1U\n#define RS_SOC_HAS_CRC 1U\n"
"#define RS_SOC_HAS_WS2812 1U\n#define RS_SOC_HAS_CRYPTO 1U\n"
"#define RS_SOC_HAS_I2S 1U\n#define RS_SOC_HAS_SDIO0 1U\n"
"#define RS_SOC_HAS_DVP 0U\n#define RS_SOC_RNG_QUALIFIED 0U\n"
"#define RS_SOC_HAS_DVP 1U\n#define RS_SOC_DVP_DMA_CHANNEL 2U\n"
"#define RS_SOC_DVP_DMA_BURST_BEATS 4U\n#define RS_SOC_RNG_QUALIFIED 0U\n"
f"#define RS_SOC_TECHNOLOGY_ID 0x{platform['technology']:08X}U\n"
f"#define RS_SOC_PLL_PRESENT {platform['pll_present']}U\n"
f"#define RS_SOC_GPIO_HAS_PULLS {platform['gpio_pulls']}U\n#endif\n")
@@ -187,6 +188,10 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
f".gated_o(s_actual_gate[{target_bit}]), "
f".held_o(s_reset_held[{target_bit}]));")
extra_ready = " && s_i2s_reset_ready" if name == "i2s" else ""
if name == "dvp":
# Local camera reset waits for its pixel domain. Global CPU
# boot uses APB/AXI barriers, not this local-reset vector.
extra_ready = " && s_dvp_reset_ready"
if name == "dma":
# CDC-ready outputs are observed in SYS, matching APB readiness.
extra_ready = " && s_dma_ready_sys && s_dma_idle_sys"
+16
View File
@@ -35,6 +35,7 @@ TEST_SOURCES = (
"crt/src/hal/i2s_math.c",
"crt/src/hal/dma_math.c",
"crt/src/hal/dma.c",
"crt/src/hal/dvp.c",
"crt/src/hal/crypto_lifecycle.c",
"crt/src/hal/jpeg_math.c",
"crt/src/hal/apu.c",
@@ -115,6 +116,7 @@ def main() -> int:
"-fdata-sections",
"-Wl,--gc-sections",
"-DRS_DMA_TEST_MMIO",
"-DRS_DVP_TEST_MMIO",
"-DRS_WS2812_TEST_MMIO",
"-DRS_APU_TEST_MMIO",
"-DRS_CRYPTO_TEST_MMIO",
@@ -154,6 +156,20 @@ def main() -> int:
"-o", str(tiny_object),
], check=True)
command.append(str(tiny_object))
tiny_dvp_object = temporary_root / "tiny_dvp.o"
dvp_functions = ("probe", "configure", "start", "abort", "flush", "get_status",
"interrupt_enable", "interrupt_clear", "interrupt_test",
"capture_dma", "capture_pio")
subprocess.run([
compiler, "-std=c11", "-Wall", "-Wextra", "-Werror", "-DRS_SOC_TINY",
"-DRS_DVP_TEST_MMIO", "-DRS_SOC_DVP_DMA_BURST_BEATS=4U",
*[f"-Drs_dvp_{name}=rs_tiny_dvp_{name}" for name in dvp_functions],
"-Dip_dvp_test=ip_tiny_dvp_test",
"-I", str(memory_map_root / "include"),
"-I", str(user_extensions_root / "include"), "-I", str(root / "crt/include"),
"-c", str(root / "crt/src/hal/dvp.c"), "-o", str(tiny_dvp_object),
], check=True)
command.append(str(tiny_dvp_object))
subprocess.run(command, check=True)
subprocess.run([str(executable)], check=True)
return 0
+349
View File
@@ -0,0 +1,349 @@
"""Validate strict Tiny R2-P9 camera workload records."""
from __future__ import annotations
import re
import hashlib
from pathlib import Path
from copy import deepcopy
import argparse
import subprocess
import sys
import shutil
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402
PHASE = "TINY-R2-P9"
def recovery_measurements(content: str, config: dict) -> dict:
if config.get("SOC") != "TINY" or config.get("PDK") not in ("ICS55", "IHP130") or config.get("TINY_P9_QUICK", "NO") != "NO":
raise ValueError("invalid recovery configuration")
cases = re.findall(r"^P9_RECOVERY_CASE id=(\d+) pass=(\d+)$", content, re.M)
if cases != [(str(index), "1") for index in range(1, 8)]:
raise ValueError("missing, failed or duplicated recovery case")
terminal = re.findall(r"^P9_RECOVERY_RESULT code=(\d+) cases=(\d+) frames=(\d+) irqs=(\d+)$", content, re.M)
if terminal != [("0", "7", "6", "6")]:
raise ValueError("incomplete recovery frame/IRQ result")
diagnostics = re.findall(r"^P9_RECOVERY_DIAG high=(\d+) reset_status=(\d+) fault=(\d+) dvp_error=(\d+) words=(\d+)$", content, re.M)
if (len(diagnostics) != 2 or [row[0] for row in diagnostics] != ["0", "1"] or
any(row[1] != str((1 << 32) - 4) or int(row[2]) == 0 for row in diagnostics)):
raise ValueError("missing stopped-clock reset rejection diagnostics")
board = re.findall(r"^P9_RECOVERY_BOARD controls=(\d+) audio_cases=(\d+)$", content, re.M)
if board != [("8", "2")]:
raise ValueError("missing recovery board-control evidence")
return {"cases": cases, "terminal": terminal[0], "diagnostics": diagnostics, "board": board}
def measurements(content: str, config: dict) -> dict:
if DEFAULT_FAILURE.search(content) or len(re.findall(
r"^SIM_TEST_PASS Tiny cycles=[1-9][0-9]*$", content, re.M)) != 1:
raise ValueError("missing strict Tiny terminal success or forbidden marker")
if config.get("TINY_P9_WORKLOAD") == "RECOVERY":
return recovery_measurements(content, config)
terminal = re.findall(r"^P9_RESULT code=(\d+) frames=(\d+) bytes=(\d+)$", content, re.M)
if len(terminal) != 1 or terminal[0][0] != "0":
raise ValueError("missing or failed P9 result")
def records(prefix: str) -> list[dict]:
result = []
for line in content.splitlines():
if not line.startswith(prefix + " "):
continue
fields = line.split()[1:]
if any(re.fullmatch(r"[a-z_]+=[0-9]+", field) is None for field in fields):
raise ValueError("malformed measurement field")
row = dict((key, int(value)) for key, value in (field.split("=") for field in fields))
if len(row) != len(fields):
raise ValueError("duplicate measurement field")
result.append(row)
return result
rows = records("P9_FRAME")
workload = config.get("TINY_P9_WORKLOAD", "QVGA_RGB565")
quick = config.get("TINY_P9_QUICK", "NO") == "YES"
if workload not in ("QVGA_RGB565", "VGA_YUV422") or (quick and workload != "QVGA_RGB565"):
raise ValueError("invalid P9 workload selection")
expected = (1, 640, 480) if workload == "VGA_YUV422" else (
(0, 16, 8) if quick else (0, 320, 240))
if len(rows) != 1 or tuple(rows[0].get(key) for key in ("format", "width", "height")) != expected:
raise ValueError("frame does not match selected workload")
if (not rows or len(rows) != int(terminal[0][1]) or
sum(row.get("bytes", 0) for row in rows) != int(terminal[0][2])):
raise ValueError("frame records do not match terminal byte count")
for row in rows:
if row.get("bytes", 0) <= 0 or row.get("dma_channel") != 2 or row.get("request") != 11:
raise ValueError("invalid DVP DMA binding or frame length")
if row.get("dma_bytes") != row.get("bytes") or row.get("guards") != 1:
raise ValueError("incomplete DMA/readback/guard evidence")
if (row.get("format") not in (0, 1) or row.get("width", 0) <= 0 or
row["width"] % 2 or row.get("height", 0) <= 0 or
row["bytes"] != row["width"] * row["height"] * 2 or row.get("readback") != 1):
raise ValueError("invalid dimensions or missing payload verification")
pio = records("P9_PIO")
counts = records("P9_COUNTS")
expected_counts = {"words": expected[1] * expected[2] // 2,
"pixels": expected[1] * expected[2], "xpi_error": 0}
if (counts and counts != [expected_counts]) or (workload == "VGA_YUV422" and not counts):
raise ValueError("missing or inconsistent full-width frame counts")
if len(pio) != 1 or pio[0] != {"width": 7, "height": 3, "valid_bytes": 42,
"storage_bytes": 48, "readback": 1, "guards": 1}:
raise ValueError("missing odd-width PIO readback")
observations = records("P9_OBSERVATION")
words = sum(row["bytes"] // 4 for row in rows) + 12
if len(observations) != 1:
raise ValueError("missing session observation")
observed = observations[0]
if (observed.get("version") != 1 or observed.get("accepted_words") != words or
observed.get("delivered_words") != words or
not 0 < observed.get("fifo_high_water", 0) <= 128 or
observed.get("backpressure", -1) < 0 or observed.get("pixel_half_ns", 0) <= 0):
raise ValueError("incomplete or inconsistent FIFO observations")
if config.get("SOC") != "TINY" or config.get("PDK") not in ("ICS55", "IHP130"):
raise ValueError("P9 evidence is not bound to a Tiny supported profile")
return {"frames": rows, "pio": pio, "counts": counts,
"observations": observations, "terminal": terminal[0]}
def validate_run(content: str, config: dict, flow: dict, source: dict, current: dict,
*, build: dict | None = None) -> dict:
"""Acceptance requires the captured run identity as well as parsed UART.
The caller retains the build record captured from fresh outputs and the
source snapshot taken before compilation, separately from the run record.
A parsed log alone is never a qualified report.
"""
digest = hashlib.sha256(content.encode()).hexdigest()
if (flow.get("status") != "passed" or flow.get("exit_code") != 0 or
flow.get("log_sha256") != digest or not flow.get("command")):
raise ValueError("unsuccessful or unbound simulation command")
if not source or source != current or any(
not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None
for value in source.values()):
raise ValueError("missing or stale source snapshot")
if config.get("APP") != "ci_smoke" or config.get("TINY_P9_ACCEPTANCE") != "YES":
raise ValueError("not the P9 acceptance image")
if flow.get("configuration") != config or flow.get("source_inputs") != source:
raise ValueError("run does not match source/configuration")
if (not build or build.get("phase") != PHASE or build.get("status") != "passed" or
build.get("source_before") != source or build.get("source_after") != source or
build.get("configuration") != config):
raise ValueError("missing or stale build identity")
if (not build.get("fresh_outputs") or not build.get("build_results") or
any(result.get("exit_code") != 0 or result.get("status") != "passed"
for result in build["build_results"])):
raise ValueError("missing successful fresh-build commands")
artifacts = build.get("artifacts", {})
if set(artifacts) != {"elf", "hex", "simulator"} or flow.get("artifacts") != artifacts:
raise ValueError("missing or mismatched run artifacts")
for name, artifact in artifacts.items():
try:
path = Path(artifact["path"])
data = path.read_bytes()
if (not path.is_absolute() or len(data) != artifact["bytes"] or
hashlib.sha256(data).hexdigest() != artifact["sha256"]):
raise ValueError(f"changed {name} artifact")
except (OSError, KeyError, TypeError) as error:
raise ValueError(f"missing {name} artifact") from error
# Bind the launch arguments to the retained executable and firmware, not
# merely to an arbitrary nonempty command string supplied with the log.
cwd = Path(flow.get("cwd", ".")).resolve()
command = flow["command"]
def resolved(value: str) -> Path:
return (cwd / value).resolve()
simulator = Path(artifacts["simulator"]["path"]).resolve()
firmware = Path(artifacts["hex"]["path"]).resolve()
if (not isinstance(command, list) or not all(isinstance(value, str) for value in command) or
simulator not in [resolved(value) for value in command if not value.startswith(("+", "-"))] or
[resolved(value.split("=", 1)[1]) for value in command if value.startswith("+firmware=")] != [firmware]):
raise ValueError("launch does not use the captured executable/firmware")
measured = measurements(content, config)
if config.get("TINY_P9_WORKLOAD") == "RECOVERY":
required = ("+tiny_psram", "+tiny_p9_camera", "+tiny_p9_small", "+tiny_p9_recovery")
forbidden = ("+tiny_p9_vga", "+tiny_p9_diag", "+tiny_p9_progress", "+tiny_p9_no_pixclk", "+p8_absent")
if any(command.count(flag) != 1 for flag in required) or any(flag.startswith(forbidden) for flag in command):
raise ValueError("recovery launch mismatch")
if config.get("TINY_P9_WORKLOAD") == "VGA_YUV422":
expected_camera = ("+tiny_psram", "+tiny_p9_camera", "+tiny_p9_vga")
periods = [value for value in command if value.startswith("+tiny_p9_half_ns=")]
rejected = ("+tiny_p9_small", "+tiny_p9_diag", "+tiny_p9_progress",
"+tiny_p9_no_pixclk", "+tiny_p9_pixclk_high", "+tiny_p8_psram", "+p8_absent")
if (any(command.count(value) != 1 for value in expected_camera) or
any(value.startswith(rejected) for value in command) or len(periods) != 1 or
periods[0] != f"+tiny_p9_half_ns={measured['observations'][0]['pixel_half_ns']}"):
raise ValueError("camera launch does not match VGA workload observations")
return measured
def require_absent_outputs(outputs: dict[str, Path]) -> None:
if any(path.exists() or path.is_symlink() for path in outputs.values()):
raise ValueError("select a fresh build variant; outputs already exist")
def prepare_build(config: dict, source: dict, outputs: dict[str, Path]) -> dict:
"""Capture input identity before building into previously absent outputs."""
if not source or set(outputs) != {"elf", "hex", "simulator"}:
raise ValueError("incomplete fresh-build inputs")
require_absent_outputs(outputs)
return deepcopy({"configuration": config, "source_before": source,
"outputs": {name: str(path.resolve()) for name, path in outputs.items()}})
def capture_build(config: dict, source_before: dict, source_after: dict,
results: list[dict], outputs: dict[str, Path], *, prepared: dict) -> dict:
"""Record fresh build outputs after all source-bound build commands succeed.
Use a new variant directory; capture source_before before compilation.
Keep this record immutable when recording subsequent simulation runs.
"""
if (not source_before or source_before != source_after or not results or
any(result.get("exit_code") != 0 or result.get("status") != "passed" for result in results)):
raise ValueError("build failed or sources changed during compilation")
if set(outputs) != {"elf", "hex", "simulator"}:
raise ValueError("incomplete build outputs")
if (prepared.get("configuration") != config or prepared.get("source_before") != source_before or
prepared.get("outputs") != {name: str(path.resolve()) for name, path in outputs.items()}):
raise ValueError("outputs do not match the pre-build capture")
artifacts = {}
for name, path in outputs.items():
data = path.read_bytes()
artifacts[name] = {"path": str(path.resolve()), "bytes": len(data),
"sha256": hashlib.sha256(data).hexdigest()}
return deepcopy({"phase": PHASE, "status": "passed", "configuration": config,
"source_before": source_before, "source_after": source_after,
"artifacts": artifacts, "build_results": results, "fresh_outputs": True})
def run_campaign(args: argparse.Namespace) -> int:
"""One fresh build and one bounded execution; never retry or overwrite."""
from scripts import tiny_r2_baseline as baseline
profile = f"configs/ci/{args.pdk.lower()}-tiny.mk"
make = ["make", "--no-print-directory", f"CONFIG={profile}", "SOC=TINY",
f"PDK={args.pdk}", "APP=ci_smoke", "TINY_P9_ACCEPTANCE=YES", "TINY_P9_QUICK=NO",
f"TINY_P9_WORKLOAD={args.workload}", "SIMU=VERILATOR",
f"BUILD_TIMESTAMP={args.timestamp}", f"JOBS={args.jobs}"]
expanded = subprocess.run([*make, "config"], cwd=ROOT, text=True,
capture_output=True, check=True)
values = dict(line.split(None, 1) for line in expanded.stdout.splitlines() if line.strip())
variant = Path(values["VARIANT_ROOT"]).resolve()
evidence = variant / "meta/tiny-r2-p9/full-frame"
outputs = {"elf": variant / "sw/firmware", "hex": variant / "sw/retrosoc_fw.hex",
"simulator": variant / "sim/verilator/emu"}
# Reject collisions before even writing evidence: generation can overwrite
# an older build's manifest/filelists although its final outputs are intact.
try:
require_absent_outputs(outputs)
if evidence.exists() or evidence.is_symlink():
raise ValueError("select a fresh build variant; evidence already exists")
evidence.mkdir(parents=True, exist_ok=False)
except (ValueError, OSError) as error:
print(f"{args.pdk}: campaign rejected: {error}", file=sys.stderr)
return 1
assessment = {"phase": PHASE, "phase_accepted": False, "profile": profile, "pdk": args.pdk,
"workload": args.workload, "operating_point": "SAFE24", "status": "running",
"maximum_attempts": 1, "unrun": ["full Icarus", "broad regression",
"synthesis/netlist", "STA", "physical qualification", "remaining P9 matrix"]}
baseline.dump(evidence / "assessment.json", assessment)
try:
identities = {}
for program in ("riscv32-unknown-elf-gcc", "verilator", "verilator_bin", "make",
"cc", "c++", "timeout", "stdbuf"):
executable = shutil.which(program)
if executable is None:
raise ValueError(f"required tool unavailable: {program}")
identities[program] = baseline.artifact(Path(executable).resolve())
baseline.dump(evidence / "tool-identity.json", {
"executables": identities, "python": baseline.artifact(Path(sys.executable).resolve()),
"lock": baseline.artifact(ROOT / "dependencies/dependencies.lock.json"),
"archive_verification": "not performed by this campaign"})
baseline.execute(ROOT, evidence, "generate", [*make, "generate_filelist", "manifest"],
cwd=ROOT, quiet=True)
config = baseline.configuration(variant)["configuration"]
def inputs() -> dict:
result = baseline.source_inputs(ROOT, args.pdk) | baseline.runtime_inputs(
ROOT, variant, "verilator")
for name in ("docs/ip/tiny-soc.md", "docs/ip/tiny-soc-r2-dvp.md",
"tests/test_tiny_r2_p9.py", "tests/test_tiny_r2_p9_evidence.py",
"tests/c/test_runtime.c"):
result[name] = baseline.sha256(ROOT / name)
return result
before = inputs()
prepared = prepare_build(config, before, outputs)
baseline.dump(evidence / "prepared-build.json", prepared)
baseline.dump(evidence / "source-snapshot.json", baseline.snapshot(ROOT, evidence / "source", before))
print(f"{args.pdk}: building fresh {args.workload}; evidence={evidence}", flush=True)
built = baseline.execute(ROOT, evidence, "build", [*make, "firmware", "comp", "manifest"],
cwd=ROOT, quiet=True)
build = capture_build(config, before, inputs(), [built], outputs, prepared=prepared)
baseline.dump(evidence / "build-identity.json", build)
for artifact in identities.values():
baseline.check_artifact(artifact)
command = ["timeout", "--foreground", "--kill-after=30s", f"{args.wall_timeout}s",
"stdbuf", "-oL", "-eL", str(outputs["simulator"]),
f"+firmware={outputs['hex']}", f"+max_cycles={args.max_cycles}",
"+tiny_psram", "+tiny_p9_camera", f"+tiny_p9_half_ns={args.pixel_half_ns}",
"+tiny_p9_monitor", f"+tiny_p9_monitor_log={evidence / 'progress.log'}"]
if args.workload == "VGA_YUV422":
command.append("+tiny_p9_vga")
elif args.workload == "RECOVERY":
command += ["+tiny_p9_small", "+tiny_p9_recovery"]
baseline.dump(evidence / "launch.json", {
"command": command, "cwd": str(ROOT), "configuration": config,
"source_inputs": before, "artifacts": build["artifacts"], "maximum_attempts": 1,
"build_identity": baseline.artifact(evidence / "build-identity.json")})
print(f"{args.pdk}: starting single {args.workload} replay", flush=True)
run = subprocess.run([sys.executable, str(ROOT / "scripts/run_flow.py"),
"--tool", "tiny-p9-full-frame", "--cwd", str(ROOT),
"--log", str(evidence / "console.log"), "--result", str(evidence / "result-sim.json"),
"--", *command], cwd=ROOT, check=False, stdout=subprocess.DEVNULL)
flow = baseline.read_json(evidence / "result-sim.json")
content = (evidence / "console.log").read_text()
bound = {**flow, "source_inputs": before, "configuration": config,
"artifacts": build["artifacts"], "log_sha256": hashlib.sha256(content.encode()).hexdigest()}
baseline.dump(evidence / "bound-result.json", bound)
check = subprocess.run([sys.executable, str(ROOT / "scripts/check_simulation.py"),
"--log", str(evidence / "console.log"),
"--result", str(evidence / "result-sim-check.json"),
"--require", "SIM_TEST_PASS Tiny"], cwd=ROOT, check=False)
measured = validate_run(content, config, bound, before, inputs(), build=build)
if run.returncode != 0 or check.returncode != 0:
raise ValueError("simulation command/check failed")
for artifact in identities.values():
baseline.check_artifact(artifact)
assessment.update(status="workload_passed", measurements=measured,
duration_seconds=flow.get("duration_seconds"),
build=baseline.artifact(evidence / "build-identity.json"),
log=baseline.artifact(evidence / "console.log"))
except (ValueError, OSError, subprocess.CalledProcessError) as error:
assessment.update(status="failed_or_incomplete", reason=str(error))
baseline.dump(evidence / "assessment.json", assessment)
print(f"{args.pdk}: {assessment['status']}; {evidence / 'assessment.json'}", flush=True)
return 0 if assessment["status"] == "workload_passed" else 1
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
actions = parser.add_subparsers(dest="action", required=True)
run = actions.add_parser("run", help="fresh build plus one source-bound Verilator execution")
run.add_argument("--pdk", required=True, choices=("ICS55", "IHP130"))
run.add_argument("--workload", required=True, choices=("QVGA_RGB565", "VGA_YUV422", "RECOVERY"))
run.add_argument("--timestamp", required=True)
run.add_argument("--jobs", type=int, default=4)
run.add_argument("--pixel-half-ns", type=int, default=500)
run.add_argument("--max-cycles", type=int, default=160000000)
run.add_argument("--wall-timeout", type=int, default=43200)
args = parser.parse_args()
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}", args.timestamp):
parser.error("timestamp must be YYYY-MM-DD-HH-MM")
if min(args.jobs, args.pixel_half_ns, args.max_cycles, args.wall_timeout) <= 0:
parser.error("jobs, pixel period and execution limits must be positive")
return run_campaign(args)
if __name__ == "__main__":
sys.exit(main())