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

📅 2026/7/23 9:02:55
MSI / MSI-X 消息中断控制器运行逻辑
MSI / MSI-X 消息中断控制器运行逻辑1. 核心概念MSIMessage Signaled Interrupts不是发消息而是PCIe 设备往中断控制器的 MMIO 寄存器地址发一次 Memory Write 事务。┌──────────┐ Memory Write TLP ┌──────────────┐ │ PCIe 设备 │ ───────────────────────→ │ 中断控制器 │ │ (Endpoint)│ {addr, data} │ (LAPIC/GIC/IMSIC) └──────────┘ └──────┬───────┘ │ ┌───────▼───────┐ │ CPU Hart │ └───────────────┘addr— 决定发给谁哪个 Hart 的哪个特权级data— 决定哪个中断int_id0-2047addr 和 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 的设备都写同一个地址0x28000103IMSIC 收到后不关心是谁发的只根据 data 值决定 pending 哪个 bit。3. MSI vs MSI-X 硬件结构对比┌─────────────────────────────────────────────────────────────────┐ │ MSI老 │ │ │ │ 全部在 PCIe 配置空间 (Cap ID0x05) │ │ ┌──────────────────────────────────────┐ │ │ │ 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 ID0x11) 只存指针 → 指向 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_EN1 时) │ │ │ └────────────────────────────────────┘ │ │ │ │ 优点: 最多 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 → CapID0x11) ├─ 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 0Phase 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μsPhase 3 — 软件 ISRStep 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_id42 → nvme_isr pending ~(1 bit) Step 7: NVMe ISR nvme_isr(dev, vec2, int_id42): ├─ 读 NVMe 完成队列寄存器 → 处理完成项 └─ 清 pending: miselect 0x00 // 选 clreipnum 页 mireg 42 // 写即清: eip[42] 0 Step 8: sret 返回 → sstatus.SIE sstatus.SPIE → PC sepc6. 非 PCIe 设备传统线中断→ APLIC 转换IMSIC、LAPIC、GIC 都只接受 MSIMemory 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 DistributorGIC 有线输入 RISC-V: APLIC 转 MSI → IMSICIMSIC 完全没有线输入7. 三大架构中断控制器对比x86 LAPICARM GICv3ITSRISC-V IMSICMSI 地址编码addr[19:12]目标核addr 只是敲门砖addr[11:8]目标 Hart中断号来源data[7:0] 直接是向量EventID → ITS 查表 → LPIdata[10:0] int_id向量数224LPI 81922047 (per file)优先级仲裁硬件 IRR→ISR 自动硬件优先级仲裁器无硬件仲裁软件扫 eippending 清写 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. 关键设计要点MSI 的本质是 Memory Write— 不是独立协议PCIe 设备直接往中断控制器的 MMIO 地址写一个值。addr 决定路由data 决定身份— 所有设备共享同一个 IMSIC 地址软件靠 dataint_id区分中断来源。MSI-X 每条向量有独立 addr— 但实际驱动全部指向同一地址区分只靠 data。设备不知道中断号— 中断事件 → vec_id 是芯片固定映射vec_id → int_iddata是驱动初始化时写表决定的。RISC-V IMSIC 没有硬件仲裁— 只负责置 pending bit。优先级、嵌套全由软件扫描 eip 决定。换来极简硬件位图 OR 门。PBA 只在 MSIX_EN1 时 auto-clear— 卸载时必须先清 PBA 再关 MSIX_EN否则残留 pending bit 无法消除。弱序架构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, data10} SPI ──线──→ APLIC输入7 ──→ 源7 配成: {目标IMSIC_Hart1_S, data15} GPIO ──线──→ APLIC输入12 ──→ 源12配成: {目标IMSIC_Hart2_S, data20} Timer ──线──→ APLIC输入20 ──→ 源20配成: {目标IMSIC_Hart0_M, data5} ↑ ↑ 流片时就定了 软件初始化时写寄存器软件只干一件事告诉 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_BASE3*4,LEVEL_HIGH);// 配置目标发到 Hart 1 的 S-level IMSICuint64_ttarget_addrIMSIC_BASE|(18)|3;aplic_write64(APLIC_TARGET_BASE3*8,target_addr);// 配置 data 值中断号就用 10aplic_write(APLIC_SETIPNUM_BASE3*4,10);// 再注册 irq_mapirq_map[10]{.handleruart_isr,.devuart_dev};之后 UART 拉高中断线 → APLIC 自动发出Memory Write {addr: IMSIC, data: 10}→ IMSIC eip[10]1。10.5 和 PCIe MSI-X 对比PCIe MSI-XAPLICdata 存在哪BAR 里的 VectorTable[vec_id].msg_dataAPLIC 的 setipnum[src_id] 寄存器谁决定的驱动初始化时写入驱动初始化时写入硬件固定部分vec_id中断事件→表索引src_id中断线→APLIC 端口号软件可配部分msg_data发什么值setipnum发什么值思路完全一样硬件固定哪条线/哪个事件软件配置这个事件映射到哪个中断号。