From 6e7ea326ef4b2ef4b5183c97556f2f07d080a3ab Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Thu, 30 Apr 2026 11:31:15 +0200 Subject: [PATCH 1/3] 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() From 9ba71a7ddd6366aff9ab31f5d384c3688663b7c5 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Thu, 7 May 2026 16:56:03 +0200 Subject: [PATCH 2/3] fix(BlockPackageDef): use linear int pin numbering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match QuadPackageDef so the chipflow-backend can use a single packaging.map convention (W→S→E→N counter-clockwise) to translate pin indices to physical (side, slot) locations. The earlier (_Side, idx) tuple form would serialize as [side, idx] in the lockfile, which the backend's pad loader collapses to the side component alone — making every N-edge pin indistinguishable. --- chipflow/packaging/standard.py | 19 ++++++++++++------- tests/test_block_package.py | 10 ++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/chipflow/packaging/standard.py b/chipflow/packaging/standard.py index 376ce04a..b824c87a 100644 --- a/chipflow/packaging/standard.py +++ b/chipflow/packaging/standard.py @@ -93,10 +93,11 @@ 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: + 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 fixed clock/reset/power locations: blocks take power via straps from the parent and route their @@ -107,6 +108,10 @@ class BlockPackageDef(LinearAllocPackageDef): 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. @@ -119,9 +124,9 @@ class BlockPackageDef(LinearAllocPackageDef): 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) + self._ordered_pins: List[int] = list( + range(1, 2 * (self.width + self.height) + 1) + ) return super().model_post_init(__context) @property diff --git a/tests/test_block_package.py b/tests/test_block_package.py index d09c8629..7176a2a3 100644 --- a/tests/test_block_package.py +++ b/tests/test_block_package.py @@ -4,17 +4,15 @@ import unittest -from chipflow.packaging.standard import BlockPackageDef, _Side +from chipflow.packaging.standard import BlockPackageDef 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.""" + """A 5×3 block has 5+5+3+3 = 16 linear pin slots, numbered + from 1 to 16 — same convention as QuadPackageDef.""" 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}) + self.assertEqual(pkg._ordered_pins, list(range(1, 17))) def test_does_not_reserve_bringup_slots(self): """Unlike chip packages, BlockPackageDef must not subtract any From 5f194b74df04f2e726ff51026d3e4d997663f52c Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Thu, 7 May 2026 19:56:27 +0200 Subject: [PATCH 3/3] fix(BlockPackageDef): emit clk/rst_n in lockfile bringup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A block macro still needs real clock and reset boundary pins — the parent design drives them through ordinary block pins, so they must appear in the lockfile under `_core.bringup_pins`. The previous BlockPackageDef.allocate_pins skipped bringup allocation wholesale, leaving the macro with no clk/rst_n entries. Make BringupPins minimally constructible: core_power defaults to [], core_heartbeat to None. Base _allocate_bringup walks an empty power list and skips heartbeat when the pin is None, so targets without a chip-style debug ring drop those entries naturally. BlockPackageDef.bringup_pins now returns BringupPins(core_clock=1, core_reset=2) and the rest of the flow (model_post_init / allocate_pins) reuses the base path — no overrides, no special case in wire-up. Lockfile output for a block carries the same _core shape as a chip, just containing only clk and rst_n. --- chipflow/packaging/base.py | 6 ++-- chipflow/packaging/pins.py | 22 +++++++----- chipflow/packaging/standard.py | 62 ++++++++++------------------------ tests/test_block_package.py | 46 ++++++++++++++++++------- 4 files changed, 68 insertions(+), 68 deletions(-) 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/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 b824c87a..3bd5ad39 100644 --- a/chipflow/packaging/standard.py +++ b/chipflow/packaging/standard.py @@ -13,11 +13,9 @@ 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 + pass class _Side(IntEnum): @@ -99,10 +97,12 @@ class BlockPackageDef(LinearAllocPackageDef): 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. + - 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 @@ -123,50 +123,22 @@ class BlockPackageDef(LinearAllocPackageDef): height: int def model_post_init(self, __context): - """Initialize pin ordering. No bringup pins to subtract.""" - self._ordered_pins: List[int] = list( - range(1, 2 * (self.width + self.height) + 1) - ) + """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: - """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. + """Minimal bringup: clock at pin 1, reset at pin 2. - 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. + 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``. """ - 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, - ) + return BringupPins(core_clock=1, core_reset=2) class QuadPackageDef(LinearAllocPackageDef): diff --git a/tests/test_block_package.py b/tests/test_block_package.py index 7176a2a3..f12ba558 100644 --- a/tests/test_block_package.py +++ b/tests/test_block_package.py @@ -3,30 +3,50 @@ 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, numbered - from 1 to 16 — same convention as QuadPackageDef.""" + """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(1, 17))) + self.assertEqual(pkg._ordered_pins, list(range(3, 17))) - 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.""" + 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) - # All 16 perimeter slots remain available. - self.assertEqual(len(pkg._ordered_pins), 16) + 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_bringup_pins_property_raises(self): - """The abstract bringup_pins property must not be silently usable - on a block — calling it should fail loudly.""" + 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) - with self.assertRaises(NotImplementedError): - pkg.bringup_pins + # _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