"""FPGA fabric definition module.
This module contains the Fabric class which represents the complete FPGA fabric
including tile layout, configuration parameters, and connectivity information. The
fabric is the top-level container for all tiles, BELs, and routing resources.
"""
from collections.abc import Generator
from dataclasses import dataclass, field
from FABulous.fabric_definition.Bel import Bel
from FABulous.fabric_definition.define import (
ConfigBitMode,
Direction,
MultiplexerStyle,
Side,
)
from FABulous.fabric_definition.SuperTile import SuperTile
from FABulous.fabric_definition.Tile import Tile
from FABulous.fabric_definition.Wire import Wire
@dataclass
[docs]
class Fabric:
"""Store the configuration of a fabric.
All the information is parsed from the CSV file.
Attributes
----------
tile : list[list[Tile]]
The tile map of the fabric
name : str
The name of the fabric
numberOfRows : int
The number of rows of the fabric
numberOfColumns : int
The number of columns of the fabric
configBitMode : ConfigBitMode
The configuration bit mode of the fabric.
Currently supports frame based or ff chain
frameBitsPerRow : int
The number of frame bits per row of the fabric
maxFramesPerCol : int
The maximum number of frames per column of the fabric
package : str
The extra package used by the fabric. Only useful for VHDL output.
generateDelayInSwitchMatrix : int
The amount of delay in a switch matrix.
multiplexerStyle : MultiplexerStyle
The style of the multiplexer used in the fabric.
Currently supports custom or generic
frameSelectWidth : int
The width of the frame select signal.
rowSelectWidth : int
The width of the row select signal.
desync_flag : int
The flag indicating desynchronization status,
used to manage timing issues within the fabric.
numberOfBRAMs : int
The number of BRAMs in the fabric.
superTileEnable : bool
Whether the fabric has super tile.
tileDic : dict[str, Tile]
A dictionary of tiles used in the fabric. The key is the name of the tile and
the value is the tile.
superTileDic : dict[str, SuperTile]
A dictionary of super tiles used in the fabric. The key is the name of the
supertile and the value is the supertile.
unusedTileDic: dict[str, Tile]
A dictionary of tiles that are not used in the fabric,
but defined in the fabric.csv.
The key is the name of the tile and the value is the tile.
unusedSuperTileDic : dict[str, SuperTile]
A dictionary of super tiles that are not used in the fabric,
but defined in the fabric.csv.
The key is the name of the tile and the value is the tile.
commonWirePair : list[tuple[str, str]]
A list of common wire pairs in the fabric.
"""
[docs]
tile: list[list[Tile]] = field(default_factory=list)
[docs]
numberOfColumns: int = 15
[docs]
configBitMode: ConfigBitMode = ConfigBitMode.FRAME_BASED
[docs]
frameBitsPerRow: int = 32
[docs]
maxFramesPerCol: int = 20
[docs]
package: str = "use work.my_package.all"
[docs]
generateDelayInSwitchMatrix: int = 80
[docs]
multiplexerStyle: MultiplexerStyle = MultiplexerStyle.CUSTOM
[docs]
frameSelectWidth: int = 5
[docs]
rowSelectWidth: int = 5
[docs]
numberOfBRAMs: int = 10
[docs]
superTileEnable: bool = True
[docs]
tileDic: dict[str, Tile] = field(default_factory=dict)
[docs]
superTileDic: dict[str, SuperTile] = field(default_factory=dict)
[docs]
unusedTileDic: dict[str, Tile] = field(default_factory=dict)
[docs]
unusedSuperTileDic: dict[str, SuperTile] = field(default_factory=dict)
[docs]
commonWirePair: list[tuple[str, str]] = field(default_factory=list)
def __post_init__(self) -> None:
"""Generate and get all the wire pairs in the fabric.
The wire pair are used during model generation when some of the signals have
source or destination of "NULL".
The wires are used during model generation to work with wire that going cross
tile.
"""
for row in self.tile:
for tile in row:
if tile is None:
continue
for port in tile.portsInfo:
self.commonWirePair.append((port.sourceName, port.destinationName))
self.commonWirePair = list(dict.fromkeys(self.commonWirePair))
self.commonWirePair = [
(i, j) for i, j in self.commonWirePair if i != "NULL" and j != "NULL"
]
for y, row in enumerate(self.tile):
for x, tile in enumerate(row):
if tile is None:
continue
for port in tile.portsInfo:
if (
abs(port.xOffset) <= 1
and abs(port.yOffset) <= 1
and port.sourceName != "NULL"
and port.destinationName != "NULL"
):
for i in range(port.wireCount):
tile.wireList.append(
Wire(
direction=port.wireDirection,
source=f"{port.sourceName}{i}",
xOffset=port.xOffset,
yOffset=port.yOffset,
destination=f"{port.destinationName}{i}",
sourceTile="",
destinationTile="",
)
)
elif port.sourceName != "NULL" and port.destinationName != "NULL":
# clamp the xOffset to 1 or -1
value = min(max(port.xOffset, -1), 1)
cascadedI = 0
for i in range(port.wireCount * abs(port.xOffset)):
if i < port.wireCount:
cascadedI = i + port.wireCount * (abs(port.xOffset) - 1)
else:
cascadedI = i - port.wireCount
tile.wireList.append(
Wire(
direction=Direction.JUMP,
source=f"{port.destinationName}{i}",
xOffset=0,
yOffset=0,
destination=f"{port.sourceName}{i}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x}Y{y}",
)
)
tile.wireList.append(
Wire(
direction=port.wireDirection,
source=f"{port.sourceName}{i}",
xOffset=value,
yOffset=port.yOffset,
destination=f"{port.destinationName}{cascadedI}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x + value}Y{y + port.yOffset}",
)
)
# clamp the yOffset to 1 or -1
value = min(max(port.yOffset, -1), 1)
cascadedI = 0
for i in range(port.wireCount * abs(port.yOffset)):
if i < port.wireCount:
cascadedI = i + port.wireCount * (abs(port.yOffset) - 1)
else:
cascadedI = i - port.wireCount
tile.wireList.append(
Wire(
direction=Direction.JUMP,
source=f"{port.destinationName}{i}",
xOffset=0,
yOffset=0,
destination=f"{port.sourceName}{i}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x}Y{y}",
)
)
tile.wireList.append(
Wire(
direction=port.wireDirection,
source=f"{port.sourceName}{i}",
xOffset=port.xOffset,
yOffset=value,
destination=f"{port.destinationName}{cascadedI}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x + port.xOffset}Y{y + value}",
)
)
elif port.sourceName != "NULL" and port.destinationName == "NULL":
sourceName = port.sourceName
destName = port.sourceName
# if sourcename is not in a common pair wire we assume
# the source name is the same as destination name
wire_pair = dict(self.commonWirePair)
if sourceName in wire_pair:
destName = wire_pair[sourceName]
value = min(max(port.xOffset, -1), 1)
for i in range(port.wireCount * abs(port.xOffset)):
tile.wireList.append(
Wire(
direction=port.wireDirection,
source=f"{sourceName}{i}",
xOffset=value,
yOffset=port.yOffset,
destination=f"{destName}{i}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x + value}Y{y + port.yOffset}",
)
)
value = min(max(port.yOffset, -1), 1)
for i in range(port.wireCount * abs(port.yOffset)):
tile.wireList.append(
Wire(
direction=port.wireDirection,
source=f"{sourceName}{i}",
xOffset=port.xOffset,
yOffset=value,
destination=f"{destName}{i}",
sourceTile=f"X{x}Y{y}",
destinationTile=f"X{x + port.xOffset}Y{y + value}",
)
)
tile.wireList = list(dict.fromkeys(tile.wireList))
def __repr__(self) -> str:
"""Return the string representation of the fabric.
Returns
-------
str
A formatted string showing the fabric layout and key parameters.
"""
fabric = ""
for i in range(self.numberOfRows):
for j in range(self.numberOfColumns):
if self.tile[i][j] is None:
fabric += "Null".ljust(15) + "\t"
else:
fabric += f"{str(self.tile[i][j].name).ljust(15)}\t"
fabric += "\n"
fabric += "\n"
fabric += f"numberOfColumns: {self.numberOfColumns}\n"
fabric += f"numberOfRows: {self.numberOfRows}\n"
fabric += f"configBitMode: {self.configBitMode}\n"
fabric += f"frameBitsPerRow: {self.frameBitsPerRow}\n"
fabric += f"maxFramesPerCol: {self.maxFramesPerCol}\n"
fabric += f"package: {self.package}\n"
fabric += f"generateDelayInSwitchMatrix: {self.generateDelayInSwitchMatrix}\n"
fabric += f"multiplexerStyle: {self.multiplexerStyle}\n"
fabric += f"superTileEnable: {self.superTileEnable}\n"
fabric += f"tileDic: {list(self.tileDic.keys())}\n"
return fabric
def __iter__(self) -> Generator[tuple[tuple[int, int], Tile | None]]:
"""Iterate over all tiles in the fabric in row-major order.
Yields
------
Generator[tuple[tuple[int, int], Tile | None]]
Generator yielding a tuple where the first element is the (x, y)
coordinates and the second is the Tile at that position or None
if the position is empty.
"""
for y, row in enumerate(self.tile):
for x, tile in enumerate(row):
yield (x, y), tile
[docs]
def getTileByName(self, name: str) -> Tile | SuperTile:
"""Get a tile by its name from the fabric.
Search for the tile first in the used tiles dictionary, then in the unused tiles
dictionary then in the supertiles if not found.
Parameters
----------
name : str
The name of the tile to retrieve.
Returns
-------
Tile | SuperTile
The tile or supertile object if found.
Raises
------
KeyError
If the tile name is not found in either used or unused tiles.
"""
ret = self.tileDic.get(name)
if ret is None:
ret = self.unusedTileDic.get(name)
if ret is None:
ret = self.getSuperTileByName(name) # Check if it's a supertile
if ret is None:
raise KeyError(f"Tile {name} not found in fabric.")
return ret
[docs]
def getSuperTileByName(self, name: str) -> SuperTile:
"""Get a supertile by its name from the fabric.
Searches for the supertile first in the used supertiles dictionary, then in the
unused supertiles dictionary if not found.
Parameters
----------
name : str
The name of the supertile to retrieve.
Returns
-------
SuperTile
The super tile object if found.
Raises
------
KeyError
If the super tile name is not found in either used or unused super tiles.
"""
ret = self.superTileDic.get(name)
if ret is None:
ret = self.unusedSuperTileDic.get(name)
if ret is None:
raise KeyError(f"SuperTile {name} not found in fabric.")
return ret
[docs]
def getAllUniqueBels(self) -> list[Bel]:
"""Get all unique BELs from all tiles in the fabric.
Returns
-------
list[Bel]
A list of all unique BELs across all tiles.
"""
bels = list()
for tile in self.tileDic.values():
bels.extend(tile.bels)
return bels
[docs]
def getBelsByTileXY(self, x: int, y: int) -> list[Bel]:
"""Get all the Bels of a tile.
Parameters
----------
x : int
The x coordinate of / column the tile.
y : int
The y coordinate / row of the tile.
Returns
-------
list[Bel]
A list of Bels in the tile.
Raises
------
ValueError
Tile coordinates are out of range.
"""
if x < 0 or x >= self.numberOfColumns or y < 0 or y >= self.numberOfRows:
raise ValueError(
f"Invalid tile coordinates: ({x},{y}) max (0,0) - ({self.numberOfRows},"
f"{self.numberOfColumns})"
)
if self.tile[y][x] is None:
return []
return self.tile[y][x].bels
[docs]
def find_tile_positions(
self, tile: Tile | SuperTile
) -> list[tuple[int, int]] | None:
"""Find all positions where a tile or supertile appears in the fabric grid.
Parameters
----------
tile : Tile | SuperTile
The tile or supertile to search for
Returns
-------
list[tuple[int, int]] | None
List of (x, y) positions where the tile/supertile appears,
or None if not found
"""
positions = []
if isinstance(tile, SuperTile):
# For SuperTiles, find where they appear
for y, row in enumerate(self.tile):
for x, fabric_tile in enumerate(row):
if fabric_tile is None:
continue
# Check if this fabric tile belongs to the supertile
for st in self.superTileDic.values():
if st == tile:
# Check if fabric_tile is part of this supertile
for st_row in st.tileMap:
for st_tile in st_row:
if st_tile and st_tile.name == fabric_tile.name:
positions.append((x, y))
else:
# For regular Tiles, find where they appear
for y, row in enumerate(self.tile):
for x, fabric_tile in enumerate(row):
if fabric_tile and fabric_tile.name == tile.name:
positions.append((x, y))
return positions if positions else None
[docs]
def determine_border_side(self, x: int, y: int) -> Side | None:
"""Determine which border side a tile position is on, if any.
Parameters
----------
x : int
X coordinate in the fabric grid
y : int
Y coordinate in the fabric grid
Returns
-------
Side | None
The border side (NORTH, SOUTH, EAST, or WEST) if the position is on
a border, None otherwise. If on a corner, returns the vertical side
(NORTH or SOUTH) as priority.
"""
is_north = y == 0
is_south = y == self.numberOfRows - 1
is_east = x == self.numberOfColumns - 1
is_west = x == 0
# Priority: corners get vertical sides (NORTH/SOUTH)
if is_north:
return Side.NORTH
if is_south:
return Side.SOUTH
if is_east:
return Side.EAST
if is_west:
return Side.WEST
return None
[docs]
def get_unique_tile_types(self) -> list[Tile]:
"""Get list of unique tile types used in the fabric.
Returns
-------
list[Tile]
List of unique tile types (one instance per type name)
"""
unique_tiles: dict[str, Tile] = {}
for row in self.tile:
for tile in row:
if tile is not None and tile.name not in unique_tiles:
unique_tiles[tile.name] = tile
return list(unique_tiles.values())
[docs]
def get_tile_row_column_indices(self, tile_name: str) -> tuple[set[int], set[int]]:
"""Get all row and column indices where a tile type appears.
Parameters
----------
tile_name : str
Name of the tile type to search for
Returns
-------
tuple[set[int], set[int]]
(row_indices, column_indices) where the tile type appears
"""
rows: set[int] = set()
cols: set[int] = set()
for row_idx, row in enumerate(self.tile):
for col_idx, tile in enumerate(row):
if tile is not None and tile.name == tile_name:
rows.add(row_idx)
cols.add(col_idx)
return rows, cols