ARM SYSTEM IP · PRESENTATION 01

GIC — the Generic Interrupt Controller

The interrupt brain of every Cortex-A and Neoverse SoC
GICv2 · GICv3 · GICv4 · GIC-400 / 500 / 600 / 700 · SPI · PPI · SGI · LPI · ITS · vGIC
02

Why A-profile Has a Separate Interrupt Controller

  • Cortex-M has NVIC integrated in the core — simple, deterministic, phone/MCU-sized.
  • Cortex-A needs to route interrupts across:
    • Many cores (1-128+)
    • Multiple Exception Levels (EL0-EL3)
    • Two security states (Secure / Non-secure)
    • Virtualized guests (vGIC)
  • Scale + complexity = a dedicated architectural interrupt controller IP, the GIC.
  • Every A-profile OS (Linux, XNU, Windows) drives the GIC via a standard memory-mapped + system-register interface. Kernel has one GIC driver, not one per silicon vendor.

An architectural vs IP distinction

"GIC" can mean two things: GIC Architecture (spec, e.g. GICv3) or GIC IP block (Arm-sold IP, e.g. GIC-600). An Arm A-profile SoC must implement the architecture; most use the Arm IP, but some implement their own (AWS Graviton 1 did).

SBSA requires GICv3+

From SBSA Level 3 (2016), Arm server platforms must have GICv3 or later. GICv2 is extinct on server silicon.

03

GIC Generations

VersionYearMax INTIDsMax coresKey features
GICv1200410208Original; Cortex-A9/A15 classic
GICv2201110208Virtualization extensions (optional); MMIO-only
GICv32014> 1 M (LPIs)UnboundedAffinity routing, MSIs (ITS), LPIs, system registers (ICC_*_EL1)
GICv42016> 1 MUnboundedDirect injection of vLPIs to guest vCPUs — massive KVM speedup
GICv4.12019> 1 MUnboundedDirect injection of vSGIs (inter-processor) for guest-to-guest fast path

GICv3 was the biggest shift: the distributor + per-CPU redistributor + system-register interface model. GICv4.x adds direct hardware paths for virt IRQs without hypervisor round-trips.

04

Interrupt Types — SPI / PPI / SGI / LPI

AbbrevFull nameINTID rangeSemantics
SGISoftware-Generated Interrupt0-15Inter-processor interrupts (IPI) — core → core
PPIPrivate Peripheral Interrupt16-31Per-core interrupt (e.g., per-core timer, PMU overflow)
SPIShared Peripheral Interrupt32-1019Global device interrupt — UART, Ethernet, SATA, fabric-level
LPILocality-specific Peripheral Interrupt8192+GICv3+ only; used for MSI/MSI-X (1 M+ supported)
Extended SPIExtended SPI (GICv3.1)4096-5119Additional 1024 SPIs for big systems
Extended PPIExtended PPI (GICv3.1)1056-1119Additional 64 PPIs per core

SGI + PPI + SPI come from the Distributor (and its redistributor partners). LPIs come from the ITS (Interrupt Translation Service), which translates an EventID + DeviceID from a PCIe MSI write into an LPI number + target redistributor.

05

GICv3 Architecture

GICv3 — one Distributor + per-CPU Redistributor Distributor GICD_* MMIO RD-0 GICR_* RD-1 RD-2 ... CPU 0 CPU 1 CPU 2 ... CPU ↔ RD via ICC_*_EL1 system registers
  • Distributor — global state for SPIs; enable/priority/target routing tables.
  • Redistributors — one per CPU; hold PPI + SGI + LPI state for that CPU.
  • CPU interface — in GICv3, accessed via system registers (ICC_IAR1_EL1, ICC_EOIR1_EL1, ICC_PMR_EL1), not MMIO. Much faster.
  • Affinity routing — each core has a 32-bit Aff3.Aff2.Aff1.Aff0; interrupts are routed to targets by affinity tuple.
06

An Interrupt's Journey (SPI from a UART)

  1. UART raises its level interrupt line → Distributor.
  2. Distributor marks the SPI Pending; consults target routing (GICD_IROUTER<n>) to pick a CPU affinity.
  3. Distributor forwards to target CPU's redistributor.
  4. Redistributor raises CPU's IRQ (or FIQ) line.
  5. CPU enters exception at lower-EL synchronous IRQ vector of EL1 (or EL2 / EL3 depending on routing).
  6. Handler reads ICC_IAR1_EL1 → gets INTID and acknowledges.
  7. Handler services device, clears device IRQ.
  8. Handler writes ICC_EOIR1_EL1 → GIC moves INT from Active to Inactive.
// Linux arm64 IRQ entry (simplified)

el1_irq:
  kernel_entry 1, irq
  irq_handler_entry 1

  // inside handler -> GIC driver
  mrs  x0, icc_iar1_el1          // ack interrupt
  // lower 24 bits = INTID
  ubfx x1, x0, #0, #24

  bl   generic_handle_irq_by_intid

  msr  icc_eoir1_el1, x0          // end-of-interrupt

  kernel_exit 1
07

Priority & Preemption

  • GIC supports up to 256 priority values (8 bits); many implementations expose 5-8 bits.
  • Each INTID has a priority register (GICD_IPRIORITYR<n> for SPI, GICR_IPRIORITYR<n> for PPI/SGI).
  • The Priority Mask Register (ICC_PMR_EL1) blocks interrupts below a given priority at the CPU interface.
  • The Binary Point Register (ICC_BPR1_EL1) splits the priority field into group vs sub-priority — only group differences preempt.
  • Interrupts with higher priority (lower numeric value) preempt running lower-priority handlers — similar to Cortex-M NVIC but at much larger scale.

Two groups

Group 0 = Secure IRQ → routed as FIQ to the Secure world (EL3). Group 1 = Non-secure IRQ → routed as IRQ to EL1/EL2. Separate group-specific ICC_IAR0/ICC_IAR1/EOIR registers.

Compare to Cortex-M NVIC

NVIC: fixed 8-bit priority field, up to 256 exceptions, fully automatic tail-chaining, lazy FP stacking. GIC is larger (SPIs/LPIs) but lacks tail-chaining — the software model is different.

08

ITS — Interrupt Translation Service

  • Problem: PCIe MSI/MSI-X devices write to a memory address to signal an interrupt. The CPU needs to turn that write into an LPI pointed at a specific core.
  • GICv3 introduced the ITS to do exactly this, in hardware.
  • PCIe device writes to ITS's GITS_TRANSLATER register, supplying an EventID.
  • ITS looks up DeviceID (from AXI Requester ID) + EventID in its tables → produces (LPI ID, target CPU).
  • ITS injects LPI into the target redistributor → CPU IRQ.
  • Tables are in DRAM, programmed by firmware/OS: Device Table, Collection Table, Interrupt Translation Table.
// ITS lookup (simplified)

PCIe MSI write: DeviceID + EventID
   |
   v
ITS Device Table[DeviceID]
   -> pointer to this device's ITT
   |
   v
ITS Interrupt Translation Table[EventID]
   -> { LPI ID, Collection ID }
   |
   v
ITS Collection Table[Collection ID]
   -> target Redistributor (CPU)
   |
   v
Redistributor raises LPI->CPU

ITS commands (MAPD, MAPI, MAPC, SYNC, INV) are issued via a DRAM-resident command queue. Linux GIC-ITS driver sends these to set up a device's MSI routing.

09

Virtualization — vGIC

  • A guest at EL1 believes it has its own GIC. The hypervisor at EL2 arranges this via the vGIC:
    • Guest accesses to ICC_*_EL1 registers are trapped (HCR_EL2 settings) and remapped to virtual state
    • Guest IRQ delivery uses a virtual CPU interface (ICH_*_EL2 registers)
    • Up to 16 list registers (ICH_LR<n>_EL2) hold guest-pending interrupts
  • GICv2 had rudimentary vGIC (optional); GICv3 made it mandatory.
  • GICv4 adds direct injection of vLPIs — the ITS can target a guest's redistributor directly, no hypervisor trap. Huge for I/O-heavy VMs.
  • GICv4.1 adds direct injection of vSGIs — guest-to-guest IPIs also skip the hypervisor.

Why vGIC matters

Without direct injection, every interrupt delivered to a guest costs ~2× EL1↔EL2 switches (~2000 cycles). GICv4 vLPI direct injection takes this to zero — huge for DPU / SR-IOV NICs with 100k+ IRQs/sec.

Nested virt note

Under nested virtualization (guest hypervisor inside a host hypervisor), vGIC gets an extra level. GICv3 handles this; GICv4 direct injection does not yet apply to nested guests.

10

GIC IP Products

IPArchYearMax coresMax SPIsITSTarget
GIC-400GICv220128480NoPhone SoC (A53/A57-era)
GIC-500GICv32014128960YesFirst GICv3 IP, servers + high-end phones
GIC-600GICv3.12016Unbounded> 1000 + ESPIYes, multipleServer-class; multi-chip support
GIC-600AEGICv32019--YesAutomotive ASIL-D
GIC-700GICv4.12022Unbounded> 4000YesModern server + Neoverse Gen 2/3

Modern Cortex-A phones typically use GIC-600 or GIC-700. Every Neoverse-based server uses GIC-600 or GIC-700. GIC-400 is legacy (A72-era and older mobile).

11

Power Management — PPI Timers & Wake-up

  • Every CPU has a PPI-timer interrupt (PPI 30 — CNTP physical EL1 timer; PPI 27 for EL1 virtual; PPI 26 for EL2 timer).
  • These are delivered per core via the redistributor — no cross-CPU traffic.
  • Timers + CPU idle states coordinate via PSCI: OS tells TF-A/EL3 to put a core to sleep; GIC retains interrupt pending state so the core wakes on IRQ.
  • The GIC wake-up interface — redistributor can be quiesced to save power but must remain responsive to incoming interrupts that will wake the core.
  • GIC itself has multiple power domains — on GIC-600/700 the distributor and redistributors can be power-managed independently.

Idle state flow

(1) Linux picks an idle state. (2) Kernel calls cpu_do_idle() → PSCI CPU_SUSPEND. (3) TF-A saves GIC state (via GICR_WAKER) and parks the core. (4) Any pending interrupt at the redistributor un-parks the core. (5) On return, GIC state is restored.

WFI + GIC

When a core runs WFI, the redistributor retains state; the CPU clock gates. Interrupt arrival wakes the core without any software intervention.

12

Secure vs Non-secure Interrupts

  • Each INTID has a Group (G0/G1) and a Non-secure bit.
  • Routing per Group:
    • Group 0 (Secure, unbanked) → FIQ to Secure EL1/EL3
    • Group 1 Non-secure → IRQ to Non-secure EL1/EL2
    • Group 1 Secure → FIQ to Secure EL1 (via SCR_EL3 routing)
  • Banked registers: secure and non-secure worlds each see their own view of most GIC registers. Secure state can see all; non-secure is limited.
  • TF-A / secure monitor at EL3 configures GIC security before handing off to Non-secure.

Why it matters

Secure interrupts (e.g., TrustZone secure timer) must not be visible or serviceable by the Non-secure kernel. The GIC enforces this at the redistributor by Group + Non-secure attribution, not by trusting software.

RME / CCA

With Armv9 RME, GIC-700 is aware of the four worlds. Realm interrupts get their own routing, with enforcement at the redistributor + ITS.

13

GIC and Linux

  • ACPI path: UEFI-booted servers describe GIC topology in the MADT table (distributor base, redistributor bases, ITS bases).
  • Device-tree path: phones + embedded use DT nodes like:
    • compatible = "arm,gic-v3"
    • #interrupt-cells = <3> (type + ID + flags)
  • Linux driver: drivers/irqchip/irq-gic-v3.c — ~3000 LoC, implements everything from IRQ allocation to GICv4 direct injection.
  • IRQ domain abstraction isolates the driver from per-SoC board code.
  • Every interrupt in /proc/interrupts on a Cortex-A Linux box comes from this driver (SPI/PPI/SGI/LPI hierarchical).
// Device-tree GIC-700 excerpt (simplified)
gic: interrupt-controller@40000000 {
  compatible = "arm,gic-v3";
  #interrupt-cells = <3>;
  interrupt-controller;
  ranges;

  reg = <0x4000_0000 0x10000>,     // GICD
        <0x4010_0000 0x200000>;    // GICR ×128

  msi-controller;
  #msi-cells = <1>;

  its: msi-controller@4200000 {
    compatible = "arm,gic-v3-its";
    msi-controller;
    reg = <0x4200_0000 0x20000>;
  };
};

// Device referencing SPI 42 rising-edge:
uart@...  { interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>; };
14

Debugging GIC Issues

  • /proc/interrupts — which IRQs are firing and on which CPUs.
  • /sys/kernel/debug/irq/irqs/<irq> — affinity, edge/level, trigger counts.
  • dmesg | grep GIC — probe messages, redistributor + ITS bring-up.
  • perf counters for irq_handler_entry + irq_handler_exit — latency histograms.
  • Common failure modes:
    • "Interrupt stuck" — usually EOI not issued
    • "Lost wakeup" — priority mask wrong in idle
    • "CPU never takes IRQ" — affinity masks wrong
    • "Guest hangs" — vGIC list-register exhaustion

The "spurious" INTID

INTID 1023 is reserved for "spurious". Returned from ICC_IAR1_EL1 if no interrupt is active. Common cause: interrupt was asserted then de-asserted before the CPU could ack. Handler should just EOI and return.

GIC PMU events

GIC-600 / GIC-700 expose their own PMU — count LPI injections, ITS translations, TLB-like cache hits in the redistributor. Invaluable for DPU / high-IRQ-rate debugging.

15

GIC vs x86 APIC/IOAPIC

ConceptArm GICx86 APIC family
Per-CPU componentRedistributorLocal APIC (LAPIC / x2APIC)
Global componentDistributorIOAPIC (one per host bridge)
MSI routingITSIOAPIC with MSI redirection
IPIsSGI (0-15)ICR IPI messages
Address modelFixed registers + LPI IDsMemory-mapped APIC page
Interrupt #s24-bit INTID (up to 1 M)8-bit vector (256)
VirtualizationArchitectural vGIC; GICv4 direct injectAPICv (post-2010 Intel)

Same conceptual split (per-CPU + global) but architectural specs diverged heavily. GIC's 24-bit INTID space is far larger than x86's 8-bit — necessary for 1000+ PCIe functions with MSI-X vectors.

16

Lessons

  • "Why two halves — Distributor + Redistributors?" → Distributor holds SPI state (global); redistributors hold per-core PPI/SGI/LPI state (private). Scales to thousands of cores because per-core state is distributed, not central.
  • "What is an LPI and why do we need ITS?" → LPI = Locality-specific Peripheral Interrupt, used for PCIe MSI/MSI-X. ITS translates (DeviceID, EventID) → LPI + target CPU, avoiding the SPI namespace (which is too small for 1000s of PCIe functions).
  • "What does GICv4 add?" → direct injection of vLPIs — ITS delivers guest interrupts to guest redistributors without hypervisor trap. GICv4.1 adds vSGI direct injection.
  • "Difference between ICC_IAR1_EL1 and ICC_EOIR1_EL1?" → IAR = Interrupt Acknowledge Register, reads the next pending INTID (moves it Pending → Active). EOIR = End-of-Interrupt, writes back to mark Active → Inactive.
  • "Why Group 0 vs Group 1?" → Group 0 is Secure-side (FIQ to EL3), Group 1 is Non-secure + Secure-EL1 variants. Separates TrustZone OS interrupts from Linux.
  • "Why GICv3 for servers but GIC-400 for old phones?" → GIC-400 (GICv2) tops out at 8 cores and no LPIs — insufficient for server MSI loads. Every modern A-profile system uses GICv3+.
17

References

Arm Ltd. — Arm Generic Interrupt Controller Architecture Specification (IHI 0069 / IHI 0048) — covers GICv2 and GICv3/v4
Arm Ltd. — GIC-400 / 500 / 600 / 700 Technical Reference Manuals
Arm Ltd. — Learn the architecture: GICv3 and GICv4 overview — excellent free intro
Arm DEN 0029 — Server Base System Architecture — what a server must implement
Linux kernel — drivers/irqchip/irq-gic-v3.c, irq-gic-v3-its.c, Documentation/arch/arm64/
Marc Zyngier — KVM/Arm vGIC blog posts + upstream patches (2013-2022)
Arm Community Blog — GICv4.1 direct-inject explanations (2020)
Hotchips — 2019 "Interrupts in Arm SoCs" talk

Presentation built with Reveal.js 4.6 · Saira Semi Condensed + Archivo + Share Tech Mono
Educational use.