blob: 54ca7c7a3e46cdf742303406e50be29bbf3da423 [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
""" 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()