From 6e7ea326ef4b2ef4b5183c97556f2f07d080a3ab Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Thu, 30 Apr 2026 11:31:15 +0200 Subject: [PATCH] feat(packaging): add BlockPackageDef for hard-macro builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a parameterized package type used when chipflow.toml declares `package = "block"`. Structurally a sibling of BareDiePackageDef — pins on four sides addressed by (Side, index) — but for hard-macro targets rather than packaged chips. Differences vs chip packages: - No I/O pad ring, no JTAG, no fixed clock/reset/power slots. Blocks take power via straps from the parent and route clocks/resets through regular pins. allocate_pins() skips the bringup-pins step entirely. - width/height are user-defined per project rather than pulled from a fixed PACKAGE_DEFINITIONS entry, sourced from a new [chipflow.silicon.block] table: [chipflow.silicon] process = "ihp_sg13g2" package = "block" [chipflow.silicon.block] width = 50 height = 80 width/height are pin-slot counts, same units as QuadPackageDef. The backend translates to physical microns using the process's pin pitch. The lockfile flows through the existing bundle.zip pipeline unchanged — package_type discriminator routes deserialization, and the `_file`-keyed manifest contract on the receiving side already covers everything. --- chipflow/config/models.py | 14 +++++- chipflow/packaging/__init__.py | 2 + chipflow/packaging/lockfile.py | 10 ++++- chipflow/packaging/standard.py | 82 +++++++++++++++++++++++++++++++++- chipflow/packaging/utils.py | 25 ++++++++++- tests/test_block_package.py | 47 +++++++++++++++++++ 6 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 tests/test_block_package.py 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/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/standard.py b/chipflow/packaging/standard.py index d1363180..376ce04a 100644 --- a/chipflow/packaging/standard.py +++ b/chipflow/packaging/standard.py @@ -9,10 +9,15 @@ 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 +from .lockfile import LockFile +from .allocation import _linear_allocate_components + +if TYPE_CHECKING: + from ..config import Config, Process class _Side(IntEnum): @@ -84,6 +89,81 @@ def bringup_pins(self) -> BringupPins: ) +class BlockPackageDef(LinearAllocPackageDef): + """ + Definition of a hard-macro target with pins on four sides. + + Structurally a sibling of :class:`BareDiePackageDef` — pins are + addressed by ``(_Side, index)`` tuples — 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 fixed clock/reset/power locations: + blocks take power via straps from the parent and route their + clocks/resets through regular pins. Bringup-pin allocation is + skipped. + - ``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. + + 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. No bringup pins to subtract.""" + pins = set(itertools.product((_Side.N, _Side.S), range(self.width))) + pins |= set(itertools.product((_Side.W, _Side.E), range(self.height))) + self._ordered_pins: List[BareDiePin] = sorted(pins) + return super().model_post_init(__context) + + @property + def bringup_pins(self) -> BringupPins: + """Blocks have no chip-style bringup pins. + + The base ``bringup_pins`` property is abstract and must return a + :class:`BringupPins` instance, but :meth:`allocate_pins` below + is overridden to skip the bringup step entirely so this value is + never read. We raise here to make any accidental future caller + fail loudly rather than silently allocating wrong locations. + """ + raise NotImplementedError( + "BlockPackageDef has no bringup pins — clocks, resets and " + "power are wired through regular pins or via parent abutment." + ) + + def allocate_pins( + self, config: 'Config', process: 'Process', lockfile: LockFile | None + ) -> LockFile: + """Allocate pins without the chip-package bringup step. + + Blocks don't have an I/O ring, so the parent class's + ``_allocate_bringup`` (which reserves clock/reset/power/JTAG + slots at fixed positions) doesn't apply. Just allocate registered + components linearly from the perimeter slots. + """ + portmap = _linear_allocate_components( + self._interfaces, + lockfile, + self._allocate, + set(self._ordered_pins), + ) + package = self._get_package() + return LockFile( + package=package, + process=process, + metadata=self._interfaces, + port_map=portmap, + ) + + 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..d09c8629 --- /dev/null +++ b/tests/test_block_package.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Tests for BlockPackageDef — the parameterized per-project package used +when ``[chipflow.silicon] package = "block"``.""" + +import unittest + +from chipflow.packaging.standard import BlockPackageDef, _Side + + +class BlockPackageDefTestCase(unittest.TestCase): + def test_pin_slots_match_perimeter(self): + """A 5×3 block has 5 N + 5 S + 3 W + 3 E = 16 slots.""" + pkg = BlockPackageDef(name="block", width=5, height=3) + slots = pkg._ordered_pins + self.assertEqual(len(slots), 5 + 5 + 3 + 3) + sides = {s for s, _ in slots} + self.assertEqual(sides, {_Side.N, _Side.S, _Side.W, _Side.E}) + + def test_does_not_reserve_bringup_slots(self): + """Unlike chip packages, BlockPackageDef must not subtract any + bringup pins from the available set — blocks have no I/O ring.""" + pkg = BlockPackageDef(name="block", width=4, height=4) + # All 16 perimeter slots remain available. + self.assertEqual(len(pkg._ordered_pins), 16) + + def test_bringup_pins_property_raises(self): + """The abstract bringup_pins property must not be silently usable + on a block — calling it should fail loudly.""" + pkg = BlockPackageDef(name="block", width=4, height=4) + with self.assertRaises(NotImplementedError): + pkg.bringup_pins + + 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()