| #!/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 |
| """ This takes a JSON file generated with write_timing_info.tcl and generates |
| a spreadsheet with the prjxray timing model and compares it with the |
| interconnect timing output from Vivado. |
| |
| """ |
| import argparse |
| import json |
| from openpyxl import Workbook, utils |
| from prjxray.tile import OutPinTiming, InPinTiming |
| from prjxray.timing import Outpin, Inpin, Wire, Buffer, \ |
| PassTransistor, IntristicDelay, RcElement, PvtCorner |
| from prjxray.math_models import ExcelMathModel |
| from prjxray.db import Database |
| from prjxray.util import OpenSafeFile |
| from prjxray import util |
| |
| FAST = PvtCorner.FAST |
| SLOW = PvtCorner.SLOW |
| |
| |
| class TimingLookup(object): |
| def __init__(self, db, nodes): |
| self.db = db |
| self.grid = db.grid() |
| self.nodes = nodes |
| |
| def try_find_site_pin(self, site_pin_node, node_idx): |
| site_pin_wire = self.nodes[site_pin_node]['wires'][node_idx]['name'] |
| tile, wire_in_tile = site_pin_wire.split('/') |
| |
| gridinfo = self.grid.gridinfo_at_tilename(tile) |
| |
| tile_type = self.db.get_tile_type(gridinfo.tile_type) |
| |
| for site in tile_type.get_sites(): |
| for site_pin in site.site_pins: |
| if site_pin.wire == wire_in_tile: |
| return site_pin |
| |
| return None |
| |
| def find_site_pin(self, site_pin_node, node_idx): |
| site_pin = self.try_find_site_pin(site_pin_node, node_idx) |
| |
| assert site_pin is not None, site_pin_node |
| return site_pin |
| |
| def find_pip(self, pip_name): |
| tile, pip = pip_name.split('/') |
| |
| gridinfo = self.grid.gridinfo_at_tilename(tile) |
| |
| tile_type = self.db.get_tile_type(gridinfo.tile_type) |
| |
| return tile_type.get_pip_by_name(pip) |
| |
| def find_wire(self, wire_name): |
| tile, wire_in_tile = wire_name.split('/') |
| |
| gridinfo = self.grid.gridinfo_at_tilename(tile) |
| |
| tile_type = self.db.get_tile_type(gridinfo.tile_type) |
| |
| return tile_type.wires[wire_in_tile] |
| |
| |
| def delays_to_cells(ws, row, delays, cells): |
| cells['FAST_MAX'] = 'E{}'.format(row) |
| cells['FAST_MIN'] = 'F{}'.format(row) |
| cells['SLOW_MAX'] = 'G{}'.format(row) |
| cells['SLOW_MIN'] = 'H{}'.format(row) |
| |
| if delays is not None: |
| ws[cells['FAST_MAX']] = delays[FAST].max |
| ws[cells['FAST_MIN']] = delays[FAST].min |
| ws[cells['SLOW_MAX']] = delays[SLOW].max |
| ws[cells['SLOW_MIN']] = delays[SLOW].min |
| else: |
| ws[cells['FAST_MAX']] = 0 |
| ws[cells['FAST_MIN']] = 0 |
| ws[cells['SLOW_MAX']] = 0 |
| ws[cells['SLOW_MIN']] = 0 |
| |
| |
| def cells_to_delays(cells): |
| return { |
| FAST: IntristicDelay(min=cells['FAST_MIN'], max=cells['FAST_MAX']), |
| SLOW: IntristicDelay(min=cells['SLOW_MIN'], max=cells['SLOW_MAX']), |
| } |
| |
| |
| class Net(object): |
| def __init__(self, net): |
| self.net = net |
| self.ipin_nodes = {} |
| self.row = None |
| self.math = ExcelMathModel() |
| self.models = {} |
| |
| for ipin in net['ipins']: |
| for ipin_node in ipin['node'].strip().split(' '): |
| self.ipin_nodes[ipin_node] = ipin |
| |
| # Map of wire name to parent node |
| self.wire_to_node = {} |
| |
| # Map of node name to node |
| self.node_name_to_node = {} |
| |
| for node in net['nodes']: |
| self.node_name_to_node[node['name']] = node |
| for wire in node['wires']: |
| self.wire_to_node[wire['name']] = node |
| |
| # Map of (src node, dst wire). |
| self.pips = {} |
| for pip in net['pips']: |
| src_node = self.wire_to_node[pip['src_wire']]['name'] |
| dst_wire = pip['dst_wire'].split('/')[1] |
| self.pips[(src_node, dst_wire)] = pip |
| |
| if not int(pip['is_directional']): |
| dst_node = self.wire_to_node[pip['dst_wire']]['name'] |
| src_wire = pip['src_wire'].split('/')[1] |
| self.pips[(dst_node, src_wire)] = pip |
| |
| def extend_rc_tree(self, ws, current_rc_root, timing_lookup, node): |
| rc_elements = [] |
| |
| # LV nodes have a workaround applied because of a work around in the |
| # pip timing data. |
| is_lv_node = any( |
| wire['name'].split('/')[1].startswith('LV') |
| for wire in node['wires']) |
| for idx, wire in enumerate(node['wires']): |
| wire_timing = timing_lookup.find_wire(wire['name']) |
| ws['A{}'.format(self.row)] = wire['name'] |
| ws['B{}'.format(self.row)] = 'Part of wire' |
| |
| if wire_timing is not None: |
| cells = {} |
| cells['R'] = 'C{}'.format(self.row) |
| cells['C'] = 'D{}'.format(self.row) |
| if not is_lv_node: |
| ws[cells['R']] = wire_timing.resistance |
| ws[cells['C']] = wire_timing.capacitance |
| else: |
| # Only use first 2 wire RC's, ignore the rest. It appears |
| # that some of the RC constant was lumped into the switch |
| # timing, so don't double count. |
| if idx < 2: |
| ws[cells['R']] = wire_timing.resistance |
| ws[cells['C']] = wire_timing.capacitance |
| else: |
| ws[cells['R']] = 0 |
| ws[cells['C']] = 0 |
| |
| rc_elements.append( |
| RcElement( |
| resistance=cells['R'], |
| capacitance=cells['C'], |
| )) |
| |
| self.row += 1 |
| |
| wire_rc_node = Wire(rc_elements=rc_elements, math=self.math) |
| self.models[self.row - 1] = wire_rc_node |
| |
| current_rc_root.set_sink_wire(wire_rc_node) |
| |
| return wire_rc_node |
| |
| def descend_route( |
| self, |
| ws, |
| timing_lookup, |
| current_node, |
| route, |
| route_idx, |
| current_rc_root, |
| was_opin=False): |
| """ Traverse the next pip, or recurse deeper. """ |
| |
| # descend_route should've consumed this token |
| assert route[route_idx] != '}' |
| |
| while route[route_idx] == '{': |
| # Go deeper |
| route_idx = self.descend_route( |
| ws, |
| timing_lookup, |
| current_node, |
| route, |
| route_idx=route_idx + 1, |
| current_rc_root=current_rc_root, |
| was_opin=was_opin) |
| |
| next_edge = (current_node, route[route_idx]) |
| route_idx += 1 |
| assert next_edge in self.pips, (next_edge, self.pips.keys()) |
| |
| pip = self.pips[next_edge] |
| is_backward = self.wire_to_node[ |
| pip['dst_wire']]['name'] == current_node |
| if not is_backward: |
| assert self.wire_to_node[ |
| pip['src_wire']]['name'] == current_node, (current_node, pip) |
| |
| pip_info = timing_lookup.find_pip(pip['name']) |
| if not is_backward: |
| pip_timing = pip_info.timing |
| current_node = self.wire_to_node[pip['dst_wire']]['name'] |
| else: |
| pip_timing = pip_info.backward_timing |
| current_node = self.wire_to_node[pip['src_wire']]['name'] |
| |
| ws['A{}'.format(self.row)] = pip['name'] |
| |
| cells = {} |
| cells['R'] = 'C{}'.format(self.row) |
| cells['C'] = 'D{}'.format(self.row) |
| delays_to_cells( |
| ws, row=self.row, delays=pip_timing.delays, cells=cells) |
| delays = cells_to_delays(cells) |
| |
| if pip_info.is_pass_transistor: |
| ws['B{}'.format(self.row)] = 'PassTransistor' |
| ws[cells['R']] = pip_timing.drive_resistance |
| |
| pip_model = PassTransistor( |
| drive_resistance=cells['R'], |
| delays=delays, |
| ) |
| else: |
| ws['B{}'.format(self.row)] = 'Buffer' |
| if pip_timing.drive_resistance is not None: |
| ws[cells['R']] = pip_timing.drive_resistance |
| if pip_timing.drive_resistance == 0 and was_opin: |
| new_site_pin = timing_lookup.try_find_site_pin( |
| current_node, node_idx=0) |
| if new_site_pin is not None: |
| ws[cells['R']] = new_site_pin.timing.drive_resistance |
| else: |
| ws[cells['R']] = 0 |
| |
| if pip_timing.internal_capacitance is not None: |
| ws[cells['C']] = pip_timing.internal_capacitance |
| else: |
| ws[cells['C']] = 0 |
| |
| pip_model = Buffer( |
| drive_resistance=cells['R'], |
| internal_capacitance=cells['C'], |
| delays=cells_to_delays(cells), |
| ) |
| |
| self.models[self.row] = pip_model |
| |
| current_rc_root.add_child(pip_model) |
| self.row += 1 |
| |
| if current_node in self.ipin_nodes: |
| assert route[route_idx] in ['}', 'IOB_O_OUT0', 'IOB_T_OUT0'], ( |
| route_idx, |
| route[route_idx], |
| ) |
| route_idx += 1 |
| |
| node = self.node_name_to_node[current_node] |
| |
| current_rc_root = self.extend_rc_tree( |
| ws, pip_model, timing_lookup, node) |
| |
| if current_node in self.ipin_nodes: |
| ipin = self.ipin_nodes[current_node] |
| |
| cells = {} |
| name = '{} to {}'.format(self.net['opin']['name'], ipin['name']) |
| ws['A{}'.format(self.row)] = ipin['name'] |
| ws['B{}'.format(self.row)] = 'Inpin' |
| |
| site_pin = timing_lookup.find_site_pin(current_node, node_idx=-1) |
| assert isinstance(site_pin.timing, InPinTiming) |
| |
| cells = {} |
| cells['C'] = 'D{}'.format(self.row) |
| delays_to_cells( |
| ws, row=self.row, delays=site_pin.timing.delays, cells=cells) |
| delays = cells_to_delays(cells) |
| |
| ws[cells['C']] = site_pin.timing.capacitance |
| ipin_model = Inpin( |
| capacitance=cells['C'], delays=delays, name=name) |
| self.models[self.row] = ipin_model |
| current_rc_root.add_child(ipin_model) |
| self.row += 1 |
| |
| #Sum delays only (sum*1000) |
| #Sum delays + capacitive delay |
| # |
| #Total delay (from Vivado) |
| ws['A{}'.format(self.row)] = '{}: {} sum delays'.format( |
| self.net['net'], name) |
| self.row += 1 |
| |
| ws['A{}'.format(self.row)] = '{}: {}sum delays + cap delay'.format( |
| self.net['net'], name) |
| self.row += 2 |
| |
| ws['A{}'.format( |
| self.row)] = '{}: {}Total delay (from Vivado)'.format( |
| self.net['net'], name) |
| |
| ws['E{}'.format(self.row)] = ipin['ic_delays']['FAST_MAX'] |
| ws['F{}'.format(self.row)] = ipin['ic_delays']['FAST_MIN'] |
| ws['G{}'.format(self.row)] = ipin['ic_delays']['SLOW_MAX'] |
| ws['H{}'.format(self.row)] = ipin['ic_delays']['SLOW_MIN'] |
| |
| self.row += 2 |
| |
| return route_idx |
| else: |
| return self.descend_route( |
| ws, |
| timing_lookup, |
| current_node, |
| route, |
| route_idx=route_idx, |
| current_rc_root=current_rc_root) |
| |
| def walk_route(self, ws, timing_lookup): |
| """ Walk route, creating rows in table. |
| |
| First row will always be the OPIN, followed by the node/wire connected |
| to the OPIN. After a node/wire is always 1 or more pips. After a pip |
| is always a node/wire. A terminal node/wire will then reach an IPIN. |
| """ |
| |
| self.row = 2 |
| ws['A{}'.format(self.row)] = self.net['opin']['wire'] |
| |
| site_pin = timing_lookup.find_site_pin( |
| self.net['opin']['node'], node_idx=0) |
| assert isinstance(site_pin.timing, OutPinTiming) |
| |
| ws['B{}'.format(self.row)] = 'Outpin' |
| ws['C{}'.format(self.row)] = site_pin.timing.drive_resistance |
| |
| cells = {} |
| cells['R'] = 'C{}'.format(self.row) |
| |
| delays_to_cells( |
| ws, row=self.row, delays=site_pin.timing.delays, cells=cells) |
| |
| model_root = Outpin( |
| resistance=cells['R'], delays=cells_to_delays(cells)) |
| self.models[self.row] = model_root |
| self.row += 1 |
| |
| node = self.net['opin']['node'] |
| |
| tile, first_wire = self.net['opin']['node'].split('/') |
| |
| route = [r for r in self.net['route'].strip().split(' ') if r != ''] |
| assert route[0] == '{' |
| assert route[1] == first_wire |
| |
| node = self.node_name_to_node[node] |
| current_rc_root = self.extend_rc_tree( |
| ws, model_root, timing_lookup, node) |
| |
| self.descend_route( |
| ws, |
| timing_lookup, |
| node['name'], |
| route, |
| route_idx=2, |
| current_rc_root=current_rc_root, |
| was_opin=True) |
| |
| model_root.propigate_delays(self.math) |
| |
| model_rows = {} |
| |
| for row, model in self.models.items(): |
| model_rows[id(model)] = row |
| |
| for row, model in self.models.items(): |
| rc_delay = model.get_rc_delay() |
| if rc_delay is not None: |
| ws['J{}'.format(row)] = self.math.eval(rc_delay) |
| |
| downstream_cap = model.get_downstream_cap() |
| if downstream_cap is not None: |
| ws['I{}'.format(row)] = self.math.eval(downstream_cap) |
| |
| if isinstance(model, Inpin): |
| ipin_results = { |
| 'Name': model.name, |
| 'truth': {}, |
| 'computed': {}, |
| } |
| |
| ipin_delays = {} |
| |
| DELAY_COLS = ( |
| ('E', 'FAST_MAX'), |
| ('F', 'FAST_MIN'), |
| ('G', 'SLOW_MAX'), |
| ('H', 'SLOW_MIN'), |
| ) |
| |
| for col, value in DELAY_COLS: |
| ipin_delays[value] = [] |
| ipin_results['computed'][ |
| value] = '{title}!{col}{row}'.format( |
| title=utils.quote_sheetname(ws.title), |
| col=col, |
| row=row + 2) |
| ipin_results['truth'][value] = '{title}!{col}{row}'.format( |
| title=utils.quote_sheetname(ws.title), |
| col=col, |
| row=row + 4) |
| |
| rc_delays = [] |
| |
| for model in model.get_delays(): |
| delays = model.get_intrinsic_delays() |
| if delays is not None: |
| ipin_delays['FAST_MAX'].append(delays[FAST].max) |
| ipin_delays['FAST_MIN'].append(delays[FAST].min) |
| ipin_delays['SLOW_MAX'].append(delays[SLOW].max) |
| ipin_delays['SLOW_MIN'].append(delays[SLOW].min) |
| |
| if id(model) in model_rows: |
| rc_delays.append('J{}'.format(model_rows[id(model)])) |
| |
| ws['J{}'.format(row + 1)] = self.math.eval( |
| self.math.sum(rc_delays)) |
| |
| for col, value in DELAY_COLS: |
| ws['{}{}'.format(col, row + 1)] = self.math.eval( |
| self.math.sum(ipin_delays[value])) |
| ws['{}{}'.format( |
| col, row + 2)] = '=1000*({col}{row} + J{row})'.format( |
| col=col, row=row + 1) |
| |
| yield ipin_results |
| |
| |
| def add_net(wb, net, timing_lookup): |
| replace_underscore = str.maketrans('[]\\:/', '_____') |
| ws = wb.create_sheet( |
| title="Net {}".format(net['net'].translate(replace_underscore))) |
| |
| # Header |
| ws['A1'] = 'Name' |
| ws['B1'] = 'Type' |
| ws['C1'] = 'RES' |
| ws['D1'] = 'CAP' |
| ws['E1'] = 'FAST_MAX' |
| ws['F1'] = 'FAST_MIN' |
| ws['G1'] = 'SLOW_MAX' |
| ws['H1'] = 'SLOW_MIN' |
| ws['I1'] = 'Downstream C' |
| ws['J1'] = 'Delay from cap' |
| |
| net_obj = Net(net) |
| yield from net_obj.walk_route(ws, timing_lookup) |
| |
| |
| def build_wire_filter(wire_filter): |
| wires_to_include = set() |
| |
| with OpenSafeFile(wire_filter) as f: |
| for l in f: |
| wire = l.strip() |
| if not wire: |
| continue |
| wires_to_include.add(wire) |
| |
| def filter_net(net): |
| wires_in_net = set() |
| |
| for node in net['nodes']: |
| for wire in node['wires']: |
| wires_in_net.add(wire['name']) |
| |
| return len(wires_in_net & wires_to_include) > 0 |
| |
| return filter_net |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Create timing worksheet for 7-series timing analysis.") |
| |
| util.db_root_arg(parser) |
| util.part_arg(parser) |
| parser.add_argument('--timing_json', required=True) |
| parser.add_argument('--output_xlsx', required=True) |
| parser.add_argument( |
| '--wire_filter', |
| help='List of wires that must be present in a net to be output') |
| |
| args = parser.parse_args() |
| |
| with OpenSafeFile(args.timing_json) as f: |
| timing = json.load(f) |
| |
| db = Database(args.db_root, args.part) |
| |
| nodes = {} |
| for net in timing: |
| for node in net['nodes']: |
| nodes[node['name']] = node |
| |
| timing_lookup = TimingLookup(db, nodes) |
| |
| wb = Workbook() |
| summary_ws = wb[wb.sheetnames[0]] |
| summary_ws.title = 'Summary' |
| |
| summary_ws['A1'] = 'Name' |
| |
| cols = ['FAST_MAX', 'FAST_MIN', 'SLOW_MAX', 'SLOW_MIN'] |
| cur_col = 'B' |
| for col in cols: |
| summary_ws['{}1'.format(cur_col)] = col |
| cur_col = chr(ord(cur_col) + 1) |
| summary_ws['{}1'.format(cur_col)] = 'Computed ' + col |
| cur_col = chr(ord(cur_col) + 3) |
| |
| if args.wire_filter: |
| wire_filter = build_wire_filter(args.wire_filter) |
| else: |
| wire_filter = lambda x: True |
| |
| summary_row = 2 |
| |
| timing = [net for net in timing if wire_filter(net)] |
| for idx, net in enumerate(timing): |
| if '<' in net['route']: |
| print( |
| "WARNING: Skipping net {} because it has complicated route description." |
| .format(net['net'])) |
| continue |
| |
| print('Process net {} ({} / {})'.format(net['net'], idx, len(timing))) |
| for summary_cells in add_net(wb, net, timing_lookup): |
| summary_ws['A{}'.format(summary_row)] = summary_cells['Name'] |
| |
| cur_col = 'B' |
| for col in cols: |
| truth_col = chr(ord(cur_col) + 0) |
| computed_col = chr(ord(cur_col) + 1) |
| error_col = chr(ord(cur_col) + 2) |
| error_per_col = chr(ord(cur_col) + 3) |
| summary_ws['{}{}'.format( |
| truth_col, |
| summary_row)] = '=' + summary_cells['truth'][col] |
| summary_ws['{}{}'.format( |
| computed_col, |
| summary_row)] = '=' + summary_cells['computed'][col] |
| summary_ws['{}{}'.format( |
| error_col, |
| summary_row)] = '={truth}{row}-{comp}{row}'.format( |
| truth=truth_col, comp=computed_col, row=summary_row) |
| summary_ws['{}{}'.format( |
| error_per_col, |
| summary_row)] = '={error}{row}/{truth}{row}'.format( |
| error=error_col, truth=truth_col, row=summary_row) |
| |
| cur_col = chr(ord(cur_col) + 4) |
| |
| summary_row += 1 |
| |
| wb.save(filename=args.output_xlsx) |
| |
| |
| if __name__ == "__main__": |
| main() |