mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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:
61 files changed
+6038
-410
No files matched your search
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user