Files
retroSoC/physical/librelane/tiny/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

173 lines
6.0 KiB
Python

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