mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: enhance Tiny ICS55 support in quality workflow and add validation tests
This commit is contained in:
6 files changed
+186
-7
No files matched your search
+2
-1
@@ -11,7 +11,8 @@ interface impact, validation evidence, and remaining risks. Follow the
|
||||
to merge-commit integration. These documents do not configure branch protection.
|
||||
|
||||
`workflows/quality.yml` runs the quality gate. It restores the locked IHP130
|
||||
PDK, third-party simulation models, and APU/NPU references and installs the
|
||||
and Tiny ICS55 PDKs (including the locked SRAM, IO and PLL views), third-party
|
||||
simulation models, and APU/NPU references and installs the
|
||||
locked Verilator, Icarus, sv2v, and Yosys tools before the complete Pytest
|
||||
suite. The reference cache includes the NPU MLPerf Tiny models, MFCC inputs,
|
||||
TensorFlow/gemmlowp oracle sources, and decoded VWW corpus; setup still
|
||||
|
||||
@@ -33,10 +33,12 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
physical/pdk/IHP-Open-PDK
|
||||
physical/pdk/icsprout55-pdk
|
||||
.cache/retrosoc/sources/ics55_ecos_pll
|
||||
rtl/managed/third_party
|
||||
.cache/retrosoc/pdk
|
||||
.cache/retrosoc/stamps
|
||||
key: sources-IHP130-${{ runner.os }}-${{ hashFiles('dependencies/dependencies.lock.json') }}
|
||||
key: quality-sources-${{ runner.os }}-${{ hashFiles('dependencies/dependencies.lock.json') }}
|
||||
|
||||
- name: Restore locked toolchains
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
@@ -88,6 +90,9 @@ jobs:
|
||||
- name: Set up locked IHP130 PDK
|
||||
run: make CONFIG=configs/ci/ihp130.mk setup-pdk
|
||||
|
||||
- name: Set up locked Tiny ICS55 PDK and PLL
|
||||
run: make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 setup-pdk
|
||||
|
||||
- name: Validate SoC integration topology
|
||||
run: make check-soc-topology check-user-extensions check-clock-reset-domains
|
||||
|
||||
|
||||
@@ -11,6 +11,30 @@ earlier documentation/dependency refreeze and IHP130 R2 measurements.
|
||||
Implementation and validation are complete for review; human phase acceptance
|
||||
is still pending by this entry.
|
||||
|
||||
## TINY-ICS55-P1 review fixes (2026-10-08)
|
||||
|
||||
The read-only review found two implementation issues and both are corrected in
|
||||
the current working tree:
|
||||
|
||||
- The quality workflow now restores the locked ICS55 PDK, SRAM views and PLL
|
||||
checkout before the mandatory Pytest step. A fresh checkout with no ICS55
|
||||
cache restored all inputs through `setup-pdk`; lock revisions and archive
|
||||
hashes were unchanged.
|
||||
- The SAFE24 audit now traces SYS backwards through the selected ICS55
|
||||
`P65_1233_PBMUX` input receiver and `BUFX0P7H7R` clock buffer to
|
||||
`extclk_i_pad`, verifies constant CMOS input enables and rejects constant,
|
||||
unknown, wrong-source, inverter, mux and multiple-driver paths. It records
|
||||
the source trace in the structural report.
|
||||
|
||||
Focused review-fix tests pass (`128 passed`), including negative clock-source
|
||||
mutations and the quality-workflow cache-miss ordering assertion. Ruff,
|
||||
yamllint, actionlint, dependency-lock validation, regression dry-runs and
|
||||
`git diff --check` pass. A fresh source capture, Yosys synthesis and OpenSTA
|
||||
run at
|
||||
`build/ics55-tiny-2026-10-08-10-05-42d5beccb726/` produce a current
|
||||
`meta/tiny-ics55-p1/report.json`; this remains SAFE24 synthesis/STA observation,
|
||||
not physical qualification. Human review and phase acceptance remain required.
|
||||
|
||||
The [platform runbook](tiny-ics55-platform.md) defines ownership and commands.
|
||||
The default `configs/ci/ics55-tiny.mk` selects TINY/ICS55, HAVE_PLL=YES,
|
||||
128 KiB main SRAM and external 24 MHz SYS. The PLL is physically represented
|
||||
|
||||
@@ -20,6 +20,53 @@ PHASE = "TINY-ICS55-P1"
|
||||
RAM = "ics55_ecos_sram_1024x32_m8"
|
||||
|
||||
|
||||
def inspect_clock_source(leaves: list[dict], system: str, reference: str,
|
||||
constants: dict) -> list[dict]:
|
||||
"""Prove the supported SAFE24 pad/buffer path, rejecting unknown logic.
|
||||
|
||||
This deliberately recognizes only the selected ICS55 input receiver and
|
||||
non-inverting clock buffer. A future clock mux/gate needs its own review.
|
||||
Inout drivers are included so a second driver on REF24 cannot be hidden.
|
||||
"""
|
||||
drivers = {}
|
||||
for cell in leaves:
|
||||
for port, bits in cell["ports"].items():
|
||||
if cell["directions"].get(port) in ("output", "inout"):
|
||||
for bit in bits:
|
||||
drivers.setdefault(bit, []).append((cell, port))
|
||||
|
||||
def scalar(cell, port):
|
||||
bits = cell["ports"].get(port, [])
|
||||
if len(bits) != 1:
|
||||
raise ValueError(f"missing scalar clock-path pin: {cell['path']}/{port}")
|
||||
return bits[0]
|
||||
|
||||
trace, seen = [], set()
|
||||
cursor = system
|
||||
while True:
|
||||
if cursor in constants or cursor in ("x", "z") or cursor in seen:
|
||||
raise ValueError("constant, unknown or cyclic SYS clock source")
|
||||
seen.add(cursor)
|
||||
candidates = drivers.get(cursor, [])
|
||||
if len(candidates) != 1:
|
||||
raise ValueError("SYS clock source has missing or multiple drivers")
|
||||
cell, port = candidates[0]
|
||||
trace.append({"path": cell["path"], "type": cell["type"], "output": port})
|
||||
if cell["type"] == "BUFX0P7H7R" and port == "Y":
|
||||
cursor = scalar(cell, "A")
|
||||
elif cell["type"] == "P65_1233_PBMUX" and port == "C":
|
||||
if scalar(cell, "PAD") != reference:
|
||||
raise ValueError("SYS input receiver is not connected to external REF24")
|
||||
if any(constants.get(scalar(cell, pin)) != value
|
||||
for pin, value in (("IE", 1), ("OE", 0), ("CS", 1))):
|
||||
raise ValueError("REF24 pad is not a constant-enabled CMOS input")
|
||||
if drivers.get(reference) != [(cell, "PAD")]:
|
||||
raise ValueError("external REF24 has an additional internal driver")
|
||||
return trace
|
||||
else:
|
||||
raise ValueError(f"unsupported SYS clock source: {cell['type']}/{port}")
|
||||
|
||||
|
||||
def verify_locked_views(root: Path) -> dict:
|
||||
"""Check derived cache bytes against the unchanged locked archives."""
|
||||
lock = b.read_json(root / "dependencies/dependencies.lock.json")
|
||||
@@ -114,7 +161,11 @@ def inspect_netlist(document: dict) -> dict:
|
||||
if kind in modules and not int(str(attrs.get("blackbox", "0")), 2):
|
||||
visit(kind, path, ports)
|
||||
else:
|
||||
leaves.append({"path": path, "type": kind, "ports": ports})
|
||||
directions = cell.get("port_directions", {
|
||||
p: decl["direction"] for p, decl in modules.get(kind, {}).get("ports", {}).items()
|
||||
})
|
||||
leaves.append({"path": path, "type": kind, "ports": ports,
|
||||
"directions": directions})
|
||||
|
||||
visit(top, top, {})
|
||||
for c in leaves:
|
||||
@@ -139,6 +190,15 @@ def inspect_netlist(document: dict) -> dict:
|
||||
if c["type"] in ("TIELOH7R", "TIEHIH7R"):
|
||||
for bit in c["ports"]["Z"]:
|
||||
constants[bit] = int(c["type"] == "TIEHIH7R")
|
||||
ref_port = modules[top]["ports"].get("extclk_i_pad", {})
|
||||
ref_bits = ref_port.get("bits", [])
|
||||
if (ref_port.get("direction") not in ("input", "inout") or len(ref_bits) != 1
|
||||
or not isinstance(ref_bits[0], int)):
|
||||
raise ValueError("missing live external REF24 input port")
|
||||
reference = canonical(f"{top}:{ref_bits[0]}")
|
||||
if reference in constants or reference in ("x", "z"):
|
||||
raise ValueError("external REF24 input is tied off or unknown")
|
||||
clock_path = inspect_clock_source(leaves, system, reference, constants)
|
||||
enable = pll[0]["ports"].get("EN", [])
|
||||
if len(enable) != 1 or constants.get(enable[0]) != 0:
|
||||
raise ValueError("PLL enable is not proven constant zero")
|
||||
@@ -149,7 +209,9 @@ def inspect_netlist(document: dict) -> dict:
|
||||
pll_outputs = {bit for name in ("CKOUT1", "CKOUT2", "CKTST") for bit in pll[0]["ports"].get(name, [])}
|
||||
if system in pll_outputs:
|
||||
raise ValueError("PLL drives SYS")
|
||||
return {"system_net": system, "cpu_clocked_cells": len(cpu),
|
||||
return {"system_net": system,
|
||||
"system_clock_source": {"port": "extclk_i_pad", "net": reference, "path": clock_path},
|
||||
"cpu_clocked_cells": len(cpu),
|
||||
"sram": [{"path": c["path"], "clock": c["ports"]["CLK"]} for c in ram],
|
||||
"pll": pll[0], "pll_enable": 0, "qualification": "SAFE24 digital connectivity only"}
|
||||
|
||||
|
||||
@@ -1800,6 +1800,22 @@ def test_quality_runs_accelerator_tests_with_locked_references_and_simulators()
|
||||
)
|
||||
|
||||
|
||||
def test_quality_prepares_required_ics55_inputs_even_on_cache_miss() -> None:
|
||||
import yaml
|
||||
|
||||
quality = yaml.safe_load((ROOT / ".github/workflows/quality.yml").read_text())
|
||||
steps = quality["jobs"]["scripts"]["steps"]
|
||||
command = "make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 setup-pdk"
|
||||
setup = next(step for step in steps if step.get("run") == command)
|
||||
pytest_step = next(step for step in steps if step.get("run") == "python3 -m pytest -q")
|
||||
assert steps.index(setup) < steps.index(pytest_step)
|
||||
assert "if" not in setup and not setup.get("continue-on-error", False)
|
||||
cache = next(step for step in steps if step.get("name") == "Restore locked source dependencies")
|
||||
assert "physical/pdk/icsprout55-pdk" in cache["with"]["path"].splitlines()
|
||||
assert ".cache/retrosoc/sources/ics55_ecos_pll" in cache["with"]["path"].splitlines()
|
||||
assert cache["with"]["key"].startswith("quality-sources-")
|
||||
|
||||
|
||||
def test_regression_observations_do_not_block_or_skip_metrics(
|
||||
monkeypatch, tmp_path: Path, capsys
|
||||
) -> None:
|
||||
|
||||
@@ -103,19 +103,90 @@ def test_consumed_netlist_provenance(tmp_path, monkeypatch, changed):
|
||||
@pytest.fixture
|
||||
def mapped_netlist():
|
||||
def cell(kind, **ports):
|
||||
return {"type": kind, "connections": {k: [v] for k, v in ports.items()}}
|
||||
cells = {"u_clock_buffer": cell("BUF", clk_o=2),
|
||||
return {"type": kind, "connections": {k: [v] for k, v in ports.items()},
|
||||
"port_directions": {k: "inout" if k == "PAD" else "output"
|
||||
if k in ("Y", "Z", "C", "CKOUT1", "CKOUT2", "CKTST")
|
||||
else "input" for k in ports}}
|
||||
cells = {"u_clock_buffer": cell("clock_buffer", clk_i=7, clk_o=2),
|
||||
"receiver": cell("P65_1233_PBMUX", PAD=1, C=7, IE="1", OE="0", CS="1"),
|
||||
"u_soc.u_cpu.u_hazard3_cpu_1port.core.pc_reg": cell("DFF", CK=2),
|
||||
"pll": cell("PLL_TOP", EN=3, CKOUT1=4, CKOUT2=5, CKTST=6),
|
||||
"low": cell("TIELOH7R", Z=3)}
|
||||
cells.update({f"bank{i}": cell(RAM, CLK=2) for i in range(32)})
|
||||
return {"modules": {"retrosoc_tiny_asic": {"ports": {}, "cells": cells}}}
|
||||
return {"modules": {
|
||||
"retrosoc_tiny_asic": {"ports": {"extclk_i_pad": {"direction": "inout", "bits": [1]},
|
||||
"jtag_tck_i_pad": {"direction": "inout", "bits": [11]}},
|
||||
"cells": cells},
|
||||
"clock_buffer": {"ports": {"clk_i": {"direction": "input", "bits": [100]},
|
||||
"clk_o": {"direction": "output", "bits": [101]}},
|
||||
"cells": {"u_buf": cell("BUFX0P7H7R", A=100, Y=101)}}}}
|
||||
|
||||
|
||||
def test_safe24_structural_binding(mapped_netlist):
|
||||
result = inspect_netlist(mapped_netlist)
|
||||
assert len(result["sram"]) == 32 and result["cpu_clocked_cells"] == 1
|
||||
assert result["pll_enable"] == 0
|
||||
assert result["system_clock_source"]["port"] == "extclk_i_pad"
|
||||
assert [c["type"] for c in result["system_clock_source"]["path"]] == [
|
||||
"BUFX0P7H7R", "P65_1233_PBMUX"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["0", "1", "x", "z", 99, 11, 4, 2])
|
||||
def test_clock_buffer_cannot_hide_invalid_source(mapped_netlist, source):
|
||||
# Constants, undriven nets, JTAG, PLL output and feedback all leave the
|
||||
# output-side CPU/SRAM net unchanged. The old audit accepted these cases.
|
||||
mapped_netlist["modules"]["retrosoc_tiny_asic"]["cells"]["u_clock_buffer"]["connections"]["clk_i"] = [source]
|
||||
with pytest.raises(ValueError):
|
||||
inspect_netlist(mapped_netlist)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["missing_ref", "wrong_pad", "disabled_input",
|
||||
"unknown_enable", "driven_pad", "unknown_mode",
|
||||
"duplicate_sys_driver", "driven_ref", "inverter", "mux"])
|
||||
def test_ref24_path_must_be_unambiguous_and_enabled(mapped_netlist, mutation):
|
||||
modules = mapped_netlist["modules"]
|
||||
top = modules["retrosoc_tiny_asic"]
|
||||
cells = top["cells"]
|
||||
pad = cells["receiver"]["connections"]
|
||||
if mutation == "missing_ref":
|
||||
del top["ports"]["extclk_i_pad"]
|
||||
elif mutation == "wrong_pad":
|
||||
pad["PAD"] = [11]
|
||||
elif mutation == "disabled_input":
|
||||
pad["IE"] = ["0"]
|
||||
elif mutation == "unknown_enable":
|
||||
pad["IE"] = ["x"]
|
||||
elif mutation == "driven_pad":
|
||||
pad["OE"] = ["1"]
|
||||
elif mutation == "unknown_mode":
|
||||
pad["CS"] = ["x"]
|
||||
elif mutation == "duplicate_sys_driver":
|
||||
cells["second_driver"] = {"type": "BUFX0P7H7R", "connections": {"A": [7], "Y": [2]},
|
||||
"port_directions": {"A": "input", "Y": "output"}}
|
||||
elif mutation == "driven_ref":
|
||||
cells["pll"]["connections"]["CKOUT1"] = [1]
|
||||
else:
|
||||
modules["clock_buffer"]["cells"]["u_buf"]["type"] = (
|
||||
"INVX0P5H7R" if mutation == "inverter" else "MUX2X1H7R")
|
||||
with pytest.raises(ValueError):
|
||||
inspect_netlist(mapped_netlist)
|
||||
|
||||
|
||||
def test_ref24_path_accepts_hierarchical_receiver_and_tie_cells(mapped_netlist):
|
||||
modules = mapped_netlist["modules"]
|
||||
cells = modules["retrosoc_tiny_asic"]["cells"]
|
||||
receiver = cells.pop("receiver")
|
||||
receiver["connections"].update(PAD=[100], C=[101], IE=[102], CS=[102], OE=[103])
|
||||
cells["receiver"] = {"type": "input_receiver", "connections": {
|
||||
"pad": [1], "data": [7], "high": [8], "low": [3]}}
|
||||
cells["high"] = {"type": "TIEHIH7R", "connections": {"Z": [8]},
|
||||
"port_directions": {"Z": "output"}}
|
||||
modules["input_receiver"] = {"ports": {
|
||||
"pad": {"direction": "inout", "bits": [100]},
|
||||
"data": {"direction": "output", "bits": [101]},
|
||||
"high": {"direction": "input", "bits": [102]},
|
||||
"low": {"direction": "input", "bits": [103]}}, "cells": {"u_pad": receiver}}
|
||||
assert inspect_netlist(mapped_netlist)["system_clock_source"]["port"] == "extclk_i_pad"
|
||||
|
||||
|
||||
def test_hierarchical_constant_output_is_resolved(mapped_netlist):
|
||||
|
||||
Reference in new issue
Block a user