mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: enhance core-STA integration and timing analysis
- Updated soc_topology.json to include new fabric links for core, DMA, native, and APB interfaces. - Refactored retrosoc.sv to utilize generated fabric links and included necessary SVH files for better modularity. - Improved check_clock_reset_domains.py to validate hierarchical pins and added support for positive number checks. - Modified regress.py to include OpenSTA in the regression commands. - Enhanced README.md in the STA directory to clarify the timing analysis flow and supported PDK profiles. - Removed outdated gen2.sdc file and replaced it with a dynamic SDC generation approach. - Updated opensta.mk to generate SDC files dynamically based on the current domain map and pin map. - Created generate_sdc.py to automate SDC generation from clock/reset domain inventory. - Added tests for generated core-STA constraints to ensure coverage of current clock domains and validate pin map integrity. - Introduced new timing metrics and warnings for the sky130 PDK in the quality checks.
This commit is contained in:
30 files changed
+719
-149
No files matched your search
@@ -98,8 +98,7 @@ jobs:
|
||||
SIMU=IVERILOG SYNTH=YOSYS STA=NONE doctor
|
||||
make CONFIG=${{ inputs.profile }} SIMU=VERILATOR doctor
|
||||
|
||||
- name: Check IHP130 timing environment
|
||||
if: inputs.pdk == 'IHP130'
|
||||
- name: Check timing environment
|
||||
run: make CONFIG=${{ inputs.profile }} SIMU=IVERILOG STA=OPENSTA doctor
|
||||
|
||||
- name: Run ${{ inputs.suite }} regression
|
||||
|
||||
@@ -123,8 +123,8 @@ endif
|
||||
endif
|
||||
|
||||
ifeq ($(STA),OPENSTA)
|
||||
ifneq ($(PDK),IHP130)
|
||||
$(error OpenSTA currently supports PDK=IHP130 only)
|
||||
ifeq ($(HAVE_PLL),YES)
|
||||
$(error STA=OPENSTA requires a qualified PDK PLL timing profile; HAVE_PLL=YES is unsupported)
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
@@ -122,9 +122,9 @@ make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG \
|
||||
| Assembly self-test with Icarus | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG RTL_SIM_TIMEOUT=5200000 sim-asm` |
|
||||
| Yosys synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SYNTH=YOSYS synth` |
|
||||
| Icarus netlist simulation after synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG netsim` |
|
||||
| OpenSTA timing analysis after synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk STA=OPENSTA sta` |
|
||||
| OpenSTA core timing analysis after synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk STA=OPENSTA sta` |
|
||||
`make regress-pr` runs the supported IHP130, GF180, and SKY130 PR matrices in
|
||||
sequence. IHP130 additionally covers OpenSTA timing analysis.
|
||||
sequence, including slow-corner OpenSTA core timing analysis for each PDK.
|
||||
| Pull-request regression suite | `make regress-pr` |
|
||||
| Nightly regression suite | `make regress-nightly` |
|
||||
| Format C, Makefile, and RTL sources | `make format` |
|
||||
|
||||
+9
-5
@@ -5,15 +5,19 @@
|
||||
| Tier | Profile | Automated coverage |
|
||||
| --- | --- | --- |
|
||||
| Pull request | `configs/ci/hazard3-rv32im-ihp130.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
|
||||
| Pull request | `configs/ci/hazard3-rv32im-gf180.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus |
|
||||
| Pull request | `configs/ci/hazard3-rv32im-sky130.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus |
|
||||
| Pull request | `configs/ci/hazard3-rv32im-gf180.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
|
||||
| Pull request | `configs/ci/hazard3-rv32im-sky130.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
|
||||
| Pull request | `configs/ci/mdd-rv32im-ihp130.mk` | firmware and Verilator |
|
||||
| Nightly | `configs/nightly/picorv32-rv32im-ihp130.mk` | firmware, Verilator, and Icarus |
|
||||
| Cluster | `configs/cluster/hazard3-ics55.mk` | configuration only; run with site PDK/tool access |
|
||||
|
||||
OpenSTA timing signoff remains IHP130-only. GF180 and SKY130 use their TT standard-cell libraries
|
||||
for synthesis and functional netlist simulation, but do not publish an STA result until their
|
||||
corner, SDC, and frequency policy have been independently qualified.
|
||||
OpenSTA runs a reproducible core-STA baseline for every CI PDK: IHP130 uses
|
||||
`slow_1p08V_125C`, GF180 uses `ss_125C_4v50`, and SKY130 uses `ss_100C_1v40`.
|
||||
The SDC is generated from `rtl/mini/integration/clock_reset_domains.json`, applies
|
||||
the external, audio, and DVP clocks at their technology-buffer observation pins,
|
||||
and groups unrelated clocks asynchronously. It intentionally excludes package,
|
||||
board, pad-ring, extraction, and PLL timing, so it is a regression-quality core
|
||||
analysis rather than physical signoff.
|
||||
|
||||
VCS and ICS55 flows remain cluster-only because public runners do not have the licensed simulator or
|
||||
the validated site environment. They use the same variant layout, manifests, result JSON, warning
|
||||
|
||||
+12
-6
@@ -11,12 +11,18 @@ work. PDK changes require the affected synthesis, timing, physical-design, or
|
||||
FPGA validation flow and the ownership review defined by `.github/CODEOWNERS`.
|
||||
|
||||
The supported CI PDKs are IHP130, GF180, and SKY130. GF180 assembles its
|
||||
locked `gf180mcu_fd_sc_mcu7t5v0` TT 5.0 V cell fragments, and SKY130 generates
|
||||
its `sky130_fd_sc_hd` TT 1.8 V Liberty model from locked upstream JSON files.
|
||||
Both derived Liberty files are stored below `.cache/retrosoc/pdk/` and are not
|
||||
committed. SKY130 setup initializes only the locked HD library submodule;
|
||||
the remaining upstream libraries are outside the SoC integration boundary.
|
||||
OpenSTA timing remains IHP130-only.
|
||||
locked `gf180mcu_fd_sc_mcu7t5v0` Liberty fragments and the two SoC-used IO
|
||||
cells (`in_c` and `bi_t`) at both its TT and core-STA slow corners. SKY130
|
||||
generates its `sky130_fd_sc_hd` Liberty model from locked upstream JSON files
|
||||
at TT and slow corners. Derived Liberty files are stored below
|
||||
`.cache/retrosoc/pdk/` and are not committed. SKY130 setup initializes only
|
||||
the locked HD library submodule; the remaining upstream libraries are outside
|
||||
the SoC integration boundary.
|
||||
|
||||
All three PDK profiles run slow-corner OpenSTA core timing in CI. The analysis
|
||||
uses the PDK standard-cell Liberty plus the linked IO-cell model required to
|
||||
read the synthesized top-level wrapper. It excludes board, pad-ring,
|
||||
extraction, and PLL timing and is therefore not a physical signoff result.
|
||||
|
||||
The GF180 and SKY130 CI flows synth the digital core and run functional
|
||||
standard-cell netlist simulation. Their generic SoC wrappers explicitly use
|
||||
|
||||
@@ -12,37 +12,51 @@ sys.path.insert(0, str(ROOT))
|
||||
from scripts.setup_helpers import atomic_write # noqa: E402
|
||||
|
||||
|
||||
CORNER = "tt_025C_5v00"
|
||||
LIBRARY = "gf180mcu_fd_sc_mcu7t5v0"
|
||||
DEFAULT_CORNER = "tt_025C_5v00"
|
||||
DEFAULT_LIBRARY = "gf180mcu_fd_sc_mcu7t5v0"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Assemble the GF180 standard-cell Liberty model from locked PDK fragments"
|
||||
description="Assemble a GF180 Liberty model from locked PDK fragments"
|
||||
)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument("--revision", required=True)
|
||||
parser.add_argument("--library", default=DEFAULT_LIBRARY)
|
||||
parser.add_argument("--cell", action="append")
|
||||
parser.add_argument("--output-name")
|
||||
parser.add_argument("--corner", default=DEFAULT_CORNER)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
source = args.source.resolve()
|
||||
library_dir = source / "libraries" / LIBRARY / "latest"
|
||||
header = library_dir / "liberty" / f"{LIBRARY}__{CORNER}.lib"
|
||||
library_dir = source / "libraries" / args.library / "latest"
|
||||
header = library_dir / "liberty" / f"{args.library}__{args.corner}.lib"
|
||||
cells_dir = library_dir / "cells"
|
||||
cell_libraries = sorted(cells_dir.glob(f"*/*__{CORNER}.lib"))
|
||||
if args.cell:
|
||||
cell_libraries = [
|
||||
cells_dir / cell / f"{args.library}__{cell}__{args.corner}.lib"
|
||||
for cell in sorted(set(args.cell))
|
||||
]
|
||||
else:
|
||||
cell_libraries = sorted(cells_dir.glob(f"*/*__{args.corner}.lib"))
|
||||
if not header.is_file() or not cell_libraries:
|
||||
raise FileNotFoundError(f"missing GF180 Liberty inputs below {library_dir}")
|
||||
|
||||
output_dir = args.output_dir.resolve()
|
||||
output = output_dir / f"{LIBRARY}__{CORNER}.lib"
|
||||
output_name = args.output_name or f"{args.library}__{args.corner}.lib"
|
||||
output = output_dir / output_name
|
||||
revision = output.with_suffix(output.suffix + ".revision")
|
||||
revision_contents = (
|
||||
f"{args.revision}\n{args.library}\n{','.join(sorted(set(args.cell or ())))}\n"
|
||||
)
|
||||
if (
|
||||
output.is_file()
|
||||
and revision.is_file()
|
||||
and revision.read_text(encoding="utf-8") == f"{args.revision}\n"
|
||||
and revision.read_text(encoding="utf-8") == revision_contents
|
||||
):
|
||||
print(f"GF180 Liberty is ready: {output}")
|
||||
return 0
|
||||
@@ -52,7 +66,7 @@ def main() -> int:
|
||||
raise RuntimeError(f"GF180 Liberty header is malformed: {header}")
|
||||
cells = "\n\n".join(path.read_text(encoding="utf-8") for path in cell_libraries)
|
||||
atomic_write(output, f"{prefix}\n\n{cells}\n}}\n")
|
||||
atomic_write(revision, f"{args.revision}\n")
|
||||
atomic_write(revision, revision_contents)
|
||||
print(f"generated GF180 Liberty: {output}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ sys.path.insert(0, str(ROOT))
|
||||
from scripts.setup_helpers import atomic_write # noqa: E402
|
||||
|
||||
|
||||
CORNER = "tt_025C_1v80"
|
||||
DEFAULT_CORNER = "tt_025C_1v80"
|
||||
LIBRARY = "sky130_fd_sc_hd"
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument("--revision", required=True)
|
||||
parser.add_argument("--corner", default=DEFAULT_CORNER)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ def main() -> int:
|
||||
generator_dir = source / "scripts" / "python-skywater-pdk"
|
||||
required = (
|
||||
library_dir / "timing" / f"{LIBRARY}__common.lib.json",
|
||||
library_dir / "timing" / f"{LIBRARY}__{CORNER}.lib.json",
|
||||
library_dir / "timing" / f"{LIBRARY}__{args.corner}.lib.json",
|
||||
generator_dir / "skywater_pdk" / "liberty.py",
|
||||
)
|
||||
missing = [str(path) for path in required if not path.is_file()]
|
||||
@@ -44,7 +45,7 @@ def main() -> int:
|
||||
raise FileNotFoundError("missing SKY130 Liberty input(s): " + ", ".join(missing))
|
||||
|
||||
output_dir = args.output_dir.resolve()
|
||||
output = output_dir / f"{LIBRARY}__{CORNER}.lib"
|
||||
output = output_dir / f"{LIBRARY}__{args.corner}.lib"
|
||||
revision = output.with_suffix(output.suffix + ".revision")
|
||||
if (
|
||||
output.is_file()
|
||||
@@ -62,7 +63,7 @@ def main() -> int:
|
||||
"-m",
|
||||
"skywater_pdk.liberty",
|
||||
str(library_dir),
|
||||
CORNER,
|
||||
args.corner,
|
||||
"--output_directory",
|
||||
str(temporary_dir),
|
||||
)
|
||||
|
||||
@@ -49,6 +49,44 @@ def main() -> int:
|
||||
),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
(
|
||||
sys.executable,
|
||||
str(ROOT / "pdk/generate_gf180_liberty.py"),
|
||||
"--source",
|
||||
str(destination),
|
||||
"--output-dir",
|
||||
str(ROOT / ".cache/retrosoc/pdk/gf180"),
|
||||
"--revision",
|
||||
dependency["revision"],
|
||||
"--corner",
|
||||
"ss_125C_4v50",
|
||||
),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
(
|
||||
sys.executable,
|
||||
str(ROOT / "pdk/generate_gf180_liberty.py"),
|
||||
"--source",
|
||||
str(destination),
|
||||
"--output-dir",
|
||||
str(ROOT / ".cache/retrosoc/pdk/gf180"),
|
||||
"--revision",
|
||||
dependency["revision"],
|
||||
"--library",
|
||||
"gf180mcu_fd_io",
|
||||
"--cell",
|
||||
"bi_t",
|
||||
"--cell",
|
||||
"in_c",
|
||||
"--corner",
|
||||
"ss_125C_4v50",
|
||||
"--output-name",
|
||||
"gf180mcu_fd_io_retrosoc__ss_125C_4v50.lib",
|
||||
),
|
||||
check=True,
|
||||
)
|
||||
elif name == "pdk_sky130":
|
||||
subprocess.run(
|
||||
(
|
||||
@@ -63,6 +101,21 @@ def main() -> int:
|
||||
),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
(
|
||||
sys.executable,
|
||||
str(ROOT / "pdk/generate_sky130_liberty.py"),
|
||||
"--source",
|
||||
str(destination),
|
||||
"--output-dir",
|
||||
str(ROOT / ".cache/retrosoc/pdk/sky130"),
|
||||
"--revision",
|
||||
dependency["revision"],
|
||||
"--corner",
|
||||
"ss_100C_1v40",
|
||||
),
|
||||
check=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"count": 1,
|
||||
"example": "1171:$ROOT/.cache/retrosoc/pdk/sky130/sky130_fd_sc_hd__ss_100C_1v40.lib line 24, default_fanout_load is 0.0.",
|
||||
"signature": "3c380feb1d0cd00a"
|
||||
}
|
||||
],
|
||||
"profile": "hazard3-rv32im-sky130",
|
||||
"schema_version": 1,
|
||||
"tool": "opensta"
|
||||
}
|
||||
@@ -46,6 +46,7 @@ module nmi_dvp (
|
||||
// dvp clk
|
||||
logic s_dvp_pclk_buf;
|
||||
logic s_dvp_pclk_rst_n_sync;
|
||||
logic s_dvp_recven_pclk_q;
|
||||
// rx fifo
|
||||
logic s_rx_push_valid, s_rx_full, s_rx_empty;
|
||||
logic s_rx_pop_valid, s_rx_pop_ready;
|
||||
@@ -125,6 +126,17 @@ module nmi_dvp (
|
||||
.rst_n_o(s_dvp_pclk_rst_n_sync)
|
||||
);
|
||||
|
||||
// The software enable is stable while a frame is captured, but it originates
|
||||
// in the system domain and must be synchronized before it gates DVP writes.
|
||||
cdc_sync #(
|
||||
.STAGE (2),
|
||||
.DATA_WIDTH(1)
|
||||
) u_dvp_recven_cdc_sync (
|
||||
.clk_i (s_dvp_pclk_buf),
|
||||
.rst_n_i(s_dvp_pclk_rst_n_sync),
|
||||
.dat_i (s_dvp_recven_q),
|
||||
.dat_o (s_dvp_recven_pclk_q)
|
||||
);
|
||||
|
||||
async_fifo #(
|
||||
.DATA_WIDTH (32),
|
||||
@@ -132,7 +144,7 @@ module nmi_dvp (
|
||||
) u_rx_async_fifo (
|
||||
.wr_clk_i (s_dvp_pclk_buf),
|
||||
.wr_rst_n_i(s_dvp_pclk_rst_n_sync),
|
||||
.wr_en_i (s_rx_push_valid & s_dvp_recven_q),
|
||||
.wr_en_i (s_rx_push_valid & s_dvp_recven_pclk_q),
|
||||
.wr_data_i (s_rx_push_data),
|
||||
.wr_full_o (s_rx_full),
|
||||
.rd_clk_i (clk_i),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"reset_ports": ["ext_rst_n_i_pad"],
|
||||
"domains": [
|
||||
{
|
||||
"name": "external",
|
||||
@@ -8,7 +9,13 @@
|
||||
"reset_source": "ext_rst_n_i",
|
||||
"reset_primitive": "rst_sync",
|
||||
"path": "rtl/mini/top/rcu.sv",
|
||||
"instance": "u_ext_rst_sync"
|
||||
"instance": "u_ext_rst_sync",
|
||||
"sta": {
|
||||
"source_port": "extclk_i_pad",
|
||||
"pin": "u_rcu.u_ext_clk_buf/clk_o",
|
||||
"period_ns": 13.888888889,
|
||||
"async_group": "external"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
@@ -17,7 +24,13 @@
|
||||
"reset_source": "ext_rst_n_i",
|
||||
"reset_primitive": "rst_sync",
|
||||
"path": "rtl/mini/top/rcu.sv",
|
||||
"instance": "u_sys_rst_sync"
|
||||
"instance": "u_sys_rst_sync",
|
||||
"sta": {
|
||||
"source_domain": "external",
|
||||
"pin": "u_rcu.u_sys_clk_buf/clk_o",
|
||||
"period_ns": 13.888888889,
|
||||
"async_group": "external"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
@@ -26,7 +39,28 @@
|
||||
"reset_source": "ext_rst_n_i",
|
||||
"reset_primitive": "rst_sync",
|
||||
"path": "rtl/mini/top/rcu.sv",
|
||||
"instance": "u_aud_rst_sync"
|
||||
"instance": "u_aud_rst_sync",
|
||||
"sta": {
|
||||
"source_port": "audclk_i_pad",
|
||||
"pin": "u_rcu.u_aud_clk_buf/clk_o",
|
||||
"period_ns": 54.253472222,
|
||||
"async_group": "audio"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dvp",
|
||||
"clock": "s_dvp_pclk_buf",
|
||||
"reset": "s_dvp_pclk_rst_n_sync",
|
||||
"reset_source": "rst_n_i",
|
||||
"reset_primitive": "rst_sync",
|
||||
"path": "rtl/ip/native/multimedia/dvp.sv",
|
||||
"instance": "u_dvp_pclk_rst_sync",
|
||||
"sta": {
|
||||
"source_port": "gpio_10_io_pad",
|
||||
"pin": "u_retrosoc.u_ip_nmi_wrapper.u_nmi_dvp.u_dvp_pclk_clk_buf/clk_o",
|
||||
"period_ns": 41.666666667,
|
||||
"async_group": "dvp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"crossings": [
|
||||
@@ -45,6 +79,22 @@
|
||||
"primitive": "cdc_2phase",
|
||||
"path": "rtl/mini/top/rcu.sv",
|
||||
"instance": "u_pll_response_cdc"
|
||||
},
|
||||
{
|
||||
"name": "dvp_enable",
|
||||
"source": "system",
|
||||
"destination": "dvp",
|
||||
"primitive": "cdc_sync",
|
||||
"path": "rtl/ip/native/multimedia/dvp.sv",
|
||||
"instance": "u_dvp_recven_cdc_sync"
|
||||
},
|
||||
{
|
||||
"name": "dvp_fifo",
|
||||
"source": "dvp",
|
||||
"destination": "system",
|
||||
"primitive": "async_fifo",
|
||||
"path": "rtl/ip/native/multimedia/dvp.sv",
|
||||
"instance": "u_rx_async_fifo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,6 +16,7 @@ from typing import Any
|
||||
SV_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
|
||||
SV_REFERENCE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*(?:\[[0-9]+\])?$")
|
||||
SV_CONSTANT_RE = re.compile(r"(?:1'[bB][01]|'[01])$")
|
||||
FABRIC_LINK_NAMES = ("core", "dma", "native", "apb")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -37,6 +38,12 @@ class ApbTarget:
|
||||
requires_ip: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FabricLink:
|
||||
name: str
|
||||
interface: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GpioMode:
|
||||
inputs: tuple[str, ...]
|
||||
@@ -287,6 +294,31 @@ def parse_apb_targets(
|
||||
return ordered
|
||||
|
||||
|
||||
def parse_fabric_links(value: Any) -> list[FabricLink]:
|
||||
if not isinstance(value, list) or len(value) != len(FABRIC_LINK_NAMES):
|
||||
raise ValueError("fabric_links must define every SoC fabric role exactly once")
|
||||
links: list[FabricLink] = []
|
||||
names: set[str] = set()
|
||||
interfaces: set[str] = set()
|
||||
for index, entry in enumerate(value):
|
||||
link = require_object(entry, f"fabric_links[{index}]")
|
||||
name = require_identifier(link.get("name"), f"fabric_links[{index}].name")
|
||||
interface = require_identifier(link.get("interface"), f"fabric_links[{index}].interface")
|
||||
if name not in FABRIC_LINK_NAMES:
|
||||
raise ValueError(f"fabric_links[{index}].name is not a supported SoC fabric role")
|
||||
if name in names:
|
||||
raise ValueError(f"SoC fabric role {name} is duplicated")
|
||||
if interface in interfaces:
|
||||
raise ValueError(f"SoC fabric interface {interface} is duplicated")
|
||||
names.add(name)
|
||||
interfaces.add(interface)
|
||||
links.append(FabricLink(name, interface))
|
||||
if names != set(FABRIC_LINK_NAMES):
|
||||
missing = ", ".join(sorted(set(FABRIC_LINK_NAMES) - names))
|
||||
raise ValueError(f"fabric_links does not cover required roles: {missing}")
|
||||
return sorted(links, key=lambda item: FABRIC_LINK_NAMES.index(item.name))
|
||||
|
||||
|
||||
def parse_gpio_mode(value: Any, field: str) -> GpioMode:
|
||||
mode = require_object(value, field)
|
||||
inputs_value = mode.get("inputs")
|
||||
@@ -332,7 +364,7 @@ def parse_gpio_functions(value: Any, gpio_pins: int) -> list[GpioFunction]:
|
||||
|
||||
def read_topology(
|
||||
topology_path: Path, memory_map_path: Path, ip: str
|
||||
) -> tuple[list[NativeTarget], list[ApbTarget], list[GpioFunction]]:
|
||||
) -> tuple[list[NativeTarget], list[ApbTarget], list[FabricLink], list[GpioFunction]]:
|
||||
document = require_object(json.loads(topology_path.read_text(encoding="utf-8")), "topology")
|
||||
if document.get("schema_version") != 1:
|
||||
raise ValueError("schema_version must be 1")
|
||||
@@ -343,6 +375,7 @@ def read_topology(
|
||||
return (
|
||||
parse_native_targets(document.get("native_targets"), memory_regions),
|
||||
parse_apb_targets(document.get("apb_targets"), memory_regions, ip),
|
||||
parse_fabric_links(document.get("fabric_links")),
|
||||
parse_gpio_functions(document.get("gpio_alt_functions"), gpio_pins),
|
||||
)
|
||||
|
||||
@@ -518,6 +551,36 @@ def render_apb_response_mux(targets: list[ApbTarget]) -> str:
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_fabric_interfaces(links: list[FabricLink]) -> str:
|
||||
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
|
||||
lines.extend(f" nmi_if {link.interface} ();" for link in links)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_fabric_connection(links: list[FabricLink], name: str, port: str) -> str:
|
||||
interface = next(link.interface for link in links if link.name == name)
|
||||
return "\n".join(
|
||||
[
|
||||
"// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit.",
|
||||
f" .{port}({interface}),",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render_bus_fabric_connections(links: list[FabricLink]) -> str:
|
||||
port_names = {
|
||||
"core": "core_nmi",
|
||||
"dma": "dma_nmi",
|
||||
"native": "natv_nmi",
|
||||
"apb": "apb_nmi",
|
||||
}
|
||||
interfaces = {link.name: link.interface for link in links}
|
||||
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
|
||||
lines.extend(f" .{port_names[name]}({interfaces[name]})," for name in FABRIC_LINK_NAMES)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_gpio_bindings(functions: list[GpioFunction]) -> str:
|
||||
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
|
||||
for function in functions:
|
||||
@@ -538,7 +601,9 @@ def render_gpio_bindings(functions: list[GpioFunction]) -> str:
|
||||
|
||||
|
||||
def generate(topology_path: Path, memory_map_path: Path, output_dir: Path, ip: str) -> None:
|
||||
targets, apb_targets, gpio_functions = read_topology(topology_path, memory_map_path, ip)
|
||||
targets, apb_targets, fabric_links, gpio_functions = read_topology(
|
||||
topology_path, memory_map_path, ip
|
||||
)
|
||||
memory_regions = read_memory_regions(memory_map_path)
|
||||
rtl_dir = output_dir / "rtl"
|
||||
atomic_write(rtl_dir / "soc_nmi_interfaces.svh", render_nmi_interfaces(targets))
|
||||
@@ -551,6 +616,21 @@ def generate(topology_path: Path, memory_map_path: Path, output_dir: Path, ip: s
|
||||
atomic_write(rtl_dir / "soc_apb_request_routes.svh", render_apb_request_routes(apb_targets))
|
||||
atomic_write(rtl_dir / "soc_apb_select_routes.svh", render_apb_select_routes(apb_targets))
|
||||
atomic_write(rtl_dir / "soc_apb_response_mux.svh", render_apb_response_mux(apb_targets))
|
||||
atomic_write(rtl_dir / "soc_fabric_interfaces.svh", render_fabric_interfaces(fabric_links))
|
||||
atomic_write(
|
||||
rtl_dir / "soc_core_wrapper_fabric.svh",
|
||||
render_fabric_connection(fabric_links, "core", "nmi"),
|
||||
)
|
||||
atomic_write(rtl_dir / "soc_bus_fabric.svh", render_bus_fabric_connections(fabric_links))
|
||||
atomic_write(
|
||||
rtl_dir / "soc_ip_nmi_wrapper_fabric.svh",
|
||||
render_fabric_connection(fabric_links, "native", "nmi")
|
||||
+ render_fabric_connection(fabric_links, "dma", "dma_nmi"),
|
||||
)
|
||||
atomic_write(
|
||||
rtl_dir / "soc_ip_apb_wrapper_fabric.svh",
|
||||
render_fabric_connection(fabric_links, "apb", "nmi"),
|
||||
)
|
||||
atomic_write(rtl_dir / "soc_gpio_alt_bindings.svh", render_gpio_bindings(gpio_functions))
|
||||
atomic_write(output_dir / "soc_topology.fl", f"+incdir+{rtl_dir}\n")
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
{"slot": 16, "name": "opipsram", "interface": "u_opipsram_nmi_if", "disabled": true},
|
||||
{"slot": 17, "name": "i2c1", "interface": "u_i2c1_nmi_if", "regions": ["NMI_I2C1"]}
|
||||
],
|
||||
"fabric_links": [
|
||||
{"name": "core", "interface": "u_core_nmi_if"},
|
||||
{"name": "dma", "interface": "u_dma_nmi_if"},
|
||||
{"name": "native", "interface": "u_nmi_nmi_if"},
|
||||
{"name": "apb", "interface": "u_apb_nmi_if"}
|
||||
],
|
||||
"apb_targets": [
|
||||
{"slot": 0, "name": "archinfo", "timed_interface": "u_archinfo_apb_if", "pure_interface": "u_archinfo_apb_pure_if", "region": "APB_ARCHINFO"},
|
||||
{"slot": 1, "name": "rng", "timed_interface": "u_rng_apb_if", "pure_interface": "u_rng_apb_pure_if", "region": "APB_RNG"},
|
||||
|
||||
@@ -33,11 +33,8 @@ module retrosoc (
|
||||
);
|
||||
|
||||
// verilog_format: off
|
||||
// bus interface
|
||||
nmi_if u_core_nmi_if ();
|
||||
nmi_if u_dma_nmi_if ();
|
||||
nmi_if u_nmi_nmi_if ();
|
||||
nmi_if u_apb_nmi_if ();
|
||||
// Generated fabric links reuse the common nmi_if contract.
|
||||
`include "soc_fabric_interfaces.svh"
|
||||
user_gpio_if u_user_gpio_if ();
|
||||
// ip interface
|
||||
gpio_if u_gpio_if ();
|
||||
@@ -94,7 +91,7 @@ core_wrapper u_core_wrapper (
|
||||
`ifdef CORE_MDD
|
||||
.core_sel_i(core_sel_i),
|
||||
`endif
|
||||
.nmi (u_core_nmi_if),
|
||||
`include "soc_core_wrapper_fabric.svh"
|
||||
.irq_i (s_irq)
|
||||
);
|
||||
|
||||
@@ -104,10 +101,7 @@ core_wrapper u_core_wrapper (
|
||||
`ifdef HAVE_SRAM_IF
|
||||
.ram (ram),
|
||||
`endif
|
||||
.core_nmi (u_core_nmi_if),
|
||||
.dma_nmi (u_dma_nmi_if),
|
||||
.natv_nmi (u_nmi_nmi_if),
|
||||
.apb_nmi (u_apb_nmi_if),
|
||||
`include "soc_bus_fabric.svh"
|
||||
.fault_valid_o (s_bus_fault_valid),
|
||||
.fault_addr_o (s_bus_fault_addr),
|
||||
.fault_wstrb_o (s_bus_fault_wstrb),
|
||||
@@ -119,7 +113,7 @@ core_wrapper u_core_wrapper (
|
||||
.rst_n_i (rst_n_i),
|
||||
.clk_aud_i (clk_aud_i),
|
||||
.rst_aud_n_i (rst_aud_n_i),
|
||||
.nmi (u_nmi_nmi_if),
|
||||
`include "soc_ip_nmi_wrapper_fabric.svh"
|
||||
.gpio (u_gpio_if),
|
||||
.user_gpio (u_user_gpio_if),
|
||||
.uart (uart0),
|
||||
@@ -129,7 +123,6 @@ core_wrapper u_core_wrapper (
|
||||
.i2s (u_i2s_if),
|
||||
.onewire (u_onewire_if),
|
||||
.xpi (xpi),
|
||||
.dma_nmi (u_dma_nmi_if),
|
||||
.sysctrl (u_sysctrl_if),
|
||||
.pll_ctrl (pll_ctrl),
|
||||
.sdram (sdram),
|
||||
@@ -150,7 +143,7 @@ core_wrapper u_core_wrapper (
|
||||
.clk_aud_i (clk_aud_i),
|
||||
.rst_aud_n_i(rst_aud_n_i),
|
||||
.tmr_capch_i(s_tmr_capch),
|
||||
.nmi (u_apb_nmi_if),
|
||||
`include "soc_ip_apb_wrapper_fabric.svh"
|
||||
.uart (u_uart1_if),
|
||||
.pwm (u_pwm_if),
|
||||
.ps2 (u_ps2_if),
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any
|
||||
RESET_PRIMITIVES = {"rst_sync"}
|
||||
CDC_PRIMITIVES = {"async_fifo", "cdc_2phase", "cdc_sync"}
|
||||
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
|
||||
HIERARCHICAL_PIN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_.]*/[A-Za-z_][A-Za-z0-9_]*$")
|
||||
|
||||
|
||||
def require_object(value: Any, field: str) -> dict[str, Any]:
|
||||
@@ -34,6 +35,19 @@ def require_identifier(value: Any, field: str) -> str:
|
||||
return identifier
|
||||
|
||||
|
||||
def require_positive_number(value: Any, field: str) -> float:
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool) or value <= 0:
|
||||
raise ValueError(f"{field} must be a positive number")
|
||||
return float(value)
|
||||
|
||||
|
||||
def require_hierarchical_pin(value: Any, field: str) -> str:
|
||||
pin = require_string(value, field)
|
||||
if HIERARCHICAL_PIN_RE.fullmatch(pin) is None:
|
||||
raise ValueError(f"{field} must be a hierarchical instance pin")
|
||||
return pin
|
||||
|
||||
|
||||
def require_instance(path: Path, primitive: str, instance: str, field: str) -> None:
|
||||
if not path.is_file():
|
||||
raise ValueError(f"{field}.path does not exist: {path}")
|
||||
@@ -45,13 +59,14 @@ def require_instance(path: Path, primitive: str, instance: str, field: str) -> N
|
||||
|
||||
def validate(document_path: Path, root: Path) -> None:
|
||||
document = require_object(json.loads(document_path.read_text(encoding="utf-8")), "domain map")
|
||||
if document.get("schema_version") != 1:
|
||||
raise ValueError("schema_version must be 1")
|
||||
if document.get("schema_version") != 2:
|
||||
raise ValueError("schema_version must be 2")
|
||||
|
||||
raw_domains = document.get("domains")
|
||||
if not isinstance(raw_domains, list) or not raw_domains:
|
||||
raise ValueError("domains must be a non-empty list")
|
||||
names: set[str] = set()
|
||||
sta_sources: dict[str, str | None] = {}
|
||||
for index, value in enumerate(raw_domains):
|
||||
field = f"domains[{index}]"
|
||||
domain = require_object(value, field)
|
||||
@@ -68,6 +83,23 @@ def validate(document_path: Path, root: Path) -> None:
|
||||
path = root / require_string(domain.get("path"), f"{field}.path")
|
||||
instance = require_identifier(domain.get("instance"), f"{field}.instance")
|
||||
require_instance(path, primitive, instance, field)
|
||||
sta = require_object(domain.get("sta"), f"{field}.sta")
|
||||
source_port = sta.get("source_port")
|
||||
source_domain = sta.get("source_domain")
|
||||
if (source_port is None) == (source_domain is None):
|
||||
raise ValueError(f"{field}.sta must declare exactly one source")
|
||||
if source_port is not None:
|
||||
require_identifier(source_port, f"{field}.sta.source_port")
|
||||
sta_sources[name] = None
|
||||
else:
|
||||
sta_sources[name] = require_identifier(source_domain, f"{field}.sta.source_domain")
|
||||
require_hierarchical_pin(sta.get("pin"), f"{field}.sta.pin")
|
||||
require_positive_number(sta.get("period_ns"), f"{field}.sta.period_ns")
|
||||
require_identifier(sta.get("async_group"), f"{field}.sta.async_group")
|
||||
|
||||
for name, source_domain in sta_sources.items():
|
||||
if source_domain is not None and source_domain not in names:
|
||||
raise ValueError(f"domain {name} STA source references unknown domain {source_domain}")
|
||||
|
||||
raw_crossings = document.get("crossings")
|
||||
if not isinstance(raw_crossings, list):
|
||||
|
||||
@@ -78,6 +78,7 @@ def pdk_pr_commands(profile: str) -> tuple[tuple[str, tuple[str, ...]], ...]:
|
||||
(profile, ("SIMU=VERILATOR", "HAVE_SVA=YES", "firmware", "sim")),
|
||||
(profile, ("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm")),
|
||||
(profile, ("SYNTH=YOSYS", "synth")),
|
||||
(profile, ("STA=OPENSTA", "sta")),
|
||||
(
|
||||
profile,
|
||||
(
|
||||
|
||||
+12
-4
@@ -4,13 +4,21 @@ This directory contains OpenSTA constraints, Tcl setup, and Make integration
|
||||
for retroSoC timing analysis. The selected profile and synthesized netlist
|
||||
define the actual timing context.
|
||||
|
||||
Run synthesis before timing analysis, then invoke the supported OpenSTA flow:
|
||||
Run synthesis before timing analysis, then invoke the supported OpenSTA flow.
|
||||
All CI PDK profiles are supported:
|
||||
|
||||
```sh
|
||||
make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SYNTH=YOSYS synth
|
||||
make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk STA=OPENSTA sta
|
||||
make CONFIG=configs/ci/hazard3-rv32im-gf180.mk SYNTH=YOSYS synth
|
||||
make CONFIG=configs/ci/hazard3-rv32im-gf180.mk STA=OPENSTA sta
|
||||
make CONFIG=configs/ci/hazard3-rv32im-sky130.mk SYNTH=YOSYS synth
|
||||
make CONFIG=configs/ci/hazard3-rv32im-sky130.mk STA=OPENSTA sta
|
||||
```
|
||||
|
||||
Review WNS/TNS and warning changes through the generated reports and quality
|
||||
checks. Timing results are PDK/tool/profile dependent; do not compare unrelated
|
||||
variants as a gate.
|
||||
The generated SDC derives clock and reset objects from
|
||||
`rtl/mini/integration/clock_reset_domains.json`. It analyzes internal core
|
||||
paths at the PDK slow corner and treats the external, audio, and DVP clocks as
|
||||
separate asynchronous groups. Review WNS/TNS and warning changes through the
|
||||
generated reports and quality checks. This flow excludes board, pad-ring,
|
||||
extraction, and PLL timing; it is not a physical signoff constraint set.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Mini SoC default external-safe-clock timing mode.
|
||||
# Dynamic PLL timing modes are enabled only after a PDK backend supplies a
|
||||
# characterized PLL macro and a glitch-free switch cell.
|
||||
|
||||
set clk_ext_freq 72.0
|
||||
set clk_aud_freq 18.432
|
||||
set clk_ext_period [expr 1000.0 / $clk_ext_freq]
|
||||
set clk_aud_period [expr 1000.0 / $clk_aud_freq]
|
||||
|
||||
create_clock -name clk_ext -period $clk_ext_period [get_ports extclk_i_pad]
|
||||
create_clock -name clk_aud -period $clk_aud_period [get_ports audclk_i_pad]
|
||||
create_generated_clock -name clk_i2s_mclk_o -source [get_pins \u_rcu.u_aud_clk_buf/clk_o] \
|
||||
-divide_by 1 [get_ports gpio_10_io_pad]
|
||||
|
||||
set_input_transition .1 [all_inputs]
|
||||
set_clock_uncertainty -setup 0.2 [get_clocks clk_ext]
|
||||
set_clock_uncertainty -hold 0.1 [get_clocks clk_ext]
|
||||
set_clock_uncertainty -setup 0.2 [get_clocks clk_aud]
|
||||
set_clock_uncertainty -hold 0.1 [get_clocks clk_aud]
|
||||
set_clock_uncertainty -setup 0.2 [get_clocks clk_i2s_mclk_o]
|
||||
set_clock_uncertainty -hold 0.1 [get_clocks clk_i2s_mclk_o]
|
||||
|
||||
set_timing_derate -cell_delay -net_delay -late 1
|
||||
set_timing_derate -net_delay -clock -early 0.95
|
||||
set_timing_derate -net_delay -data -early 1
|
||||
set_timing_derate -cell_delay -clock -early 0.95
|
||||
set_timing_derate -cell_delay -data -early 1
|
||||
set_clock_groups -name cgp_async -asynchronous -group [get_clocks clk_ext] \
|
||||
-group [get_clocks clk_aud]
|
||||
|
||||
set_false_path -from [get_cells \u_rcu.u_ext_rst_sync*]
|
||||
set_false_path -from [get_cells \u_rcu.u_aud_rst_sync*]
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate core-STA constraints from the active SoC clock/reset inventory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from rtl.mini.pin_map.generate_pin_map import read_map # noqa: E402
|
||||
from scripts.check_clock_reset_domains import validate # noqa: E402
|
||||
from scripts.setup_helpers import atomic_write # noqa: E402
|
||||
|
||||
|
||||
def require_object(value: Any, field: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{field} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def require_string(value: Any, field: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"{field} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def parse_document(path: Path, pin_map: Path) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
validate(path, ROOT)
|
||||
document = require_object(json.loads(path.read_text(encoding="utf-8")), "domain map")
|
||||
pads, _ = read_map(pin_map)
|
||||
pad_names = {pad.name for pad in pads}
|
||||
reset_ports = document.get("reset_ports")
|
||||
if not isinstance(reset_ports, list) or not reset_ports:
|
||||
raise ValueError("reset_ports must be a non-empty list")
|
||||
parsed_resets = [require_string(port, "reset_ports entry") for port in reset_ports]
|
||||
if len(parsed_resets) != len(set(parsed_resets)):
|
||||
raise ValueError("reset_ports contains duplicates")
|
||||
for port in parsed_resets:
|
||||
if port not in pad_names:
|
||||
raise ValueError(f"reset port is not declared in pin map: {port}")
|
||||
|
||||
domains = document["domains"]
|
||||
names = {domain["name"] for domain in domains}
|
||||
for domain in domains:
|
||||
sta = domain["sta"]
|
||||
source_port = sta.get("source_port")
|
||||
if source_port is not None and source_port not in pad_names:
|
||||
raise ValueError(f"domain {domain['name']} source port is not declared in pin map")
|
||||
source_domain = sta.get("source_domain")
|
||||
if source_domain is not None and source_domain not in names:
|
||||
raise ValueError(f"domain {domain['name']} source domain is not declared")
|
||||
return domains, parsed_resets
|
||||
|
||||
|
||||
def render(domains: list[dict[str, Any]], reset_ports: list[str]) -> str:
|
||||
lines = [
|
||||
"# Generated by sta/opensta/generate_sdc.py; do not edit.",
|
||||
"# Core-STA baseline: pad/package timing is intentionally excluded.",
|
||||
"",
|
||||
"proc require_pins {label name} {",
|
||||
" set objects [get_pins $name]",
|
||||
" if {$objects eq \"\"} {",
|
||||
" error \"required SDC object is missing: $label ($name)\"",
|
||||
" }",
|
||||
" return $objects",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
pin_variables: dict[str, str] = {}
|
||||
for domain in domains:
|
||||
name = domain["name"]
|
||||
sta = domain["sta"]
|
||||
pin_variable = f"clk_{name}_pin"
|
||||
pin_variables[name] = pin_variable
|
||||
lines.append(
|
||||
f"set {pin_variable} [require_pins \"clock {name}\" {{{sta['pin']}}}]"
|
||||
)
|
||||
period = f"{float(sta['period_ns']):.12g}"
|
||||
source_domain = sta.get("source_domain")
|
||||
if source_domain is None:
|
||||
lines.append(f"create_clock -name clk_{name} -period {period} ${pin_variable}")
|
||||
else:
|
||||
lines.append(
|
||||
f"create_generated_clock -name clk_{name} -source ${pin_variables[source_domain]} "
|
||||
f"-divide_by 1 ${pin_variable}"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
clock_names = " ".join(f"clk_{domain['name']}" for domain in domains)
|
||||
lines.extend(
|
||||
[
|
||||
f"set_clock_uncertainty -setup 0.2 [get_clocks {{{clock_names}}}]",
|
||||
f"set_clock_uncertainty -hold 0.1 [get_clocks {{{clock_names}}}]",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for domain in domains:
|
||||
lines.append(f"set_clock_transition 0.1 [get_clocks {{clk_{domain['name']}}}]")
|
||||
lines.append("")
|
||||
groups: dict[str, list[str]] = {}
|
||||
for domain in domains:
|
||||
groups.setdefault(domain["sta"]["async_group"], []).append(f"clk_{domain['name']}")
|
||||
if len(groups) > 1:
|
||||
lines.append("set_clock_groups -name retrosoc_async -asynchronous \\")
|
||||
group_items = list(groups.values())
|
||||
for index, clocks in enumerate(group_items):
|
||||
continuation = " \\" if index != len(group_items) - 1 else ""
|
||||
lines.append(f" -group [get_clocks {{{' '.join(clocks)}}}]{continuation}")
|
||||
lines.append("")
|
||||
|
||||
for reset_port in reset_ports:
|
||||
lines.extend(
|
||||
[
|
||||
f"set reset_{reset_port} [get_ports {{{reset_port}}}]",
|
||||
f"if {{$reset_{reset_port} eq \"\"}} {{",
|
||||
f" error \"required reset port is missing: {reset_port}\"",
|
||||
"}",
|
||||
f"set_false_path -from $reset_{reset_port}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"# This profile closes sequential core paths only. Board and pad timing",
|
||||
"# are supplied by a later board-specific SDC profile.",
|
||||
"set_false_path -from [all_inputs]",
|
||||
"set_false_path -to [all_outputs]",
|
||||
"",
|
||||
]
|
||||
)
|
||||
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("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
domains, reset_ports = parse_document(args.domains, args.pin_map)
|
||||
atomic_write(args.output, render(domains, reset_ports))
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
parser.error(str(error))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+20
-14
@@ -1,19 +1,25 @@
|
||||
|
||||
OPENSTA ?= sta
|
||||
OPENSTA_THREADS ?= $(JOBS)
|
||||
OPENSTA_NETLIST ?= $(SYN_BUILD_ROOT)/out/retrosoc_asic_yosys.v
|
||||
OPENSTA_LIBERTY ?= $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_stdcell/lib/sg13g2_stdcell_typ_1p20V_25C.lib
|
||||
OPENSTA_IO_LIB ?= $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_io/lib/sg13g2_io_typ_1p2V_3p3V_25C.lib
|
||||
OPENSTA_SRAM_LIBS ?= $(wildcard $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/lib/*_typ_1p20V_25C.lib)
|
||||
OPENSTA_SDC ?= $(ROOT_PATH)/sta/opensta/gen2.sdc
|
||||
OPENSTA_REPORT ?= $(STA_BUILD_ROOT)/retrosoc_sta.log
|
||||
OPENSTA_LOG ?= $(STA_BUILD_ROOT)/opensta.log
|
||||
OPENSTA_METRICS ?= $(STA_BUILD_ROOT)/timing_metrics.rpt
|
||||
OPENSTA_CONFIG ?= $(SYN_BUILD_ROOT)/out/retrosoc_asic_yosys.config
|
||||
OPENSTA ?= sta
|
||||
OPENSTA_THREADS ?= $(JOBS)
|
||||
OPENSTA_NETLIST ?= $(SYN_BUILD_ROOT)/out/retrosoc_asic_yosys.v
|
||||
OPENSTA_REPORT ?= $(STA_BUILD_ROOT)/retrosoc_sta.log
|
||||
OPENSTA_LOG ?= $(STA_BUILD_ROOT)/opensta.log
|
||||
OPENSTA_METRICS ?= $(STA_BUILD_ROOT)/timing_metrics.rpt
|
||||
OPENSTA_CONFIG ?= $(SYN_BUILD_ROOT)/out/retrosoc_asic_yosys.config
|
||||
OPENSTA_SDC ?= $(STA_BUILD_ROOT)/retrosoc_core.sdc
|
||||
OPENSTA_SDC_GEN := $(ROOT_PATH)/sta/opensta/generate_sdc.py
|
||||
OPENSTA_DOMAIN_MAP := $(ROOT_PATH)/rtl/mini/integration/clock_reset_domains.json
|
||||
OPENSTA_PIN_MAP := $(ROOT_PATH)/rtl/mini/pin_map/pin_map.json
|
||||
|
||||
sta: | manifest
|
||||
include $(ROOT_PATH)/sta/opensta/pdk_timing.mk
|
||||
|
||||
$(OPENSTA_SDC): $(OPENSTA_SDC_GEN) $(OPENSTA_DOMAIN_MAP) $(OPENSTA_PIN_MAP) | manifest
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(OPENSTA_SDC_GEN) --domains $(OPENSTA_DOMAIN_MAP) --pin-map $(OPENSTA_PIN_MAP) --output $@
|
||||
|
||||
sta: $(OPENSTA_SDC) | manifest
|
||||
@mkdir -p $(STA_BUILD_ROOT)
|
||||
@for input in $(OPENSTA_NETLIST) $(OPENSTA_LIBERTY) $(OPENSTA_IO_LIB) $(OPENSTA_SRAM_LIBS) $(OPENSTA_SDC) $(OPENSTA_CONFIG); do \
|
||||
@for input in $(OPENSTA_NETLIST) $(OPENSTA_LIBERTY) $(OPENSTA_LINK_LIBS) $(OPENSTA_SRAM_LIBS) $(OPENSTA_SDC) $(OPENSTA_CONFIG); do \
|
||||
test -f "$$input" || { echo "OpenSTA input missing: $$input" >&2; exit 1; }; \
|
||||
done
|
||||
@grep -qx 'PDK=$(PDK)' $(OPENSTA_CONFIG) || { \
|
||||
@@ -23,7 +29,7 @@ sta: | manifest
|
||||
python3 $(ROOT_PATH)/scripts/run_flow.py --tool opensta --log $(OPENSTA_LOG) \
|
||||
--result $(STA_BUILD_ROOT)/result-sta.json \
|
||||
--env OPENSTA_NETLIST=$(OPENSTA_NETLIST) --env OPENSTA_LIBERTY=$(OPENSTA_LIBERTY) \
|
||||
--env OPENSTA_IO_LIB=$(OPENSTA_IO_LIB) --env 'OPENSTA_SRAM_LIBS=$(OPENSTA_SRAM_LIBS)' \
|
||||
--env 'OPENSTA_LINK_LIBS=$(OPENSTA_LINK_LIBS)' --env 'OPENSTA_SRAM_LIBS=$(OPENSTA_SRAM_LIBS)' \
|
||||
--env OPENSTA_SDC=$(OPENSTA_SDC) --env OPENSTA_REPORT=$(OPENSTA_REPORT) \
|
||||
--env OPENSTA_METRICS=$(OPENSTA_METRICS) -- \
|
||||
$(OPENSTA) $(ROOT_PATH)/sta/opensta/opensta.tcl -threads $(OPENSTA_THREADS)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_IO_LIB OPENSTA_SRAM_LIBS OPENSTA_SDC OPENSTA_REPORT OPENSTA_METRICS} {
|
||||
foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_LINK_LIBS OPENSTA_SDC OPENSTA_REPORT OPENSTA_METRICS} {
|
||||
if {![info exists ::env($variable)] || $::env($variable) eq ""} {
|
||||
error "required environment variable is missing: $variable"
|
||||
}
|
||||
}
|
||||
|
||||
read_liberty $::env(OPENSTA_IO_LIB)
|
||||
read_liberty $::env(OPENSTA_LIBERTY)
|
||||
foreach liberty $::env(OPENSTA_SRAM_LIBS) {
|
||||
foreach liberty $::env(OPENSTA_LINK_LIBS) {
|
||||
read_liberty $liberty
|
||||
}
|
||||
read_liberty $::env(OPENSTA_LIBERTY)
|
||||
if {[info exists ::env(OPENSTA_SRAM_LIBS)] && $::env(OPENSTA_SRAM_LIBS) ne ""} {
|
||||
foreach liberty $::env(OPENSTA_SRAM_LIBS) {
|
||||
read_liberty $liberty
|
||||
}
|
||||
}
|
||||
read_verilog $::env(OPENSTA_NETLIST)
|
||||
link_design retrosoc_asic
|
||||
read_sdc $::env(OPENSTA_SDC)
|
||||
|
||||
report_checks -path_delay min_max -path_group {clk_ext clk_aud } -sort_by_slack -slack_max 0.0 -group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_REPORT)
|
||||
report_checks -path_delay min_max -sort_by_slack -slack_max 0.0 -group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_REPORT)
|
||||
|
||||
set metrics_file $::env(OPENSTA_METRICS)
|
||||
set metrics_tmp "${metrics_file}.tmp"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# PDK-specific Liberty inputs for the reproducible core-STA baseline.
|
||||
|
||||
ifeq ($(PDK),IHP130)
|
||||
OPENSTA_LIBERTY := $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_stdcell/lib/sg13g2_stdcell_slow_1p08V_125C.lib
|
||||
OPENSTA_LINK_LIBS := $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_io/lib/sg13g2_io_slow_1p08V_3p0V_125C.lib
|
||||
ifeq ($(HAVE_SRAM_MACRO),YES)
|
||||
OPENSTA_SRAM_LIBS := $(wildcard $(ROOT_PATH)/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/lib/*_slow_1p08V_125C.lib)
|
||||
endif
|
||||
else ifeq ($(PDK),GF180)
|
||||
OPENSTA_LIBERTY := $(ROOT_PATH)/.cache/retrosoc/pdk/gf180/gf180mcu_fd_sc_mcu7t5v0__ss_125C_4v50.lib
|
||||
OPENSTA_LINK_LIBS := $(ROOT_PATH)/.cache/retrosoc/pdk/gf180/gf180mcu_fd_io_retrosoc__ss_125C_4v50.lib
|
||||
else ifeq ($(PDK),SKY130)
|
||||
OPENSTA_LIBERTY := $(ROOT_PATH)/.cache/retrosoc/pdk/sky130/sky130_fd_sc_hd__ss_100C_1v40.lib
|
||||
OPENSTA_LINK_LIBS := $(ROOT_PATH)/sta/opensta/sky130_io_blackbox.liberty
|
||||
else
|
||||
$(error OpenSTA core-STA does not support PDK=$(PDK))
|
||||
endif
|
||||
|
||||
OPENSTA_SRAM_LIBS ?=
|
||||
@@ -1,20 +0,0 @@
|
||||
#120MHz
|
||||
set clk_period 8.33
|
||||
|
||||
create_clock -period $clk_period [get_ports xi_i_pad]
|
||||
create_clock -period $clk_period [get_ports extclk_i_pad]
|
||||
|
||||
create_generated_clock -name CLK_sys_peri -divide_by 1 -source u_xtal_io_pad.u_PXWE1W/XC [get_pins u_sys_mux.u_LVT_CKMUX2HDV4/Z]
|
||||
create_generated_clock -name CLK_flash_clk_o -divide_by 1 -source u_retrosoc.u_spimemio.xfer.flash_clk_reg/Q [get_ports flash_clk_o_pad]
|
||||
create_generated_clock -name CLK_cust_qspi_spi_clk_o -divide_by 1 -source u_retrosoc.u_axil_ip_wrapper.u_apb_spi_master.u_spictrl.u_clkgen.spi_clk_reg/Q [get_ports cust_qspi_spi_clk_o_pad]
|
||||
create_generated_clock -name CLK_cust_psram_sclk_o -divide_by 2 -source u_retrosoc.u_psram.sclk_reg/Q [get_ports cust_psram_sclk_o_pad]
|
||||
create_generated_clock -name CLK_cust_spfs_clk_o -divide_by 1 -source u_retrosoc.u_axil_ip_wrapper.u_spi_flash.u0_spi_top.clgen.clk_out_reg/Q [get_ports cust_spfs_clk_o_pad]
|
||||
|
||||
set_false_path -from [get_cells s_ext_rst_n_sync_reg]
|
||||
|
||||
set clk_period_factor .2
|
||||
set clk_delay [expr $clk_period * $clk_period_factor]
|
||||
# set_input_delay $clk_delay -clock clk {req_val reset resp_rdy req_msg[*]}
|
||||
# set_input_delay $clk_delay -clock xi_i_pad {}
|
||||
set_output_delay $clk_delay -clock xi_i_pad [all_outputs]
|
||||
set_input_transition .1 [all_inputs]
|
||||
@@ -1,20 +0,0 @@
|
||||
#120MHz
|
||||
set clk_period 8.33
|
||||
|
||||
create_clock -period $clk_period [get_ports xi_i_pad]
|
||||
create_clock -period $clk_period [get_ports extclk_i_pad]
|
||||
|
||||
create_generated_clock -name CLK_sys_peri -divide_by 1 -source u_xtal_io_pad/clk [get_pins u_sys_mux/clk_o]
|
||||
create_generated_clock -name CLK_flash_clk_o -divide_by 1 -source u_retrosoc/u_spimemio/xfer/flash_clk_reg/Q [get_ports flash_clk_o_pad]
|
||||
create_generated_clock -name CLK_cust_qspi_spi_clk_o -divide_by 1 -source u_retrosoc/u_axil_ip_wrapper/u_apb_spi_master/u_spictrl/u_clkgen/spi_clk_reg/Q [get_ports cust_qspi_spi_clk_o_pad]
|
||||
create_generated_clock -name CLK_cust_psram_sclk_o -divide_by 2 -source u_retrosoc/u_psram/sclk_reg/Q [get_ports cust_psram_sclk_o_pad]
|
||||
create_generated_clock -name CLK_cust_spfs_clk_o -divide_by 1 -source u_retrosoc/u_axil_ip_wrapper/u_spi_flash/u0_spi_top/clgen/clk_out_reg/Q [get_ports cust_spfs_clk_o_pad]
|
||||
|
||||
set_false_path -from [get_cells u_rst_sync/RST_SYNC_BLOCK[4].genblk1.u_sync_dffr/dat_o_reg]
|
||||
|
||||
set clk_period_factor .2
|
||||
set clk_delay [expr $clk_period * $clk_period_factor]
|
||||
# set_input_delay $clk_delay -clock clk {req_val reset resp_rdy req_msg[*]}
|
||||
# set_input_delay $clk_delay -clock xi_i_pad {}
|
||||
set_output_delay $clk_delay -clock xi_i_pad [all_outputs]
|
||||
set_input_transition .1 [all_inputs]
|
||||
@@ -6,5 +6,6 @@
|
||||
# its own generated-clock period (24, 48, 72, 96, 120, 144, 168, 192 MHz),
|
||||
# and declare the external safe clock and PLL clock physically exclusive.
|
||||
#
|
||||
# The current IHP130 and ICS55 wrappers report PLL capability disabled, so
|
||||
# their valid signoff mode is sta/opensta/gen2.sdc.
|
||||
# The active core-STA flow generates its external-safe-clock constraints from
|
||||
# rtl/mini/integration/clock_reset_domains.json. The current IHP130 and ICS55
|
||||
# wrappers report PLL capability disabled, so this template remains unselected.
|
||||
@@ -0,0 +1,46 @@
|
||||
library (retrosoc_sky130_io_blackbox) {
|
||||
delay_model : table_lookup;
|
||||
time_unit : "1ns";
|
||||
capacitive_load_unit (1, pf);
|
||||
|
||||
type (DM_type) {
|
||||
base_type : array;
|
||||
data_type : bit;
|
||||
bit_width : 3;
|
||||
bit_from : 2;
|
||||
bit_to : 0;
|
||||
}
|
||||
|
||||
cell (sky130_fd_io__top_gpiov2) {
|
||||
pin (OUT) { direction : input; }
|
||||
pin (OE_N) { direction : input; }
|
||||
pin (HLD_H_N) { direction : input; }
|
||||
pin (INP_DIS) { direction : input; }
|
||||
pin (IB_MODE_SEL) { direction : input; }
|
||||
pin (ENABLE_H) { direction : input; }
|
||||
pin (ENABLE_VDDA_H) { direction : input; }
|
||||
pin (ENABLE_INP_H) { direction : input; }
|
||||
pin (ENABLE_VDDIO) { direction : input; }
|
||||
pin (ENABLE_VSWITCH_H) { direction : input; }
|
||||
pin (SLOW) { direction : input; }
|
||||
pin (VTRIP_SEL) { direction : input; }
|
||||
pin (HLD_OVR) { direction : input; }
|
||||
pin (ANALOG_EN) { direction : input; }
|
||||
pin (ANALOG_SEL) { direction : input; }
|
||||
pin (ANALOG_POL) { direction : input; }
|
||||
bus (DM) {
|
||||
bus_type : DM_type;
|
||||
direction : input;
|
||||
}
|
||||
pin (PAD) { direction : inout; }
|
||||
pin (PAD_A_NOESD_H) { direction : inout; }
|
||||
pin (PAD_A_ESD_0_H) { direction : inout; }
|
||||
pin (PAD_A_ESD_1_H) { direction : inout; }
|
||||
pin (AMUXBUS_A) { direction : inout; }
|
||||
pin (AMUXBUS_B) { direction : inout; }
|
||||
pin (IN) { direction : output; }
|
||||
pin (IN_H) { direction : output; }
|
||||
pin (TIE_HI_ESD) { direction : output; }
|
||||
pin (TIE_LO_ESD) { direction : output; }
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@ def write_invalid_map(tmp_path: Path, document: dict[str, object]) -> Path:
|
||||
def test_clock_reset_domain_inventory_matches_the_rcu() -> None:
|
||||
result = check(DOMAIN_MAP)
|
||||
assert result.returncode == 0, result.stderr
|
||||
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
|
||||
assert document["schema_version"] == 2
|
||||
assert {domain["name"] for domain in document["domains"]} == {
|
||||
"external",
|
||||
"system",
|
||||
"audio",
|
||||
"dvp",
|
||||
}
|
||||
|
||||
|
||||
def test_clock_reset_domain_inventory_rejects_unknown_domain_and_instance(tmp_path: Path) -> None:
|
||||
@@ -44,3 +52,9 @@ def test_clock_reset_domain_inventory_rejects_unknown_domain_and_instance(tmp_pa
|
||||
result = check(write_invalid_map(tmp_path, document))
|
||||
assert result.returncode != 0
|
||||
assert "does not declare rst_sync instance" in result.stderr
|
||||
|
||||
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
|
||||
document["domains"][1]["sta"]["source_domain"] = "unknown"
|
||||
result = check(write_invalid_map(tmp_path, document))
|
||||
assert result.returncode != 0
|
||||
assert "STA source references unknown domain" in result.stderr
|
||||
@@ -376,6 +376,7 @@ def test_pdk_pr_regressions_cover_firmware_rtl_and_netlist() -> None:
|
||||
assert any("SIMU=VERILATOR" in values and "firmware" in values for values in command_values)
|
||||
assert any("SIMU=IVERILOG" in values and "sim-asm" in values for values in command_values)
|
||||
assert any("SYNTH=YOSYS" in values and "synth" in values for values in command_values)
|
||||
assert ("STA=OPENSTA", "sta") in command_values
|
||||
assert any("SIMU=IVERILOG" in values and "netsim" in values for values in command_values)
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path
|
||||
apb_declarations = (tmp_path / "rtl/soc_apb_declarations.svh").read_text(encoding="utf-8")
|
||||
apb_routes = (tmp_path / "rtl/soc_apb_request_routes.svh").read_text(encoding="utf-8")
|
||||
apb_response = (tmp_path / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
|
||||
fabric = (tmp_path / "rtl/soc_fabric_interfaces.svh").read_text(encoding="utf-8")
|
||||
bus_fabric = (tmp_path / "rtl/soc_bus_fabric.svh").read_text(encoding="utf-8")
|
||||
filelist = (tmp_path / "soc_topology.fl").read_text(encoding="utf-8")
|
||||
|
||||
assert interfaces.count("nmi_if u_") == 18
|
||||
@@ -82,6 +84,9 @@ def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path
|
||||
assert "assign tmr.paddr = nmi.addr;" in apb_routes
|
||||
assert "({32{s_psel_q[8]}} & tmr.prdata)" in apb_response
|
||||
assert "localparam int NSLV = 9;" in apb_declarations
|
||||
assert fabric.count("nmi_if u_") == 4
|
||||
assert ".core_nmi(u_core_nmi_if)" in bus_fabric
|
||||
assert ".apb_nmi(u_apb_nmi_if)" in bus_fabric
|
||||
assert filelist.startswith("+incdir+")
|
||||
|
||||
|
||||
@@ -129,6 +134,12 @@ def test_topology_rejects_duplicate_region_and_disabled_owner(tmp_path: Path) ->
|
||||
assert result.returncode != 0
|
||||
assert "disabled but declares regions" in result.stderr
|
||||
|
||||
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
|
||||
document["fabric_links"][1]["name"] = "core"
|
||||
result = validate(write_invalid_topology(tmp_path, document))
|
||||
assert result.returncode != 0
|
||||
assert "fabric role core is duplicated" in result.stderr
|
||||
|
||||
|
||||
def test_topology_rejects_invalid_apb_target_ownership(tmp_path: Path) -> None:
|
||||
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Tests for generated core-STA constraints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOMAINS = ROOT / "rtl/mini/integration/clock_reset_domains.json"
|
||||
PIN_MAP = ROOT / "rtl/mini/pin_map/pin_map.json"
|
||||
GENERATOR = ROOT / "sta/opensta/generate_sdc.py"
|
||||
GF180_GENERATOR = ROOT / "pdk/generate_gf180_liberty.py"
|
||||
|
||||
|
||||
def generate(domains: Path, output: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(GENERATOR),
|
||||
"--domains",
|
||||
str(domains),
|
||||
"--pin-map",
|
||||
str(PIN_MAP),
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def test_core_sdc_covers_current_clock_domains(tmp_path: Path) -> None:
|
||||
output = tmp_path / "retrosoc_core.sdc"
|
||||
result = generate(DOMAINS, output)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
sdc = output.read_text(encoding="utf-8")
|
||||
assert "create_clock -name clk_external -period 13.888888889" in sdc
|
||||
assert "create_generated_clock -name clk_system" in sdc
|
||||
assert "create_clock -name clk_audio -period 54.253472222" in sdc
|
||||
assert "create_clock -name clk_dvp -period 41.666666667" in sdc
|
||||
assert "u_dvp_pclk_clk_buf/clk_o" in sdc
|
||||
assert "-group [get_clocks {clk_external clk_system}]" in sdc
|
||||
assert "set_clock_transition 0.1 [get_clocks {clk_dvp}]" in sdc
|
||||
assert "set_input_transition" not in sdc
|
||||
assert "set_false_path -from $reset_ext_rst_n_i_pad" in sdc
|
||||
|
||||
|
||||
def test_core_sdc_rejects_pads_missing_from_the_pin_map(tmp_path: Path) -> None:
|
||||
document = json.loads(DOMAINS.read_text(encoding="utf-8"))
|
||||
document["domains"][3]["sta"]["source_port"] = "missing_pad"
|
||||
invalid = tmp_path / "clock_reset_domains.json"
|
||||
invalid.write_text(json.dumps(document), encoding="utf-8")
|
||||
|
||||
result = generate(invalid, tmp_path / "retrosoc_core.sdc")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "source port is not declared in pin map" in result.stderr
|
||||
|
||||
|
||||
def test_gf180_liberty_generator_assembles_requested_io_cells(tmp_path: Path) -> None:
|
||||
library = "gf180mcu_fd_io"
|
||||
corner = "ss_125C_4v50"
|
||||
source = tmp_path / "source"
|
||||
library_dir = source / "libraries" / library / "latest"
|
||||
liberty_dir = library_dir / "liberty"
|
||||
cells_dir = library_dir / "cells"
|
||||
liberty_dir.mkdir(parents=True)
|
||||
(liberty_dir / f"{library}__{corner}.lib").write_text(
|
||||
"library (gf180_io) {\n}\n", encoding="utf-8"
|
||||
)
|
||||
for cell in ("bi_t", "in_c"):
|
||||
cell_dir = cells_dir / cell
|
||||
cell_dir.mkdir(parents=True)
|
||||
(cell_dir / f"{library}__{cell}__{corner}.lib").write_text(
|
||||
f"cell ({cell}) {{}}\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(GF180_GENERATOR),
|
||||
"--source",
|
||||
str(source),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
"--revision",
|
||||
"locked-revision",
|
||||
"--library",
|
||||
library,
|
||||
"--cell",
|
||||
"bi_t",
|
||||
"--cell",
|
||||
"in_c",
|
||||
"--corner",
|
||||
corner,
|
||||
"--output-name",
|
||||
"retrosoc_io.lib",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
output = output_dir / "retrosoc_io.lib"
|
||||
assert "library (gf180_io)" in output.read_text(encoding="utf-8")
|
||||
assert "cell (bi_t) {}" in output.read_text(encoding="utf-8")
|
||||
assert "cell (in_c) {}" in output.read_text(encoding="utf-8")
|
||||
assert (output.with_suffix(".lib.revision")).read_text(encoding="utf-8") == (
|
||||
"locked-revision\ngf180mcu_fd_io\nbi_t,in_c\n"
|
||||
)
|
||||
Reference in new issue
Block a user