Zynq PS7 Interrupts

Hello there,
PYNQ PL interrupts handling isn’t very clear to me. I went through the documentation, examples and even an external blog, but I do not understand how it works, at all.
My requirement is very simple: when an AXI peripheral raise an interrupt, I want to print something (possibly an incrementing counter and a timestamp, when it works…).

  • I connected my peripheral interrupt line to IRQ_F2P[15] in the block design.

  • I run PL.interrupt_pins and it seems the interrupt line is where it should be:

  • How do I get my notebook to print a timestamp every time the interrupt is raised?
    I copied and pasted this from the documentation:

async def interrupt_handler_async(self, value):
    if self.iop.interrupt is None:
        raise RuntimeError('Interrupts not available in this Overlay')
        await self.iop.interrupt.wait() # Wait for interrupt
        # Do something when an interrupt is received

And then I did:

interrupt_handler_async("int_handler", 15)

Is that right? All I got is:

<coroutine object interrupt_handler_async at 0xafdea630>

But no printouts…

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}}

Thanks, I’ll try to understand better the “Exploring interrupts in software” part that is the one most relevant to my current problem… The hardware part is OK.

I cannot get it working.
This below is my hardware design, the highlighted line ends up in PS7 IRQ_F2P[15:0]

I attach, because the output is too long for a picture, the output of ol.ip_dict and ol.interrupt_pins.
When I do the same as in your example, i.e.:

I get:

Where is my mistake?
ol_ip_dict.txt (48.3 KB)
ol_interrupt_pins.txt (2.6 KB)
by the way, I tried even the following but without success:

It looks like our hwh parsing is getting confused by an IP with two AXI slave ports. Would you be able to share your hwh file so we can add it to our test suite?

You can instantiate the pynq.Interrupt class directly by passing it the name of the pin - "BERT_0/event_irq" in your case. Note that PYNQ can only interrupt with interrupts that ultimately attach to IRQ line 0 so this still may not work in your example. You can either move the interrupts around or change the UIO entry in device-tree to point to correct interrupt line - that should work but we haven’t tested it.


Sure. Here it is. (91.6 KB)
"Note that PYNQ can only interrupt with interrupts that ultimately attach to IRQ line 0"
Isn’t that a huge limitation? How to deal with a multi-interrupted system?

That’s why we recommend using AXI interrupt controllers to collapse all of the interrupts in the design down to a single interrupt line into the PS. The base overlay has 12 fabric interrupts accessible this way.


1 Like