"""Configuration memory generation module.
This module provides functions to generate configuration memory initialization files and
RTL code for fabric tiles. It handles the mapping of configuration bits to frames and
generates the necessary hardware description language code for memory access and
control.
"""
import csv
from pathlib import Path
from typing import TYPE_CHECKING
from bitarray import bitarray
from loguru import logger
from fabulous.fabric_definition.define import IO
from fabulous.fabric_generator.code_generator.code_generator import CodeGenerator
from fabulous.fabric_generator.code_generator.code_generator_Verilog import (
VerilogCodeGenerator,
)
from fabulous.fabric_generator.parser.parse_configmem import parseConfigMem
if TYPE_CHECKING:
from fabulous.fabric_definition.configmem import ConfigMem
from fabulous.fabric_definition.supertile import SuperTile
[docs]
def generateConfigMemInit(
file: Path,
tileConfigBitsCount: int,
frame_bits_per_row: int = 32,
max_frame_per_col: int = 20,
) -> None:
"""Generate the config memory initialization file.
The amount of configuration bits is determined
by `frame_bits_per_row`. The function will pack the configuration bit from
the highest to the lowest bit in the config memory. I. e. if there are 100
configuration bits, with 32 frame bits per row, the function will pack from
bit 99 starting from bit 31 of frame 0 to bit 28 of frame 3.
Parameters
----------
file : Path
The output file of the config memory initialization file.
tileConfigBitsCount : int
The number of tile config bits of the tile.
frame_bits_per_row : int
The number of configuration bits per frame row.
max_frame_per_col : int
The number of frames stored per tile column.
Raises
------
ValueError
If the tile config bits exceed the fabric capacity.
"""
if tileConfigBitsCount > frame_bits_per_row * max_frame_per_col:
raise ValueError(
f"Tile config bits ({tileConfigBitsCount}) exceed fabric capacity "
f"({frame_bits_per_row * max_frame_per_col} bits). "
f"Please adjust the tile configuration."
)
fieldName = [
"frame_name",
"frame_index",
"bits_used_in_frame",
"used_bits_mask",
"ConfigBits_ranges",
]
with file.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(fieldName)
bits = bitarray(frame_bits_per_row * max_frame_per_col)
bits[:tileConfigBitsCount] = 1
# adjust for zero-based indexing in subsequent calculations
tileConfigBitsCount -= 1
count = 0
for k in range(max_frame_per_col):
entry = {}
# frame0, frame1, ...
entry["frame_name"] = f"frame{k}"
# and the index (0, 1, 2, ...), in case we need
entry["frame_index"] = str(k)
bitSlice = bits[count : count + frame_bits_per_row]
entry["bits_used_in_frame"] = bitSlice.count(1)
entry["used_bits_mask"] = bitSlice.to01(group=4, sep="_")
if bitSlice.count(1) == 0:
entry["ConfigBits_ranges"] = "# NULL"
else:
entry["ConfigBits_ranges"] = (
f"{tileConfigBitsCount}:"
f"{max(tileConfigBitsCount - frame_bits_per_row + 1, 0)}"
)
count += frame_bits_per_row
tileConfigBitsCount -= frame_bits_per_row
writer.writerow([entry[field] for field in fieldName])
[docs]
def generateConfigMem(
writer: CodeGenerator,
name: str,
config_bits_count: int,
configMemCsv: Path,
frame_bits_per_row: int = 32,
max_frame_per_col: int = 20,
) -> None:
"""Generate the RTL code for configuration memory.
If the given configMemCsv file does not exist, it will be created using
`generateConfigMemInit`.
We use a file to describe the exact configuration bits to frame mapping
the following command generates an init file with a
simple enumerated default mapping (e.g. 'LUT4AB_ConfigMem.init.csv')
if we run this function again, but have such a file (without the .init),
then that mapping will be used
Parameters
----------
writer : CodeGenerator
The code generator instance for RTL output
name : str
Name of the tile or module (used for module naming and log messages).
config_bits_count : int
Total number of configuration bits.
configMemCsv : Path
The directory of the config memory CSV file.
frame_bits_per_row : int
The number of configuration bits per frame row.
max_frame_per_col : int
The number of frames stored per tile column.
Raises
------
ValueError
- If the config bits exceed the fabric capacity.
- If the total config bits in the config memory CSV file does not match
config_bits_count.
"""
if config_bits_count > frame_bits_per_row * max_frame_per_col:
raise ValueError(
f"{name} has {config_bits_count} global config bits, "
" which exceeds fabric capacity "
f"({frame_bits_per_row * max_frame_per_col} bits). "
"Please adjust the configuration."
)
configMemList: list[ConfigMem] = []
if configMemCsv.exists():
if config_bits_count <= 0:
logger.warning(
f"Found bitstream mapping file {name}_configMem.csv for {name}, "
"but no global config bits are defined"
)
else:
logger.info(f"Found bitstream mapping file {name}_configMem.csv for {name}")
logger.info(f"Parsing {name}_configMem.csv")
configMemList = parseConfigMem(
configMemCsv,
max_frame_per_col,
frame_bits_per_row,
config_bits_count,
)
elif config_bits_count > 0:
logger.info(f"{name}_configMem.csv does not exist")
logger.info(f"Generating a default configMem for {name}")
generateConfigMemInit(
configMemCsv,
config_bits_count,
frame_bits_per_row=frame_bits_per_row,
max_frame_per_col=max_frame_per_col,
)
logger.info(f"Parsing {name}_configMem.csv")
configMemList = parseConfigMem(
configMemCsv,
max_frame_per_col,
frame_bits_per_row,
config_bits_count,
)
else:
logger.info(
f"No config bits defined and no bitstream mapping file provided for {name}"
)
return
totalConfigBits = sum(i.bitsUsedInFrame for i in configMemList)
logger.info(
f"Found {len(configMemList)} config memory entries in "
f"{name}_configMem.csv with a total of {totalConfigBits} bits"
)
logger.info(f"{name} has {config_bits_count} global config bits")
if totalConfigBits != config_bits_count:
raise ValueError(
f"Total config bits in {name}_configMem.csv ({totalConfigBits}) "
f"does not match global config bits ({config_bits_count})"
)
# start writing the file
logger.info(f"Generating {writer.outFileName} for {name}")
writer.addHeader(f"{name}_ConfigMem")
writer.addParameterStart(indentLevel=1)
if isinstance(writer, VerilogCodeGenerator): # emulation only in Verilog
maxBits = frame_bits_per_row * max_frame_per_col
writer.addPreprocIfDef("EMULATION")
writer.addParameter(
"Emulate_Bitstream",
f"[{maxBits - 1}:0]",
f"{maxBits}'b0",
indentLevel=2,
)
writer.addPreprocEndif()
if max_frame_per_col != 0:
writer.addParameter(
"MaxFramesPerCol", "integer", max_frame_per_col, indentLevel=2
)
if frame_bits_per_row != 0:
writer.addParameter(
"FrameBitsPerRow", "integer", frame_bits_per_row, indentLevel=2
)
writer.addParameter("NoConfigBits", "integer", config_bits_count, indentLevel=2)
writer.addParameterEnd(indentLevel=1)
writer.addPortStart(indentLevel=1)
# the port definitions are generic
writer.addPortVector("FrameData", IO.INPUT, "FrameBitsPerRow - 1", indentLevel=2)
writer.addPortVector("FrameStrobe", IO.INPUT, "MaxFramesPerCol - 1", indentLevel=2)
writer.addPortVector("ConfigBits", IO.OUTPUT, "NoConfigBits - 1", indentLevel=2)
writer.addPortVector("ConfigBits_N", IO.OUTPUT, "NoConfigBits - 1", indentLevel=2)
writer.addPortEnd(indentLevel=1)
writer.addHeaderEnd(f"{name}_ConfigMem")
writer.addNewLine()
# declare architecture
writer.addDesignDescriptionStart(f"{name}_ConfigMem")
if isinstance(writer, VerilogCodeGenerator): # emulation only in Verilog
writer.addPreprocIfDef("EMULATION")
for i in configMemList:
counter = 0
for k in range(frame_bits_per_row):
# Safely check if bit is set, treat missing bits as '0'
bit_value = i.usedBitMask[k] if k < len(i.usedBitMask) else "0"
if bit_value == "1":
index = i.frameIndex * frame_bits_per_row + (
frame_bits_per_row - 1 - k
)
writer.addAssignScalar(
f"ConfigBits[{i.configBitRanges[counter]}]",
f"Emulate_Bitstream[{index}]",
)
counter += 1
writer.addPreprocElse()
writer.addNewLine()
writer.addNewLine()
writer.addLogicStart()
writer.addComment("instantiate frame latches", end="")
for i in configMemList:
counter = 0
for k in range(frame_bits_per_row):
# Safely check if bit is set, treat missing bits as '0'
bit_value = i.usedBitMask[k] if k < len(i.usedBitMask) else "0"
if bit_value == "1":
writer.addInstantiation(
compName="config_latch",
compInsName=(f"Inst_{i.frameName}_bit{frame_bits_per_row - 1 - k}"),
portsPairs=[
("D", f"FrameData[{frame_bits_per_row - 1 - k}]"),
("E", f"FrameStrobe[{i.frameIndex}]"),
("Q", f"ConfigBits[{i.configBitRanges[counter]}]"),
("QN", f"ConfigBits_N[{i.configBitRanges[counter]}]"),
],
)
counter += 1
if isinstance(writer, VerilogCodeGenerator): # emulation only in Verilog
writer.addPreprocEndif()
writer.addDesignDescriptionEnd()
writer.writeToFile()
def _read_config_mem_masks(
config_mem_csv: Path, max_frames_per_col: int
) -> dict[int, str]:
"""Read a ConfigMem CSV into a `{frame_index: used_bits_mask}` mapping.
Parameters
----------
config_mem_csv : Path
Path to a `*_ConfigMem.csv` file.
max_frames_per_col : int
Expected number of frame rows.
Raises
------
ValueError
If the file does not have exactly `max_frames_per_col` rows.
Returns
-------
dict[int, str]
Mapping from frame index to its `used_bits_mask` (underscores stripped).
"""
with config_mem_csv.open() as f:
rows = list(csv.DictReader(f))
if len(rows) != max_frames_per_col:
raise ValueError(
f"ConfigMem {config_mem_csv} has {len(rows)} rows but MaxFramesPerCol "
f"is {max_frames_per_col}."
)
return {int(r["frame_index"]): r["used_bits_mask"].replace("_", "") for r in rows}
[docs]
def validate_super_tile_config_mem(
super_tile_config_mem_csv: Path,
master_config_mem_csv: Path,
num_bits_needed: int,
frame_bits_per_row: int = 32,
max_frames_per_col: int = 20,
) -> None:
"""Validate an existing supertile ConfigMem against the master tile.
A supertile ConfigMem reuses the *free* bit slots of its master tile's frame
space. Reusing an existing file is only safe if it still matches the current
supertile bit count and does not overlap any bit the master tile itself uses.
Parameters
----------
super_tile_config_mem_csv : Path
Path to the existing supertile `*_ConfigMem.csv` to validate.
master_config_mem_csv : Path
Path to the master tile's `*_ConfigMem.csv`.
num_bits_needed : int
Number of supertile configuration bits that must be present.
frame_bits_per_row : int
Number of bits per frame row.
max_frames_per_col : int
Number of frames per column.
Raises
------
ValueError
If either file has the wrong row count, the supertile does not use exactly
`num_bits_needed` bits, or any frame bit is used by both ConfigMems.
"""
st_masks = _read_config_mem_masks(super_tile_config_mem_csv, max_frames_per_col)
master_masks = _read_config_mem_masks(master_config_mem_csv, max_frames_per_col)
used_bits = sum(mask.count("1") for mask in st_masks.values())
if used_bits != num_bits_needed:
raise ValueError(
f"Supertile ConfigMem {super_tile_config_mem_csv} uses {used_bits} config "
f"bits but the supertile switch matrix needs {num_bits_needed}. Delete "
"the file to regenerate it."
)
for frame_idx, st_mask in st_masks.items():
master_mask = master_masks.get(frame_idx, "0" * frame_bits_per_row)
conflicts = [
k
for k, (a, b) in enumerate(zip(st_mask, master_mask, strict=True))
if a == "1" and b == "1"
]
if conflicts:
raise ValueError(
f"Supertile ConfigMem {super_tile_config_mem_csv} conflicts with "
f"the master tile ConfigMem {master_config_mem_csv} in frame "
f"{frame_idx} at "
f"bit position(s) {conflicts}: both drive the same physical config "
"bit. Delete the supertile ConfigMem to regenerate it."
)
[docs]
def build_super_tile_config_mem_csv(
master_config_mem_csv: Path,
num_bits_needed: int,
output_path: Path,
frame_bits_per_row: int = 32,
max_frames_per_col: int = 20,
) -> None:
"""Build a ConfigMem CSV for a supertile SM using free slots from the master tile.
Reads the master tile's ConfigMem CSV, collects bit positions where the
`used_bits_mask` is `'0'` (free), and writes a new CSV that maps
`ST_ConfigBits[0..num_bits_needed-1]` to those positions. The output CSV
has exactly `max_frames_per_col` rows so it is accepted by `parseConfigMem`.
Parameters
----------
master_config_mem_csv : Path
Path to the master tile's existing `*_ConfigMem.csv`.
num_bits_needed : int
Number of supertile configuration bits to place.
output_path : Path
Destination path for the generated supertile ConfigMem CSV.
frame_bits_per_row : int
Number of bits per frame row (must match the fabric setting).
max_frames_per_col : int
Number of frames per column (must match the fabric setting).
Raises
------
ValueError
If the master tile's ConfigMem CSV does not have exactly `max_frames_per_col`
rows, or if there are fewer free slots than `num_bits_needed`.
"""
# Reuse an existing (possibly hand-tuned) supertile ConfigMem instead of
# overwriting it, but only after confirming it is still consistent with the
# master tile's frame usage. A mismatch raises so the user deletes the file
# to force regeneration rather than silently shipping a broken bitstream.
if output_path.exists():
validate_super_tile_config_mem(
output_path,
master_config_mem_csv,
num_bits_needed,
frame_bits_per_row,
max_frames_per_col,
)
return
with master_config_mem_csv.open() as f:
master_rows = list(csv.DictReader(f))
if len(master_rows) != max_frames_per_col:
raise ValueError(
f"Master tile ConfigMem {master_config_mem_csv} has "
f"{len(master_rows)} rows but MaxFramesPerCol is {max_frames_per_col}."
)
# Collect free (frame_index, bit_k) slots in reading order.
# bit_k is the left-to-right index in the mask string (0 = MSB).
free_slots: list[tuple[int, int]] = []
for row in master_rows:
mask = row["used_bits_mask"].replace("_", "")
frame_idx = int(row["frame_index"])
for k, bit in enumerate(mask):
if bit == "0":
free_slots.append((frame_idx, k))
if len(free_slots) < num_bits_needed:
raise ValueError(
f"Not enough free config bit slots in master tile "
f"({master_config_mem_csv.parent.name}): need {num_bits_needed}, "
f"found only {len(free_slots)} free slots."
)
# Assign config bit indices to the first num_bits_needed free slots.
frame_assignments: dict[int, list[tuple[int, int]]] = {}
for config_bit_idx, (frame_idx, bit_k) in enumerate(free_slots[:num_bits_needed]):
frame_assignments.setdefault(frame_idx, []).append((bit_k, config_bit_idx))
field_names = [
"frame_name",
"frame_index",
"bits_used_in_frame",
"used_bits_mask",
"ConfigBits_ranges",
]
with output_path.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(field_names)
for row in master_rows:
frame_idx = int(row["frame_index"])
assignments = frame_assignments.get(frame_idx, [])
if not assignments:
mask_str = "0" * frame_bits_per_row
config_bits_ranges = "# NULL"
else:
mask_list = ["0"] * frame_bits_per_row
for bit_k, _ in assignments:
mask_list[bit_k] = "1"
mask_str = "_".join(
"".join(mask_list[i : i + 4])
for i in range(0, frame_bits_per_row, 4)
)
assignments.sort(key=lambda x: x[0])
config_bits_ranges = ";".join(str(cb_idx) for _, cb_idx in assignments)
bits_used = mask_str.replace("_", "").count("1")
writer.writerow(
[row["frame_name"], frame_idx, bits_used, mask_str, config_bits_ranges]
)
[docs]
def generate_super_tile_config_mem(
writer: CodeGenerator,
superTile: "SuperTile",
master_config_mem_csv: Path,
frame_bits_per_row: int = 32,
max_frame_per_col: int = 20,
) -> None:
"""Generate the ConfigMem RTL for a supertile switch matrix.
Builds a ConfigMem CSV that places the supertile SM's config bits into the
free slots of the master tile's frame space, then generates the Verilog/VHDL
module via `generateConfigMem`.
Parameters
----------
writer : CodeGenerator
Code generator instance for RTL output.
superTile : SuperTile
The supertile whose SM config bits need a ConfigMem.
master_config_mem_csv : Path
Path to the master tile's existing `*_ConfigMem.csv`.
frame_bits_per_row : int
Number of bits per frame row.
max_frame_per_col : int
Number of frames per column.
"""
st_config_bits = superTile.total_config_bits
if st_config_bits <= 0:
return
output_csv = superTile.tileDir.parent / f"{superTile.name}_ConfigMem.csv"
build_super_tile_config_mem_csv(
master_config_mem_csv,
st_config_bits,
output_csv,
frame_bits_per_row=frame_bits_per_row,
max_frames_per_col=max_frame_per_col,
)
generateConfigMem(
writer,
superTile.name,
st_config_bits,
output_csv,
frame_bits_per_row=frame_bits_per_row,
max_frame_per_col=max_frame_per_col,
)