"""Switch matrix generation module for FABulous FPGA tiles.
This module generates RTL code for configurable switch matrices within FPGA tiles.
Switch matrices handle the routing of signals between tile ports, BEL inputs/outputs,
and jump wires. The module supports various configuration modes and multiplexer styles.
Key features:
- CSV and list file parsing for switch matrix configurations
- Support for custom and generic multiplexer implementations
- Configuration bit calculation and management
- Debug signal generation for switch matrix analysis
- Multiple configuration modes (FlipFlop chain, Frame-based)
"""
import math
from pathlib import Path
from loguru import logger
from fabulous.custom_exception import InvalidFileType
from fabulous.fabric_definition.define import (
IO,
SWITCH_MATRIX_CONSTANTS,
ConfigBitMode,
Direction,
MultiplexerStyle,
)
from fabulous.fabric_definition.port import Port
from fabulous.fabric_definition.supertile import SuperTile
from fabulous.fabric_definition.tile import Tile
from fabulous.fabric_generator.code_generator.code_generator import CodeGenerator
from fabulous.fabric_generator.code_generator.code_generator_VHDL import (
VHDLCodeGenerator,
)
from fabulous.fabric_generator.gen_fabric.gen_helper import (
bootstrapSwitchMatrix,
list2CSV,
)
from fabulous.fabric_generator.parser.parse_switchmatrix import parseList, parseMatrix
def _unconnected_port_diagnostic(ports: list[Port], port_name: str) -> str:
"""Explain an unconnected switch matrix port caused by NULL-wire expansion.
A NULL-terminated spanning wire expands to ``wires x distance`` nested
wires (see `Port.expandPortInfoByName`). When the switch matrix leaves some
of those nested wires unconnected, the bare wire name is unhelpful, so this
traces the wire back to its originating port and explains the expansion.
Parameters
----------
ports : list[Port]
The ports of the tile whose switch matrix is being generated.
port_name : str
The expanded wire name that has no connections.
Returns
-------
str
A diagnostic message to append to the base error, or an empty string
when `port_name` is not a nested wire of a NULL-terminated spanning
wire.
"""
for port in ports:
expanded = port.expandPortInfoByName()
if port_name not in expanded:
continue
distance = abs(port.xOffset) + abs(port.yOffset)
isNullTerminated = port.sourceName == "NULL" or port.destinationName == "NULL"
if not (isNullTerminated and distance > 1):
return ""
return (
f"\n '{port_name}' is one of {len(expanded)} nested wires expanded "
f"from wire spec '{port.name}' (wires={port.wireCount}, "
f"distance={distance}). A NULL-terminated wire connects all nested "
f"wires: wires x distance = {port.wireCount} x {distance} = "
f"{len(expanded)} ({expanded[0]}..{expanded[-1]}). The switch matrix "
f"connects fewer than {len(expanded)} of them. Either connect all "
f"{len(expanded)} nested wires, or name both ends of the wire "
f"(instead of NULL) for a direct {port.wireCount}-wire "
"point-to-point bus."
)
return ""
[docs]
def genTileSwitchMatrix(
writer: CodeGenerator,
tile: Tile,
switch_matrix_debug_signal: bool,
csv_output_dir: Path | None = None,
config_bit_mode: ConfigBitMode = ConfigBitMode.FRAME_BASED,
multiplexer_style: MultiplexerStyle = MultiplexerStyle.CUSTOM,
default_pip_delay: int = 80,
preserve_list_order: bool = False,
) -> None:
"""Generate the RTL code for the tile switch matrix.
The switch matrix generated will be based on the `matrixDir` attribute of the tile.
If the given file format is `.csv`, it will be parsed as a switch matrix
`.csv` file.
If the given file format is `.list`, the tool will convert the `.list` file
into a switch matrix with specific ordering first before progressing. If the given
file format is Verilog or VHDL, then the function will not generate anything.
Parameters
----------
writer : CodeGenerator
The code generator instance for RTL output
tile : Tile
The tile object containing BELs and port information
switch_matrix_debug_signal : bool
Whether to generate debug signals for the switch matrix.
csv_output_dir : Path | None
Optional directory to write the generated CSV file when converting from
`.list` format. If None, the CSV is written to the same directory as the
source `.list` file. This parameter is ignored when the input is already
a `.csv` file.
config_bit_mode : ConfigBitMode
The configuration-bit mode for the tile (frame-based or flip-flop chain).
multiplexer_style : MultiplexerStyle
The multiplexer style used to implement switch-matrix muxes.
default_pip_delay : int
Per-mux delay (ps) emitted on assign statements in the switch matrix.
preserve_list_order : bool
When True, `list2CSV` writes a per-row 1-based index encoding the
connection's position in the `.list` file so the mux input order
can be recovered downstream. Defaults to False (legacy behaviour).
Raises
------
InvalidFileType
If `matrixDir` does not contain a valid file format.
ValueError
If any port in the switch matrix is not connected to anything.
"""
# convert the matrix to a dictionary map and performs entry check
connections: dict[str, list[str]] = {}
if tile.matrixDir.suffix == ".csv":
connections = parseMatrix(tile.matrixDir, tile.name)
elif tile.matrixDir.suffix == ".list":
logger.info(f"{tile.name} matrix is a list file")
logger.info(
f"Bootstrapping {tile.name} to matrix form and adding the list file to the "
"matrix"
)
# Determine CSV output path
if csv_output_dir is not None:
csv_output_dir.mkdir(parents=True, exist_ok=True)
matrixDir = csv_output_dir / f"{tile.matrixDir.stem}.csv"
else:
matrixDir = tile.matrixDir.with_suffix(".csv")
bootstrapSwitchMatrix(tile, matrixDir)
list2CSV(tile.matrixDir, matrixDir, preserve_list_order)
logger.info(
f"Update matrix directory to {matrixDir} for Fabric Tile Dictionary"
)
tile.matrixDir = matrixDir
connections = parseMatrix(tile.matrixDir, tile.name)
elif tile.matrixDir.suffix in [".v", ".sv", ".vhdl"]:
logger.info(
f"A switch matrix file is provided in {tile.name}, "
"will skip the matrix generation process"
)
return
else:
raise InvalidFileType("Invalid matrix file format.")
noConfigBits = 0
for port_name in connections:
if not connections[port_name]:
hint = _unconnected_port_diagnostic(tile.portsInfo, port_name)
raise ValueError(f"{port_name} not connected to anything!{hint}")
mux_size = len(connections[port_name])
if mux_size >= 2:
noConfigBits += (mux_size - 1).bit_length()
# we pass the NumberOfConfigBits as a comment in the beginning of the file.
# This simplifies it to generate the configuration port only if needed later when
# building the fabric where we are only working with the VHDL files
# Generate header
writer.addComment(f"NumberOfConfigBits: {noConfigBits}")
writer.addHeader(f"{tile.name}_switch_matrix")
if noConfigBits > 0:
writer.addParameterStart(indentLevel=1)
writer.addParameter("NoConfigBits", "integer", noConfigBits, indentLevel=2)
writer.addParameterEnd(indentLevel=1)
writer.addPortStart(indentLevel=1)
# normal wire input (excludes JUMP and SJUMP which are handled separately)
for i in tile.portsInfo:
if (
i.wireDirection not in (Direction.JUMP, Direction.SJUMP)
and i.inOut == IO.INPUT
):
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.INPUT, indentLevel=2)
# bel wire input
for b in tile.bels:
for p in b.outputs:
writer.addPortScalar(p, IO.INPUT, indentLevel=2)
# jump wire input
for i in tile.portsInfo:
if i.wireDirection == Direction.JUMP and i.inOut == IO.INPUT:
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.INPUT, indentLevel=2)
# normal wire output (excludes JUMP and SJUMP which are handled separately)
for i in tile.portsInfo:
if (
i.wireDirection not in (Direction.JUMP, Direction.SJUMP)
and i.inOut == IO.OUTPUT
):
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.OUTPUT, indentLevel=2)
# bel wire output
for b in tile.bels:
for p in b.inputs:
writer.addPortScalar(p, IO.OUTPUT, indentLevel=2)
# jump wire output
for i in tile.portsInfo:
if i.wireDirection == Direction.JUMP and i.inOut == IO.OUTPUT:
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.OUTPUT, indentLevel=2)
# sjump wire output - SM drives OUTPUT signals exiting to supertile SM
for i in tile.portsInfo:
if i.wireDirection == Direction.SJUMP and i.inOut == IO.OUTPUT:
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.OUTPUT, indentLevel=2)
# sjump wire input - SM receives INPUT signals arriving from supertile SM
for i in tile.portsInfo:
if i.wireDirection == Direction.SJUMP and i.inOut == IO.INPUT:
for p in i.expandPortInfoByName():
writer.addPortScalar(p, IO.INPUT, indentLevel=2)
writer.addComment("global", onNewLine=True)
if noConfigBits > 0:
if config_bit_mode == ConfigBitMode.FLIPFLOP_CHAIN:
writer.addPortScalar("MODE", IO.INPUT, indentLevel=2)
writer.addComment("global signal 1: configuration, 0: operation")
writer.addPortScalar("CONFin", IO.INPUT, indentLevel=2)
writer.addPortScalar("CONFout", IO.OUTPUT, indentLevel=2)
writer.addPortScalar("CLK", IO.INPUT, indentLevel=2)
if config_bit_mode == ConfigBitMode.FRAME_BASED:
writer.addPortVector(
"ConfigBits", IO.INPUT, "NoConfigBits-1", indentLevel=2
)
writer.addPortVector(
"ConfigBits_N", IO.INPUT, "NoConfigBits-1", indentLevel=2
)
writer.addPortEnd()
writer.addHeaderEnd(f"{tile.name}_switch_matrix")
writer.addDesignDescriptionStart(f"{tile.name}_switch_matrix")
_gen_switch_matrix_body(
writer,
tile.name,
connections,
noConfigBits,
config_bit_mode,
multiplexer_style,
default_pip_delay,
switch_matrix_debug_signal,
)
def _gen_switch_matrix_body(
writer: CodeGenerator,
name: str,
connections: dict[str, list[str]],
noConfigBits: int,
config_bit_mode: ConfigBitMode,
multiplexer_style: MultiplexerStyle,
default_pip_delay: int,
switch_matrix_debug_signal: bool,
) -> None:
"""Emit the body of a switch matrix module (constants, signals, mux logic).
Called after the port list has been written. Handles constant declarations,
signal declarations, mux instantiation, optional debug signals, and the
closing `addDesignDescriptionEnd` / `writeToFile` calls.
Parameters
----------
writer : CodeGenerator
Code generator instance for RTL output.
name : str
Module/tile name used in log messages.
connections : dict[str, list[str]]
Mapping from sink port name to list of source port names.
noConfigBits : int
Total number of configuration bits for this matrix.
config_bit_mode : ConfigBitMode
Frame-based or flip-flop chain configuration.
multiplexer_style : MultiplexerStyle
Custom or generic multiplexer implementation.
default_pip_delay : int
Per-mux delay (ps) emitted on assign statements.
switch_matrix_debug_signal : bool
Whether to generate debug signals.
"""
# constant declaration - provides '0'/'1' as padding inputs to muxes
vhdl = isinstance(writer, VHDLCodeGenerator)
for const in SWITCH_MATRIX_CONSTANTS:
if const.startswith("GND"):
writer.addConstant(const, "0" if vhdl else "1'b0")
else:
writer.addConstant(const, "1" if vhdl else "1'b1")
writer.addNewLine()
# signal declaration - one input-concat vector per multi-input mux
for portName in connections:
if len(connections[portName]) > 1:
writer.addConnectionVector(
f"{portName}_input", f"{len(connections[portName])}-1"
)
### SwitchMatrixDebugSignals ### SwitchMatrixDebugSignals ###
if switch_matrix_debug_signal:
writer.addNewLine()
for portName in connections:
muxSize = len(connections[portName])
if muxSize >= 2:
paddedMuxSize = 2 ** (muxSize - 1).bit_length() - 1
writer.addConnectionVector(
f"DEBUG_select_{portName}",
f"{paddedMuxSize.bit_length() - 1}",
)
writer.addComment(
"The configuration bits (if any) are just a long shift register",
onNewLine=True,
)
writer.addComment(
"This shift register is padded to an even number of flops/latches",
onNewLine=True,
)
if noConfigBits > 0:
if config_bit_mode == "ff_chain":
writer.addConnectionVector("ConfigBits", noConfigBits)
if config_bit_mode == "FlipFlopChain":
writer.addConnectionVector(
"ConfigBits", int(math.ceil(noConfigBits / 2.0)) * 2
)
writer.addConnectionVector(
"ConfigBitsInput", int(math.ceil(noConfigBits / 2.0)) * 2
)
writer.addLogicStart()
# TODO Should ff_chain be the same as FlipFlopChain?
if noConfigBits > 0:
if config_bit_mode == "ff_chain":
writer.addShiftRegister(noConfigBits)
elif config_bit_mode == ConfigBitMode.FLIPFLOP_CHAIN:
writer.addFlipFlopChain(noConfigBits)
elif config_bit_mode == ConfigBitMode.FRAME_BASED:
pass
# the switch matrix implementation
# we use the following variable to count the configuration bits of a
# long shift register which actually holds the switch matrix configuration
configBitstreamPosition = 0
for portName in connections:
muxSize = len(connections[portName])
writer.addComment(
f"switch matrix multiplexer {portName} MUX-{muxSize}", onNewLine=True
)
if muxSize == 0:
logger.warning(
f"Input port {portName} of switch matrix in {name} is unused"
)
writer.addComment(
f"WARNING unused multiplexer MUX-{portName}", onNewLine=True
)
elif muxSize == 1:
if connections[portName][0] == "0":
writer.addAssignScalar(portName, 0)
elif connections[portName][0] == "1":
writer.addAssignScalar(portName, 1)
else:
writer.addAssignScalar(
portName,
connections[portName][0],
delay=default_pip_delay,
)
writer.addNewLine()
elif muxSize >= 2:
paddedMuxSize = 2 ** (muxSize - 1).bit_length()
muxComponentName = f"cus_mux{paddedMuxSize}1"
portsPairs = []
start = 0
for start in range(muxSize):
portsPairs.append((f"A{start}", f"{portName}_input[{start}]"))
for end in range(start + 1, paddedMuxSize):
portsPairs.append((f"A{end}", "GND0"))
if multiplexer_style == MultiplexerStyle.CUSTOM:
if paddedMuxSize == 2:
portsPairs.append(("S", f"ConfigBits[{configBitstreamPosition}+0]"))
else:
for i in range(paddedMuxSize.bit_length() - 1):
portsPairs.append(
(f"S{i}", f"ConfigBits[{configBitstreamPosition}+{i}]")
)
portsPairs.append(
(
f"S{i}N",
f"ConfigBits_N[{configBitstreamPosition}+{i}]",
)
)
portsPairs.append(("X", f"{portName}"))
# Drive the mux input vector for both mux styles.
writer.addAssignScalar(
f"{portName}_input",
connections[portName][::-1],
delay=default_pip_delay,
)
if multiplexer_style == MultiplexerStyle.CUSTOM:
writer.addInstantiation(
compName=muxComponentName,
compInsName=f"inst_{muxComponentName}_{portName}",
portsPairs=portsPairs,
)
if muxSize not in (2, 4, 8, 16):
logger.warning(
f"creating a MUX-{muxSize} for port {portName} using "
f"MUX-{muxSize} in switch matrix for {name}"
)
else:
# generic multiplexer: select the input behaviorally so it
# synthesises to standard cells. The writer emits the indexing
# in language-correct syntax for Verilog and VHDL.
select_width = paddedMuxSize.bit_length() - 1
writer.addMuxAssign(
portName,
f"{portName}_input",
"ConfigBits",
configBitstreamPosition,
select_width,
delay=default_pip_delay,
)
configBitstreamPosition += paddedMuxSize.bit_length() - 1
if switch_matrix_debug_signal:
logger.info(f"Generate debug signals for switch matrix in {name}")
writer.addNewLine()
configBitstreamPosition = 0
old_ConfigBitstreamPosition = 0
for portName in connections:
muxSize = len(connections[portName])
if muxSize >= 2:
paddedMuxSize = 2 ** (muxSize - 1).bit_length()
configBitstreamPosition += paddedMuxSize.bit_length() - 1
writer.addAssignVector(
f"DEBUG_select_{portName:<15}",
"ConfigBits",
f"{configBitstreamPosition - 1}",
old_ConfigBitstreamPosition,
)
old_ConfigBitstreamPosition = configBitstreamPosition
### SwitchMatrixDebugSignals ### SwitchMatrixDebugSignals ###
writer.addDesignDescriptionEnd()
writer.writeToFile()
[docs]
def gen_super_tile_switch_matrix(
writer: CodeGenerator,
superTile: SuperTile,
config_bit_mode: ConfigBitMode = ConfigBitMode.FRAME_BASED,
multiplexer_style: MultiplexerStyle = MultiplexerStyle.CUSTOM,
default_pip_delay: int = 80,
) -> None:
"""Generate the switch matrix RTL for a supertile.
The supertile switch matrix routes SJUMP output signals from child tiles to
the input ports of supertile-level BELs. Its connectivity is described by
`superTile.supertile_matrix_dir` (a `.list` or `.csv` file using the same
format as tile switch matrices).
Parameters
----------
writer : CodeGenerator
Code generator instance for RTL output.
superTile : SuperTile
The supertile whose BELs and SJUMP ports drive this matrix.
config_bit_mode : ConfigBitMode
Frame-based or flipflop-chain configuration.
multiplexer_style : MultiplexerStyle
Custom or generic multiplexer implementation.
default_pip_delay : int
Default PIP delay value for timing annotation.
"""
if superTile.supertile_matrix_dir is None:
return
noConfigBits = superTile.supertile_matrix_config_bits
module_name = f"{superTile.name}_switch_matrix"
# Parse connectivity (destination -> [sources]).
matrix_path = superTile.supertile_matrix_dir
if matrix_path.suffix == ".list":
raw_pairs = parseList(matrix_path)
connections: dict[str, list[str]] = {}
for dest, src in raw_pairs:
connections.setdefault(dest, []).append(src)
else:
connections = parseMatrix(matrix_path, superTile.name)
writer.addComment(f"NumberOfConfigBits: {noConfigBits}")
writer.addHeader(module_name)
if noConfigBits > 0:
writer.addParameterStart(indentLevel=1)
writer.addParameter("NoConfigBits", "integer", noConfigBits, indentLevel=2)
writer.addParameterEnd(indentLevel=1)
writer.addPortStart(indentLevel=1)
# Inputs: SJUMP OUTPUT signals from each child tile ({tileName}_{portName}{i})
all_sjump_ports = superTile.get_all_sjump_ports()
if all_sjump_ports:
writer.addComment("SJUMP inputs from child tiles", onNewLine=True)
for lx, ly, p in all_sjump_ports:
tileName = superTile.tileMap[ly][lx].name
for k in range(p.wireCount):
writer.addPortScalar(f"{tileName}_{p.name}{k}", IO.INPUT, indentLevel=2)
# Outputs: input ports of supertile BELs (SM drives BEL inputs)
if superTile.bels:
writer.addComment("BEL input ports (SM outputs)", onNewLine=True)
for bel in superTile.bels:
for p in bel.inputs:
writer.addPortScalar(p, IO.OUTPUT, indentLevel=2)
# Inputs: output ports of supertile BELs (SM routes them back to child tiles)
if any(bel.outputs for bel in superTile.bels):
writer.addComment("BEL output ports (SM inputs)", onNewLine=True)
for bel in superTile.bels:
for p in bel.outputs:
writer.addPortScalar(p, IO.INPUT, indentLevel=2)
# Outputs: reverse SJUMP signals driven back into child tiles
all_input_sjump = superTile.get_all_input_sjump_ports()
if all_input_sjump:
writer.addComment("Reverse SJUMP outputs (SM -> child tile)", onNewLine=True)
for lx, ly, p in all_input_sjump:
tileName = superTile.tileMap[ly][lx].name
for k in range(p.wireCount):
writer.addPortScalar(
f"{tileName}_{p.name}{k}", IO.OUTPUT, indentLevel=2
)
writer.addComment("global", onNewLine=True)
if noConfigBits > 0:
if config_bit_mode == ConfigBitMode.FLIPFLOP_CHAIN:
writer.addPortScalar("MODE", IO.INPUT, indentLevel=2)
writer.addPortScalar("CONFin", IO.INPUT, indentLevel=2)
writer.addPortScalar("CONFout", IO.OUTPUT, indentLevel=2)
writer.addPortScalar("CLK", IO.INPUT, indentLevel=2)
if config_bit_mode == ConfigBitMode.FRAME_BASED:
writer.addPortVector(
"ConfigBits", IO.INPUT, "NoConfigBits-1", indentLevel=2
)
writer.addPortVector(
"ConfigBits_N", IO.INPUT, "NoConfigBits-1", indentLevel=2
)
writer.addPortEnd()
writer.addHeaderEnd(module_name)
writer.addDesignDescriptionStart(module_name)
_gen_switch_matrix_body(
writer,
superTile.name,
connections,
noConfigBits,
config_bit_mode,
multiplexer_style,
default_pip_delay,
switch_matrix_debug_signal=False,
)