Skip to content

PCIe · Module 19

MSI — An Interrupt That Is Just a Memory Write

MSI turns an interrupt into a posted memory write to an address the host supplied. That one change gives interrupts identity and removes sharing — and subjects them to credits, ordering, and a race that silently corrupts data.

Chapter 19.1 ended on a structural limitation. INTx cannot carry identity: two stages of OR discard which Function and which source, so the host calls every driver on a shared pin and each one polls its own registers.

MSI's answer is startlingly simple. Stop signalling, and write.

An MSI interrupt is a memory write TLP — the same kind of packet the device uses to move data — sent to an address software gave it, carrying a value software chose. The host's interrupt controller is memory-mapped, so a write to that address is an interrupt.

What does it buy, what does it cost, and what new failure becomes possible once an interrupt travels the same path as the data it announces?

1. The Verified Sources

2. An Interrupt With an Address

The whole mechanism in four steps.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1.  enumeration   software writes an ADDRESS and a DATA value into the
                  device's MSI Capability registers
 
2.  event         something happens in the device
 
3.  interrupt     the device transmits a MEMORY WRITE of that data to
                  that address -- an ordinary posted write TLP
 
4.  delivery      the host's interrupt controller is mapped at that
                  address; the write IS the interrupt

Nothing in step 3 is interrupt-specific. It is the same packet type the device uses for DMA, on the same transmit interface (§1), through the same flow control and ordering machinery.

3. What Changes From INTx

INTx (19.1)MSI
TransportAssert/Deassert messagesa memory write TLP
Semanticslevel, held until clearededge — one write per event
Identitynone — two stages of OR (§19.1 §7)the data value (§5)
Sharingfour pins across many devicesnone — each vector is its own
Host actionpoll every driver on the pindispatch directly
Count4 pins, shared1 to 32, powers of two (§6)
Deassertrequirednone — nothing is held
Flow controla message, but still a TLPposted credits (16.2)
Ordering hazardnone§8

Two rows deserve emphasis.

"Deassert: none" is a real simplification. Chapter 19.1's entire reconciler exists to keep a remote belief in sync with a local level. MSI has no level and no belief — the write happened or it did not, and there is nothing to keep synchronized.

And "Ordering hazard: §8" is the price. INTx messages did not carry data and could not be confused with it. An MSI is a write among writes.

4. One Address, One Data Value

§1's field map is precise, and its shape is the source of §5 and §6:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
MSI Address [63:0]      ONE address, for the whole Function
MSI Data    [15:0]      ONE base value, 16 bits

Software writes both during enumeration. The address is wherever the host's interrupt controller expects writes; the data is a value the host will recognize.

Note what is missing: any per-vector storage. With up to 32 vectors and exactly one address and one data field, the vectors have to be encoded inside the single Data value — which is §5, and which is the entire architectural difference from MSI-X.

And the device does not choose either value. It stores what software wrote and uses it verbatim. A device that modified the address, or composed data from anything other than the programmed base plus its vector (§5), would be writing somewhere software did not intend — which is not an interrupt, it is a stray DMA write.

5. The Vector Lives in the Low Bits

With one Data value and up to 32 vectors, the encoding is forced: the vector number replaces the low bits of the programmed Data value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
granted vectors = 2^n     →     low n bits carry the vector
 
  n=0 ( 1 vector )   data = base                    (no bits used)
  n=2 ( 4 vectors)   data = {base[15:2], vec[1:0]}
  n=5 (32 vectors)   data = {base[15:5], vec[4:0]}

Verified across every combination (§16): all granted sizes × three base values × every vector — zero encoding errors, and every produced value has the vector recoverable from its low n bits.

6. Powers of Two

§1's parameter takes six values for a range of thirty-two. That is a 2^n encoding, and it has a cost.

Executed exhaustively over every request from 1 to 32 (§16):

RequestedGrantedWasted
1, 2, 4, 8, 16, 32exact0
341
583
9167
173215
31321

Of the 32 possible requests, 26 cannot be granted exactly — 81%. Total waste across all requests: 155 vectors. Worst single case: a device wanting 17 must ask for 32 and leave 15 unused.

And there is a second constraint §1 makes explicit: Multiple Message Capable is what the device requests; Multiple Message Enable is what software grants, and it may be fewer.

7. An Interrupt That Waits in Line

Because an MSI is a posted memory write, everything that applies to a posted write applies to it.

RequirementChapterConsequence for an interrupt
Posted credits (PH and PD)16.2no credits, no interrupt — it waits
Replay retention15.1it occupies replay buffer space until acknowledged
Ordering rules13.4it takes its place among other posted writes (§8)
LTSSM state18.6 §7only transmittable in L0
Power states18.7, 18.8a pending interrupt wakes the Link and then waits for it

The last row is worth sitting with. Chapter 18.8 §11's L1 exit latency encodings reach "more than 64 μs". On a Link with aggressive power policy, interrupt latency is dominated by wake latency, and nothing in the interrupt path can improve it.

And the credit row explains a symptom that looks like a lost interrupt. A device whose posted credits are exhausted — because the host is slow to return them (Chapter 16.6) — cannot transmit its interrupt. The interrupt is not lost; it is queued behind the very congestion it might have helped relieve.

This is a genuine coupling INTx did not have. An INTx message is also a TLP, but it announces a level the host can act on late (§19.1 §3). An MSI delayed is an event delayed, with no self-correcting level behind it.

8. The Race That Corrupts Data

9. Masking and Pending

§1 gives 32 mask bits and 32 pending bits, and notes per-vector masking is a capability.

The contract, where masking is implemented: a masked vector's interrupt is not sent, and its pending bit is set. When software unmasks it, the interrupt is delivered then.

Verified (§16): across every mask and pending combination, zero interrupts lost — a masked vector always sets pending.

Which is the point. Masking must not mean discarding. Software masks a vector to stop being interrupted right now — typically while it is already handling that vector — not to be told the event never happened.

And §1's note that the Pending Bits register is writable matters for verification: it means the pending state is observable and controllable, which is what makes §16's directed tests possible at all.

Where masking is not implementedVEC_MASK_CAPABLE unset — software must not rely on it, and a driver that does will find its "masked" vector still interrupting. This is exactly the kind of capability the driver must read rather than assume.

10. The Exchange

An MSI episode. During enumeration software writes the MSI address, the base data value, and the number of vectors it grants. Later the device writes buffer data with posted writes, then transmits the MSI as a posted memory write carrying the base data with the vector substituted into the low bits. The host interrupt controller receives the write and dispatches directly to the handler for that vector. No acknowledgement is returned and nothing is deasserted.Host softwareDeviceInterrupt controllerenumeration: writeMSI Address + Datagrant N vectors (maybe fewer thanrequested)posted writes:buffer data FIRSTposted write: MSIdata to MSI addressdispatch directly -vector identifiesthe sourcehandler reads buffer- data is alreadythere (ordering)
Figure 1 — an MSI episode. Software programs the address and data during enumeration and grants a vector count that may be smaller than requested. When an event occurs, the device writes its data value — base with the vector substituted into the low bits — to the programmed address, after the data writes it is announcing. The write itself is the interrupt; there is no acknowledgement and nothing to deassert.

Three things to read out of the figure.

The data writes come first, and that is not stylistic. §8 is the entire reason — the ordering between those two arrows is what makes the handler's read safe.

There is no return arrow to the device. The write is posted; nothing is acknowledged at the interrupt level, and nothing is deasserted.

And the dispatch is direct. No polling of other drivers, because the data value identified the source (§3).

11. A Trace

Internal teaching signals. Four vectors granted (n = 2), base data 0x4000.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycle              1     2     3     4     5     6     7     8     9
event_vec2         0     1     0     0     0     0     0     0     0
msi_enable         1     1     1     1     1     1     1     1     1
granted_n          2     2     2     2     2     2     2     2     2
 
data_pending       0     1     1     1     0     0     0     0     0
mask_bits[2]       0     0     0     0     0     0     1     1     0
pending_bits[2]    0     0     0     0     0     0     0     1     0
 
msi_valid          0     0     0     0     1     1     0     0     1
msi_ready          0     0     0     0     0     1     0     0     1
msi_addr          --    --    --    --   ADDR  ADDR   --    --   ADDR
msi_data          --    --    --    -- 0x4002 0x4002  --    -- 0x4002

Read cycle 2. The event occurs — and no MSI is issued. data_pending is high: buffer writes are still outstanding (§8).

Read cycle 5. data_pending falls, and only then does msi_valid rise. The interrupt is issued after the data it announces, which is the ordering §8 depends on.

Read msi_data0x4002. Base 0x4000 with vector 2 substituted into the low 2 bits (§5). The host reads 2 from those bits and dispatches directly.

Read cycles 5–6. msi_valid is high with msi_ready low at cycle 5 — the transmit path is busy (§7). The request is held, and transfers at cycle 6.

Read cycles 7–8. A second event on vector 2, but the vector is now masked. No write is issued, and pending_bits[2] sets (§9).

Read cycle 9. The mask clears and the interrupt is delivered then — not discarded. The pending bit was the memory that made that possible.

12. RTL — Types and Vector Composition

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Normalized MSI types.
// 32 VECTORS MAX and a 16-bit Data field are canonical (section 1). The
// 2^n encoding follows from section 1's parameter taking six values for a
// range of thirty-two.
package msi_pkg;
 
  parameter int MSI_MAX_VEC = 32;
  parameter int VEC_W       = 5;    // $clog2(32)
  parameter int DATA_W      = 16;   // section 1: MSI Data is Bits 15:0
 
  typedef struct packed {
    logic                 enable;        // MSI Enable
    logic [2:0]           mult_msg_en;   // Multiple Message ENABLE (granted)
    logic [63:0]          address;       // MSI Address [63:0]
    logic [DATA_W-1:0]    base_data;     // MSI Data
    logic                 vec_mask_capable;
    logic [MSI_MAX_VEC-1:0] mask_bits;
    logic [MSI_MAX_VEC-1:0] pending_bits;
  } msi_cfg_t;
 
  // Granted vector count = 2^mult_msg_en. Section 6 measured what this
  // rounding costs: 26 of 32 possible requests cannot be granted exactly.
  function automatic int unsigned granted_vectors(input logic [2:0] n);
    return (n > 3'd5) ? 32 : (1 << n);   // clamp: section 1 caps n at 5
  endfunction
 
  // ==================================================================
  // VECTOR SUBSTITUTION, NOT AN OR (section 5).
  //
  // The vector REPLACES the low n bits. An OR into an unaligned base
  // produces a value that is neither the base nor a recoverable vector,
  // turning a software misconfiguration into an unattributable interrupt.
  // Substitution at least fails predictably -- and section 16 verified
  // the vector is recoverable from the low bits in every case.
  // ==================================================================
  function automatic logic [DATA_W-1:0] msi_compose(
      input logic [DATA_W-1:0] base,
      input logic [VEC_W-1:0]  vec,
      input logic [2:0]        n);
    logic [DATA_W-1:0] mask;
    mask = (n == 3'd0) ? '0 : DATA_W'((1 << n) - 1);
    return (base & ~mask) | (DATA_W'(vec) & mask);
  endfunction
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import msi_pkg::*;
 
// SYNTHESIZABLE. Clamp a requested vector to what software actually granted.
// SECTION 6: a device must work correctly with FEWER vectors than it asked
// for. A design that emitted a vector number larger than the grant would
// write data the host never allocated (section 16, mutation 4).
module msi_vector_map (
  input  logic [2:0]       mult_msg_en,     // granted, 2^n
  input  logic [VEC_W-1:0] requested_vec,   // what the condition wants
 
  output logic [VEC_W-1:0] effective_vec,
  output logic             vector_shared    // several conditions collapse here
);
 
  wire int unsigned granted = granted_vectors(mult_msg_en);
 
  // ==================================================================
  // FOLD, DON'T TRUNCATE ARBITRARILY -- and never emit out of range.
  //
  // With 32 vectors requested and 8 granted, conditions must SHARE
  // (section 6). Folding modulo the grant is deterministic and keeps
  // every vector in range; the DRIVER learns the grant from
  // Configuration Space and knows which conditions collapsed.
  // ==================================================================
  assign effective_vec = (granted >= MSI_MAX_VEC)
                       ? requested_vec
                       : VEC_W'(requested_vec & VEC_W'(granted - 1));
 
  assign vector_shared = (granted < MSI_MAX_VEC);
 
endmodule

Classification: both synthesizable.

msi_compose substitutes rather than ORs, for §5's reason: an OR into a misaligned base produces a value from which the vector cannot be recovered, converting a host configuration error into an unattributable interrupt.

And msi_vector_map never emits an out-of-range vector. With 8 granted, requested_vec & 7 is always 0–7. A design that passed the raw vector through would compose data with bits outside the granted field — landing on values the host allocated to something else.

Failure — four. OR instead of substitution (§5). Ignoring the grant and emitting a vector larger than allocated. Taking the grant as a build parameter rather than a runtime input. And n = 0 handling — with one vector the mask must be zero, or the composition corrupts bit 0.

13. RTL — MSI Request Owner

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import msi_pkg::*;
 
// SYNTHESIZABLE. THE FLAGSHIP BLOCK. Issue one MSI as a posted memory write.
//
// TWO THINGS MAKE THIS HARDER THAN IT LOOKS:
//   1. It must not be issued before the data it announces (section 8).
//   2. It is an ordinary posted write and can be stalled indefinitely
//      by credits, arbitration or LTSSM state (section 7).
module msi_request (
  input  logic clk,
  input  logic rst_n,
 
  input  msi_cfg_t cfg,                  // from Configuration Space
 
  // Section 1: MSI is generated from an EVENT (a single-cycle pulse),
  // not from a held level -- Chapter 19.1 section 14's mode distinction.
  input  logic             event_pulse,
  input  logic [VEC_W-1:0] event_vec,
 
  // ==============================================================
  // THE ORDERING INTERLOCK (section 8).
  //
  // High while any posted write this interrupt announces is still
  // outstanding on the transmit path. The MSI is NOT issued until it
  // falls -- so it cannot overtake its own data.
  // ==============================================================
  input  logic data_pending,
 
  // Transmit path -- the SAME path as data (section 1).
  output logic          tx_valid,
  output logic [63:0]   tx_addr,
  output logic [DATA_W-1:0] tx_data,
  input  logic          tx_ready,
  input  logic          link_ok,       // LTSSM permits normal traffic
 
  output logic [MSI_MAX_VEC-1:0] pending_out,
  output logic          dropped_disabled
);
 
  logic [MSI_MAX_VEC-1:0] pend_q;
  logic [VEC_W-1:0]       cur_vec_q;
  logic                   busy_q, drop_q;
 
  assign pending_out      = pend_q;
  assign dropped_disabled = drop_q;
 
  logic [VEC_W-1:0] eff_vec;
  logic             shared;
  msi_vector_map u_map (.mult_msg_en(cfg.mult_msg_en),
                        .requested_vec(event_vec),
                        .effective_vec(eff_vec), .vector_shared(shared));
 
  // Masked vectors are held pending, never discarded (section 9).
  wire vec_masked = cfg.vec_mask_capable && cfg.mask_bits[eff_vec];
 
  // ==================================================================
  // EVERY CONDITION MUST HOLD BEFORE A WRITE IS OFFERED.
  //
  //   enable       software turned MSI on
  //   !data_pending  the announced data has been handed to the path
  //   link_ok      the Link can carry normal traffic (Chapter 18.6)
  // ==================================================================
  assign tx_valid = busy_q && cfg.enable && !data_pending && link_ok;
  assign tx_addr  = cfg.address;                                // verbatim
  assign tx_data  = msi_compose(cfg.base_data, cur_vec_q, cfg.mult_msg_en);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      pend_q <= '0; cur_vec_q <= '0; busy_q <= 1'b0; drop_q <= 1'b0;
    end else begin
      if (event_pulse) begin
        if (!cfg.enable) begin
          // MSI disabled entirely: nothing to send, and REPORTED rather
          // than silently swallowed. In Legacy mode the level path owns
          // this condition instead (Chapter 19.1 section 14).
          drop_q <= 1'b1;
        end else if (vec_masked) begin
          // MASKED -> PENDING, never dropped (section 9). Unmasking
          // delivers it.
          pend_q[eff_vec] <= 1'b1;
        end else if (!busy_q) begin
          cur_vec_q <= eff_vec;   // CAPTURED; not re-read at transmit time
          busy_q    <= 1'b1;
        end else begin
          // Already carrying one. Hold this vector pending rather than
          // losing it -- the transmit path may stall for a long time.
          pend_q[eff_vec] <= 1'b1;
        end
      end
 
      if (busy_q && tx_valid && tx_ready) begin
        busy_q <= 1'b0;
        pend_q[cur_vec_q] <= 1'b0;
      end else if (!busy_q && cfg.enable) begin
        // Drain pending vectors once the path is free and unmasked.
        for (int v = 0; v < MSI_MAX_VEC; v++) begin
          if (pend_q[v] && !(cfg.vec_mask_capable && cfg.mask_bits[v])) begin
            cur_vec_q <= VEC_W'(v);
            busy_q    <= 1'b1;
            break;
          end
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

data_pending is the single most important input. It is §8's ordering interlock made structural: the MSI is not offered to the transmit path until the writes it announces have been. A design without it is correct only by luck of arbitration.

The vector is captured at acceptance, not re-read at transmit time — the Chapter 18.5 §12 rule, and here it matters because a second event can arrive during a stall.

And masked vectors set pending rather than being dropped (§9), because masking means "not now", never "never".

Failure — six. No data_pending interlock — §8's corruption. Re-reading event_vec at transmit time sends the wrong vector. Dropping on mask loses interrupts. Dropping when busy loses interrupts under load, which is when they matter most. Ignoring link_ok offers a TLP to a Link that must not carry one. And a separate low-latency path for MSI that bypasses the data queue — which removes the ordering guarantee entirely (§8).

14. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the MSI blocks. LOCAL contract only. Nothing asserts that the
// host services an interrupt, that credits become available, or that the
// Link ever permits traffic.
 
// ---- COMPOSITION ------------------------------------------------------
 
// P1: THE VECTOR IS RECOVERABLE from the low bits of the transmitted data.
// Section 16 verified this across every granted size, base and vector.
property p_vector_recoverable;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && (cfg.mult_msg_en != 3'd0))
    |-> ((tx_data & DATA_W'((1 << cfg.mult_msg_en) - 1))
         == DATA_W'(cur_vec_q));
endproperty
a_vec : assert property (p_vector_recoverable);
 
// P2: the high bits of the programmed base are PRESERVED. A design that
// ORed rather than substituted would still pass P1 on an aligned base and
// fail here on an unaligned one (section 5).
property p_base_preserved;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> ((tx_data & ~DATA_W'((1 << cfg.mult_msg_en) - 1))
                == (cfg.base_data & ~DATA_W'((1 << cfg.mult_msg_en) - 1)));
endproperty
a_base : assert property (p_base_preserved);
 
// P3: THE VECTOR IS ALWAYS WITHIN THE GRANT. Section 6: software may grant
// fewer than requested, and the device must not exceed it.
property p_vector_in_range;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> (cur_vec_q < VEC_W'(granted_vectors(cfg.mult_msg_en)));
endproperty
a_range : assert property (p_vector_in_range);
 
// P4: the address is used VERBATIM. The device does not compute it.
property p_addr_verbatim;
  @(posedge clk) disable iff (!rst_n) tx_valid |-> (tx_addr == cfg.address);
endproperty
a_addr : assert property (p_addr_verbatim);
 
// ---- ORDERING -- THE CENTRAL PROPERTY ---------------------------------
 
// P5: NO MSI WHILE THE DATA IT ANNOUNCES IS STILL OUTSTANDING.
// Section 8's corruption stated as a property.
property p_no_msi_before_data;
  @(posedge clk) disable iff (!rst_n) tx_valid |-> !data_pending;
endproperty
a_order : assert property (p_no_msi_before_data);
 
// P5b: and it uses the ORDINARY transmit path, so ordering applies.
// A bypass would satisfy P5 and still corrupt, because ordering is
// defined among packets in the same stream (section 8).
property p_same_path;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_ready) |-> dut_txpath.accepted_this_cycle;
endproperty
a_path : assert property (p_same_path);
 
// ---- DELIVERY ---------------------------------------------------------
 
// P6: an MSI is only transmitted when enabled.
property p_enabled;
  @(posedge clk) disable iff (!rst_n) tx_valid |-> cfg.enable;
endproperty
a_en : assert property (p_enabled);
 
// P7: NO TRANSMISSION WHILE THE LINK FORBIDS NORMAL TRAFFIC.
property p_link;
  @(posedge clk) disable iff (!rst_n) tx_valid |-> link_ok;
endproperty
a_link : assert property (p_link);
 
// P8: the request is HELD and STABLE under stall.
property p_held;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && !tx_ready) |=> (tx_valid || !link_ok || data_pending)
                           && $stable(cur_vec_q);
endproperty
a_held : assert property (p_held);
 
// ---- MASKING AND PENDING ----------------------------------------------
 
// P9: A MASKED VECTOR IS NOT TRANSMITTED.
property p_masked_not_sent;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && cfg.vec_mask_capable) |-> !cfg.mask_bits[cur_vec_q];
endproperty
a_mask : assert property (p_masked_not_sent);
 
// P10: AND IT IS NOT LOST -- it becomes pending. Section 16 verified zero
// losses across every mask/pending combination.
property p_masked_pends;
  @(posedge clk) disable iff (!rst_n)
  (event_pulse && cfg.enable && cfg.vec_mask_capable
                             && cfg.mask_bits[eff_vec]) |=> pending_out[eff_vec];
endproperty
a_pend : assert property (p_masked_pends);
 
// P11: an event arriving while busy is held pending, not dropped.
property p_busy_pends;
  @(posedge clk) disable iff (!rst_n)
  (event_pulse && cfg.enable && busy_q && !vec_masked) |=> pending_out[eff_vec];
endproperty
a_busy : assert property (p_busy_pends);
 
// P12: a pending bit clears only when its vector is actually transmitted.
property p_pending_clears_on_send;
  @(posedge clk) disable iff (!rst_n)
  (pending_out[v] && !$past(pending_out[v]) == 0 && !pending_out[v])
    |-> ($past(tx_valid) && $past(tx_ready) && ($past(cur_vec_q) == VEC_W'(v)));
endproperty
a_clear : assert property (p_pending_clears_on_send);
 
// P13: MSI and INTx are mutually exclusive -- Chapter 19.1 P14 from the
// other side.
property p_not_both;
  @(posedge clk) disable iff (!rst_n)
  cfg.enable |-> !dut_intx.msg.valid;
endproperty
a_excl : assert property (p_not_both);
 
// P14: reset.
property p_reset;
  @(posedge clk) !rst_n |=> (!tx_valid && (pending_out == '0));
endproperty
a_reset : assert property (p_reset);

P5 and P5b are the ordering pair, and both are needed. P5 forbids issuing before the data is out; P5b requires the MSI to use the same transmit path, because a bypass would satisfy P5 and still corrupt — ordering is defined among packets in one stream (§8).

P1 and P2 are the composition pair. P1 alone passes for an OR-based implementation on an aligned base; P2 catches it on an unaligned one (§5).

And P10 with P11 are the two ways an interrupt can be silently lost — masked, or arriving while busy. Both must become pending.

No liveness. "The interrupt is eventually delivered" depends on credits, on the Link, and on software unmasking.

15. Verification, Fault Injection, and Model Verification

Executed before publication.

Vector allocation — exhaustive over every request

All requests 1 through 32, against the 2^n encoding §1 implies:

Result
Requests grantable exactly6 of 32 (1, 2, 4, 8, 16, 32)
Requests requiring rounding up26 of 32 — 81%
Total vectors wasted across all requests155
Worst single caserequest 17 → granted 32, 15 wasted

Data composition — exhaustive

Every granted size × three base values × every vector in range: 0 encoding errors — the vector is recoverable from the low n bits in every case.

And the alignment hazard, computed: base 0x40F0 with 32 vectors granted, vector 0 → 0x40E0. The low 5 bits software had set are overwritten, shifting every vector into a range software did not allocate (§5).

Masking — exhaustive

Every mask / function-mask / pending combination: 0 interrupts lost. A masked vector always sets its pending bit.

Directed tests

  • Every granted sizemult_msg_en = 0 through 5; verify P1, P2, P3 for every vector.
  • Granted fewer than requested — verify folding stays in range and vector_shared asserts (§6). Required.
  • n = 0 (one vector) — verify the mask is zero and bit 0 is not corrupted.
  • Unaligned base data — verify P2 fails for an OR-based implementation and holds for substitution.
  • data_pending high across an event — verify no transmission until it falls (P5). Required, and §8's test.
  • Event with the transmit path stalled — verify hold and vector stability (P8).
  • Second event during a stall — verify it becomes pending, not lost (P11). Required.
  • Masked vector — verify pending set, nothing sent, and delivery on unmask (P9, P10). Required.
  • vec_mask_capable unset — verify masking is not relied upon (§9).
  • link_ok low — verify no transmission and no loss (P7).
  • MSI disabled — verify dropped_disabled reports rather than silently swallowing.
  • Mode switched from Legacy — verify no INTx message accompanies the MSI (P13).

The scoreboard composes its own expected (address, data) from (base_data, mult_msg_en, vector) and never calls msi_compose or reads cur_vec_q.

Mutations

#MutationCaught byLab symptom
1MSI issued before data_pending fallsP5handler reads stale buffer data — intermittent, load-dependent (§8)
2OR instead of substitution into base dataP2vectors land on values the host allocated elsewhere (§5)
3address computed rather than used verbatimP4a stray DMA write instead of an interrupt
4vector emitted beyond the grantP3data the host never allocated; delivered as another interrupt
5separate low-latency path for MSIP5bordering guarantee removed; §8's corruption with P5 still passing
6masked vector dropped instead of pendedP10interrupt lost whenever software masks during handling
7event dropped while busyP11interrupts lost under load, when they matter most
8event_vec re-read at transmit timeP8a stalled interrupt sends a later event's vector
9transmitted with link_ok lowP7TLP offered to a Link that must not carry one
10pending cleared on tx_valid rather than the handshakeP12interrupt recorded as sent but never transmitted
11MSI sent while INTx also activeP13one condition signalled twice
12grant treated as a build parameterP3works at 32 vectors, fails whenever software grants fewer
13Relaxed Ordering set on the MSI writeP5b + reviewordering removed device-wide "for throughput" (§8)
14reset leaves pending bits setP14spurious interrupts immediately after reset

16. Debugging

Symptom → is it composition, ordering, or delivery? → signal → distinguishing experiment.

The handler runs but the data is wrong

Suspect ordering before suspecting the driver (§8) — this is mutation 1, and it presents as a software bug.

The signature: intermittent, load-dependent, worse on busy systems, and it disappears when anything slows down — a debugger, a print, an added delay.

The distinguishing experiment: add a delay in the handler before reading the buffer. If the data is then correct, the interrupt overtook it — which is a hardware ordering defect, not a driver one, and the delay must not be the fix.

Then check three things on the device. Is there a data_pending interlock at all (P5)? Does the MSI use the same transmit path as the data (P5b, mutation 5)? And is Relaxed Ordering set on the write (Chapter 11.6, mutation 13)?

Interrupts arrive on vectors nobody assigned

Composition (§5) — and the fault is often software's, not the device's.

Check the alignment of the programmed base data against the granted vector count. With 2^n vectors granted, the low n bits of the base must be zero. Base 0x40F0 with 32 granted produces 0x40E0 for vector 0 (§15) — every vector shifted.

The distinguishing experiment: compute the expected data for vector 0 by hand and compare with what the device transmits. If they differ, the base was misaligned — and the device cannot detect that, because it has no idea which values the host allocated.

A second candidate: mutation 4, a vector emitted beyond the grant. Distinguished by reading Multiple Message Enable — what software granted — rather than Capable.

Interrupts stop under heavy load

Three candidates, in the order worth checking.

Posted credits exhausted (§7). The MSI is a posted write; no credits, no interrupt. Read the available PH/PD credits (Chapter 16.2). The interrupt is queued behind the congestion it might have relieved.

Events dropped while busy — mutation 7. Distinguished by whether pending_out accumulates: it should. A design that drops on busy loses interrupts exactly under load.

And masked without pending — mutation 6. If the driver masks a vector while handling it, and the device discards rather than pends, every event during handling is lost.

Interrupts are slow but not lost

Read the LTSSM state (§7). An MSI cannot be transmitted outside L0 (Chapter 18.6 §7), and in a power state the pending write wakes the Link and then waits for it.

Chapter 18.8 §11's L1 exit latency reaches "more than 64 μs". On an aggressive power policy, interrupt latency is wake latency, and no interrupt-path change improves it. The fix is power policy — or advertising a smaller L1 Acceptable Latency so software declines to enable L1.

17. Common Misconceptions

  • "MSI is a special interrupt packet." It is an ordinary posted memory write (§2).
  • "MSI bypasses flow control." It needs posted credits like any write (§7).
  • "MSI cannot be delayed." Credits, arbitration, ordering and LTSSM state all delay it (§7).
  • "MSI has a deassert." Nothing is held; there is nothing to deassert (§3).
  • "A device gets the number of vectors it asks for." 81% of requests cannot be granted exactly (§6, §15).
  • "Software grants what the device requested." Capable and Enable are different fields; it may grant fewer (§6).
  • "With MSI there is no polling." With fewer vectors than conditions, shared vectors poll again (§6).
  • "The device chooses the address or data." Software programs both; the device uses them verbatim (§4).
  • "The vector is a separate field." It is substituted into the low bits of one Data value (§5).
  • "Any base data value works." It must be aligned to the granted count (§5).
  • "Masking discards the interrupt." It sets a pending bit; unmasking delivers it (§9).
  • "Per-vector masking is always available." It is a capability that may be absent (§9).
  • "An interrupt cannot arrive before its data." It can, and nothing detects it (§8).
  • "Giving MSI its own fast path improves latency safely." It removes the ordering guarantee (§15).

18. Understanding Check

19. What's Next

MSI makes an interrupt an ordinary memory write, and every consequence follows from that.

It gains identity — the data value says which interrupt — so the host dispatches directly instead of polling every driver on a shared pin (§3).

It inherits everything a write has: posted credits, replay retention, ordering, and the LTSSM's rules about when traffic is legal (§7). An interrupt now waits in line.

And it introduces a failure INTx could not have (§8). The interrupt and the data it announces share a path, and if the interrupt overtakes the data, the host reads a buffer that is not ready — with every byte valid and nothing reporting an error.

Its limits are structural: powers of two, at most 32, one address and one data value — 81% of possible requests cannot be granted exactly (§15), and a grant smaller than the request forces conditions to share.

Chapter 19.3 — MSI-X removes those limits by moving the interrupt configuration out of Configuration Space and into memory. A table behind a BAR, up to 2048 entries, each with its own address and its own data — which changes not only how many interrupts a device can have, but where they can be delivered, and how independently each one can be masked.

The idea to carry forward: a notification must never be able to overtake what it notifies.