From 83fe263e40db158cf321aef086a355810a4d4dca Mon Sep 17 00:00:00 2001 From: maksyuki Date: Sat, 17 Oct 2026 15:33:13 +0800 Subject: [PATCH] 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. --- physical/commercial/config/products/tiny.mk | 2 +- .../commercial/tcl/common/products/tiny.tcl | 4 +- physical/librelane/tiny/Makefile | 17 ++++-- physical/librelane/tiny/README.md | 20 +++++++ .../tiny/scripts/generate_chip_config.py | 6 +- .../librelane/tiny/scripts/generate_sdc.py | 32 +++++++--- tests/test_commercial_flow.py | 4 +- tests/test_librelane_tiny_flow.py | 58 +++++++++++++++++++ 8 files changed, 124 insertions(+), 19 deletions(-) diff --git a/physical/commercial/config/products/tiny.mk b/physical/commercial/config/products/tiny.mk index 598e6a60..4b5404c6 100644 --- a/physical/commercial/config/products/tiny.mk +++ b/physical/commercial/config/products/tiny.mk @@ -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 diff --git a/physical/commercial/tcl/common/products/tiny.tcl b/physical/commercial/tcl/common/products/tiny.tcl index 7fbd90dd..c92f186d 100644 --- a/physical/commercial/tcl/common/products/tiny.tcl +++ b/physical/commercial/tcl/common/products/tiny.tcl @@ -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 {} { diff --git a/physical/librelane/tiny/Makefile b/physical/librelane/tiny/Makefile index e67e6f21..0c43d1bc 100644 --- a/physical/librelane/tiny/Makefile +++ b/physical/librelane/tiny/Makefile @@ -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 $@ diff --git a/physical/librelane/tiny/README.md b/physical/librelane/tiny/README.md index 3c7430a9..7422593f 100644 --- a/physical/librelane/tiny/README.md +++ b/physical/librelane/tiny/README.md @@ -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 diff --git a/physical/librelane/tiny/scripts/generate_chip_config.py b/physical/librelane/tiny/scripts/generate_chip_config.py index 85a9e08b..97746f6b 100644 --- a/physical/librelane/tiny/scripts/generate_chip_config.py +++ b/physical/librelane/tiny/scripts/generate_chip_config.py @@ -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) diff --git a/physical/librelane/tiny/scripts/generate_sdc.py b/physical/librelane/tiny/scripts/generate_sdc.py index 49084619..f67469f5 100644 --- a/physical/librelane/tiny/scripts/generate_sdc.py +++ b/physical/librelane/tiny/scripts/generate_sdc.py @@ -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: diff --git a/tests/test_commercial_flow.py b/tests/test_commercial_flow.py index ce093a44..34a17205 100644 --- a/tests/test_commercial_flow.py +++ b/tests/test_commercial_flow.py @@ -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", }, diff --git a/tests/test_librelane_tiny_flow.py b/tests/test_librelane_tiny_flow.py index 600c4259..cfce3983 100644 --- a/tests/test_librelane_tiny_flow.py +++ b/tests/test_librelane_tiny_flow.py @@ -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: