GIC

Setting up and using ARM's Generic Interrupt Controller

13103

ARM program execution is automatically interrupted when certain exceptions occur, like the assertion of an externally generated interrupt, or the occurrence of an internal fault like an attempt to execute an undefined instruction. When an exception occurs, the processor interrupts normal program execution, and fetches its next instruction from a specially designated address that corresponds to the type of exception. That address is called the “exception vector”, and the instruction stored there is typically a jump to an user-written “exception handler” subroutine. The exception handler preserves context (i.e., makes copies of all CPU registers), deals with the event that caused the exception, restores context, and then returns to the normal program flow.

ZYNQ’s ARM processor defines seven types of exceptions. They include a system reset, an attempt to execute an undefined instruction, an aborted instruction fetch, an aborted data memory cycle, an aborted hypervisor call, and IRQ and FIQ interrupt signal assertions. Each exception type has its own vector, and the vectors are stored in a section of consecutive memory locations called a vector table. When one of the seven exceptions occur, the corresponding vector is automatically loaded into the PC, and the ARM automatically switches out of User mode and into a corresponding “privileged” operating mode. Switching to privileged mode stores copies of the stack pointer (SP), link register (LR), and CPSR in dedicated “shadow registers”, and allows certain privileged actions (like modifying the CSPR). You can read more about operating modes and exceptions here: Exceptions Processor Modes


Figure 1. ARM vector table
Figure 1. ARM vector table

This document discusses the ARMs response to external interrupts. Internally generated faults are discussed separately. The ARM processor has two interrupt input signals called IRQ (interrupt request) and FIQ (fast interrupt request). They are very similar, except the FIQ minimizes the time taken for context switching. This discussion focuses on the standard IRQ system.

Immediately after the IRQ signal is asserted, the next instruction to be executed will be loaded from the IRQ vector stored in the vector table. That vector typically jumps to an interrupt handler subprogram to determine the cause of the interrupt, and to branch to code to deal with the specific interrupt. On the ZYNQ device, nearly 100 interrupt signals are generated by the IP blocks and FPGA that surround the ARM processor, but there is only one IRQ input signal. An IP block called the “Generic Interrupt Controller” (GIC) receives all the interrupt signals, processes them, and drives the IRQ signal if certain conditions are met. The GIC keeps track of which interrupt sources are active, so when IRQ is asserted the interrupt handler can query the GIC to learn which interrupt is active, and then branch to an Interrupt Service Routine (ISR) written for that interrupt. The GIC also allows software to enable, disable, mask, and prioritize the interrupt sources. Like all IP blocks, the GIC’s status and control registers are available on the ARM’s bus. Before interrupts can be used, the GIC must be configured to assign interrupt priorities and enable the desired interrupts.

Interrupts signals can be generated by the on-board IP blocks/peripheral circuits, by the FPGA (or passed through the FPGA from external peripheral devices), or by software running on the ARM, and any number of them may be asserted at any given time. The GIC assigns a fixed ID number and a programmable priority level to every interrupt, and always maintains a list of all asserted interrupts. Asserted interrupt priority levels are compared to the current CPU priority level (as set in the ICCPMR register); if at least one pending interrupt has a priority higher than the current CPU priority level, then the IRQ signal is asserted, and the highest priority interrupt is taken. The GIC holds any other pending interrupts in a prioritized list for subsequent issuance. If no pending interrupt priority is higher than the CPU’s current priority level, then the IRQ signal is not asserted, and all interrupts are held pending until the CPU priority level is decreased.

Figure 2. ARM’s GIC block
Figure 2. ARM’s GIC block

To use interrupts in any given application, several steps must be completed:

  • the interrupt source(s) must be configured to generate interrupts under the desired conditions;
  • the GIC must be properly configured (individual interrupts must be enabled and their priorities set);
  • an interrupt handler (IH) must be written to check the Interrupt Acknowledge Register (ICCIAR) in the GIC to see which interrupt ID is active, and then branch to the specific ISR written for that interrupt;
  • an ISR must be written for each interrupt that may be serviced;
  • and the ARM must be enabled to receive and process interrupts.

When writing in Assembly, the ISR should preserve context, including the SP and LR if nested interrupts are possible (recall the ARM automatically switches from User to IRQ mode and copies the PC, LR and CPSR into shadow registers when an interrupt is taken). If the current/active ISR isn’t servicing the highest priority interrupt, interrupts should re-enabled by setting the IRQ disable bit to a ‘0’ so that any higher priority interrupts can be serviced. On completion, the ISR can restore context, write the interrupt’s ID number back to the ICCIAR to signal the GIC the interrupt has been serviced, and then use the instruction “MOVS PC, LR” to restore the shadowed registers and return to the calling program.

After the interrupt handler is written, it must be properly integrated so that it is called when an interrupt occurs. The Xilinx SDK tool creates a vector table and a default handler for you, and provides a library function that you can use to replace their handler with your own (see below).

Figure 2. ARM’s GIC block
Figure 2. ARM’s GIC block

ICCICR (0xF8F00100): CPU Interface Control Register. Controls/enables interrupt signal connection to processor(s). Set EN and ES bits to 1 to enable interrupts (read/write).

SB: Secure Binary Point Register config (1 to use SBPR for secure and non-secure interrupts)
FE: FIQ enable for secure interrupts (1 to use FIQ, 0 to use IRQ)
SA: Secure Acknowledge (1 to acknowledge non-secure high priority interrupts)
EN: Allow software to enable non-secure interrupts (1 to enable)
ES: Enable interrupts (1 to enable)


ICCPMR (0xF8F00104): Interrupt Priority Mask Register. Sets CPU priority – only interrupts with higher priority will be taken (read/write).

PRIORITY: Sets CPU priority level (interrupts with lower priority setting will not be taken)


ICCIAR (0xF8F0010C): Interrupt Acknowledge Register. Interrupt ID can be read from lower 10 bits (CPUID identifies which processor core requested the interrupt – it is irrelevant for single-core processors) (read/write).

ACKINTID: Reading this 10-bit field returns the ID of the active interrupt, and also acknowledges to the GIC the interrupt has been taken.
CPUID: In a multi-core processor, identifies the core that requested the interrupt.


ICCEOIR (0xF8F00110): End of Interrupt Register. The ID of the interrupt that was being serviced must be written to this register to signal the GIC the interrupt has been dealt with (CPUID identifies which processor core requested the interrupt – it is irrelevant for single-core processors) (read/write).

EOIINTID: Writing the ID of the active interrupt signifies the interrupt has been serviced.
CPUID: In a multi-core processor, identifies the core that requested the interrupt.


ICCHPIR (0xF8F00118): Highest Pending Interrupt Register. Shows highest priority pending interrupt ID (read/write)

PENDINTID: Priority level of highest priority pending interrupt.
CPUID: In a multi-core processor, identifies the core that requested the interrupt.


ICDDCR (0xF8F01000): Distributor Control Register. Enables distributor to update registers (read/write).

EN: Enables distributor to update various non-secure interrupt control bits (1 to enable).
ES: Enables distributor to update various secure interrupt control bits (1 to enable).


ICDISER (0xF8F01100 – 0xF8F01108): Interrupt Set Enables (x3). Set-enable bits for each interrupt supported by GIC. Writing a 1 to a given bit location enables forwarding of corresponding interrupt (for example, writing a 1 to bit #52 – the 20th bit in the ICDISER1 – enables interrupt #52).


ICDICER (0xF8F01180 – 0xF8F01188): Interrupt Clear Enables (x3). Clear-enable bits for each interrupt supported by GIC. Writing a 1 to a given bit location disables forwarding of corresponding interrupt (for example, writing a 1 to bit #52 – the 20th bit in the ICDICER1 – disables forwarding of interrupt #52).

ICDIPR (0xF8F01400 – 0xF8F0145C): Interrupt Priority Registers (x24). Five-bit fields in the bytes of these 24 registers set the priority levels for all interrupt sources (read/write).

PR_Nx: Priority level for corresponding interrupt signal.


ICDIPTR (0xF8F01800 – 0xF8F0185C): Interrupt Processor Targets Register (x24). Two-bit fields in the bytes of these 24 registers identify CPU targeted by interrupt (in a single-core device, these fields must always be 01). (read only)

T_Nx: CPU target (01 for CPU0, 10 for CPU1. Use 01 for single-core device).


ICDICFR (0xF8F01C00 – 0xF8F01C14): Interrupt Sensitivity Configuration Register (x6): The two-bit fields in these registers select edge vs. level sensitivity for interrupt signals. Note that for level-sensitive signals, software must properly deal with interrupt source to avoid multiple interrupts. ICDICFR register 0 (address 0XF8F01C00) is a read-only register that controls software-generated interrupts - it always returns 10 to indicate SGI’s are always edge sensitive. All other regsiters are read/write. Writing a 01 selects high-level active, and 11 selects rising-edge sensitive.

S_NXX: Sensitivity for corresponding interrupt (edge vs. level).


Configuring the ZYNQ system to use interrupts

To use interrupts in the ZYNQ system, the IP blocks that will generate interrupt signals must be configured, the ARM core must be configured to accept interrupts, and the GIC must be configured to process the interrupt signals correctly. Several sections following this section present several examples of configuring IP blocks to generate interrupt signals.

Configuring the ARM processor to receive interrupts

Clearing the “I” bit in the CPSR (bit7) enables the ARM processor to respond to interrupts; setting the I bit disables interrupts. The CPSR is a protected resource, and a special instruction called “move special register” (msr) is required. The C compiler cannot generate that instruction, so inline assembly must be used as shown (the “asm” keyword causes the argument to be placed into the instruction stream). To enable or disable interrupts, only the I bit should be modified, and all other bits should be left unchanged. The code segments below illustrate the process.

CPSR: Current Program Status Register: The fields in this register configure ARM core operations.

N: ALU Negative condition flag, set to bit 31 of the result of the instruction.
Z: ALU Zero condition flag, set to 1 if the result of the instruction is zero.
C: ALU Carry flag, set to 1 is the instruction results in a carry.
V: ALU oVerflow flag, set to 1 if the instruction results in an overflow condition.
Q: Saturation flag, set to 1 if saturation occurred during DSP operations.
IT: If-then execution bits for Thumb instruction set.
J: Jazelle bit, used with T bit to select instruction set (ARM/Thumb/Jazelle/ThumbEE) (0 for ARM).
GE: Greater than or Equal flags indicating results from instructions on bytes or half-words.
E: Endianness (0 for little endian, 1 for big endian).
A: Asynchronous Abort mask bit.
I: IRQ mask bit.
F: FIQ mask bit.
T: Thumb bit, used with J bit to select instruction set (ARM/Thumb/Jazelle/ThumbEE) (0 for ARM).
MODE: Mode field that determines operating mode of processor.

void disable_arm_interrupts(void)
{
	uint32_t cpsr_val =0;
	asm("mrs %0, cpsr\n" : "=r" (cpsr_val) );	//get current cpsr
	cpsr_val |= 0x80;				//set the I bit (bit7)
	asm("msr cpsr, %0\n" : : "r" (cpsr_val));	//writeback modified value
	return;

}

void enable_arm_interrupts(void)
{
	uint32_t cpsr_val =0;
	asm("mrs %0, cpsr\n" : "=r" (cpsr_val) );	//get current cpsr
	cpsr_val &= 0xFFFFFF7F;				//clear the I bit (bit7)
	asm("msr cpsr, %0\n" : : "r" (cpsr_val));	//writeback modified value
	return;
}

Configuring the GIC to process interrupts

Every potential interrupt source on ZYNQ is assigned a unique ID number (a table in the ZYNQ TRM on page 230 lists all the IRQ sources and their ID numbers). The GIC can be configured to deal with each specific interrupt in different ways. In general, the procedure for configuring the GIC includes several steps:

  • Disable the GIC Distributor;
  • Enable secure interrupts;
  • Set the GIC priority mask;
  • Configure priority and sensitivity for individual interrupt(s);
  • And re-enable the GIC Distributor.

Macros are defined in the example C code below to make the ensuing code more readable. The latter five macros use a variable that allows an offset to be added to the base address. This is useful when dealing with groups of related registers – the offset is added to the defined base address, and since the values are cast as 32-bit pointers, the final address will be the base address + (4 * offset).

//definitions for GIC configuration regisjters
#define ICCPMR		*((uint32_t *) 0xF8F00104)		//Priority mask reg
#define ICCICR		*((uint32_t *) 0xF8F00100)		//CPU interface control reg
#define ICDDCR		*((uint32_t *) 0xF8F01000)		//Distributor Control reg
#define ICDISER(n)	*(((uint32_t *) 0xF8F01100) + n )	//Int. set enable reg
#define ICDICER(n)	*(((uint32_t *) 0xF8F01180) + n )	//Int. Clear Enable reg
#define ICDIPR(n)	*(((uint32_t *) 0xF8F01400) + n )	//Int. Priority reg
#define ICDIPTR(n)	*(((uint32_t *) 0xF8F01800) + n )	//Int. Processor Targets reg
#define ICDICFR(n)	*(((uint32_t *) 0xF8F01C00) + n )	//Int. Configuration reg

Following are a few examples of using these macros to modify registers.

int val =0;

//reading from ICCPMR
val = ICCPMR;

//writing to ICCPMR
ICCPMR = 5;

//writing to ICDISER0
ICDISER(0) = 7;

//reading from ICDISER2
val = ICDISER(2);

Five step process for configuring the GIC


1. Disable the GIC distributor

The Interrupt distributor determines which interrupts target which CPU in a multicore system. In a single core system, it can be thought of as a master enable/disable for the GIC. Writing a 0 to the ICDDCR register will disable interrupts from being passed to processor 0, and writing a 1 will enable interrupts.

//disable GIC distributor
void disable_gic_dist() { (ICDDCR = 0); }
2. Configure GIC to drive IRQ in CPU interface Control Register

The ICCICR configures how interrupts are passed to the processor from the GIC; to enable the GIC to drive the IRQ interrupt signal, set the lower two bits in the ICCICR.

//Drive IRQ from the GIC
void disable_IRQ_passthrough() { (ICCICR =0x3); }
3. Configure the Priority Mask

The ARM supports interrupts with priorities from 0-255 (0 is the highest priority, 255 is the lowest). The interrupt priority mask register (ICCPMR) sets the current CPU priority level. Pending interrupts with priorities lower than the value in this register will not be taken. Setting the ICCPMR to 255 will allow all interrupts to be taken.

//sets the GIC priority mask to ‘priority’
void set_gic_pmr(uint32_t priority) { ICCPMR = (priority & 0xFF); }
4.Configure interrupt sensitivity and priority

Before making changes to a specific interrupt’s configuration registers, the interrupt should be disabled by writing to the ICDIPTR and ICDICER registers. The ICDIPTR registers each have 16 2-bit wide fields, with each bit enabling the corresponding interrupt for a specific processor (1 to enable, 0 to disable). For single-core devices, only the least significant bit in each pair is relevant. As an example, to disable the GPIO interrupt (ID #52), bit1 and bit0 of ICDIPTR13 should be reset to 0, and bit20 of ICDICER1 should be set to a 1.

//disables interrupt ID 52
void disable_interrupt52(void)
{
	ICDIPTR(13) &= ~0x3; //remove processors from interrupt
	ICDICER(1) = 0x00100000; //disable interrupts from GPIO module
}

Each IP block generates interrupts with particular characteristics. Table 7-4 in the ZYNQ TRM lists interrupt IDs and the required sensitivity (edge or level) for each interrupt. As an example, GPIO interrupts use “High Level” sensitivity, meaning an interrupt will be generated whenever the GPIO IP block’s interrupt request signal is a 1 (this sensitivity is used for most ZYNQ interrupts). These interrupt types will continually generate interrupt requests for as long as they are at a high level, so if only a single interrupt response is desired, the Interrupt Service Routine must take action (like resetting a flag) to ensure multiple interrupts are not generated during the time the interrupt signal is high. The code below sets interrupt 52 for the given sensitivity (passed as a 3-bit value).

//sets interrupt sensitivity of interrupt52 to ‘sens’
void set_interrupt52_sensitivity(uint8_t sens)
{
	ICDICFR(3) &= ~0x300;
	ICDICFR(3) |= (sens&0x3)<<8;
}

Priorities for individual interrupts are set in the ICDIPR registers. There are 24 32-bit ICDIPR registers, and each holds 8-bit priority values for 4 different interrupts. The most-significant 5 bits in each 8-bit field define the priority level, and the least-significant three bits are always 0. Typically, an 8-bit priority is used, but the lower three bits are ignored, so really only 32 priority levels are available (0 is the highest priority, and 255 is the lowest). The example below sets the GPIO interrupt to priority level to 160 (0xA0). Since the lower 3 bits are zero, the value of the upper 5 bits is multiplied by 8 to get the priority level (in this case 5’b10100 (decimal 20) multiplied by 8 is 160).

//sets the interrupt priority of 52 to ‘priority_val’
void set_interrupt27_priority(uint8_t priority_val)
{
	ICDIPR(6) &= ~0xFF000000; //clear priority bits for interrupt 52
	ICDIPR(6) |= ((priority_val) & 0xF8)<<24; //set top 5 bits based on passed value
}

After the configurations have been set, the interrupt can be re-enabled using the ICDIPTR and ICDISER registers. For interrupt ID #52, bit0 in ICDIPTR13 and bit20 in ICDICER1 can be set to a 1.

//enables interrupt 52
void enable_interrupt52()
{
	ICDIPTR(13) |= 1; //set bit 1 of ICDIPTR13 (enable for cpu0)
	ICDISER(1) = 0x00100000;
}
5. Enable CPU Distributor

After the GIC has been configured, the distributor can be re-enabled to pass interrupt signals through to the CPU.

//enables GIC distributor
void enable_gic_dist() { (ICDDCR = 1); }

Summary

The macro definitions and C functions discussed above are presented below for reference.

Macro definitions

//definitions for GIC configuration regisjters
#define ICCPMR	*((uint32_t *) 0xF8F00104)			//Priority mask reg
#define ICCICR	*((uint32_t *) 0xF8F00100)			//CPU interface control reg
#define ICDDCR	*((uint32_t *) 0xF8F01000)			//Distributor Control reg
#define ICDISER(n)	*(((uint32_t *) 0xF8F01100) + n )	//Int. set enable reg
#define ICDICER(n)	*(((uint32_t *) 0xF8F01180) + n )	//Int. Clear Enable reg
#define ICDIPR(n)	*(((uint32_t *) 0xF8F01400) + n )	//Int. Priority reg
#define ICDIPTR(n)	*(((uint32_t *) 0xF8F01800) + n )	//Int. Processor Targets reg
#define ICDICFR(n)	*(((uint32_t *) 0xF8F01C00) + n )	//Int. Configuration reg

C Functions

//disables the GIC distributor
void disable_gic_dist() { (ICDDCR = 0); }

//enables the GIC distributor
void enable_gic_dist() { (ICDDCR = 1); }

//Drive IRQ from the GIC
void disable_IRQ_passthrough() { (ICCICR =0x3); }

//sets the priority mask to the passed value 'priority'
void set_gic_pmr(uint32_t priority) { ICCPMR = (priority & 0xFF); }

//disables interrupt ID 52
void disable_interrupt52(void)
{
	ICDIPTR(13) &= ~0x3; //remove processors from interrupt
	ICDICER(1) = 0x00100000; //disable interrupts from GPIO module
}

//enables interrupt 52
void enable_interrupt52()
{
	ICDIPTR(13) |= 1; //set bit 1 of ICDIPTR13 (enable for cpu0)
	ICDISER(1) = 0x00100000;
}

//sets the interrupt priority of 52 to ‘priority_val’
void set_interrupt52_priority(uint8_t priority_val)
{
	ICDIPR(13) &= ~0xFF; //clear priority bits for interrupt 52
	ICDIPR(13) |= (priority_val) & 0xF8; //set top 5 bits based on passed value
}

//sets interrupt sensitivity of interrupt52 to ‘sens’
void set_interrupt52_sensitivity(uint8_t sens)
{
	ICDICFR(3) &= ~0x300;
	ICDICFR(3) |= (sens&0x3)<<8;
}

The example function below uses the code above to configure the GIC for the single interrupt (ID #52), and enables the GIC.


void configure_GIC_interrupt52(void)
{

	//disable and configure GIC
	disable_gic_dist(); //disable GIC interrupt generation
	disable_IRQ_passthrough();	//drive IRQ from GIC

	set_gic_pmr(0xFF); //set priority mask

	//disable and configure interrupt 52
	disable_interrupt52();
	set_interrupt52_priority(0xA0); //set 52’s priority to 160
	set_interrupt52_sensitivity(1); //set to high level

	enable_interrupt52();	//reenable interrupt52

	enable_gic_dist();		//reenable distributor

}

This example has used interrupt #52 (from the GPIO block). To configure a different interrupt, the corresponding registers for the interrupt would need configured following the example code provided. The GIC supports multiple simultaneous interrupts, so to add interrupts in addition to the GPIO interrupt #52, statements to modify the additional registers can be added to the code above.

Setting up an Interrupt Handler

The interrupt handler executes immediately after an interrupt is taken; an interrupt service routine (ISR) is a separate program, called by the handler, that deals with a particular interrupt (in simple cases where only one or two interrupts are serviced, the handler and service routine may be combined). The interrupt handler is entered directly from the IRQ vector, and it uses a switching statement to branch to a particular ISR. Many tools and environments provide a default/basic handler that is linked to the vector table, so users don’t need to setup and link to vectors directly.

The SDK tool automatically places an ARM vector table in memory, and provides labels for the vectors that can be used as branch points. Below is an example of the disassembled default vector table produced by the SDK tool. Note the entry at address 100018, which is the IRQ vector. It is a branch to an “IRQHandler” label at address 100020. The IRQHandler will execute whenever IRQ is asserted, and the disassembled code is also shown.

00100000 <_vector_table>:
  100000:	ea000049 	b	10012c <_boot>
  100004:	ea000025 	b	1000a0 <Undefined>
  100008:	ea00002b 	b	1000bc <SVCHandler>
  10000c:	ea00003b 	b	100100 <PrefetchAbortHandler>
  100010:	ea000032 	b	1000e0 <DataAbortHandler>
  100014:	e320f000 	nop	{0}
  100018:	ea000000 	b	100020 <IRQHandler>
  10001c:	ea00000f 	b	100060 <FIQHandler>
00100020 <IRQHandler>:
  100020:	e92d500f 	push	{r0, r1, r2, r3, ip, lr}
  100024:	ed2d0b10 	vpush	{d0-d7}
  100028:	ed6d0b20 	vpush	{d16-d31}
  10002c:	eef11a10 	vmrs	r1, fpscr
  100030:	e52d1004 	push	{r1}		; (str r1, [sp, #-4]!)
  100034:	eef81a10 	vmrs	r1, fpexc
  100038:	e52d1004 	push	{r1}		; (str r1, [sp, #-4]!)
  10003c:	eb0001a2 	bl	1006cc <IRQInterrupt>
  100040:	e49d1004 	pop	{r1}		; (ldr r1, [sp], #4)
  100044:	eee81a10 	vmsr	fpexc, r1
  100048:	e49d1004 	pop	{r1}		; (ldr r1, [sp], #4)
  10004c:	eee11a10 	vmsr	fpscr, r1
  100050:	ecfd0b20 	vpop	{d16-d31}
  100054:	ecbd0b10 	vpop	{d0-d7}
  100058:	e8bd500f 	pop	{r0, r1, r2, r3, ip, lr}
  10005c:	e25ef004 	subs	pc, lr, #4

The IRQHandler preserves some context (instructions at 100020 – 100038), then branches to another label called IRQInterrupt. On return, the IRQHandler restores context and returns to the calling program (the instruction “subs pc, lr, #4” pulls the return address from the “shadowed” link register). The IRQInterrupt label is also defined by SDK, as well as the small program at that label. The IRQInterrupt code simply branches to a base address loaded into R3 (0xBC4), plus an offset of 0x28. The SDK tool places a software vector table called “XExc_VectorTable” at address 0xBC4, and the entries in that vector table can easily be modified using software libraries provided by Xilinx.

001006cc <IRQInterrupt>:
  1006cc:	e3003bc4 	movw	r3, #3012	; 0xbc4
  1006d0:	e3403010 	movt	r3, #16
  1006d4:	e5932028 	ldr	r2, [r3, #40]	; 0x28
  1006d8:	e593002c 	ldr	r0, [r3, #44]	; 0x2c
  1006dc:	e12fff12 	bx	r2

The XExc_VectorTable vector table that resides at address 0xBC4 is shown below, this time in C. The fifth entry in the table (starting from 0) is at offset 0x28, and it’s a function called Xil_ExceptionNullHandler. You can replace that function with your own, by using a function from the xil_exception.c file. If you include xil_exception.c in your source, you can access the function below to insert/use your own handler:

void Xil_exceptionRegisterHandler(u32 Exception_id, Xil_ExceptionHandler Handler, void *Data)

This function takes 3 arguments: the exception number, the exception vector (i.e., the name of your exception handler), and any data that may accompany the vector. For registering an IRQ handler, the “Exception_ID” is 5 (for the fifth table entry). The “Xil_ExceptionHandler_Handler” vector name can be replaced with the name of the handler function you write, and the last argument can just be the keyword “NULL” since no data need be passed.

XExc_VectorTableEntry XExc_VectorTable[XIL_EXCEPTION_ID_LAST + 1] =
{
	{Xil_ExceptionNullHandler, NULL},	//offset 0x0, 0x4
	{Xil_UndefinedExceptionHandler, NULL},	//offset 0x8, 0xC
	{Xil_ExceptionNullHandler, NULL},	//offset 0x10, 0x14
	{Xil_PrefetchAbortHandler, NULL},	//offset 0x18, 0x1C
	{Xil_DataAbortHandler, NULL},		//offset 0x20, 0x24
	{Xil_ExceptionNullHandler, NULL},	//offset 0x28, 0x2C
	{Xil_ExceptionNullHandler, NULL},	//offset 0x30, 0x34
};

The example below illustrates.

//include definitions for xilinx's vector table and functions relevant to it
#include "xil_exception.h"	

void interrupt_handler(void);	//prototype for handler (code needs to be written)

void register_my_irq_handler(void) //registers the above defined function to xilinx's table
{
	//this register's the interrupt handler to the 6th entry in Xilinx's table
	Xil_ExceptionRegisterHandler(5, interrupt_handler, NULL);

}

Configuring Peripheral Circuits (IP blocks) to generate interrupts

Prior to using interrupts, the IP blocks that generate the interrupt signals must be properly configured. The IP blocks all use configuration registers that enable the creation and forwarding of signals that can generate interrupts – refer to the IP block documentation to learn which registers and which bits are relevant. Note that some signals from some IP blocks must be manually reset, typically by reading or writing a given register - if the interrupt-generating signals are not reset when an interrupt is serviced, new interrupts will be continuously generated.

After configuring the IP block to generate interrupt signals, the GIC must also be configured to enable/forward interrupts to the processor. The procedures discussed above can be followed to program the GIC for any interrupt source (you will need the interrupt ID#, and the polarity and sensitivity of the interrupt signal - see table 7-4 on page 230 of the ZYNQ TRM). A general procedure for configuring an interrupt from any given IP block might be:

  • Write an interrupt handler (and/or ISR) to deal with the interrupt, and be sure to manage any status or flag bits in the IP block as necessary;
  • Register the interrupt handler using the Xilinx function discussed above;
  • Disable the IP block’s output, typically using bits in the block’s configuration (and/or interrupt configuration) register;
  • Configure the required options in the IP block (e.g., set comparator values for timers);
  • Configure the GIC for the given interrupt ID#;
  • Enable the IP block to generate the desired interrupt signals.

Setting up interrupts for the Global Timer can serve as an example. First, write an ISR to deal with the timer interrupt. For example, if you will increment a clock display based on a timer tick, write that function, and be sure your function clears the timer’s event flag bit by writing a 1 to the event flag register. Call the Xilinx function to register your ISR so that it is called when a timer event occurs.

When you have an ISR, configure the ARM system to call it at the appropriate time. Disable the timer and its outputs by setting the appropriate bits in the timer’s control register (the ID, CE, and EN bits). Then, to generate a single interrupt at a specific timer value, program the desired comparator value into the timer’s COMP registers; to generate periodic interrupts, set the AI (auto-increment) bit to a 1, and program the AI register with an auto-increment value. Next, re-enable the timer by setting the IE (interrupt enable), CE (comparator enable) and EN (enable) bits in the timer’s CR register to a 1. Then configure the GIC to enable/forward interrupts from the timer block (ID #27) using a positive edge sensitive interrupt signal. Finally, to verify your interrupts are working, write and run a background program that can be interrupted to check your code.