Zynq PS7 Interrupts

Interrupts are an area that is causing confusing and I’m working on creating some better documentation for them. I’ve also noticed some bugs in the code that can potentially cause spurious interrupt responses that I’m working to fix. In the meantime, I’ve pasted below an example I’m working on in the hope it can be of use to you - just remember it is a work in progress.

One thing I will highlight relevant to your example is that we only support interrupts that are ultimately connected to IRQ_F2P[0] - this is due to how the Linux drivers are configured in the stock image.


An Example for the PYNQ Interrupt Subsystem

The PYNQ Interrupt class is an asyncio-compatible interface to handling interrupts from the fabric. This notebook aims to:

  • Show how to create a block design compatible with PYNQ
  • Introduce the asyncio interface and how to call it from other contexts
  • Provide an example of the example of the recommended way to write a driver for existing IP

Hardware design

In this example we are going to use the AXI Timer IP from the Xilinx IP library (product guide) putting two independent timers in the fabric.

The PYNQ interrupt software layer is dependent on the hardware design meeting the following restrictions

  • All interrupts must ultimately be connected to the first interrupt line of the ZYNQ block
  • Multiple interrupts must be combined using AXI interrupt controllers

As we have two timers we will also need an interrupt controller to combine them into a single interrupt line into the PS. The result is a block diagram as follows.

This block design shows the pattern of using a concat IP block to combine all of the single interrupts into a single interrupt bus that then passed into the input of both the interrupt controller and the processing system. For more details on how to construct a block diagram see (…)

Exploring interrupts in software

With the hardware design complete we can start exploring the software architecture. To do this first we load the new overlay

import pynq

ol = pynq.Overlay('timer_interrupts.bit')

We can get access to instances of the interrupt class by navigating the overlay object. Each IP instances has a _interrupts dictionary which lists the names of the interrupts

timer1 = ol.timer_1
{'interrupt': {'controller': 'axi_intc_0',
  'fullpath': 'timer_1/interrupt',
  'index': 1}}

And the interrupts object can then be accessed by its name

interrupt = timer1.interrupt

The Interrupt class provides a single function wait which is an asyncio coroutine that returns when the interrupt is signalled. To demonstrate this we first need to look at the documentation for the timer and see how to get it to fire after a specific period of time. We can also look at the register map of the IP in Python to assist

RegisterMap {
  TCSR0 = Register(MDT0=0, UDT0=1, GENT0=0, CAPT0=0, ARHT0=0, LOAD0=0, ENIT0=1, ENT0=1, T0INT=0, PWMA0=0, ENALL=0, CASC=0),
  TLR0 = Register(TCLR0=500000000),
  TCR0 = Register(TCR0=4294967295),
  TCSR1 = Register(MDT1=0, UDT1=0, GENT1=0, CAPT1=0, ARHT1=0, LOAD1=0, ENIT1=0, ENT1=0, T1INT=0, PWMA1=0, ENALL=0),
  TLR1 = Register(TCLR1=0),
  TCR1 = Register(TCR1=0)

The programming steps for the timer are to do the following:

  1. Load the value to count from in the TLR0 register
  2. Set then clear the LOAD0 bit to trigger the load
  3. Set the ENIT0 bit to enable the interrupt output
  4. Set the UDT0 bit to get the timer to count down
  5. Set the ENT0 bit start the timer

Once the interrupt is signalled we then need to write to the T0INT bit to clear the interrupt.

We can package all of this into a coroutine as follows

async def wait_for_timer1(cycles):
    timer1.register_map.TLR0 = cycles
    timer1.register_map.TCSR0.LOAD0 = 1
    timer1.register_map.TCSR0.LOAD0 = 0
    timer1.register_map.TCSR0.ENIT0 = 1
    timer1.register_map.TCSR0.ENT0 = 1
    timer1.register_map.TCSR0.UDT0 = 1
    await timer1.interrupt.wait()
    timer1.register_map.TCSR0.T0INT = 1

To test this we need to use the asyncio library to schedule our new coroutine. asyncio uses event loops to execute coroutines. When python starts it will create a default event loop which is what the PYNQ interrupt subsystem uses to handle interrupts.

import asyncio

loop = asyncio.get_event_loop()
task = loop.create_task(wait_for_timer1(500000000))

The low-level details

To see what interrupts are in the system we can look at the interrupt_pins dictionary. Each entry is a mapping from the name of a pin in the block diagram to the interrupt controller that manages it.

{'pynq_interrupts/In0': {'controller': 'axi_intc_0',
  'fullpath': 'pynq_interrupts/In0',
  'index': 0},
 'pynq_interrupts/In1': {'controller': 'axi_intc_0',
  'fullpath': 'pynq_interrupts/In1',
  'index': 1},
 'timer_0/interrupt': {'controller': 'axi_intc_0',
  'fullpath': 'timer_0/interrupt',
  'index': 0},
 'timer_1/interrupt': {'controller': 'axi_intc_0',
  'fullpath': 'timer_1/interrupt',
  'index': 1}}

This is a low level description of what’s going on but can be useful to make sure that interrupts are being detected as intended. At a slightly higher level, each entry in the IP dictionary contains the subset of the complete dictionary applicable only to that IP.

{'interrupt': {'controller': 'axi_intc_0',
  'fullpath': 'timer_0/interrupt',
  'index': 0}}