mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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.
This commit is contained in:
29 files changed
+1520
-63
No files matched your search
@@ -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
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
@@ -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/<variant>/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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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/<variant>/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.
|
||||
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
+3
-3
@@ -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)],
|
||||
+2
-2
@@ -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",
|
||||
"",
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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)
|
||||
@@ -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/<variant>/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 <repo>'
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 '<empty>'}")
|
||||
|
||||
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 '<empty>'}"
|
||||
)
|
||||
|
||||
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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// 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
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"power_pads": {
|
||||
"vdd": 8,
|
||||
"vss": 8,
|
||||
"iovdd": 8,
|
||||
"iovss": 8
|
||||
},
|
||||
"pads": [
|
||||
{
|
||||
"kind": "tri_input",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user