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…

1 Like

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:

1 Like

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.


1 Like

Sure. Here it is.
ax_top_bd_wrapper_with_ILA.zip (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

I tried this one and it mostly works.
I have one unexpected behaviour: the first time this waits for 5 seconds:

task = loop.create_task(wait_for_timer1(500000000))

Next time it returns immediately.
3rd time it waits 5 seconds, … etc

I documented my attempt here: https://www.element14.com/community/groups/fpga-group/blog/2021/09/05/learning-xilinx-zynq-interrupt-arm-from-fpga-fabric

Looks like you’ve run into a bug/limitation of our API for handling interrupts. Unfortunately there’s no simple solution that comes to mind. I’ll have a think and see if there’s something we can add to the API to solve this issue.

Basically there’s a race condition between wait() returning and your code resetting the interrupt line which means that a second interrupt gets enqueued before you have a chance to clear it. There’s nothing you can do in your code to resolve this and it’s inevitable with the current API so we’ll need to add an extra feature in the next version.

Sorry about this



I was playing around with this example. I had the same issues as @jancumps .

I believe that the PYNQ implementation is correct/good enough. The main problem was that the block design automation decided to change the interrupt inputs to use the “level detection”, which results in an interrupt being sent immediately after clearing it because timer1.register_map.TCSR0.T0INT = 1 is only set after the waiting is over. Therefore, during the wait operation, the interrupt will be triggered, PYNQ sees the new input and clears it, and lets wait continue. Now, because the interrupt line from the timer is still high and “level detection” is used, another interrupt is immediately triggered. And only afterwards are we able to disable the interrupt from the timer.

I then tested if changing the level detection to “edge” will fix the observed behavior, because only one interrupt should be triggered. This did indeed fix the behavior.

In conclusion, I believe that PYNQ does work as intended. When setting the interrupt controller to do “edge detection”, it works as expected.

I am, however, curious how one would try to work with “level detection” in PYNQ. There would probably be need a way to be able to execute some code right before the clear command is sent to the interrupt controller (https://github.com/Xilinx/PYNQ/blob/master/pynq/interrupt.py#L235 ).

Please mention/describe this issue regarding the level interrupt detection type in the newly added documentation:

By default it chooses level detection:

Had to set interrupt detection to use edge detection manually:

Block diagram (as described in this example, I did use timers to generate interrupts):


Makes sense. I am going to test this now …

Confirming that this setup works.

The interrupt now waits the expected time, each time.

I submitted a pull request to adapt the documentation: https://github.com/Xilinx/PYNQ/pull/1318


Thanks for trying and confirming this. This is not something I’d thought of and seems to fix the issue nicely. I’ve been trying various ways to make level-triggered interrupts not double-trigger and come to the conclusion we need an API change. Exactly what that would look like is still up for discussion but will most likely involve some form of callback mechanism where the interrupt can be cleared in the IP before we reset the interrupt controller. Anything else is prone to race conditions where interrupts don’t trigger at all which is even worse than where we are now.