prjxray/fuzzers/005-tilegrid/generate.py

570 lines
19 KiB
Python

#!/usr/bin/env python3
'''
Historically we grouped data into "segments"
These were a region of the bitstream that encoded one or more tiles
However, this didn't scale with certain tiles like BRAM
Some sites had multiple bitstream areas and also occupied multiple tiles
Decoding was then shifted to instead describe how each title is encoded
A post processing step verifies that two tiles don't reference the same bitstream area
'''
import os, sys, json, re
# matches lib/include/prjxray/xilinx/xc7series/block_type.h
block_type_i2s = {
0: 'CLB_IO_CLK',
1: 'BLOCK_RAM',
2: 'CFG_CLB',
# special...maybe should error until we know what it is?
# 3: 'RESERVED',
}
def addr2btype(base_addr):
'''
Convert integer address to block type
Table 5-24: Frame Address Register Description
Bit Index: [25:23]
https://www.xilinx.com/support/documentation/user_guides/ug470_7Series_Config.pdf
"Valid block types are CLB, I/O, CLK ( 000 ), block RAM content ( 001 ), and CFG_CLB ( 010 ). A normal bitstream does not include type 011 ."
'''
block_type_i = (base_addr >> 23) & 0x7
return block_type_i2s[block_type_i]
def nolr(tile_type):
'''
Remove _L or _R suffix tile_type suffix, if present
Ex: BRAM_INT_INTERFACE_L => BRAM_INT_INTERFACE
Ex: VBRK => VBRK
'''
postfix = tile_type[-2:]
if postfix in ('_L', '_R'):
return tile_type[:-2]
else:
return tile_type
def load_tiles(tiles_fn):
'''
"$type $tile $grid_x $grid_y $typed_sites"
typed_sites: foreach t $site_types s $sites
'''
tiles = list()
with open(tiles_fn) as f:
for line in f:
# CLBLM_L CLBLM_L_X10Y98 30 106 SLICEL SLICE_X13Y98 SLICEM SLICE_X12Y98
record = line.split()
tile_type, tile_name, grid_x, grid_y = record[0:4]
grid_x, grid_y = int(grid_x), int(grid_y)
sites = {}
for i in range(4, len(record), 2):
site_type, site_name = record[i:i + 2]
sites[site_name] = site_type
tile = {
'type': tile_type,
'name': tile_name,
'grid_x': grid_x,
'grid_y': grid_y,
'sites': sites,
}
tiles.append(tile)
return tiles
def load_baseaddrs(deltas_fns):
site_baseaddr = dict()
for arg in deltas_fns:
with open(arg) as f:
line = f.read().strip()
site = arg[7:-6]
frame = int(line[5:5 + 8], 16)
# was "0x%08x"
site_baseaddr[site] = frame & ~0x7f
return site_baseaddr
def make_database(tiles):
# tile database with X, Y, and list of sites
# tile name as keys
database = dict()
for tile in tiles:
database[tile["name"]] = {
"type": tile["type"],
"sites": tile["sites"],
"grid_x": tile["grid_x"],
"grid_y": tile["grid_y"],
"bits": {},
}
return database
def make_tile_baseaddrs(tiles, site_baseaddr, verbose=False):
# Look up a base address by tile name
tile_baseaddrs = dict()
verbose and print('')
verbose and print('%u tiles' % len(tiles))
added = 0
for tile in tiles:
for site_name in tile["sites"].keys():
if site_name not in site_baseaddr:
continue
framebaseaddr = site_baseaddr[site_name]
bt = addr2btype(framebaseaddr)
tile_baseaddr = tile_baseaddrs.setdefault(tile["name"], {})
if bt in tile_baseaddr:
# actually lets just fail these, better to remove at tcl level to speed up processing
assert 0, 'duplicate base address'
assert tile_baseaddr[bt] == [framebaseaddr, 0]
else:
tile_baseaddr[bt] = [framebaseaddr, 0]
verbose and print(
"baseaddr: %s.%s @ %s.0x%08x" %
(tile["name"], site_name, bt, framebaseaddr))
added += 1
assert added
return tile_baseaddrs
def make_tiles_by_grid(tiles):
# lookup tile names by (X, Y)
tiles_by_grid = dict()
for tile in tiles:
tiles_by_grid[(tile["grid_x"], tile["grid_y"])] = tile["name"]
return tiles_by_grid
def make_segments(database, tiles_by_grid, tile_baseaddrs, verbose=False):
'''
Create segments data structure
Indicates how tiles are related to bitstream locations
Also modify database to annotate which segment the tiles belong to
segments key examples:
SEG_CLBLM_R_X13Y72
SEG_BRAM3_L_X6Y85
'''
segments = dict()
verbose and print('')
for tile_name, tile_data in database.items():
tile_type = tile_data["type"]
grid_x = tile_data["grid_x"]
grid_y = tile_data["grid_y"]
def add_segment(name, tiles, segtype, baseaddr=None):
assert name not in segments
segment = segments.setdefault(name, {})
segment["tiles"] = tiles
segment["type"] = segtype
if baseaddr:
verbose and print(
'make_segment: %s baseaddr %s' % (
name,
baseaddr,
))
segment["baseaddr"] = baseaddr
for tile_name in tiles:
database[tile_name]["segment"] = name
def process_clb():
if tile_type in ["CLBLL_L", "CLBLM_L"]:
int_tile_name = tiles_by_grid[(grid_x + 1, grid_y)]
else:
int_tile_name = tiles_by_grid[(grid_x - 1, grid_y)]
add_segment(
name="SEG_" + tile_name,
tiles=[tile_name, int_tile_name],
segtype=tile_type.lower(),
baseaddr=tile_baseaddrs.get(tile_name, None))
def process_hclk():
add_segment(
name="SEG_" + tile_name,
tiles=[tile_name],
segtype=tile_type.lower())
def process_bram_dsp():
for k in range(5):
if tile_type in ["BRAM_L", "DSP_L"]:
interface_tile_name = tiles_by_grid[(
grid_x + 1, grid_y - k)]
int_tile_name = tiles_by_grid[(grid_x + 2, grid_y - k)]
elif tile_type in ["BRAM_R", "DSP_R"]:
interface_tile_name = tiles_by_grid[(
grid_x - 1, grid_y - k)]
int_tile_name = tiles_by_grid[(grid_x - 2, grid_y - k)]
else:
assert 0
'''
BRAM/DSP itself is at the base y address
There is one huge switchbox on the right for the 5 tiles
These fan into 5 BRAM_INT_INTERFACE tiles each which feed into their own CENTER_INTER (just like a CLB has)
'''
if k == 0:
tiles = [tile_name, interface_tile_name, int_tile_name]
baseaddr = tile_baseaddrs.get(tile_name, None)
else:
tiles = [interface_tile_name, int_tile_name]
baseaddr = None
add_segment(
# BRAM_L_X6Y70 => SEG_BRAM4_L_X6Y70
name="SEG_" + tile_name.replace("_", "%d_" % k, 1),
tiles=tiles,
# BRAM_L => bram4_l
segtype=tile_type.lower().replace("_", "%d_" % k, 1),
baseaddr=baseaddr)
def process_default():
#verbose and nolr(tile_type) not in ('VBRK', 'INT', 'NULL') and print('make_segment: drop %s' % (tile_type,))
pass
{
"CLBLL": process_clb,
"CLBLM": process_clb,
"HCLK": process_hclk,
"BRAM": process_bram_dsp,
"DSP": process_bram_dsp,
}.get(nolr(tile_type), process_default)()
return segments
def get_inttile(database, segment):
'''Return interconnect tile for given segment'''
inttiles = [
tile for tile in segment["tiles"]
if database[tile]["type"] in ["INT_L", "INT_R"]
]
assert len(inttiles) == 1
return inttiles[0]
def get_bramtile(database, segment):
inttiles = [
tile for tile in segment["tiles"]
if database[tile]["type"] in ["BRAM_L", "BRAM_R"]
]
assert len(inttiles) == 1
return inttiles[0]
def seg_base_addr_lr_INT(database, segments, tiles_by_grid, verbose=False):
'''Populate segment base addresses: L/R along INT column'''
'''
Create BRAM base addresses based on nearby CLBs
ie if we have a BRAM_L, compute as nearby CLB_R base address + offset
'''
verbose and print('')
for segment_name in sorted(segments.keys()):
segment = segments[segment_name]
baseaddrs = segment.get("baseaddr", None)
if not baseaddrs:
continue
for block_type, (framebase, wordbase) in sorted(baseaddrs.items()):
verbose and print(
'lr_INT: %s: %s.0x%08X:%u' %
(segment_name, block_type, framebase, wordbase))
if block_type != 'CLB_IO_CLK':
verbose and print(' Skip non CLB')
continue
inttile = get_inttile(database, segment)
grid_x = database[inttile]["grid_x"]
grid_y = database[inttile]["grid_y"]
if database[inttile]["type"] == "INT_L":
grid_x += 1
framebase = framebase + 0x80
elif database[inttile]["type"] == "INT_R":
grid_x -= 1
framebase = framebase - 0x80
else:
assert 0
# ROI at edge?
if (grid_x, grid_y) not in tiles_by_grid:
verbose and print(' Skip edge')
continue
tile = tiles_by_grid[(grid_x, grid_y)]
if database[inttile]["type"] == "INT_L":
assert database[tile]["type"] == "INT_R"
elif database[inttile]["type"] == "INT_R":
assert database[tile]["type"] == "INT_L"
else:
assert 0
assert "segment" in database[tile]
seg = database[tile]["segment"]
seg_baseaddrs = segments[seg].setdefault("baseaddr", {})
# At least one duplicate when we re-compute the entry for the base address
# should give the same address
if block_type in seg_baseaddrs:
assert seg_baseaddrs[block_type] == [
framebase, wordbase
], (seg_baseaddrs[block_type], [framebase, wordbase])
verbose and print(' Existing OK')
else:
seg_baseaddrs[block_type] = [framebase, wordbase]
verbose and print(' Add new')
def seg_base_addr_up_INT(database, segments, tiles_by_grid, verbose=False):
'''Populate segment base addresses: Up along INT/HCLK columns'''
verbose and print('')
# Copy the initial list containing only base addresses
# and soon to have derived addresses
src_segment_names = list()
for segment_name in segments.keys():
if "baseaddr" in segments[segment_name]:
src_segment_names.append(segment_name)
verbose and print('up_INT: %u base addresses' % len(src_segment_names))
for src_segment_name in sorted(src_segment_names):
src_segment = segments[src_segment_name]
for block_type, (framebase,
wordbase) in sorted(src_segment["baseaddr"].items()):
verbose and print(
'up_INT: %s: %s.0x%08X:%u' %
(src_segment_name, block_type, framebase, wordbase))
def process_CLB_IO_CLK(wordbase):
'''
Lookup interconnect tile associated with this segment
Use it to locate in the grid, and find other segments related by tile offset
'''
inttile = get_inttile(database, src_segment)
verbose and print(
' up_INT CLK_IO_CLK: %s => inttile %s' %
(src_segment_name, inttile))
grid_x = database[inttile]["grid_x"]
grid_y = database[inttile]["grid_y"]
for i in range(50):
grid_y -= 1
dst_tile = database[tiles_by_grid[(grid_x, grid_y)]]
if wordbase == 50:
wordbase += 1
else:
wordbase += 2
#verbose and print(' dst_tile', dst_tile)
dst_segment_name = dst_tile["segment"]
#verbose and print('up_INT: %s => %s' % (src_segment_name, dst_segment_name))
segments[dst_segment_name].setdefault(
"baseaddr", {})[block_type] = [framebase, wordbase]
def process_BLOCK_RAM(wordbase):
'''
Lookup BRAM0 tile associated with this segment
Use it to locate in the grid, and find other BRAM0 related by tile offset
From minitest:
build/roi_bramd_bit01.diff (lowest BRAM coordinate)
> bit_00c00000_000_00
build/roi_bramds_bit01.diff
> bit_00c00000_000_00
> bit_00c00000_010_00
> bit_00c00000_020_00
> bit_00c00000_030_00
> bit_00c00000_040_00
> bit_00c00000_051_00
> bit_00c00000_061_00
> bit_00c00000_071_00
> bit_00c00000_081_00
> bit_00c00000_091_00
'''
src_tile_name = get_bramtile(database, src_segment)
verbose and print(
' up_INT BLOCK_RAM: %s => %s' %
(src_segment_name, src_tile_name))
grid_x = database[src_tile_name]["grid_x"]
grid_y = database[src_tile_name]["grid_y"]
for i in range(9):
grid_y -= 5
wordbase += 10
# Skip HCLK
if i == 4:
grid_y -= 1
wordbase += 1
dst_tile = database[tiles_by_grid[(grid_x, grid_y)]]
assert nolr(dst_tile['type']) == 'BRAM', dst_tile
dst_segment_name = dst_tile["segment"]
assert 'BRAM0' in dst_segment_name
segments[dst_segment_name].setdefault(
"baseaddr", {})[block_type] = [framebase, wordbase]
{
'CLB_IO_CLK': process_CLB_IO_CLK,
'BLOCK_RAM': process_BLOCK_RAM,
}[block_type](
wordbase)
def add_tile_bits(tile_db, baseaddr, offset, frames, words, height=None):
'''
Record data structure geometry for the given tile baseaddr
For most tiles there is only one baseaddr, but some like BRAM have multiple
Notes on multiple block types:
https://github.com/SymbiFlow/prjxray/issues/145
'''
bits = tile_db['bits']
block_type = addr2btype(baseaddr)
assert 0 <= offset <= 100, offset
assert 1 <= words <= 101
assert offset + words <= 101, (
tile_db, offset + words, offset, words, block_type)
assert block_type not in bits
block = bits.setdefault(block_type, {})
# FDRI address
block["baseaddr"] = '0x%08X' % baseaddr
# Number of frames this entry is sretched across
# that is the following FDRI addresses are used: range(baseaddr, baseaddr + frames)
block["frames"] = frames
# Index of first word used within each frame
block["offset"] = offset
# Number of words consumed in each frame
block["words"] = words
# related to words...
# deprecated field? Don't worry about for now
if height is not None:
block["height"] = height
def db_add_bits(database, segments):
'''Transfer segment data into tiles'''
for segment_name in segments.keys():
for block_type, (baseaddr,
offset) in segments[segment_name]["baseaddr"].items():
for tile_name in segments[segment_name]["tiles"]:
tile_type = database[tile_name]["type"]
entry = {
# (tile_type, block_type): (frames, words, height)
("CLBLL", "CLB_IO_CLK"): (36, 2, 2),
("CLBLM", "CLB_IO_CLK"): (36, 2, 2),
("HCLK", "CLB_IO_CLK"): (26, 1, 1),
("INT", "CLB_IO_CLK"): (28, 2, 2),
("BRAM", "CLB_IO_CLK"): (28, 2, None),
("BRAM", "BLOCK_RAM"): (128, 5, None),
("DSP", "CLB_IO_CLK"): (28, 2, 10),
("INT_INTERFACE", "CLB_IO_CLK"): (28, 2, None),
("BRAM_INT_INTERFACE", "CLB_IO_CLK"): (28, 2, None),
}.get((nolr(tile_type), block_type), None)
if entry is None:
# Other types are rare, not expected to have these
if block_type == "CLB_IO_CLK":
raise ValueError("Unknown tile type %s" % tile_type)
continue
frames, words, height = entry
if frames:
# if we have a width, we should have a height
assert frames and words
add_tile_bits(
database[tile_name], baseaddr, offset, frames, words,
height)
def db_add_segments(database, segments):
# TODO: Migrate to new tilegrid format via library. This data is added for
# compability with unconverted tools. Update tools then remove this data from
# tilegrid.json.
# looks like only htmlgen is using this?
for tiledata in database.values():
if "segment" in tiledata:
segment = tiledata["segment"]
tiledata["segment_type"] = segments[segment]["type"]
def run(tiles_fn, json_fn, deltas_fns, verbose=False):
# Load input files
tiles = load_tiles(tiles_fn)
site_baseaddr = load_baseaddrs(deltas_fns)
# Index input
database = make_database(tiles)
tile_baseaddrs = make_tile_baseaddrs(tiles, site_baseaddr, verbose=verbose)
tiles_by_grid = make_tiles_by_grid(tiles)
segments = make_segments(
database, tiles_by_grid, tile_baseaddrs, verbose=verbose)
# Reference adjacent CLBs to locate adjacent tiles by known offsets
seg_base_addr_lr_INT(database, segments, tiles_by_grid, verbose=verbose)
seg_base_addr_up_INT(database, segments, tiles_by_grid, verbose=verbose)
db_add_bits(database, segments)
db_add_segments(database, segments)
# Save
json.dump(
database,
open(json_fn, 'w'),
sort_keys=True,
indent=4,
separators=(',', ': '))
def main():
import argparse
import glob
parser = argparse.ArgumentParser(
description='Generate tilegrid.json from bitstream deltas')
parser.add_argument('--verbose', action='store_true', help='')
parser.add_argument('--out', default='/dev/stdout', help='Output JSON')
parser.add_argument(
'--tiles', default='tiles.txt', help='Input tiles.txt tcl output')
parser.add_argument(
'deltas', nargs='*', help='.bit diffs to create base addresses from')
args = parser.parse_args()
deltas = args.deltas
if not args.deltas:
deltas = glob.glob('*.delta')
run(args.tiles, args.out, deltas, verbose=args.verbose)
if __name__ == '__main__':
main()