Files
retroSoC/physical/librelane/mini/scripts/generate_sdc.py
T
maksyuki c8187284d0 feat(tiny): add scripts for SDC generation and packaging
- Introduced  to create pad-aware timing constraints for the Tiny IHP130 chip flow.
- Added  to create a checksummed delivery archive for the Tiny LibreLane implementation.
- Created  for synthesis-only header of the locked IHP SRAM macro physical views.
- Implemented  to limit OpenSTA report enumeration for path counts.
- Updated  to include power pad definitions.
- Enhanced  to handle power pad counts and validation.
- Added tests for Tiny flow and pin map validation, including power pad overrides and constraints.
2026-09-27 08:48:12 +08:00

194 lines
7.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate pad-aware multi-clock constraints for the IHP130 Chip flow."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(ROOT))
from scripts.rtl.generate_pin_map import Pad, ihp130_pad_instance, read_map # noqa: E402
from scripts.check_clock_reset_domains import validate # noqa: E402
from scripts.setup_helpers import atomic_write # noqa: E402
CLOCK_PADS = {
"external": "extclk_i_pad",
"audio": "audclk_i_pad",
"jtag": "jtag_tck_i_pad",
"dvp": "gpio_10_io_pad",
"usb2_ulpi": "usb2_ulpi_clk_i_pad",
}
RESET_PADS = {"ext_rst_n_i_pad", "jtag_trst_n_i_pad"}
IHP130_SYSTEM_CLOCK_PIN = "u_rcu.u_sys_clk_buf.u_sg13g2_buf_1/X"
def positive_integer(value: str) -> int:
parsed = int(value)
if parsed <= 0:
raise argparse.ArgumentTypeError("value must be positive")
return parsed
def timing_clock(pad: Pad) -> str:
if pad.name.startswith("jtag_"):
return "jtag"
if pad.name.startswith("usb2_ulpi_"):
return "usb2_ulpi"
if pad.name.startswith("gpio_") and pad.index is not None and 11 <= pad.index <= 20:
return "dvp"
return "external"
def logical_directions(pad: Pad) -> tuple[bool, bool]:
input_kinds = {"tri_input", "schmitt_input"}
output_kinds = {"tri_output", "peripheral_output"}
bidir_kinds = {"full", "peripheral_bidir", "sdram_dq", "xpi_data"}
if pad.kind in input_kinds:
return True, False
if pad.kind in output_kinds:
return False, True
if pad.kind in bidir_kinds:
return True, True
return False, False
def render(args: argparse.Namespace) -> str:
validate(args.domains, ROOT)
target = getattr(args, "target", "chip")
if target not in {"chip", "core"}:
raise ValueError("target must be chip or core")
pads, _ = read_map(args.pin_map)
active = [pad for pad in pads if pad.feature is None or args.have_pll]
by_name = {pad.name: pad for pad in active}
for name in CLOCK_PADS.values():
if name not in by_name:
raise ValueError(f"clock PAD is missing from the active pin map: {name}")
periods = {
"external": 1_000_000_000 / args.ext_clk_hz,
"audio": 1_000_000_000 / args.aud_clk_hz,
"jtag": 100.0,
"dvp": 41.666666667,
"usb2_ulpi": 16.666666667,
}
groups: dict[str, dict[str, list[str]]] = {
name: {"inputs": [], "outputs": []} for name in periods
}
excluded = set(CLOCK_PADS.values()) | RESET_PADS
for pad in active:
if pad.name in excluded or (pad.kind == "xtal" and not pad.bind):
continue
is_input, is_output = logical_directions(pad)
clock = timing_clock(pad)
if is_input:
groups[clock]["inputs"].append(pad.name)
if is_output:
groups[clock]["outputs"].append(pad.name)
lines = [
"# Generated by physical/librelane/mini/scripts/generate_sdc.py; do not edit.",
"current_design $::env(DESIGN_NAME)",
"set_units -time ns",
"",
"proc require_pins {label name} {",
" set objects [get_pins -quiet $name]",
' if {$objects eq ""} { error "required SDC pin is missing: $label ($name)" }',
" return $objects",
"}",
"proc require_ports {label name} {",
" set objects [get_ports -quiet $name]",
' if {$objects eq ""} { error "required SDC port is missing: $label ($name)" }',
" return $objects",
"}",
"",
]
clock_vars: dict[str, str] = {}
for name, pad_name in CLOCK_PADS.items():
variable = f"clk_{name}_pin"
if target == "chip":
instance = ihp130_pad_instance(by_name[pad_name])
if instance is None:
raise ValueError(f"clock PAD has no IHP130 physical instance: {pad_name}")
lines.append(f'set {variable} [require_pins "clock {name}" {{{instance}/p2c}}]')
else:
lines.append(f'set {variable} [require_ports "clock {name}" {{{pad_name}}}]')
lines.append(f"create_clock -name clk_{name} -period {periods[name]:.12g} ${variable}")
lines.append("")
clock_vars[name] = variable
lines.extend(
[
f'set clk_system_pin [require_pins "clock system" {{{IHP130_SYSTEM_CLOCK_PIN}}}]',
"create_generated_clock -name clk_system -source $clk_external_pin "
"-divide_by 1 $clk_system_pin",
"",
"set_clock_groups -name retrosoc_async -asynchronous \\",
" -group [get_clocks {clk_external clk_system}] \\",
" -group [get_clocks {clk_audio}] \\",
" -group [get_clocks {clk_jtag}] \\",
" -group [get_clocks {clk_dvp}] \\",
" -group [get_clocks {clk_usb2_ulpi}]",
"",
"set all_retrosoc_clocks [get_clocks {clk_external clk_system clk_audio clk_jtag clk_dvp clk_usb2_ulpi}]",
"set_clock_uncertainty -setup 0.2 $all_retrosoc_clocks",
"set_clock_uncertainty -hold 0.1 $all_retrosoc_clocks",
"set_clock_transition 0.1 $all_retrosoc_clocks",
"set_timing_derate -early 0.95",
"set_timing_derate -late 1.05",
"set_max_fanout 10 [current_design]",
"",
]
)
for reset in sorted(RESET_PADS):
lines.extend(
[
f'set reset_{reset} [require_ports "reset {reset}" {{{reset}}}]',
f"set_false_path -from $reset_{reset}",
"",
]
)
for clock, port_groups in groups.items():
delay = periods[clock] * 0.2
for direction, ports in port_groups.items():
if not ports:
continue
variable = f"{clock}_{direction}"
lines.append(f"set {variable} [get_ports {{{' '.join(ports)}}}]")
if direction == "inputs":
lines.append(f"set_input_delay -min 0 -clock clk_{clock} ${variable}")
lines.append(f"set_input_delay -max {delay:.12g} -clock clk_{clock} ${variable}")
else:
lines.append(f"set_output_delay -min 0 -clock clk_{clock} ${variable}")
lines.append(f"set_output_delay -max {delay:.12g} -clock clk_{clock} ${variable}")
lines.append(f"set_load 0.006 ${variable}")
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--domains", type=Path, required=True)
parser.add_argument("--pin-map", type=Path, required=True)
parser.add_argument("--ext-clk-hz", type=positive_integer, required=True)
parser.add_argument("--aud-clk-hz", type=positive_integer, required=True)
parser.add_argument("--have-pll", action="store_true")
parser.add_argument("--target", choices=("chip", "core"), default="chip")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
atomic_write(args.output, render(args))
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())