diff --git a/chipflow/config/models.py b/chipflow/config/models.py index dea7333e..066616e6 100644 --- a/chipflow/config/models.py +++ b/chipflow/config/models.py @@ -47,13 +47,25 @@ class VoltageRange(SelectiveSerializationModel): typical: Annotated[Optional[Voltage], OmitIfNone()] = None +class BlockConfig(BaseModel): + """Per-project block dimensions when ``[chipflow.silicon] package = "block"``. + + A block is a hard-macro target rather than a packaged chip — the user + declares how many pin slots they want on each axis, and the backend + sizes the macro to fit them at the process's preferred pin pitch. + """ + width: int + height: int + + class SiliconConfig(BaseModel): """Configuration for silicon in chipflow.toml.""" process: 'Process' package: str power: Dict[str, Voltage] = {} debug: Optional[Dict[str, bool]] = None - # This is still kept around to allow forcing pad locations. + # Required only when package = "block". + block: Optional[BlockConfig] = None class SimulationConfig(BaseModel): """Configuration for simulation settings.""" diff --git a/chipflow/packaging/__init__.py b/chipflow/packaging/__init__.py index d836f21f..c6d64e9e 100644 --- a/chipflow/packaging/__init__.py +++ b/chipflow/packaging/__init__.py @@ -51,6 +51,7 @@ # Concrete package types from .standard import ( BareDiePackageDef, + BlockPackageDef, QuadPackageDef, ) @@ -116,6 +117,7 @@ 'LinearAllocPackageDef', # Package types 'BareDiePackageDef', + 'BlockPackageDef', 'QuadPackageDef', 'GAPin', 'GALayout', diff --git a/chipflow/packaging/base.py b/chipflow/packaging/base.py index dfebcbb3..db24a91f 100644 --- a/chipflow/packaging/base.py +++ b/chipflow/packaging/base.py @@ -121,10 +121,12 @@ def _allocate_bringup(self, config: 'Config') -> Component: iomodel=IOModel(width=len(pins), direction=io.Direction.Input) ) - # Add heartbeat if enabled + # Add heartbeat if enabled and the target actually has a + # heartbeat pin (blocks / minimal targets may not). assert config.chipflow.silicon if config.chipflow.silicon.debug and \ - config.chipflow.silicon.debug['heartbeat']: + config.chipflow.silicon.debug['heartbeat'] and \ + self.bringup_pins.core_heartbeat is not None: d['heartbeat'] = PortDesc( type='heartbeat', pins=[self.bringup_pins.core_heartbeat], diff --git a/chipflow/packaging/lockfile.py b/chipflow/packaging/lockfile.py index 38caf919..ba7f3503 100644 --- a/chipflow/packaging/lockfile.py +++ b/chipflow/packaging/lockfile.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: # Forward references to package definitions from .grid_array import GAPackageDef - from .standard import QuadPackageDef, BareDiePackageDef + from .standard import QuadPackageDef, BareDiePackageDef, BlockPackageDef from .openframe import OpenframePackageDef # Import Process directly for pydantic to work properly @@ -23,7 +23,13 @@ # Union of all package definition types -PackageDef = Union['GAPackageDef', 'QuadPackageDef', 'BareDiePackageDef', 'OpenframePackageDef'] +PackageDef = Union[ + 'GAPackageDef', + 'QuadPackageDef', + 'BareDiePackageDef', + 'BlockPackageDef', + 'OpenframePackageDef', +] class Package(pydantic.BaseModel): diff --git a/chipflow/packaging/pins.py b/chipflow/packaging/pins.py index 34e33e3b..27cdcb6b 100644 --- a/chipflow/packaging/pins.py +++ b/chipflow/packaging/pins.py @@ -6,7 +6,7 @@ physical pin assignments and power/signal groupings in IC packages. """ -from dataclasses import dataclass, asdict +from dataclasses import dataclass, asdict, field from enum import StrEnum, auto from typing import Set, List, Union, Optional, TypeVar, Generic @@ -92,23 +92,29 @@ class BringupPins(Generic[Pin]): Essential pins for bringing up an IC, always in fixed locations. These pins are used for initial testing and debug of the IC. + Clock and reset are required (every target needs them); power, + heartbeat, and JTAG are optional so minimal targets — e.g. hard + macros that take power via abutment and have no debug ring — can + declare just the signals they actually have. Attributes: - core_power: List of core power pin pairs core_clock: Core clock input pin core_reset: Core reset input pin - core_heartbeat: Heartbeat output pin (for liveness testing) + core_power: List of core power pin pairs (empty for blocks) + core_heartbeat: Heartbeat output pin (None when not present) core_jtag: Optional JTAG interface pins """ - core_power: List[PowerPins] core_clock: Pin core_reset: Pin - core_heartbeat: Pin + core_power: List[PowerPins] = field(default_factory=list) + core_heartbeat: Optional[Pin] = None core_jtag: Optional[JTAGPins] = None def to_set(self) -> Set[Pin]: - """Convert all bringup pins to a set""" + """Convert all bringup pins to a set, skipping unset signals.""" jtag = self.core_jtag.to_set() if self.core_jtag else set() + present = {self.core_clock, self.core_reset} + if self.core_heartbeat is not None: + present.add(self.core_heartbeat) return {p for pp in self.core_power for p in asdict(pp).values()} | \ - set([self.core_clock, self.core_reset, self.core_heartbeat]) | \ - jtag + present | jtag diff --git a/chipflow/packaging/standard.py b/chipflow/packaging/standard.py index d1363180..3bd5ad39 100644 --- a/chipflow/packaging/standard.py +++ b/chipflow/packaging/standard.py @@ -9,11 +9,14 @@ import itertools from enum import IntEnum -from typing import List, Literal, Tuple +from typing import TYPE_CHECKING, List, Literal, Tuple from .base import LinearAllocPackageDef from .pins import PowerPins, JTAGPins, BringupPins +if TYPE_CHECKING: + pass + class _Side(IntEnum): """Die sides for bare die packages""" @@ -84,6 +87,60 @@ def bringup_pins(self) -> BringupPins: ) +class BlockPackageDef(LinearAllocPackageDef): + """ + Definition of a hard-macro target with pins on four sides. + + Structurally close to :class:`QuadPackageDef` — pins are numbered + linearly counter-clockwise starting from the top of the West edge + — but used when the build is producing a block (LEF / Liberty / + GDS for embedding into another design) rather than a packaged + chip. Differences: + + - No I/O pad ring, no JTAG, no power pins: blocks take power via + straps from the parent and have no chip-level debug ring. + - The bringup pin set contains only ``core_clock`` (pin 1) and + ``core_reset`` (pin 2). Clock and reset are real boundary + signals on the macro that the parent design drives through + ordinary block pins. + - ``width`` and ``height`` are pin-slot counts, same units as + :class:`QuadPackageDef.width` / ``.height`` — not microns. + Translation to physical dimensions happens at the backend using + the process's pin pitch. + + Linear pin numbering matches :class:`QuadPackageDef` so the + backend can use a single ``packaging.map`` convention to translate + pin indices to physical (side, slot) locations. + + Attributes: + width: Number of pin slots on top and bottom edges. + height: Number of pin slots on left and right edges. + """ + + package_type: Literal["BlockPackageDef"] = "BlockPackageDef" + + width: int + height: int + + def model_post_init(self, __context): + """Initialize pin ordering, subtracting the bringup slots.""" + pins = set(range(1, 2 * (self.width + self.height) + 1)) + pins -= self.bringup_pins.to_set() + self._ordered_pins: List[int] = sorted(pins) + return super().model_post_init(__context) + + @property + def bringup_pins(self) -> BringupPins: + """Minimal bringup: clock at pin 1, reset at pin 2. + + No power (parent abutment), no heartbeat, no JTAG. The base + :meth:`_allocate_bringup` walks the empty ``core_power`` list + and skips heartbeat when ``core_heartbeat is None``, so the + resulting ``_core`` portmap contains just ``clk`` and ``rst_n``. + """ + return BringupPins(core_clock=1, core_reset=2) + + class QuadPackageDef(LinearAllocPackageDef): """ Definition of a quad flat package. diff --git a/chipflow/packaging/utils.py b/chipflow/packaging/utils.py index 68a30847..f56dac4f 100644 --- a/chipflow/packaging/utils.py +++ b/chipflow/packaging/utils.py @@ -79,9 +79,30 @@ def lock_pins(config: Optional['Config'] = None) -> None: if not config.chipflow.silicon: raise ChipFlowError("no [chipflow.silicon] section found in chipflow.toml") - # Get package definition from dict + # Resolve the package definition. Most packages are fixed entries in + # PACKAGE_DEFINITIONS (PGA144, BGA144, …). The special name "block" + # is parameterized per project from [chipflow.silicon.block]. package_name = config.chipflow.silicon.package - package_def = PACKAGE_DEFINITIONS[package_name] + if package_name == "block": + from .standard import BlockPackageDef + block_cfg = config.chipflow.silicon.block + if block_cfg is None: + raise ChipFlowError( + 'package = "block" requires a [chipflow.silicon.block] ' + 'section with `width` and `height` (pin slot counts).' + ) + package_def = BlockPackageDef( + name="block", + width=block_cfg.width, + height=block_cfg.height, + ) + else: + if package_name not in PACKAGE_DEFINITIONS: + raise ChipFlowError( + f'Unknown package {package_name!r}. Known: ' + f'{sorted(PACKAGE_DEFINITIONS.keys()) + ["block"]}' + ) + package_def = PACKAGE_DEFINITIONS[package_name] process = config.chipflow.silicon.process top = top_components(config) diff --git a/tests/test_block_package.py b/tests/test_block_package.py new file mode 100644 index 00000000..f12ba558 --- /dev/null +++ b/tests/test_block_package.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Tests for BlockPackageDef — the parameterized per-project package used +when ``[chipflow.silicon] package = "block"``.""" + +import unittest +from unittest.mock import MagicMock + +from chipflow.packaging.standard import BlockPackageDef + + +class BlockPackageDefTestCase(unittest.TestCase): + def test_pin_slots_match_perimeter(self): + """A 5×3 block has 5+5+3+3 = 16 linear pin slots; pins 1 and 2 + are taken by clk and rst_n, leaving 14 user pins (3..16).""" + pkg = BlockPackageDef(name="block", width=5, height=3) + self.assertEqual(pkg._ordered_pins, list(range(3, 17))) + + def test_bringup_only_clk_and_rst(self): + """Block bringup pins contain only clock and reset — no power + (parent abutment), no heartbeat, no JTAG.""" + pkg = BlockPackageDef(name="block", width=4, height=4) + bp = pkg.bringup_pins + self.assertEqual(bp.core_clock, 1) + self.assertEqual(bp.core_reset, 2) + self.assertEqual(bp.core_power, []) + self.assertIsNone(bp.core_heartbeat) + self.assertIsNone(bp.core_jtag) + # Bringup pins must be subtracted from the user-allocatable set. + self.assertNotIn(1, pkg._ordered_pins) + self.assertNotIn(2, pkg._ordered_pins) + + def test_allocate_bringup_emits_clk_and_rst(self): + """The base ``_allocate_bringup`` must produce clk and rst_n + PortDescs for a block — and skip power/heartbeat/JTAG, since + those are absent from the bringup pin set.""" + pkg = BlockPackageDef(name="block", width=4, height=4) + # _allocate_bringup reads silicon.debug only when checking for + # heartbeat; supply a config with no debug section. + config = MagicMock() + config.chipflow.silicon.debug = None + bringup = pkg._allocate_bringup(config) + ports = bringup['bringup_pins'] + self.assertEqual(set(ports.keys()), {'clk', 'rst_n'}) + self.assertEqual(ports['clk'].pins, [1]) + self.assertEqual(ports['rst_n'].pins, [2]) + self.assertEqual(ports['clk'].type, 'clock') + self.assertEqual(ports['rst_n'].type, 'reset') + # rst_n is active-low: must come through inverted. + self.assertTrue(ports['rst_n'].iomodel['invert']) + + def test_serialization_round_trip(self): + """Block defs survive pydantic serialize/deserialize so they fit + into LockFile / Package / bundle.zip.""" + pkg = BlockPackageDef(name="block", width=10, height=20) + dumped = pkg.model_dump() + self.assertEqual(dumped["package_type"], "BlockPackageDef") + self.assertEqual(dumped["width"], 10) + self.assertEqual(dumped["height"], 20) + round = BlockPackageDef.model_validate(dumped) + self.assertEqual(round.width, 10) + self.assertEqual(round.height, 20) + + +if __name__ == "__main__": + unittest.main()