mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
fix(physical): separate Tiny camera timing constraints
Add isolated default and camera LibreLane IO profiles, assigning camera inputs to the pixel clock and keeping ordinary GPIO constraints in default. Separate generated output roots and synchronize the commercial clock-domain inventory with the P9 DVP domain. Retained validation: nine LibreLane scenario tests and 50 commercial-flow/ Tiny reset-inventory tests pass. Constraints retain development assumptions; no commercial EDA, physical signoff or board timing qualification is claimed. Specification: docs/ip/tiny-soc.md, TINY-R2-P9.
This commit is contained in:
8 files changed
+124
-19
No files matched your search
@@ -18,7 +18,7 @@ TOP := retrosoc_tiny_asic
|
||||
# validated against rtl/tiny/integration/clock_reset_domains.json when the
|
||||
# commercial timing contract is generated. This adapter analyzes the SAFE24
|
||||
# boot scenario, explicitly constraining RCU mux selections and PLL enable.
|
||||
COMMERCIAL_CLOCK_DOMAINS := aon system memory peripheral jtag audio
|
||||
COMMERCIAL_CLOCK_DOMAINS := aon system memory peripheral jtag audio dvp_pixel
|
||||
|
||||
# Qualified-I/O interface groups and the budget variables each group requires.
|
||||
COMMERCIAL_IO_INTERFACES := JTAG XPI ASYNC
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
namespace eval product {}
|
||||
|
||||
proc product::expected_clock_domains {} {
|
||||
return {aon system memory peripheral jtag audio}
|
||||
return {aon system memory peripheral jtag audio dvp_pixel}
|
||||
}
|
||||
|
||||
proc product::custom_clock_domains {} {
|
||||
@@ -26,7 +26,7 @@ proc product::custom_clock_domains {} {
|
||||
}
|
||||
|
||||
proc product::clock_names {} {
|
||||
return {clk_aon clk_system clk_memory clk_peripheral clk_jtag clk_audio clk_sys_div2 clk_sys_div4 clk_clkout_source}
|
||||
return {clk_aon clk_system clk_memory clk_peripheral clk_jtag clk_audio clk_dvp_pixel clk_sys_div2 clk_sys_div4 clk_clkout_source}
|
||||
}
|
||||
|
||||
proc product::io_interface_groups {} {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
LIBRELANE_IO_PROFILE ?= default
|
||||
ifneq ($(LIBRELANE_IO_PROFILE),default)
|
||||
ifneq ($(LIBRELANE_IO_PROFILE),camera)
|
||||
$(error LIBRELANE_IO_PROFILE must be default or camera)
|
||||
endif
|
||||
endif
|
||||
|
||||
LIBRELANE_FLOW_ROOT := $(ROOT_PATH)/physical/librelane/tiny
|
||||
LIBRELANE_RUN_ROOT := $(VARIANT_ROOT)/physical/librelane/tiny/chip
|
||||
LIBRELANE_RUN_ROOT := $(VARIANT_ROOT)/physical/librelane/tiny/$(if $(filter camera,$(LIBRELANE_IO_PROFILE)),chip-camera,chip)
|
||||
LIBRELANE_INPUT_DIR := $(LIBRELANE_RUN_ROOT)/input
|
||||
LIBRELANE_RTL := $(LIBRELANE_INPUT_DIR)/retrosoc_tiny_asic_sources.sv
|
||||
LIBRELANE_SDC := $(LIBRELANE_INPUT_DIR)/retrosoc_tiny_chip.sdc
|
||||
@@ -8,8 +15,8 @@ LIBRELANE_BONDPAD_LEF := $(ROOT_PATH)/physical/librelane/bondpad/bondpad_70
|
||||
LIBRELANE_CONFIG := $(LIBRELANE_RUN_ROOT)/config.json
|
||||
LIBRELANE_LOG := $(LIBRELANE_RUN_ROOT)/librelane.log
|
||||
LIBRELANE_RESULT := $(LIBRELANE_RUN_ROOT)/result.json
|
||||
LIBRELANE_META_RESULT := $(META_DIR)/librelane-chip.json
|
||||
LIBRELANE_DOCTOR_RESULT := $(META_DIR)/librelane-chip-doctor.json
|
||||
LIBRELANE_META_RESULT := $(META_DIR)/librelane-chip$(if $(filter camera,$(LIBRELANE_IO_PROFILE)),-camera).json
|
||||
LIBRELANE_DOCTOR_RESULT := $(META_DIR)/librelane-chip$(if $(filter camera,$(LIBRELANE_IO_PROFILE)),-camera)-doctor.json
|
||||
LIBRELANE_FINAL_DIR := $(LIBRELANE_RUN_ROOT)/final
|
||||
LIBRELANE_ARCHIVE := $(LIBRELANE_RUN_ROOT)/retrosoc-tiny-ihp130-chip.tar.gz
|
||||
LIBRELANE_RUN_TAG ?= current
|
||||
@@ -76,7 +83,7 @@ $(LIBRELANE_SDC): $(LIBRELANE_FLOW_ROOT)/scripts/generate_sdc.py \
|
||||
@mkdir -p $(@D)
|
||||
$(FLOW_PYTHON) $(LIBRELANE_FLOW_ROOT)/scripts/generate_sdc.py \
|
||||
--domains $(LIBRELANE_CLOCK_DOMAINS) --pin-map $(LIBRELANE_PIN_MAP) \
|
||||
--ext-clk-hz $(EXT_CLK_HZ) --output $@
|
||||
--ext-clk-hz $(EXT_CLK_HZ) --io-profile $(LIBRELANE_IO_PROFILE) --output $@
|
||||
|
||||
$(LIBRELANE_BONDPAD_GDS): $(LIBRELANE_BONDPAD_SCRIPT) | librelane-check-pdk
|
||||
@mkdir -p $(@D)
|
||||
@@ -90,7 +97,7 @@ $(LIBRELANE_CONFIG): $(LIBRELANE_CONFIG_DEPS)
|
||||
--pin-map $(LIBRELANE_PIN_MAP) --rtl $(LIBRELANE_RTL) --sdc $(LIBRELANE_SDC) \
|
||||
--pdn $(LIBRELANE_PDN) --bondpad-gds $(LIBRELANE_BONDPAD_GDS) \
|
||||
--bondpad-lef $(LIBRELANE_BONDPAD_LEF) --sram-vh $(LIBRELANE_SRAM_VH) \
|
||||
--ext-clk-hz $(EXT_CLK_HZ) --sram-size-kib $(SRAM_SIZE_KIB) \
|
||||
--ext-clk-hz $(EXT_CLK_HZ) --io-profile $(LIBRELANE_IO_PROFILE) --sram-size-kib $(SRAM_SIZE_KIB) \
|
||||
$(if $(filter YES,$(HAVE_SRAM_MACRO)),--have-sram-macro) \
|
||||
--output $@
|
||||
|
||||
|
||||
@@ -47,6 +47,26 @@ east = GPIO[15:0], west = GPIO[31:16]; the `power_pads` counts come from
|
||||
|
||||
## Constraints
|
||||
|
||||
`LIBRELANE_IO_PROFILE=default` preserves the ordinary GPIO scenario and writes
|
||||
to `physical/librelane/tiny/chip/` within the selected build variant.
|
||||
`LIBRELANE_IO_PROFILE=camera` selects a separate scenario under `chip-camera/`;
|
||||
its doctor/result metadata also has a camera suffix. Both SDC and chip-config
|
||||
generators receive `--io-profile`, and unknown profiles are rejected.
|
||||
|
||||
The camera scenario declares GPIO20 as the pixel clock and assigns GPIO12-19,
|
||||
GPIO21 and GPIO22 input delays to that clock, excluding their ordinary output
|
||||
constraints. The config's clock-port list includes GPIO20. The default
|
||||
scenario treats those pads, including GPIO20, as ordinary GPIO and does not
|
||||
create a pixel clock. GPIO23 retains its existing output constraint. These
|
||||
are separate pin-function scenarios; they do not qualify simultaneous audio
|
||||
and camera ownership. Audio-specific timing remains a separate physical gap.
|
||||
|
||||
The camera period of 83.333333333 ns and input max delay of 20% of that period
|
||||
are development assumptions, not measured sensor/board timing. Asynchronous
|
||||
clock groups must not remove camera-data-to-pixel-register timing paths.
|
||||
The row-size remediation validates generated constraints and mode isolation
|
||||
without running a new LibreLane physical campaign.
|
||||
|
||||
The SDC constrains the external clock at 24 MHz and JTAG at 10 MHz, defines a
|
||||
generated system clock at the top-level clock buffer, and keeps the two groups
|
||||
asynchronous. Mid-PnR timing report passes are skipped because LibreLane 3.0.5
|
||||
|
||||
@@ -135,6 +135,9 @@ def macro_hooks(instances: dict[str, tuple[list[int], str]]) -> list[str]:
|
||||
|
||||
|
||||
def build_config(args: argparse.Namespace) -> dict[str, object]:
|
||||
io_profile = getattr(args, "io_profile", "default")
|
||||
if io_profile not in ("default", "camera"):
|
||||
raise ValueError("IO profile must be default or camera")
|
||||
pads, _ = read_map(args.pin_map)
|
||||
power_counts = read_power_pad_counts(args.pin_map)
|
||||
signal_instances: dict[str, list[str]] = {side: [] for side in SIDE_ORDER}
|
||||
@@ -213,7 +216,7 @@ def build_config(args: argparse.Namespace) -> dict[str, object]:
|
||||
"STA_EXTRA_CORNER_TCL_FILE": config_path(
|
||||
ROOT / "physical/librelane/tiny/sta_report_limit.tcl", config_dir
|
||||
),
|
||||
"CLOCK_PORT": list(CLOCK_PORTS),
|
||||
"CLOCK_PORT": [*CLOCK_PORTS, *(["gpio_20_io_pad"] if io_profile == "camera" else [])],
|
||||
"CLOCK_PERIOD": 1_000_000_000 / args.ext_clk_hz,
|
||||
"VDD_NETS": ["VDD"],
|
||||
"GND_NETS": ["VSS"],
|
||||
@@ -270,6 +273,7 @@ def main() -> int:
|
||||
parser.add_argument("--bondpad-lef", type=Path, required=True)
|
||||
parser.add_argument("--sram-vh", type=Path, required=True)
|
||||
parser.add_argument("--ext-clk-hz", type=positive_integer, required=True)
|
||||
parser.add_argument("--io-profile", choices=("default", "camera"), default="default")
|
||||
parser.add_argument("--have-sram-macro", action="store_true")
|
||||
parser.add_argument("--sram-size-kib", type=positive_integer, default=128)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
|
||||
@@ -20,9 +20,11 @@ CLOCK_PADS = {
|
||||
"external": "extclk_i_pad",
|
||||
"jtag": "jtag_tck_i_pad",
|
||||
}
|
||||
CAMERA_INPUTS = frozenset(f"gpio_{pin}_io_pad" for pin in (*range(12, 20), 21, 22))
|
||||
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
|
||||
PIXEL_PERIOD_NS = 83.333333333
|
||||
|
||||
|
||||
def positive_integer(value: str) -> int:
|
||||
@@ -32,9 +34,11 @@ def positive_integer(value: str) -> int:
|
||||
return parsed
|
||||
|
||||
|
||||
def timing_clock(pad: Pad) -> str:
|
||||
def timing_clock(pad: Pad, io_profile: str = "default") -> str:
|
||||
if pad.name.startswith("jtag_"):
|
||||
return "jtag"
|
||||
if io_profile == "camera" and pad.name in CAMERA_INPUTS:
|
||||
return "pixel"
|
||||
return "external"
|
||||
|
||||
|
||||
@@ -52,10 +56,16 @@ def logical_directions(pad: Pad) -> tuple[bool, bool]:
|
||||
|
||||
|
||||
def render(args: argparse.Namespace) -> str:
|
||||
io_profile = getattr(args, "io_profile", "default")
|
||||
if io_profile not in ("default", "camera"):
|
||||
raise ValueError("IO profile must be default or camera")
|
||||
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():
|
||||
clock_pads = dict(CLOCK_PADS)
|
||||
if io_profile == "camera":
|
||||
clock_pads["pixel"] = "gpio_20_io_pad"
|
||||
for name in clock_pads.values():
|
||||
if name not in by_name:
|
||||
raise ValueError(f"clock PAD is missing from the pin map: {name}")
|
||||
|
||||
@@ -63,22 +73,25 @@ def render(args: argparse.Namespace) -> str:
|
||||
"external": 1_000_000_000 / args.ext_clk_hz,
|
||||
"jtag": JTAG_PERIOD_NS,
|
||||
}
|
||||
if io_profile == "camera":
|
||||
periods["pixel"] = PIXEL_PERIOD_NS
|
||||
groups: dict[str, dict[str, list[str]]] = {
|
||||
name: {"inputs": [], "outputs": []} for name in periods
|
||||
}
|
||||
excluded = set(CLOCK_PADS.values()) | RESET_PADS
|
||||
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)
|
||||
clock = timing_clock(pad, io_profile)
|
||||
if is_input:
|
||||
groups[clock]["inputs"].append(pad.name)
|
||||
if is_output:
|
||||
if is_output and not (io_profile == "camera" and pad.name in CAMERA_INPUTS):
|
||||
groups[clock]["outputs"].append(pad.name)
|
||||
|
||||
lines = [
|
||||
"# Generated by physical/librelane/tiny/scripts/generate_sdc.py; do not edit.",
|
||||
f"# IO profile: {io_profile}; development assumptions, not board qualification.",
|
||||
"current_design $::env(DESIGN_NAME)",
|
||||
"set_units -time ns",
|
||||
"",
|
||||
@@ -94,7 +107,7 @@ def render(args: argparse.Namespace) -> str:
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name, pad_name in CLOCK_PADS.items():
|
||||
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}")
|
||||
@@ -114,9 +127,11 @@ def render(args: argparse.Namespace) -> str:
|
||||
"",
|
||||
"set_clock_groups -name retrosoc_async -asynchronous \\",
|
||||
" -group [get_clocks {clk_external clk_system}] \\",
|
||||
" -group [get_clocks {clk_jtag}]",
|
||||
" -group [get_clocks {clk_jtag}]" + (" \\" if io_profile == "camera" else ""),
|
||||
*([" -group [get_clocks {clk_pixel}]"] if io_profile == "camera" else []),
|
||||
"",
|
||||
"set all_retrosoc_clocks [get_clocks {clk_external clk_system clk_jtag}]",
|
||||
"set all_retrosoc_clocks [get_clocks {clk_external clk_system clk_jtag" +
|
||||
(" clk_pixel" if io_profile == "camera" else "") + "}]",
|
||||
"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",
|
||||
@@ -159,6 +174,7 @@ def main() -> int:
|
||||
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("--io-profile", choices=("default", "camera"), default="default")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
|
||||
@@ -421,7 +421,7 @@ def test_commercial_timing_contract_supports_tiny(tmp_path: Path) -> None:
|
||||
check=True,
|
||||
)
|
||||
assert result.stdout.splitlines() == [
|
||||
"aon,audio,jtag,memory,peripheral,system",
|
||||
"aon,audio,dvp_pixel,jtag,memory,peripheral,system",
|
||||
"ext_rst_n_i_pad,jtag_trst_n_i_pad",
|
||||
]
|
||||
assert "observation {u_clock_buffer/clk_o}" in output.read_text(
|
||||
@@ -663,7 +663,7 @@ CONSTRAINT_SMOKE_CASES = {
|
||||
"TINY": {
|
||||
"domains": "rtl/tiny/integration/clock_reset_domains.json",
|
||||
"pin_map": "rtl/tiny/pin_map/pin_map.json",
|
||||
"masters": "clk_aon clk_jtag clk_audio",
|
||||
"masters": "clk_aon clk_jtag clk_audio clk_dvp_pixel",
|
||||
"generated": "clk_system clk_memory clk_peripheral clk_sys_div2 clk_sys_div4 clk_clkout_source",
|
||||
"groups": "1",
|
||||
},
|
||||
|
||||
@@ -4,9 +4,12 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PIN_MAP = ROOT / "rtl/tiny/pin_map/pin_map.json"
|
||||
@@ -153,6 +156,61 @@ def test_chip_sdc_is_pad_aware_and_does_not_false_path_all_io() -> None:
|
||||
assert "set_false_path -from $reset_jtag_trst_n_i_pad" in sdc
|
||||
assert "set_false_path -from [all_inputs]" not in sdc
|
||||
assert "set_false_path -to [all_outputs]" not in sdc
|
||||
assert "clk_pixel" not in sdc
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", ["default", "camera"])
|
||||
def test_camera_io_clock_scenario(profile: str, tmp_path: Path) -> None:
|
||||
module = load_module("tiny_sdc_scenario", FLOW_ROOT / "scripts/generate_sdc.py")
|
||||
sdc = module.render(SimpleNamespace(domains=DOMAINS, pin_map=PIN_MAP,
|
||||
ext_clk_hz=24_000_000, io_profile=profile))
|
||||
groups = {name: set(ports.split()) for name, ports in re.findall(
|
||||
r"set (\w+_(?:inputs|outputs)) \[get_ports \{([^}]*)\}\]", sdc)}
|
||||
camera = {f"gpio_{pin}_io_pad" for pin in (*range(12, 20), 21, 22)}
|
||||
config_module = load_module("tiny_config_scenario", FLOW_ROOT / "scripts/generate_chip_config.py")
|
||||
arguments = chip_arguments(tmp_path)
|
||||
arguments.io_profile = profile
|
||||
config = config_module.build_config(arguments)
|
||||
if profile == "camera":
|
||||
assert groups["pixel_inputs"] == camera
|
||||
assert not camera & groups["external_inputs"]
|
||||
assert not camera & groups["external_outputs"]
|
||||
assert "gpio_20_io_pad" not in groups["external_inputs"]
|
||||
assert "gpio_20_io_pad" not in groups["external_outputs"]
|
||||
assert "set_input_delay -min 0 -clock clk_pixel $pixel_inputs" in sdc
|
||||
assert "set_input_delay -max 16.6666666666 -clock clk_pixel $pixel_inputs" in sdc
|
||||
assert "-group [get_clocks {clk_pixel}]" in sdc
|
||||
assert config["CLOCK_PORT"] == ["extclk_i_pad", "jtag_tck_i_pad", "gpio_20_io_pad"]
|
||||
else:
|
||||
assert "pixel_inputs" not in groups
|
||||
assert camera | {"gpio_20_io_pad"} <= groups["external_inputs"]
|
||||
assert camera | {"gpio_20_io_pad"} <= groups["external_outputs"]
|
||||
assert "clk_pixel" not in sdc
|
||||
assert config["CLOCK_PORT"] == ["extclk_i_pad", "jtag_tck_i_pad"]
|
||||
assert "gpio_23_io_pad" in groups["external_outputs"]
|
||||
|
||||
|
||||
def test_camera_io_profile_rejects_unknown_value(tmp_path: Path) -> None:
|
||||
module = load_module("tiny_sdc_bad_scenario", FLOW_ROOT / "scripts/generate_sdc.py")
|
||||
with pytest.raises(ValueError, match="IO profile"):
|
||||
module.render(SimpleNamespace(io_profile="both"))
|
||||
module = load_module("tiny_config_bad_scenario", FLOW_ROOT / "scripts/generate_chip_config.py")
|
||||
arguments = chip_arguments(tmp_path)
|
||||
arguments.io_profile = "both"
|
||||
with pytest.raises(ValueError, match="IO profile"):
|
||||
module.build_config(arguments)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", ["default", "camera"])
|
||||
def test_camera_make_scenario_isolated(profile: str) -> None:
|
||||
result = subprocess.run([
|
||||
"make", "--no-print-directory", "-n", "CONFIG=configs/ci/ihp130-tiny.mk",
|
||||
"SOC=TINY", "PDK=IHP130", f"LIBRELANE_IO_PROFILE={profile}", "librelane-input",
|
||||
], cwd=ROOT, capture_output=True, text=True, check=False)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
directory = "chip-camera" if profile == "camera" else "chip"
|
||||
assert f"physical/librelane/tiny/{directory}/input/retrosoc_tiny_chip.sdc" in result.stdout
|
||||
assert result.stdout.count(f"--io-profile {profile}") == 2
|
||||
|
||||
|
||||
def test_tiny_flow_exposes_chip_target_only() -> None:
|
||||
|
||||
Reference in new issue
Block a user