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.
Peter
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
timer1._interrupts
{'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
timer1.register_map
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:
- Load the value to count from in the
TLR0
register - Set then clear the
LOAD0
bit to trigger the load - Set the
ENIT0
bit to enable the interrupt output - Set the
UDT0
bit to get the timer to count down - 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))
loop.run_until_complete(task)
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.
ol.interrupt_pins
{'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.
ol.ip_dict['timer_0']['interrupts']
{'interrupt': {'controller': 'axi_intc_0',
'fullpath': 'timer_0/interrupt',
'index': 0}}