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: