From 7d064ce78225ce535e50b5a49af16b70ff05c240 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Fri, 10 Jul 2026 13:13:59 +0800 Subject: [PATCH] feat: implement setup helpers and refactor setup script - Added to manage Git repository dependencies and file downloads. - Refactored to utilize the new helper functions for cloning the MPW generator. - Enhanced OpenSTA Makefile to validate input files and configurations before execution. - Updated OpenSTA TCL script to use environment variables for file paths. - Refactored source export script to improve file handling and path resolution. - Added tests for atomic writing, filelist parsing, and dependency management. - Improved Yosys synthesis scripts to include configuration recording. --- .github/workflows/test.yml | 7 +- .gitignore | 4 +- Makefile | 165 ++++++--- README.md | 27 +- app/setup.py | 112 +++--- crt/src/main.c | 1 + crt/ver.py | 68 ++-- pdk/setup.py | 48 ++- rtl/clusterip/setup.py | 50 ++- rtl/ip/setup.py | 31 +- rtl/mini/Makefile | 21 +- rtl/mini/core/setup.py | 36 +- rtl/mini/mk/iverilog.mk | 134 ++++--- rtl/mini/mk/software.mk | 31 +- rtl/mini/mk/vcs.mk | 24 +- rtl/mini/mk/verilator.mk | 26 +- rtl/mini/script/comb.py | 43 ++- rtl/mini/script/convt_sv2v.py | 62 ++-- rtl/mini/script/filelist.py | 162 +++++++++ rtl/mini/script/gen_iverilog_filelist.py | 106 ++++-- rtl/mini/script/generate_filelist.py | 210 +++++------ rtl/mini/script/prepare_norflash.py | 84 +++-- scripts/__init__.py | 1 + scripts/clean.py | 46 +++ scripts/doctor.py | 72 ++++ scripts/prepare_mpw.py | 94 +++++ scripts/setup_helpers.py | 148 ++++++++ setup.py | 29 +- sta/opensta/opensta.mk | 23 +- sta/opensta/opensta.tcl | 22 +- syn/tools/export_soc_sources.py | 437 +++++++++-------------- syn/yosys/script/common.tcl | 9 +- syn/yosys/script/synth.tcl | 10 + syn/yosys/yosys.mk | 37 +- tests/test_script_tools.py | 183 ++++++++++ 35 files changed, 1792 insertions(+), 771 deletions(-) create mode 100644 rtl/mini/script/filelist.py create mode 100644 scripts/__init__.py create mode 100644 scripts/clean.py create mode 100644 scripts/doctor.py create mode 100644 scripts/prepare_mpw.py create mode 100644 scripts/setup_helpers.py create mode 100644 tests/test_script_tools.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c352af8..1ec4a71d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 + - name: Test build scripts + run: | + python3 -m pip install pytest + python3 -m pytest -q tests + - name: Download and Extract Verilator Artifact run: | verilator_path="artifact-verilator" @@ -147,4 +152,4 @@ jobs: # retention-days: 90 # Retain for 90 days - name: Success Notification - run: echo "RTL simulation completed successfully" \ No newline at end of file + run: echo "RTL simulation completed successfully" diff --git a/.gitignore b/.gitignore index b2877a01..0d491051 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ *.rc *.aig *.so +*.py[cod] +__pycache__/ .vscode pic video.h @@ -67,4 +69,4 @@ coremark-main *wave picorv32_ver.v socver.* -3rd-party \ No newline at end of file +3rd-party diff --git a/Makefile b/Makefile index 017f2d03..92bd3f01 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help +.DELETE_ON_ERROR: + SOC ?= MINI SIMU ?= VCS SYNTH ?= NONE @@ -16,8 +20,8 @@ RTL_SIM_PLLCFG ?= NONE RTL_SIM_CORESEL ?= 0 RTL_SIM_TIMEOUT ?= -1 -ROOT_PATH ?= $(CURDIR) -RTL_PATH ?= NONE +ROOT_PATH ?= $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) +RTL_PATH := $(ROOT_PATH)/rtl/mini CORE ?= HAZARD3 IP ?= NONE @@ -30,40 +34,50 @@ FIRMWARE_NAME ?= retrosoc_fw PROG_TYPE ?= FULL LINK_TYPE ?= ld2_sram -$(info ============== BASE INFO ==================================) -$(info retroSoC: ) -$(info author: Yuchi Miao ) -$(info license: MulanPSL-2.0 license) -$(info ROOT_PATH: $(CURDIR)) -$(info SHELL: $(SHELL)) -$(info MAKE VERSION: $(MAKE_VERSION)) -$(info MAKE APP: $(MAKE)) -$(info MAKE CMDGOAL: $(MAKECMDGOALS)) -$(info MAKE FILELIST: $(MAKEFILE_LIST)) -$(info ============== HW CONFIG INFO ===================================) -$(info SOC [TINY, MINI]: $(SOC)) -$(info CORE [PICORV32, HAZARD3, MDD]: $(CORE)) -$(info IP [NONE, MDD]: $(IP)) -$(info SIMU [VCS, VERILATOR, IVERILOG]: $(SIMU)) -$(info SYNTH [NONE, YOSYS, DC]: $(SYNTH)) -$(info STA [NONE, OPENSTA, ISTA]: $(STA)) -$(info PDK [ICS55, IHP130, SKY130, GF180]: $(PDK)) -$(info HAVE_PLL [YES, NO]: $(HAVE_PLL)) -$(info HAVE_SRAM_IF [YES, NO]: $(HAVE_SRAM_IF)) -$(info HAVE_SRAM_MACRO [YES, NO]: $(HAVE_SRAM_MACRO)) -$(info HAVE_SVA [YES, NO]: $(HAVE_SVA)) -$(info WAVE: [YES, NO]: $(WAVE)) -$(info RTL_SIM_PLLEN: $(RTL_SIM_PLLEN)) -$(info RTL_SIM_PLLCFG: $(RTL_SIM_PLLCFG)) -$(info RTL_SIM_CORESEL: $(RTL_SIM_CORESEL)) -$(info RTL_SIM_TIMEOUT: $(RTL_SIM_TIMEOUT)) -$(info ============== SW CONFIG INFO ===================================) -$(info ISA [RV32E RV32I RV32IM]: $(ISA)) -$(info HAVE_CSR [YES, NO]: $(HAVE_CSR)) -$(info FIRMWARE_NAME [retrosoc_fw]: $(FIRMWARE_NAME)) -$(info PROG_TYPE [BASE, FULL]: $(PROG_TYPE)) -$(info LINK_TYPE [xip, ld2_sram, ld2_psram, ld2_sdram]: $(LINK_TYPE)) -$(info =================================================================) +VALID_SOC := MINI +VALID_CORE := PICORV32 HAZARD3 MDD +VALID_IP := NONE MDD +VALID_SIMU := VCS VERILATOR IVERILOG +VALID_SYNTH := NONE YOSYS +VALID_STA := NONE OPENSTA +VALID_PDK := ICS55 IHP130 SKY130 GF180 +VALID_BOOL := YES NO +VALID_ISA := RV32E RV32I RV32IM +VALID_PROG_TYPE := BASE FULL +VALID_LINK_TYPE := xip ld2_sram ld2_psram ld2_sdram + +define validate_value +$(if $(filter $($(1)),$(2)),,$(error Invalid $(1)='$($(1))'; expected one of: $(2))) +endef + +$(call validate_value,SOC,$(VALID_SOC)) +$(call validate_value,CORE,$(VALID_CORE)) +$(call validate_value,IP,$(VALID_IP)) +$(call validate_value,SIMU,$(VALID_SIMU)) +$(call validate_value,SYNTH,$(VALID_SYNTH)) +$(call validate_value,STA,$(VALID_STA)) +$(call validate_value,PDK,$(VALID_PDK)) +$(call validate_value,HAVE_PLL,$(VALID_BOOL)) +$(call validate_value,HAVE_SRAM_IF,$(VALID_BOOL)) +$(call validate_value,HAVE_SRAM_MACRO,$(VALID_BOOL)) +$(call validate_value,HAVE_SVA,$(VALID_BOOL)) +$(call validate_value,WAVE,$(VALID_BOOL)) +$(call validate_value,ISA,$(VALID_ISA)) +$(call validate_value,HAVE_CSR,$(VALID_BOOL)) +$(call validate_value,PROG_TYPE,$(VALID_PROG_TYPE)) +$(call validate_value,LINK_TYPE,$(VALID_LINK_TYPE)) + +ifeq ($(SYNTH),YOSYS) +ifeq ($(filter $(PDK),IHP130 ICS55),) +$(error Yosys synthesis supports PDK=IHP130 or PDK=ICS55, not PDK=$(PDK)) +endif +endif + +ifeq ($(STA),OPENSTA) +ifneq ($(PDK),IHP130) +$(error OpenSTA currently supports PDK=IHP130 only) +endif +endif DEF_LIST ?= +define+PDK_$(PDK) DEF_LIST += +define+CORE_$(CORE) @@ -86,27 +100,74 @@ ifeq ($(HAVE_SVA), NO) DEF_LIST += +define+SV_ASSRT_DISABLE endif -ifeq ($(SOC), MINI) - RTL_PATH = $(ROOT_PATH)/rtl/mini - $(info DEF_LIST: $(DEF_LIST)) - $(shell mkdir -p $(RTL_PATH)/.generated_fl) - $(file > $(RTL_PATH)/.generated_fl/def.fl, $(DEF_LIST)) - include rtl/mini/Makefile +ifeq ($(SYNTH), YOSYS) + DEF_LIST += +define+SYNTHESIS endif +include rtl/mini/Makefile + ifeq ($(SYNTH), YOSYS) - #HACK: for some core - DEF_LIST += +define+SYNTHESIS - $(info SYNTH DEF_LIST: $(DEF_LIST)) - $(shell mkdir -p $(RTL_PATH)/.generated_fl) - $(file > $(RTL_PATH)/.generated_fl/def.fl, $(DEF_LIST)) - include syn/yosys/yosys.mk - -else ifeq ($(SYNTH), DC) - include syn/dc.mk - +include syn/yosys/yosys.mk endif ifeq ($(STA), OPENSTA) include sta/opensta/opensta.mk endif + +.PHONY: help config doctor setup setup-mpw setup-core setup-clusterip setup-ip setup-pdk setup-app clean-all +.NOTPARALLEL: setup + +help: + @printf '%s\n' \ + 'retroSoC build targets:' \ + ' firmware | asm build firmware' \ + ' comp | sim behavioral simulation' \ + ' netcomp | netsim synthesized-netlist simulation' \ + ' postcomp | postsim post-layout simulation' \ + ' synth | sta synthesis and timing analysis' \ + ' setup install pinned external dependencies' \ + ' doctor check tools, paths, and selected configuration' \ + ' config print selected configuration' \ + ' clean | clean-all clean current backend or all generated output' \ + '' \ + 'Key variables: SOC CORE IP SIMU SYNTH STA PDK ISA PROG_TYPE LINK_TYPE' + +config: + @printf '%-18s %s\n' \ + ROOT_PATH '$(ROOT_PATH)' SOC '$(SOC)' CORE '$(CORE)' IP '$(IP)' \ + SIMU '$(SIMU)' SYNTH '$(SYNTH)' STA '$(STA)' PDK '$(PDK)' \ + HAVE_PLL '$(HAVE_PLL)' HAVE_SRAM_IF '$(HAVE_SRAM_IF)' \ + HAVE_SRAM_MACRO '$(HAVE_SRAM_MACRO)' HAVE_SVA '$(HAVE_SVA)' \ + ISA '$(ISA)' HAVE_CSR '$(HAVE_CSR)' PROG_TYPE '$(PROG_TYPE)' \ + LINK_TYPE '$(LINK_TYPE)' + +doctor: + @python3 $(ROOT_PATH)/scripts/doctor.py \ + --root $(ROOT_PATH) --simu $(SIMU) --synth $(SYNTH) --sta $(STA) \ + --pdk $(PDK) --core $(CORE) --ip $(IP) + +setup: setup-mpw setup-core setup-clusterip setup-ip setup-pdk setup-app + +setup-mpw: + python3 $(ROOT_PATH)/setup.py + python3 $(ROOT_PATH)/scripts/prepare_mpw.py + @mkdir -p $(dir $(MPW_STAMP)) + @touch $(MPW_STAMP) + +setup-core: setup-mpw + python3 $(ROOT_PATH)/rtl/mini/core/setup.py + +setup-clusterip: + python3 $(ROOT_PATH)/rtl/clusterip/setup.py + +setup-ip: + python3 $(ROOT_PATH)/rtl/ip/setup.py + +setup-pdk: + python3 $(ROOT_PATH)/pdk/setup.py + +setup-app: + python3 $(ROOT_PATH)/app/setup.py + +clean-all: + python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) diff --git a/README.md b/README.md index d9128fbf..3bd93266 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,29 @@ [![style](https://github.com/retroSoC/retroSoC/actions/workflows/style.yml/badge.svg)](https://github.com/retroSoC/retroSoC/actions/workflows/style.yml) [![lint](https://github.com/retroSoC/retroSoC/actions/workflows/lint.yml/badge.svg)](https://github.com/retroSoC/retroSoC/actions/workflows/lint.yml) -[![test](https://github.com/retroSoC/retroSoC/actions/workflows/test.yml/badge.svg)](https://github.com/retroSoC/retroSoC/actions/workflows/test.yml) \ No newline at end of file +[![test](https://github.com/retroSoC/retroSoC/actions/workflows/test.yml/badge.svg)](https://github.com/retroSoC/retroSoC/actions/workflows/test.yml) + +## Build automation + +Run `make help` for the supported build targets and `make config` for the active +configuration. `make doctor` checks the selected simulator, synthesis/STA tools, +RISC-V compiler, PDK, and external source trees before a long build starts. + +External dependencies are pinned to known commits. Install them with `make setup`; +individual targets such as `setup-pdk`, `setup-ip`, and `setup-app` are also +available. Setup scripts are idempotent and accept `--update` when an existing +checkout must be moved to the pinned revision. + +Typical open-source flows are: + +```sh +make PROG_TYPE=BASE firmware +make SIMU=VERILATOR sim +make SIMU=IVERILOG sim +make SYNTH=YOSYS synth +make STA=OPENSTA sta +``` + +Generated outputs are isolated under ignored build directories. `make clean` +cleans the selected simulator backend and `make clean-all` removes all generated +flow output while leaving dependency checkouts intact. diff --git a/app/setup.py b/app/setup.py index 7c90fd2b..7e316773 100644 --- a/app/setup.py +++ b/app/setup.py @@ -1,47 +1,79 @@ -#!/bin/python +#!/usr/bin/env python3 -import os - -# Always use the script directory as the base to avoid failures in different working directories -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -APP_DIR = SCRIPT_DIR -FATFS_DIR = os.path.join(APP_DIR, 'fatfs') -COREMARK_DIR = os.path.join(APP_DIR, 'coremark') -LVGL_DIR = os.path.join(APP_DIR, 'lvgl') -LVGL_NAME = 'v9.4.0.tar.gz' - -# print(APP_DIR) -# print(FATFS_DIR) -# print(COREMARK_DIR) +import argparse +import shutil +import sys +from pathlib import Path +from zipfile import ZipFile -def prepend_line(filename, line): - with open(filename, 'r') as f: - content = f.read() - with open(filename, 'w') as f: - f.write(line + '\n' + content) - -def replace_line(filename, old, new): - lines = '' - with open(filename, 'r', encoding='utf-8') as fp: - for v in fp: - if old in v: - lines += v.replace(old, new) - else: - lines += v - - with open(filename, 'w', encoding='utf-8') as fp: - fp.writelines(lines) +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import atomic_write, download_file, ensure_git_repo # noqa: E402 -os.system(f'mkdir -p "{FATFS_DIR}"/ff16 && cd "{FATFS_DIR}"/ff16 && if [ -d source ]; then echo "[fatfs] already exists, skip"; else wget https://elm-chan.org/fsw/ff/arc/ff16.zip && unzip -q ff16.zip && echo "[fatfs] downloaded and extracted"; fi') -os.system(f'cd "{FATFS_DIR}" && cp -rf ffconf.h diskio.c ff16/source/') - -os.system(f'git clone https://github.com/eembc/coremark.git {COREMARK_DIR}/coremark-main') -os.system(f'cd {COREMARK_DIR}/coremark-main && git checkout 1f483d5b8316753a742cbf5590caf5bd0a4e4777') -os.chdir(f'{COREMARK_DIR}/coremark-main') -prepend_line('coremark.h', '#include ') -replace_line('core_main.c', 'main', 'core_main') +APP_DIR = Path(__file__).resolve().parent +FATFS_DIR = APP_DIR / "fatfs" +FATFS_ARCHIVE = FATFS_DIR / "ff16/ff16.zip" +FATFS_URL = "https://elm-chan.org/fsw/ff/arc/ff16.zip" +FATFS_SHA256 = "41d98115f72b090c2d0c269a001c5c0216efd78fbd84bb6427be808a76315a5a" +COREMARK_DIR = APP_DIR / "coremark/coremark-main" +COREMARK_REVISION = "1f483d5b8316753a742cbf5590caf5bd0a4e4777" -# os.system(f'cd "{LVGL_DIR}" && if ls -1d lvgl-* >/dev/null 2>&1; then echo "[lvgl] already exists, skip"; else wget https://github.com/lvgl/lvgl/archive/refs/tags/{LVGL_NAME} && tar -xf {LVGL_NAME} && echo "[lvgl] downloaded and extracted"; fi') \ No newline at end of file +def patch_coremark() -> None: + header = COREMARK_DIR / "coremark.h" + content = header.read_text(encoding="utf-8") + include = "#include " + if include not in content: + marker = '#include "core_portme.h"' + if marker not in content: + raise RuntimeError(f"CoreMark patch marker missing in {header}") + content = content.replace(marker, f"{marker}\n{include}", 1) + atomic_write(header, content) + + source = COREMARK_DIR / "core_main.c" + content = source.read_text(encoding="utf-8") + if "core_main(" not in content: + if "main(" not in content: + raise RuntimeError(f"CoreMark entry point not found in {source}") + content = content.replace("/* Function: main", "/* Function: core_main", 1) + content = content.replace("\nmain(", "\ncore_main(") + atomic_write(source, content) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Install pinned application dependencies") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + + download_file( + FATFS_URL, + FATFS_ARCHIVE, + FATFS_SHA256, + update=args.update, + ) + fatfs_source = FATFS_ARCHIVE.parent / "source" + if not fatfs_source.is_dir(): + with ZipFile(FATFS_ARCHIVE) as archive: + archive.extractall(FATFS_ARCHIVE.parent) + for name in ("ffconf.h", "diskio.c"): + source = FATFS_DIR / name + if not source.is_file(): + raise FileNotFoundError(f"FatFs configuration file missing: {source}") + shutil.copy2(source, fatfs_source / name) + + ensure_git_repo( + "https://github.com/eembc/coremark.git", + COREMARK_DIR, + COREMARK_REVISION, + update=args.update, + allow_dirty=True, + ) + patch_coremark() + print("application dependencies are ready") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/crt/src/main.c b/crt/src/main.c index 20923721..2c14f1ef 100644 --- a/crt/src/main.c +++ b/crt/src/main.c @@ -11,6 +11,7 @@ void main() { uart0_init(CPU_FREQ, UART_BPS); #ifdef SW_BASE app_info(); + ip_archinfo_test(0, NULL); #else tinybooter(); i2c0_init((uint8_t)(CPU_FREQ / 2 - 1)); diff --git a/crt/ver.py b/crt/ver.py index afccade5..6619fa17 100644 --- a/crt/ver.py +++ b/crt/ver.py @@ -1,43 +1,45 @@ -#!/bin/python +#!/usr/bin/env python3 -import os +import argparse import subprocess +import sys +from pathlib import Path -ROOT_PATH = os.getcwd() -def get_git_info(): +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import atomic_write # noqa: E402 + + +def git_value(root: Path, *args: str, fallback: str = "unknown") -> str: try: - branch = subprocess.check_output( - ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - stderr=subprocess.STDOUT - ).decode('utf-8').strip() + return subprocess.check_output( + ["git", "-C", str(root), *args], + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return fallback - # get full commit hash - commit = subprocess.check_output( - ['git', 'rev-parse', 'HEAD'], - stderr=subprocess.STDOUT - ).decode('utf-8').strip()[0:6] - return branch, commit - except subprocess.CalledProcessError: - return None, None +def main() -> int: + parser = argparse.ArgumentParser(description="Generate the firmware version header") + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + root = args.root.resolve() + output = args.output or root / "crt/inc/socver.h" -branch, commit = get_git_info() -print(f"current branch: {branch}") -print(f"current commit: {commit}") -print(ROOT_PATH) + branch = git_value(root, "rev-parse", "--abbrev-ref", "HEAD") + commit = git_value(root, "rev-parse", "--short=6", "HEAD") + template = (root / "crt/ver.tmpl").read_text(encoding="utf-8") + content = template.replace("SOC_DEFAULT_BRANCH", branch).replace( + "SOC_DEFAULT_COMMIT", commit + ) + changed = atomic_write(output, content) + print(f"version {branch}@{commit}: {'updated' if changed else 'unchanged'}") + return 0 -res = '' -with open(f'{ROOT_PATH}/crt/ver.tmpl', 'r', encoding='utf-8') as fp: - file = fp.readlines() - for line in file: - if 'SOC_DEFAULT_BRANCH' in line: - res += line.replace('SOC_DEFAULT_BRANCH', branch) - elif 'SOC_DEFAULT_COMMIT' in line: - res += line.replace('SOC_DEFAULT_COMMIT', commit) - else: - res += line - -with open(f'{ROOT_PATH}/crt/inc/socver.h', 'w', encoding='utf-8') as fp: - fp.writelines(res) \ No newline at end of file +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pdk/setup.py b/pdk/setup.py index ac647ef1..8442806d 100644 --- a/pdk/setup.py +++ b/pdk/setup.py @@ -1,10 +1,44 @@ -#!/bin/python +#!/usr/bin/env python3 -import os - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +import argparse +import sys +from pathlib import Path -os.system(f'git clone https://github.com/IHP-GmbH/IHP-Open-PDK.git {SCRIPT_DIR}/IHP-Open-PDK') -os.system(f'cd {SCRIPT_DIR}/IHP-Open-PDK && git checkout 68eebafcd9b2f5e92c69d37a8d3d90eb266550f5 && git submodule init') -os.system(f'git clone --recursive --depth 1 https://github.com/openecos-projects/icsprout55-pdk.git {SCRIPT_DIR}/icsprout55-pdk') +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import ensure_git_repo # noqa: E402 + + +DEPENDENCIES = ( + ( + "https://github.com/IHP-GmbH/IHP-Open-PDK.git", + "IHP-Open-PDK", + "68eebafcd9b2f5e92c69d37a8d3d90eb266550f5", + ), + ( + "https://github.com/openecos-projects/icsprout55-pdk.git", + "icsprout55-pdk", + "e696e093129ca2212487aa169af74d06ebd86eb6", + ), +) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Install pinned open PDKs") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + pdk_dir = Path(__file__).resolve().parent + for url, name, revision in DEPENDENCIES: + ensure_git_repo( + url, + pdk_dir / name, + revision, + recursive=True, + update=args.update, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/clusterip/setup.py b/rtl/clusterip/setup.py index 3c7ebc6d..87aa042e 100644 --- a/rtl/clusterip/setup.py +++ b/rtl/clusterip/setup.py @@ -1,10 +1,48 @@ -#!/bin/python +#!/usr/bin/env python3 -import os +import argparse +import sys +from pathlib import Path -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ip = ['archinfo', 'clint', 'common', 'crc', 'i2c', 'i2s', 'plic', 'ps2', 'pwm', 'rng', 'rtc', 'spi', 'timer', 'uart', 'wdg'] +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import ensure_git_repo # noqa: E402 -for v in ip: - os.system(f'git clone https://github.com/retroSoC/{v}.git {SCRIPT_DIR}/{v}') \ No newline at end of file + +REVISIONS = { + "archinfo": "d65dd13c83a1176668bf5178004814a1551bf6ba", + "clint": "ce49f1a5501c0a9f18c0e3f358417648f3f2fb18", + "common": "d9921eb05160be873aebc46bb2f820bee2c7eebc", + "crc": "4654a63d06d487b5b9fe22af228c36c736b90bad", + "i2c": "73b024547c5cd70fb8bf801cc7757db2536c604f", + "i2s": "bd0978e8b45e4cf4c1335a1fa7311385d2a4e805", + "plic": "c851d93829d2e75b224003a058e718925855c0cc", + "ps2": "60921e0f72d6113f8615d03ce54567388fc2410f", + "pwm": "0777ef085c661d2b103f2fa0f8519f97eea3c950", + "rng": "7374e1e7f25bfd1ff9954ea99f19b77a583def29", + "rtc": "69963b8424cb00e04c8be70157b8ca738464e342", + "spi": "515cb408806584525605b4747db5e890dbdf1928", + "timer": "0c44b0d8b742c6a477fda15a812cf22962b54dd8", + "uart": "eca12213ba06f42e0a6dd0f27907fd4c447e2774", + "wdg": "b2603c1faeba96603392f0fe9e692e645d7e061f", +} + + +def main() -> int: + parser = argparse.ArgumentParser(description="Install pinned cluster IP repositories") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + destination = Path(__file__).resolve().parent + for name, revision in REVISIONS.items(): + ensure_git_repo( + f"https://github.com/retroSoC/{name}.git", + destination / name, + revision, + update=args.update, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/ip/setup.py b/rtl/ip/setup.py index 80bae0b3..6c2756c9 100644 --- a/rtl/ip/setup.py +++ b/rtl/ip/setup.py @@ -1,7 +1,30 @@ -#!/bin/python +#!/usr/bin/env python3 -import os +import argparse +import sys +from pathlib import Path -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -os.system(f'git clone https://github.com/retroSoC/3rd-party.git {SCRIPT_DIR}/3rd-party') +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import ensure_git_repo # noqa: E402 + + +REVISION = "d991ec9f7bf696e7e5d840758487068b11bd4121" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Install pinned third-party IP") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + ensure_git_repo( + "https://github.com/retroSoC/3rd-party.git", + Path(__file__).resolve().parent / "3rd-party", + REVISION, + update=args.update, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/Makefile b/rtl/mini/Makefile index fa548bc6..cb20bfa1 100644 --- a/rtl/mini/Makefile +++ b/rtl/mini/Makefile @@ -1,7 +1,16 @@ -$(info RTL_PATH: $(RTL_PATH)) +GENERATED_FL_DIR := $(RTL_PATH)/.generated_fl +MPW_STAMP := $(RTL_PATH)/mpw/.build/.prepared -gen_mpw_code: - python3 $(RTL_PATH)/mpw/setup.py +$(MPW_STAMP): + @test -d $(RTL_PATH)/mpw/.git || { \ + echo "missing $(RTL_PATH)/mpw; run 'make setup-mpw' first" >&2; \ + exit 1; \ + } + python3 $(ROOT_PATH)/scripts/prepare_mpw.py + @mkdir -p $(@D) + @touch $@ + +gen_mpw_code: $(MPW_STAMP) ifeq ($(IP), MDD) @echo "[mpw] generating user ip ..." python3 $(RTL_PATH)/mpw/ip.py @@ -18,7 +27,9 @@ else endif generate_filelist: - python3 $(RTL_PATH)/script/generate_filelist.py + python3 $(RTL_PATH)/script/generate_filelist.py \ + --output-dir $(GENERATED_FL_DIR) \ + $(foreach define,$(DEF_LIST),--define $(define)) # software include $(RTL_PATH)/mk/software.mk @@ -67,4 +78,4 @@ else ifeq ($(SIMU), IVERILOG) include $(RTL_PATH)/mk/iverilog.mk endif -.PHONY: clean firmware +.PHONY: gen_mpw_code generate_filelist firmware diff --git a/rtl/mini/core/setup.py b/rtl/mini/core/setup.py index 2bc58ff4..1e25ba82 100644 --- a/rtl/mini/core/setup.py +++ b/rtl/mini/core/setup.py @@ -1,19 +1,25 @@ -#!/bin/python +#!/usr/bin/env python3 -import os +import sys +from pathlib import Path -ROOT_PATH = os.getcwd() -lines = '' -# remove parallel case statement -with open(f'{ROOT_PATH}/rtl/mini/core/picorv32.v', 'r', encoding='utf-8') as fp: - for v in fp: - if '// synopsys parallel_case' in v: - lines += v.replace('// synopsys parallel_case', '') - elif '// synopsys full_case parallel_case' in v: - lines += v.replace('// synopsys full_case parallel_case', '') - else: - lines += v +ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import atomic_write # noqa: E402 -with open(f'{ROOT_PATH}/rtl/mini/core/picorv32_ver.v', 'w', encoding='utf-8') as fp: - fp.writelines(lines) \ No newline at end of file + +def main() -> int: + core_dir = Path(__file__).resolve().parent + source = core_dir / "picorv32.v" + destination = core_dir / "picorv32_ver.v" + content = source.read_text(encoding="utf-8") + content = content.replace("// synopsys full_case parallel_case", "") + content = content.replace("// synopsys parallel_case", "") + changed = atomic_write(destination, content) + print(f"{'updated' if changed else 'unchanged'}: {destination}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/mk/iverilog.mk b/rtl/mini/mk/iverilog.mk index b690831b..bee09aae 100644 --- a/rtl/mini/mk/iverilog.mk +++ b/rtl/mini/mk/iverilog.mk @@ -1,76 +1,94 @@ -# tools & paths -SIM_TOOL := iverilog -SIM_BINY := vvp simv -fst -GTKWAVE_TOOL := gtkwave -COMP_LOG := compile.log -SIM_LOG := sim.log -# netlist file path -NETLIST_PATH := $(ROOT_PATH)/syn/yosys/.synth_build/out/retrosoc_asic_yosys.v -POST_PATH := $(ROOT_PATH)/pd/sdf/retrosoc_asic.v -SDF_PATH := $(ROOT_PATH)/pd/sdf/retrosoc_asic_CTS_MIN.sdf.gz -# testbench filelist -TB_FLIST := -f $(RTL_PATH)/.generate_verilogd_fl/tb.fl +IVERILOG ?= iverilog +VVP ?= vvp +GTKWAVE ?= gtkwave +NETLIST_PATH ?= $(ROOT_PATH)/syn/yosys/.synth_build/out/retrosoc_asic_yosys.v +POST_PATH ?= $(ROOT_PATH)/pd/sdf/retrosoc_asic.v +SDF_PATH ?= $(ROOT_PATH)/pd/sdf/retrosoc_asic_CTS_MIN.sdf.gz +SDF_SCOPE ?= $(RTL_TOP).u_retrosoc_asic -# --- Compilation Flags --- -COMMON_OPTS := -g2012 +IVERILOG_ROOT := $(RTL_PATH)/.iverilog_build +IVERILOG_BEHV_DIR := $(IVERILOG_ROOT)/behv +IVERILOG_NETL_DIR := $(IVERILOG_ROOT)/netl +IVERILOG_POST_DIR := $(IVERILOG_ROOT)/post +IVERILOG_BEHV_FLIST := $(IVERILOG_BEHV_DIR)/iverilog.fl +IVERILOG_NETL_FLIST := $(IVERILOG_NETL_DIR)/iverilog.fl +IVERILOG_POST_FLIST := $(IVERILOG_POST_DIR)/iverilog.fl +CONVERTED_SOC := $(IVERILOG_BEHV_DIR)/converted_soc.v -POST_OPTS := -ghello +IVERILOG_COMMON_OPTS := -g2012 +IVERILOG_TIME_OPTS := -gno-specify +IVERILOG_POST_OPTS := -gspecify -Tmin +IVERILOG_SIM_OPTS := +$(RTL_SIM_PLLEN) +$(RTL_SIM_PLLCFG) \ + +core_sel=$(RTL_SIM_CORESEL) +sim_timeout=$(RTL_SIM_TIMEOUT) +wave_$(WAVE) -TIME_OPTS := -gno-specify -SIM_OPTS := +$(RTL_SIM_PLLEN) +$(RTL_SIM_PLLCFG) +core_sel=$(RTL_SIM_CORESEL) +sim_timeout=$(RTL_SIM_TIMEOUT) \ - +wave_$(WAVE) +$(CONVERTED_SOC): gen_mpw_code generate_filelist + @mkdir -p $(@D) + python3 $(RTL_PATH)/script/convt_sv2v.py $(RTL_FLIST) --output $@ +$(IVERILOG_BEHV_FLIST): generate_filelist $(CONVERTED_SOC) + @mkdir -p $(@D) + python3 $(RTL_PATH)/script/gen_iverilog_filelist.py \ + --mode behv --pdk $(PDK) --output $@ --converted $(CONVERTED_SOC) -comp: DIR := .iverilog_build/behv -comp: FLIST := -f $(RTL_PATH)/.generated_fl/iverilog.fl -comp: OPTS := $(TIME_OPTS) +$(IVERILOG_NETL_FLIST): generate_filelist + @mkdir -p $(@D) + python3 $(RTL_PATH)/script/gen_iverilog_filelist.py \ + --mode netl --pdk $(PDK) --output $@ --netlist $(NETLIST_PATH) -netcomp: DIR := .iverilog_build/netl -netcomp: FLIST := -f $(RTL_PATH)/.generated_fl/iverilog.fl -netcomp: OPTS := $(TIME_OPTS) +$(IVERILOG_POST_FLIST): generate_filelist + @mkdir -p $(@D) + python3 $(RTL_PATH)/script/gen_iverilog_filelist.py \ + --mode post --pdk $(PDK) --output $@ --netlist $(POST_PATH) --sdf $(SDF_PATH) \ + --sdf-scope $(SDF_SCOPE) -postcomp: DIR := .iverilog_build/post -postcomp: FLIST := $(NET_FLIST) $(TB_FLIST) -postcomp: OPTS := $(POST_OPTS) +convt_sv2v: $(CONVERTED_SOC) +gen_iverilog_filelist: $(IVERILOG_BEHV_FLIST) -sim: DIR := .iverilog_build/behv -netsim: DIR := .iverilog_build/netl -postsim: DIR := .iverilog_build/post +comp: $(IVERILOG_BEHV_FLIST) + @mkdir -p $(IVERILOG_BEHV_DIR) + @cd $(IVERILOG_BEHV_DIR) && set -o pipefail && \ + $(IVERILOG) $(IVERILOG_COMMON_OPTS) $(IVERILOG_TIME_OPTS) -f $< \ + -o simv -s $(RTL_TOP) 2>&1 | tee compile.log -wave: DIR := .iverilog_build/behv -netwave: DIR := .iverilog_build/netl -postwave: DIR := .iverilog_build/post +netcomp: $(IVERILOG_NETL_FLIST) + @mkdir -p $(IVERILOG_NETL_DIR) + @cd $(IVERILOG_NETL_DIR) && set -o pipefail && \ + $(IVERILOG) $(IVERILOG_COMMON_OPTS) $(IVERILOG_TIME_OPTS) -f $< \ + -o simv -s $(RTL_TOP) 2>&1 | tee compile.log +postcomp: $(IVERILOG_POST_FLIST) + @mkdir -p $(IVERILOG_POST_DIR) + @cd $(IVERILOG_POST_DIR) && set -o pipefail && \ + $(IVERILOG) $(IVERILOG_COMMON_OPTS) $(IVERILOG_POST_OPTS) -f $< \ + -o simv -s $(RTL_TOP) -s retrosoc_sdf_annotator 2>&1 | tee compile.log -sim: comp prepare_norflash -netsim: netcomp prepare_norflash -postsim: postcomp prepare_norflash +sim: comp + python3 $(RTL_PATH)/script/prepare_norflash.py --sim-dir $(IVERILOG_BEHV_DIR) \ + --firmware $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).hex + @cd $(IVERILOG_BEHV_DIR) && set -o pipefail && \ + stdbuf -oL -eL $(VVP) simv -fst $(IVERILOG_SIM_OPTS) 2>&1 | tee sim.log -convt_sv2v: generate_filelist - @mkdir -p $(RTL_PATH)/$(DIR) - python3 $(RTL_PATH)/script/convt_sv2v.py $(RTL_FLIST) +netsim: netcomp + python3 $(RTL_PATH)/script/prepare_norflash.py --sim-dir $(IVERILOG_NETL_DIR) \ + --firmware $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).hex + @cd $(IVERILOG_NETL_DIR) && set -o pipefail && \ + stdbuf -oL -eL $(VVP) simv -fst $(IVERILOG_SIM_OPTS) 2>&1 | tee sim.log -gen_iverilog_filelist: - @mkdir -p $(RTL_PATH)/$(DIR) - python3 $(RTL_PATH)/script/gen_iverilog_filelist.py $(MAKECMDGOALS) $(PDK) $(NETLIST_PATH) +postsim: postcomp + python3 $(RTL_PATH)/script/prepare_norflash.py --sim-dir $(IVERILOG_POST_DIR) \ + --firmware $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).hex + @cd $(IVERILOG_POST_DIR) && set -o pipefail && \ + stdbuf -oL -eL $(VVP) simv -fst $(IVERILOG_SIM_OPTS) 2>&1 | tee sim.log -prepare_norflash: - @mkdir -p $(RTL_PATH)/$(DIR) - python3 $(RTL_PATH)/script/prepare_norflash.py $(MAKECMDGOALS) - -comp netcomp postcomp: convt_sv2v gen_iverilog_filelist - @mkdir -p $(RTL_PATH)/$(DIR) - cd $(RTL_PATH)/$(DIR) && ($(SIM_TOOL) $(COMMON_OPTS) $(OPTS) $(FLIST) -o simv -s $(RTL_TOP)) 2>&1 | tee $(COMP_LOG) - - -sim netsim postsim: - cd $(RTL_PATH)/$(DIR) && (stdbuf -oL -eL $(SIM_BINY) $(SIM_OPTS)) 2>&1 | tee $(SIM_LOG) - -wave netwave postwave: - cd $(RTL_PATH)/$(DIR) && ($(GTKWAVE_TOOL) $(RTL_TOP).fst &) +wave: + cd $(IVERILOG_BEHV_DIR) && $(GTKWAVE) $(RTL_TOP).fst & +netwave: + cd $(IVERILOG_NETL_DIR) && $(GTKWAVE) $(RTL_TOP).fst & +postwave: + cd $(IVERILOG_POST_DIR) && $(GTKWAVE) $(RTL_TOP).fst & clean: - rm -rf .iverilog_build/behv $(RTL_PATH)/.iverilog_build/netl $(RTL_PATH)/.iverilog_build/post + rm -rf $(IVERILOG_ROOT) -.PHONY: comp netcomp postcomp sim netsim postsim wave clean \ No newline at end of file +.PHONY: convt_sv2v gen_iverilog_filelist comp netcomp postcomp sim netsim postsim wave netwave postwave clean diff --git a/rtl/mini/mk/software.mk b/rtl/mini/mk/software.mk index d9ce780f..938007dc 100644 --- a/rtl/mini/mk/software.mk +++ b/rtl/mini/mk/software.mk @@ -1,5 +1,5 @@ # software -CROSS=riscv32-unknown-elf- +CROSS ?= riscv32-unknown-elf- CP = $(CROSS)cpp CC = $(CROSS)gcc @@ -108,7 +108,7 @@ INC_PATH := -I$(ROOT_PATH)/crt/inc \ ifneq ($(findstring MDD,$(CORE) $(IP)),) -INC_PATH += -I$(ROOT_PATH)/rtl/min/mpw/.build +INC_PATH += -I$(ROOT_PATH)/rtl/mini/mpw/.build endif @@ -129,22 +129,23 @@ ifneq ($(filter RV32E RV32I,$(ISA)),) SRC_PATH += $(ROOT_PATH)/crt/libgcc/mulsi3.c endif -LDS_PATH := $(ROOT_PATH)/crt/./lds/$(LINK_TYPE).lds +LDS_PATH := $(ROOT_PATH)/crt/lds/$(LINK_TYPE).lds upd_ver_info: - python3 crt/ver.py + python3 $(ROOT_PATH)/crt/ver.py --root $(ROOT_PATH) asm: gen_mpw_code - cd app/asm && make - cp -rf app/asm/hello-asm.flash .sw_build/retrosoc_fw.hex - cp -rf app/asm/hello-asm.bin .sw_build/retrosoc_fw.bin - cp -rf app/asm/hello-asm.txt .sw_build/retrosoc_fw_all.txt + @mkdir -p $(ROOT_PATH)/.sw_build + $(MAKE) -C $(ROOT_PATH)/app/asm + cp $(ROOT_PATH)/app/asm/hello-asm.flash $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).hex + cp $(ROOT_PATH)/app/asm/hello-asm.bin $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).bin + cp $(ROOT_PATH)/app/asm/hello-asm.txt $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME)_all.txt firmware: gen_mpw_code upd_ver_info - @mkdir -p .sw_build - cd .sw_build && ($(CP) -P -o $(LINK_TYPE).lds $(LDS_PATH)) - cd .sw_build && ($(CC) $(CFLAGS) $(INC_PATH) -o $@ $(SRC_PATH)) - cd .sw_build && ($(OBJC) -O verilog $@ $(FIRMWARE_NAME).hex) - cd .sw_build && ($(OBJC) -O binary $@ $(FIRMWARE_NAME).bin) - cd .sw_build && ($(DUMP) -d $@ > $(FIRMWARE_NAME).txt) - cd .sw_build && ($(DUMP) -D $@ > $(FIRMWARE_NAME)_all.txt) \ No newline at end of file + @mkdir -p $(ROOT_PATH)/.sw_build + cd $(ROOT_PATH)/.sw_build && $(CP) -P -o $(LINK_TYPE).lds $(LDS_PATH) + cd $(ROOT_PATH)/.sw_build && $(CC) $(CFLAGS) $(INC_PATH) -o $@ $(SRC_PATH) + cd $(ROOT_PATH)/.sw_build && $(OBJC) -O verilog $@ $(FIRMWARE_NAME).hex + cd $(ROOT_PATH)/.sw_build && $(OBJC) -O binary $@ $(FIRMWARE_NAME).bin + cd $(ROOT_PATH)/.sw_build && $(DUMP) -d $@ > $(FIRMWARE_NAME).txt + cd $(ROOT_PATH)/.sw_build && $(DUMP) -D $@ > $(FIRMWARE_NAME)_all.txt diff --git a/rtl/mini/mk/vcs.mk b/rtl/mini/mk/vcs.mk index 612a4a99..c66b466d 100644 --- a/rtl/mini/mk/vcs.mk +++ b/rtl/mini/mk/vcs.mk @@ -1,9 +1,12 @@ # tools & paths -NOVAS := /nfs/tools/synopsys/verdi/V-2023.12-SP1-1/share/PLI/VCS/LINUX64 -EXTRA := -P $(NOVAS)/novas.tab $(NOVAS)/pli.a -SIM_TOOL := bsub -Is vcs -SIM_BINY := bsub -Is ./simv -VERDI_TOOL := bsub -Is verdi +NOVAS ?= /nfs/tools/synopsys/verdi/V-2023.12-SP1-1/share/PLI/VCS/LINUX64 +VCS_RUNNER ?= bsub -Is +VCS ?= vcs +VERDI ?= verdi +EXTRA ?= -P $(NOVAS)/novas.tab $(NOVAS)/pli.a +SIM_TOOL := $(VCS_RUNNER) $(VCS) +SIM_BINY := $(VCS_RUNNER) ./simv +VERDI_TOOL := $(VCS_RUNNER) $(VERDI) COMP_LOG := -l compile.log SIM_LOG := -l sim.log # netlist file path @@ -56,7 +59,7 @@ netcomp: FLIST := $(NETLIST_PATH) $(NET_FLIST) $(TB_FLIST) netcomp: OPTS := $(TIME_OPTION) postcomp: DIR := .vcs_build/post -postcomp: FLIST := $(NET_FLIST) $(TB_FLIST) +postcomp: FLIST := $(POST_PATH) $(NET_FLIST) $(TB_FLIST) postcomp: OPTS := $(POST_OPTS) sim: DIR := .vcs_build/behv @@ -72,18 +75,19 @@ sim: comp netsim: netcomp postsim: postcomp +comp netcomp postcomp: gen_mpw_code generate_filelist comp netcomp postcomp: @mkdir -p $(RTL_PATH)/$(DIR) - cd $(RTL_PATH)/$(DIR) && ($(SIM_TOOL) $(COMMON_OPTS) $(OPTS) $(FLIST) -top $(RTL_TOP) $(COMP_LOG)) + cd $(RTL_PATH)/$(DIR) && $(SIM_TOOL) $(COMMON_OPTS) $(OPTS) $(FLIST) -top $(RTL_TOP) $(COMP_LOG) sim netsim postsim: - cd $(RTL_PATH)/$(DIR) && ($(SIM_BINY) $(SIM_OPTS) $(if $(filter netsim postsim,$@),+bus_conflict_off) $(SIM_LOG)) + cd $(RTL_PATH)/$(DIR) && $(SIM_BINY) $(SIM_OPTS) $(if $(filter netsim postsim,$@),+bus_conflict_off) $(SIM_LOG) wave netwave postwave: cd $(RTL_PATH)/$(DIR) && ($(VERDI_TOOL) -ssf $(RTL_TOP).fsdb -nologo &) clean: - rm -rf .vcs_build/behv $(RTL_PATH)/.vcs_build/netl $(RTL_PATH)/.vcs_build/post + rm -rf $(RTL_PATH)/.vcs_build -.PHONY: comp netcomp postcomp sim netsim postsim wave clean \ No newline at end of file +.PHONY: comp netcomp postcomp sim netsim postsim wave clean diff --git a/rtl/mini/mk/verilator.mk b/rtl/mini/mk/verilator.mk index 3148957d..e115c8e4 100644 --- a/rtl/mini/mk/verilator.mk +++ b/rtl/mini/mk/verilator.mk @@ -1,7 +1,7 @@ BUILD_DIR := $(RTL_PATH)/.verilator_build SOC_CSRC_HOME += $(RTL_PATH)/csrc SOC_CSRC_LIB_HOME += $(RTL_PATH)/csrc -SOC_CXXFILES += $(shell find $(SOC_CSRC_HOME) -name "*.cpp") +SOC_CXXFILES += $(sort $(wildcard $(SOC_CSRC_HOME)/*.cpp)) SOC_CSRC_INCLPATH += -I$(SOC_CSRC_HOME) SOC_CSRC_INCLPATH += $(foreach val, $(SOC_CSRC_LIB_HOME), -I$(val)) @@ -18,7 +18,8 @@ SOC_VXXFILES += $(RTL_PATH)/vsrc/QSPIFlash.sv SOC_VXXFILES += $(RTL_PATH)/vsrc/retrosoc_top.sv SOC_VSRC_INCLPATH += -I$(SOC_VSRC_HOME) -VERILATOR_CXXFLAGS += -std=c++17 -static -Wall $(SOC_CSRC_INCLPATH) -DDUMP_WAVE_FST +VERILATOR ?= verilator +VERILATOR_CXXFLAGS += -std=c++17 -Wall $(SOC_CSRC_INCLPATH) -DDUMP_WAVE_FST VERILATOR_FLAGS += --cc --exe --no-timing --top-module $(SOC_VSRC_TOP) VERILATOR_FLAGS += --x-assign unique -O3 -CFLAGS "$(VERILATOR_CXXFLAGS)" VERILATOR_FLAGS += --trace-fst --assert --stats-vars --output-split 30000 --output-split-cfuncs 30000 @@ -27,28 +28,33 @@ VERILATOR_FLAGS += -o $(BUILD_DIR)/emu VERILATOR_FLAGS += -Mdir $(SOC_COMPILE_HOME) VERILATOR_FLAGS += $(SOC_VSRC_INCLPATH) $(SOC_CXXFILES) $(SOC_VXXFILES) -SOC_SIM_TIME ?= -1 +SOC_SIM_TIME ?= 40 -CCACHE := $(if $(shell which ccache),ccache,) +CCACHE := $(shell command -v ccache 2>/dev/null) ifneq ($(CCACHE),) export OBJCACHE = ccache +export CCACHE_DIR = $(BUILD_DIR)/ccache +export CCACHE_TEMPDIR = $(BUILD_DIR)/ccache/tmp endif lint: gen_mpw_code generate_filelist @mkdir -p $(BUILD_DIR) + @mkdir -p $(BUILD_DIR)/ccache/tmp comp: lint -# verilator $(VERILATOR_FLAGS) - verilator $(VERILATOR_FLAGS) > $(BUILD_DIR)/verilating.log 2>&1 - $(MAKE) VM_PARALLEL_BUILDS=1 OPT_FAST="-O3" -C $(SOC_COMPILE_HOME) -f V$(SOC_VSRC_TOP).mk -j$(nproc) > $(BUILD_DIR)/compile.log 2>&1 + $(VERILATOR) $(VERILATOR_FLAGS) > $(BUILD_DIR)/verilating.log 2>&1 + +$(MAKE) VM_PARALLEL_BUILDS=1 OPT_FAST="-O3" -C $(SOC_COMPILE_HOME) \ + -f V$(SOC_VSRC_TOP).mk > $(BUILD_DIR)/compile.log 2>&1 sim: comp - $(BUILD_DIR)/emu -i .sw_build/retrosoc_fw.bin -s $(RTL_SIM_CORESEL) -t 40 + @test -f $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).bin || { \ + echo "firmware image missing; run 'make firmware' first" >&2; exit 1; } + $(BUILD_DIR)/emu -i $(ROOT_PATH)/.sw_build/$(FIRMWARE_NAME).bin \ + -s $(RTL_SIM_CORESEL) -t $(SOC_SIM_TIME) wave: clean: - rm -rf .verilator_build + rm -rf $(BUILD_DIR) .PHONY: comp sim clean - diff --git a/rtl/mini/script/comb.py b/rtl/mini/script/comb.py index 1c773d08..c4ec62ed 100644 --- a/rtl/mini/script/comb.py +++ b/rtl/mini/script/comb.py @@ -1,24 +1,33 @@ -#!/bin/python +#!/usr/bin/env python3 -import sys -import os +from __future__ import annotations + +import argparse from pathlib import Path -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -MINI_DIR = os.path.abspath(f'{SCRIPT_DIR}/..') -GEN_DIR = os.path.abspath(f'{MINI_DIR}/.generated_fl') +from filelist import parse_filelists, write_filelist -res = f'{GEN_DIR}/yosys.fl' -is_first = True -for v in sys.argv[1:]: - if '-f' not in v: - if 'pdk' in v: continue +DEFAULT_OUTPUT = Path(__file__).resolve().parent.parent / ".generated_fl" / "yosys.fl" - print(f'gen code: {v}') - if is_first: - os.system(f'cat {v} > {res}') - is_first = False - else: - os.system(f'cat {v} >> {res}') +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Combine synthesis filelists") + parser.add_argument("-f", "--filelist", action="append", type=Path, default=[]) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + selected = [path for path in args.filelist if not path.name.startswith("pdk_")] + if not selected: + raise SystemExit("at least one non-PDK -f/--filelist is required") + combined = parse_filelists(selected) + write_filelist(args.output.resolve(), combined) + print(f"generated synthesis filelist: {args.output.resolve()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/script/convt_sv2v.py b/rtl/mini/script/convt_sv2v.py index e5b98834..4d7160e6 100644 --- a/rtl/mini/script/convt_sv2v.py +++ b/rtl/mini/script/convt_sv2v.py @@ -1,38 +1,38 @@ -#!/bin/python +#!/usr/bin/env python3 -import os -import sys +from __future__ import annotations + +import argparse import subprocess +from pathlib import Path -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -MINI_DIR = os.path.abspath(f'{SCRIPT_DIR}/..') -GEN_DIR = os.path.abspath(f'{MINI_DIR}/.iverilog_build/behv') +from filelist import parse_filelists -sv2v_cmd = 'sv2v' - -for v in sys.argv[1:]: - if '-f' not in v: - if 'pdk' in v: continue - # print(f'filelist: {v}') - try: - result = subprocess.run( - f"sed -e 's/+incdir+/-I/g' -e 's/+define+/-D/g' -e 's/^-v/-y/g' {v}", - shell=True, - capture_output=True, - text=True, - check=True - ) - sed_output = result.stdout - # print(sed_output.split('\n')) - lines = sed_output.split('\n') - for v in lines: - if v != '': - sv2v_cmd = f'{sv2v_cmd} {v}' - except Exception as e: - print(f"fail:{str(e)}") +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Convert the behavioral RTL with sv2v") + parser.add_argument("-f", "--filelist", action="append", type=Path, default=[]) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--sv2v", default="sv2v", help="sv2v executable") + return parser.parse_args() -sv2v_cmd = f'{sv2v_cmd} --write {GEN_DIR}/converted_soc.v' -# print(f'sv2v_cmd: {sv2v_cmd}') -os.system(sv2v_cmd) \ No newline at end of file +def main() -> int: + args = parse_args() + selected = [path for path in args.filelist if not path.name.startswith("pdk_")] + if not selected: + raise SystemExit("at least one non-PDK -f/--filelist is required") + + filelist = parse_filelists(selected) + args.output.parent.mkdir(parents=True, exist_ok=True) + command = [args.sv2v] + command.extend(f"-D{item[len('+define+'):]}" for item in filelist.defines) + command.extend(f"-I{path}" for path in filelist.incdirs) + command.extend(str(path) for path in filelist.files) + command.extend(("--write", str(args.output.resolve()))) + subprocess.run(command, check=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/script/filelist.py b/rtl/mini/script/filelist.py new file mode 100644 index 00000000..9ee8f865 --- /dev/null +++ b/rtl/mini/script/filelist.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import os +import shlex +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable + + +@dataclass +class FileList: + defines: list[str] = field(default_factory=list) + incdirs: list[Path] = field(default_factory=list) + files: list[Path] = field(default_factory=list) + library_files: list[Path] = field(default_factory=list) + options: list[str] = field(default_factory=list) + + def extend(self, other: "FileList") -> None: + self.defines.extend(other.defines) + self.incdirs.extend(other.incdirs) + self.files.extend(other.files) + self.library_files.extend(other.library_files) + self.options.extend(other.options) + + def deduplicate(self) -> "FileList": + self.defines = _unique(self.defines) + self.incdirs = _unique(self.incdirs) + self.files = _unique(self.files) + self.library_files = _unique(self.library_files) + self.options = _unique(self.options) + return self + + def as_tokens(self) -> list[str]: + return [ + *self.defines, + *(_quote(f"+incdir+{path}") for path in self.incdirs), + *(item for path in self.library_files for item in ("-v", _quote(str(path)))), + *self.options, + *(_quote(str(path)) for path in self.files), + ] + + +def _unique(values: Iterable): + return list(dict.fromkeys(values)) + + +def _quote(value: str) -> str: + if not any(character.isspace() for character in value): + return value + return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def atomic_write(path: Path, content: str) -> bool: + """Write content atomically and preserve mtime when content is unchanged.""" + path = Path(path) + if path.exists() and path.read_text(encoding="utf-8") == content: + return False + + path.parent.mkdir(parents=True, exist_ok=True) + fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream: + stream.write(content) + os.replace(temp_name, path) + except Exception: + Path(temp_name).unlink(missing_ok=True) + raise + return True + + +def tokenize_filelist(path: Path) -> list[str]: + tokens: list[str] = [] + for line_number, raw_line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + stripped = raw_line.strip() + if not stripped or stripped.startswith("//"): + continue + try: + tokens.extend(shlex.split(raw_line, comments=True, posix=True)) + except ValueError as error: + raise ValueError(f"{path}:{line_number}: {error}") from error + return tokens + + +def parse_filelists(paths: Iterable[Path], *, require_files: bool = True) -> FileList: + result = FileList() + visited: set[Path] = set() + for path in paths: + result.extend( + _parse_filelist(Path(path).resolve(), visited, require_files=require_files) + ) + return result.deduplicate() + + +def _parse_filelist( + path: Path, visited: set[Path], *, require_files: bool +) -> FileList: + if path in visited: + return FileList() + if not path.is_file(): + raise FileNotFoundError(f"filelist not found: {path}") + visited.add(path) + + result = FileList() + tokens = tokenize_filelist(path) + index = 0 + while index < len(tokens): + token = tokens[index] + if token == "-f": + index += 1 + if index >= len(tokens): + raise ValueError(f"{path}: -f requires a filelist path") + nested = _resolve(tokens[index], path.parent) + result.extend( + _parse_filelist(nested, visited, require_files=require_files) + ) + elif token.startswith("-f") and len(token) > 2: + nested = _resolve(token[2:], path.parent) + result.extend( + _parse_filelist(nested, visited, require_files=require_files) + ) + elif token.startswith("+define+"): + result.defines.append(token) + elif token.startswith("+incdir+"): + incdir = _resolve(token[len("+incdir+") :], path.parent) + if require_files and not incdir.is_dir(): + raise FileNotFoundError(f"include directory not found: {incdir}") + result.incdirs.append(incdir) + elif token == "-v": + index += 1 + if index >= len(tokens): + raise ValueError(f"{path}: -v requires a source path") + library_file = _resolve(tokens[index], path.parent) + _require_source(library_file, require_files) + result.library_files.append(library_file) + elif token.startswith("-") or token.startswith("+"): + result.options.append(token) + else: + source = _resolve(token, path.parent) + _require_source(source, require_files) + result.files.append(source) + index += 1 + return result + + +def _resolve(value: str, base: Path) -> Path: + path = Path(value).expanduser() + return path.resolve() if path.is_absolute() else (base / path).resolve() + + +def _require_source(path: Path, required: bool) -> None: + if required and not path.is_file(): + raise FileNotFoundError(f"source file not found: {path}") + + +def write_filelist(path: Path, filelist: FileList) -> bool: + content = "\n".join(filelist.as_tokens()) + "\n" + return atomic_write(path, content) diff --git a/rtl/mini/script/gen_iverilog_filelist.py b/rtl/mini/script/gen_iverilog_filelist.py index 5687f4dd..2b50cd95 100644 --- a/rtl/mini/script/gen_iverilog_filelist.py +++ b/rtl/mini/script/gen_iverilog_filelist.py @@ -1,44 +1,84 @@ -#!/bin/python +#!/usr/bin/env python3 -import sys -import os +from __future__ import annotations -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -MINI_DIR = os.path.abspath(f'{SCRIPT_DIR}/..') -GEN_DIR = os.path.abspath(f'{MINI_DIR}/.generated_fl') +import argparse +import gzip +from pathlib import Path -cmd = ['+timescale+1ns/1ps\n'] -filelist = ['def', 'sys_def', 'inc', 'tb'] - -for file in filelist: - with open(f'{GEN_DIR}/{file}.fl', 'r', encoding='utf-8') as fp: - tmp = fp.readlines() - cmd += tmp +from filelist import FileList, atomic_write, parse_filelists, write_filelist -tgt = sys.argv[1] -pdk = sys.argv[2] -netl = sys.argv[3] -print(f'tgt: {tgt}') -print(f'pdk: {pdk}') -print(f'netl: {netl}') +SCRIPT_DIR = Path(__file__).resolve().parent +MINI_DIR = SCRIPT_DIR.parent +DEFAULT_GENERATED_DIR = MINI_DIR / ".generated_fl" -if tgt == 'sim': - cmd += [f'{MINI_DIR}/.iverilog_build/behv/converted_soc.v\n'] -elif tgt == 'netsim': - with open(f'{GEN_DIR}/commonip.fl', 'r', encoding='utf-8') as fp: - tmp = fp.readlines() - cmd += tmp - cmd += f'{netl}\n' +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate an Icarus Verilog filelist") + parser.add_argument("--mode", required=True, choices=("behv", "netl", "post")) + parser.add_argument("--pdk", required=True) + parser.add_argument("--generated-dir", type=Path, default=DEFAULT_GENERATED_DIR) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--converted", type=Path) + parser.add_argument("--netlist", type=Path) + parser.add_argument("--sdf", type=Path) + parser.add_argument("--sdf-scope", default="retrosoc_tb.u_retrosoc_asic") + return parser.parse_args() -elif tgt == 'postsim': - pass -with open(f'{GEN_DIR}/pdk_{pdk.lower()}.fl', 'r', encoding='utf-8') as fp: - tmp = fp.readlines() - cmd += tmp +def _require(path: Path | None, description: str) -> Path: + if path is None or not path.is_file(): + raise FileNotFoundError(f"{description} not found: {path}") + return path.resolve() -with open(f'{GEN_DIR}/iverilog.fl', 'w', encoding='utf-8') as fp: - fp.writelines(cmd) +def _prepare_sdf(source: Path, output_dir: Path, scope: str) -> Path: + sdf_output = output_dir / "timing.sdf" + if source.suffix == ".gz": + content = gzip.open(source, "rt", encoding="utf-8", errors="replace").read() + else: + content = source.read_text(encoding="utf-8", errors="replace") + atomic_write(sdf_output, content) + + annotator = output_dir / "sdf_annotator.sv" + escaped_sdf = str(sdf_output.resolve()).replace("\\", "\\\\").replace('"', '\\"') + atomic_write( + annotator, + "module retrosoc_sdf_annotator;\n" + " initial begin\n" + f' $sdf_annotate("{escaped_sdf}", {scope}, , "sdf.log", "MINIMUM");\n' + " end\n" + "endmodule\n", + ) + return annotator + + +def main() -> int: + args = parse_args() + generated_dir = args.generated_dir.resolve() + base_names = ["def.fl", "sys_def.fl", "inc.fl", "tb.fl"] + base = parse_filelists(generated_dir / name for name in base_names) + base.options.insert(0, "+timescale+1ns/1ps") + + if args.mode == "behv": + base.files.append(_require(args.converted, "converted behavioral RTL")) + else: + common = parse_filelists([generated_dir / "commonip.fl"]) + base.extend(common) + base.files.append(_require(args.netlist, f"{args.mode} netlist")) + + if args.mode == "post": + sdf = _require(args.sdf, "post-layout SDF") + base.files.append(_prepare_sdf(sdf, args.output.resolve().parent, args.sdf_scope)) + + pdk_filelist = generated_dir / f"pdk_{args.pdk.lower()}.fl" + base.extend(parse_filelists([pdk_filelist])) + base.deduplicate() + write_filelist(args.output.resolve(), base) + print(f"generated {args.mode} filelist: {args.output.resolve()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/script/generate_filelist.py b/rtl/mini/script/generate_filelist.py index 0af0945f..e3261788 100644 --- a/rtl/mini/script/generate_filelist.py +++ b/rtl/mini/script/generate_filelist.py @@ -1,114 +1,118 @@ -#!/bin/python +#!/usr/bin/env python3 -import os -import glob +from __future__ import annotations + +import argparse import re +from pathlib import Path -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ROOT_DIR = os.path.abspath(f'{SCRIPT_DIR}/../../..') -RTL_DIR = os.path.abspath(f'{SCRIPT_DIR}/../..') -MINI_DIR = os.path.abspath(f'{SCRIPT_DIR}/..') -GEN_DIR = os.path.abspath(f'{MINI_DIR}/.generated_fl') +from filelist import atomic_write -def process_fl_files(folder_path): - # source files - pdk_patn = re.compile(r'^(/(pdk))') - rtl_patn = re.compile(r'^(/(clusterip|ip|tech))') - mini_patn = re.compile(r'^(/(core|mpw|tb|top))') - # incdir - inc_pdk_patn = re.compile(r'^(\+incdir\+)(/(pdk))') - inc_rtl_patn = re.compile(r'^(\+incdir\+)(/(clusterip|ip|tech))') - inc_mini_patn = re.compile(r'^(\+incdir\+)(/(core|mpw|tb|top))') +SCRIPT_DIR = Path(__file__).resolve().parent +MINI_DIR = SCRIPT_DIR.parent +RTL_DIR = MINI_DIR.parent +ROOT_DIR = RTL_DIR.parent +DEFAULT_OUTPUT_DIR = MINI_DIR / ".generated_fl" - msg_files = glob.glob(os.path.join(folder_path, "*.fl")) - if not msg_files: - print("no files found") - return - - for msg_file in msg_files: - try: - with open(msg_file, 'r', encoding='utf-8') as f: - lines = f.readlines() - - processed_lines = [] - for line in lines: - pdk_repl = f'{ROOT_DIR}' + r'\1' - rtl_repl = f'{RTL_DIR}' + r'\1' - mini_repl = f'{MINI_DIR}' + r'\1' - - inc_pdk_repl = r'\1' + f'{ROOT_DIR}' + r'\2' - inc_rtl_repl = r'\1' + f'{RTL_DIR}' + r'\2' - inc_mini_repl = r'\1' + f'{MINI_DIR}' + r'\2' - - new_line = pdk_patn.sub(pdk_repl, line) - new_line = rtl_patn.sub(rtl_repl, new_line) - new_line = mini_patn.sub(mini_repl, new_line) - new_line = inc_pdk_patn.sub(inc_pdk_repl, new_line) - new_line = inc_rtl_patn.sub(inc_rtl_repl, new_line) - new_line = inc_mini_patn.sub(inc_mini_repl, new_line) - - processed_lines.append(new_line) - - with open(msg_file, 'w', encoding='utf-8') as f: - f.writelines(processed_lines) - - except FileNotFoundError: - print(f"ERROR: file {msg_file} is not found") - except PermissionError: - print(f"ERROR: file {msg_file} is not permission") - except Exception as e: - print(f"ERROR: file {msg_file} unknown {str(e)}") - -def process_lint_files(folder_path): - # source files - pdk_patn = re.compile(r'(\,)(/(pdk))') - rtl_patn = re.compile(r'(\,)(/(clusterip|ip|tech))') - mini_patn = re.compile(r'(\,)(/(core|mpw|tb|top))') - first_patn = re.compile(r'(\=)(/(core|mpw|tb|top))') +PATH_PREFIXES = { + "pdk": ROOT_DIR / "pdk", + "clusterip": RTL_DIR / "clusterip", + "ip": RTL_DIR / "ip", + "tech": RTL_DIR / "tech", + "core": MINI_DIR / "core", + "mpw": MINI_DIR / "mpw", + "tb": MINI_DIR / "tb", + "top": MINI_DIR / "top", +} - msg_files = glob.glob(os.path.join(folder_path, "*.msg")) - if not msg_files: - print("no files found") - return - - for msg_file in msg_files: - try: - with open(msg_file, 'r', encoding='utf-8') as f: - lines = f.readlines() - - processed_lines = [] - for line in lines: - pdk_repl = r'\1' + f'{ROOT_DIR}' + r'\2' - rtl_repl = r'\1' + f'{RTL_DIR}' + r'\2' - mini_repl = r'\1' + f'{MINI_DIR}' + r'\2' - first_repl = r'\1' + f'{MINI_DIR}' + r'\2' - - # seq is important! - new_line = pdk_patn.sub(pdk_repl, line) - new_line = rtl_patn.sub(rtl_repl, new_line) - new_line = mini_patn.sub(mini_repl, new_line) - new_line = first_patn.sub(first_repl, new_line) - - processed_lines.append(new_line) - - with open(msg_file, 'w', encoding='utf-8') as f: - f.writelines(processed_lines) - - except FileNotFoundError: - print(f'ERROR: file {msg_file} is not found') - except PermissionError: - print(f'ERROR: file {msg_file} has not permission') - except Exception as e: - print(f'ERROR: file {msg_file} unknown {str(e)}') +def expand_filelist(content: str) -> str: + output: list[str] = [] + for line in content.splitlines(): + expanded = line + for prefix, directory in PATH_PREFIXES.items(): + expanded = re.sub( + rf"(? str: + expanded = content + for prefix, directory in PATH_PREFIXES.items(): + expanded = expanded.replace(f",/{prefix}", f",{directory}") + expanded = expanded.replace(f"=/{prefix}", f"={directory}") + return expanded -process_fl_files(GEN_DIR) -process_lint_files(GEN_DIR) \ No newline at end of file + +def generate_all(output_dir: Path, defines: list[str]) -> list[Path]: + output_dir.mkdir(parents=True, exist_ok=True) + generated: list[Path] = [] + + templates = sorted((RTL_DIR / "filelist").glob("pdk_*.fl")) + templates.extend( + MINI_DIR / "filelist" / name + for name in ( + "clusterip.fl", + "commonip.fl", + "core_hazard3.fl", + "core_picorv32.fl", + "inc.fl", + "ip.fl", + "sys_def.fl", + "tb.fl", + "tech.fl", + "top.fl", + ) + ) + for source in templates: + if not source.is_file(): + raise FileNotFoundError(f"canonical filelist not found: {source}") + destination = output_dir / source.name + atomic_write( + destination, + expand_filelist(source.read_text(encoding="utf-8")), + ) + generated.append(destination) + + lint_source = MINI_DIR / "lint.msg" + lint_destination = output_dir / lint_source.name + atomic_write( + lint_destination, + expand_lint_config(lint_source.read_text(encoding="utf-8")), + ) + generated.append(lint_destination) + + def_file = output_dir / "def.fl" + atomic_write(def_file, " ".join(defines) + "\n") + generated.append(def_file) + return generated + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate absolute RTL filelists") + parser.add_argument( + "--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="output directory" + ) + parser.add_argument( + "--define", action="append", default=[], help="filelist +define+ token" + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + invalid = [item for item in args.define if not item.startswith("+define+")] + if invalid: + raise SystemExit(f"invalid define token(s): {' '.join(invalid)}") + generated = generate_all(args.output_dir.resolve(), args.define) + print(f"generated {len(generated)} files in {args.output_dir.resolve()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rtl/mini/script/prepare_norflash.py b/rtl/mini/script/prepare_norflash.py index 662c8a40..2bb6641f 100644 --- a/rtl/mini/script/prepare_norflash.py +++ b/rtl/mini/script/prepare_norflash.py @@ -1,39 +1,55 @@ -#!/bin/python +#!/usr/bin/env python3 -import sys -import os +from __future__ import annotations -sim_dir = { 'sim': 'behv', 'netsim': 'netl', 'postsim': 'post' } -tgt = sys.argv[1] - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -MINI_DIR = os.path.abspath(f'{SCRIPT_DIR}/..') -ROOT_DIR = os.path.abspath(f'{MINI_DIR}/../..') -NORLASH_DIR = os.path.abspath(f'{MINI_DIR}/../ip/3rd-party/norflash') -SIM_DIR = os.path.abspath(f'{MINI_DIR}/.iverilog_build/{sim_dir[tgt]}') +import argparse +import shutil +from pathlib import Path -# check -check_files = ['SECSI', 'SFDP', 'SREG', 'MEM'] +SCRIPT_DIR = Path(__file__).resolve().parent +MINI_DIR = SCRIPT_DIR.parent +ROOT_DIR = MINI_DIR.parent.parent +DEFAULT_MODELS = MINI_DIR.parent / "ip" / "3rd-party" / "norflash" +DEFAULT_FIRMWARE = ROOT_DIR / ".sw_build" / "retrosoc_fw.hex" -try: - dir_file_list = [ - fname for fname in os.listdir(SIM_DIR) - if os.path.isfile(os.path.join(SIM_DIR, fname)) - ] - - missing_files = [] - for fname in check_files: - if f'{fname}.TXT' not in dir_file_list: - missing_files.append(fname) - - if not missing_files: - print(f'contains all files') - else: - os.system(f'cp -rf {NORLASH_DIR}/*.TXT {SIM_DIR}') - os.system(f'ln -sf {ROOT_DIR}/.sw_build/retrosoc_fw.hex {SIM_DIR}/MEM.TXT') - -except PermissionError: - print(f"ERROR: {SIM_DIR} has not permission") -except Exception as e: - print(f"ERROR: unknown {str(e)}") \ No newline at end of file + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Prepare NOR Flash simulation files") + parser.add_argument("--sim-dir", type=Path, required=True) + parser.add_argument("--models-dir", type=Path, default=DEFAULT_MODELS) + parser.add_argument("--firmware", type=Path, default=DEFAULT_FIRMWARE) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + sim_dir = args.sim_dir.resolve() + models_dir = args.models_dir.resolve() + firmware = args.firmware.resolve() + sim_dir.mkdir(parents=True, exist_ok=True) + + model_names = ("SECSI.TXT", "SFDP.TXT", "SREG.TXT") + missing_models = [name for name in model_names if not (models_dir / name).is_file()] + if missing_models: + raise FileNotFoundError( + f"NOR Flash model files missing in {models_dir}: {', '.join(missing_models)}" + ) + if not firmware.is_file(): + raise FileNotFoundError( + f"firmware image not found: {firmware}; run 'make firmware' first" + ) + + for name in model_names: + shutil.copy2(models_dir / name, sim_dir / name) + + memory = sim_dir / "MEM.TXT" + if memory.is_symlink() or memory.exists(): + memory.unlink() + memory.symlink_to(firmware) + print(f"prepared NOR Flash files in {sim_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..c4712a90 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Repository automation helpers.""" diff --git a/scripts/clean.py b/scripts/clean.py new file mode 100644 index 00000000..bada6dd7 --- /dev/null +++ b/scripts/clean.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Remove retroSoC generated output") + parser.add_argument("--root", type=Path, required=True) + return parser.parse_args() + + +def remove(root: Path, relative: str) -> None: + target = (root / relative).resolve() + target.relative_to(root) + if target.is_dir() and not target.is_symlink(): + shutil.rmtree(target) + elif target.exists() or target.is_symlink(): + target.unlink() + print(f"cleaned {target}") + + +def main() -> int: + root = parse_args().root.resolve() + if not (root / "Makefile").is_file() or not (root / ".git").exists(): + raise SystemExit(f"not a retroSoC repository root: {root}") + generated = ( + ".sw_build", + "rtl/mini/.generated_fl", + "rtl/mini/.iverilog_build", + "rtl/mini/.verilator_build", + "rtl/mini/.vcs_build", + "syn/yosys/.synth_build", + "sta/opensta/retrosoc_sta.log", + "export/rtl", + ) + for relative in generated: + remove(root, relative) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/doctor.py b/scripts/doctor.py new file mode 100644 index 00000000..dcb5be2b --- /dev/null +++ b/scripts/doctor.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Check the retroSoC build environment") + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--simu", required=True) + parser.add_argument("--synth", required=True) + parser.add_argument("--sta", required=True) + parser.add_argument("--pdk", required=True) + parser.add_argument("--core", required=True) + parser.add_argument("--ip", required=True) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + root = args.root.resolve() + tools = ["python3", "make", "git", "riscv32-unknown-elf-gcc"] + if args.simu == "IVERILOG": + tools.extend(("iverilog", "vvp", "sv2v")) + elif args.simu == "VERILATOR": + tools.extend(("verilator", "c++")) + elif args.simu == "VCS": + tools.extend(("bsub", "vcs")) + if args.synth == "YOSYS": + tools.extend(("yosys", "gawk")) + if args.sta == "OPENSTA": + tools.append("sta") + + paths = { + "MPW generator": root / "rtl/mini/mpw/.git", + "cluster IP": root / "rtl/clusterip/common", + "third-party models": root / "rtl/ip/3rd-party", + } + pdk_paths = { + "IHP130": root / "pdk/IHP-Open-PDK", + "ICS55": root / "pdk/icsprout55-pdk", + "SKY130": root / "pdk/skywater-sdk", + "GF180": root / "pdk/gf180mcu-pdk", + } + paths[f"PDK {args.pdk}"] = pdk_paths[args.pdk] + + failures = 0 + print("Tools:") + for tool in dict.fromkeys(tools): + location = shutil.which(tool) + status = location or "MISSING" + print(f" {tool:<28} {status}") + failures += location is None + + print("Paths:") + for name, path in paths.items(): + exists = path.exists() + print(f" {name:<28} {path} {'[ok]' if exists else '[MISSING]'}") + failures += not exists + + if failures: + print(f"doctor found {failures} missing requirement(s)") + return 1 + print("doctor: all selected requirements are available") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prepare_mpw.py b/scripts/prepare_mpw.py new file mode 100644 index 00000000..ec843ad3 --- /dev/null +++ b/scripts/prepare_mpw.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import shutil +import sys +import tarfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.setup_helpers import download_file, ensure_git_repo # noqa: E402 + + +MPW_DIR = ROOT / "rtl/mini/mpw" +HAZARD3_REVISION = "5b3a34f6955e50e03bdcb964201c91462f7078c3" +IBEX_REVISION = "0c233f54361d769f370889223acc456f2ac19d46" +SERV_ARCHIVE = MPW_DIR / "serv-1.4.0.tar.gz" +SERV_URL = "https://github.com/olofk/serv/archive/refs/tags/1.4.0.tar.gz" +SERV_SHA256 = "f81c37b9f9d548c658e23bb7eb90fbed8b54e2e9e51fa42b8cd2f34cecc472ab" + + +def tree_digest(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + digest.update(path.relative_to(root).as_posix().encode("utf-8")) + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def sync_tree(source: Path, destination: Path) -> None: + if destination.is_dir() and tree_digest(source) == tree_digest(destination): + print(f"[dependency] source tree unchanged: {destination}") + return + if destination.exists(): + shutil.rmtree(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, destination) + + +def extract_serv() -> Path: + extracted = MPW_DIR / "serv-1.4.0" + if extracted.is_dir(): + return extracted + with tarfile.open(SERV_ARCHIVE, "r:gz") as archive: + for member in archive.getmembers(): + (MPW_DIR / member.name).resolve().relative_to(MPW_DIR.resolve()) + archive.extractall(MPW_DIR) + if not extracted.is_dir(): + raise RuntimeError(f"SERV archive did not create {extracted}") + return extracted + + +def main() -> int: + parser = argparse.ArgumentParser(description="Prepare pinned MPW core sources") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + if not (MPW_DIR / ".git").is_dir(): + raise SystemExit("MPW generator is missing; run 'python3 setup.py' first") + + ensure_git_repo( + "https://github.com/Wren6991/Hazard3.git", + MPW_DIR / "Hazard3", + HAZARD3_REVISION, + update=args.update, + ) + ensure_git_repo( + "https://github.com/lowRISC/ibex.git", + MPW_DIR / "ibex", + IBEX_REVISION, + update=args.update, + ) + download_file( + SERV_URL, + SERV_ARCHIVE, + SERV_SHA256, + update=args.update, + ) + serv = extract_serv() + + sync_tree(MPW_DIR / "Hazard3/hdl", MPW_DIR / "core/username3/Hazard3") + sync_tree(serv / "rtl", MPW_DIR / "core/username4/serv") + sync_tree(MPW_DIR / "ibex/rtl", MPW_DIR / "core/username7/ibex") + (MPW_DIR / ".build").mkdir(exist_ok=True) + print("pinned MPW core sources are ready") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/setup_helpers.py b/scripts/setup_helpers.py new file mode 100644 index 00000000..c1471e1a --- /dev/null +++ b/scripts/setup_helpers.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import hashlib +import os +import subprocess +import tempfile +import time +import urllib.request +from pathlib import Path +from typing import Iterable + + +def run(command: Iterable[str], *, cwd: Path | None = None) -> None: + printable = " ".join(str(item) for item in command) + print(f"+ {printable}") + subprocess.run( + [str(item) for item in command], + cwd=cwd, + check=True, + ) + + +def git_output(repo: Path, *args: str) -> str: + return subprocess.check_output( + ["git", "-C", str(repo), *args], text=True, stderr=subprocess.STDOUT + ).strip() + + +def ensure_git_repo( + url: str, + destination: Path, + revision: str, + *, + recursive: bool = False, + update: bool = False, + allow_dirty: bool = False, +) -> None: + destination = destination.resolve() + created = False + if not destination.exists(): + destination.parent.mkdir(parents=True, exist_ok=True) + command = ["git", "clone"] + if recursive: + command.append("--recursive") + command.extend((url, str(destination))) + run(command) + created = True + elif not (destination / ".git").exists(): + raise RuntimeError(f"dependency path is not a Git repository: {destination}") + + current = git_output(destination, "rev-parse", "HEAD") + dirty = git_output(destination, "status", "--porcelain") + if current != revision: + if not created and not update: + raise RuntimeError( + f"{destination} is at {current}, expected {revision}; rerun with --update" + ) + if not created and dirty: + raise RuntimeError(f"refusing to update a dirty dependency: {destination}") + try: + git_output(destination, "cat-file", "-e", f"{revision}^{{commit}}") + except subprocess.CalledProcessError: + run(("git", "fetch", "origin", revision), cwd=destination) + run(("git", "checkout", "--detach", revision), cwd=destination) + elif dirty and not allow_dirty: + raise RuntimeError(f"pinned dependency has local changes: {destination}") + + if recursive: + run(("git", "submodule", "update", "--init", "--recursive"), cwd=destination) + print(f"[dependency] {destination.name}: {revision}") + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def download_file( + url: str, + destination: Path, + expected_sha256: str, + *, + update: bool = False, + retries: int = 3, + timeout: int = 30, +) -> None: + destination = destination.resolve() + if destination.is_file(): + actual = sha256(destination) + if actual == expected_sha256: + print(f"[dependency] archive verified: {destination}") + return + if not update: + raise RuntimeError( + f"checksum mismatch for {destination}: {actual}; rerun with --update" + ) + + destination.parent.mkdir(parents=True, exist_ok=True) + last_error: Exception | None = None + for attempt in range(1, retries + 1): + fd, temporary_name = tempfile.mkstemp( + prefix=f".{destination.name}.", dir=destination.parent + ) + os.close(fd) + temporary = Path(temporary_name) + try: + request = urllib.request.Request( + url, headers={"User-Agent": "retroSoC-setup/1"} + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + with temporary.open("wb") as output: + while chunk := response.read(1024 * 1024): + output.write(chunk) + actual = sha256(temporary) + if actual != expected_sha256: + raise RuntimeError( + f"checksum mismatch for {url}: {actual}, expected {expected_sha256}" + ) + os.replace(temporary, destination) + print(f"[dependency] downloaded: {destination}") + return + except Exception as error: + last_error = error + temporary.unlink(missing_ok=True) + if attempt < retries: + time.sleep(attempt) + raise RuntimeError(f"failed to download {url}: {last_error}") + + +def atomic_write(path: Path, content: str) -> bool: + path = path.resolve() + if path.exists() and path.read_text(encoding="utf-8") == content: + return False + path.parent.mkdir(parents=True, exist_ok=True) + fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(fd, "w", encoding="utf-8") as stream: + stream.write(content) + os.replace(temporary_name, path) + except Exception: + Path(temporary_name).unlink(missing_ok=True) + raise + return True diff --git a/setup.py b/setup.py index 8434f867..43e789c2 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,27 @@ #!/usr/bin/env python3 -import sys -import os +import argparse +from pathlib import Path -project_path = os.getcwd() -os.chdir(project_path) -os.system("git clone https://github.com/retroSoC/mini-ver-mpw.git rtl/mini/mpw") +from scripts.setup_helpers import ensure_git_repo + + +ROOT = Path(__file__).resolve().parent +MPW_REVISION = "30503eb8a7cf33a0c955913faba5014ebd00186f" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Install the pinned MPW generator") + parser.add_argument("--update", action="store_true") + args = parser.parse_args() + ensure_git_repo( + "https://github.com/retroSoC/mini-ver-mpw.git", + ROOT / "rtl/mini/mpw", + MPW_REVISION, + update=args.update, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sta/opensta/opensta.mk b/sta/opensta/opensta.mk index 97a312e0..a432c3c5 100644 --- a/sta/opensta/opensta.mk +++ b/sta/opensta/opensta.mk @@ -1,5 +1,24 @@ +OPENSTA ?= sta +OPENSTA_NETLIST ?= $(ROOT_PATH)/syn/yosys/.synth_build/out/retrosoc_asic_yosys.v +OPENSTA_LIBERTY ?= $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_stdcell/lib/sg13g2_stdcell_typ_1p20V_25C.lib +OPENSTA_IO_LIB ?= $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_io/lib/sg13g2_io_typ_1p2V_3p3V_25C.lib +OPENSTA_SRAM_LIBS ?= $(wildcard $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/lib/*_typ_1p20V_25C.lib) +OPENSTA_SDC ?= $(ROOT_PATH)/sta/opensta/gen2.sdc +OPENSTA_REPORT ?= $(ROOT_PATH)/sta/opensta/retrosoc_sta.log +OPENSTA_CONFIG ?= $(ROOT_PATH)/syn/yosys/.synth_build/out/retrosoc_asic_yosys.config + sta: - sta $(RTL_PATH)/../../sta/opensta/opensta.tcl -threads max + @for input in $(OPENSTA_NETLIST) $(OPENSTA_LIBERTY) $(OPENSTA_IO_LIB) $(OPENSTA_SRAM_LIBS) $(OPENSTA_SDC) $(OPENSTA_CONFIG); do \ + test -f "$$input" || { echo "OpenSTA input missing: $$input" >&2; exit 1; }; \ + done + @grep -qx 'PDK=$(PDK)' $(OPENSTA_CONFIG) || { \ + echo "OpenSTA netlist configuration does not match PDK=$(PDK); rerun synthesis" >&2; \ + exit 1; \ + } + OPENSTA_NETLIST=$(OPENSTA_NETLIST) OPENSTA_LIBERTY=$(OPENSTA_LIBERTY) \ + OPENSTA_IO_LIB=$(OPENSTA_IO_LIB) OPENSTA_SRAM_LIBS="$(OPENSTA_SRAM_LIBS)" OPENSTA_SDC=$(OPENSTA_SDC) \ + OPENSTA_REPORT=$(OPENSTA_REPORT) \ + $(OPENSTA) $(ROOT_PATH)/sta/opensta/opensta.tcl -threads max -.PHONY: sta \ No newline at end of file +.PHONY: sta diff --git a/sta/opensta/opensta.tcl b/sta/opensta/opensta.tcl index 91ef156e..2f9921c0 100644 --- a/sta/opensta/opensta.tcl +++ b/sta/opensta/opensta.tcl @@ -1,11 +1,19 @@ -# report_timing -read_liberty pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_io/lib/sg13g2_io_typ_1p2V_3p3V_25C.lib -read_liberty pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_stdcell/lib/sg13g2_stdcell_typ_1p20V_25C.lib -read_verilog syn/yosys/.synth_build/out/retrosoc_asic_yosys.v -link_design retrosoc_asic -read_sdc sta/opensta/gen2.sdc +foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_IO_LIB OPENSTA_SRAM_LIBS OPENSTA_SDC OPENSTA_REPORT} { + if {![info exists ::env($variable)] || $::env($variable) eq ""} { + error "required environment variable is missing: $variable" + } +} -report_checks -path_delay min_max -path_group {clk_ext clk_aud } -sort_by_slack -slack_max 0.0 -group_path_count 1000 -endpoint_path_count 1000 > sta/opensta/retrosoc_sta.log +read_liberty $::env(OPENSTA_IO_LIB) +read_liberty $::env(OPENSTA_LIBERTY) +foreach liberty $::env(OPENSTA_SRAM_LIBS) { + read_liberty $liberty +} +read_verilog $::env(OPENSTA_NETLIST) +link_design retrosoc_asic +read_sdc $::env(OPENSTA_SDC) + +report_checks -path_delay min_max -path_group {clk_ext clk_aud } -sort_by_slack -slack_max 0.0 -group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_REPORT) # set_propagated_clock clk # read_spef gcd_sky130hd.spef diff --git a/syn/tools/export_soc_sources.py b/syn/tools/export_soc_sources.py index 9e9f2a0b..99585e77 100644 --- a/syn/tools/export_soc_sources.py +++ b/syn/tools/export_soc_sources.py @@ -1,290 +1,207 @@ #!/usr/bin/env python3 -# Export retroSoC sources (tar.gz or single .sv). -# Rebuild filelists to absolute paths; inline `include` in .sv. -# Usage: python3 syn/tools/export_soc_sources.py [sv|tar] --soc MINI|TINY -# Outputs: sv -> export/retrosoc_asic_sources.sv; tar -> export/retrosoc__sources.tar.gz +"""Export the configured retroSoC RTL as a tarball or a single SV file.""" + +from __future__ import annotations + import argparse -import os -import tarfile +import re import shutil import sys +import tarfile +import tempfile from pathlib import Path -import re REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_DIR = REPO_ROOT / "rtl/mini/script" +sys.path.insert(0, str(SCRIPT_DIR)) +from filelist import FileList, atomic_write, parse_filelists # noqa: E402 +from generate_filelist import generate_all # noqa: E402 -def read_flist(flist: Path): - """Parse a filelist where entries may be space-separated on a single line. - - Robustly handles lines like: "+define+FOO +define+BAR +incdir+dir path/to/file.sv ..." - by splitting into tokens and classifying each token. - """ - defines = [] - incdirs = [] - files = [] - if not flist.exists(): - return defines, incdirs, files - for raw in flist.read_text().splitlines(): - # strip comments (# or //) and split by whitespace - line = raw.split('//', 1)[0].split('#', 1)[0].strip() - if not line: - continue - tokens = line.split() - for tok in tokens: - if not tok: - continue - if tok.startswith('+define+'): - defines.append(tok) - elif tok.startswith('+incdir+'): - # Everything after +incdir+ is the directory path - incdirs.append(tok[len('+incdir+'):].strip()) - else: - # Treat as a file path entry - files.append(tok) - return defines, incdirs, files +INCLUDE_RE = re.compile(r'^\s*`include\s+"([^"]+)"') -def build_flist_if_missing(soc_dir: Path): - """(Re)build yosys.fl and yosys-abspath.fl from canonical filelists. - - Always reconstruct them to avoid stale or inconsistent content. - """ - fl_dir = soc_dir / 'filelist' - yosys_fl = fl_dir / 'yosys.fl' - yosys_abs = fl_dir / 'yosys-abspath.fl' - # Compose in-order lists (always rebuild to avoid historical issues) - def_fl = fl_dir / 'def.fl' - inc_fl = fl_dir / 'inc.fl' - ip_fl = fl_dir / 'ip.fl' - tech_fl = fl_dir / 'tech.fl' - top_fl = fl_dir / 'top.fl' - # Guess core from def.fl - core_name = None - if def_fl.exists(): - text = def_fl.read_text() - m = re.search(r"\+define\+CORE_([A-Z0-9_]+)", text) - if m: - core_name = m.group(1).lower() - if core_name is None: - core_name = 'picorv32' - core_fl = fl_dir / f'core_{core_name}.fl' - - rel_lines = [] - if def_fl.exists(): - rel_lines.extend(def_fl.read_text().splitlines()) - if inc_fl.exists(): - rel_lines.extend(inc_fl.read_text().splitlines()) - if ip_fl.exists(): - rel_lines.extend(ip_fl.read_text().splitlines()) - if tech_fl.exists(): - rel_lines.extend(tech_fl.read_text().splitlines()) - if core_fl.exists(): - rel_lines.extend(core_fl.read_text().splitlines()) - if top_fl.exists(): - rel_lines.extend(top_fl.read_text().splitlines()) - - # Write yosys.fl - yosys_fl.write_text('\n'.join(rel_lines) + '\n') - - # Convert to abspath list - defines, incdirs, files = read_flist(yosys_fl) - abs_lines = [] - abs_lines.extend(defines) - for d in incdirs: - p = Path(d) - if not p.is_absolute(): - p = (fl_dir / p).resolve() - abs_lines.append(f'+incdir+{p}') - for f in files: - p = Path(f) - if not p.is_absolute(): - p = (fl_dir / p).resolve() - abs_lines.append(str(p)) - yosys_abs.write_text('\n'.join(abs_lines) + '\n') - return yosys_fl, yosys_abs +def build_defines(args: argparse.Namespace) -> list[str]: + defines = [ + f"+define+PDK_{args.pdk}", + f"+define+CORE_{args.core}", + f"+define+IP_{args.ip}", + f"+define+SIMU_{args.simu}", + "+define+SYNTHESIS", + ] + if args.have_pll: + defines.append("+define+HAVE_PLL") + if args.have_sram_if: + defines.append("+define+HAVE_SRAM_IF") + if args.have_sram_macro: + defines.append("+define+HAVE_SRAM_MACRO") + if not args.have_sva: + defines.append("+define+SV_ASSRT_DISABLE") + return defines -def copy_sources_to_export(files, export_root: Path): - for f in files: - src = Path(f) - # compute path relative to repo root if possible - try: - rel = src.relative_to(REPO_ROOT) - except ValueError: - # fall back to absolute path mirroring under export - rel = Path('_abs') / src.as_posix().lstrip('/') - dst = export_root / rel - dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src, dst) +def configured_filelist(args: argparse.Namespace, generated_dir: Path) -> FileList: + names = [ + "def.fl", + "sys_def.fl", + "commonip.fl", + "inc.fl", + "clusterip.fl", + "ip.fl", + "tech.fl", + ] + if args.core in ("HAZARD3", "PICORV32"): + names.append(f"core_{args.core.lower()}.fl") + else: + dynamic_core = REPO_ROOT / "rtl/mini/mpw/.build/core/core.fl" + if not dynamic_core.is_file(): + raise FileNotFoundError("MDD core is not generated; run 'make CORE=MDD gen_mpw_code'") + names.append(str(dynamic_core)) + if args.ip == "MDD": + dynamic_ip = REPO_ROOT / "rtl/mini/mpw/.build/ip/ip.fl" + if not dynamic_ip.is_file(): + raise FileNotFoundError("MDD IP is not generated; run 'make IP=MDD gen_mpw_code'") + names.append(str(dynamic_ip)) + names.append("top.fl") + paths = [Path(name) if Path(name).is_absolute() else generated_dir / name for name in names] + return parse_filelists(paths) -def collect_includes(files, incdirs): - """Collect all directly and transitively included headers for a set of files.""" - headers = set() # store resolved absolute paths as strings - - def add_and_recurse(hpath: Path): - sp = str(hpath.resolve()) - if sp in headers: - return - headers.add(sp) - # recurse into header to discover nested includes - try: - text = hpath.read_text() - except Exception: - return - for line in text.splitlines(): - m = INCLUDE_RE.search(line) - if m: - nested = resolve_include(m.group(1), incdirs, hpath) - if nested: - add_and_recurse(nested) - - for f in files: - fp = Path(f) - if not fp.exists(): - continue - try: - text = fp.read_text() - except Exception: - continue - for line in text.splitlines(): - m = INCLUDE_RE.search(line) - if m: - name = m.group(1) - cand = resolve_include(name, incdirs, fp) - if cand: - add_and_recurse(cand) - return sorted(headers) - - -def defines_to_sv(defines): - sv_lines = [] - for d in defines: - # +define+FOO or +define+FOO=BAR - if '=' in d: - name = d[len('+define+'):].split('=')[0] - val = d[len('+define+'):].split('=')[1] - sv_lines.append(f'`define {name} {val}') - else: - name = d[len('+define+'):] - sv_lines.append(f'`define {name}') - return sv_lines - - -INCLUDE_RE = re.compile(r"`include\s+\"([^\"]+)\"") - - -def resolve_include(name: str, incdirs, current_file: Path): - # search order: incdirs then alongside current working file dir - for d in incdirs: - cand = Path(d) / name - if cand.exists(): - return cand - # fallback: relative to current file directory - cand = current_file.parent / name - if cand.exists(): - return cand +def resolve_include(name: str, incdirs: list[Path], current_file: Path) -> Path | None: + candidates = [current_file.parent / name, *(directory / name for directory in incdirs)] + for candidate in candidates: + if candidate.is_file(): + return candidate.resolve() return None -def inline_file(path: Path, incdirs, visited_headers): - out = [] - if not path.exists(): - return out - text = path.read_text() - for line in text.splitlines(): - m = INCLUDE_RE.search(line) - if m: - hdr = m.group(1) - hdr_path = resolve_include(hdr, incdirs, path) - if hdr_path: - rp = str(hdr_path.resolve()) - if rp in visited_headers: - continue - visited_headers.add(rp) - out.append(f'// `include "{hdr}" inlined from {hdr_path}') - out.extend(inline_file(hdr_path, incdirs, visited_headers)) - out.append('// end inline') - else: - # keep original include if not found - out.append(line) - else: - out.append(line) - return out +def collect_includes(files: list[Path], incdirs: list[Path]) -> list[Path]: + headers: set[Path] = set() + + def visit(path: Path) -> None: + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + match = INCLUDE_RE.search(line) + if not match: + continue + header = resolve_include(match.group(1), incdirs, path) + if header is None: + continue + if header not in headers: + headers.add(header) + visit(header) + + for source in files: + visit(source) + return sorted(headers) -def build_single_sv(defines, incdirs, files, out_path: Path): - lines = [] - # header - lines.append('// retroSoC single-file export') - lines.append(f'// Generated from {REPO_ROOT}') - # translate defines - lines.extend(defines_to_sv(defines)) - lines.append('') - visited_headers = set() - for f in files: - fp = Path(f) - lines.append(f'// ====== file: {fp} ======') - lines.extend(inline_file(fp, incdirs, visited_headers)) - lines.append('') - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text('\n'.join(lines) + '\n') +def inline_file(path: Path, incdirs: list[Path], stack: tuple[Path, ...] = ()) -> list[str]: + resolved = path.resolve() + if resolved in stack: + cycle = " -> ".join(str(item) for item in (*stack, resolved)) + raise RuntimeError(f"cyclic `include detected: {cycle}") + + output: list[str] = [] + for line in resolved.read_text(encoding="utf-8", errors="replace").splitlines(): + match = INCLUDE_RE.search(line) + if not match: + output.append(line) + continue + header = resolve_include(match.group(1), incdirs, resolved) + if header is None: + output.append(line) + continue + output.append(f'// begin `include "{match.group(1)}"') + output.extend(inline_file(header, incdirs, (*stack, resolved))) + output.append(f'// end `include "{match.group(1)}"') + return output -def main(): - ap = argparse.ArgumentParser(description='Export retroSoC sources') - ap.add_argument('mode', choices=['tar', 'sv'], help='export as tar.gz or single .sv') - ap.add_argument('--soc', default='MINI', help='SOC: MINI or TINY') - args = ap.parse_args() +def defines_to_sv(defines: list[str]) -> list[str]: + output = [] + for define in defines: + body = define[len("+define+") :] + name, separator, value = body.partition("=") + output.append(f"`define {name}{' ' + value if separator else ''}") + return output - soc = args.soc.lower() - soc_dir = REPO_ROOT / 'rtl' / soc - if not soc_dir.exists(): - print(f'Error: SOC directory not found: {soc_dir}', file=sys.stderr) - sys.exit(1) - # ensure flists are present or build them - _, yosys_abs = build_flist_if_missing(soc_dir) - defines, incdirs, files = read_flist(yosys_abs) +def write_single_sv(filelist: FileList, output: Path) -> None: + lines = ["// Generated by syn/tools/export_soc_sources.py", *defines_to_sv(filelist.defines), ""] + for source in [*filelist.library_files, *filelist.files]: + lines.append(f"// ===== file: {source.relative_to(REPO_ROOT)} =====") + lines.extend(inline_file(source, filelist.incdirs)) + lines.append("") + atomic_write(output, "\n".join(lines) + "\n") - export_dir = REPO_ROOT / 'export' - export_dir.mkdir(exist_ok=True) - if args.mode == 'tar': - bundle_root = export_dir / 'rtl' - if bundle_root.exists(): - shutil.rmtree(bundle_root) - bundle_root.mkdir(parents=True) - # copy sources - copy_sources_to_export(files, bundle_root) - # copy headers referenced by sources - hdrs = collect_includes(files, incdirs) - copy_sources_to_export(hdrs, bundle_root) - # write manifests (absolute and relative-to-bundle) - abs_manifest = '\n'.join([*defines, *[f'+incdir+{d}' for d in incdirs], *files]) + '\n' - (bundle_root / 'filelist_abspath.fl').write_text(abs_manifest) - # relative manifest for portability - def to_bundle_rel(p: Path): - try: - rel = p.resolve().relative_to(REPO_ROOT) - except Exception: - rel = Path('_abs') / p.as_posix().lstrip('/') - return rel - rel_incdirs = [f'+incdir+{to_bundle_rel(Path(d))}' for d in incdirs] - rel_files = [str(to_bundle_rel(Path(f))) for f in files] - (bundle_root / 'filelist_rel.fl').write_text('\n'.join([*defines, *rel_incdirs, *rel_files]) + '\n') - # create tar.gz - tar_path = export_dir / f'retrosoc_{soc}_sources.tar.gz' - with tarfile.open(tar_path, 'w:gz') as tf: - tf.add(bundle_root, arcname='rtl') - print(f'Generated: {tar_path}') +def bundle_relative(path: Path) -> Path: + try: + relative = path.resolve().relative_to(REPO_ROOT) + except ValueError: + return Path("_abs") / path.as_posix().lstrip("/") + if relative.parts and relative.parts[0] == "rtl": + return Path(*relative.parts[1:]) + return Path("repo") / relative + + +def write_tar(filelist: FileList, export_dir: Path, soc: str) -> Path: + bundle = export_dir / "rtl" + if bundle.exists(): + shutil.rmtree(bundle) + bundle.mkdir(parents=True) + + sources = [*filelist.library_files, *filelist.files] + for source in [*sources, *collect_includes(sources, filelist.incdirs)]: + destination = bundle / bundle_relative(source) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + manifest = [*filelist.defines] + manifest.extend(f"+incdir+{bundle_relative(path)}" for path in filelist.incdirs) + manifest.extend(str(bundle_relative(path)) for path in sources) + atomic_write(bundle / "filelist.fl", "\n".join(manifest) + "\n") + + tar_path = export_dir / f"retrosoc_{soc.lower()}_sources.tar.gz" + with tarfile.open(tar_path, "w:gz") as archive: + archive.add(bundle, arcname="rtl") + return tar_path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Export configured retroSoC RTL sources") + parser.add_argument("mode", choices=("tar", "sv")) + parser.add_argument("--soc", default="MINI", choices=("MINI",)) + parser.add_argument("--pdk", default="IHP130", choices=("IHP130", "ICS55", "SKY130", "GF180")) + parser.add_argument("--core", default="HAZARD3", choices=("HAZARD3", "PICORV32", "MDD")) + parser.add_argument("--ip", default="NONE", choices=("NONE", "MDD")) + parser.add_argument("--simu", default="VCS", choices=("VCS", "VERILATOR", "IVERILOG")) + parser.add_argument("--have-pll", action="store_true") + parser.add_argument("--have-sram-if", action="store_true") + parser.add_argument("--have-sram-macro", action="store_true") + parser.add_argument("--have-sva", action="store_true") + parser.add_argument("--output-dir", type=Path, default=REPO_ROOT / "export") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + with tempfile.TemporaryDirectory(prefix="retrosoc-filelists-") as temporary: + generated_dir = Path(temporary) + generate_all(generated_dir, build_defines(args)) + filelist = configured_filelist(args, generated_dir) + + export_dir = args.output_dir.resolve() + export_dir.mkdir(parents=True, exist_ok=True) + if args.mode == "sv": + output = export_dir / "retrosoc_asic_sources.sv" + write_single_sv(filelist, output) else: - sv_path = export_dir / f'retrosoc_asic_sources.sv' - build_single_sv(defines, incdirs, files, sv_path) - print(f'Generated: {sv_path}') + output = write_tar(filelist, export_dir, args.soc) + print(f"generated: {output}") + return 0 -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/syn/yosys/script/common.tcl b/syn/yosys/script/common.tcl index cdc9cc5a..335b8d80 100644 --- a/syn/yosys/script/common.tcl +++ b/syn/yosys/script/common.tcl @@ -12,6 +12,9 @@ set variables { vlog_files { VLOG_FILES "" } sv_flist { SV_FLIST "" } pdk { PDK "" } + soc { SOC "" } + core { CORE "" } + ip { IP "" } top_design { TOP_DESIGN "" } hier_depth { HIER_DEPTH 0 } period_ps { YOSYS_TARGET_PERIOD_PS 5000 } @@ -20,6 +23,7 @@ set variables { work_dir { WORK "[set dir [pwd]]/WORK" } report_dir { REPORTS "[set dir [pwd]]/report" } netlist { NETLIST "" } + config { CONFIG "" } } # either use env-var or default to fallback @@ -40,6 +44,9 @@ foreach var [dict keys $variables] { if {[string eq $netlist ""]} { set netlist ${build_dir}/${top_design}_netlist.v } +if {[string eq $config ""]} { + set config ${netlist}.config +} proc envVarValid {var_name} { if { [info exists ::env($var_name)]} { @@ -64,4 +71,4 @@ proc processAbcScript {abc_script} { flush $abc_out close $abc_out return $abc_out_path -} \ No newline at end of file +} diff --git a/syn/yosys/script/synth.tcl b/syn/yosys/script/synth.tcl index 0771b6c5..47cf7928 100644 --- a/syn/yosys/script/synth.tcl +++ b/syn/yosys/script/synth.tcl @@ -176,3 +176,13 @@ yosys tee -q -o "${report_dir}/${proj_name}_area_logic.rpt" stat -top $top_desig # final netlist yosys write_verilog -noattr -noexpr -nohex -nodec $netlist +# Record the configuration only after the netlist has been written successfully. +set config_tmp "${config}.tmp" +set config_file [open $config_tmp "w"] +puts $config_file "PDK=$pdk" +puts $config_file "SOC=$soc" +puts $config_file "CORE=$core" +puts $config_file "IP=$ip" +puts $config_file "TOP_DESIGN=$top_design" +close $config_file +file rename -force $config_tmp $config diff --git a/syn/yosys/yosys.mk b/syn/yosys/yosys.mk index 14de67c1..167d5cc8 100644 --- a/syn/yosys/yosys.mk +++ b/syn/yosys/yosys.mk @@ -29,41 +29,38 @@ include $(YOSYS_DIR)/synth_config.mk TOP_DESIGN ?= retrosoc_asic RTL_NAME ?= retrosoc_asic -SV_FLIST := $(YOSYS_DIR)/../../rtl/$(shell echo $(SOC) | tr A-Z a-z )/.generated_fl/yosys.fl - -$(info SYNTH SV_FLIST PATH: $(SV_FLIST)) +SV_FLIST := $(RTL_PATH)/.generated_fl/yosys.fl NETLIST := $(YOSYS_OUT)/$(RTL_NAME)_yosys.v -NETLIST_DEBUG := $(YOSYS_OUT)/$(RTL_NAME)_debug_yosys.v +NETLIST_DEBUG := $(YOSYS_OUT)/$(RTL_NAME)_yosys_debug.v +NETLIST_CONFIG := $(YOSYS_OUT)/$(RTL_NAME)_yosys.config gen_synth_filelist: gen_mpw_code generate_filelist - $(info SYNTH RTL_FLIST: $(RTL_FLIST)) - @python3 $(RTL_PATH)/script/comb.py $(RTL_FLIST) + @python3 $(RTL_PATH)/script/comb.py $(RTL_FLIST) --output $(SV_FLIST) ## Synthesize netlist using Yosys -synth: gen_synth_filelist $(NETLIST) +synth: $(NETLIST) -$(NETLIST) $(NETLIST_DEBUG): +$(NETLIST): gen_synth_filelist @mkdir -p $(YOSYS_OUT) @mkdir -p $(YOSYS_TMP) @mkdir -p $(YOSYS_RPT) - PDK="$(PDK)" \ - SV_FLIST="$(SV_FLIST)" \ - TOP_DESIGN="$(TOP_DESIGN)" \ - PROJ_NAME="$(RTL_NAME)" \ - WORK="$(YOSYS_TMP)" \ - BUILD="$(YOSYS_OUT)" \ - REPORTS="$(YOSYS_RPT)" \ - NETLIST="$(NETLIST)" \ - yosys -c $(YOSYS_DIR)/script/synth.tcl \ + export PDK="$(PDK)" SOC="$(SOC)" CORE="$(CORE)" IP="$(IP)" \ + SV_FLIST="$(SV_FLIST)" TOP_DESIGN="$(TOP_DESIGN)" CONFIG="$(NETLIST_CONFIG)" \ + PROJ_NAME="$(RTL_NAME)" WORK="$(YOSYS_TMP)" BUILD="$(YOSYS_OUT)" \ + REPORTS="$(YOSYS_RPT)" NETLIST="$(NETLIST)"; \ + set -o pipefail; yosys -c $(YOSYS_DIR)/script/synth.tcl \ 2>&1 | TZ=UTC-8 gawk '{ print strftime("[%Y-%m-%d %H:%M %Z]"), $$0 }' \ | tee "$(YOSYS_BUILD)/$(RTL_NAME).log" \ - | gawk -f $(YOSYS_DIR)/script/filter_output.awk; + | gawk -f $(YOSYS_DIR)/script/filter_output.awk + +$(NETLIST_DEBUG): $(NETLIST) + @test -f $@ synth_clean: rm -rf $(YOSYS_OUT) rm -rf $(YOSYS_TMP) rm -rf $(YOSYS_RPT) - rm -f $(YOSYS_DIR)/$(RTL_NAME).log + rm -f $(YOSYS_BUILD)/$(RTL_NAME).log -.PHONY: synth_clean yosys synth $(NETLIST) $(NETLIST_DEBUG) +.PHONY: gen_synth_filelist synth_clean synth diff --git a/tests/test_script_tools.py b/tests/test_script_tools.py new file mode 100644 index 00000000..5e9caea0 --- /dev/null +++ b/tests/test_script_tools.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import hashlib +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILELIST_SCRIPT_DIR = ROOT / "rtl/mini/script" +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(FILELIST_SCRIPT_DIR)) + +from filelist import FileList, atomic_write, parse_filelists, write_filelist # noqa: E402 +from generate_filelist import generate_all # noqa: E402 +from scripts.setup_helpers import download_file, ensure_git_repo # noqa: E402 + + +def run(*command: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + cwd=cwd or ROOT, + text=True, + capture_output=True, + check=True, + ) + + +def test_atomic_write_preserves_unchanged_mtime(tmp_path: Path) -> None: + output = tmp_path / "output.fl" + assert atomic_write(output, "same\n") is True + first_mtime = output.stat().st_mtime_ns + assert atomic_write(output, "same\n") is False + assert output.stat().st_mtime_ns == first_mtime + + +def test_nested_filelist_and_space_path_round_trip(tmp_path: Path) -> None: + include_dir = tmp_path / "include files" + include_dir.mkdir() + source = tmp_path / "source file.sv" + library = tmp_path / "library file.v" + source.write_text("module source_file; endmodule\n", encoding="utf-8") + library.write_text("module library_file; endmodule\n", encoding="utf-8") + + nested = tmp_path / "nested.fl" + nested.write_text( + f'"{source}"\n-v "{library}"\n', + encoding="utf-8", + ) + top = tmp_path / "top.fl" + top.write_text( + f'+define+TEST=1\n"+incdir+{include_dir}"\n-f "{nested}"\n', + encoding="utf-8", + ) + + parsed = parse_filelists([top]) + assert parsed.defines == ["+define+TEST=1"] + assert parsed.incdirs == [include_dir.resolve()] + assert parsed.files == [source.resolve()] + assert parsed.library_files == [library.resolve()] + + output = tmp_path / "round trip.fl" + write_filelist(output, parsed) + assert parse_filelists([output]) == parsed + + +def test_generate_all_is_stable_and_expands_paths(tmp_path: Path) -> None: + defines = ["+define+PDK_IHP130", "+define+CORE_HAZARD3"] + generated = generate_all(tmp_path, defines) + mtimes = {path: path.stat().st_mtime_ns for path in generated} + generate_all(tmp_path, defines) + assert {path: path.stat().st_mtime_ns for path in generated} == mtimes + assert (tmp_path / "def.fl").read_text(encoding="utf-8") == " ".join(defines) + "\n" + cluster = (tmp_path / "clusterip.fl").read_text(encoding="utf-8") + assert str(ROOT / "rtl/clusterip") in cluster + + +def test_prepare_norflash_and_missing_firmware(tmp_path: Path) -> None: + models = tmp_path / "models" + models.mkdir() + for name in ("SECSI.TXT", "SFDP.TXT", "SREG.TXT"): + (models / name).write_text(name, encoding="utf-8") + firmware = tmp_path / "firmware.hex" + firmware.write_text("00\n", encoding="utf-8") + sim_dir = tmp_path / "sim" + script = ROOT / "rtl/mini/script/prepare_norflash.py" + + run( + sys.executable, + str(script), + "--sim-dir", + str(sim_dir), + "--models-dir", + str(models), + "--firmware", + str(firmware), + ) + assert (sim_dir / "MEM.TXT").resolve() == firmware.resolve() + assert (sim_dir / "SFDP.TXT").read_text(encoding="utf-8") == "SFDP.TXT" + + firmware.unlink() + result = subprocess.run( + [ + sys.executable, + str(script), + "--sim-dir", + str(sim_dir), + "--models-dir", + str(models), + "--firmware", + str(firmware), + ], + text=True, + capture_output=True, + ) + assert result.returncode != 0 + assert "firmware image not found" in result.stderr + + +def test_dependency_helpers_are_idempotent(tmp_path: Path) -> None: + source = tmp_path / "source" + run("git", "init", str(source)) + run("git", "config", "user.email", "test@example.com", cwd=source) + run("git", "config", "user.name", "Test", cwd=source) + run("git", "config", "commit.gpgsign", "false", cwd=source) + (source / "dependency.txt").write_text("pinned\n", encoding="utf-8") + run("git", "add", "dependency.txt", cwd=source) + run("git", "commit", "-m", "pinned", cwd=source) + revision = run("git", "rev-parse", "HEAD", cwd=source).stdout.strip() + + destination = tmp_path / "checkout" + ensure_git_repo(str(source), destination, revision) + first_head = run("git", "rev-parse", "HEAD", cwd=destination).stdout.strip() + ensure_git_repo(str(source), destination, revision) + assert run("git", "rev-parse", "HEAD", cwd=destination).stdout.strip() == first_head + + payload = tmp_path / "payload.bin" + payload.write_bytes(b"verified") + digest = hashlib.sha256(payload.read_bytes()).hexdigest() + downloaded = tmp_path / "downloaded.bin" + download_file(payload.as_uri(), downloaded, digest) + first_mtime = downloaded.stat().st_mtime_ns + download_file(payload.as_uri(), downloaded, digest) + assert downloaded.stat().st_mtime_ns == first_mtime + + +def test_make_dry_run_and_validation_do_not_write_filelists() -> None: + generated_def = ROOT / "rtl/mini/.generated_fl/def.fl" + before = generated_def.stat().st_mtime_ns if generated_def.exists() else None + run("make", "-n", "help") + run("make", "-n", "SIMU=IVERILOG", "comp") + after = generated_def.stat().st_mtime_ns if generated_def.exists() else None + assert after == before + + invalid = subprocess.run( + ["make", "SIMU=UNKNOWN", "help"], + cwd=ROOT, + text=True, + capture_output=True, + ) + assert invalid.returncode != 0 + assert "Invalid SIMU='UNKNOWN'" in invalid.stderr + + +def test_clean_all_stays_within_repository(tmp_path: Path) -> None: + fake_root = tmp_path / "repository" + (fake_root / ".git").mkdir(parents=True) + (fake_root / "Makefile").write_text("all:\n", encoding="utf-8") + generated = fake_root / "rtl/mini/.iverilog_build" + generated.mkdir(parents=True) + (generated / "simv").write_text("generated", encoding="utf-8") + dependency = fake_root / "rtl/ip/3rd-party" + dependency.mkdir(parents=True) + (dependency / "model.v").write_text("dependency", encoding="utf-8") + + run( + sys.executable, + str(ROOT / "scripts/clean.py"), + "--root", + str(fake_root), + ) + assert not generated.exists() + assert (dependency / "model.v").is_file()