feat: add ECC flow for padless ICS55 core hardening

- Introduced a new ECC directory containing scripts and Makefile for the padless ICS55 core hardening flow.
- Updated the main Makefile to include the new ECC Makefile and added relevant help commands.
- Added dependencies for the ECC CLI in the dependencies.lock.json file.
- Created README documentation for the ECC flow detailing usage and requirements.
- Implemented various scripts for configuration generation, validation, output checking, and packaging of ECC results.
- Added unit tests to validate the ECC configuration and flow behavior.
This commit is contained in:
maksyuki committed 2026-08-25 20:02:58 +08:00
1 parent 71d52f357e
commit 87df13fb46
11 files changed
+977

No files matched your search

+5
View File
@@ -277,6 +277,7 @@ ifeq ($(STA), OPENSTA)
endif endif
include physical/librelane/Makefile include physical/librelane/Makefile
include physical/ecc/Makefile
.PHONY: help config doctor setup setup-regression setup-mpw setup-clusterip setup-ip setup-pdk setup-app \ .PHONY: help config doctor setup setup-regression setup-mpw setup-clusterip setup-ip setup-pdk setup-app \
clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \ clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \
@@ -304,6 +305,10 @@ help:
' librelane-openroad open the current Chip run in OpenROAD' \ ' librelane-openroad open the current Chip run in OpenROAD' \
' librelane-klayout open the current Chip run in KLayout' \ ' librelane-klayout open the current Chip run in KLayout' \
' librelane-package package full-chip views and evidence' \ ' librelane-package package full-chip views and evidence' \
' ecc-setup install the pinned ECC CLI and ICS55 inputs' \
' ecc-doctor validate the padless ICS55 ECC hardening flow' \
' ecc-core run the padless ICS55 ECC hardening flow' \
' ecc-package package ECC core views and evidence' \
' setup install pinned external dependencies' \ ' setup install pinned external dependencies' \
' setup-regression install pinned dependencies for all PR PDK profiles' \ ' setup-regression install pinned dependencies for all PR PDK profiles' \
' doctor check tools, paths, and selected configuration' \ ' doctor check tools, paths, and selected configuration' \
+6
View File
@@ -70,6 +70,12 @@
"cluster_wdg": {"url": "https://github.com/retroSoC/wdg.git", "revision": "f5d39fa246054d678d34195824d943367dfcce44", "destination": "rtl/managed/clusterip/wdg", "license": "MulanPSL-2.0"} "cluster_wdg": {"url": "https://github.com/retroSoC/wdg.git", "revision": "f5d39fa246054d678d34195824d943367dfcce44", "destination": "rtl/managed/clusterip/wdg", "license": "MulanPSL-2.0"}
}, },
"archives": { "archives": {
"ecc_cli_linux_x86_64": {
"url": "https://github.com/openecos-projects/ecc/releases/download/v0.1.0-alpha.10/ecc-cli-linux-x86_64.tar.gz",
"sha256": "fc3daaca24dddb04ba3490329042f52da05190da03c3831042293dd0cbffdca6",
"destination": ".cache/retrosoc/downloads/ecc/ecc-cli-linux-x86_64-v0.1.0-alpha.10.tar.gz",
"license": "Apache-2.0"
},
"fatfs": { "fatfs": {
"url": "https://github.com/retroSoC/artifact/releases/download/fatfs-r0.16/fatfs-r0.16.zip", "url": "https://github.com/retroSoC/artifact/releases/download/fatfs-r0.16/fatfs-r0.16.zip",
"sha256": "99f7dc1f7e095356e4a9e3dbe29959090d8b948afe2bbc5441e52fdf4b85449e", "sha256": "99f7dc1f7e095356e4a9e3dbe29959090d8b948afe2bbc5441e52fdf4b85449e",
+2
View File
@@ -8,6 +8,8 @@ drivers.
- `smoke/syn/` contains Yosys synthesis and source-export integration. - `smoke/syn/` contains Yosys synthesis and source-export integration.
- `smoke/sta/` contains OpenSTA constraints and timing integration. - `smoke/sta/` contains OpenSTA constraints and timing integration.
- `librelane/` contains the open-source IHP130 core-hardening and full-chip pad-ring flows. - `librelane/` contains the open-source IHP130 core-hardening and full-chip pad-ring flows.
- `ecc/` contains the open-source ICS55 padless-core hardening flow backed by
the locked ECOS Chip Compiler release.
- `commercial/` contains licensed-tool orchestration and flow logic. PDKs, - `commercial/` contains licensed-tool orchestration and flow logic. PDKs,
foundry decks, commercial libraries, site configuration, and results remain foundry decks, commercial libraries, site configuration, and results remain
outside Git. outside Git.
+119
View File
@@ -0,0 +1,119 @@
ECC_FLOW_ROOT := $(ROOT_PATH)/physical/ecc
ECC_TARGET ?= core
ECC_RUN_ROOT := $(VARIANT_ROOT)/physical/ecc/$(ECC_TARGET)
ECC_INPUT_DIR := $(ECC_RUN_ROOT)/input
ECC_PROJECT_DIR := $(ECC_RUN_ROOT)/project
ECC_RTL := $(ECC_INPUT_DIR)/retrosoc_asic_sources.sv
ECC_SDC := $(ECC_INPUT_DIR)/retrosoc_core.sdc
ECC_CONFIG := $(ECC_PROJECT_DIR)/ecc.toml
ECC_LOG := $(ECC_RUN_ROOT)/ecc.log
ECC_RESULT := $(ECC_RUN_ROOT)/result.json
ECC_VERIFY := $(ECC_RUN_ROOT)/verification.json
ECC_STATUS := $(ECC_RUN_ROOT)/status.json
ECC_META_RESULT := $(META_DIR)/ecc-$(ECC_TARGET).json
ECC_DOCTOR_RESULT := $(META_DIR)/ecc-$(ECC_TARGET)-doctor.json
ECC_ARCHIVE := $(ECC_RUN_ROOT)/retrosoc-ics55-$(ECC_TARGET).tar.gz
ECC_CACHE_ROOT := $(CACHE_ROOT)/tools/ecc/v0.1.0-alpha.10
ECC_BIN := $(ECC_CACHE_ROOT)/ecc
ECC_DOWNLOAD := $(ROOT_PATH)/.cache/retrosoc/downloads/ecc/ecc-cli-linux-x86_64-v0.1.0-alpha.10.tar.gz
ECC_PDK_ROOT := $(ROOT_PATH)/physical/pdk/icsprout55-pdk
ECC_H7CR_LIBERTY := $(CACHE_ROOT)/pdk/ics55/ics55_h7cr_ss.lib
ECC_CLOCK_DOMAINS := $(ROOT_PATH)/rtl/mini/integration/clock_reset_domains.json
ECC_PIN_MAP := $(ROOT_PATH)/rtl/mini/pin_map/pin_map.json
ECC_SOURCE_PYTHON ?= /usr/bin/python3
ECC_ENV := XDG_CACHE_HOME=$(CACHE_ROOT)/fontconfig
ECC_TRACKED_RTL := $(shell git -C $(ROOT_PATH) ls-files \
'rtl/**/*.sv' 'rtl/**/*.svh' 'rtl/**/*.v' 'rtl/**/*.vh')
ifneq ($(ECC_TARGET),core)
$(error ECC_TARGET must be core, got '$(ECC_TARGET)')
endif
ifneq ($(filter ecc-setup ecc-input ecc-doctor ecc-check ecc-core ecc-status ecc-log ecc-package,$(MAKECMDGOALS)),)
FLOW_PYTHON := $(ECC_SOURCE_PYTHON)
endif
.PHONY: ecc-check-profile ecc-setup ecc-input ecc-doctor ecc-check ecc-core \
ecc-status ecc-log ecc-package
ecc-check-profile:
@test '$(PDK)' = ICS55 || { \
echo 'physical/ecc requires PDK=ICS55' >&2; exit 2; }
@test '$(HAVE_PLL)' = NO || { \
echo 'physical/ecc has no qualified PLL hardening configuration' >&2; exit 2; }
@test '$(HAVE_SRAM_IF)' = NO || { \
echo 'physical/ecc currently hardens the committed padless ICS55 profile only' >&2; exit 2; }
@test '$(HAVE_SRAM_MACRO)' = NO || { \
echo 'physical/ecc currently has no qualified ICS55 SRAM macro configuration' >&2; exit 2; }
$(ECC_BIN): $(LOCK_FILE) $(ECC_FLOW_ROOT)/scripts/setup.py | ecc-check-profile
$(FLOW_PYTHON) $(ECC_FLOW_ROOT)/scripts/setup.py --root $(ROOT_PATH) \
--lock $(LOCK_FILE) --output-dir $(ECC_CACHE_ROOT)
@test -x $@
ecc-setup: setup-pdk $(ECC_BIN)
$(ECC_H7CR_LIBERTY):
+$(MAKE) setup-pdk
@test -f $@
$(ECC_RTL): $(MPW_VARIANT_STAMP) $(FILELIST_STAMP) $(ECC_TRACKED_RTL) \
$(ROOT_PATH)/physical/smoke/syn/tools/export_soc_sources.py | ecc-check-profile
@mkdir -p $(@D)
$(FLOW_PYTHON) $(ROOT_PATH)/physical/smoke/syn/tools/export_soc_sources.py sv \
--output-dir $(@D) --pdk ICS55 --simu $(SIMU) \
--jtag-idcode $(JTAG_IDCODE) --ext-clk-hz $(EXT_CLK_HZ) \
--aud-clk-hz $(AUD_CLK_HZ) --clint-timebase-hz $(CLINT_TIMEBASE_HZ) \
--dynamic-core-filelist $(MPW_OUTPUT_DIR)/core/core.fl \
--dynamic-ip-filelist $(MPW_OUTPUT_DIR)/ip/ip.fl \
--memory-map-filelist $(MEMORY_MAP_FILELIST) \
--soc-topology-filelist $(SOC_TOPOLOGY_FILELIST) \
--user-extensions-filelist $(USER_EXTENSIONS_FILELIST) \
--pin-map-filelist $(PIN_MAP_FILELIST) \
--archinfo-incdir $(ARCHINFO_METADATA_DIR) --librelane-safe
$(ECC_SDC): $(ROOT_PATH)/physical/smoke/sta/opensta/generate_sdc.py \
$(ECC_CLOCK_DOMAINS) $(ECC_PIN_MAP) | ecc-check-profile
@mkdir -p $(@D)
$(FLOW_PYTHON) $(ROOT_PATH)/physical/smoke/sta/opensta/generate_sdc.py \
--domains $(ECC_CLOCK_DOMAINS) --pin-map $(ECC_PIN_MAP) --output $@
$(ECC_CONFIG): $(ECC_RTL) $(ECC_SDC) $(ECC_H7CR_LIBERTY) \
$(ECC_FLOW_ROOT)/scripts/generate_config.py | ecc-check-profile
@mkdir -p $(@D)
$(FLOW_PYTHON) $(ECC_FLOW_ROOT)/scripts/generate_config.py \
--rtl $(ECC_RTL) --sdc $(ECC_SDC) --pdk-root $(ECC_PDK_ROOT) \
--h7cr-liberty $(ECC_H7CR_LIBERTY) --ext-clk-hz $(EXT_CLK_HZ) --output $@
ecc-input: ecc-setup $(ECC_CONFIG)
$(ECC_DOCTOR_RESULT): $(ECC_CONFIG) $(ECC_BIN) $(ECC_FLOW_ROOT)/scripts/doctor.py
@mkdir -p $(@D)
$(FLOW_PYTHON) $(ECC_FLOW_ROOT)/scripts/doctor.py --root $(ROOT_PATH) \
--lock $(LOCK_FILE) --ecc $(ECC_BIN) --project $(ECC_PROJECT_DIR) \
--pdk-root $(ECC_PDK_ROOT) --sdc $(ECC_SDC) --output $@
ecc-doctor: ecc-setup $(ECC_DOCTOR_RESULT)
ecc-check: ecc-doctor
ecc-core: ecc-doctor manifest
$(FLOW_PYTHON) $(ROOT_PATH)/scripts/run_flow.py --tool ecc --log $(ECC_LOG) \
--result $(ECC_RESULT) --cwd $(ECC_PROJECT_DIR) -- \
env $(ECC_ENV) $(ECC_BIN) run --project $(ECC_PROJECT_DIR) --overwrite --json
$(FLOW_PYTHON) $(ECC_FLOW_ROOT)/scripts/check_outputs.py --ecc $(ECC_BIN) \
--project $(ECC_PROJECT_DIR) --sdc $(ECC_SDC) --output $(ECC_VERIFY)
@mkdir -p $(META_DIR)
@cp $(ECC_RESULT) $(ECC_META_RESULT)
ecc-status: ecc-doctor
@mkdir -p $(ECC_RUN_ROOT)
env $(ECC_ENV) $(ECC_BIN) status --project $(ECC_PROJECT_DIR) --json > $(ECC_STATUS)
@cat $(ECC_STATUS)
ecc-log: ecc-doctor
env $(ECC_ENV) $(ECC_BIN) log --project $(ECC_PROJECT_DIR)
ecc-package: ecc-core
$(FLOW_PYTHON) $(ECC_FLOW_ROOT)/scripts/package.py --root $(ROOT_PATH) \
--variant-root $(VARIANT_ROOT) --run-root $(ECC_RUN_ROOT) --output $(ECC_ARCHIVE)
+31
View File
@@ -0,0 +1,31 @@
# ECC ICS55 Core Hardening
This directory owns the on-demand ECOS Chip Compiler (ECC) hardening adapter
for the padless `retrosoc_core` macro on the committed
`configs/ci/ics55.mk` profile. It consumes the locked ECC
`v0.1.0-alpha.10` Linux CLI release, the locked ICS55 PDK checkout, the
cached H7CR slow Liberty view, and the canonical generated SoC sources and
clock-domain constraints.
The flow deliberately does not use `retrosoc_asic`, IO LEFs/Liberties, pad
cells, a pad ring, bondpads, package constraints, or board timing. Logical
ports remain macro pins. The generated SDC constrains the external, generated
system, audio, JTAG, DVP, and ULPI domains; ECC must accept that SDC through
its PDK overrides. A single-clock fallback is not permitted.
Run the flow through the root Makefile:
```sh
make CONFIG=configs/ci/ics55.mk ecc-setup
make CONFIG=configs/ci/ics55.mk ecc-doctor
make CONFIG=configs/ci/ics55.mk ecc-core
make CONFIG=configs/ci/ics55.mk ecc-package
```
Inputs, logs, the ECC workspace, reports, and packaged views live below
`build/<variant>/physical/ecc/core/`; the CLI archive and extracted tool live
below `.cache/retrosoc/`. They are generated evidence, not tracked source.
ECC hardening is development evidence only. The ICS55 preview PDK and this
padless flow do not establish foundry DRC/LVS, IR/EM, ESD, package, board, or
production-signoff closure.
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Check that an ECC harden run produced complete padless-core evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
REQUIRED_STEPS = {
"SYNTHESIS",
"FLOORPLAN",
"NETLIST_OPT",
"PLACEMENT",
"CTS",
"LEGALIZATION",
"ROUTING",
"DRC",
"FILLER",
"RCX",
"STA",
"HARDEN",
}
STEP_ALIASES = {
"FIXFANOUT": "NETLIST_OPT",
"PLACE": "PLACEMENT",
"ROUTE": "ROUTING",
}
ARTIFACT_GROUPS = {
"gds": ("*.gds", "*.gds.gz"),
"def": ("*.def", "*.def.gz"),
"netlist": ("*.v", "*.v.gz"),
"spef": ("*.spef", "*.spef.gz"),
"sta_report": ("*sta*.rpt", "*STA*.rpt", "*timing*.rpt"),
"drc_report": ("*drc*.rpt", "*DRC*.rpt", "*drc*.json", "*DRC*.json"),
}
def digest(path: Path) -> str:
hasher = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
hasher.update(block)
return hasher.hexdigest()
def collect_step_names(value: Any) -> set[str]:
names: set[str] = set()
if isinstance(value, dict):
for key, item in value.items():
if key.lower() in {"step", "name", "type"} and isinstance(item, str):
names.add(item.upper())
names.update(collect_step_names(item))
elif isinstance(value, list):
for item in value:
names.update(collect_step_names(item))
return names
def canonical_steps(steps: set[str]) -> set[str]:
return {STEP_ALIASES.get(step, step) for step in steps}
def find_artifacts(workspace: Path, patterns: tuple[str, ...]) -> list[Path]:
found: set[Path] = set()
for pattern in patterns:
found.update(path for path in workspace.rglob(pattern) if path.is_file())
return sorted(found)
def command_environment(project: Path) -> dict[str, str]:
cache = project / ".fontconfig-cache"
cache.mkdir(parents=True, exist_ok=True)
environment = os.environ.copy()
environment["XDG_CACHE_HOME"] = str(cache)
return environment
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ecc", type=Path, required=True)
parser.add_argument("--project", type=Path, required=True)
parser.add_argument("--sdc", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
project = args.project.resolve()
workspace = project / "runs/default"
errors: list[str] = []
details: dict[str, object] = {"workspace": str(workspace)}
flow_path = workspace / "home/flow.json"
if not flow_path.is_file():
errors.append(f"ECC flow state is missing: {flow_path}")
else:
try:
flow = json.loads(flow_path.read_text(encoding="utf-8"))
steps = canonical_steps(collect_step_names(flow))
details["steps"] = sorted(steps)
missing_steps = sorted(REQUIRED_STEPS - steps)
if missing_steps:
errors.append("ECC harden flow is missing step(s): " + ", ".join(missing_steps))
except (OSError, json.JSONDecodeError) as error:
errors.append(f"cannot read ECC flow state: {error}")
copied_sdc = workspace / "origin" / args.sdc.name
if not copied_sdc.is_file():
errors.append(f"ECC workspace did not retain the generated SDC: {copied_sdc}")
elif digest(copied_sdc) != digest(args.sdc):
errors.append("ECC workspace SDC does not match the generated multi-clock SDC")
for label, patterns in ARTIFACT_GROUPS.items():
artifacts = find_artifacts(workspace, patterns)
details[f"{label}_artifacts"] = [str(path.relative_to(workspace)) for path in artifacts]
if not artifacts:
errors.append(f"ECC harden output is missing {label} evidence")
try:
status = subprocess.run(
[str(args.ecc), "status", "--project", str(project), "--json"],
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=command_environment(project),
)
details["status"] = json.loads(status.stdout)
except (OSError, subprocess.CalledProcessError, json.JSONDecodeError) as error:
errors.append(f"cannot obtain ECC status: {error}")
result = {
"schema_version": 1,
"status": "passed" if not errors else "failed",
"errors": errors,
"details": details,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"ECC output verification passed: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""Validate the reproducible padless ICS55 ECC hardening environment."""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from scripts.dependency_lock import load_lock # noqa: E402
from scripts.setup_helpers import atomic_write # noqa: E402
EXPECTED_ECC_VERSION = "ecc 0.1.0a10"
EXPECTED_CLOCKS = (
"clk_external",
"clk_system",
"clk_audio",
"clk_jtag",
"clk_dvp",
"clk_usb2_ulpi",
)
def command_environment(project: Path) -> dict[str, str]:
cache = project / ".fontconfig-cache"
cache.mkdir(parents=True, exist_ok=True)
environment = os.environ.copy()
environment["XDG_CACHE_HOME"] = str(cache)
return environment
def command_output(command: list[str], project: Path) -> str:
return subprocess.check_output(
command,
text=True,
stderr=subprocess.STDOUT,
env=command_environment(project),
).strip()
def ecc_version(binary: Path, project: Path) -> str:
output = command_output([str(binary), "--version"], project)
lines = [line.strip() for line in output.splitlines() if line.strip()]
for line in reversed(lines):
if line.startswith("ecc "):
return line
return output
def read_config(path: Path) -> dict[str, Any]:
with path.open("rb") as source:
data = tomllib.load(source)
if not isinstance(data, dict):
raise ValueError("ECC configuration must be a TOML object")
return data
def validate_config(config: dict[str, Any], sdc: Path) -> tuple[list[str], dict[str, object]]:
errors: list[str] = []
details: dict[str, object] = {}
design = config.get("design")
pdk = config.get("pdk")
flow = config.get("flow")
if not isinstance(design, dict):
errors.append("ecc.toml [design] table is missing")
design = {}
if not isinstance(pdk, dict):
errors.append("ecc.toml [pdk] table is missing")
pdk = {}
if not isinstance(flow, dict):
errors.append("ecc.toml [flow] table is missing")
flow = {}
details["top"] = design.get("top")
details["pdk"] = pdk.get("name")
details["preset"] = flow.get("preset")
if design.get("name") != "retrosoc_core" or design.get("top") != "retrosoc_core":
errors.append("ECC design must target the padless retrosoc_core macro")
if design.get("clock_port") != "extclk_i_pad":
errors.append("ECC primary clock must be the logical extclk_i_pad port")
if pdk.get("name") != "ics55":
errors.append("ECC PDK must be ics55")
if flow.get("preset") != "harden":
errors.append("ECC flow preset must be harden")
overrides = pdk.get("overrides")
if not isinstance(overrides, dict):
errors.append("ECC PDK overrides are required for padless H7CR hardening")
overrides = {}
for field in ("tech", "lefs", "libs", "sdc", "site_core", "tap_cell", "end_cap", "fillers"):
if field not in overrides:
errors.append(f"ECC PDK override is missing: {field}")
lefs = overrides.get("lefs", [])
libs = overrides.get("libs", [])
if not isinstance(lefs, list) or len(lefs) != 1 or "H7CR" not in str(lefs[0]):
errors.append("ECC must use exactly one H7CR standard-cell LEF")
if not isinstance(libs, list) or len(libs) != 1 or "h7cr" not in str(libs[0]).lower():
errors.append("ECC must use exactly one H7CR standard-cell Liberty")
no_pad_markers = ("IO", "PAD", "bondpad")
lef_values = lefs if isinstance(lefs, list) else []
lib_values = libs if isinstance(libs, list) else []
flattened = " ".join(str(value) for value in [*lef_values, *lib_values])
if any(marker.lower() in flattened.lower() for marker in no_pad_markers):
errors.append("ECC padless flow may not reference IO, PAD, or bondpad collateral")
if not sdc.is_file():
errors.append(f"ECC SDC is missing: {sdc}")
else:
text = sdc.read_text(encoding="utf-8")
missing = [clock for clock in EXPECTED_CLOCKS if f"-name {clock}" not in text]
if missing:
errors.append("ECC SDC is missing clock(s): " + ", ".join(missing))
if "set_clock_groups -name retrosoc_async -asynchronous" not in text:
errors.append("ECC SDC must preserve asynchronous clock groups")
return errors, details
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--lock", type=Path, required=True)
parser.add_argument("--ecc", type=Path, required=True)
parser.add_argument("--project", type=Path, required=True)
parser.add_argument("--pdk-root", type=Path, required=True)
parser.add_argument("--sdc", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
errors: list[str] = []
details: dict[str, object] = {}
project = args.project.resolve()
config_path = project / "ecc.toml"
lock = load_lock(args.lock)
details["ecc_archive_sha256"] = lock["archives"]["ecc_cli_linux_x86_64"]["sha256"]
details["locked_pdk_revision"] = lock["sources"]["pdk_ics55"]["revision"]
if not args.ecc.is_file() or not args.ecc.stat().st_mode & 0o111:
errors.append(f"ECC executable is missing or not executable: {args.ecc}")
else:
try:
version = ecc_version(args.ecc, project)
details["ecc_version"] = version
if version != EXPECTED_ECC_VERSION:
errors.append(f"expected {EXPECTED_ECC_VERSION}, found {version or '<empty>'}")
except (OSError, subprocess.CalledProcessError) as error:
errors.append(f"cannot execute ECC: {error}")
if not (args.pdk_root / ".git").is_dir():
errors.append(f"ICS55 PDK checkout is missing: {args.pdk_root}")
else:
try:
revision = command_output(
["git", "-C", str(args.pdk_root), "rev-parse", "HEAD"], project
)
details["pdk_revision"] = revision
if revision != details["locked_pdk_revision"]:
errors.append(
f"expected ICS55 PDK {details['locked_pdk_revision']}, found {revision}"
)
except (OSError, subprocess.CalledProcessError) as error:
errors.append(f"cannot inspect ICS55 PDK revision: {error}")
required = (
args.pdk_root / "prtech/techLEF/N551P6M_ecos.lef",
args.pdk_root
/ "IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/lef/"
"ics55_LLSC_H7CR_ecos.lef",
)
for path in required:
if not path.is_file():
errors.append(f"required padless ICS55 PDK input is missing: {path}")
if not config_path.is_file():
errors.append(f"ECC project configuration is missing: {config_path}")
else:
try:
config_errors, config_details = validate_config(read_config(config_path), args.sdc.resolve())
errors.extend(config_errors)
details.update(config_details)
except (OSError, tomllib.TOMLDecodeError, ValueError) as error:
errors.append(f"cannot validate ECC project configuration: {error}")
if not errors and args.ecc.is_file():
try:
check = subprocess.run(
[str(args.ecc), "check", "--project", str(project), "--json"],
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=command_environment(project),
)
details["ecc_check"] = json.loads(check.stdout)
except subprocess.CalledProcessError as error:
errors.append(f"ECC project check failed: {error.stdout.strip()}")
except (OSError, json.JSONDecodeError) as error:
errors.append(f"ECC project check failed: {error}")
result = {
"schema_version": 1,
"status": "passed" if not errors else "failed",
"errors": errors,
"details": details,
}
atomic_write(args.output, json.dumps(result, indent=2, sort_keys=True) + "\n")
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"ECC doctor passed: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Generate a padless ICS55 ECC project configuration from canonical inputs."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
EXPECTED_CLOCKS = (
"clk_external",
"clk_system",
"clk_audio",
"clk_jtag",
"clk_dvp",
"clk_usb2_ulpi",
)
def relative(path: Path, base: Path) -> str:
return os.path.relpath(path.resolve(), base.resolve())
def render(args: argparse.Namespace) -> str:
project_dir = args.output.resolve().parent
pdk_root = args.pdk_root.resolve()
sdc = args.sdc.resolve()
liberty = args.h7cr_liberty.resolve()
tech = pdk_root / "prtech/techLEF/N551P6M_ecos.lef"
lef = (
pdk_root
/ "IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/lef/"
"ics55_LLSC_H7CR_ecos.lef"
)
required = (args.rtl.resolve(), sdc, pdk_root, tech, lef, liberty)
missing = [str(path) for path in required if not path.exists()]
if missing:
raise FileNotFoundError("missing ECC configuration input(s): " + ", ".join(missing))
sdc_text = sdc.read_text(encoding="utf-8")
missing_clocks = [clock for clock in EXPECTED_CLOCKS if f"-name {clock}" not in sdc_text]
if missing_clocks:
raise ValueError("ECC SDC is missing clock(s): " + ", ".join(missing_clocks))
return "\n".join(
(
"# Generated by physical/ecc/scripts/generate_config.py; do not edit.",
"[design]",
'name = "retrosoc_core"',
'top = "retrosoc_core"',
f'rtl = ["{relative(args.rtl, project_dir)}"]',
'clock_port = "extclk_i_pad"',
f"frequency_mhz = {args.ext_clk_hz / 1_000_000:.9g}",
"",
"[pdk]",
'name = "ics55"',
f'root = "{relative(pdk_root, project_dir)}"',
"",
"# ECC v0.1.0-alpha.10 resolves PDK content paths relative to pdk.root.",
"# Supply only the padless H7CR core library and the generated multi-clock SDC.",
"[pdk.overrides]",
f'tech = "{relative(tech, pdk_root)}"',
f'lefs = ["{relative(lef, pdk_root)}"]',
f'libs = ["{relative(liberty, pdk_root)}"]',
f'sdc = "{relative(sdc, project_dir)}"',
'site_core = "core7"',
'tap_cell = "FILLTAPH7R"',
'end_cap = "FILLTAPH7R"',
'fillers = ["FILLER64H7R", "FILLER32H7R", "FILLER16H7R", "FILLER8H7R", "FILLER4H7R", "FILLER2H7R", "FILLER1H7R"]',
"",
"[flow]",
'preset = "harden"',
'run = "default"',
"",
"[params.floorplan]",
"core_util = 0.60",
"aspect_ratio = 1.0",
"",
"[params.place]",
"target_density = 0.55",
"",
)
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--rtl", type=Path, required=True)
parser.add_argument("--sdc", type=Path, required=True)
parser.add_argument("--pdk-root", type=Path, required=True)
parser.add_argument("--h7cr-liberty", type=Path, required=True)
parser.add_argument("--ext-clk-hz", type=int, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.ext_clk_hz <= 0:
parser.error("--ext-clk-hz must be positive")
try:
content = render(args)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(content, encoding="utf-8")
except (OSError, ValueError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Create a checksummed development delivery from an ECC core harden run."""
from __future__ import annotations
import argparse
import hashlib
import shutil
import tarfile
import tempfile
from pathlib import Path
def hash_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--variant-root", type=Path, required=True)
parser.add_argument("--run-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
root = args.root.resolve()
variant_root = args.variant_root.resolve()
run_root = args.run_root.resolve()
project = run_root / "project"
workspace = project / "runs/default"
required = (
project / "ecc.toml",
workspace,
run_root / "result.json",
run_root / "verification.json",
variant_root / "meta/manifest.json",
variant_root / "meta/ecc-core-doctor.json",
variant_root / "meta/ecc-core.json",
root / "dependencies/dependencies.lock.json",
)
missing = [str(path) for path in required if not path.exists()]
if missing:
parser.error("missing ECC delivery input(s): " + ", ".join(missing))
args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="retrosoc-ecc-package-") as temporary:
staging = Path(temporary) / "retrosoc-ics55-core"
shutil.copytree(run_root / "input", staging / "input")
shutil.copy2(project / "ecc.toml", staging / "evidence/ecc.toml")
shutil.copytree(workspace, staging / "evidence/workspace")
for source, destination in (
(run_root / "result.json", "ecc-result.json"),
(run_root / "verification.json", "verification.json"),
(variant_root / "meta/manifest.json", "manifest.json"),
(variant_root / "meta/ecc-core-doctor.json", "ecc-doctor.json"),
(variant_root / "meta/ecc-core.json", "ecc-core.json"),
(root / "dependencies/dependencies.lock.json", "dependencies.lock.json"),
):
target = staging / "evidence" / destination
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
files = sorted(path for path in staging.rglob("*") if path.is_file())
checksums = "\n".join(
f"{hash_file(path)} {path.relative_to(staging)}" for path in files
)
(staging / "SHA256SUMS").write_text(checksums + "\n", encoding="utf-8")
with tarfile.open(args.output, "w:gz") as archive:
archive.add(staging, arcname=staging.name)
print(f"created ECC delivery: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Install the checksum-locked ECOS Chip Compiler CLI into the local cache."""
from __future__ import annotations
import argparse
import os
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from scripts.dependency_lock import archive # noqa: E402
from scripts.install_toolchain import safe_extract # noqa: E402
from scripts.setup_helpers import atomic_write, download_file # noqa: E402
ARCHIVE_NAME = "ecc_cli_linux_x86_64"
EXPECTED_VERSION = "0.1.0a10"
def ecc_version(binary: Path) -> str:
result = subprocess.run(
[str(binary), "--version"],
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
for line in reversed(lines):
if line.startswith("ecc "):
return line
return result.stdout.strip()
def install(root: Path, lock_path: Path, output_dir: Path, update: bool) -> Path:
spec = archive(ARCHIVE_NAME, lock_path)
archive_path = root / spec["destination"]
download_file(spec["url"], archive_path, spec["sha256"], update=update, timeout=120)
output_dir = output_dir.resolve()
marker = output_dir / ".complete"
binary = output_dir / "ecc"
marker_text = f"{EXPECTED_VERSION}\n{spec['sha256']}\n"
if binary.is_file() and marker.is_file() and marker.read_text(encoding="utf-8") == marker_text:
return binary
output_dir.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f".{output_dir.name}.", dir=output_dir.parent) as temp:
extracted = Path(temp) / "extracted"
extracted.mkdir()
safe_extract(archive_path, extracted)
candidates = sorted(
path for path in extracted.rglob("ecc") if path.is_file() and os.access(path, os.X_OK)
)
if len(candidates) != 1:
raise RuntimeError(
"expected exactly one executable named ecc in the ECC release archive, "
f"found {len(candidates)}"
)
bundle_root = candidates[0].parent
candidate = Path(temp) / "content"
shutil.copytree(bundle_root, candidate)
installed_binary = candidate / "ecc"
installed_binary.chmod(installed_binary.stat().st_mode | stat.S_IXUSR)
version = ecc_version(installed_binary)
if version != f"ecc {EXPECTED_VERSION}":
raise RuntimeError(
f"expected ECC version ecc {EXPECTED_VERSION}, found {version or '<empty>'}"
)
atomic_write(candidate / ".complete", marker_text)
if output_dir.exists():
shutil.rmtree(output_dir)
os.replace(candidate, output_dir)
return binary
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=ROOT)
parser.add_argument("--lock", type=Path, required=True)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
binary = install(args.root.resolve(), args.lock.resolve(), args.output_dir, args.update)
print(f"ECC is ready: {binary}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+155
View File
@@ -0,0 +1,155 @@
"""Tests for the padless ICS55 ECC hardening adapter."""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
from types import SimpleNamespace
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
ECC_ROOT = ROOT / "physical/ecc"
def load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def write_pdk(root: Path) -> None:
tech = root / "prtech/techLEF/N551P6M_ecos.lef"
lef = (
root
/ "IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/lef/"
"ics55_LLSC_H7CR_ecos.lef"
)
tech.parent.mkdir(parents=True)
lef.parent.mkdir(parents=True)
tech.write_text("VERSION 5.8 ;\n", encoding="utf-8")
lef.write_text("VERSION 5.8 ;\n", encoding="utf-8")
def write_sdc(path: Path) -> None:
path.write_text(
"\n".join(
[
f"create_clock -name {name} -period 10 [get_ports clk]"
for name in (
"clk_external",
"clk_system",
"clk_audio",
"clk_jtag",
"clk_dvp",
"clk_usb2_ulpi",
)
]
+ ["set_clock_groups -name retrosoc_async -asynchronous"]
)
+ "\n",
encoding="utf-8",
)
def test_ecc_config_is_padless_and_uses_the_full_clock_inventory(tmp_path: Path) -> None:
module = load_module("retrosoc_ecc_config", ECC_ROOT / "scripts/generate_config.py")
pdk = tmp_path / "pdk"
write_pdk(pdk)
rtl = tmp_path / "input/retrosoc_asic_sources.sv"
rtl.parent.mkdir()
rtl.write_text("module retrosoc_core; endmodule\n", encoding="utf-8")
sdc = tmp_path / "input/retrosoc_core.sdc"
write_sdc(sdc)
liberty = tmp_path / "cache/ics55_h7cr_ss.lib"
liberty.parent.mkdir()
liberty.write_text("library (h7cr) {}\n", encoding="utf-8")
output = tmp_path / "project/ecc.toml"
args = SimpleNamespace(
rtl=rtl,
sdc=sdc,
pdk_root=pdk,
h7cr_liberty=liberty,
ext_clk_hz=72_000_000,
output=output,
)
config = module.render(args)
output.parent.mkdir()
output.write_text(config, encoding="utf-8")
document = tomllib.loads(config)
assert document["design"]["name"] == "retrosoc_core"
assert document["design"]["top"] == "retrosoc_core"
assert document["design"]["clock_port"] == "extclk_i_pad"
assert document["design"]["frequency_mhz"] == 72.0
assert document["pdk"]["name"] == "ics55"
assert document["flow"]["preset"] == "harden"
assert document["params"]["floorplan"]["core_util"] == 0.60
assert document["params"]["place"]["target_density"] == 0.55
overrides = document["pdk"]["overrides"]
assert len(overrides["lefs"]) == 1
assert "H7CR" in overrides["lefs"][0]
assert len(overrides["libs"]) == 1
assert "h7cr" in overrides["libs"][0].lower()
assert "IO" not in " ".join(overrides["lefs"] + overrides["libs"])
assert "PAD" not in config
assert "bondpad" not in config
def test_ecc_doctor_rejects_single_clock_or_pad_collateral(tmp_path: Path) -> None:
module = load_module("retrosoc_ecc_doctor", ECC_ROOT / "scripts/doctor.py")
sdc = tmp_path / "retrosoc_core.sdc"
write_sdc(sdc)
config = {
"design": {
"name": "retrosoc_core",
"top": "retrosoc_core",
"clock_port": "extclk_i_pad",
},
"pdk": {
"name": "ics55",
"overrides": {
"tech": "prtech/techLEF/N551P6M_ecos.lef",
"lefs": ["IP/STD_cell/H7CR/lef/ics55_LLSC_H7CR_ecos.lef"],
"libs": ["cache/ics55_h7cr_ss.lib"],
"sdc": "retrosoc_core.sdc",
"site_core": "core7",
"tap_cell": "FILLTAPH7R",
"end_cap": "FILLTAPH7R",
"fillers": ["FILLER1H7R"],
},
},
"flow": {"preset": "harden"},
}
errors, details = module.validate_config(config, sdc)
assert not errors
assert details["top"] == "retrosoc_core"
config["pdk"]["overrides"]["lefs"] = ["IP/IO/ics55_pad.lef"]
errors, _ = module.validate_config(config, sdc)
assert any("exactly one H7CR" in error for error in errors)
assert any("IO, PAD, or bondpad" in error for error in errors)
def test_ecc_flow_is_on_demand_and_locked() -> None:
makefile = (ECC_ROOT / "Makefile").read_text(encoding="utf-8")
root_makefile = (ROOT / "Makefile").read_text(encoding="utf-8")
lock = json.loads((ROOT / "dependencies/dependencies.lock.json").read_text(encoding="utf-8"))
assert "ecc-core:" in makefile
assert "ecc-package:" in makefile
assert "ecc-all" not in makefile
assert "--librelane-safe" in makefile
assert "include physical/ecc/Makefile" in root_makefile
assert "ecc-core" not in (ROOT / "scripts/regress.py").read_text(encoding="utf-8")
archive = lock["archives"]["ecc_cli_linux_x86_64"]
assert archive["url"].endswith("v0.1.0-alpha.10/ecc-cli-linux-x86_64.tar.gz")
assert archive["sha256"] == "fc3daaca24dddb04ba3490329042f52da05190da03c3831042293dd0cbffdca6"