| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright (C) 2017-2020 The Project X-Ray Authors. |
| # |
| # Use of this source code is governed by a ISC-style |
| # license that can be found in the LICENSE file or at |
| # https://opensource.org/licenses/ISC |
| # |
| # SPDX-License-Identifier: ISC |
| ''' |
| Take raw .bits files and decode them to higher level functionality |
| |
| A segment as currently used is defined as a tile type + a memory region |
| Ex: BRAM_L_X6Y100:CLB_IO_CLK |
| ''' |
| |
| import sys, os, json, re |
| import copy |
| from prjxray import bitstream |
| from prjxray import db as prjxraydb |
| from prjxray.util import OpenSafeFile, parse_tagbit, db_root_arg, part_arg |
| |
| |
| class NoDB(Exception): |
| pass |
| |
| |
| # cache |
| segbitsdb = dict() |
| |
| |
| # int and sites are loaded together so that bit coverage can be checked together |
| # however, as currently written, each segment is essentially printed twice |
| def process_db(db, tile_type, process, verbose): |
| ttdb = db.get_tile_type(tile_type) |
| |
| fns = [ttdb.tile_dbs.segbits, ttdb.tile_dbs.ppips] |
| verbose and print("process_db(%s): %s" % (tile_type, fns)) |
| for fn in fns: |
| if fn: |
| with OpenSafeFile(fn, "r") as f: |
| for line in f: |
| process(line) |
| |
| |
| def get_database(db, tile_type, bit_only=False, verbose=False): |
| tags = list() |
| |
| if tile_type in segbitsdb: |
| return segbitsdb[tile_type] |
| |
| def process(l): |
| # l like: CLBLL_L.SLICEL_X0.AMUX.CY !30_07 !30_11 30_06 30_08 |
| # Parse tags to do math when multiple tiles share an address space |
| parts = l.split() |
| name = parts[0] |
| |
| if parts[1] == 'always' or parts[1] == 'hint' or parts[1] == 'default': |
| if bit_only: |
| return |
| tagbits = [] |
| else: |
| tagbits = [parse_tagbit(x) for x in parts[1:]] |
| |
| tags.append(list([name] + tagbits)) |
| |
| process_db(db, tile_type, process, verbose=verbose) |
| |
| if len(tags) == 0: |
| raise NoDB(tile_type) |
| |
| segbitsdb[tile_type] = tags |
| return tags |
| |
| |
| def mk_segbits(seginfo, bitdata): |
| ''' |
| Given a tile memory region (seginfo), return list of bits in that region |
| |
| seginfo: mk_segments()s object supplying address range |
| bitdata: all bits in the entire bitstream |
| ''' |
| |
| segbits = set() |
| |
| block = seginfo["block"] |
| baseaddr = int(block["baseaddr"], 0) |
| frames = block["frames"] |
| word_offset = block["offset"] |
| words = block["words"] |
| |
| for frame in range(baseaddr, baseaddr + frames): |
| if frame not in bitdata: |
| continue |
| for wordidx in range(word_offset, word_offset + words): |
| if wordidx not in bitdata[frame]: |
| continue |
| for bitidx in bitdata[frame][wordidx]: |
| frame_addr = frame - baseaddr |
| bit_addr = 32 * (wordidx - word_offset) + bitidx |
| #segbits.add( "%02d_%02d" % (frame_addr, word_addr)) |
| segbits.add((frame_addr, bit_addr)) |
| |
| return segbits |
| |
| |
| def gen_tilegrid_masks(tiles): |
| """yield (addr_min, addr_max + 1, word_min, word_max + 1)""" |
| for tilek, tilev in tiles.items(): |
| for block_type, blockj in tilev["bits"].items(): |
| baseaddr = int(blockj["baseaddr"], 0) |
| frames = blockj["frames"] |
| offset = blockj["offset"] |
| words = blockj["words"] |
| yield (baseaddr, baseaddr + frames, offset, offset + words) |
| |
| |
| def print_unknown_bits(tiles, bitdata): |
| ''' |
| Print bits not covered by known tiles |
| |
| tiles: tilegrid json |
| bitdata[addr][word] = set of bit indices (0 to 31) |
| ''' |
| # Start with an open set and remove elements as we find them |
| tocheck = copy.deepcopy(bitdata) |
| |
| for addr_min, addr_max_p1, word_min, word_max_p1 in gen_tilegrid_masks( |
| tiles): |
| for addr in range(addr_min, addr_max_p1): |
| if addr not in tocheck: |
| continue |
| for word in range(word_min, word_max_p1): |
| if word not in tocheck[addr]: |
| continue |
| del tocheck[addr][word] |
| |
| # print uncovered locations |
| print('Non-database bits:') |
| for frame in sorted(tocheck.keys()): |
| for wordidx in sorted(tocheck[frame].keys()): |
| for bitidx in sorted(tocheck[frame][wordidx]): |
| print("bit_%08x_%03d_%02d" % (frame, wordidx, bitidx)) |
| |
| |
| def tagmatch(entry, segbits): |
| '''Does tag appear in segbits?''' |
| |
| # Entry like "CLBLL_L.SLICEL_X0.AMUX.CY !30_07 !30_11 30_06 30_08".split() |
| for bit in entry[1:]: |
| isset, bitaddr = bit |
| |
| # Reject if bit polarity is incorrect |
| if bitaddr not in segbits if isset else bitaddr in segbits: |
| return False |
| return True |
| |
| |
| def tag_matched(entry, segbits): |
| for bit in entry[1:]: |
| isset, bitaddr = bit |
| if isset: |
| segbits.remove(bitaddr) |
| |
| |
| # tile types that failed to decode |
| decode_warnings = set() |
| |
| |
| def seg_decode(db, seginfo, segbits, segments, bit_only=False, verbose=False): |
| ''' |
| Remove matched tags from segbits |
| Returns a list of all matched tags |
| ''' |
| |
| segtags = set() |
| |
| # Valid addresses for refereced tiles |
| ref_block = seginfo["block"] |
| # ref_frame_as = (int(ref_block["baseaddr"], 0), int(ref_block["baseaddr"], 0) + ref_block["frames"] - 1) |
| ref_frame_as = (0, ref_block["frames"] - 1) |
| ref_bit_as = ( |
| 32 * ref_block["offset"], |
| 32 * (ref_block["offset"] + ref_block["words"]) - 1) |
| |
| def process(cmp_seginfo, ref_tile_name): |
| tile_type = cmp_seginfo["tile"]["type"] |
| |
| # already failed? |
| if tile_type in decode_warnings: |
| return |
| |
| try: |
| entries = get_database( |
| db, tile_type, bit_only=bit_only, verbose=verbose) |
| except NoDB: |
| verbose and print("WARNING: failed to load DB for %s" % tile_type) |
| assert tile_type != 'BRAM_L' |
| decode_warnings.add(tile_type) |
| return |
| |
| cmp_block = cmp_seginfo["block"] |
| ref_frame_delta = int(cmp_block["baseaddr"], 0) - int( |
| ref_block["baseaddr"], 0) |
| ref_bit_delta = 32 * (cmp_block["offset"] - ref_block["offset"]) |
| |
| def adjust_entry_addr(entry): |
| '''Return bits that apply to this tile at the correct address''' |
| tagname = entry[0] |
| bits = entry[1:] |
| if len(bits) == 0: |
| return None |
| ret = [tagname] |
| |
| def adjust_entry(): |
| '''Adjust entry in another tile address space to be in our reference tile address space''' |
| for isset, old_bitaddr, in bits: |
| old_frame_addr, old_bit_addr = old_bitaddr |
| new_frame_addr = old_frame_addr + ref_frame_delta |
| if not (ref_frame_as[0] <= new_frame_addr <= |
| ref_frame_as[1]): |
| verbose and print( |
| "out frame range: %d <= %d <= %d" % |
| (ref_frame_as[0], new_frame_addr, ref_frame_as[1])) |
| return False |
| |
| new_bit_addr = old_bit_addr + ref_bit_delta |
| # Verify in range of original tile |
| # This can happen if a smaller tile references a larger tile |
| if not (ref_bit_as[0] <= new_bit_addr <= ref_bit_as[1]): |
| verbose and print( |
| "out bit range: %d <= %d <= %d" % |
| (ref_bit_as[0], new_bit_addr, ref_bit_as[1])) |
| return False |
| ret.append((isset, (new_frame_addr, new_bit_addr))) |
| verbose and print( |
| "ent %02d_%02d => %02d_%02d" % ( |
| old_frame_addr, old_bit_addr, new_frame_addr, |
| new_bit_addr)) |
| return True |
| |
| if not adjust_entry(): |
| return None |
| return ret |
| |
| for entry in entries: |
| if ref_tile_name: |
| entry = adjust_entry_addr(entry) |
| if entry is None: |
| continue |
| verbose and print('adjusted entry', entry) |
| |
| if not tagmatch(entry, segbits): |
| continue |
| tag_matched(entry, segbits) |
| tagname = entry[0] |
| # Prefix matches not from this tile |
| if ref_tile_name: |
| segtags.add('%s:%s' % (ref_tile_name, tagname)) |
| else: |
| segtags.add(tagname) |
| |
| # Reference tile |
| process(seginfo, None) |
| # Tiles that share our address space |
| for (ref_tile_name, cmp_block_name) in seginfo['segtiles']: |
| process( |
| segments[mksegment(ref_tile_name, cmp_block_name)], ref_tile_name) |
| |
| return segtags |
| |
| |
| def print_seg( |
| segname, seginfo, nbits, segbits, segtags, decode_emit, verbose=False): |
| '''Print segment like used by segmaker/segmatch''' |
| |
| print("seg %s" % (segname, )) |
| if verbose: |
| print("Bits: %s" % nbits) |
| print( |
| "Address: %s, +%s" % |
| (seginfo["block"]["baseaddr"], seginfo["block"]["frames"])) |
| print( |
| "Words: %s, +%s" % |
| (seginfo["block"]["offset"], seginfo["block"]["words"])) |
| |
| # Bits that weren't decoded |
| for bit in sorted(segbits): |
| print("bit %02d_%02d" % bit) |
| |
| if decode_emit: |
| for tag in sorted(segtags): |
| print("tag %s" % tag) |
| |
| |
| def handle_segment( |
| db, |
| segname, |
| bitdata, |
| decode_emit, |
| decode_omit, |
| omit_empty_segs, |
| segments, |
| bit_only=False, |
| verbose=False): |
| |
| seginfo = segments[segname] |
| |
| segbits = mk_segbits(seginfo, bitdata) |
| nbits = len(segbits) |
| |
| if decode_emit or decode_omit: |
| segtags = seg_decode( |
| db, seginfo, segbits, segments, bit_only=bit_only, verbose=verbose) |
| else: |
| segtags = set() |
| |
| # Found something to print? |
| keep = not omit_empty_segs or len(segbits) > 0 or ( |
| len(segtags) > 0 and not decode_omit) |
| if not keep: |
| return |
| |
| print() |
| print_seg( |
| segname, |
| seginfo, |
| nbits, |
| segbits, |
| segtags, |
| decode_emit, |
| verbose=verbose) |
| |
| |
| def overlap(a, b): |
| return a[0] <= b[0] <= a[1] or b[0] <= a[0] <= b[1] |
| |
| |
| def mk_segtiles(tiles): |
| ''' |
| Return a dictionary of tile_name:tiles |
| Where tiles is a list of tiles that are in our address space |
| |
| Assumption: tiles in the same minor address region have the same base address and number frames |
| |
| As DB is written, not all have the same number of frames |
| Ex: CLBLM_R_X7Y108 36 frames, INT_R_X7Y108 28 frames |
| We could check for this, but don't think its worth the effort |
| Maybe this should be corrected in the DB? |
| ''' |
| |
| segtiles = {} |
| |
| # Group by base address |
| baseaddrs = {} |
| for tile_name, tile in tiles.items(): |
| for block_name, block in tile['bits'].items(): |
| baseaddrs.setdefault(block["baseaddr"], []).append( |
| (block["offset"], tile_name, block, block_name)) |
| |
| for baseaddr, values in baseaddrs.items(): |
| ''' |
| There are only 256 addresses per minor address |
| Just do a set brute force search for now? |
| Maybe too slow with the number of tiles |
| |
| Around 50 IP blocks max per minor address |
| ''' |
| |
| # Sort by block offset |
| values = sorted(values) |
| |
| for refi, (_ref_block_offset, ref_tile_name, ref_block, |
| ref_block_name) in enumerate(values): |
| seglets = segtiles.setdefault(ref_tile_name, []) |
| ref_as = ( |
| ref_block["offset"], |
| ref_block["offset"] + ref_block["words"] - 1) |
| |
| for cmpi in range(refi + 1, len(values)): |
| (_cmp_block_offset, cmp_tile_name, cmp_block, |
| cmp_block_name) = values[cmpi] |
| cmp_as = ( |
| cmp_block["offset"], |
| cmp_block["offset"] + cmp_block["words"] - 1) |
| |
| if overlap(ref_as, cmp_as): |
| seglets.append((cmp_tile_name, cmp_block_name)) |
| # sorting => first non-intersection means no future will intersect |
| else: |
| break |
| |
| return segtiles |
| |
| |
| def mk_segments(tiles): |
| segments = {} |
| segtiles = mk_segtiles(tiles) |
| |
| for tile_name, tile in tiles.items(): |
| for block_name, block in tile['bits'].items(): |
| segname = mksegment(tile_name, block_name) |
| segments[segname] = { |
| 'tile': tile, |
| 'tile_name': tile_name, |
| 'block': block, |
| 'block_name': block_name, |
| 'segtiles': segtiles[tile_name], |
| } |
| return segments |
| |
| |
| def mksegment(tile_name, block_name): |
| '''Create a segment name''' |
| return '%s:%s' % (tile_name, block_name) |
| |
| |
| def tile_segnames(tiles): |
| '''Create a list of all (tile_name, block_name) from input tiles''' |
| ret = [] |
| for tile_name, tile in tiles.items(): |
| if 'bits' not in tile: |
| continue |
| |
| for block_name in tile['bits'].keys(): |
| ret.append(mksegment(tile_name, block_name)) |
| return ret |
| |
| |
| def load_tiles(db_root, part): |
| # TODO: Migrate to new tilegrid format via library. |
| with OpenSafeFile("%s/%s/tilegrid.json" % (db_root, part), "r") as f: |
| tiles = json.load(f) |
| return tiles |
| |
| |
| def run( |
| db_root, |
| part, |
| bits_file, |
| segnames, |
| omit_empty_segs=False, |
| flag_unknown_bits=False, |
| flag_decode_emit=False, |
| flag_decode_omit=False, |
| bit_only=False, |
| verbose=False): |
| db = prjxraydb.Database(db_root, part) |
| tiles = load_tiles(db_root, part) |
| segments = mk_segments(tiles) |
| with OpenSafeFile(bits_file) as f: |
| bitdata = bitstream.load_bitdata2(f) |
| |
| if flag_unknown_bits: |
| print_unknown_bits(tiles, bitdata) |
| print("") |
| |
| # Default: print all |
| if segnames: |
| for i, segname in enumerate(segnames): |
| # Default to common tile config area if tile given without explicit block |
| if ':' not in segname: |
| segnames[i] = mksegment(segname, 'CLB_IO_CLK') |
| else: |
| segnames = sorted(tile_segnames(tiles)) |
| print('Segments: %u' % len(segnames)) |
| |
| # XXX: previously this was sorted by address, not name |
| # revisit? |
| for segname in segnames: |
| handle_segment( |
| db, |
| segname, |
| bitdata, |
| flag_decode_emit, |
| flag_decode_omit, |
| omit_empty_segs, |
| segments, |
| bit_only=bit_only, |
| verbose=verbose) |
| |
| |
| def main(): |
| import argparse |
| |
| parser = argparse.ArgumentParser( |
| description="Decode bits within a tile's address space") |
| |
| db_root_arg(parser) |
| part_arg(parser) |
| parser.add_argument('--verbose', action='store_true', help='') |
| parser.add_argument( |
| '-z', |
| action='store_true', |
| help="do not print a 'seg' header for empty segments") |
| parser.add_argument( |
| '-b', action='store_true', help='print bits outside of known segments') |
| parser.add_argument( |
| '-d', |
| action='store_true', |
| help='decode known segment bits and write them as tags') |
| parser.add_argument( |
| '-D', |
| action='store_true', |
| help='decode known segment bits and omit them in the output') |
| parser.add_argument( |
| '--bit-only', |
| action='store_true', |
| help='only decode real bitstream directives') |
| parser.add_argument('bits_file', help='') |
| parser.add_argument( |
| 'segnames', nargs='*', help='List of tile or tile:block to print') |
| args = parser.parse_args() |
| |
| run( |
| args.db_root, |
| args.part, |
| args.bits_file, |
| args.segnames, |
| args.z, |
| args.b, |
| args.d, |
| args.D, |
| bit_only=args.bit_only, |
| verbose=args.verbose) |
| |
| |
| if __name__ == '__main__': |
| main() |