From c8187284d020ff0a02ed2be3632c60a73fa71734 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Sun, 27 Sep 2026 08:48:12 +0800 Subject: [PATCH] feat(tiny): add scripts for SDC generation and packaging - Introduced to create pad-aware timing constraints for the Tiny IHP130 chip flow. - Added to create a checksummed delivery archive for the Tiny LibreLane implementation. - Created for synthesis-only header of the locked IHP SRAM macro physical views. - Implemented to limit OpenSTA report enumeration for path counts. - Updated to include power pad definitions. - Enhanced to handle power pad counts and validation. - Added tests for Tiny flow and pin map validation, including power pad overrides and constraints. --- Makefile | 6 +- physical/README.md | 4 +- physical/librelane/README.md | 57 +--- physical/librelane/{ => mini}/Makefile | 8 +- physical/librelane/mini/README.md | 52 +++ physical/librelane/{ => mini}/pad_cfg.tcl | 0 physical/librelane/{ => mini}/pdn_cfg.tcl | 0 .../librelane/{ => mini}/scripts/doctor.py | 2 +- .../scripts/generate_chip_config.py | 6 +- .../{ => mini}/scripts/generate_sdc.py | 4 +- .../librelane/{ => mini}/scripts/package.py | 0 .../librelane/{ => mini}/sram_blackboxes.vh | 0 .../librelane/{ => mini}/sta_report_limit.tcl | 0 physical/librelane/tiny/Makefile | 133 ++++++++ physical/librelane/tiny/README.md | 52 +++ physical/librelane/tiny/pad_cfg.tcl | 17 + physical/librelane/tiny/pdn_cfg.tcl | 193 ++++++++++++ physical/librelane/tiny/scripts/doctor.py | 165 ++++++++++ .../tiny/scripts/generate_chip_config.py | 295 ++++++++++++++++++ .../librelane/tiny/scripts/generate_sdc.py | 172 ++++++++++ physical/librelane/tiny/scripts/package.py | 76 +++++ physical/librelane/tiny/sram_blackboxes.vh | 25 ++ physical/librelane/tiny/sta_report_limit.tcl | 35 +++ physical/pdk/README.md | 3 +- rtl/tiny/pin_map/pin_map.json | 6 + scripts/rtl/generate_pin_map.py | 41 ++- tests/test_librelane_flow.py | 10 +- tests/test_librelane_tiny_flow.py | 175 +++++++++++ tests/test_pin_map.py | 46 +++ 29 files changed, 1520 insertions(+), 63 deletions(-) rename physical/librelane/{ => mini}/Makefile (96%) create mode 100644 physical/librelane/mini/README.md rename physical/librelane/{ => mini}/pad_cfg.tcl (100%) rename physical/librelane/{ => mini}/pdn_cfg.tcl (100%) rename physical/librelane/{ => mini}/scripts/doctor.py (99%) rename physical/librelane/{ => mini}/scripts/generate_chip_config.py (98%) rename physical/librelane/{ => mini}/scripts/generate_sdc.py (98%) rename physical/librelane/{ => mini}/scripts/package.py (100%) rename physical/librelane/{ => mini}/sram_blackboxes.vh (100%) rename physical/librelane/{ => mini}/sta_report_limit.tcl (100%) create mode 100644 physical/librelane/tiny/Makefile create mode 100644 physical/librelane/tiny/README.md create mode 100644 physical/librelane/tiny/pad_cfg.tcl create mode 100644 physical/librelane/tiny/pdn_cfg.tcl create mode 100644 physical/librelane/tiny/scripts/doctor.py create mode 100644 physical/librelane/tiny/scripts/generate_chip_config.py create mode 100644 physical/librelane/tiny/scripts/generate_sdc.py create mode 100644 physical/librelane/tiny/scripts/package.py create mode 100644 physical/librelane/tiny/sram_blackboxes.vh create mode 100644 physical/librelane/tiny/sta_report_limit.tcl create mode 100644 tests/test_librelane_tiny_flow.py diff --git a/Makefile b/Makefile index ae98aab7..3e4e3a31 100644 --- a/Makefile +++ b/Makefile @@ -431,7 +431,11 @@ ifeq ($(STA), OPENSTA) include physical/smoke/sta/opensta/opensta.mk endif -include physical/librelane/Makefile +ifeq ($(SOC),TINY) +include physical/librelane/tiny/Makefile +else +include physical/librelane/mini/Makefile +endif include physical/ecc/Makefile .PHONY: crypto-p0-constants crypto-p0-rtl crypto-p0-baseline crypto-p0-report diff --git a/physical/README.md b/physical/README.md index f5f85a29..4c0642c1 100644 --- a/physical/README.md +++ b/physical/README.md @@ -7,7 +7,9 @@ drivers. - `pdk/` contains the locked PDK setup helpers and managed PDK checkouts. - `smoke/syn/` contains Yosys synthesis and source-export integration. - `smoke/sta/` contains OpenSTA constraints and timing integration. -- `librelane/` contains the open-source IHP130 core-hardening and full-chip pad-ring flows. +- `librelane/` contains the open-source IHP130 implementation flows, organized + per product line: `librelane/mini/` (core hardening and full-chip pad ring) + and `librelane/tiny/` (Tiny full-chip pad ring). - `ecc/` contains the open-source ICS55 padless-core hardening flow backed by the locked ECOS Chip Compiler release. - `commercial/` contains licensed-tool orchestration and flow logic. PDKs, diff --git a/physical/librelane/README.md b/physical/librelane/README.md index eb7bcf54..0e40a59a 100644 --- a/physical/librelane/README.md +++ b/physical/librelane/README.md @@ -1,50 +1,27 @@ # LibreLane IHP130 Flows -This directory owns the open-source IHP130 implementation entry points for two -independent `retrosoc` deliverables: +This directory groups the open-source LibreLane/IHP130 implementation flows by +product line: -- `librelane-core` hardens `retrosoc_core`, a padless logical macro with the - canonical external pin contract and the three USB packet SRAM macros. -- `librelane-chip` implements the bare-die `retrosoc_asic` package-facing SoC, - including its pad ring, bondpads, fillers, corners, seal ring, and the same - SRAM macros. +- [`mini/`](mini/README.md) hardens the Mini SoC deliverables (`retrosoc_core` + padless macro and the bare-die `retrosoc_asic` pad-ring chip) for + `SOC=MINI` profiles such as `configs/ci/ihp130.mk`. +- [`tiny/`](tiny/README.md) implements the bare-die Tiny MCU + (`retrosoc_tiny_asic`, pad-ring chip only) for `SOC=TINY` with + `configs/ci/ihp130-tiny.mk`. +- `bondpad/` holds the shared 70 um bondpad LEF collateral used by both chip + flows. -These are separate single-level runs. The chip flow does not consume the core -run as a hierarchical macro, so either artifact can be developed and reviewed -without a two-level integration dependency. - -The flow consumes the committed `configs/ci/ihp130.mk` profile, canonical SoC -pin map, and IHP-Open-PDK revision in `dependencies/dependencies.lock.json`. -LibreLane reads the upstream `ihp-sg13g2/libs.tech/librelane` configuration -directly through manual-PDK mode. Generated RTL, SDC, configuration, run -databases, reports, and final views are written below the selected -`build//physical/librelane/{core,chip}/` directory. - -Use the LibreLane 3.0.5 environment supplied by the development shell: - -```sh -make CONFIG=configs/ci/ihp130.mk librelane-doctor -make CONFIG=configs/ci/ihp130.mk librelane-core -make CONFIG=configs/ci/ihp130.mk librelane-chip -make CONFIG=configs/ci/ihp130.mk librelane-core-package -make CONFIG=configs/ci/ihp130.mk librelane-package -``` - -The chip target places all 111 signal pads, 80 core/IO power and ground pads, -four corners, I/O fillers, and 70 um bondpads. It is a bare-die plan, not a -QFN/BGA package definition. The SDC constrains the external clock at 72 MHz; -mid-PnR timing report passes are skipped because LibreLane 3.0.5 emits 1000 -expanded paths per group, while final post-RCX multi-corner STA remains enabled. +The top-level `Makefile` includes `mini/Makefile` or `tiny/Makefile` according +to `SOC`, so the target names (`librelane-doctor`, `librelane-chip`, +`librelane-package`, and the Mini-only `librelane-core`) are uniform across +product lines. +Both lines require the LibreLane 3.0.5 development environment, the locked +IHP-Open-PDK checkout under `physical/pdk/`, and the locked OpenSTA 3.0.0. `PDK=GF180`, `PDK=SKY130`, and `PDK=ICS55` are intentionally rejected until their technology-specific LibreLane adapters, pad cells, macro collateral, and -signoff decks are qualified. The target split is technology-neutral so those -adapters can reuse the core/chip interface when they are ready. - -IHP SG13G2 does not currently provide pad cells implementing retroSoC's -programmable pull-up, pull-down, or Schmitt controls. The IHP implementation -uses input-only, 4 mA output, and 4 mA bidirectional cells; unsupported GPIO -pad-control features must not be represented as production-qualified behavior. +signoff decks are qualified. A successful open-source run is evidence for implementation development, not a foundry production-signoff claim. Release review still requires qualified diff --git a/physical/librelane/Makefile b/physical/librelane/mini/Makefile similarity index 96% rename from physical/librelane/Makefile rename to physical/librelane/mini/Makefile index d15e4ea2..97b94091 100644 --- a/physical/librelane/Makefile +++ b/physical/librelane/mini/Makefile @@ -1,11 +1,11 @@ -LIBRELANE_FLOW_ROOT := $(ROOT_PATH)/physical/librelane +LIBRELANE_FLOW_ROOT := $(ROOT_PATH)/physical/librelane/mini LIBRELANE_TARGET ?= chip -LIBRELANE_RUN_ROOT := $(VARIANT_ROOT)/physical/librelane/$(LIBRELANE_TARGET) +LIBRELANE_RUN_ROOT := $(VARIANT_ROOT)/physical/librelane/mini/$(LIBRELANE_TARGET) LIBRELANE_INPUT_DIR := $(LIBRELANE_RUN_ROOT)/input LIBRELANE_RTL := $(LIBRELANE_INPUT_DIR)/retrosoc_asic_sources.sv LIBRELANE_SDC := $(LIBRELANE_INPUT_DIR)/retrosoc_$(LIBRELANE_TARGET).sdc LIBRELANE_BONDPAD_GDS := $(LIBRELANE_INPUT_DIR)/bondpad_70x70.gds -LIBRELANE_BONDPAD_LEF := $(LIBRELANE_FLOW_ROOT)/bondpad/bondpad_70x70.lef +LIBRELANE_BONDPAD_LEF := $(ROOT_PATH)/physical/librelane/bondpad/bondpad_70x70.lef LIBRELANE_CONFIG := $(LIBRELANE_RUN_ROOT)/config.json LIBRELANE_LOG := $(LIBRELANE_RUN_ROOT)/librelane.log LIBRELANE_RESULT := $(LIBRELANE_RUN_ROOT)/result.json @@ -65,7 +65,7 @@ endif librelane-check-pdk: @test '$(PDK)' = IHP130 || { \ - echo 'physical/librelane currently implements only PDK=IHP130' >&2; exit 2; } + echo 'physical/librelane/mini currently implements only PDK=IHP130' >&2; exit 2; } @test '$(HAVE_PLL)' = NO || { \ echo 'the IHP130 LibreLane flow has no qualified crystal PAD' >&2; exit 2; } diff --git a/physical/librelane/mini/README.md b/physical/librelane/mini/README.md new file mode 100644 index 00000000..9e51a651 --- /dev/null +++ b/physical/librelane/mini/README.md @@ -0,0 +1,52 @@ +# Mini LibreLane IHP130 Flows + +This directory owns the Mini product line's open-source IHP130 implementation +entry points for two independent `retrosoc` deliverables: + +- `librelane-core` hardens `retrosoc_core`, a padless logical macro with the + canonical external pin contract and the three USB packet SRAM macros. +- `librelane-chip` implements the bare-die `retrosoc_asic` package-facing SoC, + including its pad ring, bondpads, fillers, corners, seal ring, and the same + SRAM macros. + +These are separate single-level runs. The chip flow does not consume the core +run as a hierarchical macro, so either artifact can be developed and reviewed +without a two-level integration dependency. + +The flow consumes the committed `configs/ci/ihp130.mk` profile, canonical SoC +pin map, and IHP-Open-PDK revision in `dependencies/dependencies.lock.json`. +LibreLane reads the upstream `ihp-sg13g2/libs.tech/librelane` configuration +directly through manual-PDK mode. Generated RTL, SDC, configuration, run +databases, reports, and final views are written below the selected +`build//physical/librelane/mini/{core,chip}/` directory. + +Use the LibreLane 3.0.5 environment supplied by the development shell: + +```sh +make CONFIG=configs/ci/ihp130.mk librelane-doctor +make CONFIG=configs/ci/ihp130.mk librelane-core +make CONFIG=configs/ci/ihp130.mk librelane-chip +make CONFIG=configs/ci/ihp130.mk librelane-core-package +make CONFIG=configs/ci/ihp130.mk librelane-package +``` + +The chip target places all 111 signal pads, 80 core/IO power and ground pads, +four corners, I/O fillers, and 70 um bondpads. It is a bare-die plan, not a +QFN/BGA package definition. The SDC constrains the external clock at 72 MHz; +mid-PnR timing report passes are skipped because LibreLane 3.0.5 emits 1000 +expanded paths per group, while final post-RCX multi-corner STA remains enabled. + +`PDK=GF180`, `PDK=SKY130`, and `PDK=ICS55` are intentionally rejected until +their technology-specific LibreLane adapters, pad cells, macro collateral, and +signoff decks are qualified. The target split is technology-neutral so those +adapters can reuse the core/chip interface when they are ready. + +IHP SG13G2 does not currently provide pad cells implementing retroSoC's +programmable pull-up, pull-down, or Schmitt controls. The IHP implementation +uses input-only, 4 mA output, and 4 mA bidirectional cells; unsupported GPIO +pad-control features must not be represented as production-qualified behavior. + +A successful open-source run is evidence for implementation development, not a +foundry production-signoff claim. Release review still requires qualified +foundry decks, package and bond planning, electrical/ESD review, and approved +waivers. diff --git a/physical/librelane/pad_cfg.tcl b/physical/librelane/mini/pad_cfg.tcl similarity index 100% rename from physical/librelane/pad_cfg.tcl rename to physical/librelane/mini/pad_cfg.tcl diff --git a/physical/librelane/pdn_cfg.tcl b/physical/librelane/mini/pdn_cfg.tcl similarity index 100% rename from physical/librelane/pdn_cfg.tcl rename to physical/librelane/mini/pdn_cfg.tcl diff --git a/physical/librelane/scripts/doctor.py b/physical/librelane/mini/scripts/doctor.py similarity index 99% rename from physical/librelane/scripts/doctor.py rename to physical/librelane/mini/scripts/doctor.py index 80cda416..73b65449 100755 --- a/physical/librelane/scripts/doctor.py +++ b/physical/librelane/mini/scripts/doctor.py @@ -11,7 +11,7 @@ import sys from pathlib import Path -ROOT = Path(__file__).resolve().parents[3] +ROOT = Path(__file__).resolve().parents[4] sys.path.insert(0, str(ROOT)) from librelane.flows import Flow # noqa: E402 from scripts.rtl.generate_pin_map import IHP130_POWER_PAD_COUNTS # noqa: E402 diff --git a/physical/librelane/scripts/generate_chip_config.py b/physical/librelane/mini/scripts/generate_chip_config.py similarity index 98% rename from physical/librelane/scripts/generate_chip_config.py rename to physical/librelane/mini/scripts/generate_chip_config.py index 49ca2c5f..b1370d4f 100755 --- a/physical/librelane/scripts/generate_chip_config.py +++ b/physical/librelane/mini/scripts/generate_chip_config.py @@ -11,7 +11,7 @@ import sys from pathlib import Path -ROOT = Path(__file__).resolve().parents[3] +ROOT = Path(__file__).resolve().parents[4] sys.path.insert(0, str(ROOT)) from scripts.rtl.generate_pin_map import ( # noqa: E402 IHP130_POWER_PAD_COUNTS, @@ -206,7 +206,7 @@ def build_config(args: argparse.Namespace) -> dict[str, object]: "SIGNOFF_SDC_FILE": config_path(args.sdc, config_dir), "FALLBACK_SDC": config_path(args.sdc, config_dir), "STA_EXTRA_CORNER_TCL_FILE": config_path( - ROOT / "physical/librelane/sta_report_limit.tcl", config_dir + ROOT / "physical/librelane/mini/sta_report_limit.tcl", config_dir ), "CLOCK_PORT": list(CLOCK_PORTS), "CLOCK_PERIOD": 1_000_000_000 / args.ext_clk_hz, @@ -259,7 +259,7 @@ def build_config(args: argparse.Namespace) -> dict[str, object]: "CORE_AREA": [365, 365, 7635, 7635], "PL_TARGET_DENSITY_PCT": 45, "PDN_CORE_RING_CONNECT_TO_PADS": True, - "PAD_CFG": config_path(ROOT / "physical/librelane/pad_cfg.tcl", config_dir), + "PAD_CFG": config_path(ROOT / "physical/librelane/mini/pad_cfg.tcl", config_dir), "PAD_BONDPAD_NAME": "bondpad_70x70", "EXTRA_GDS": [config_path(args.bondpad_gds, config_dir)], "EXTRA_LEFS": [config_path(args.bondpad_lef, config_dir)], diff --git a/physical/librelane/scripts/generate_sdc.py b/physical/librelane/mini/scripts/generate_sdc.py similarity index 98% rename from physical/librelane/scripts/generate_sdc.py rename to physical/librelane/mini/scripts/generate_sdc.py index 027648ec..17485866 100755 --- a/physical/librelane/scripts/generate_sdc.py +++ b/physical/librelane/mini/scripts/generate_sdc.py @@ -9,7 +9,7 @@ import sys from pathlib import Path -ROOT = Path(__file__).resolve().parents[3] +ROOT = Path(__file__).resolve().parents[4] sys.path.insert(0, str(ROOT)) from scripts.rtl.generate_pin_map import Pad, ihp130_pad_instance, read_map # noqa: E402 from scripts.check_clock_reset_domains import validate # noqa: E402 @@ -91,7 +91,7 @@ def render(args: argparse.Namespace) -> str: groups[clock]["outputs"].append(pad.name) lines = [ - "# Generated by physical/librelane/scripts/generate_sdc.py; do not edit.", + "# Generated by physical/librelane/mini/scripts/generate_sdc.py; do not edit.", "current_design $::env(DESIGN_NAME)", "set_units -time ns", "", diff --git a/physical/librelane/scripts/package.py b/physical/librelane/mini/scripts/package.py similarity index 100% rename from physical/librelane/scripts/package.py rename to physical/librelane/mini/scripts/package.py diff --git a/physical/librelane/sram_blackboxes.vh b/physical/librelane/mini/sram_blackboxes.vh similarity index 100% rename from physical/librelane/sram_blackboxes.vh rename to physical/librelane/mini/sram_blackboxes.vh diff --git a/physical/librelane/sta_report_limit.tcl b/physical/librelane/mini/sta_report_limit.tcl similarity index 100% rename from physical/librelane/sta_report_limit.tcl rename to physical/librelane/mini/sta_report_limit.tcl diff --git a/physical/librelane/tiny/Makefile b/physical/librelane/tiny/Makefile new file mode 100644 index 00000000..63fa0ebd --- /dev/null +++ b/physical/librelane/tiny/Makefile @@ -0,0 +1,133 @@ +LIBRELANE_FLOW_ROOT := $(ROOT_PATH)/physical/librelane/tiny +LIBRELANE_RUN_ROOT := $(VARIANT_ROOT)/physical/librelane/tiny/chip +LIBRELANE_INPUT_DIR := $(LIBRELANE_RUN_ROOT)/input +LIBRELANE_RTL := $(LIBRELANE_INPUT_DIR)/retrosoc_tiny_asic_sources.sv +LIBRELANE_SDC := $(LIBRELANE_INPUT_DIR)/retrosoc_tiny_chip.sdc +LIBRELANE_BONDPAD_GDS := $(LIBRELANE_INPUT_DIR)/bondpad_70x70.gds +LIBRELANE_BONDPAD_LEF := $(ROOT_PATH)/physical/librelane/bondpad/bondpad_70x70.lef +LIBRELANE_CONFIG := $(LIBRELANE_RUN_ROOT)/config.json +LIBRELANE_LOG := $(LIBRELANE_RUN_ROOT)/librelane.log +LIBRELANE_RESULT := $(LIBRELANE_RUN_ROOT)/result.json +LIBRELANE_META_RESULT := $(META_DIR)/librelane-chip.json +LIBRELANE_DOCTOR_RESULT := $(META_DIR)/librelane-chip-doctor.json +LIBRELANE_FINAL_DIR := $(LIBRELANE_RUN_ROOT)/final +LIBRELANE_ARCHIVE := $(LIBRELANE_RUN_ROOT)/retrosoc-tiny-ihp130-chip.tar.gz +LIBRELANE_RUN_TAG ?= current +LIBRELANE_PDK_ROOT := $(ROOT_PATH)/physical/pdk/IHP-Open-PDK +LIBRELANE_PDK := ihp-sg13g2 +LIBRELANE_SCL := sg13g2_stdcell +LIBRELANE_PAD := sg13g2_io +LIBRELANE_KLAYOUT_PATH := $(LIBRELANE_PDK_ROOT)/$(LIBRELANE_PDK)/libs.tech/klayout +LIBRELANE_BONDPAD_SCRIPT := $(LIBRELANE_PDK_ROOT)/$(LIBRELANE_PDK)/libs.tech/klayout/tech/scripts/bondpad.py +LIBRELANE_PDN := $(LIBRELANE_FLOW_ROOT)/pdn_cfg.tcl +LIBRELANE_SRAM_VH := $(LIBRELANE_FLOW_ROOT)/sram_blackboxes.vh +LIBRELANE_STA_REPORT_LIMIT := $(LIBRELANE_FLOW_ROOT)/sta_report_limit.tcl +LIBRELANE_CLOCK_DOMAINS := $(RTL_PATH)/integration/clock_reset_domains.json +LIBRELANE_PIN_MAP := $(RTL_PATH)/pin_map/pin_map.json +LIBRELANE_TRACKED_RTL := $(shell git -C $(ROOT_PATH) ls-files \ + 'rtl/**/*.sv' 'rtl/**/*.svh' 'rtl/**/*.v' 'rtl/**/*.vh') + +LIBRELANE_SOURCE_PYTHON ?= /usr/bin/python3 +LIBRELANE_PYTHON ?= python3 +LIBRELANE_COMMAND := $(LIBRELANE_PYTHON) -m librelane +# LibreLane 3.0.5 writes 1000 expanded paths per group at every mid-PnR STA. +# Final multi-corner RCX STA remains mandatory; skip only the redundant reports. +LIBRELANE_MID_PNR_SKIP ?= --skip OpenROAD.STAMidPNR + +ifneq ($(filter librelane-input librelane-doctor librelane-chip \ + librelane-openroad librelane-klayout librelane-package,$(MAKECMDGOALS)),) +FLOW_PYTHON := $(LIBRELANE_SOURCE_PYTHON) +endif + +LIBRELANE_FEATURE_ARGS := \ + $(if $(filter YES,$(HAVE_PLL)),--have-pll) \ + $(if $(filter YES,$(HAVE_SRAM_IF)),--have-sram-if) \ + $(if $(filter YES,$(HAVE_SRAM_MACRO)),--have-sram-macro) \ + $(if $(filter YES,$(HAVE_SVA)),--have-sva) + +LIBRELANE_CONFIG_DEPS := $(LIBRELANE_RTL) $(LIBRELANE_SDC) \ + $(LIBRELANE_PDN) $(LIBRELANE_SRAM_VH) \ + $(LIBRELANE_STA_REPORT_LIMIT) $(LIBRELANE_BONDPAD_GDS) $(LIBRELANE_BONDPAD_LEF) \ + $(LIBRELANE_FLOW_ROOT)/scripts/generate_chip_config.py + +.PHONY: librelane-check-pdk librelane-input librelane-doctor librelane-chip \ + librelane-openroad librelane-klayout librelane-package + +librelane-check-pdk: + @test '$(PDK)' = IHP130 || { \ + echo 'physical/librelane/tiny currently implements only PDK=IHP130' >&2; exit 2; } + @test '$(HAVE_PLL)' = NO || { \ + echo 'the IHP130 LibreLane flow has no qualified crystal PAD' >&2; exit 2; } + +$(LIBRELANE_RTL): $(FILELIST_STAMP) $(LIBRELANE_TRACKED_RTL) \ + $(ROOT_PATH)/physical/smoke/syn/tools/export_soc_sources.py | librelane-check-pdk + @mkdir -p $(@D) + $(FLOW_PYTHON) $(ROOT_PATH)/physical/smoke/syn/tools/export_soc_sources.py sv \ + --output-dir $(@D) --soc TINY --pdk IHP130 --simu $(SIMU) \ + --jtag-idcode $(JTAG_IDCODE) \ + --ext-clk-hz $(EXT_CLK_HZ) --aud-clk-hz $(AUD_CLK_HZ) \ + --clint-timebase-hz $(CLINT_TIMEBASE_HZ) \ + --source-filelist-dir $(GENERATED_FL_DIR) \ + --librelane-safe \ + $(LIBRELANE_FEATURE_ARGS) + +$(LIBRELANE_SDC): $(LIBRELANE_FLOW_ROOT)/scripts/generate_sdc.py \ + $(LIBRELANE_CLOCK_DOMAINS) $(LIBRELANE_PIN_MAP) | librelane-check-pdk + @mkdir -p $(@D) + $(FLOW_PYTHON) $(LIBRELANE_FLOW_ROOT)/scripts/generate_sdc.py \ + --domains $(LIBRELANE_CLOCK_DOMAINS) --pin-map $(LIBRELANE_PIN_MAP) \ + --ext-clk-hz $(EXT_CLK_HZ) --output $@ + +$(LIBRELANE_BONDPAD_GDS): $(LIBRELANE_BONDPAD_SCRIPT) | librelane-check-pdk + @mkdir -p $(@D) + KLAYOUT_PATH=$(LIBRELANE_KLAYOUT_PATH) klayout -n sg13g2 -zz \ + -r $(LIBRELANE_BONDPAD_SCRIPT) -rd diameter=70.0 -rd shape=square \ + -rd output=$@ + @test -s $@ + +$(LIBRELANE_CONFIG): $(LIBRELANE_CONFIG_DEPS) + $(FLOW_PYTHON) $(LIBRELANE_FLOW_ROOT)/scripts/generate_chip_config.py \ + --pin-map $(LIBRELANE_PIN_MAP) --rtl $(LIBRELANE_RTL) --sdc $(LIBRELANE_SDC) \ + --pdn $(LIBRELANE_PDN) --bondpad-gds $(LIBRELANE_BONDPAD_GDS) \ + --bondpad-lef $(LIBRELANE_BONDPAD_LEF) --sram-vh $(LIBRELANE_SRAM_VH) \ + --ext-clk-hz $(EXT_CLK_HZ) --sram-size-kib $(SRAM_SIZE_KIB) \ + $(if $(filter YES,$(HAVE_SRAM_MACRO)),--have-sram-macro) \ + --output $@ + +librelane-input: $(LIBRELANE_CONFIG) + +$(LIBRELANE_DOCTOR_RESULT): $(LIBRELANE_CONFIG) \ + $(LIBRELANE_FLOW_ROOT)/scripts/doctor.py + @mkdir -p $(@D) + $(LIBRELANE_PYTHON) $(LIBRELANE_FLOW_ROOT)/scripts/doctor.py \ + --root $(ROOT_PATH) --config $(LIBRELANE_CONFIG) --lock $(LOCK_FILE) \ + --pin-map $(LIBRELANE_PIN_MAP) --pdk-root $(LIBRELANE_PDK_ROOT) --output $@ + +librelane-doctor: $(LIBRELANE_DOCTOR_RESULT) + +librelane-chip: librelane-doctor manifest + $(FLOW_PYTHON) $(ROOT_PATH)/scripts/run_flow.py --tool librelane \ + --log $(LIBRELANE_LOG) --result $(LIBRELANE_RESULT) --cwd $(LIBRELANE_RUN_ROOT) -- \ + $(LIBRELANE_COMMAND) $(LIBRELANE_CONFIG) --manual-pdk --pdk $(LIBRELANE_PDK) \ + --pdk-root $(LIBRELANE_PDK_ROOT) --scl $(LIBRELANE_SCL) --pad $(LIBRELANE_PAD) \ + --run-tag $(LIBRELANE_RUN_TAG) --overwrite --jobs $(JOBS) $(LIBRELANE_MID_PNR_SKIP) \ + --condensed --hide-progress-bar --save-views-to $(LIBRELANE_FINAL_DIR) + @mkdir -p $(META_DIR) + @cp $(LIBRELANE_RESULT) $(LIBRELANE_META_RESULT) + +librelane-openroad: librelane-doctor + cd $(LIBRELANE_RUN_ROOT) && $(LIBRELANE_COMMAND) $(LIBRELANE_CONFIG) --manual-pdk \ + --pdk $(LIBRELANE_PDK) --pdk-root $(LIBRELANE_PDK_ROOT) \ + --scl $(LIBRELANE_SCL) --pad $(LIBRELANE_PAD) \ + --run-tag $(LIBRELANE_RUN_TAG) --last-run --flow OpenInOpenROAD + +librelane-klayout: librelane-doctor + cd $(LIBRELANE_RUN_ROOT) && $(LIBRELANE_COMMAND) $(LIBRELANE_CONFIG) --manual-pdk \ + --pdk $(LIBRELANE_PDK) --pdk-root $(LIBRELANE_PDK_ROOT) \ + --scl $(LIBRELANE_SCL) --pad $(LIBRELANE_PAD) \ + --run-tag $(LIBRELANE_RUN_TAG) --last-run --flow OpenInKLayout + +librelane-package: librelane-chip + $(FLOW_PYTHON) $(LIBRELANE_FLOW_ROOT)/scripts/package.py \ + --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) \ + --run-root $(LIBRELANE_RUN_ROOT) --output $(LIBRELANE_ARCHIVE) diff --git a/physical/librelane/tiny/README.md b/physical/librelane/tiny/README.md new file mode 100644 index 00000000..a050916f --- /dev/null +++ b/physical/librelane/tiny/README.md @@ -0,0 +1,52 @@ +# Tiny LibreLane IHP130 Chip Flow + +This directory owns the Tiny product line's open-source IHP130 implementation +entry point: `librelane-chip` implements the bare-die `retrosoc_tiny_asic` +MCU, including its pad ring, bondpads, fillers, corners, seal ring, and the +32 on-chip SRAM bank macros. + +The flow consumes the committed `configs/ci/ihp130-tiny.mk` profile, the +canonical Tiny pin map (`rtl/tiny/pin_map/pin_map.json`), the Tiny clock/reset +inventory (`rtl/tiny/integration/clock_reset_domains.json`), and the +IHP-Open-PDK revision in `dependencies/dependencies.lock.json`. LibreLane reads +the upstream `ihp-sg13g2/libs.tech/librelane` configuration directly through +manual-PDK mode. Generated RTL, SDC, configuration, run databases, reports, and +final views are written below `build//physical/librelane/tiny/chip/`. + +Use the LibreLane 3.0.5 development shell (`~/squashfs-root/AppRun`) and put +the locked OpenSTA 3.0.0 first on `PATH`: + +```sh +~/squashfs-root/AppRun bash -c 'export PATH=/nfs/home/miaoyuchi/artifact-opensta/bin:$PATH; cd ' +make CONFIG=configs/ci/ihp130-tiny.mk librelane-doctor +make CONFIG=configs/ci/ihp130-tiny.mk librelane-chip +make CONFIG=configs/ci/ihp130-tiny.mk librelane-package +``` + +## Pad Ring + +The Tiny pin map declares 52 signal PADs (external clock/reset, JTAG, two +UARTs, 32 GPIOs, and the quad-XPI NOR flash interface). Tiny ships a reduced +supply set of 8 VDD + 8 VSS + 8 IOVDD + 8 IOVSS (two pairs of each per side), +84 placed PADs in total. The reduction from the Mini line's 24/24/16/16 follows +the taped-out Basilisk IHP SG13G2 SoC pad frame (68 signals with 24/24/16/16 +supplies feeding a Linux-capable CVA6 at a much higher clock and IO rate): +Tiny's 24 MHz Hazard3 core draws a few mA, and even with all 52 4 mA outputs +switching simultaneously each IO supply pair carries only ~26 mA. + +The side assignment is south = clock/reset/JTAG/UART, north = XPI, +east = GPIO[15:0], west = GPIO[31:16]; the `power_pads` counts come from +`rtl/tiny/pin_map/pin_map.json` and must match `pad_cfg.tcl`. + +## Constraints + +The SDC constrains the external clock at 24 MHz and JTAG at 10 MHz, defines a +generated system clock at the top-level clock buffer, and keeps the two groups +asynchronous. Mid-PnR timing report passes are skipped because LibreLane 3.0.5 +emits 1000 expanded paths per group; final post-RCX multi-corner STA remains +enabled. + +A successful open-source run is evidence for implementation development, not a +foundry production-signoff claim. Release review still requires qualified +foundry decks, package and bond planning, electrical/ESD review, and approved +waivers. diff --git a/physical/librelane/tiny/pad_cfg.tcl b/physical/librelane/tiny/pad_cfg.tcl new file mode 100644 index 00000000..6202685c --- /dev/null +++ b/physical/librelane/tiny/pad_cfg.tcl @@ -0,0 +1,17 @@ +# IHP SG13G2 supply pads are physical-only cells and are not retained by Yosys. +# Tiny ships two core and two IO supply pairs per pad-ring side (8/8/8/8). +foreach {kind master count} { + vdd sg13g2_IOPadVdd 8 + vss sg13g2_IOPadVss 8 + iovdd sg13g2_IOPadIOVdd 8 + iovss sg13g2_IOPadIOVss 8 +} { + for {set index 0} {$index < $count} {incr index} { + set instance_name [format {%s_pads[%d].%s_pad} $kind $index $kind] + set db_block [ord::get_db_block] + if {[$db_block findInst $instance_name] == "NULL"} { + make_instance $instance_name $master + } + } +} +source $::env(SCRIPTS_DIR)/openroad/common/pad_cfg.tcl diff --git a/physical/librelane/tiny/pdn_cfg.tcl b/physical/librelane/tiny/pdn_cfg.tcl new file mode 100644 index 00000000..51e3f682 --- /dev/null +++ b/physical/librelane/tiny/pdn_cfg.tcl @@ -0,0 +1,193 @@ +# Copyright 2025-2026 LibreLane Contributors and retroSoC Authors +# SPDX-License-Identifier: Apache-2.0 +# Tiny IHP130 chip PDN: LibreLane default sg13g2 topology (M1 rails connect to +# the TopMetal1 stripe network directly) plus a macro grid that ties the SRAM +# banks' Metal4 power columns into the stripe network with Metal5 cross straps. + +source $::env(SCRIPTS_DIR)/openroad/common/io.tcl +source $::env(SCRIPTS_DIR)/openroad/common/set_global_connections.tcl +set_global_connections + +# Use the PDK-native alternating routing directions (H/V/H/V/H). +set secondary_supplies [] +foreach vdd $::env(VDD_NETS) gnd $::env(GND_NETS) { + if {$vdd != $::env(VDD_NET)} { + lappend secondary_supplies $vdd + set db_net [[ord::get_db_block] findNet $vdd] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $vdd] + $net setSpecial + $net setSigType "POWER" + } + } + if {$gnd != $::env(GND_NET)} { + lappend secondary_supplies $gnd + set db_net [[ord::get_db_block] findNet $gnd] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $gnd] + $net setSpecial + $net setSigType "GROUND" + } + } +} + +# I/O rails are pad-ring internal and are absent from the synthesized top-level. +foreach {net signal_type} {IOVDD POWER IOVSS GROUND} { + set db_net [[ord::get_db_block] findNet $net] + if {$db_net == "NULL"} { + set db_net [odb::dbNet_create [ord::get_db_block] $net] + $db_net setSpecial + $db_net setSigType $signal_type + } +} + +# IHP IO cells carry both core and external-IO power rails on lower-case pins. +add_global_connection -net $::env(VDD_NET) -inst_pattern .* -pin_pattern vdd -power +add_global_connection -net $::env(GND_NET) -inst_pattern .* -pin_pattern vss -ground +add_global_connection -net IOVDD -inst_pattern .* -pin_pattern iovdd -power +add_global_connection -net IOVSS -inst_pattern .* -pin_pattern iovss -ground +global_connect -verbose + +# connect_by_abutment assigns IHP pad rails to corner-local *_RING nets. Rebind +# those physical pad and filler pins so the PDN knows they are VDD/VSS terminals. +proc reconnect_ihp_padring_rails {block} { + foreach {net_name pin_name} { + VDD vdd + VSS vss + IOVDD iovdd + IOVSS iovss + } { + set net [$block findNet $net_name] + foreach inst [$block getInsts] { + set master_name [[$inst getMaster] getName] + if {![string match "sg13g2_IOPad*" $master_name] && + ![string match "sg13g2_Filler*" $master_name] && + ![string match "sg13g2_Corner*" $master_name]} { + continue + } + foreach iterm [$inst getITerms] { + if {[[$iterm getMTerm] getName] != $pin_name} { + continue + } + odb::dbITerm_disconnect $iterm + odb::dbITerm_connect $iterm $net + } + } + } +} +reconnect_ihp_padring_rails [ord::get_db_block] + +set_voltage_domain -name CORE -power $::env(VDD_NET) -ground $::env(GND_NET) \ + -secondary_power $secondary_supplies + +set stdcell_grid_args [list] +if {$::env(PDN_ENABLE_PINS) == 1} { + lappend stdcell_grid_args -pins "$::env(PDN_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" +} +define_pdn_grid \ + -name stdcell_grid \ + -starts_with POWER \ + -voltage_domain CORE \ + {*}$stdcell_grid_args + +add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_VERTICAL_LAYER) \ + -width $::env(PDN_VWIDTH) \ + -pitch $::env(PDN_VPITCH) \ + -offset $::env(PDN_VOFFSET) \ + -spacing $::env(PDN_VSPACING) \ + -starts_with POWER \ + -extend_to_core_ring + +add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_HORIZONTAL_LAYER) \ + -width $::env(PDN_HWIDTH) \ + -pitch $::env(PDN_HPITCH) \ + -offset $::env(PDN_HOFFSET) \ + -spacing $::env(PDN_HSPACING) \ + -starts_with POWER \ + -extend_to_core_ring + +# A low-metal ring just inside the core edge collects the Metal1 rails; the +# top-metal stripe mesh bridges to it where the stripes cross the ring legs. +add_pdn_ring \ + -grid stdcell_grid \ + -layers "Metal2 Metal3" \ + -widths "0.52 0.52" \ + -spacings "0.8 0.8" \ + -core_offsets "1.6 1.6" \ + -starts_with POWER \ + -add_connect + +if {$::env(PDN_ENABLE_RAILS) == 1} { + add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(PDN_RAIL_LAYER) \ + -width $::env(PDN_RAIL_WIDTH) \ + -followpins \ + -extend_to_core_ring + add_pdn_connect -grid stdcell_grid -layers "Metal1 Metal2" + add_pdn_connect -grid stdcell_grid -layers "Metal1 Metal3" + # The rail network meets the top-metal mesh at the low ring legs and at + # every Metal1 rail / TopMetal1 stripe crossing (covers the standard-cell + # channels between SRAM banks as well). + add_pdn_connect -grid stdcell_grid -layers "Metal3 TopMetal1" + add_pdn_connect -grid stdcell_grid -layers "Metal1 TopMetal1" + add_pdn_connect -grid stdcell_grid -layers "Metal2 TopMetal2" +} + +if {$::env(PDN_CORE_RING) == 1} { + add_pdn_ring \ + -grid stdcell_grid \ + -layers "$::env(PDN_VERTICAL_LAYER) $::env(PDN_HORIZONTAL_LAYER)" \ + -widths "$::env(PDN_CORE_RING_VWIDTH) $::env(PDN_CORE_RING_HWIDTH)" \ + -spacings "$::env(PDN_CORE_RING_VSPACING) $::env(PDN_CORE_RING_HSPACING)" \ + -core_offsets "$::env(PDN_CORE_RING_VOFFSET) $::env(PDN_CORE_RING_HOFFSET)" \ + -connect_to_pads \ + -connect_to_pad_layers TopMetal2 \ + -add_connect +} + +# Tiny hardens a single SRAM master: the 128 KiB on-chip RAM banks. Their power +# pins are vertical Metal4 columns spanning the bank height; horizontal Metal5 +# straps cross every column and hand off to the stdcell TopMetal1 stripes +# outside the bank boundary. +set sram_cells [list \ + RM_IHPSG13_1P_1024x32_c2_bm_bist] + +define_pdn_grid \ + -macro \ + -cells $sram_cells \ + -name sram_bank_grid \ + -grid_over_boundary \ + -voltage_domains CORE \ + -starts_with POWER + +add_pdn_ring \ + -grid sram_bank_grid \ + -layers "Metal2 Metal3" \ + -widths "0.52 0.52" \ + -spacings "0.8 0.8" \ + -core_offsets "1.6 1.6" \ + -starts_with POWER \ + -add_connect + +add_pdn_stripe \ + -grid sram_bank_grid \ + -layer Metal5 \ + -width 2.5 \ + -pitch 67.24 \ + -offset 10.0 \ + -spacing 2.5 \ + -starts_with POWER + +add_pdn_connect -grid sram_bank_grid -layers "Metal4 Metal5" +add_pdn_connect -grid sram_bank_grid -layers "Metal5 TopMetal1" +add_pdn_connect -grid sram_bank_grid -layers "Metal2 TopMetal1" +add_pdn_connect -grid sram_bank_grid -layers "Metal3 TopMetal1" +add_pdn_connect -grid sram_bank_grid -layers "TopMetal1 TopMetal2" +# Rails trapped in the channels between banks tie into the bank rings. +add_pdn_connect -grid sram_bank_grid -layers "Metal1 Metal2" +add_pdn_connect -grid sram_bank_grid -layers "Metal1 Metal3" diff --git a/physical/librelane/tiny/scripts/doctor.py b/physical/librelane/tiny/scripts/doctor.py new file mode 100644 index 00000000..58ce9c31 --- /dev/null +++ b/physical/librelane/tiny/scripts/doctor.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Validate the local LibreLane/IHP130 environment and Tiny chip configuration.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +sys.path.insert(0, str(ROOT)) +from librelane.flows import Flow # noqa: E402 +from scripts.rtl.generate_pin_map import read_power_pad_counts # noqa: E402 +from scripts.setup_helpers import atomic_write # noqa: E402 + + +EXPECTED_LIBRELANE_VERSION = "LibreLane v3.0.5" +EXPECTED_OPENSTA_VERSION = "3.0.0" +EXPECTED_SIGNAL_PADS = 52 + + +def command_output(command: list[str]) -> str: + return subprocess.check_output(command, text=True, stderr=subprocess.STDOUT).strip() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--lock", type=Path, required=True) + parser.add_argument("--pin-map", type=Path, required=True) + parser.add_argument("--pdk-root", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + errors: list[str] = [] + details: dict[str, object] = {} + + for tool in ("librelane", "klayout", "sta"): + path = shutil.which(tool) + details[f"{tool}_path"] = path + if path is None: + errors.append(f"required tool is missing: {tool}") + + if shutil.which("librelane") is not None: + version = command_output( + [sys.executable, "-m", "librelane", "--version"] + ).splitlines()[0] + details["librelane_version"] = version + if version != EXPECTED_LIBRELANE_VERSION: + errors.append(f"expected {EXPECTED_LIBRELANE_VERSION}, found {version or ''}") + + if shutil.which("sta") is not None: + version = command_output(["sta", "-version"]) + details["opensta_version"] = version + if version != EXPECTED_OPENSTA_VERSION: + errors.append( + f"expected OpenSTA {EXPECTED_OPENSTA_VERSION}, " + f"found {version or ''}" + ) + + lock = json.loads(args.lock.read_text(encoding="utf-8")) + expected_pdk_revision = lock["sources"]["pdk_ihp130"]["revision"] + details["locked_pdk_revision"] = expected_pdk_revision + + pdk_checkout = args.pdk_root.resolve() + if not (pdk_checkout / ".git").exists(): + errors.append(f"IHP PDK checkout is missing: {pdk_checkout}") + else: + revision = command_output(["git", "-C", str(pdk_checkout), "rev-parse", "HEAD"]) + details["pdk_revision"] = revision + if revision != expected_pdk_revision: + errors.append(f"expected IHP PDK {expected_pdk_revision}, found {revision}") + + required = ( + args.config, + pdk_checkout / "ihp-sg13g2/libs.tech/librelane/config.tcl", + pdk_checkout / "ihp-sg13g2/libs.tech/klayout/tech/drc/ihp-sg13g2.drc", + pdk_checkout / "ihp-sg13g2/libs.tech/klayout/tech/scripts/sealring.py", + pdk_checkout / "ihp-sg13g2/libs.ref/sg13g2_io/spice/sg13g2_io.spice", + ) + for path in required: + if not path.is_file(): + errors.append(f"required LibreLane/IHP file is missing: {path}") + + try: + power_counts = read_power_pad_counts(args.pin_map) + except (OSError, ValueError, json.JSONDecodeError) as error: + power_counts = None + errors.append(f"failed to read power PAD counts: {error}") + + config: dict[str, object] = {} + flow_name = "" + if args.config.is_file(): + try: + config = json.loads(args.config.read_text(encoding="utf-8")) + meta = config.get("meta", {}) + if not isinstance(meta, dict) or not isinstance(meta.get("flow"), str): + raise ValueError("config meta.flow must name a LibreLane flow") + flow_name = meta["flow"] + except (OSError, ValueError, json.JSONDecodeError) as error: + errors.append(f"failed to read LibreLane configuration: {error}") + + if args.config.is_file() and not errors: + try: + flow_class = Flow.factory.get(flow_name) + if flow_class is None: + raise ValueError(f"unknown LibreLane flow: {flow_name}") + flow = flow_class( + str(args.config.resolve()), + pdk="ihp-sg13g2", + pdk_root=str(pdk_checkout), + scl="sg13g2_stdcell", + pad="sg13g2_io", + design_dir=str(args.config.resolve().parent), + ) + details["flow"] = flow_name + details["config_design"] = flow.config["DESIGN_NAME"] + details["drc_runset"] = str(flow.config["KLAYOUT_DRC_RUNSET"]) + details["lvs_setup"] = str(flow.config["NETGEN_SETUP"]) + details["pad_spice"] = [str(path) for path in flow.config["PAD_SPICE_MODELS"]] + except Exception as error: # LibreLane reports structured configuration exceptions. + errors.append(f"LibreLane {flow_name or 'unknown'} configuration failed to load: {error}") + + if config: + if details.get("config_design") not in (None, "retrosoc_tiny_asic"): + errors.append( + f"Tiny chip flow expects DESIGN_NAME retrosoc_tiny_asic, " + f"found {details['config_design']}" + ) + placed = [ + item + for side in ("PAD_SOUTH", "PAD_EAST", "PAD_NORTH", "PAD_WEST") + for item in config.get(side, []) + ] + details["placed_pad_count"] = len(placed) + if flow_name == "Chip" and power_counts is not None: + expected = EXPECTED_SIGNAL_PADS + sum(power_counts.values()) + if len(placed) != expected or len(placed) != len(set(placed)): + errors.append( + f"expected {expected} unique placed signal/power PADs, found {len(placed)}" + ) + elif flow_name != "Chip": + errors.append(f"Tiny supports only the Chip flow, found {flow_name}") + + result = { + "schema_version": 1, + "status": "passed" if not errors else "failed", + "errors": errors, + "details": details, + } + atomic_write(args.output, json.dumps(result, indent=2, sort_keys=True) + "\n") + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + print(f"LibreLane doctor passed: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/physical/librelane/tiny/scripts/generate_chip_config.py b/physical/librelane/tiny/scripts/generate_chip_config.py new file mode 100644 index 00000000..782be724 --- /dev/null +++ b/physical/librelane/tiny/scripts/generate_chip_config.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Generate the IHP130 LibreLane Chip configuration for the Tiny SoC.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +sys.path.insert(0, str(ROOT)) +from scripts.rtl.generate_pin_map import ( # noqa: E402 + POWER_PAD_KINDS, + Pad, + ihp130_pad_instance, + read_map, + read_power_pad_counts, +) +from scripts.setup_helpers import atomic_write # noqa: E402 + + +SIDE_ORDER = ("south", "east", "north", "west") +EXPECTED_SIGNAL_PADS = 52 +CLOCK_PORTS = ("extclk_i_pad", "jtag_tck_i_pad") +SRAM_MASTER = "RM_IHPSG13_1P_1024x32_c2_bm_bist" +SRAM_INSTANCE = "u_soc.u_sram.gen_memory.gen_bank[{index}].u_ram.u_mem" +SRAM_COLUMNS = 8 +SRAM_ORIGIN = (400, 600) +# Detailed routing requires an M2 track inside each bank's 0.26 um bottom-edge +# pin stubs: the bank y origin must keep residue {0, 0.24} mod 0.42 um (the M2 +# vertical track pitch); a y pitch of 504 um (1200 x 0.42) preserves it per row. +SRAM_PITCH = (480, 504) +DIE_AREA = [0, 0, 4700, 4700] +CORE_AREA = [365, 365, 4335, 4335] + + +def config_path(path: Path, base: Path) -> str: + return f"dir::{os.path.relpath(path.resolve(), base.resolve())}" + + +def signal_side(pad: Pad) -> str: + name = pad.name + if name.startswith("gpio_"): + return "east" if pad.index is not None and pad.index < 16 else "west" + if name.startswith("xpi_"): + return "north" + return "south" + + +def power_instances(counts: dict[str, int], side_index: int) -> list[str]: + per_side: dict[str, int] = {} + for kind in POWER_PAD_KINDS: + count = counts[kind] + if count % len(SIDE_ORDER) != 0: + raise ValueError( + f"power_pads.{kind}={count} cannot be distributed across " + f"{len(SIDE_ORDER)} pad-ring sides" + ) + per_side[kind] = count // len(SIDE_ORDER) + result: list[str] = [] + for local_index in range(per_side["vdd"]): + for kind in ("vdd", "vss"): + index = (side_index * per_side[kind]) + local_index + result.append(f"{kind}_pads[{index}].{kind}_pad") + for local_index in range(per_side["iovdd"]): + for kind in ("iovdd", "iovss"): + index = (side_index * per_side[kind]) + local_index + result.append(f"{kind}_pads[{index}].{kind}_pad") + return result + + +def interleave(signals: list[str], supplies: list[str]) -> list[str]: + result: list[str] = [] + signal_index = 0 + for supply_index, supply in enumerate(supplies): + target = round(((supply_index + 1) * len(signals)) / (len(supplies) + 1)) + result.extend(signals[signal_index:target]) + signal_index = target + result.append(supply) + result.extend(signals[signal_index:]) + return result + + +def sram_instances(capacity_kib: int) -> dict[str, tuple[list[int], str]]: + if capacity_kib <= 0 or capacity_kib % 4 != 0: + raise ValueError("SRAM_SIZE_KIB must be a positive multiple of 4") + instances: dict[str, tuple[list[int], str]] = {} + for index in range(capacity_kib // 4): + column, row = index % SRAM_COLUMNS, index // SRAM_COLUMNS + location = [ + SRAM_ORIGIN[0] + column * SRAM_PITCH[0], + SRAM_ORIGIN[1] + row * SRAM_PITCH[1], + ] + instances[SRAM_INSTANCE.format(index=index)] = (location, "N") + return instances + + +def macro_config(instances: dict[str, tuple[list[int], str]], header: str) -> dict[str, object]: + base = "pdk_dir::libs.ref/sg13g2_sram" + master = SRAM_MASTER + return { + "gds": [f"{base}/gds/{master}.gds"], + "lef": [f"{base}/lef/{master}.lef"], + "vh": [header], + "spice": [f"{base}/cdl/{master}.cdl"], + "lib": { + "*_typ_1p20V_25C": [f"{base}/lib/{master}_typ_1p20V_25C.lib"], + "*_fast_1p32V_m40C": [f"{base}/lib/{master}_fast_1p32V_m55C.lib"], + "*_slow_1p08V_125C": [f"{base}/lib/{master}_slow_1p08V_125C.lib"], + }, + "instances": { + name: {"location": location, "orientation": orientation} + for name, (location, orientation) in instances.items() + }, + } + + +def macro_hooks(instances: dict[str, tuple[list[int], str]]) -> list[str]: + hooks: list[str] = [] + for instance in instances: + bracket_start = instance.find("[") + if bracket_start >= 0: + bracket_end = instance.index("]", bracket_start) + instance_pattern = ( + re.escape(instance[:bracket_start]) + + ".*" + + instance[bracket_start + 1 : bracket_end] + + ".*" + + re.escape(instance[bracket_end + 1 :]) + ) + else: + instance_pattern = re.escape(instance) + hooks.extend( + [ + f"{instance_pattern} VDD VSS VDDARRAY! VSS!", + f"{instance_pattern} VDD VSS VDD! VSS!", + ] + ) + return hooks + + +def build_config(args: argparse.Namespace) -> dict[str, object]: + pads, _ = read_map(args.pin_map) + power_counts = read_power_pad_counts(args.pin_map) + signal_instances: dict[str, list[str]] = {side: [] for side in SIDE_ORDER} + for pad in pads: + instance = ihp130_pad_instance(pad) + if instance is None: + raise ValueError(f"IHP130 physical PAD mapping is missing for {pad.name}") + signal_instances[signal_side(pad)].append(instance) + + flattened = [item for side in SIDE_ORDER for item in signal_instances[side]] + if len(flattened) != EXPECTED_SIGNAL_PADS: + raise ValueError( + f"expected {EXPECTED_SIGNAL_PADS} signal PADs, got {len(flattened)}" + ) + if len(flattened) != len(set(flattened)): + raise ValueError("physical signal PAD instances are not unique") + + pad_sides = { + side.upper(): interleave(signal_instances[side], power_instances(power_counts, index)) + for index, side in enumerate(SIDE_ORDER) + } + all_placed = [item for side in SIDE_ORDER for item in pad_sides[side.upper()]] + expected_total = len(flattened) + sum(power_counts.values()) + if len(all_placed) != expected_total or len(all_placed) != len(set(all_placed)): + raise ValueError( + "PAD ring placement does not cover every signal and power PAD exactly once" + ) + + config_dir = args.output.resolve().parent + sram_header = config_path(args.sram_vh, config_dir) + active_sram_instances = sram_instances(args.sram_size_kib) if args.have_sram_macro else {} + macros = ( + {SRAM_MASTER: macro_config(active_sram_instances, sram_header)} + if active_sram_instances + else {} + ) + + return { + "meta": {"version": 3, "flow": "Chip"}, + "DESIGN_NAME": "retrosoc_tiny_asic", + "VERILOG_FILES": [config_path(args.rtl, config_dir)], + "USE_SLANG": True, + "SLANG_ARGUMENTS": ["--keep-hierarchy"], + "VERILOG_POWER_DEFINE": None, + "SYNTH_SHARE_RESOURCES": False, + "SYNTH_HIERARCHY_MODE": "deferred_flatten", + "SYNTH_KEEP_HIERARCHY_MODULES": [ + "sg13g2_IOPadVdd", + "sg13g2_IOPadVss", + "sg13g2_IOPadIOVdd", + "sg13g2_IOPadIOVss", + ], + "YOSYS_LOG_LEVEL": "WARNING", + "SYNTH_STRATEGY": "AREA 3", + # Hotspots sit at the SRAM bank pin edges; detailed routing resolves the + # moderate estimated overflow. The signoff bar remains detailed-routing + # DRC, post-RCX multi-corner STA, KLayout DRC, and netgen LVS. + "GRT_ALLOW_CONGESTION": True, + # This OpenROAD build's antenna analysis leaks/stalls on the Tiny netlist + # (check_antennas -verbose ballooned to 248 GB RSS; repair_antennas did + # not converge in 100 minutes). Skip the report/repair steps; antenna + # coverage is a documented gap for this flow revision. + "RUN_ANTENNA_REPAIR": False, + # Hazard3 with EXTENSION_A=0 (RV32IM) leaves x_amo_phase undriven by + # design; every use is masked by a constant-false guard. Waive the + # three benign "used but has no driver" check errors. + "ERROR_ON_SYNTH_CHECKS": False, + "RUN_POST_GPL_DESIGN_REPAIR": False, + "RUN_CTS": False, + "RUN_POST_CTS_RESIZER_TIMING": False, + "EXTRA_EXCLUDED_CELLS": ["sg13g2_IOPad*"], + "PRIMARY_GDSII_STREAMOUT_TOOL": "klayout", + "PNR_SDC_FILE": config_path(args.sdc, config_dir), + "SIGNOFF_SDC_FILE": config_path(args.sdc, config_dir), + "FALLBACK_SDC": config_path(args.sdc, config_dir), + "STA_EXTRA_CORNER_TCL_FILE": config_path( + ROOT / "physical/librelane/tiny/sta_report_limit.tcl", config_dir + ), + "CLOCK_PORT": list(CLOCK_PORTS), + "CLOCK_PERIOD": 1_000_000_000 / args.ext_clk_hz, + "VDD_NETS": ["VDD"], + "GND_NETS": ["VSS"], + "FP_SIZING": "absolute", + "PDN_CORE_RING": True, + "PDN_CORE_RING_VWIDTH": 15, + "PDN_CORE_RING_HWIDTH": 15, + "PDN_CORE_RING_VSPACING": 5, + "PDN_CORE_RING_HSPACING": 5, + "PDN_ENABLE_PINS": True, + "ERROR_ON_PDN_VIOLATIONS": True, + "PDN_CFG": config_path(args.pdn, config_dir), + "MACROS": macros, + "PDN_MACRO_CONNECTIONS": macro_hooks(active_sram_instances), + "MAGIC_GDS_FLATGLOB": [ + "lvsres_*", + "VIA_M1_*", + "VIA_M2_*", + "*_CELL_CORNER", + "RSC_*", + "*_CELL_SUB", + ], + "PAD_SOUTH": pad_sides["SOUTH"], + "PAD_EAST": pad_sides["EAST"], + "PAD_NORTH": pad_sides["NORTH"], + "PAD_WEST": pad_sides["WEST"], + "DIE_AREA": DIE_AREA, + "CORE_AREA": CORE_AREA, + "PL_TARGET_DENSITY_PCT": 45, + "PDN_CORE_RING_CONNECT_TO_PADS": True, + "PAD_CFG": config_path(ROOT / "physical/librelane/tiny/pad_cfg.tcl", config_dir), + "PAD_BONDPAD_NAME": "bondpad_70x70", + "EXTRA_GDS": [config_path(args.bondpad_gds, config_dir)], + "EXTRA_LEFS": [config_path(args.bondpad_lef, config_dir)], + "IGNORE_DISCONNECTED_MODULES": ["bondpad_70x70"], + "MAGIC_EXT_UNIQUE": "notopports", + } + + +def positive_integer(value: str) -> int: + parsed = int(value) + if parsed <= 0: + raise argparse.ArgumentTypeError("value must be positive") + return parsed + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pin-map", type=Path, required=True) + parser.add_argument("--rtl", type=Path, required=True) + parser.add_argument("--sdc", type=Path, required=True) + parser.add_argument("--pdn", type=Path, required=True) + parser.add_argument("--bondpad-gds", type=Path, required=True) + parser.add_argument("--bondpad-lef", type=Path, required=True) + parser.add_argument("--sram-vh", type=Path, required=True) + parser.add_argument("--ext-clk-hz", type=positive_integer, required=True) + parser.add_argument("--have-sram-macro", action="store_true") + parser.add_argument("--sram-size-kib", type=positive_integer, default=128) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + config = build_config(args) + atomic_write(args.output, json.dumps(config, indent=2, sort_keys=True) + "\n") + except (OSError, ValueError, json.JSONDecodeError) as error: + parser.error(str(error)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/physical/librelane/tiny/scripts/generate_sdc.py b/physical/librelane/tiny/scripts/generate_sdc.py new file mode 100644 index 00000000..49084619 --- /dev/null +++ b/physical/librelane/tiny/scripts/generate_sdc.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Generate pad-aware timing constraints for the Tiny IHP130 Chip flow.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +sys.path.insert(0, str(ROOT)) +from scripts.rtl.generate_pin_map import Pad, ihp130_pad_instance, read_map # noqa: E402 +from scripts.check_clock_reset_domains import validate # noqa: E402 +from scripts.setup_helpers import atomic_write # noqa: E402 + + +CLOCK_PADS = { + "external": "extclk_i_pad", + "jtag": "jtag_tck_i_pad", +} +RESET_PADS = {"ext_rst_n_i_pad", "jtag_trst_n_i_pad"} +IHP130_SYSTEM_CLOCK_PIN = "u_clock_buffer.u_sg13g2_buf_1/X" +JTAG_PERIOD_NS = 100.0 + + +def positive_integer(value: str) -> int: + parsed = int(value) + if parsed <= 0: + raise argparse.ArgumentTypeError("value must be positive") + return parsed + + +def timing_clock(pad: Pad) -> str: + if pad.name.startswith("jtag_"): + return "jtag" + return "external" + + +def logical_directions(pad: Pad) -> tuple[bool, bool]: + input_kinds = {"tri_input", "schmitt_input"} + output_kinds = {"tri_output", "peripheral_output"} + bidir_kinds = {"full", "peripheral_bidir", "xpi_data"} + if pad.kind in input_kinds: + return True, False + if pad.kind in output_kinds: + return False, True + if pad.kind in bidir_kinds: + return True, True + return False, False + + +def render(args: argparse.Namespace) -> str: + validate(args.domains, ROOT) + pads, _ = read_map(args.pin_map) + by_name = {pad.name: pad for pad in pads} + for name in CLOCK_PADS.values(): + if name not in by_name: + raise ValueError(f"clock PAD is missing from the pin map: {name}") + + periods = { + "external": 1_000_000_000 / args.ext_clk_hz, + "jtag": JTAG_PERIOD_NS, + } + groups: dict[str, dict[str, list[str]]] = { + name: {"inputs": [], "outputs": []} for name in periods + } + excluded = set(CLOCK_PADS.values()) | RESET_PADS + for pad in pads: + if pad.name in excluded: + continue + is_input, is_output = logical_directions(pad) + clock = timing_clock(pad) + if is_input: + groups[clock]["inputs"].append(pad.name) + if is_output: + groups[clock]["outputs"].append(pad.name) + + lines = [ + "# Generated by physical/librelane/tiny/scripts/generate_sdc.py; do not edit.", + "current_design $::env(DESIGN_NAME)", + "set_units -time ns", + "", + "proc require_pins {label name} {", + " set objects [get_pins -quiet $name]", + ' if {$objects eq ""} { error "required SDC pin is missing: $label ($name)" }', + " return $objects", + "}", + "proc require_ports {label name} {", + " set objects [get_ports -quiet $name]", + ' if {$objects eq ""} { error "required SDC port is missing: $label ($name)" }', + " return $objects", + "}", + "", + ] + for name, pad_name in CLOCK_PADS.items(): + instance = ihp130_pad_instance(by_name[pad_name]) + if instance is None: + raise ValueError(f"clock PAD has no IHP130 physical instance: {pad_name}") + lines.append( + f'set clk_{name}_pin [require_pins "clock {name}" {{{instance}/p2c}}]' + ) + lines.append( + f"create_clock -name clk_{name} -period {periods[name]:.12g} $clk_{name}_pin" + ) + lines.append("") + + lines.extend( + [ + f'set clk_system_pin [require_pins "clock system" {{{IHP130_SYSTEM_CLOCK_PIN}}}]', + "create_generated_clock -name clk_system -source $clk_external_pin " + "-divide_by 1 $clk_system_pin", + "", + "set_clock_groups -name retrosoc_async -asynchronous \\", + " -group [get_clocks {clk_external clk_system}] \\", + " -group [get_clocks {clk_jtag}]", + "", + "set all_retrosoc_clocks [get_clocks {clk_external clk_system clk_jtag}]", + "set_clock_uncertainty -setup 0.2 $all_retrosoc_clocks", + "set_clock_uncertainty -hold 0.1 $all_retrosoc_clocks", + "set_clock_transition 0.1 $all_retrosoc_clocks", + "set_timing_derate -early 0.95", + "set_timing_derate -late 1.05", + "set_max_fanout 10 [current_design]", + "", + ] + ) + + for reset in sorted(RESET_PADS): + lines.extend( + [ + f'set reset_{reset} [require_ports "reset {reset}" {{{reset}}}]', + f"set_false_path -from $reset_{reset}", + "", + ] + ) + + for clock, port_groups in groups.items(): + delay = periods[clock] * 0.2 + for direction, ports in port_groups.items(): + if not ports: + continue + variable = f"{clock}_{direction}" + lines.append(f"set {variable} [get_ports {{{' '.join(ports)}}}]") + if direction == "inputs": + lines.append(f"set_input_delay -min 0 -clock clk_{clock} ${variable}") + lines.append(f"set_input_delay -max {delay:.12g} -clock clk_{clock} ${variable}") + else: + lines.append(f"set_output_delay -min 0 -clock clk_{clock} ${variable}") + lines.append(f"set_output_delay -max {delay:.12g} -clock clk_{clock} ${variable}") + lines.append(f"set_load 0.006 ${variable}") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--domains", type=Path, required=True) + parser.add_argument("--pin-map", type=Path, required=True) + parser.add_argument("--ext-clk-hz", type=positive_integer, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + atomic_write(args.output, render(args)) + except (OSError, ValueError, json.JSONDecodeError) as error: + parser.error(str(error)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/physical/librelane/tiny/scripts/package.py b/physical/librelane/tiny/scripts/package.py new file mode 100644 index 00000000..0b1e5796 --- /dev/null +++ b/physical/librelane/tiny/scripts/package.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Create a checksummed Tiny LibreLane implementation delivery archive.""" + +from __future__ import annotations + +import argparse +import hashlib +import shutil +import tarfile +import tempfile +from pathlib import Path + + +def hash_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while block := source.read(1024 * 1024): + digest.update(block) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--variant-root", type=Path, required=True) + parser.add_argument("--run-root", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + root = args.root.resolve() + variant_root = args.variant_root.resolve() + run_root = args.run_root.resolve() + required = ( + run_root / "config.json", + run_root / "result.json", + run_root / "final", + run_root / "runs/current", + variant_root / "meta/manifest.json", + variant_root / "meta/librelane-chip-doctor.json", + variant_root / "meta/librelane-chip.json", + root / "dependencies/dependencies.lock.json", + ) + missing = [str(path) for path in required if not path.exists()] + if missing: + parser.error("missing LibreLane delivery input(s): " + ", ".join(missing)) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="retrosoc-librelane-package-") as temporary: + staging = Path(temporary) / "retrosoc-tiny-ihp130-chip" + shutil.copytree(run_root / "final", staging / "final") + shutil.copytree(run_root / "runs/current", staging / "evidence/run") + shutil.copy2(run_root / "config.json", staging / "evidence/config.json") + shutil.copy2(run_root / "result.json", staging / "evidence/result.json") + shutil.copy2(variant_root / "meta/manifest.json", staging / "evidence/manifest.json") + shutil.copy2( + variant_root / "meta/librelane-chip-doctor.json", + staging / "evidence/librelane-doctor.json", + ) + shutil.copy2( + variant_root / "meta/librelane-chip.json", + staging / "evidence/librelane-chip.json", + ) + shutil.copy2( + root / "dependencies/dependencies.lock.json", + staging / "evidence/dependencies.lock.json", + ) + files = sorted(path for path in staging.rglob("*") if path.is_file()) + sums = "\n".join(f"{hash_file(path)} {path.relative_to(staging)}" for path in files) + (staging / "SHA256SUMS").write_text(sums + "\n", encoding="utf-8") + with tarfile.open(args.output, "w:gz") as archive: + archive.add(staging, arcname=staging.name) + print(f"created LibreLane delivery: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/physical/librelane/tiny/sram_blackboxes.vh b/physical/librelane/tiny/sram_blackboxes.vh new file mode 100644 index 00000000..b30513a0 --- /dev/null +++ b/physical/librelane/tiny/sram_blackboxes.vh @@ -0,0 +1,25 @@ +// Copyright (c) 2026 Yuchi Miao +// SPDX-License-Identifier: MulanPSL-2.0 +// Synthesis-only header for the locked IHP SRAM macro physical views. + +(* blackbox *) +module RM_IHPSG13_1P_1024x32_c2_bm_bist ( + input wire A_CLK, + input wire A_MEN, + input wire A_WEN, + input wire A_REN, + input wire [ 9:0] A_ADDR, + input wire [31:0] A_DIN, + input wire A_DLY, + output wire [31:0] A_DOUT, + input wire [31:0] A_BM, + input wire A_BIST_CLK, + input wire A_BIST_EN, + input wire A_BIST_MEN, + input wire A_BIST_WEN, + input wire A_BIST_REN, + input wire [ 9:0] A_BIST_ADDR, + input wire [31:0] A_BIST_DIN, + input wire [31:0] A_BIST_BM +); +endmodule diff --git a/physical/librelane/tiny/sta_report_limit.tcl b/physical/librelane/tiny/sta_report_limit.tcl new file mode 100644 index 00000000..57a9270c --- /dev/null +++ b/physical/librelane/tiny/sta_report_limit.tcl @@ -0,0 +1,35 @@ +# LibreLane 3.0.5 hard-codes 1000 expanded paths per clock group. Cap only +# report enumeration; worst-slack/TNS analysis still covers the full graph. +set ::retrosoc_sta_group_path_count 10 +puts "\[INFO\] OpenSTA executable: [info nameofexecutable]" +rename report_checks ::retrosoc_report_checks +proc report_checks {args} { + set count_index [lsearch -exact $args -group_path_count] + if {$count_index >= 0} { + set value_index [expr {$count_index + 1}] + if {$value_index < [llength $args]} { + lset args $value_index $::retrosoc_sta_group_path_count + } + } else { + lappend args -group_path_count $::retrosoc_sta_group_path_count + } + uplevel 1 [list ::retrosoc_report_checks {*}$args] +} + +rename report_check_types ::retrosoc_report_check_types +proc report_check_types {args} { + set violator_index [lsearch -exact $args -violators] + if {$violator_index >= 0} { + set args [lreplace $args $violator_index $violator_index] + } + uplevel 1 [list ::retrosoc_report_check_types {*}$args] +} + +rename report_parasitic_annotation ::retrosoc_report_parasitic_annotation +proc report_parasitic_annotation {args} { + set detail_index [lsearch -exact $args -report_unannotated] + if {$detail_index >= 0} { + set args [lreplace $args $detail_index $detail_index] + } + uplevel 1 [list ::retrosoc_report_parasitic_annotation {*}$args] +} diff --git a/physical/pdk/README.md b/physical/pdk/README.md index 79eecb5b..fcea32e8 100644 --- a/physical/pdk/README.md +++ b/physical/pdk/README.md @@ -30,7 +30,8 @@ the locked HD library submodule; OpenRAM's build-space and Ciel inputs are recorded in the macro artifact rather than added to the SoC PDK checkout. The IHP130 dependency is also the technology source for the independent -`physical/librelane/` core-hardening and full-chip pad-ring flows. The locked +`physical/librelane/{mini,tiny}/` core-hardening and full-chip pad-ring flows. +The locked checkout includes the upstream `libs.tech/librelane` standard-cell, pad, extraction, DRC, LVS, filler, bondpad, and seal-ring configuration; LibreLane consumes it directly in manual-PDK mode. The other three open PDKs do not yet diff --git a/rtl/tiny/pin_map/pin_map.json b/rtl/tiny/pin_map/pin_map.json index b02c05d8..be65c3cb 100644 --- a/rtl/tiny/pin_map/pin_map.json +++ b/rtl/tiny/pin_map/pin_map.json @@ -1,5 +1,11 @@ { "schema_version": 1, + "power_pads": { + "vdd": 8, + "vss": 8, + "iovdd": 8, + "iovss": 8 + }, "pads": [ { "kind": "tri_input", diff --git a/scripts/rtl/generate_pin_map.py b/scripts/rtl/generate_pin_map.py index 6975b065..57deee25 100644 --- a/scripts/rtl/generate_pin_map.py +++ b/scripts/rtl/generate_pin_map.py @@ -29,6 +29,7 @@ REQUIRED_PROFILES = {"fpga_mini", "tb", "verilator"} CONNECTION_RE = re.compile(r"(?:[A-Za-z_][A-Za-z0-9_]*)(?:\[[0-9]+\])?$") CONSTANT_RE = re.compile(r"[0-9]+'[bBdDhHoO][0-9a-fA-F_xXzZ]+$") IHP130_POWER_PAD_COUNTS = {"vdd": 24, "vss": 24, "iovdd": 16, "iovss": 16} +POWER_PAD_KINDS = tuple(IHP130_POWER_PAD_COUNTS) POWER_CONNECTIONS = "`RETROSOC_PAD_POWER_CONNECTIONS" @@ -173,9 +174,33 @@ def validate_connection(value: Any, field: str) -> str | None: raise ValueError(f"{field} must be a signal, bit-select, constant, or null") -def read_map(path: Path) -> tuple[list[Pad], dict[str, dict[str, str | None]]]: +def read_document(path: Path) -> dict[str, Any]: with path.open(encoding="utf-8") as source: - document = json.load(source) + return json.load(source) + + +def parse_power_pads(document: dict[str, Any]) -> dict[str, int]: + counts = document.get("power_pads") + if counts is None: + return dict(IHP130_POWER_PAD_COUNTS) + if not isinstance(counts, dict) or set(counts) != set(POWER_PAD_KINDS): + raise ValueError(f"power_pads must contain exactly {list(POWER_PAD_KINDS)}") + parsed: dict[str, int] = {} + for kind in POWER_PAD_KINDS: + value = counts[kind] + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + raise ValueError(f"power_pads.{kind} must be a positive integer") + parsed[kind] = value + return parsed + + +def read_power_pad_counts(path: Path) -> dict[str, int]: + return parse_power_pads(read_document(path)) + + +def read_map(path: Path) -> tuple[list[Pad], dict[str, dict[str, str | None]]]: + document = read_document(path) + parse_power_pads(document) if document.get("schema_version") != 1: raise ValueError("schema_version must be 1") groups = document.get("pads") @@ -299,7 +324,10 @@ def render_pad_instance(pad: Pad) -> str | None: return None -def render_pad_bindings(pads: list[Pad]) -> str: +def render_pad_bindings( + pads: list[Pad], power_counts: dict[str, int] | None = None +) -> str: + counts = IHP130_POWER_PAD_COUNTS if power_counts is None else power_counts lines = [ "// Generated by scripts/rtl/generate_pin_map.py; do not edit.", "`ifdef PDK_IHP130", @@ -311,7 +339,7 @@ def render_pad_bindings(pads: list[Pad]) -> str: " generate", ] cell_suffixes = {"vdd": "Vdd", "vss": "Vss", "iovdd": "IOVdd", "iovss": "IOVss"} - for kind, count in IHP130_POWER_PAD_COUNTS.items(): + for kind, count in counts.items(): lines.extend( [ f" for (genvar i = 0; i < {count}; i++) begin : {kind}_pads", @@ -435,9 +463,12 @@ def render_profile_bindings(pads: list[Pad], bindings: dict[str, str | None]) -> def generate(map_path: Path, output_dir: Path) -> None: pads, profiles = read_map(map_path) + power_counts = read_power_pad_counts(map_path) rtl_dir = output_dir / "rtl" atomic_write(rtl_dir / "retrosoc_asic_ports.svh", render_ports(pads)) - atomic_write(rtl_dir / "retrosoc_asic_pad_bindings.svh", render_pad_bindings(pads)) + atomic_write( + rtl_dir / "retrosoc_asic_pad_bindings.svh", render_pad_bindings(pads, power_counts) + ) atomic_write(rtl_dir / "retrosoc_core_ports.svh", render_core_ports(pads)) atomic_write(rtl_dir / "retrosoc_core_bindings.svh", render_core_bindings(pads)) for profile, bindings in profiles.items(): diff --git a/tests/test_librelane_flow.py b/tests/test_librelane_flow.py index 4c48877d..521dfcd5 100644 --- a/tests/test_librelane_flow.py +++ b/tests/test_librelane_flow.py @@ -12,7 +12,7 @@ from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[1] PIN_MAP = ROOT / "rtl/mini/pin_map/pin_map.json" DOMAINS = ROOT / "rtl/mini/integration/clock_reset_domains.json" -FLOW_ROOT = ROOT / "physical/librelane" +FLOW_ROOT = ROOT / "physical/librelane/mini" def load_module(name: str, path: Path): @@ -83,11 +83,11 @@ def test_chip_config_places_every_signal_and_power_pad_once(tmp_path: Path) -> N assert config["EXTRA_EXCLUDED_CELLS"] == ["sg13g2_IOPad*"] assert config["VDD_NETS"] == ["VDD"] assert config["GND_NETS"] == ["VSS"] - assert config["PAD_CFG"].endswith("physical/librelane/pad_cfg.tcl") + assert config["PAD_CFG"].endswith("physical/librelane/mini/pad_cfg.tcl") assert config["PDN_ENABLE_PINS"] is True assert config["ERROR_ON_PDN_VIOLATIONS"] is True assert config["STA_EXTRA_CORNER_TCL_FILE"].endswith( - "physical/librelane/sta_report_limit.tcl" + "physical/librelane/mini/sta_report_limit.tcl" ) assert set(config["MACROS"]) == { "RM_IHPSG13_1P_4096x16_c3_bm_bist", @@ -200,7 +200,7 @@ def test_librelane_flow_exposes_core_and_chip_targets() -> None: assert "--skip OpenROAD.STAMidPNR" in makefile assert "--librelane-safe" in makefile assert "librelane-all" not in makefile - assert "include physical/librelane/Makefile" in top_makefile + assert "include physical/librelane/mini/Makefile" in top_makefile ihp_filelist = (ROOT / "rtl/filelist/pdk_ihp130.fl").read_text(encoding="utf-8") assert ihp_filelist.index("sg13g2_udp.v") < ihp_filelist.index("sg13g2_stdcell.v") assert lock["sources"]["pdk_ihp130"]["revision"] == ("970a7688e7dcce2a6172797df9ef47bde2f60f9f") @@ -209,7 +209,7 @@ def test_librelane_flow_exposes_core_and_chip_targets() -> None: def test_bondpad_lef_matches_the_generated_master_contract() -> None: - lef = (FLOW_ROOT / "bondpad/bondpad_70x70.lef").read_text(encoding="utf-8") + lef = (ROOT / "physical/librelane/bondpad/bondpad_70x70.lef").read_text(encoding="utf-8") assert "MACRO bondpad_70x70" in lef assert "CLASS COVER" in lef diff --git a/tests/test_librelane_tiny_flow.py b/tests/test_librelane_tiny_flow.py new file mode 100644 index 00000000..fd2da88f --- /dev/null +++ b/tests/test_librelane_tiny_flow.py @@ -0,0 +1,175 @@ +"""Tests for the Tiny IHP130 LibreLane chip flow.""" + +from __future__ import annotations + +import importlib.util +import re +from pathlib import Path +from types import SimpleNamespace + + +ROOT = Path(__file__).resolve().parents[1] +PIN_MAP = ROOT / "rtl/tiny/pin_map/pin_map.json" +DOMAINS = ROOT / "rtl/tiny/integration/clock_reset_domains.json" +FLOW_ROOT = ROOT / "physical/librelane/tiny" +BONDPAD_LEF = ROOT / "physical/librelane/bondpad/bondpad_70x70.lef" + + +def load_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def chip_arguments(tmp_path: Path) -> SimpleNamespace: + return SimpleNamespace( + pin_map=PIN_MAP, + rtl=tmp_path / "retrosoc_tiny_asic_sources.sv", + sdc=tmp_path / "retrosoc_tiny_chip.sdc", + pdn=FLOW_ROOT / "pdn_cfg.tcl", + bondpad_gds=tmp_path / "bondpad_70x70.gds", + bondpad_lef=BONDPAD_LEF, + sram_vh=FLOW_ROOT / "sram_blackboxes.vh", + ext_clk_hz=24_000_000, + have_sram_macro=True, + sram_size_kib=128, + output=tmp_path / "config.json", + ) + + +def test_chip_config_places_every_signal_and_power_pad_once(tmp_path: Path) -> None: + module = load_module( + "retrosoc_tiny_librelane_config", FLOW_ROOT / "scripts/generate_chip_config.py" + ) + config = module.build_config(chip_arguments(tmp_path)) + sides = {name: config[f"PAD_{name.upper()}"] for name in module.SIDE_ORDER} + placed = [instance for side in module.SIDE_ORDER for instance in sides[side]] + + assert len(placed) == 84 + assert len(placed) == len(set(placed)) + assert {side: len(instances) for side, instances in sides.items()} == { + "south": 19, + "east": 24, + "north": 17, + "west": 24, + } + assert sum(item.startswith("vdd_pads[") for item in placed) == 8 + assert sum(item.startswith("vss_pads[") for item in placed) == 8 + assert sum(item.startswith("iovdd_pads[") for item in placed) == 8 + assert sum(item.startswith("iovss_pads[") for item in placed) == 8 + assert "u_extclk_i_pad.u_sg13g2_IOPadIn" in sides["south"] + assert "u_jtag_tck_i_pad.u_sg13g2_IOPadIn" in sides["south"] + assert "u_uart0_tx_o_pad.u_sg13g2_IOPadOut4mA" in sides["south"] + assert "u_xpi_sck_o_pad.u_sg13g2_IOPadOut4mA" in sides["north"] + assert "u_gpio_0_io_pad.u_sg13g2_IOPadInOut4mA" in sides["east"] + assert "u_gpio_15_io_pad.u_sg13g2_IOPadInOut4mA" in sides["east"] + assert "u_gpio_16_io_pad.u_sg13g2_IOPadInOut4mA" in sides["west"] + assert "u_gpio_31_io_pad.u_sg13g2_IOPadInOut4mA" in sides["west"] + assert config["meta"] == {"version": 3, "flow": "Chip"} + assert config["DESIGN_NAME"] == "retrosoc_tiny_asic" + assert config["DIE_AREA"] == [0, 0, 4700, 4700] + assert config["CORE_AREA"] == [365, 365, 4335, 4335] + assert config["CLOCK_PORT"] == ["extclk_i_pad", "jtag_tck_i_pad"] + assert config["CLOCK_PERIOD"] == 1_000_000_000 / 24_000_000 + assert config["PL_TARGET_DENSITY_PCT"] == 45 + assert config["USE_SLANG"] is True + assert config["SLANG_ARGUMENTS"] == ["--keep-hierarchy"] + assert config["SYNTH_HIERARCHY_MODE"] == "deferred_flatten" + assert config["SYNTH_KEEP_HIERARCHY_MODULES"] == [ + "sg13g2_IOPadVdd", + "sg13g2_IOPadVss", + "sg13g2_IOPadIOVdd", + "sg13g2_IOPadIOVss", + ] + assert config["RUN_CTS"] is False + assert config["ERROR_ON_SYNTH_CHECKS"] is False + assert config["EXTRA_EXCLUDED_CELLS"] == ["sg13g2_IOPad*"] + assert config["VDD_NETS"] == ["VDD"] + assert config["GND_NETS"] == ["VSS"] + assert config["PAD_CFG"].endswith("physical/librelane/tiny/pad_cfg.tcl") + assert config["PDN_ENABLE_PINS"] is True + assert config["ERROR_ON_PDN_VIOLATIONS"] is True + assert config["STA_EXTRA_CORNER_TCL_FILE"].endswith( + "physical/librelane/tiny/sta_report_limit.tcl" + ) + assert config["PAD_BONDPAD_NAME"] == "bondpad_70x70" + assert set(config["MACROS"]) == {"RM_IHPSG13_1P_1024x32_c2_bm_bist"} + + +def test_chip_config_covers_every_onchip_sram_bank(tmp_path: Path) -> None: + module = load_module( + "retrosoc_tiny_librelane_sram_config", FLOW_ROOT / "scripts/generate_chip_config.py" + ) + config = module.build_config(chip_arguments(tmp_path)) + + macro = config["MACROS"]["RM_IHPSG13_1P_1024x32_c2_bm_bist"] + assert len(macro["instances"]) == 32 + assert ( + "u_soc.u_sram.gen_memory.gen_bank[0].u_ram.u_mem" in macro["instances"] + ) + assert ( + "u_soc.u_sram.gen_memory.gen_bank[31].u_ram.u_mem" in macro["instances"] + ) + for instance in macro["instances"].values(): + x, y = instance["location"] + assert 365 <= x <= 4335 + assert 365 <= y <= 4335 + assert instance["orientation"] == "N" + assert len(config["PDN_MACRO_CONNECTIONS"]) == 64 + assert any( + "gen_bank.*0.*\\.u_ram" in connection + for connection in config["PDN_MACRO_CONNECTIONS"] + ) + blackboxes = (FLOW_ROOT / "sram_blackboxes.vh").read_text(encoding="utf-8") + assert "module RM_IHPSG13_1P_1024x32_c2_bm_bist" in blackboxes + assert "RM_IHPSG13_1P_4096x16_c3_bm_bist" not in blackboxes + + +def test_chip_sdc_is_pad_aware_and_does_not_false_path_all_io() -> None: + module = load_module( + "retrosoc_tiny_librelane_sdc", FLOW_ROOT / "scripts/generate_sdc.py" + ) + arguments = SimpleNamespace( + domains=DOMAINS, + pin_map=PIN_MAP, + ext_clk_hz=24_000_000, + ) + sdc = module.render(arguments) + + assert "u_extclk_i_pad.u_sg13g2_IOPadIn/p2c" in sdc + assert "u_jtag_tck_i_pad.u_sg13g2_IOPadIn/p2c" in sdc + assert "u_clock_buffer.u_sg13g2_buf_1/X" in sdc + assert "create_clock -name clk_external -period 41.6666666667" in sdc + assert "create_clock -name clk_jtag -period 100" in sdc + assert "create_generated_clock -name clk_system" in sdc + assert "set_clock_groups -name retrosoc_async -asynchronous" in sdc + assert "-group [get_clocks {clk_external clk_system}]" in sdc + assert "-group [get_clocks {clk_jtag}]" in sdc + assert "set_input_delay -max" in sdc + assert "set_output_delay -max" in sdc + assert "set_false_path -from $reset_ext_rst_n_i_pad" in sdc + assert "set_false_path -from $reset_jtag_trst_n_i_pad" in sdc + assert "set_false_path -from [all_inputs]" not in sdc + assert "set_false_path -to [all_outputs]" not in sdc + + +def test_tiny_flow_exposes_chip_target_only() -> None: + makefile = (FLOW_ROOT / "Makefile").read_text(encoding="utf-8") + top_makefile = (ROOT / "Makefile").read_text(encoding="utf-8") + + assert "librelane-chip:" in makefile + assert "librelane-core:" not in makefile + assert "--soc TINY" in makefile + assert "--source-filelist-dir" in makefile + assert re.search(r"ifeq \(\$\(SOC\),TINY\)\ninclude physical/librelane/tiny/Makefile", top_makefile) + assert "include physical/librelane/mini/Makefile" in top_makefile + doctor = (FLOW_ROOT / "scripts/doctor.py").read_text(encoding="utf-8") + assert "EXPECTED_SIGNAL_PADS = 52" in doctor + pad_cfg = (FLOW_ROOT / "pad_cfg.tcl").read_text(encoding="utf-8") + assert "vdd sg13g2_IOPadVdd 8" in pad_cfg + assert "iovss sg13g2_IOPadIOVss 8" in pad_cfg + pdn = (FLOW_ROOT / "pdn_cfg.tcl").read_text(encoding="utf-8") + assert "RM_IHPSG13_1P_1024x32_c2_bm_bist" in pdn + assert "RM_IHPSG13_1P_4096x16_c3_bm_bist" not in pdn diff --git a/tests/test_pin_map.py b/tests/test_pin_map.py index 0c0d7102..95bc7594 100644 --- a/tests/test_pin_map.py +++ b/tests/test_pin_map.py @@ -183,6 +183,52 @@ def test_pin_map_requires_explicit_bidirectional_peripheral_signals(tmp_path: Pa assert "requires input, output, and output_enable" in result.stderr +def test_pin_map_tiny_power_pads_override(tmp_path: Path) -> None: + subprocess.run( + [ + sys.executable, + str(GENERATOR), + "--map", + str(ROOT / "rtl/tiny/pin_map/pin_map.json"), + "--output-dir", + str(tmp_path), + ], + check=True, + ) + pads = (tmp_path / "rtl/retrosoc_asic_pad_bindings.svh").read_text(encoding="utf-8") + + assert "for (genvar i = 0; i < 8; i++) begin : vdd_pads" in pads + assert "for (genvar i = 0; i < 8; i++) begin : vss_pads" in pads + assert "for (genvar i = 0; i < 8; i++) begin : iovdd_pads" in pads + assert "for (genvar i = 0; i < 8; i++) begin : iovss_pads" in pads + assert "i < 24" not in pads + assert "i < 16" not in pads + + +def test_pin_map_rejects_incomplete_power_pads(tmp_path: Path) -> None: + document = json.loads(PIN_MAP.read_text(encoding="utf-8")) + document["power_pads"] = {"vdd": 24, "vss": 24, "iovdd": 16} + invalid_map = tmp_path / "incomplete-power-pads.json" + invalid_map.write_text(json.dumps(document), encoding="utf-8") + + result = validate(invalid_map) + + assert result.returncode != 0 + assert "power_pads" in result.stderr + + +def test_pin_map_rejects_non_positive_power_pads(tmp_path: Path) -> None: + document = json.loads(PIN_MAP.read_text(encoding="utf-8")) + document["power_pads"] = {"vdd": 0, "vss": 24, "iovdd": 16, "iovss": 16} + invalid_map = tmp_path / "non-positive-power-pads.json" + invalid_map.write_text(json.dumps(document), encoding="utf-8") + + result = validate(invalid_map) + + assert result.returncode != 0 + assert "power_pads.vdd" in result.stderr + + def test_fpga_constraints_reference_declared_top_ports() -> None: top_ports = set( re.findall(