Python Overlay API

The Python API is the user interface for the overlay, exposing the programmable functionality in the design.

An API for a PYNQ overlay can consist of

  • a simple Python wrapper that interfaces directly with the hardware IP blocks in the overlay
  • a more substantial Python layer utilising other Python packages
  • a Python library that interfaces to a lower level higher performance library (written in C/C++ for example) to control the overlay

The API for an overlay will manage the transfer of data between the Python environment in the PS, and the overlay in the PL. This may be the transfer of data directly from the PS to a peripheral or managing system memory to allow a peripheral in the PL to read or write data from DRAM that can also be access by from the Python environment.

The Default API

When and Overlay is loaded using the pynq.Overlay function all of the IP and hierarchies in the overlay will have drivers assigned to them and used to construct an object hierarchy. The IP can then be accessed via attributes on the returned overlay class using the names of the IP and hierarchies in the block diagram.

If no driver has been specified for a type of IP then a DefaultIP will be instantiated offering read and write functions to access the IP’s address space and named accessors to any interrupts or GPIO pins connected to the IP. Hierarchies likewise will be instances of DefaultHierarchy offering access to any sub hierarchies or contained IP. The top-level DefaultOverlay also acts just like any other IP.

Customising Drivers

While the default drivers are useful for getting started with new hardware in a design it is preferable to have a higher level driver for end users to interact with. Each of DefaultIP, DefaultHierarchy and DefaultOverlay can be subclassed and automatically bound to elements of the block diagram. New drivers will only be bound when the overlay is reloaded.

Creating IP Drivers

All IP drivers should inherit from DefaultIP and include a bindto class attribute consisting of an array of strings. Each string should be a type of IP that the driver should bind to. It is also strongly recommend to call super().__init__ in the class’s constructor. The type of an IP can be found as the VLNV parameter in Vivado or from the ip_dict of the overlay.

A template for an IP driver is as follows:

from pynq import DefaultIP

class MyIP(DefaultIP):
    bindto = ['My IP Type']
    def __init__(self, description):
        super().__init__(description)

Creating Hierarchy Drivers

Hierarchy drivers should inherit from DefaultHierarchy and provide a static method checkhierarchy that takes a description and returns True if the driver can bind to it. Any class that meets these two requirements will be automatically added to a list of drivers tested against each hierarchy in a newly loaded overlay.

A template for a hierarchy driver is as follows:

from pynq import DefaultHierarchy

class MyHierarchy(DefaultHierarchy)
    def __init__(self, description):
        super().__init__(description)

    @staticmethod
    def checkhierarchy(description):
        return False

Creating Custom Overlay Classes

Finally the class changed from the DefaultOverlay to provide a more suitable high-level API or provide overlay-specific initialisation. The overlay loader will look for a python file located alongside the bitstream and TCL files, import it and then call the Overlay function.

A template for a custom overlay class is as follows:

from pynq import DefaultOverlay

class MyOverlay(DefaultOverlay):
    def __init__(self, bitfile_name, download):
        super().__init__(bitfile_name, download)

        # Other initialisation

Overlay = MyOverlay

Working with Physically Contiguous Memory

In many applications there is a need for large buffers to be transferred between the PS and PL either using DMA engines or HLS IP with AXI master interfaces. In PYNQ the allocate functions provides a mechanism to acquire numpy arrays allocated as to be physically contiguous. The allocate function takes shape and dtype parameters in a similar way to other numpy construction functions.

from pynq import allocate

matrix1 = allocate(shape=(32,32), dtype='f4')

These arrays can either be passed directly to the DMA driver’s transfer function or they contain a physical_address attribute which can be used by custom driver code.

When the array is no longer needed the underlying resources should be freed using the freebuffer function. Alternatively a context manager can be used to ensure that the buffer is freed at the end of a scope.

with allocate(shape=(32,32), dtype=np.float32) as matrix2:
    dma.sendchannel.transfer(matrix2)
    dma.recvchannel.transfer(matrix1)
    dma.sendchannel.wait()
    dma.recvchannel.wait()
    matrix1.freebuffer()