Configuring the GIC

Step-by-step procedure for setting up ZYNQ's GIC

1345

Configuring the GIC for GPIO interrupts

Configuring ZYNQ’s ARM processor to properly use interrupts requires multiple steps: first, all interrupts must be disabled by setting the IRQ disable bit in the CPSR to’1’; second, the GIC must be configured by identifying all potential interrupt signals and assigning them a unique ID code and priority; third, each peripheral device that might generate an interrupt needs to be set up to send interrupt requests to the GIC; and fourth, interrupts must be enabled. The following procedural descriptions of functions will guide you through the process for setting up an interrupt that will be generated when an MIO-connected pushbutton is pressed.

The required steps are separated into five functions that together configure the GIC to accept interrupts from the MIO-connected pushbuttons. You can adopt a similar partioning for your code and use the same general functions, or you can rearrange the steps to your taste. Also, the steps presented here work, but there may be other ways complete the required configuration as well. The functions are:

  1. Disabling the Interrupts
  2. Configuring the GIC
  3. Configuring the GPIO interrupt sources
  4. Creating the IRQ Exception Handler
  5. Enabling the Interrupts

1st Function: Disable_ARM_A9_interrupts

The first function (disable_ARM_A9_interrupts) performs two functions: It writes “11111” to the mode bits in CPSR(4:0) to put the ARM in system mode so that protected resources can be accessed; and it writes a ‘1’ to the F and I bits to disable IRQ and FIQ interrupts so that the interrupt system can be initialized. Writing a 0xDF to the lower 8 bits of the CSPR will accomplish both (this also places a ‘0’ in the T bit to disable thumb mode).

Figure 1. ARM CPSR Register
Figure 1. ARM CPSR Register

As a general protection against unintended consequences, most instructions cannot access the CPSR. The special assembly instruction “MRS”, or Move to Register from Special, is one of the few instructions that can read the CPSR: “MRS Rd, CSPR” will transfer the contents of the CSPR into Rd. Likewise, the special assembly instruction “MSR”, or Move to Special register from Register, is one of the few instructions that can write the CPSR: “MRS CPSR, Rd” will transfer the contents of Rd into the CSPR. When manipulating the CSPR (or any other special register), care should be taken to only modify the needed bits, and to leave all other bits unchanged. This can be accomplished by reading the CSPR into temporary storage, clearing the bits in the temporary storage that are to be modified (in this case, the lower 8 bits), writing the modified 8 bits back while leaving the upper 24 bits unchanged, and then writing the temporary storage back into the CSPR.

The function below uses the “asm__volatile” function that allows the needed assembly instructions to be inserted into a C program. Note the CPSR contents are read and stored, and then the stored version is masked, updated, and written back into the CPSR.

void disable_ARM_A9_interrupts(){
	uint32_t mode = 0xDF; // System mode [4:0] and IRQ disabled [7]
	uint32_t read_cpsr=0; // used to read previous CPSR value
	uint32_t bit_mask = 0xFF; // used to clear bottom 8 bits
	__asm__ __volatile__("mrs %0, cpsr\n"  : "=r" (read_cpsr) );
	__asm__ __volatile__("msr cpsr,%0\n" : : "r" ((read_cpsr & (~bit_mask))| mode));
	return;
}

2nd Function: Configure GIC

The second function (configure_GIC) configures the GIC to receive inputs from the MIO GPIO pins and pass them on to the processor. Nine steps involving updates to multiple registers are required. The required registers are defined below, and each is explained following the definitions.

#define ICCPMR_BASEADDR 0xF8F00104 // Interrupt Priority Mask Register
#define ICCICR_BASEADDR 0xF8F00100 // CPU Interface Control Register
#define ICDDCR_BASEADDR 0xF8F01000 // Distributor Control Register
#define ICDISER_BASEADDR 0xF8F01100 // Interrupt Set Enable Register
#define ICDICER_BASEADDR 0xF8F01180 // Interrupt Clear - Enable Register
#define ICDIPR_BASEADDR 0xF8F01400 // Interrupt Priority Register Base address
#define ICDIPTR_BASEADDR 0xF8F01800 // Interrupt Processor Targets Register
#define ICDICFR_BASEADDR 0xF8F01C00 // Interrupt Configuration Register

Step 1: Disable Interrupt Masks and any CPU’s of Handling Interrupts (ICDIPTR and ICDICER)

The first step in configuring the GIC is to disable the GIC from handling GPIO interrupts (GPIO interrupts are assigned ID #52). The GIC uses 24 ICDIPTR registers to enable or disable interrupts from reaching the processor(s), and each of the 24 registers has four 2-bit fields, so a total of 96 interrupts can be enabled or disabled. Interrupt #52 is controlled by the lower 2 bits in ICDIPTR13, so those bits must be cleared (see the Programmers Reference “Generic Interrupt Controller” section). There are also three ICDICER registers that are used to enable or disable interrupt sources. One bit in each register is assigned to each interrupt source (so again, 96 interrupt sources can be turned on or off). The interrupt #52 enable bit falls into bit 20 of the second ICDICER register. The first line of code below will clear all the bits of ICDIPTR13 to disable interrupt #52, and the second line sets bit 20 of ICDICER1 to a ‘1’ to disable the source of interrupt #52.

(Note: Xilinx implemented a GIC that can support multiple processors, so even though the ZYNQ on the Blackboard only has one processor, the 2-bit interrupt enable field in the ICDIRTR register must still be set to “01” to enable interrupts on CPU 0, or to “00” to disable interrupts).

*((uint32_t*) ICDIPTR_BASEADDR+0x34/4) = 0x00000000; // remove processors
*((uint32_t*) ICDICER_BASEADDR+0x04/4) = 0x00100000; // clear GPIO interrupts

Note: how to interpret *((uint32_t *) ICDICER_BASEDDR + 0x04/4)

uint32_t is the C standard type for 32-bit unsigned integers, and unsigned integers are the appropriate type to use for addresses. Placing (uint32_t) in front of a variable typecasts that variable to a 32-bit unsigned integer; placing (uint32_t *) further specifies that the typecasted value should be treated as a pointer.

A pointer is a word-aligned address, so the two least-significant bits cannot contain usable information (they are ignored, and may be thought of as ‘00’, to force word-alignment). Adding ‘1’ to a pointer therefore means adding ‘4’ to the pointer value, to keep word alignment. As an example, line 1 of the following code declares a constant to be used as an address; line 2 typecasts that constant to a pointer, and then uses that pointer to store a value at the defined address; and line three stores another constant in the next memory location. Note that in the third line, ‘1’ was added to the pointer to refer to the next memory location, resulting in address 0x40000004 being used.

#define my_baseaddr = 0x40000000			// my_baseaddr = 0x40000000
*((uint_t *) my_baseaddr ) = 0xAAAAAAAA 		// store AAAAAAAA at address 0x40000000
*((uint_t *) my_baseaddr + 0x01 ) = 0xBBBBBBBB 		// store BBBBBBBB at address 0x40000004

Most data sheets (including the ZYNQ TRM) use hex numbers to specify memory addresses. For example, there are 24 ICDIPTR registers. The first one is located at address 0xF8F01800, the second at 0xF8F01804, the third at 0xF8F01808, etc., and the 13th register is located at 0xF8F01834. If the base address is typecast as a pointer, then you would access these registers as

*((uint_t *) ICDIPTR_BASEADDR) = 0x00000000	// Write all 0’s to the first ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x01) = 0x00000000	// Write all 0’s to the second ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x02) = 0x00000000	// Write all 0’s to the third ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x13) = 0x00000000	// Write all 0’s to the 13th ICDIPTR register

Since the addresses for these registers are listed as hex address, it is common to include the hex address divided by four.

*((uint_t *) ICDIPTR_BASEADDR) = 0x00000000	// Write all 0’s to the first ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x04/4) = 0x00000000	// Write all 0’s to the second ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x08/4) = 0x00000000	// Write all 0’s to the third ICDIPTR register
*((uint_t *) ICDIPTR_BASEADDR + 0x43/4) = 0x00000000	// Write all 0’s to the 13th ICDIPTR register

End Note

Step 2: Disable the Distributor in the Distributor Control Register

Next, the interrupt distributor must be disabled before additional registers can be updated, and before interrupts can be re-enabled. Only the lower two bits in the ICDDCR register are used (bits 31:2 are ignored), so the entire register can be cleared in order to disable the distributor.

*((uint32_t*) ICDDCR_BASEADDR)= 0x0;

Step 3: Set Priority Level in the Interrupt Priority Registers (ICDIPR)

Next, the priority level of the GPIO interrupt (#52) can be set. The GIC uses 24 ICDIPR registers to set priority levels for all the interrupts, with each register holding four byte-wide priority fields for four interrupts (so once again, priorities can be defined for 96 interrupts). When a given interrupt is asserted, its priority setting will be compared with the processors priority setting, and the interrupt will only be taken if it is a higher priority than the processors current setting (the processors setting is defined in the ICCPMR register discussed below). The upper five bits in each byte define the priority level. The priority level for interrupt #52 is set by the lowest byte of the ICDIPR13 register. In the code below, priority level 160 is defined (how?), but really any priority level would be OK since no other interrupts have been enabled to compete for priority (0 is the highest priority, and 255 is the lowest).

*((uint32_t*) ICDIPR_BASEADDR+0x34/4) = 0x000000A0; 

Step 4: Configure Interrupts in the Processor Targets Registers (ICDIPTR)

Now that the priority level is set, you can re-enable the #52 interrupt to reach CPU0 by writing a “01” into the lowest two bits of the ICDIPTR13 register (the same register we cleared earlier):

*((uint32_t*) ICDIPTR_BASEADDR+0x34/4) = 0x00000001; //

Step 5: Set Interrupt Sensitivity in Interrupt Configuration Register (ICDICFR)

Next, the “sensitivity” of the interrupt (rising-edge sensitive or level sensitive) can be can be configured by writing bit fields in the ICDICFR registers. There are six ICDICFR registers, and each register uses two bits to define sensitivity. Register ICDICFR3 holds the bits to define sensitivity for interrupt #52 (bits 9:8), and writing them to “01” will define a level-sensitive signal (“11” defines an edge-triggered signal, and that would work in this application as well).

*((uint32_t*) ICDICFR_BASEADDR+0x00000C/4) = 0x00000100;

Step 6: Enable Interrupts in the Interrupt Set-Enable Register (ICDISER)

The GIC also requires that a “set-enable” bit be set for each interrupt that will be forwarded to the CPU interface. Three 32-bit ICDISER registers hold the bits, with one bit per interrupt signal. The bit for enabling interrupt #52 is located in the 20th bit of ICDISER1:

*((uint32_t*) ICDISER_BASEADDR+0x04/4) = 0x00100000;

Step 7: Enable All Priority Levels in the Interrupt Priority Mask Register (ICCPMR)

Now we can set the processor’s priority level by writing an 8-bit value to the ICCPMR register. The value in this priority-level register is compared to the priority levels defined for each individual interrupt, and only interrupts with higher priority will be taken (0 is the highest priority, and 255 is the lowest). In this example, you can write any value larger than the value set for the GPIO interrupt (so, 160 or larger).

*((uint32_t*) ICCPMR_BASEADDR) = 0xF0;

Step 8: Enable Interrupts in the CPU Interface Control Register (ICCICR)

The CPU Interface Control Register (ICCICR) contains 5 bits that must be set correctly: bit 0 = ‘1’ to enable signaling of secure interrupts; bit 1 =’1’ to allow software to enable non-secure interrupts; bit 2 = ‘0’ so a secure read of the ICCIAR causes a CPU acknowledge; bit 3 = 0 to use IRQ’ and bit 4 = ‘0’ to use secure binary point register (bits 31:5 are not used). The meaning of these bits and functions will be described later.

*((uint32_t*) ICCICR_BASEADDR) = 0x3;

Step 9: Enable Distributor in the Distributor Control Register (ICDDCR)

Finally, the interrupt distributor that you disabled earlier can be re-enabled:

*((uint32_t*) ICDDCR_BASEADDR) = 0x1;

The configure_GIC function should look something like this:

void configure_GIC(){
	*((uint32_t*) ICDIPTR_BASEADDR+0x34/4) = 0x00000000; // Step 1
	*((uint32_t*) ICDICER_BASEDDR+0x04/4) = 0x00000000; // Step 1
	*((uint32_t*) ICDDCR_BASEADDR) = 0x0; // Step 2
	*((uint32_t*) ICDIPR_BASEADDR+0x34/4) = 0x000000A0; // Step 3
	*((uint32_t*) ICDIPTR_BASEADDR+0x0000034/4) = 0x00000001; // Step 4
	*((uint32_t*) ICDICFR_BASEADDR+0x00000C/4) = 0x55555555; // Step 5
	*((uint32_t *) ICDISER_BASEADDR+0x4/4) = 0x00100000; // Step 6
	*((uint32_t*) ICCPMR_BASEADDR) = 0xFF; // Step 7
	*((uint32_t*) ICCICR_BASEADDR) = 0x3; // Step 8
	*((uint32_t*) ICDDCR_BASEADDR) = 0x1; // Step 9
	return;
}

3rd function: Configuring Interrupts on the GPIOs

After the GIC and the GPIO pins are properly configured, you must still configure the GPIOs to generate interrupts so that when a pushbutton is pressed, an interrupt is generated. Setting up the GPIOs to generate interrupts involves six steps, with multiple accesses to the several registers defined here:

#define GPIO_INT_DIS_0 0xE000A214 // Interrupt disable bank 0
#define GPIO_INT_EN_1 0xE000A250 // Interrupt enable bank 1
#define GPIO_INT_DIS_1 0xE000A254 // Interrupt disable bank 1
#define GPIO_INT_STAT_1 0xE000A258 // Interrupt status bank 1
#define GPIO_INT_TYPE_1 0xE000A25C // Interrupt type bank 1
#define GPIO_INT_POL_1 0xE000A260 // Interrupt polarity bank 1
#define GPIO_INT_ANY_1 0xE000A264 // Interrupt any edge sensitive bank 1

Step 1: Disable All Interrupts in GPIO Banks 0 and 1

As a first step, GPIO interrupts must be disabled during the time you are configuring the various interrupt settings. Interrupts can be disabled by writing all 1’s to the GPIO_INT_DIS_x interrupt disable registers at 0xE000A21 and 0xE000A25 :

*((uint32_t*) GPIO_INT_DIS_0) = 0xFFFFFFFF;
*((uint32_t*) GPIO_INT_DIS_1) = 0xFFFFFFFF;

Step 2: Clear Interrupt Status Register

Next, clear the interrupt status register responsible for storing previous interrupt signals for pins 50 and 51 (these are the two pins the pushbuttons are connected to). Writing all 1’s to GPIO_INT_STAT_1 at 0xE000A258 will clear all previous interrupt signals:

*((uint32_t*) GPIO_INT_STAT_1) = 0xFFFFFFFF;

Step 3: Set the Interrupt Type

Then you can define the “sensitivity type” of the interrupt signal (rising-edge sensitive or level sensitive) by writing bits in the GPIO_INT_TYPE_1 register. In this case, define rising edge sensitive interrupts by writing all 0’s to the GPIO_INT_TYPE_1 register:

*((uint32_t*) GPIO_INT_TYPE_1) = 0x0C0000;

Step 4: Set Interrupt Polarity

The interrupt signal polarity (active high or rising edge, or active low or falling edge) also needs to be defined by writing to the GPIO_INT_POL_1 register. Since you are defining interrupts for active-high push buttons on MIO pins 50 and 51, you can write 1’s to the corresponding bit positions in the GPIO_INT_POL_1 register:

*((uint32_t*) GPIO_INT_POL_1) = 0x0C0000;

Step 5: Set Interrupt Sensitivity

When edge triggering is enabled, interrupts can trigger on the rising edge, falling edge, or both edges. The GPIO_INT_ANY registers can configure an edge-triggered interrupt to trigger on a single edge or both edges. This configuration setting is ignored when level triggering is enabled (so far, this project is using level triggering, but edge triggering will be used later).

*((uint32_t*) GPIO_INT_ANY_1) = 0x00000000;  // trigger all on selected edge only
*((uint32_t*) GPIO_INT_ANY_1) = 0xFFFFFFFF;  // trigger all on both edges

Step 6: Enable BTN4 and BTN5 Interrupts

Finally, you can enable the interrupts for the two buttons by writing 1’s to the appropriate bits in the GPIO_INT_EN_1 register. Pins 50 and 51 are enabled by writing 1’s to bits 18 and 19 of the register:

*((uint32_t*) GPIO_INT_EN_1) = 0xC0000; // enable interrupts

All of these statements are placed in a single function as shown.

void Initialize_GPIO_Interrupts(){
	*((uint32_t*) GPIO_INT_DIS_1) = 0xFFFFFFFF;
	*((uint32_t*) GPIO_INT_DIS_0) = 0xFFFFFFFF;
	*((uint32_t*) GPIO_INT_STAT_1) = 0xFFFFFFFF; // Clear Status register
	*((uint32_t*) GPIO_INT_TYPE_1) = 0x0C0000; // Type of interrupt rising edge
	*((uint32_t*) GPIO_INT_POL_1) = 0x0C0000; // Polarity of interrupt
	*((uint32_t*) GPIO_INT_ANY_1) = 0x000000; // Interrupt any edge sensitivity
	*((uint32_t*) GPIO_INT_EN_1) = 0x0C0000; // Enable interrupts in bank 0
}

Now, whenever one of the buttons is pressed, the processor will receive the IRQ interrupt. Normal processing will be halted immediately, the IRQ vector will be loaded into the PC, and from there a branch instruction will direct the program to an interrupt handler. Now, you must write that handler. But first, you should be familiar with the vector table used by the GIC (after this breif explanation, the IRQ handler will be presented).

Note: ZYNQ’s Vector Table

When preparing executable object files based on your assembly or C source files, most CAD tool environments will include “boiler plate” code along with your source files to properly configure the processing environment. This automatically-included code takes care of system-level configurations for you, so you don’t need to always be aware of all of the details. At some point, you should be aware of everything that is being done by the tools on your behalf, because in the end, you are responsible for whatever code you produce.

The SDK tool defines a vector table for you. The code below is taken from the asm_vectors.S file that is produced by Xilinx and automatically included in your project directory. Note the second to last table entry for the IRQ vector – the table entry is a branch to the “IRQHandler” label. That label appears lower in the same asm_vectors.S source file. The IRQHandler is a fairly small subprogram that does some context switching and then executes a “bl IRQInterrupt” instruction.

.globl _vector_table

.section .vectors
_vector_table:
	B	_boot
	B	Undefined
	B	SVCHandler
	B	PrefetchAbortHandler
	B	DataAbortHandler
	NOP	/* Placeholder for address exception vector*/
	B	IRQHandler
	B	FIQHandler 

IRQHandler:					/* IRQ vector handler */

	stmdb	sp!,{r0-r3,r12,lr}		/* state save from compiled code*/
#ifdef __ARM_NEON__
	vpush {d0-d7}
	vpush {d16-d31}
	vmrs r1, FPSCR
	push {r1}
	vmrs r1, FPEXC
	push {r1}
#endif

#ifdef PROFILING
	ldr	r2, =prof_pc
	subs	r3, lr, #0
	str	r3, [r2]
#endif

	bl	IRQInterrupt			/* IRQ vector */

This code is assembled and linked with your source file, and included in the .elf executable file produced for your project (you can examine the .elf file in a source window in SDK). The code below shows the very start of the .elf file – note the first few lines define the vector table and the IRQHandler.

Disassembly of section .text:

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}

The last line of the asm_vectors.S file shown above branches to the “IRQInterrupt” function. That function is also produced by Xilinx and is automatically included in your project, but it is located in a different source file (vectors.c). The IRCInterrupt function, together with some definitions in include files, make it possible for you to write your own IRQ handler program, and have it execute whenever an IRQ is taken. You must include the “xil_exception.h” file by referencing it in your source file, and then you can include the function call:

Xil_ExceptionRegisterHandler(5, IRQ_Handler, NULL);

The function name “Xil_ExceptionRegisterHandler” is the name of the function in the include file, so it will be avaialble to you if you include the line “include xil_exception.h” in your source file. The parameters define the IRQ handler: the number 5 defines the 5th entry in the vector table (i.e., the IRQ interrupt); the name “IRQ_Handler” is the name of the handler you will write (see below); and NULL is the return value. Then in your source file, you can write the function

void IRQ_Handler(void *data){
	/* Handle the interrupt here */
}

End Note

Fourth function: The IRQ Handler

The IRQ_Handler will be called every time the IRQ signal is asserted, regardless of the source of the interrupt. This section discusses what goes into the interrupt handler.

Since the IRQ_Handler is called any time an IRQ interrupt occurs, it must do several things: determine the ID# of the interrupt source that caused the IRQ to be asserted (in our case, the GPIO interrupt #52); determine which GPIO signal caused the interrupt (in our case, button 4 or button 5); deal with the particular problem requirements (in our case, turning on an LED); and finally, acknowledging to the GIC that the interrupt was taken sucessfully. Several registers are used:

#define ICCIAR_BASEADDR 0xF8F0010C // Interrupt Acknowledge Register
#define ICCEOIR_BASEADDR 0xF8F00110 // End Of Interrupt Register
#define GPIO_MTDATA_OUT_0 0xE000A004 // Maskable data out in bank 0

The interrupt ID# can be read from the Interrupt Acknowledge Register (ICCIAR – see the Programmers Reference for more information). The following instruction will load the interrupt ID# in the lower 10 bits of the interrupt_ID variable.

uint32_t interrupt_ID = *((uint32_t*)ICCIAR_BASEADDR);

Next, you can determine which GPIO signal caused the interrupt by reading the GPIO_INT_STAT_1 register that you defined earlier. The following two lines read the contents of GPIO_INT_STAT_1 into the GPIO_INT variable, and then mask (i.e., bit-wise AND) the contents with 0x000C0000 to leave the variable with only the information resulting from the MIO pins 50 and 51 (the two pushbuttons). After these two instructions, the variable button_press will contain at most two 1’s – a 1 in bit position 19 means MIO pin 50 (button4) was asserted, and a 1 in bit position 20 means MIO pin 51 (button 5) was asserted.

uint32_t GPIO_INT = *((uint32_t*)GPIO_INT_STAT_1);
uint32_t button_press = 0xC0000 & GPIO_INT;

You are creating this IRQ handler to detect when a pushbutton is pressed, and then to turn on an LED when it is. You now have the button press information, so all this is left is turning on the LEDs.

The ZYNQ device defines several registers that can be accessed to read and write logic values on the MIO pins. In particular, the GPIO_MTDATA_OUT_0 register allows writes to MIO pins [31:16]. This register is divided into two 16-bit sections – the upper 16 bits (31:16) are mask data, and the lower 16 bits are the data to be written (see the ZYNQ TRM pages 1349-1350). This feature allows you to define only certain bits as active outputs, so you can write data to the register without effecting the masked-off bits. A ‘1’ placed in the upper 16 bits will mask the corresponding MIO pin, and prevent it from being updated (and a ‘0’ will allow it to be updated). To write data to MIO pins 16, 17, and 18 (these are the pins connected to the RGB LED), the following code can be used. Make sure you understand what each instruction (and in particular the bit fields) are doing.

if (interrupt_ID == 52) { // check if interrupt is from the GPIO
	if (button_press == 0x80000){
		*((uint32_t*) GPIO_MTDATA_OUT_0) = 0xFFF80001;
	}
	else if (button_press == 0x40000){
		*((uint32_t*) GPIO_MTDATA_OUT_0) = 0xFFF80002;
	}
}

All that is left to do is acknowledge the interrupt and clear the status register bits. You can clear the status register bits by writing all 1’s to the GPIO_INT_STAT_1 register, and you can acknowledge the interrupt by writing the interrupt_ID variable that you read earlier from the ICCIAR register into the “End of Interrupt” register:

*((uint32_t*)GPIO_INT_STAT_1) = 0xFFFFFF;
*((uint32_t*)ICCEOIR_BASEADDR) = interrupt_ID;

Your final code should look something like the following:

void IRQ_Handler(void *data){
	uint32_t GPIO_INT = *((uint32_t*)GPIO_INT_STAT_1);
	uint32_t interrupt_ID = *((uint32_t*)ICCIAR_BASEADDR);
	uint32_t button_press = 0xC0000 & GPIO_INT;
	if (interrupt_ID == 52) { // check if interrupt is from the GPIO
		if (button_press == 0x80000){
			*((uint32_t*) GPIO_MTDATA_OUT_0) = 0xFFF80001;
		}
		else if (button_press == 0x40000){
			*((uint32_t*) GPIO_MTDATA_OUT_0) = 0xFFF80002;
		}
	}
	*((uint32_t*)GPIO_INT_STAT_1) = 0xFFFFFF;
	*((uint32_t*)ICCEOIR_BASEADDR) = interrupt_ID;
}

Fifth function: Enabling interrupts

The final step is to enable the interrupts by setting the IRQ bit in the CPSR to ‘0’. This bit of code is entirely similar to the code to disable interrupts – just the one bit value is changed.

void enable_ARM_A9_interrupts(){
	uint32_t read_cpsr=0; // used to read previous CPSR value
	uint32_t mode = 0x5F; // System mode [4:0] and IRQ enabled [7]
	uint32_t bit_mask = 0xFF; // used to clear bottom 8 bits
	__asm__ __volatile__("mrs	%0, cpsr\n"  : "=r" (read_cpsr) );
	__asm__ __volatile__("msr	cpsr,%0\n" : : "r" ((read_cpsr & (~bit_mask))| mode));
	return;
}