Source code for fabulous.fabric_cad.gen_bitstream_spec

"""Bitstream specification generation module.

This module provides functionality to generate bitstream specifications from FPGA fabric
definitions. The specification defines how configuration bits map to physical frame
locations and is used during bitstream generation.
"""

import string
from importlib.metadata import version
from typing import TYPE_CHECKING

from loguru import logger

from fabulous.fabric_definition.fabric import Fabric
from fabulous.fabric_generator.parser.parse_configmem import parseConfigMem
from fabulous.fabric_generator.parser.parse_switchmatrix import parseList, parseMatrix
from fabulous.fabulous_settings import get_context

if TYPE_CHECKING:
    from fabulous.fabric_definition.configmem import ConfigMem


[docs] def generateBitstreamSpec(fabric: Fabric) -> dict[str, dict]: """Generate the fabric's bitstream specification. This is needed to tell where each FASM configuration is mapped to the physical bitstream The result file will be further parsed by `bit_gen.py`. Parameters ---------- fabric : Fabric The fabric object for generating the bitstream specification Returns ------- dict[str, dict] The bits stream specification of the fabric. """ specData = { "TileMap": {}, "TileSpecs": {}, "TileSpecs_No_Mask": {}, "FrameMap": {}, "FrameMapEncode": {}, "ArchSpecs": { "MaxFramesPerCol": fabric.maxFramesPerCol, "FrameBitsPerRow": fabric.frameBitsPerRow, "FrameSelectWidth": fabric.frameSelectWidth, "DesyncBit": fabric.desync_flag, "SyncHeaderHex": fabric.syncHeaderHex, "IncludeBorderRows": False, # Currently not supported in FABulous "FABulousVersion": version("FABulous-FPGA"), }, } tileMap = {} for y, row in enumerate(fabric.tile): for x, tile in enumerate(row): if tile is not None: tileMap[f"X{x}Y{y}"] = tile.name else: tileMap[f"X{x}Y{y}"] = "NULL" specData["TileMap"] = tileMap configMemList: list[ConfigMem] = [] for y, row in enumerate(fabric.tile): for x, tile in enumerate(row): if tile is None: continue if "fabric.csv" in str(tile.tileDir): # backward compatibility for old project structure # We need to take the matrixDir from the tile, since there # is the actual path to the tile defined in the fabric.csv if tile.matrixDir.is_file(): configMemPath = tile.matrixDir.parent / f"{tile.name}_ConfigMem.csv" elif tile.matrixDir.is_dir(): configMemPath = tile.matrixDir / f"{tile.name}_ConfigMem.csv" else: configMemPath = ( get_context().proj_dir / "Tile" / tile.name / f"{tile.name}_ConfigMem.csv" ) logger.warning( f"MatrixDir for {tile.name} is not a valid file or directory. " f"Assuming default path: {configMemPath}" ) else: configMemPath = tile.tileDir.parent.joinpath( f"{tile.name}_ConfigMem.csv" ) logger.info(f"ConfigMemPath: {configMemPath}") if configMemPath.exists() and configMemPath.is_file(): configMemList = parseConfigMem( configMemPath, fabric.maxFramesPerCol, fabric.frameBitsPerRow, tile.globalConfigBits, ) elif tile.globalConfigBits > 0: logger.critical( f"No ConfigMem csv file found for {tile.name} which " "have config bits" ) configMemList = [] else: logger.info(f"No config memory for {tile.name}.") configMemList = [] encodeDict = [-1] * (fabric.maxFramesPerCol * fabric.frameBitsPerRow) maskDic = {} for cfm in configMemList: maskDic[cfm.frameIndex] = cfm.usedBitMask # matching the value in the configBitRanges with the reversedBitMask # bit 0 in bit mask is the first value in the configBitRanges for i, char in enumerate(cfm.usedBitMask): if char == "1": encodeDict[cfm.configBitRanges.pop(0)] = ( fabric.frameBitsPerRow - 1 - i ) + fabric.frameBitsPerRow * cfm.frameIndex # filling the maskDic with the unused frames for i in range(fabric.maxFramesPerCol - len(configMemList)): maskDic[len(configMemList) + i] = "0" * fabric.frameBitsPerRow specData["FrameMap"][tile.name] = maskDic if tile.globalConfigBits == 0: logger.info(f"No config memory for X{x}Y{y}_{tile.name}.") specData["FrameMap"][tile.name] = {} specData["FrameMapEncode"][tile.name] = {} curBitOffset = 0 curTileMap = {} curTileMapNoMask = {} for i, bel in enumerate(tile.bels): for featureKey, keyDict in bel.belFeatureMap.items(): for entry in keyDict: if isinstance(entry, int): for v in keyDict[entry]: curTileMap[ f"{string.ascii_uppercase[i]}.{featureKey}" ] = {encodeDict[curBitOffset + v]: keyDict[entry][v]} curTileMapNoMask[ f"{string.ascii_uppercase[i]}.{featureKey}" ] = {encodeDict[curBitOffset + v]: keyDict[entry][v]} curBitOffset += len(keyDict[entry]) # All the generation will be working on the tile level with the tileDic # This is added to propagate the updated switch matrix to each of the tile # in the fabric if tile.matrixDir.suffix == ".list": tile.matrixDir = tile.matrixDir.with_suffix(".csv") result = parseMatrix(tile.matrixDir, tile.name) for source, sinkList in result.items(): controlWidth = 0 for i, sink in enumerate(reversed(sinkList)): controlWidth = (len(sinkList) - 1).bit_length() controlValue = f"{len(sinkList) - 1 - i:0{controlWidth}b}" pip = f"{sink}.{source}" if len(sinkList) < 2: curTileMap[pip] = {} curTileMapNoMask[pip] = {} continue for c, curChar in enumerate(controlValue[::-1]): if pip not in curTileMap: curTileMap[pip] = {} curTileMapNoMask[pip] = {} curTileMap[pip][encodeDict[curBitOffset + c]] = curChar curTileMapNoMask[pip][encodeDict[curBitOffset + c]] = curChar curBitOffset += controlWidth # And now we add empty config bit mappings for immutable connections # (i.e. wires), as nextpnr sees these the same as normal pips for wire in tile.wireList: curTileMap[f"{wire.source}.{wire.destination}"] = {} curTileMapNoMask[f"{wire.source}.{wire.destination}"] = {} specData["TileSpecs"][f"X{x}Y{y}"] = curTileMap specData["TileSpecs_No_Mask"][f"X{x}Y{y}"] = curTileMapNoMask # Supertile bitstream features. A supertile's config bits physically live in # its master tile's frame column (the master tile's own ConfigMem leaves those # bits free). Within the supertile config space the bit order is # [switch-matrix bits][BEL bits], matching genSuperTile()'s ST_ConfigBits # slicing. The BEL and switch-matrix features are added to the master tile's # TileSpecs entry alongside the master tile's own features. st_bel_count: dict[tuple[int, int], int] = {} for superTile in fabric.superTileDic.values(): if not superTile.bels and superTile.supertile_matrix_dir is None: continue st_config_bits = superTile.total_config_bits st_encode_dict = [-1] * (fabric.maxFramesPerCol * fabric.frameBitsPerRow) st_mask_dic: dict[int, str] = {} if st_config_bits > 0: st_config_mem_list = parseConfigMem( superTile.tileDir.parent / f"{superTile.name}_ConfigMem.csv", fabric.maxFramesPerCol, fabric.frameBitsPerRow, st_config_bits, ) for cfm in st_config_mem_list: st_mask_dic[cfm.frameIndex] = cfm.usedBitMask for i, char in enumerate(cfm.usedBitMask): if char == "1": st_encode_dict[cfm.configBitRanges.pop(0)] = ( fabric.frameBitsPerRow - 1 - i ) + fabric.frameBitsPerRow * cfm.frameIndex sm_connections: dict[str, list[str]] = {} if superTile.supertile_matrix_dir is not None: mat_path = superTile.supertile_matrix_dir if mat_path.suffix == ".list": for dest, src in parseList(mat_path): sm_connections.setdefault(dest, []).append(src) else: sm_connections = parseMatrix(mat_path, superTile.name) tx_local, ty_local = superTile.get_master_tile_coords() for base_fx, base_fy, _ in fabric.iter_super_tile_placements(superTile): ftx = base_fx + tx_local fty = base_fy + ty_local master_tile = fabric.tile[fty][ftx] frame_map = specData["FrameMap"].setdefault(master_tile.name, {}) for frame_idx, mask in st_mask_dic.items(): existing = frame_map.get(frame_idx, "0" * fabric.frameBitsPerRow) frame_map[frame_idx] = "".join( "1" if a == "1" or b == "1" else "0" for a, b in zip(existing, mask, strict=True) ) curTileMap = specData["TileSpecs"].setdefault(f"X{ftx}Y{fty}", {}) curTileMapNoMask = specData["TileSpecs_No_Mask"].setdefault( f"X{ftx}Y{fty}", {} ) curBitOffset = 0 for source, sinkList in sm_connections.items(): controlWidth = (len(sinkList) - 1).bit_length() if st_config_bits == 0: # No config bits — all connections are passthrough. for sink in sinkList: for t in (curTileMap, curTileMapNoMask): t[f"{sink}.{source}"] = {} continue for i, sink in enumerate(reversed(sinkList)): pip = f"{sink}.{source}" if len(sinkList) < 2: for t in (curTileMap, curTileMapNoMask): t[pip] = {} continue controlValue = f"{len(sinkList) - 1 - i:0{controlWidth}b}" for c, curChar in enumerate(controlValue[::-1]): for t in (curTileMap, curTileMapNoMask): t.setdefault(pip, {}) t[pip][st_encode_dict[curBitOffset + c]] = curChar curBitOffset += controlWidth bel_coord = (ftx, fty) bel_offset = len(master_tile.bels) + st_bel_count.get(bel_coord, 0) for i, bel in enumerate(superTile.bels): letter = string.ascii_uppercase[bel_offset + i] for featureKey, keyDict in bel.belFeatureMap.items(): for entry in keyDict: if not isinstance(entry, int): continue for v in keyDict[entry]: for t in (curTileMap, curTileMapNoMask): t[f"{letter}.{featureKey}"] = { st_encode_dict[curBitOffset + v]: keyDict[entry][v] } curBitOffset += len(keyDict[entry]) st_bel_count[bel_coord] = bel_offset + len(superTile.bels) return specData