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.fabulous_settings import get_context

if TYPE_CHECKING:
    from fabulous.fabric_definition.configmem import ConfigMem


[docs] def border_rows_have_config_bits(fabric: Fabric) -> bool: """Check whether the top or bottom fabric row holds any config bits. Parameters ---------- fabric : Fabric The fabric object whose border rows are inspected. Returns ------- bool True if any tile in the top or bottom row has configuration bits. """ if not fabric.tile: return False border_rows = (fabric.tile[0], fabric.tile[-1]) return any( tile is not None and tile.globalConfigBits > 0 for row in border_rows for tile in row )
[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": border_rows_have_config_bits(fabric), "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 compat: in the old fabric.csv-embedded layout the # tile's real location comes from its switch-matrix file path. matrix_file = tile.switch_matrix.matrix_file if matrix_file.is_file(): configMemPath = matrix_file.parent / 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 (k for k in keyDict if isinstance(k, 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]) result = tile.switch_matrix.connections 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 super_tile in fabric.superTileDic.values(): if not super_tile.bels and super_tile.supertile_matrix_dir is None: continue st_config_bits = super_tile.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( super_tile.tileDir.parent / f"{super_tile.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 super_tile.switch_matrix is not None: sm_connections = super_tile.switch_matrix.connections tx_local, ty_local = super_tile.get_master_tile_coords() for base_fx, base_fy, _ in fabric.iter_super_tile_placements(super_tile): 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(super_tile.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(super_tile.bels) return specData