Source code for qick.ip
"""
Support classes for dealing with FPGA IP blocks.
"""
from pynq.overlay import DefaultIP
import numpy as np
import logging
from qick import obtain
from .qick_asm import DummyIp
[docs]class SocIp(DefaultIP, DummyIp):
"""
Base class for firmware IP drivers.
Registers are accessed as attributes.
Configuration constants are accessed as dictionary items.
"""
REGISTERS = {}
def __init__(self, description):
"""
Constructor method
"""
DefaultIP.__init__(self, description)
# this block's unique identifier in the firmware
self.fullpath = description['fullpath']
# this block's type
self.type = description['type'].split(':')[-2]
DummyIp.__init__(self, self.type, self.fullpath)
# logger for messages associated with this block
self.logger = logging.getLogger(self.type)
def __setattr__(self, a, v):
"""
Sets the arguments associated with a register
:param a: Register specified by an offset value
:type a: int
:param v: value to be written
:type v: int
"""
try:
index = self.REGISTERS[a]
self.mmio.array[index] = np.uint32(obtain(v))
except KeyError:
super().__setattr__(a, v)
def __getattr__(self, a):
"""
Gets the arguments associated with a register
:param a: register name
:type a: str
:return: Register arguments
:rtype: *args object
"""
try:
index = self.REGISTERS[a]
return self.mmio.array[index]
except KeyError:
return super().__getattribute__(a)
[docs]class QickMetadata:
"""
Provides information about the connections between IP blocks, extracted from the HWH file.
The HWH parser is very different between PYNQ 2.6/2.7 and 3.0+, so this class serves as a common interface.
"""
def __init__(self, soc):
# We will use the HWH parser to extract information about signal connections between blocks.
# system graph object, if available
self.systemgraph = None
# root element of the HWH file
self.xml = None
# parsers for signals and busses, using system graph or XML as appropriate
self.sigparser = None
self.busparser = None
if hasattr(soc, 'systemgraph'):
# PYNQ 3.0 and higher have a "system graph"
self.systemgraph = soc.systemgraph
self.xml = soc.systemgraph._root
else:
self.sigparser = soc.parser
# Since the HWH parser doesn't parse buses, we also make our own BusParser.
self.xml = soc.parser.root
# TODO: We shouldn't need to use BusParser for PYNQ 3.0, but we think there's a bug in how pynqmetadata handles axis_switch.
self.busparser = BusParser(self.xml)
self.timestamp = self.xml.get('TIMESTAMP')
def trace_sig(self, blockname, portname):
if self.systemgraph is not None:
dests = self.systemgraph.blocks[blockname].ports[portname].destinations()
result = []
for port, block in dests.items():
blockname = block.parent().name
if blockname==self.systemgraph.name:
result.append([port])
else:
result.append([blockname, port])
return result
return self._trace_net(self.sigparser, blockname, portname)
def trace_bus(self, blockname, portname):
return self._trace_net(self.busparser, blockname, portname)
def _trace_net(self, parser, blockname, portname):
"""
Find the block and port that connect to this block and port.
If you expect to only get one block+port as a result, you can assign the result to ((block, port),)
:param parser: HWH parser object (from Overlay.parser, or BusParser)
:param blockname: the IP block of interest
:type blockname: string
:param portname: the port we want to trace
:type portname: string
:return: a list of [block, port] pairs, or just [port] for ports of the top-level design
:rtype: list
"""
fullport = blockname+"/"+portname
# the net connected to this port
netname = parser.pins[fullport]
if netname == '__NOC__':
return []
# get the list of other ports on this net, discard the port we started at and ILA ports
return [x.split('/') for x in parser.nets[netname] if x != fullport and 'system_ila_' not in x]
[docs] def get_fclk(self, blockname, portname):
"""
Find the frequency of a clock port.
:param parser: HWH parser object (from Overlay.parser, or BusParser)
:param blockname: the IP block of interest
:type blockname: string
:param portname: the port we want to trace
:type portname: string
:return: frequency in MHz
:rtype: float
"""
xmlpath = "./MODULES/MODULE[@FULLNAME='/{0}']/PORTS/PORT[@NAME='{1}']".format(
blockname, portname)
port = self.xml.find(xmlpath)
return float(port.get('CLKFREQUENCY'))/1e6
[docs] def get_param(self, blockname, parname):
"""
Find the value of an IP parameter. This works for all IPs, including those that do not show up in ip_dict because they're not addressable.
:param parser: HWH parser object (from Overlay.parser, or BusParser)
:param blockname: the IP block of interest
:type blockname: string
:param parname: the parameter of interest
:type parname: string
:return: parameter value
:rtype: string
"""
xmlpath = "./MODULES/MODULE[@FULLNAME='/{0}']/PARAMETERS/PARAMETER[@NAME='{1}']".format(
blockname, parname)
param = self.xml.find(xmlpath)
return param.get('VALUE')
def mod2type(self, blockname):
if self.systemgraph is not None:
return self.systemgraph.blocks[blockname].vlnv.name
return self.busparser.mod2type[blockname]
[docs] def trace_back(self, start_block, start_port, goal_types):
"""Follow the AXI-Stream bus backwards from a given block and port.
Raise an error if none of the requested IP types is found.
Parameters
----------
start_block : str
The fullpath for the block to start tracing from.
start_port : str
The name of the input port to start tracing from,
goal_types : list of str
IP types that we're interested in.
Returns
-------
str
The fullpath for the block we found.
str
The output port on the block we found.
str
The IP type we found.
"""
block = start_block
port = start_port
((block, port),) = self.trace_bus(block, port)
while True:
blocktype = self.mod2type(block)
if blocktype in goal_types:
return (block, port, blocktype)
elif blocktype == "axis_clock_converter":
((block, port),) = self.trace_bus(block, 'S_AXIS')
elif blocktype == "axis_dwidth_converter":
((block, port),) = self.trace_bus(block, 'S_AXIS')
elif blocktype == "axis_cdcsync_v1":
# port name is of the form 'm4_axis' - follow corresponding input 's4_axis'
((block, port),) = self.trace_bus(block, "s"+port[1:])
elif blocktype == "sg_translator":
((block, port),) = self.trace_bus(block, "s_tproc_axis")
elif blocktype == "axis_resampler_2x1_v1":
((block, port),) = self.trace_bus(block, 's_axis')
elif blocktype == "axis_register_slice":
((block, port),) = self.trace_bus(block, 'S_AXIS')
elif blocktype == "axis_broadcaster":
((block, port),) = self.trace_bus(block, 'S_AXIS')
else:
raise RuntimeError("failed to trace back from %s - unrecognized IP block %s" % (start_block, block))
[docs] def trace_forward(self, start_block, start_port, goal_types):
"""Follow the AXI-Stream bus forwards from a given block and port.
If a broadcaster is encountered, follow all outputs.
Raise an error if ~=1 matching block is found.
Parameters
----------
start_block : str
The fullpath for the block to start tracing from.
start_port : str
The name of the output port to start itracing from,
goal_types : list of str
IP types that we're interested in.
Returns
-------
str
The fullpath for the block we found.
str
The input port on the block we found.
str
The IP type we found.
"""
block = start_block
port = start_port
to_check = [(start_block, start_port)]
found = []
dead_ends = []
while to_check:
block, port = to_check.pop(0)
((block, port),) = self.trace_bus(block, port)
blocktype = self.mod2type(block)
if blocktype in goal_types:
found.append((block, port, blocktype))
elif blocktype == "axis_broadcaster":
for iOut in range(int(self.get_param(block, 'NUM_MI'))):
to_check.append((block, "M%02d_AXIS" % (iOut)))
elif blocktype == "axis_clock_converter":
to_check.append((block, "M_AXIS"))
elif blocktype == "axis_register_slice":
to_check.append((block, "M_AXIS"))
elif blocktype == "axis_register_slice_nb":
to_check.append((block, "m_axis"))
else:
dead_ends.append(block)
if len(found) != 1:
raise RuntimeError("traced forward from %s for one block of type %s, but found %s (and dead ends %s)" % (start_block, goal_types, found, dead_ends))
return found[0]
[docs]class BusParser:
"""Parses the HWH XML file to extract information on the buses connecting IP blocks.
"""
def __init__(self, root):
"""
Matching all the buses in the modules from the HWH file.
This is essentially a copy of the HWH parser's match_nets() and match_pins(),
but working on buses instead of signals.
In addition, there's a map from module names to module types.
:param root: HWH XML tree (from Overlay.parser.root)
"""
self.nets = {}
self.pins = {}
self.mod2type = {}
for module in root.findall('./MODULES/MODULE'):
fullpath = module.get('FULLNAME').lstrip('/')
self.mod2type[fullpath] = module.get('MODTYPE')
for bus in module.findall('./BUSINTERFACES/BUSINTERFACE'):
port = fullpath + '/' + bus.get('NAME')
busname = bus.get('BUSNAME')
self.pins[port] = busname
if busname in self.nets:
self.nets[busname] |= set([port])
else:
self.nets[busname] = set([port])