diff --git a/fuzzers/007-timing/bel/Makefile b/fuzzers/007-timing/bel/Makefile index 7cc32d49..eda5433d 100644 --- a/fuzzers/007-timing/bel/Makefile +++ b/fuzzers/007-timing/bel/Makefile @@ -6,8 +6,16 @@ clean: build/bel_timings.txt: bash runme.sh -build/bel_timings.json: build/bel_timings.txt - python3 tim2json.py --timings=build/bel_timings.txt --json=build/bel_timings.json --properties=build/bel_properties.txt --propertiesmap=properties_map.json --belpins=build/bel_pins.txt --sitepins=build/tile_pins.txt --debug true +build/fixup_timings: build/bel_timings.txt + python3 fixup_timings_txt.py --txtin build/bel_timings.txt --txtout build/bel_timings.txt --site RAMBFIFO36E1 --slice BRAM_L --type timings + python3 fixup_timings_txt.py --txtin build/bel_timings.txt --txtout build/bel_timings.txt --site RAMBFIFO36E1 --slice BRAM_R --type timings + python3 fixup_timings_txt.py --txtin build/bel_pins.txt --txtout build/bel_pins.txt --site RAMBFIFO36E1 --slice BRAM_L --type pins + python3 fixup_timings_txt.py --txtin build/bel_pins.txt --txtout build/bel_pins.txt --site RAMBFIFO36E1 --slice BRAM_R --type pins + python3 fixup_timings_txt.py --txtin build/bel_properties.txt --txtout build/bel_properties.txt --site RAMBFIFO36E1 --slice BRAM_L --type properties + python3 fixup_timings_txt.py --txtin build/bel_properties.txt --txtout build/bel_properties.txt --site RAMBFIFO36E1 --slice BRAM_R --type properties + +build/bel_timings.json: build/fixup_timings + python3 tim2json.py --timings=build/bel_timings.txt --json=build/bel_timings.json --properties=build/bel_properties.txt --propertiesmap=properties_map.json --pinaliasmap=pin_alias_map.json --belpins=build/bel_pins.txt --sitepins=build/tile_pins.txt --debug true build/sdf: build/bel_timings.json python3 ${XRAY_UTILS_DIR}/makesdf.py --json=${PWD}/build/bel_timings.json --sdf=${PWD}/build diff --git a/fuzzers/007-timing/bel/README.md b/fuzzers/007-timing/bel/README.md new file mode 100644 index 00000000..9d90160f --- /dev/null +++ b/fuzzers/007-timing/bel/README.md @@ -0,0 +1,45 @@ +# BEL timings fuzzer + +This fuzzer is suppose to extract all the BEL timings for Xilinx 7-series FPGAs. +The extracted timings are saved as SDF files. +Single SDF file is produced for every FPGA tile type. + +## Fuzzer flow + +Fuzzer flow is the following: + +Vivado/`runme.tcl` -> `fixup_timings_txt.py` -> `tim2json.py` -> `makesdf.py` -> `sdfmerge.py` + +The fuzzer uses Vivado tcl script to read all the FPGA timings data. +This data is dumped to a text file. +Beside timings data the tcl scipt dumps the following information: + +* Sites pins +* BELs pins +* BELs attributes + +The tcl script iterates over every BEL for all the sites in each tile type. +The timings, pins and attributes are dumped in the same hierarchy. +This hierarchy is used by the `tim2json` script to process the txt dump. +Main task of the `tim2json` script is to figure out what the dumped timings mean and what paths do they describe. +This is done by parsing the string timings, searching the BEL/Sites pins and BEL attributes. +Since for some pins there was no naming convention, the names are provided directly via `pin_alias_map.json`. + +Some BELs (e.g. BRAMs) reports the timings with incorrect site name. +To work around this issue the `fixup_timings_txt.py` script is used to flatten the timings dump file. +As a result all the timings for a selected tile are squashed into one site. + +### Text files format + +The files dumped by the Vivado scrips have the following format: + +Each line starts with a tile name, proceeded by a number of sites in the tile. +After the tiles count, each tile info is placed. +The tile info starts with a tile name followed by number of BELs in the tile. +The bels number is followed by BEL entries. +Each BEL entry starts with BEL name followed by a number of entries. +The entries differ between the file types: + +* timings file: each timing entry starts with entry name followed by 5 timings values +* properties file: each properties entry starts with property name followed by a number of possible property values, followed by a list of the possible property values +* pins file: each pin entry starts with pin name, followed by pin direction (IN/OUT), followed by 0 or 1 flag saying if the pin is a clock pin diff --git a/fuzzers/007-timing/bel/fixup_timings_txt.py b/fuzzers/007-timing/bel/fixup_timings_txt.py new file mode 100644 index 00000000..71c1a2fa --- /dev/null +++ b/fuzzers/007-timing/bel/fixup_timings_txt.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import argparse + + +def fix_line(line, site, filetype): + """ + Squashes the entries for multiple sites into one. + This is required when entries are defined for a different + site than they are reported. + Such situation happend e.g. + for BRAM_[LR]. All the entries are defined for RAMBFIFO36E1, + while they are reported for RAMB18E1 or FIFO18E1 + + Parameters + ---------- + line: str + raw dump file line + site: str + site to which all the entries will be squashed + filetype: str + entries type. One of [timings, pins, properties] + + Returns + ------- + str + line with squashed entries + """ + + assert filetype in [ + "timings", "pins", "properties" + ], "Unsupported filetype" + + line = line.split() + newline = list() + sites_count = int(line[1]) + newline.append(line[0]) + # we'll emit only one site + newline.append("1") + newline.append(site) + newline.append("1") + newline.append(site) + entries = list() + all_entries = 0 + loc = 2 + for site in range(0, sites_count): + bels_count = int(line[loc + 1]) + loc += 2 + for bel in range(0, bels_count): + entries_count = int(line[loc + 1]) + loc += 2 + all_entries += entries_count + for entry in range(0, entries_count): + if filetype == 'timings': + for delay_word in range(0, 6): + entries.append(line[loc]) + loc += 1 + elif filetype == 'pins': + for pin_word in range(0, 3): + entries.append(line[loc]) + loc += 1 + elif filetype == 'properties': + entries.append(line[loc]) + loc += 1 + values_count = int(line[loc]) + entries.append(line[loc]) + loc += 1 + for value in range(0, values_count): + entries.append(line[loc]) + loc += 1 + newline.append(str(all_entries)) + newline.extend(entries) + return " ".join(newline) + "\n" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--txtin', type=str, help='Input text file') + parser.add_argument('--txtout', type=str, help='Output text file') + parser.add_argument( + '--site', + type=str, + help='Site to which the entries should be squashed') + parser.add_argument( + '--slice', type=str, help='Slice for which the entries shall be fixed') + parser.add_argument( + '--type', type=str, choices=['timings', 'pins', 'properties']) + + args = parser.parse_args() + lines = list() + + with open(args.txtin, 'r') as fp: + for line in fp: + if line.startswith(args.slice): + line = fix_line(line, args.site, args.type) + lines.append(line) + + with open(args.txtout, 'w') as fp: + for line in lines: + fp.write(line) + + +if __name__ == "__main__": + main() diff --git a/fuzzers/007-timing/bel/pin_alias_map.json b/fuzzers/007-timing/bel/pin_alias_map.json new file mode 100644 index 00000000..d984d6c4 --- /dev/null +++ b/fuzzers/007-timing/bel/pin_alias_map.json @@ -0,0 +1,60 @@ +{ + "ff_init": { + "Q": { + "names" : ["QL", "QH"], + "is_property_related" : true + } + }, + "reg_init": { + "Q": { + "names" : ["QL", "QH"], + "is_property_related" : true + } + }, + "rambfifo36e1" : { + "RSTRAMARSTRAMLRST" : { + "names" : ["rstramarstl"], + "is_property_related" : false + }, + "ADDRARDADDRU0" : { + "names" : ["addrau"], + "is_property_related" : false + }, + "ADDRARDADDRL15" : { + "names" : ["addra15l"], + "is_property_related" : false + }, + "RSTRAMARSTRAMU" : { + "names" : ["rstramau"], + "is_property_related" : false + }, + "ADDRBWRADDRU0" : { + "names" : ["addrbu"], + "is_property_related" : false + }, + "ADDRBWRADDRL15" : { + "names" : ["addrb15l"], + "is_property_related" : false + }, + "WEBWEU0" : { + "names" : ["webu"], + "is_property_related" : false + }, + "DOADO0" : { + "names" : ["doadol", "doadou"], + "is_property_related" : false + }, + "DOBDO0" : { + "names" : ["dobdol", "dobdou"], + "is_property_related" : false + }, + "DOPADOP0" : { + "names" : ["dopadopl", "dopadopu"], + "is_property_related" : false + }, + "DOPBDOP0" : { + "names" : ["dopbdopl", "dopbdopu"], + "is_property_related" : false + } + } +} diff --git a/fuzzers/007-timing/bel/tim2json.py b/fuzzers/007-timing/bel/tim2json.py index 002d8805..7a75e0bc 100644 --- a/fuzzers/007-timing/bel/tim2json.py +++ b/fuzzers/007-timing/bel/tim2json.py @@ -45,11 +45,120 @@ def clean_bname(bname): return bname -def pin_in_model(pin, model, direction=None): +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 pin_in_model(pin, pin_aliases, model, direction=None): + """ + 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. + + >>> pin_in_model("d", None, "ff_init_din_q", "in") + (True, 'din') + + >>> pin_in_model("q", None, "ff_init_clk_q", None) + (True, 'q') + + >>> pin_in_model("q", {"Q": {"names" : ["QL", "QH"], "is_property_related" : True}}, "ff_init_clk_ql", None) + (True, 'q') + + >>> pin_in_model("logic_out", None, "my_cell_i_logic_out", None) + (True, 'logic_out') + + >>> pin_in_model("logic_out", {"LOGIC_OUT": {"names" : ["LOGIC_O", "O"], "is_property_related" : False}}, "my_cell_i_logic_o", None) + (True, 'logic_o') + + >>> pin_in_model("logic_out", {"LOGIC_OUT": {"names" : ["LOGIC_O", "O"], "is_property_related" : False}}, "my_cell_i_o", None) + (True, 'o') + """ # 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 @@ -63,26 +172,62 @@ def pin_in_model(pin, model, direction=None): return True, pin elif extended_pin_name in model.split('_'): return True, extended_pin_name + elif aliased_pin: + return True, aliased_pin_name else: return False, None else: # pin name is multi word, search for a string - return (pin in model), pin + if pin in model: + return True, pin + elif aliased_pin: + return True, aliased_pin_name + else: + return False, None 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('_') - tmp.remove(pin) - return "_".join(tmp) + 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 read_raw_timings(fin, properties, pins, site_pins): +def read_raw_timings(fin, properties, pins, site_pins, pin_alias_map): timings = dict() with open(fin, "r") as f: @@ -150,15 +295,21 @@ def read_raw_timings(fin, properties, pins, site_pins): if speed_model.startswith(delay_btype): speed_model_clean = speed_model[len(delay_btype):] + # Get pin alias map + pin_aliases = pin_alias_map.get(delay_btype, None) + # 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') + pin.lower(), pin_aliases, speed_model_clean, + 'in') + if pim: if pins[slice][site_name][delay_btype_orig][ orig_pin]['is_clock']: bel_clock = pin + bel_clock_orig_pin = orig_pin elif pins[slice][site_name][delay_btype_orig][ orig_pin]['direction'] == 'IN': bel_input = pin @@ -168,11 +319,13 @@ def read_raw_timings(fin, properties, pins, site_pins): 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) + pin.lower(), pin_aliases, + speed_model_clean) if pim: if site_pins[slice][site_name.lower( )][orig_pin]['is_clock']: @@ -181,13 +334,13 @@ def read_raw_timings(fin, properties, pins, site_pins): 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_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') + pin.lower(), pin_aliases, + speed_model_clean, 'in') if pim: if site_pins[slice][site_name.lower( )][orig_pin]['direction'] == 'IN': @@ -199,7 +352,8 @@ def read_raw_timings(fin, properties, pins, site_pins): for pin in site_pins[slice][site_name.lower()]: orig_pin = pin pim, pin = pin_in_model( - pin.lower(), speed_model_clean) + pin.lower(), pin_aliases, + speed_model_clean) if pim: if site_pins[slice][site_name.lower( )][orig_pin]['direction'] == 'OUT': @@ -207,6 +361,15 @@ def read_raw_timings(fin, properties, pins, site_pins): 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 + # check if the input is not a BEL property if bel_input is None: # if there is anything not yet decoded @@ -218,38 +381,38 @@ def read_raw_timings(fin, properties, pins, site_pins): speed_model_clean = remove_pin_from_model( prop.lower(), speed_model_clean) break - # if we couldn't find input, check if the clock is the - # only input - if 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 import re - if bel_input is None: + if (bel_input is None) or (bel_output is None): for pin in pins[slice][site_name][ delay_btype_orig]: number = re.search(r'\d+$', 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) + orig_pin.lower(), pin_aliases, + 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(), + orig_pin.lower(), pin_aliases, speed_model_clean) if pim: - bel_input = pin + 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) @@ -263,7 +426,8 @@ def read_raw_timings(fin, properties, pins, site_pins): speed_model = delay_btype + speed_model_clean if sequential is not None: - if bel_output is None and bel_clock is None: + if bel_output is None and bel_clock is None or \ + bel_output is None and bel_clock == bel_input: delay_loc += 6 continue else: @@ -439,11 +603,16 @@ def main(): '--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: @@ -460,7 +629,8 @@ def main(): 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) + 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)