#!/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 import re import argparse import json import functools NUMBER_RE = re.compile(r'\d+$') def check_sequential(speed_model): # combinational path models do not contain # the following keywords timing_keywords = { 'setup': 'setup', 'remov': 'removal', 'hold': 'hold', 'recov': 'recovery', 'removal': 'removal', 'recovery': 'recovery' } tmp = speed_model.split('_') for keyword in sorted(timing_keywords): if keyword in tmp: # return found keyword and it's map in SDF return [keyword, timing_keywords[keyword]] return None # FF's can be configured to work as FF or latch def check_ff_latch(speed_model): tmp = speed_model.split('_') if 'ff' in tmp: return 'ff' elif 'lat' in tmp: return 'lat' else: return None # some bels have duplicate names e.g bufmrce_bufmrce # this function cleans them def clean_bname(bname): tmp = bname.split('_') if len(tmp) > 1 and tmp[0] == tmp[1]: return '_'.join(tmp[1:]) return bname def find_aliased_pin(pin, model, pin_aliases): """ Searches for aliased pins in the timing model. The check is done using data from pin_aliases dictionary. The dictionary has an entry for each aliased pin. Each entry has two fields: * names : a list of all the possible aliases * is_property_related: a flag saying if the alias is in fact pin name combined with BEL property (e.g. Q[LH] pins in FF - in this case the pin name is Q [original name], but is named Q[LH] in the timing model. The suffix determines polarity of the FF's set/reset input). If is_property_related is set the function returns the original pin name, aliased name is returned otherwise. Parameters ---------- pin: str Pin name to look for model: str Timing model pin_aliases: dict A dict of list of aliases for given bel/site Returns ------- bool, str The first bool value is set to true if pin is found in the timing model, false otherwise. The second returned value is found pin name. If pin is not found None is returned >>> find_aliased_pin("a", "a_b_some_test_string", None) (False, None) >>> find_aliased_pin("d", "din_dout_setup", {"D": {"names" : ["din"], "is_property_related" : False}}) (True, 'din') >>> find_aliased_pin("d", "din_dout_setup", {"D": {"names" : ["din"], "is_property_related" : True}}) (True, 'd') >>> find_aliased_pin("d", "din_dout_setup", {"D": {"names" : ["notdin"], "is_property_related" : True}}) (False, None) """ if (pin_aliases is not None) and (pin.upper() in pin_aliases): for alias in pin_aliases[pin.upper()]['names']: single_word_alias = (len(alias.split('_')) == 1) pin_alias = alias.lower() if single_word_alias: model_to_check = model.split('_') else: model_to_check = model if pin_alias in model_to_check: if pin_aliases[pin.upper()]['is_property_related']: return True, pin.lower() else: return True, pin_alias return False, None def instance_in_model(instance, model): if len(instance.split('_')) == 1: # instance name is one word, search it in the model return instance in model.split('_') else: # instance name is multi word, search for a string return instance in model def create_pin_in_model(pin_aliases): """ Checks if a given pin belongs to the model. Parameters ---------- pin: str Pin name to look for pin_aliases: dict A dict of list of aliases for given bel/site model: str Timing model name direction: str Optional pin direction suffix [IN|OUT] Returns ------- bool, str The first returned value is set to true if pin is found, false otherwise. The second returned value contains found pin name. If the pin is not found, None is returned. >>> create_pin_in_model(None)("d", "ff_init_din_q", "in") (True, 'din') >>> create_pin_in_model(None)("q", "ff_init_clk_q", None) (True, 'q') >>> create_pin_in_model({"Q": {"names" : ["QL", "QH"], "is_property_related" : True}})("q", "ff_init_clk_ql", None) (True, 'q') >>> create_pin_in_model(None)("logic_out", "my_cell_i_logic_out", None) (True, 'logic_out') >>> create_pin_in_model({"LOGIC_OUT": {"names" : ["LOGIC_O", "O"], "is_property_related" : False}})("logic_out", "my_cell_i_logic_o", None) (True, 'logic_o') >>> create_pin_in_model({"LOGIC_OUT": {"names" : ["LOGIC_O", "O"], "is_property_related" : False}})("logic_out", "my_cell_i_o", None) (True, 'o') """ @functools.lru_cache(maxsize=10000) def pin_in_model(pin, model, direction=None): # strip site location model = model.split(':')[0] extended_pin_name = pin aliased_pin, aliased_pin_name = find_aliased_pin( pin.upper(), model, pin_aliases) # some timings reports pins with their directions # this happens for e.g. CLB reg_init D pin, which # timing is reported as DIN if direction is not None: extended_pin_name = pin + direction if instance_in_model(pin, model): return True, pin elif instance_in_model(extended_pin_name, model): return True, extended_pin_name elif aliased_pin: return True, aliased_pin_name else: return False, None return pin_in_model def remove_pin_from_model(pin, model): """ Removes the pin from model name if present. Arguments --------- pin: str Pin name mode: str Timing model name Returns ------- str Updated timing model name >>> remove_pin_from_model("q", "ff_init_d_q") 'ff_init_d' >>> remove_pin_from_model("q", "ff_init_d_ql") 'ff_init_d_ql' >>> remove_pin_from_model("logic_out", "ff_init_d_logic_out") 'ff_init_d' >>> remove_pin_from_model("logic_out", "ff_init_d_second_out") 'ff_init_d_second_out' """ if len(pin.split('_')) == 1: # pin name is one word, search it in the model tmp = model.split('_') if pin in tmp: tmp.remove(pin) return "_".join(tmp) else: return model else: # pin name is multi word, search for a string return "_".join(list(filter(None, model.replace(pin, '').split('_')))) def merged_dict(itr): """ Create a merged dict of dict (of dict) based on input. Input is an iteratable of (keys, value). Return value is root dictionary Keys are successive dictionaries indicies. For example: (('a', 'b', 'c'), 1) would set: output['a']['b']['c'] = 1 This function returns an error if two values conflict. >>> merged_dict(((('a', 'b', 'c'), 1), (('a', 'b', 'd'), 2))) {'a': {'b': {'c': 1, 'd': 2}}} """ output = {} for keys, value in itr: target = output for key in keys[:-1]: if key not in target: target[key] = {} target = target[key] if keys[-1] in target: assert target[keys[-1]] == value, (keys, value, target[keys[-1]]) else: target[keys[-1]] = value return output def extract_properties(tile, site, bel, properties, model): if tile not in properties: return None if site not in properties[tile]: return None if bel not in properties[tile][site]: return None model_properties = dict() for prop in properties[tile][site][bel]: if prop in model_properties: continue if instance_in_model(prop.lower(), model): # if there is property there must be value # value always follow the property for value in properties[tile][site][bel][prop]: value = value.replace(',', '') prop_val_str = "_".join([prop, value]) if instance_in_model(prop_val_str.lower(), model): model_properties[prop] = value break return model_properties def parse_raw_timing(fin): with open(fin, "r") as f: for line in f: raw_data = line.split() slice = raw_data[0] sites_count = int(raw_data[1]) loc = 2 for site in range(0, sites_count): site_name = raw_data[loc] bels_count = int(raw_data[loc + 1]) # read all BELs data within loc += 2 for bel in range(0, bels_count): bel = raw_data[loc] delay_count = int(raw_data[loc + 1]) # get all the delays loc += 2 for delay in range(0, delay_count): speed_model = raw_data[loc] # each timing entry reports 5 delays timing = [ raw_data[d + 1 + loc].split(':') for d in range(0, 5) ] yield slice, site_name, bel, speed_model, timing # 5 delay values + name loc += 6 def read_raw_timings(fin, properties, pins, site_pins, pin_alias_map): def inner(): raw = list(parse_raw_timing(fin)) pin_in_models = {} for slice, site_name, bel, speed_model, timing in raw: btype = bel.lower() delay_btype = clean_bname(btype) delay_btype_orig = delay_btype # all the bel names seem to start with "bel_d_" # let's get rid of it if speed_model.startswith('bel_d_'): speed_model = speed_model[6:] # keep original speed model string to use as unique dict entry speed_model_orig = speed_model # if more than one BEL type exists in the slice # location is added at the end of the name tmp = speed_model.split(':') speed_model = tmp[0] bel_location = site_name if len(tmp) > 2: bel_location += "/" + "/".join(tmp[2:]) bel_location = bel_location.upper() sequential = check_sequential(speed_model) if sequential is not None: tmp = speed_model.split('_') tmp.remove(sequential[0]) speed_model = '_'.join(tmp) bel_input = None bel_output = None bel_clock = None # strip btype from speed model so we can search for pins speed_model_clean = speed_model if speed_model.startswith(delay_btype): speed_model_clean = speed_model[len(delay_btype):] # remove properties from the model speed_model_properties = extract_properties( slice, site_name, delay_btype_orig, properties, speed_model_clean) if speed_model_properties is not None: for prop in speed_model_properties: # properties values in the model always follow properties name prop_string = "_".join( [prop, speed_model_properties[prop]]) speed_model_clean = remove_pin_from_model( prop_string.lower(), speed_model_clean) # Get pin alias map if delay_btype not in pin_in_models: pin_aliases = pin_alias_map.get(delay_btype, None) pin_in_models[delay_btype] = create_pin_in_model(pin_aliases) pin_in_model = pin_in_models[delay_btype] # locate pins for pin in pins[slice][site_name][delay_btype_orig]: orig_pin = pin pim, pin = pin_in_model(pin.lower(), speed_model_clean, 'in') if pim: if pins[slice][site_name][delay_btype_orig][orig_pin][ 'is_clock'] and not pins[slice][site_name][ delay_btype_orig][orig_pin]['is_part_of_bus']: bel_clock = pin bel_clock_orig_pin = orig_pin elif pins[slice][site_name][delay_btype_orig][orig_pin][ 'direction'] == 'IN': bel_input = pin elif pins[slice][site_name][delay_btype_orig][orig_pin][ 'direction'] == 'OUT': bel_output = pin speed_model_clean = remove_pin_from_model( pin.lower(), speed_model_clean) # Some speed models describe delays from/to site pins instead of BEL pins if bel_clock is None: for pin in site_pins[slice][site_name.lower()]: orig_pin = pin pim, pin = pin_in_model(pin.lower(), speed_model_clean) if pim: if site_pins[slice][site_name.lower( )][orig_pin]['is_clock'] and not site_pins[slice][ site_name.lower()][orig_pin]['is_part_of_bus']: bel_clock = pin bel_clock_orig_pin = orig_pin speed_model_clean = remove_pin_from_model( pin.lower(), speed_model_clean) if bel_input is None: # search site inputs for pin in site_pins[slice][site_name.lower()]: orig_pin = pin pim, pin = pin_in_model( pin.lower(), speed_model_clean, 'in') if pim: if site_pins[slice][site_name.lower( )][orig_pin]['direction'] == 'IN': bel_input = pin speed_model_clean = remove_pin_from_model( pin.lower(), speed_model_clean) if bel_output is None: for pin in site_pins[slice][site_name.lower()]: orig_pin = pin pim, pin = pin_in_model(pin.lower(), speed_model_clean) if pim: if site_pins[slice][site_name.lower( )][orig_pin]['direction'] == 'OUT': bel_output = pin speed_model_clean = remove_pin_from_model( pin.lower(), speed_model_clean) # if we couldn't find input, check if the clock is the # only input. This applies only to combinational paths if (sequential is None) and (bel_input is None) and (bel_clock is not None): if bel_clock_orig_pin in site_pins[slice][site_name.lower()] and \ site_pins[slice][site_name.lower( )][bel_clock_orig_pin]['direction'] == 'IN': bel_input = bel_clock # if we still don't have the input check if the input # is wider than 1 bit and timing defined for the whole # port if (bel_input is None) or (bel_output is None): for pin in pins[slice][site_name][delay_btype_orig]: number = NUMBER_RE.search(pin) if number is not None: orig_pin = pin[:-(len(str(number.group())))] orig_pin_full = pin pim, pin = pin_in_model( orig_pin.lower(), speed_model_clean) if not pim: # some inputs pins are named with unsignificant zeros # remove ti and try again orig_pin = orig_pin + str(int(number.group())) pim, pin = pin_in_model( orig_pin.lower(), speed_model_clean) if pim: if pins[slice][site_name][delay_btype_orig][orig_pin_full]['direction'] == 'IN' \ and bel_input is None: bel_input = pin if pins[slice][site_name][delay_btype_orig][orig_pin_full]['direction'] == 'OUT' \ and bel_output is None: bel_output = pin speed_model_clean = remove_pin_from_model( orig_pin.lower(), speed_model_clean) # check if the input is not a BEL property if bel_input is None: # if there is anything not yet decoded if len(speed_model_clean.split("_")) > 1: if len(speed_model_properties.keys()) == 1: bel_input = list(speed_model_properties.keys())[0] # if we still don't have input, give up if bel_input is None: continue # restore speed model name speed_model = delay_btype + speed_model_clean if sequential is not None: if bel_clock is None: continue if bel_output is None and bel_clock is None or \ bel_output is None and bel_clock == bel_input: continue else: if bel_input is None or bel_output is None: continue delay_btype = speed_model # add properties to the delay_btype if speed_model_properties is not None: for prop in sorted(speed_model_properties): prop_string = "_".join( [prop, speed_model_properties[prop]]) delay_btype += "_" + prop_string yield (slice, bel_location, delay_btype, speed_model_orig, 'type'), btype.upper() yield ( slice, bel_location, delay_btype, speed_model_orig, 'input'), bel_input.upper() if bel_output is not None: yield ( slice, bel_location, delay_btype, speed_model_orig, 'output'), bel_output.upper() if bel_clock is not None: yield ( slice, bel_location, delay_btype, speed_model_orig, 'clock'), bel_clock.upper() yield ( slice, bel_location, delay_btype, speed_model_orig, 'location'), bel_location.upper() #XXX: debug yield ( slice, bel_location, delay_btype, speed_model_orig, 'model'), speed_model_orig if sequential is not None: assert bel_clock is not None, ( slice, bel_location, delay_btype, speed_model_orig) yield ( slice, bel_location, delay_btype, speed_model_orig, 'sequential'), sequential[1] for t, v in timing: yield ( slice, bel_location, delay_btype, speed_model_orig, t), v return merged_dict(inner()) def read_bel_properties(properties_file, properties_map): def inner(): with open(properties_file, 'r') as f: for line in f: raw_props = line.split() tile = raw_props[0] sites_count = int(raw_props[1]) prop_loc = 2 if sites_count == 0: yield (tile, ), {} for site in range(0, sites_count): site_name = raw_props[prop_loc] bels_count = int(raw_props[prop_loc + 1]) prop_loc += 2 for bel in range(0, bels_count): bel_name = raw_props[prop_loc] bel_name = clean_bname(bel_name) bel_name = bel_name.lower() bel_properties_count = int(raw_props[prop_loc + 1]) props = 0 prop_loc += 2 for prop in range(0, bel_properties_count): prop_name = raw_props[prop_loc] # the name always starts with "CONFIG." and ends with ".VALUES" # let's get rid of that if prop_name.startswith( 'CONFIG.') and prop_name.endswith( '.VALUES'): prop_name = prop_name[7:-7] prop_values_count = int(raw_props[prop_loc + 1]) if prop_name not in [ 'RAM_MODE', 'WRITE_WIDTH_A', 'WRITE_WIDTH_B', 'READ_WIDTH_A', 'READ_WIDTH_B', ]: if bel_name in properties_map: if prop_name in properties_map[bel_name]: prop_name = properties_map[bel_name][ prop_name] yield (tile, site_name, bel_name, prop_name), \ raw_props[prop_loc + 2:prop_loc + 2 + prop_values_count] props += 1 prop_loc += 2 + prop_values_count if props == 0: yield (tile, site_name, bel_name), {} return merged_dict(inner()) def read_bel_pins(pins_file): def inner(): with open(pins_file, 'r') as f: for line in f: raw_pins = line.split() tile = raw_pins[0] sites_count = int(raw_pins[1]) pin_loc = 2 if sites_count == 0: yield (tile, ), {} for site in range(0, sites_count): site_name = raw_pins[pin_loc] bels_count = int(raw_pins[pin_loc + 1]) pin_loc += 2 for bel in range(0, bels_count): bel_name = raw_pins[pin_loc] bel_name = clean_bname(bel_name) bel_name = bel_name.lower() bel_pins_count = int(raw_pins[pin_loc + 1]) pin_loc += 2 for pin in range(0, bel_pins_count): pin_name = raw_pins[pin_loc] pin_direction = raw_pins[pin_loc + 1] pin_is_clock = raw_pins[pin_loc + 2] pin_is_part_of_bus = raw_pins[pin_loc + 3] yield ( tile, site_name, bel_name, pin_name, 'direction'), pin_direction yield ( tile, site_name, bel_name, pin_name, 'is_clock'), int(pin_is_clock) == 1 yield ( tile, site_name, bel_name, pin_name, 'is_part_of_bus' ), int(pin_is_part_of_bus) == 1 pin_loc += 4 return merged_dict(inner()) def read_site_pins(pins_file): def inner(): with open(pins_file, 'r') as f: for line in f: raw_pins = line.split() tile = raw_pins[0] site_count = int(raw_pins[1]) pin_loc = 2 if site_count == 0: yield (tile, ), {} for site in range(0, site_count): site_name = raw_pins[pin_loc] site_name = site_name.lower() site_pins_count = int(raw_pins[pin_loc + 1]) pin_loc += 2 for pin in range(0, site_pins_count): pin_name = raw_pins[pin_loc] pin_direction = raw_pins[pin_loc + 1] pin_is_part_of_bus = raw_pins[pin_loc + 2] yield ( (tile, site_name, pin_name, 'direction'), pin_direction) yield ( (tile, site_name, pin_name, 'is_clock'), pin_name.lower() == 'clk') yield ( (tile, site_name, pin_name, 'is_part_of_bus'), int(pin_is_part_of_bus)) # site clock pins are always named 'CLK' pin_loc += 3 return merged_dict(inner()) def main(): parser = argparse.ArgumentParser() parser.add_argument('--timings', type=str, help='Raw timing input file') parser.add_argument('--json', type=str, help='json output file') parser.add_argument( '--properties', type=str, help='Bel properties input file') parser.add_argument('--belpins', type=str, help='Bel pins input file') parser.add_argument('--sitepins', type=str, help='Site pins input file') parser.add_argument( '--debug', type=bool, default=False, help='Enable debug json dumps') parser.add_argument( '--propertiesmap', type=str, help='Properties names mappings') parser.add_argument( '--pinaliasmap', type=str, help='Pin name alias mappings') args = parser.parse_args() with open(args.propertiesmap, 'r') as fp: properties_map = json.load(fp) with open(args.pinaliasmap, 'r') as fp: pin_alias_map = json.load(fp) properties = read_bel_properties(args.properties, properties_map) if args.debug: with open('debug_prop.json', 'w') as fp: json.dump(properties, fp, indent=4, sort_keys=True) pins = read_bel_pins(args.belpins) if args.debug: with open('debug_pins.json', 'w') as fp: json.dump(pins, fp, indent=4, sort_keys=True) site_pins = read_site_pins(args.sitepins) if args.debug: with open('debug_site_pins.json', 'w') as fp: json.dump(site_pins, fp, indent=4, sort_keys=True) timings = read_raw_timings( args.timings, properties, pins, site_pins, pin_alias_map) with open(args.json, 'w') as fp: json.dump(timings, fp, indent=4, sort_keys=True) if __name__ == '__main__': main()