blob: 620ea2afaaae8ec11d8cf7f2c916b30d6182246c [file] [log] [blame]
#!/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()