Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion chipflow/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 2 additions & 0 deletions chipflow/packaging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
# Concrete package types
from .standard import (
BareDiePackageDef,
BlockPackageDef,
QuadPackageDef,
)

Expand Down Expand Up @@ -116,6 +117,7 @@
'LinearAllocPackageDef',
# Package types
'BareDiePackageDef',
'BlockPackageDef',
'QuadPackageDef',
'GAPin',
'GALayout',
Expand Down
6 changes: 4 additions & 2 deletions chipflow/packaging/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
10 changes: 8 additions & 2 deletions chipflow/packaging/lockfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@
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
from ..config import Process


# Union of all package definition types
PackageDef = Union['GAPackageDef', 'QuadPackageDef', 'BareDiePackageDef', 'OpenframePackageDef']
PackageDef = Union[
'GAPackageDef',
'QuadPackageDef',
'BareDiePackageDef',
'BlockPackageDef',
'OpenframePackageDef',
]


class Package(pydantic.BaseModel):
Expand Down
22 changes: 14 additions & 8 deletions chipflow/packaging/pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
59 changes: 58 additions & 1 deletion chipflow/packaging/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 23 additions & 2 deletions chipflow/packaging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions tests/test_block_package.py
Original file line number Diff line number Diff line change
@@ -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()
Loading