MSI / MSI-X 消息中断控制器运行逻辑

MSI / MSI-X 消息中断控制器运行逻辑

1. 核心概念

MSI(Message Signaled Interrupts)不是"发消息",而是PCIe 设备往中断控制器的 MMIO 寄存器地址发一次 Memory Write 事务

┌──────────┐ Memory Write TLP ┌──────────────┐ │ PCIe 设备 │ ───────────────────────→ │ 中断控制器 │ │ (Endpoint)│ {addr, data} │ (LAPIC/GIC/IMSIC) └──────────┘ └──────┬───────┘ │ ┌───────▼───────┐ │ CPU Hart │ └───────────────┘
  • addr— 决定"发给谁"(哪个 Hart 的哪个特权级)
  • data— 决定"哪个中断"(int_id,0-2047)
  • addr 和 data 合在一起就是一次写操作,没有额外的协议头

2. 中断控制器地址不区分设备

所有设备共享同一个 IMSIC/LAPIC 地址,靠 data 值区分中断来源。

IMSIC 地址空间: 0x28000000 + (hart 0, M-file) 0x28000000 + (hart 0, S-file) 0x28000000 + (hart 1, M-file) 0x28000000 + (hart 1, S-file) ... 不是: 0x28000000 + NVMe 专用地址 ← 不存在 0x28000000 + 网卡专用地址 ← 也不存在

所有发往 Hart 1 S-file 的设备都写同一个地址0x28000103,IMSIC 收到后不关心是谁发的,只根据 data 值决定 pending 哪个 bit。


3. MSI vs MSI-X 硬件结构对比

┌─────────────────────────────────────────────────────────────────┐ │ MSI(老) │ │ │ │ 全部在 PCIe 配置空间 (Cap ID=0x05) │ │ ┌──────────────────────────────────────┐ │ │ │ msg_ctrl: MSI_EN, 向量数(1/2/4/.../32) │ │ │ msg_addr: 64-bit, 所有向量共用 │ │ │ msg_data0: NVMe AdminQ │ │ │ msg_data1: NVMe IOQ0 │ │ │ msg_data2: NVMe IOQ1 ← 只靠 data 区分 │ │ └──────────────────────────────────────┘ │ │ │ │ 限制: 最多 32 向量, 无独立 Mask, 无 PBA pending │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ MSI-X(新) │ │ │ │ PCIe 配置空间 (Cap ID=0x11) 只存指针 → 指向 BAR 内存中的两张表 │ │ │ │ Vector Table (BAR 内存, 16B/条 × N 条) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ [0] { msg_addr(64), msg_data(32), vec_ctrl(32) }│ │ │ │ [1] { msg_addr(64), msg_data(32), vec_ctrl(32) }│ │ │ │ [2] { msg_addr(64), msg_data(32), vec_ctrl(32) }│ │ │ │ ... 每条向量独立 addr + data + Mask │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ PBA 位图 (BAR 内存, N bit) │ │ ┌────────────────────────────────────┐ │ │ │ bit 0 → vec 0 pending bit 1 → vec 1 pending ... │ │ │ │ HW 置位, SW 读即自动清零 (MSIX_EN=1 时) │ │ │ └────────────────────────────────────┘ │ │ │ │ 优点: 最多 2048 向量, 独立 Mask, 硬件 pending 跟踪 │ └─────────────────────────────────────────────────────────────────┘

MSI-X 每条向量有独立 addr 和 data,理论上可以发到不同 CPU 核,但实际驱动几乎都写同一 addr,只靠 data 区分。


4. 中断号(data)和设备怎么绑定

绑定分两步:硬件固定映射(芯片设计时) +软件配置映射(驱动初始化时)。

4.1 硬件固定映射:中断事件 → vec_id

以 NVMe SSD 为例(芯片设计时定死,写在手冊里): ┌──────────────────────┬─────────┐ │ 中断事件 │ vec_id │ ├──────────────────────┼─────────┤ │ Admin Queue 完成 │ 0 │ │ IO Queue 0 完成 │ 1 │ │ IO Queue 1 完成 │ 2 │ │ IO Queue 2 完成 │ 3 │ │ 热插拔 │ 4 │ │ 错误 │ 5 │ └──────────────────────┴─────────┘

vec_id 就是 Vector Table 的数组下标。设备不知道"中断号"这个概念。

4.2 软件配置映射:VectorTable[vec_id] 里写什么

驱动初始化时: for (i = 0; i < 4; i++) { VectorTable[i].msg_addr = IMSIC_BASE | (hart_idx << 8) | file_sel; // ↑ 所有向量写同一个地址(发到同一个 Hart) VectorTable[i].msg_data = base_int_id + i; // ↑ 只有 data 不同:40, 41, 42, 43 } irq_map[40] = { .dev = nvme, .handler = nvme_isr, .vec = 0 }; irq_map[41] = { .dev = nvme, .handler = nvme_isr, .vec = 1 }; irq_map[42] = { .dev = nvme, .handler = nvme_isr, .vec = 2 }; irq_map[43] = { .dev = nvme, .handler = nvme_isr, .vec = 3 };

设备不知道"42 号中断"这个概念——它只知道 IOQ1 完成时用 VectorTable[2] 里的 data 值发出去。data 是什么,完全由驱动初始化决定。


5. 完整中断生命周期(以 RISC-V IMSIC + NVMe SSD 为例)

Phase 1 — 软件初始化

Step 1: PCI 枚举 ├─ 读 Vendor/Device ID → 识别设备 └─ 读 MSI-X Cap (offset → CapID=0x11) ├─ table_info → BIR + Offset → 算出 VectorTable 物理地址 ├─ pba_info → BIR + Offset → 算出 PBA 物理地址 └─ msg_ctrl.TableSize → 向量总数 = TableSize + 1 Step 2: 分配中断资源 ├─ 选目标 Hart: hart_idx = 1 ├─ 选特权级: S-level file = hart_idx * 2 + 1 = 3 ├─ 分配 int_id: 40-43(从软件维护的 id 池中取出) └─ 计算 IMSIC 地址: msi_addr = 0x28000000 | (hart_idx << 8) | file_sel Step 3: 写设备 MSI-X 表 for i in 0..3: VectorTable[i].msg_addr = msi_addr ← 物理地址 VectorTable[i].msg_data = 40 + i ← 中断 ID VectorTable[i].vec_ctrl = 0 ← 解 Mask wmb() // RISC-V 弱序架构必须加屏障 Step 4: 使能 ├─ IMSIC 侧: eie[40..43] = 1(通过 miselect 间接写 CSR) └─ 设备侧: msg_ctrl[0] MSIX_EN = 1, msg_ctrl[14] FuncMask = 0

Phase 2 — 硬件中断触发

T0: NVMe SSD IOQ1 完成一个命令 → 内部硬件信号 → MSI-X 控制器, vec_id = 2 T1: 三级校验 ├─ msg_ctrl[0] MSIX_EN == 1 ? ✓ ├─ msg_ctrl[14] FuncMask == 0 ? ✓ └─ VectorTable[2].vec_ctrl.Mask == 0 ? ✓ T2: 查表 ├─ VectorTable[2].msg_addr = 0x28000103 └─ VectorTable[2].msg_data = 42 T3: 组装 PCIe TLP Memory Write { addr: 0x28000103, data: 0x2A } T4: PCIe 传输 → Root Complex → 地址路由到 IMSIC T5: IMSIC 解码 ├─ addr[11:8] = 1 → Hart 1 ├─ addr[7:0] = 3 → S-level interrupt file └─ data[10:0] = 42 → interrupt identity T6: IMSIC 内部 ├─ eip[42] = 1 └─ (eip[42] & eie[42]) → seip 引脚拉高 (全程硬件自动,约 1-2μs)

Phase 3 — 软件 ISR

Step 5: CPU trap 前提: sstatus.SIE == 1 → seip == 1 触发 trap → scause = 9 (Supervisor external interrupt) → PC 跳转 stvec Step 6: 扫描 eip 找 pending for word in 0..63: // 2048 bits / 32 miselect = 0x10 + word // 选 eip 寄存器页 pending = mireg // 读 32-bit pending while pending: bit = ctz(pending) int_id = word * 32 + bit irq_map[int_id].handler() // 查到 int_id=42 → nvme_isr pending &= ~(1 << bit) Step 7: NVMe ISR nvme_isr(dev, vec=2, int_id=42): ├─ 读 NVMe 完成队列寄存器 → 处理完成项 └─ 清 pending: miselect = 0x00 // 选 clreipnum 页 mireg = 42 // 写即清: eip[42] = 0 Step 8: sret 返回 → sstatus.SIE = sstatus.SPIE → PC = sepc

6. 非 PCIe 设备(传统线中断)→ APLIC 转换

IMSIC、LAPIC、GIC 都只接受 MSI(Memory Write),传统线中断设备(UART、SPI、GPIO、定时器)需要通过协议转换器:

UART TX 完成 → 物理中断线 → APLIC 输入端 → APLIC 检测边沿/电平 → APLIC 代发 Memory Write {addr: IMSIC, data: src_id} → IMSIC 非 PCIe 的线中断设备 APLIC IMSIC ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ UART ─线─┼──────────→│ 中断源 0 │ │ │ │ SPI ─线─┼──────────→│ 中断源 1 │──MSI──→│ eip[n] │ │ GPIO ─线─┼──────────→│ 中断源 2 │ │ │ └──────────┘ └──────────────┘ └──────────┘ x86: IO-APIC 转 MSI → LAPIC ARM: SPI 直连 GIC Distributor(GIC 有线输入) RISC-V: APLIC 转 MSI → IMSIC(IMSIC 完全没有线输入)

7. 三大架构中断控制器对比

x86 LAPICARM GICv3+ITSRISC-V IMSIC
MSI 地址编码addr[19:12]=目标核addr 只是敲门砖addr[11:8]=目标 Hart
中断号来源data[7:0] 直接是向量EventID → ITS 查表 → LPIdata[10:0] = int_id
向量数224LPI 8192+2047 (per file)
优先级仲裁硬件 IRR→ISR 自动硬件优先级仲裁器无硬件仲裁,软件扫 eip
pending 清写 APIC EOI 寄存器写 ICC_EOIR1写 clreipnum CSR
表结构无(寄存器直解)DeviceTable + ITT + Collection 三级eip + eie 两组位图
硬件复杂度中等高(需访存查表)极低(位图+OR门)

8. RISC-V IMSIC 硬件细节

8.1 中断文件(Interrupt File)

每个 Hart 有 3-4 个中断文件 (M/S/VS-level): ┌──────────────────────────────┐ │ Interrupt File (S-level) │ │ eip[0..2047] 2047-bit │ ← HW 写 MSI 置位 │ eie[0..2047] 2047-bit │ ← SW 写 CSR 使能 │ seteipnum 写即置位 │ │ clreipnum 写即清零 │ └──────────────────────────────┘ 硬连线: (eip[n] & eie[n]) → OR → meip/seip 引脚 → Hart 的 trap 条件

8.2 MSI addr / data 编码

MSI addr (64-bit): ┌──────────────────────┬──────────┬─────────┬────────────┐ │ IMSIC Base (平台相关) │ guest │ hart │ file & │ │ 63:24 │ 23:12 │ 11:8 │ 识别位 │ └──────────────────────┴──────────┴─────────┴────────────┘ MSI data (32-bit): ┌──────────────────────────────────┬────────────┐ │ reserved = 0 │ int_id │ │ 31:11 │ 10:0 │ └──────────────────────────────────┴────────────┘

8.3 CSR 间接访问

RISC-V 没有给 eip/eie 分配独立的 CSR 指令编码空间 用 miselect + mireg 两个 CSR 间接寻址: // 读 eip[15:0] csrrw x0, miselect, 0x10 // 选寄存器页 csrrs x1, mireg, x0 // 读值到 x1 // 清 pending csrrw x0, miselect, 0x00 // 选 clreipnum 页 csrrw x0, mireg, 42 // 写 int_id → 硬件清 eip[42]

9. 关键设计要点

  1. MSI 的本质是 Memory Write— 不是独立协议,PCIe 设备直接往中断控制器的 MMIO 地址写一个值。

  2. addr 决定路由,data 决定身份— 所有设备共享同一个 IMSIC 地址,软件靠 data(int_id)区分中断来源。

  3. MSI-X 每条向量有独立 addr— 但实际驱动全部指向同一地址,区分只靠 data。

  4. 设备不知道中断号— 中断事件 → vec_id 是芯片固定映射;vec_id → int_id(data)是驱动初始化时写表决定的。

  5. RISC-V IMSIC 没有硬件仲裁— 只负责置 pending bit。优先级、嵌套全由软件扫描 eip 决定。换来极简硬件:位图 + OR 门。

  6. PBA 只在 MSIX_EN=1 时 auto-clear— 卸载时必须先清 PBA 再关 MSIX_EN,否则残留 pending bit 无法消除。

  7. 弱序架构(RISC-V/ARM)写 Vector Table 后必须 wmb()— 确保 MSI-X 控制器看到的是新数据而非 store buffer 里的旧值。

不需要。"注册"是硬件布线时就定死的。


10. APLIC连接(普通设备如果想发msi中断,需要在aplic中注册转换逻辑么)

SoC 设计时(Verilog/VHDL 连线,不可改): UART 的中断线 ──焊死──→ APLIC 输入端口 3 SPI 的中断线 ──焊死──→ APLIC 输入端口 7 GPIO 的中断线 ──焊死──→ APLIC 输入端口 12 Timer 的中断线 ──焊死──→ APLIC 输入端口 20 不是软件注册的 — 芯片流片后就定了。

10.1 软件只需要配 APLIC 的输出侧

硬件固定: 软件配置(启动时配一次): UART ──线──→ APLIC输入3 ──→ 源3 配成: {目标=IMSIC_Hart1_S, data=10} SPI ──线──→ APLIC输入7 ──→ 源7 配成: {目标=IMSIC_Hart1_S, data=15} GPIO ──线──→ APLIC输入12 ──→ 源12配成: {目标=IMSIC_Hart2_S, data=20} Timer ──线──→ APLIC输入20 ──→ 源20配成: {目标=IMSIC_Hart0_M, data=5} ↑ ↑ 流片时就定了 软件初始化时写寄存器

软件只干一件事:告诉 APLIC “输入端 N 来了信号,你帮我往哪个 IMSIC 地址写什么 data”


10.2 三类设备的路径总结

PCIe 设备 (原生 MSI-X) → 自己的 MSI-X 控制器 → Memory Write TLP → IMSIC (跟 APLIC 完全无关) SoC 内置设备 (UART/SPI/I2C/Timer/GPIO) → 物理中断线 → APLIC → APLIC 代发 Memory Write → IMSIC (硬件连线固定,软件配 APLIC 寄存器) 既不是 PCIe 也没有线路到 APLIC 的设备 → 发不了 MSI,只能靠轮询

需要软件配置。APLIC 每个输入源都有独立的寄存器,可以配:


10.3 APLIC 每个源的三组可配寄存器

APLIC 输入源 3 (UART 的线): ┌─────────────────────────────────┐ │ sourcecfg[3] 触发方式 │ ← 电平/边沿/禁用 │ target[3] 目标地址 │ ← 往哪个 IMSIC 发 │ setipnum[3] data 值 │ ← 发中断时带什么 int_id └─────────────────────────────────┘

msg_data 是setipnum寄存器决定的,不是硬件固定的。


10.4 软件初始化时配置

// UART 连在 APLIC 的输入端 3// 我希望 UART 中断对应 int_id = 10// 配置触发方式aplic_write(APLIC_SOURCECFG_BASE+3*4,LEVEL_HIGH);// 配置目标:发到 Hart 1 的 S-level IMSICuint64_ttarget_addr=IMSIC_BASE|(1<<8)|3;aplic_write64(APLIC_TARGET_BASE+3*8,target_addr);// 配置 data 值:中断号就用 10aplic_write(APLIC_SETIPNUM_BASE+3*4,10);// 再注册 irq_mapirq_map[10]={.handler=uart_isr,.dev=uart_dev};

之后 UART 拉高中断线 → APLIC 自动发出Memory Write {addr: IMSIC, data: 10}→ IMSIC eip[10]=1。


10.5 和 PCIe MSI-X 对比

PCIe MSI-XAPLIC
data 存在哪BAR 里的 VectorTable[vec_id].msg_dataAPLIC 的 setipnum[src_id] 寄存器
谁决定的驱动初始化时写入驱动初始化时写入
硬件固定部分vec_id(中断事件→表索引)src_id(中断线→APLIC 端口号)
软件可配部分msg_data(发什么值)setipnum(发什么值)

思路完全一样:硬件固定"哪条线/哪个事件",软件配置"这个事件映射到哪个中断号"。