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:
maksyuki committed 2026-10-17 15:33:13 +08:00
1 parent 8f5630fb41
commit 83fe263e40
8 files changed
+124 -19

No files matched your search

+1 -1
View File
@@ -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 {} {
+12 -5
View File
@@ -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 $@
+20
View File
@@ -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:
+2 -2
View File
@@ -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",
},
+58
View File
@@ -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: