PCIe Series — PCIe-24: Interrupts — INTx, MSI, and MSI-X — VLSI Trainers
PCIe Series · PCIe-24

Interrupts — INTx, MSI, and MSI-X

Three interrupt mechanisms in one post — how legacy PCI INTx pins became virtual in-band TLPs, how MSI replaces pins with a single memory write, how MSI-X gives each queue its own independent vector with per-CPU targeting, and exactly which to use and when.

📋 Why Interrupts Exist in PCIe

A PCIe device needs to notify the CPU when an event requires attention — a DMA transfer completed, a new packet arrived, an error occurred, a queue is empty. Without interrupts, the CPU would have to poll device status registers continuously, wasting cycles even when nothing is happening.

PCIe supports three interrupt mechanisms, each a generation of the same idea evolved to address limitations of the previous. All three remain valid in Gen 6 — which mechanism a device uses depends on its capability declaration in configuration space and how the driver configures it.

📋 Interrupt Evolution: Pins → Messages

Three Generations of PCIe Interrupt Mechanism INTx (Virtual Pins) Emulates PCI INTA#–INTD# pins Uses Assert_INTx / Deassert_INTx TLPs Level-sensitive, shareable, slow Only 4 virtual pins per device Used during boot / legacy devices MSI (Memory Write) Single MWr TLP to APIC address No physical pins. Edge-triggered. Up to 32 vectors, contiguous All vectors share one APIC address Mandatory for all PCIe endpoints MSI-X (Table-based) MWr TLP per vector, from MMIO table Up to 2048 vectors per function Each vector: independent address + data Per-vector CPU targeting. Non-contiguous. Optional but strongly preferred
Figure 1 — Three PCIe interrupt mechanisms. INTx is the legacy PCI model emulated in-band. MSI replaced physical pins with a single memory write. MSI-X extended MSI to 2048 independent vectors with per-CPU targeting — addressing all MSI limitations for modern multi-queue devices.

📋 Legacy PCI INTx# Pins

The original PCI interrupt model uses physical open-drain wires: INTA#, INTB#, INTC#, and INTD#. These are active-low signals — a device asserts its interrupt by pulling the line low. Because they are open-drain, multiple devices can share the same physical wire by pulling it low simultaneously without electrical damage. The interrupt controller sees the shared line as asserted if any device is pulling it.

Each PCI function declares which pin it uses in the Interrupt Pin register at offset 3Ch [15:8]:

The Interrupt Line register at offset 3Ch [7:0] stores the IRQ number (0–254) assigned by system firmware. This register has no hardware effect — it is purely a software convention for drivers to know which interrupt controller input to attach their handler to. Value FFh means “not connected to any interrupt controller input.”

When a function generates an interrupt, it both asserts its INTx# pin and sets the Interrupt Status bit in the Status register (offset 04h bit 19). The driver’s interrupt service routine reads the Status register to confirm the device is the source of the current interrupt. On shared IRQs, every driver sharing the line runs in sequence until one of them claims the interrupt.

📋 PCIe Virtual INTx — Assert/Deassert Messages

PCIe has no physical interrupt pins on the connector or link. There are no INTA#–INTD# wires. Instead, PCIe emulates the legacy interrupt model using in-band Message TLPs:

This pair of TLPs emulates the level-sensitive nature of the physical signal. The Root Complex tracks the assertion state of all four virtual wires for each downstream port and drives the corresponding interrupt controller input based on the cumulative Assert/Deassert state.

Virtual INTx — Assert + Deassert TLP Pair Emulates Physical Level-Sensitive Pin Virtual INTx# wire Inactive (high) — no interrupt pending Assert Active (low) — interrupt asserted Deassert Inactive again Assert_INTA TLP Sent upstream Sets ISR pending Deassert_INTA TLP Sent after driver clears interrupt
Figure 2 — Virtual INTx timing. When the device needs service it sends Assert_INTA upstream. The Root Complex marks that port’s virtual INTA wire as active and triggers the interrupt controller. When the driver reads and clears the device’s interrupt status, the device sends Deassert_INTA. The Root Complex deactivates the virtual wire. Only one Assert is needed even if the interrupt fires repeatedly before the driver clears it.

📋 INTx TLP Format

INTx messages are 3-DW (no data) Message TLPs. The Type field encodes both the direction (Assert vs Deassert) and the specific pin (INTA–INTD). The Routing field uses “Local — Terminate at Receiver” rather than “Route to Root” for two reasons: each intermediate bridge may remap the virtual wire to a different pin (see Swizzling below), and the TLP should be absorbed at each bridge rather than passed transparently.

INTx Message TLP Header — 3 DWs (No Data Payload) Fmt[2:0] 001 = no data Type[4:0] 10100 = Message TC [6:4] 000b = TC0 Reserved / Attr / AT All zeroes Message Routing 100b = Local Length = 0 No payload Requester ID [31:16] — BDF of the sending function Root Complex uses this to know which device sent the interrupt Message Code [15:8] — selects Assert/Deassert and INTA–INTD 20h=Assert_INTA · 21h=INTB · 22h=INTC · 23h=INTD · 24h–27h=Deassert
Figure 3 — INTx Message TLP header. Two DWs (3rd DW is all zeros / unused in this format). Routing field = 100b (Local) means each bridge in the path processes and re-sends the message upstream with potential pin remapping — the TLP is never forwarded transparently. The Message Code selects which virtual pin (INTA–INTD) is being asserted or deasserted.
Message CodeTLP typeMeaning
20hAssert_INTAINTA virtual wire: inactive → active
21hAssert_INTBINTB virtual wire: inactive → active
22hAssert_INTCINTC virtual wire: inactive → active
23hAssert_INTDINTD virtual wire: inactive → active
24hDeassert_INTAINTA virtual wire: active → inactive
25hDeassert_INTBINTB virtual wire: active → inactive
26hDeassert_INTCINTC virtual wire: active → inactive
27hDeassert_INTDINTD virtual wire: active → inactive

📋 INTx Mapping, Swizzling, and Collapsing

Mapping (Swizzling)

When a Switch or Root Port forwards an INTx message upstream, it may remap the virtual interrupt wire to a different pin. This is called swizzling and is defined per port based on the device’s slot number. The remapping formula is:

Mapped_Pin = (Original_Pin − 1 + Device_Number) mod 4 + 1

This rotation ensures that devices with different slot numbers use different IRQ inputs at the interrupt controller, preventing all slots from sharing a single IRQ. A device in slot 0 with INTA maps to INTA; slot 1 with INTA maps to INTB; slot 2 with INTA maps to INTC; and so on.

Collapsing

Because the virtual wires behave like wire-ORed signals, a Switch must never send two consecutive Assert messages for the same virtual wire without an intervening Deassert. If two devices at different downstream ports both assert INTA, the switch sends one Assert_INTA upstream when the first one fires. The second Assert from the other device is collapsed — absorbed silently because the wire is already asserted. When one device deasserts but the other is still asserting, no Deassert message is sent upstream — the shared virtual wire stays asserted. Only when the last device deasserts does the Deassert message flow upstream.

INTx shared interrupts cost performance. When multiple devices share an IRQ through collapsing, the CPU cannot determine which device fired without polling all of them. The interrupt service routine chains through handlers from all sharing devices until one claims the interrupt. On a busy system with 3–4 devices sharing INTA, this can triple or quadruple the interrupt handling latency. This is the primary reason MSI was introduced.

📋 INTx Configuration Registers

RegisterOffsetAccessFunction
Interrupt Pin3Ch [15:8]Read-onlyHardcoded by designer: 0=none, 1=INTA, 2=INTB, 3=INTC, 4=INTD
Interrupt Line3Ch [7:0]Read/WriteOS/BIOS writes the assigned IRQ number here. No hardware effect — annotation only for drivers.
Interrupt DisableCommand bit 10Read/WriteWhen 1: device must not send Assert_INTx messages. Any active virtual wires must be deasserted first. Set to 1 before enabling MSI/MSI-X.
Interrupt StatusStatus bit 19Read-onlySet by hardware when a virtual INTx assertion is pending. Cleared when the device’s interrupt cause is cleared. Not affected by Interrupt Disable bit.

📋 MSI — Message Signaled Interrupt Concept

MSI eliminates the virtual pin entirely. Instead of sending an Assert_INTx message TLP, the device signals an interrupt by performing a Memory Write transaction — a standard MWr TLP — targeting a specific MMIO address. The address is the Local APIC register of a CPU core, and the data value is the interrupt vector number.

The interrupt controller sees this write and immediately delivers the interrupt to the targeted CPU without any acknowledgment protocol. Because the address and data together uniquely identify the device and event, the CPU does not need to poll devices to find the interrupt source. The vector number itself tells the CPU which ISR to call.

MSI — Device Sends a Memory Write to the APIC PCIe Device Event occurs e.g. DMA complete Network packet received MWr TLP → Address: FEEx_xxxxh (APIC) · Data: 0000_00NNh (Vector N) Local APIC Receives write Interrupts CPU with vector N No interrupt pins. No shared IRQs. No IRQ polling. CPU knows exactly which device and event from the vector number alone. The address and data were written by the OS into the device’s MSI Capability registers during driver initialisation.
Figure 4 — MSI mechanism. The device generates a standard Memory Write TLP targeting the APIC’s memory-mapped register at FEEx_xxxxh (on x86). The data payload is the 32-bit interrupt vector (upper 16 bits always zero). The APIC delivers interrupt vector N to the designated CPU core. The entire transaction is a normal PCIe TLP — no special handling required at any switch or bridge.

📋 MSI TLP — What the Device Sends

An MSI interrupt is delivered as a standard Memory Write TLP. The only things that make it an interrupt are the target address (APIC register) and the data value (vector number). The PCIe fabric treats it identically to any other DMA write — it flows through switches, is subject to flow control, and is protected by LCRC.

MSI TLP Header and Data (4DW + 1DW Payload for Native PCIe Endpoints) Fmt=011b (4DW+data) Type=00000b (MWr) TC=0 · Attr=00b RO=0 · NS=0 Length=1 DW · Last BE=0000 · 1st BE=1111 Requester ID (BDF) [31:16] · Tag [15:8] · Byte Enables MSI Message Address Upper [63:32] — upper 32 bits of APIC address (often 0 on x86) MSI Message Address Lower [31:0] — lower 32 bits of APIC address (e.g. FEEFxx0Ch) · bits [1:0] always 0
Figure 5 — MSI TLP header. A standard 4DW Memory Write header (native PCIe endpoints must support 64-bit addressing). Fmt=011b = 4DW + data. Length field = 1 DW (single DWORD payload). First BE = 1111b (all bytes valid). Last BE = 0000b (only one DW). The data DW (not shown) carries the 32-bit interrupt vector value — upper 16 bits always zero, lower 16 bits from the MSI Message Data register.

The MSI write must use the Relaxed Ordering = 0 and No Snoop = 0 attribute settings. This ensures the interrupt TLP is strictly ordered with respect to all prior DMA writes from the same device. This ordering guarantee is critical for interrupt-driven DMA: when the CPU receives the interrupt vector, all DMA writes that preceded the MSI write in the device’s output are already visible in memory.

📋 MSI Capability Structure and Registers

The MSI Capability (ID 05h) lives in the PCI-compatible capability space (offsets 40h–FFh). It has four variants based on address width and per-vector masking support. Native PCIe endpoints must implement the 64-bit variant.

MSI Capability — Four Variants (Most Common: 64-bit with Masking) 32-bit, no mask Offset +00h: Cap ID + Next + Ctrl +04h: Address [31:0] +08h: Data [15:0] (3 DWs total) Legacy only 32-bit decode 64-bit, no mask +00h: Cap ID + Next + Ctrl +04h: Address [31:0] +08h: Address [63:32] +0Ch: Data [15:0] (4 DWs total) Mandatory PCIe 32-bit + masking +00h: Cap ID + Next + Ctrl +04h: Address [31:0] +08h: Data [15:0] +0Ch: Mask Bits [31:0] +10h: Pending Bits [31:0] (5 DWs total) 64-bit + masking ✓ +00h: Cap ID + Next + Ctrl +04h: Address [31:0] +08h: Address [63:32] +0Ch: Data / Reserved +10h: Mask Bits [31:0] +14h: Pending Bits (6 DWs)
Figure 6 — Four MSI Capability variants. Native PCIe endpoints must implement 64-bit addressing. The per-vector masking variants add 32-bit Mask Bits and Pending Bits registers. Each bit in the Mask register controls one vector (bit 0 = base vector). The Pending register tracks which masked vectors have pending interrupts.

MSI Message Control register key bits

Bit(s)FieldAccessMeaning
0MSI EnableRW1 = MSI active. Device uses MWr for interrupts. INTx and MSI-X automatically disabled.
[3:1]Multiple Message CapableROHow many vectors the device wants. 000=1 · 001=2 · 010=4 · 011=8 · 100=16 · 101=32. Must be power of two.
[6:4]Multiple Message EnableRWHow many vectors software actually allocated. Same encoding. Device varies lower N bits of Message Data for N vectors.
764-bit Address CapableRO1 = Message Address Upper register present. All native PCIe devices must set this.
8Per-Vector Masking CapableRO1 = Mask Bits and Pending Bits registers present.

📋 MSI Multiple Vectors

When more than one vector is allocated (Multiple Message Enable ≥ 1), the device uses a single base Message Data value from the register. For each event type, it sends a slightly different data value by modifying the lower N bits. With 4 vectors allocated (Enable = 010b), the device can send Data+0, Data+1, Data+2, or Data+3 for its four distinct events.

MSI Multiple Vectors — Base Data ± Low Bits Example: Message Data = 49A0h, 4 messages allocated (MME = 010b) Event 0 → Data = 49A0h Low bits [1:0] = 00b Event 1 → Data = 49A1h Low bits [1:0] = 01b Event 2 → Data = 49A2h Low bits [1:0] = 10b Event 3 → Data = 49A3h Low bits [1:0] = 11b
Figure 7 — MSI multiple vectors using one base Message Data value. With 4 messages allocated, the device modifies bits [1:0] of the data value. With 8 messages, it would modify bits [2:0]. The interrupt vectors allocated by the OS must be contiguous — e.g. 49A0h, 49A1h, 49A2h, 49A3h — because MSI has no way to assign non-contiguous vectors.
All MSI vectors share one APIC address. This means all MSI vectors for a device target the same CPU core — only the data value (vector number) differs. If software wants different events to be handled by different CPUs, it cannot use MSI. It must use MSI-X, where each table entry has an independent Message Address that can target any CPU’s APIC.

MSI Configuration Sequence

  1. Walk the PCI capability list from offset 34h. Find Cap ID = 05h (MSI).
  2. Read Message Control (bits [8:0]):
    • Bits [3:1] = Multiple Message Capable → how many vectors the device wants
    • Bit 7 = 64-bit Address Capable → which register layout is present
    • Bit 8 = Per-Vector Masking Capable → whether Mask Bits register is present
  3. Allocate interrupt vectors from the OS interrupt controller. Allocate a power-of-two count ≤ what the device requested. Get the base vector number (e.g. N) and the APIC address (e.g. FEEFxx0Ch for CPU core X).
  4. Write Multiple Message Enable bits [6:4] with the count allocated (same encoding as Capable field).
  5. Write Message Address Lower [31:0] = APIC address. If 64-bit capable, write Message Address Upper [63:32] (often 0 on x86).
  6. Write Message Data = base interrupt vector N (lower 16 bits of the 32-bit data field).
  7. Set Interrupt Disable in the Command register (bit 10 = 1) to disable INTx.
  8. Set MSI Enable (Message Control bit 0 = 1). Device is now using MSI.
  9. Register the ISR for vector N (and N+1, N+2, … for multiple-vector allocation).

📋 MSI-X — Extended MSI Concept

MSI-X was designed to remove all three remaining limitations of MSI:

The key architectural difference is where the vector information is stored. MSI stores everything in configuration space (a few registers). MSI-X stores the per-vector information in a table in MMIO space — one 128-bit entry per vector, located in a BAR-mapped region. Only the table pointer and enable bits are in configuration space.

📋 MSI-X Table and PBA

MSI-X Table in MMIO BAR Space — One 128-bit Entry per Vector Offset in BAR Msg Address Upper [63:32] Msg Address Lower [31:0] Message Data [31:0] Vector Control [31:0] 0x000 Upper APIC addr (often 0) APIC addr: FEEFxx0Ch Vector N (e.g. 0x4A) Mask bit [0] = 0 (unmasked) 0x010 Upper APIC addr APIC addr: FEEFyy0Ch ← different CPU core! Vector M (e.g. 0x6B) ← different, non-contiguous! Mask bit [0] = 0 · · · (up to 2048 entries) Pending Bit Array (PBA) — also in BAR space, offset via PBA Offset register One bit per vector. If vector N is masked and its event fires, PBA bit N is set. Device sends MSI-X write when bit N is unmasked.
Figure 8 — MSI-X Table. Entry 0 targets CPU core X (APIC FEEFxx0Ch) with vector 0x4A. Entry 1 targets a different CPU core Y (APIC FEEFyy0Ch) with a completely different non-contiguous vector 0x6B. Neither the APIC addresses nor the vector numbers need to be related. The Mask bit in Vector Control [0] individually enables/disables each vector.

MSI-X Capability Structure registers

FieldOffset from cap startKey bits
Message Control+02h (within DW0)Table Size [10:0] (N−1 encoding: 0=1 vector, 2047=2048) · Function Mask [14] · MSI-X Enable [15]
Table Offset + BIR+04h (DW1)Table BIR [2:0] = which BAR (0–5) holds the table · Table Offset [31:3] = byte offset within that BAR
PBA Offset + BIR+08h (DW2)PBA BIR [2:0] = which BAR holds the PBA · PBA Offset [31:3] = byte offset within that BAR

MSI-X Table Entry (128 bits each)

DW within entryContent
DW0 (+00h)Message Address Lower [31:0] — APIC address, bits [1:0] = 0 (DWORD aligned)
DW1 (+04h)Message Address Upper [63:32] — upper 32 bits of 64-bit APIC address
DW2 (+08h)Message Data [31:0] — interrupt vector (upper 16 bits zero on x86)
DW3 (+0Ch)Vector Control [31:0] — bit 0 = Mask (1=masked, 0=enabled). All other bits reserved.

MSI-X Configuration Sequence

  1. Find Cap ID = 11h in the capability list.
  2. Read Message Control: Table Size [10:0] + 1 = total vectors supported. Read Table BIR and Table Offset. Read PBA BIR and PBA Offset.
  3. Set Function Mask bit (Message Control bit 14 = 1) — globally mask all vectors while programming. This prevents spurious interrupts during table setup.
  4. Set MSI-X Enable (bit 15 = 1). Memory Space Enable must already be set in Command register so the BAR is accessible.
  5. Map the BAR identified by Table BIR into kernel virtual address space.
  6. For each vector N to configure: write the MMIO table entry at offset (Table_Offset + N×16):
    • DW0: Message Address Lower = APIC address of target CPU core
    • DW1: Message Address Upper = upper address (often 0)
    • DW2: Message Data = interrupt vector number
    • DW3: Vector Control = 0 (unmask this vector)
  7. Set Interrupt Disable in Command register (bit 10 = 1) to disable INTx.
  8. Clear Function Mask bit (bit 14 = 0) — all configured vectors are now active.
  9. Register ISR handlers for each allocated vector.
Function Mask is atomic. Setting Function Mask before programming the table prevents the device from generating interrupts with partially-written table entries — an entry with old Address but new Data would deliver a spurious interrupt to the wrong CPU. Always use Function Mask as a critical section wrapper around MSI-X table updates.

📋 INTx vs MSI vs MSI-X — Decision Guide

Which Interrupt Mechanism to Use — Decision by Scenario Use INTx When Device boots before OS loads (BIOS) Device does not support MSI/MSI-X Behind PCIe-to-PCI bridge (PCI device) Minimal driver, no OS interrupt mgr Avoid for any new PCIe native device Avoid if sharing IRQ with other devices Use MSI When ≤32 distinct interrupt events per function Simple single-queue device (NVMe, USB) Single-CPU system (no NUMA concerns) MSI-X not available in device hardware Good for simple embedded endpoints Always preferred over INTx Use MSI-X When Multi-queue device (NIC, NVMe, GPU) One vector per queue per CPU core NUMA-aware interrupt distribution needed SR-IOV — each VF has own MSI-X vectors >32 events or non-contiguous vectors Always use if device supports it
Figure 9 — Interrupt mechanism selection guide. INTx is a fallback for legacy scenarios. MSI is the baseline for PCIe-native devices with simple interrupt needs. MSI-X is the correct choice for any device with multiple queues, multiple CPUs, or more than 32 event types. Modern drivers (Linux, Windows) detect and prefer MSI-X automatically when it is present.
PropertyINTx (Virtual)MSIMSI-X
Max vectors per function4 (INTA–INTD)322048
Vector contiguity requiredN/AYes — contiguousNo — any vectors
Per-vector CPU targetingNo — single IRQNo — one APIC addrYes — per-entry address
Trigger modelLevel-sensitiveEdge-triggeredEdge-triggered
Sharing between devicesYes (wire-ORed)NoNo
Configuration locationConfig space registersConfig space capabilityConfig space + MMIO BAR table
Interrupt storm riskHigh (shared IRQ)LowVery low (per-vector masking)
Per-vector maskingNoOptional (Mask Bits reg)Always (per table entry)
Boot-time usabilityYes (BIOS)Limited (OS required)No (requires OS + driver)
Mandatory for PCIeYes (always emulated)Yes (must implement)No (optional)

Interrupts in Gen 6

All three interrupt mechanisms — INTx virtual wire TLPs, MSI memory writes, and MSI-X table-based writes — work identically in Gen 6. The interrupt TLP formats, capability structure layouts, and configuration sequences are unchanged. Gen 6’s changes are entirely in the Physical Layer; the Transaction Layer is the same.

What changes in Gen 6 interrupt practice:

📋 Quick Reference

ItemValue / Rule
INTx Pin registerOffset 3Ch [15:8]. RO. 0=none, 1=INTA, 2=INTB, 3=INTC, 4=INTD
INTx Line registerOffset 3Ch [7:0]. RW. OS writes IRQ number. No hardware effect.
Interrupt Disable bitCommand register bit 10. Set to 1 to disable INTx. Must be set before enabling MSI or MSI-X.
Interrupt Status bitStatus register bit 19. RO. Set when INTx virtual wire is active. Unaffected by Interrupt Disable.
Assert_INTx TLPMessage TLP, Routing=100b (Local), Message Code 20h–23h for INTA–INTD
Deassert_INTx TLPMessage TLP, Routing=100b (Local), Message Code 24h–27h for INTA–INTD
INTx swizzle formulaMapped_Pin = (Original_Pin − 1 + Device_Number) mod 4 + 1
INTx collapsing ruleSwitch sends only one Assert for shared virtual wire; suppresses subsequent Asserts until Deassert
MSI Capability ID05h — mandatory for all PCIe endpoints
MSI TLP formatStandard MWr. Fmt=011b (4DW+data). Length=1 DW. 1st BE=1111b. Last BE=0000b. RO=0. NS=0.
MSI max vectors32. Contiguous. All share one Message Address (APIC). Device varies lower N bits of Message Data.
MSI Enable sequenceWrite MME → Write Address → Write Data → Set Interrupt Disable → Set MSI Enable
MSI 64-bit requirementAll native PCIe endpoints must implement 64-bit Message Address (bit 7 of Message Control = 1)
MSI-X Capability ID11h — optional but strongly preferred
MSI-X max vectors2048. Non-contiguous. Each entry has independent Address, Data, and Mask.
MSI-X Table locationIn MMIO BAR space. Table BIR [2:0] selects which BAR. Table Offset [31:3] byte offset within BAR.
MSI-X Table entry128 bits = 4 DWs: Address Lower, Address Upper, Message Data, Vector Control (bit 0 = Mask)
MSI-X Function MaskMessage Control bit 14. Set before programming table entries to prevent spurious interrupts.
MSI-X PBAPending Bit Array. One bit per vector. Set when masked vector’s event fires. Also in BAR space via PBA Offset.
Best practice: enable sequenceSet Memory Space Enable → Set Function Mask → Set MSI-X Enable → Program table entries → Set Interrupt Disable → Clear Function Mask
Gen 6 changesAll three mechanisms unchanged. MSI-X 2048-vector limit may be reached on multi-VF AI accelerators. IDE encrypts MSI writes when active.
Scroll to Top