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:
maksyuki committed 2026-07-24 17:59:35 +08:00
1 parent bace16bfeb
commit 87fdfe49d8
30 files changed
+719 -149

No files matched your search

+1 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+23 -9
View File
@@ -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
+5 -4
View File
@@ -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),
)
+53
View File
@@ -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"
}
+13 -1
View File
@@ -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),
+54 -4
View File
@@ -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"
}
]
}
+82 -2
View File
@@ -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")
+6
View File
@@ -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"},
+6 -13
View File
@@ -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),
+34 -2
View File
@@ -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):
+1
View File
@@ -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
View File
@@ -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.
-32
View File
@@ -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*]
+154
View File
@@ -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
View File
@@ -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)
+9 -5
View File
@@ -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"
+19
View File
@@ -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 ?=
-20
View File
@@ -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]
-20
View File
@@ -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]
+3 -2
View File
@@ -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.
+46
View File
@@ -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; }
}
}
+14
View File
@@ -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
+1
View File
@@ -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)
+11
View File
@@ -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"))
+115
View File
@@ -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"
)