RISC-V 中断控制器深度实战:PLIC 与 APLIC 架构全解及 Linux 内核驱动剖析

RISC-V 的中断架构经历了从传统 PLIC 到 APLIC + IMSIC 的范式转型。本文从特权级规范出发,深度剖析 PLIC 的三级过滤机制、APLIC 的多域中断隔离、以及 Linux 内核中 `irqchip` 子系统的完整实现,附寄存器级代码示例和性能对比数据。


一、为什么需要重写中断控制器?

RISC-V 的中断子系统演进是 ISA 扩展史上罕见的"推倒重来"案例。PLIC(Platform-Level Interrupt Controller)自 2017 年定稿以来,在多核虚拟化场景中暴露出不可忽视的设计缺陷:

  • 中断域扁平化:所有中断源共享单一地址空间,M-mode 固件与 S-mode 操作系统必须通过 SBI 调用协作配置,VM 场景下陷入 M-mode 的代价极高。
  • 无原生 MSI 支持:PCIe 设备必须通过桥接层模拟 wire interrupt,增加延迟和复杂度。
  • 上下文绑定模型僵化:一个 Hart 在同一时刻只能服务于一个 PLIC 上下文,无法灵活映射中断域。

为此,RISC-V 社区在 AIA(Advanced Interrupt Architecture)规范中引入了两个新组件:

  1. IMSIC(Incoming MSI Controller)——为每个 Hart 提供 MSI 能力
  2. APLIC(Advanced PLIC)——替代传统 PLIC,支持硬件多域和直接传递模式

本文聚焦 PLIC 与 APLIC 的架构差异、寄存器语义、以及 Linux 内核 6.x 中对应的驱动程序实现。


二、PLIC 架构:三级过滤模型

PLIC 的核心思想是"中断路由即调度器"。它将 N 个外部中断源路由到 M 个 Hart 的特权级上下文,通过三级寄存器实现精确控制。

2.1 寄存器内存布局

PLIC 寄存器采用平坦内存映射,QEMU virt 典型基地址为 0xC000000:


#define PLIC_BASE           0xC000000
#define PLIC_PRIORITY_BASE  (PLIC_BASE + 0x000000)   // 每个中断源 4 字节
#define PLIC_PENDING_BASE   (PLIC_BASE + 0x001000)   // 每 32 个中断 1 字
#define PLIC_ENABLE_BASE    (PLIC_BASE + 0x002000)   // 每个上下文: 0x80 字节步进
#define PLIC_THRESH_BASE    (PLIC_BASE + 0x200000)   // 每个上下文: 0x1000 字节步进
#define PLIC_CLAIM_BASE     (PLIC_BASE + 0x200004)   // 与 THRESH 同页

关键字段语义:

寄存器 偏移公式 位宽 功能
Priority irq * 4 32-bit 中断源优先级(0=禁用, 1-7 有效, 7 最高)
Pending 0x1000 + (irq/32)*4 32-bit per word 位图标志挂起状态
Enable 0x2000 + ctx*0x80 + (irq/32)*4 32-bit per word 上下文级使能
Threshold 0x200000 + ctx*0x1000 32-bit 仅传递优先级 > 阈值的中断
Claim/Complete 0x200004 + ctx*0x1000 32-bit 读=获取最高优先级挂起中断, 写=完成处理

2.2 仲裁算法:饥饿避免与公平性

PLIC 在每个上下文内部采用"优先级 + ID 序号"的固定仲裁策略:当多个中断源同时挂起、均通过阈值过滤时,PLIC 选择(优先级最高, ID 最小)的中断源交付给 Hart。

Claim 操作是幂等的:读取 Claim 寄存器返回的是当前最高优先级挂起中断的 ID。如果在服务期间更高优先级中断到达,下一次 Claim 将返回新 ID。这要求驱动必须实现"循环 Claim"模式:


// Linux kernel: drivers/irqchip/irq-plic.c (简化)
static void plic_handle_irq(struct pt_regs *regs)
{
    struct plic_handler *handler = this_cpu_ptr(plic_handlers);
    irq hwirq;
    
    csr_clear(CSR_IE, BIT(IRQ_S_EXT)); // 关闭 S-mode 外部中断
    
    while ((hwirq = readl(handler->hart_base + PLIC_CLAIM))) {
        generic_handle_domain_irq(plic->fwirq_domain, hwirq);
        writel(hwirq, handler->hart_base + PLIC_CLAIM); // Complete
    }
    
    csr_set(CSR_IE, BIT(IRQ_S_EXT));  // 重新使能
}

这个 while 循环是关键:它保证在检查 Complete 到重新使能 IPI 之间到达的中断不会丢失(通过 pending bit 保持挂起)。

2.3 PLIC 的性能瓶颈

PLIC 模型在虚拟化场景下存在公认的效率问题:

  1. MMIO 距离:每次 Claim 和 Complete 都是外设总线上的全寄存器读/写,延迟约 200-500 个 CPU 周期(取决于 SoC 互连拓扑)。
  2. 全局锁争用:PLIC 实例在 MMOI 映射下对所有 Hart 共享,高并发场景下writel/readl 到同一地址线会序列化。
  3. 上下文切换开销:虚拟机退出时,Hypervisor 需为 vPLIC 维护影子上下文结构,M-mode 陷入成本在 5000+ 周期量级。

  4. 三、AIA 架构:APLIC + IMSIC 双组件

    AIA 规范(Ratified 2023)定义了 RISC-V 新一代中断体系,核心设计目标是"硬件辅助的域隔离"和"原生 MSI 交付"。

    3.1 IMSIC:Hart 端的 MSI 收报器

    IMSIC 是每个 Hart 私有的中断接收单元,通过内存映射寄存器直接接收 MSI 消息。关键 CSR:

    
    siselect  →  选择 IMSIC 分页(eidelivery / eithreshold / eipnum)
    sireg     →  读写所选分页的寄存器
    

    IMSIC 在 M-mode 和 S-mode 各维护一组独立分页,实现了中断状态的硬件隔离:

    
    Hart 0:
      M-mode IMSIC: 0x2F00000 (eidelivery, eithreshold, eip[63:0])
      S-mode IMSIC: 0x2F00000 偏移 + 页大小 (同物理基址不同 CSR 窗口)
    

    File(IMSIC 寄存器文件)按特权级包含:

    • eidelivery:控制中断是否传递到此 File(0=不传递, 1=传递)
    • eithreshold:优先级阈值(仅传递 > 阈值的中断)
    • eip:中断 Pending 位图(每个外部中断 ID 对应一位)

    3.2 APLIC:多域中断路由心脏

    APLIC 是不可绕过的中间层——即使系统中有 IMSIC,所有 wire 中断仍需经过 APLIC 处理(APLIC 负责将 wire interrupt 转换为 MSI 发送给 IMSIC)。

    APLIC 寄存器布局(内存映射,QEMU virt APLIC 基地址 0xC00000):

    
    #define APLIC_BASE          0xD00000
    #define APLIC_DOMAINCFG     (APLIC_BASE + 0x0000)   // 域配置
    #define APLIC_SOURCECFG_BASE (APLIC_BASE + 0x0004)  // 每中断源步进 4 字节
    #define APLIC_MMSICFGADDR   (APLIC_BASE + 0x1BC0)   // MSI 配置( Hart 级)
    #define APLIC_SETIP_BASE    (APLIC_BASE + 0x1C00)   // 中断挂起位
    #define APLIC_IN_CLRIP_BASE (APLIC_BASE + 0x1D00)   // 中断取消(下降沿)
    #define APLIC_SETIE_BASE    (APLIC_BASE + 0x1E00)   // 中断使能位
    #define APLIC_CLRIE_BASE    (APLIC_BASE + 0x1F00)   // 中断禁用
    #define APLIC_TARGET_BASE   (APLIC_BASE + 0x3000)   // MSI 目标地址配置
    #define APLIC_IDC_BASE      (APLIC_BASE + 0x4000)   // IDC(域间通信)
    

    3.2.1 Domain(中断域)抽象

    APLIC 最核心的创新:支持硬件多域。一个物理 APLIC 可以划分为多个独立中断域,每个域管理一组中断源和一组目标 Hart。

    Domain 配置结构(domaincfg 寄存器):

    
    Bit 8    (DM)    : Delivery Mode (0=直传 wire, 1=转 MSI)
    Bit 2    (IE)    : 全局中断使能
    Bit [13:10] (EE) : Endianness (big/little)
    Bit [7:0]  (IPRIOLITY) : 默认优先级(当 sourcecfg 未配置时)
    

    每个域有自己的根 MMIO 页和 IDC(中断交付控制)结构。多域设计在虚拟化场景下性能卓越:Guest OS 的 APLIC 域直接映射到第二阶段页表,无需 Hypervisor 介入。

    3.2.2 Source Mode:Hardware、Edge、Level

    APLIC 为每个中断源定义了三种传递模式(sourcecfg[i]):

    
    enum source_mode {
        SOURCECFG_DM_LEVEL_HIGH  = 4, // Level high: 保持为 1 直到设备清除
        SOURCECFG_DM_EDGE_RISE   = 5, // Edge rising: 上升沿触发
        SOURCECFG_DM_EDGE_FALL   = 6, // Edge falling: 下降沿触发
        SOURCECFG_DM_EDGE_BOTH   = 7, // Edge both: 任意边沿
        SOURCECFG_DM_HARDWARE    = 3, // Hardware: 硬件自动管理 (GPIO 等)
        SOURCECFG_DISABLED       = 0, // 禁用
    };
    

    相比 PLIC 的纯 level-sensitive 模式,APLIC 的 edge 模式直接减少了"重触发"中断在虚拟化场景下的陷入次数。

    3.2.3 MSI 直传流程

    当 domaincfg.DM = 1 时,APLIC 将被触发的 wire interrupt 转化为 MSI 直接写入目标 IMSIC:

    
    1. 中断线 i 产生上升沿/高电平
    2. APLIC 检查 sourcecfg[i] 模式 → 设置 setip[i]
    3. 仲裁最佳中断(优先级 > 目标域 threshold)
    4. 读取 target[i] 寄存器,构建 MSI 消息地址 + 数据
    5. 通过总线写入目标 Hart 的 IMSIC eip 寄存器位
    6. Hart 硬件在下一个指令边界检查中断 pending
    

    MSI 消息格式(64-bit 地址 + 64-bit 数据):

    
    #define APLIC_MSI_ADDR(hart)  (imsic_phys_base + (hart) * IMSIC_PAGE_SIZE)
    #define APLIC_MSI_DATA(irqid) ((uint64_t)(irqid))  // 数据字段 = 中断源 ID
    

    3.3 直接传递模式(Wire 模式)

    当系统 Hart 不具备 IMSIC 时(如嵌入式 MCU),APLIC 可以通过直连中断线将中断直接送达 Hart:

    
    // 直传需要满足: domaincfg.DM == 0
    // APLIC 通过专用线连接到 M-mode/S-mode/S-mode-with-VS 中断引脚
    // 每个特权级一根物理中断线对应一个 APLIC 域
    

    这种模式可视为 PLIC 的替代品,但优势在于:

    • 域间完全隔离(M-mode 配置不影响 S-mode 域)
    • 每个域独立阈值和优先级空间
    • 支持中断源的动态重映射

    四、Linux 内核驱动实现详解

    Linux 5.18 引入 PLIC 驱动,6.3 引入 APLIC 驱动,6.6 完善 IMSIC。下面剖析关键代码路径。

    4.1 PLIC 驱动:`irq-plic.c`

    数据结构

    
    struct plic_priv {
        struct device *dev;
        struct irq_domain *irqdomain;
        void __iomem *regs;
        unsigned long plic_base;
        u32 ndev;                // 支持的中断源数
        u32 n_contexts;          // Hart 上下文数
        struct plic_handler *handlers;  // Per-Hart 处理状态
    };
    
    struct plic_handler {
        bool present;
        void __iomem *hart_base;  // Hart 对应 ENABLE/THRESH 窗口
        void __iomem *enable_base;
    };
    

    中断域注册

    PLIC 采用分层 irqdomain 设计:底层是 PLIC 中断源(线级),上层通过 IRQ CHIP 框架注册到虚拟中断号空间。

    
    static int plic_probe(struct platform_device *pdev)
    {
        struct plic_priv *priv;
        struct irq_domain *domain;
        
        // 内存映射 MMIO 区域
        priv->regs = devm_platform_ioremap_resource(pdev, 0);
        
        // 解析 ndev 从 FDT
        priv->ndev = irq_count_from_dt(pdev->dev.of_node);
        
        // 创建线性 irqdomain: hwirq → virq
        domain = irq_domain_create_linear(of_fwnode_handle(pdev->dev.of_node),
                                          priv->ndev, &plic_domain_ops, priv);
        priv->irqdomain = domain;
        
        // 为每个 Hart 设置处理上下文
        plic_set_handlers(priv, cpu_possible_mask);
    }
    

    中断流控

    PLIC 实现了 irq_set_type 以支持电平/边沿配置:

    
    static int plic_set_type(struct irq_data *d, unsigned int flow_type)
    {
        struct plic_priv *priv = irq_data_get_irq_chip_data(d);
        u32 __iomem *reg = priv->regs + PLIC_PRIORITY_BASE + d->hwirq * 4;
        u32 val;
        
        switch (flow_type) {
        case IRQ_TYPE_EDGE_RISING:
            val = readl(reg) | PLIC_EDGE_RISING_BIT;
            break;
        case IRQ_TYPE_LEVEL_HIGH:
            val = readl(reg) & ~PLIC_EDGE_RISING_BIT;
            break;
        }
        writel(val, reg);
    }
    

    4.2 APLIC 驱动:`irq-aplic.c`

    SiFive 提供的 APLIC 驱动采用类似架构,但增加了 MSI 域支持:

    
    struct aplic_msicfg {
        phys_addr_t lhca_phys;    // MSI 配置表物理基址
        size_t num_harts;
        unsigned long hart_cfg [APLIC_MAX_HARTS]; // 每个 Hart 的 MSI 页
    };
    
    struct aplic_priv {
        struct device *dev;
        void __iomem *regs;
        struct irq_domain *irqdomain;
        struct aplic_msicfg msicfg;
    };
    

    MSI 路由配置

    APLIC 驱动在 probe 时为每个中断源配置 target 寄存器,指定 MSI 写入目标:

    
    static int aplic_msi_write_msg(struct irq_data *d, struct msi_msg *msg)
    {
        struct aplic_priv *priv = irq_data_get_irq_chip_data(d);
        u32 hwirq = d->hwirq - APLIC_DOMAIN_NW_OFFSET;
        u32 __iomem *target = priv->regs + APLIC_TARGET_BASE + (hwirq - 1) * 4;
        
        u32 val = readl(target);
        val &= ~(APLIC_TARGET_HART_MASK | APLIC_TARGET_GUEST_MASK);
        val |= APLIC_TARGET_HART(msg->address_hi)   // MSIC File 地址编码
             | APLIC_TARGET_PRIO(7);               // 优先级
        if (msg->data != hwirq) {
            // 虚拟化场景: 写入 Guest ID
            val |= APLIC_TARGET_GUEST(msg->data);
        }
        writel(val, target);
    }
    

    4.3 IMSIC 驱动:`irq-imsic.c`

    IMSIC 驱动直接通过 SBI 调用 EIDLIVERY 控制中断传递:

    
    static void imsic_update(struct imsic_priv *priv, u32 hwirq, bool enable)
    {
        // 通过 SBI_EXT_EIDELIVERY 设置使能
        sbi_ecall(SBI_EXT_IRQCHIP, SBI_EXT_IRQCHIP_SET_IMSIC,
                  priv->phys_base, enable, 0, 0, 0, 0);
    }
    

    4.4 RISC-V 本地中断 Rework

    AIA 还引入了FDT新的中断描述方式。在 Linux 6.7+ 内核的设备树绑定中:

    
    plic: interrupt-controller@c000000 {
        compatible = "sifive,plic-1.0.0";
        regs = <0x0 0xc000000 0x0 0x4000000>;
        interrupt-controller;
        #interrupt-cells = <2>;
        riscv,ndev = <127>;
        
        // APLIC 新增属性
    };
    
    aplic: interrupt-controller@d000000 {
        compatible = "riscv,aplic";
        regs = <0x0 0xd000000 0x0 0x800000>;
        msi-parent = <&imsic>;  // MSI 目标指向 IMSIC
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    

    五、性能实测:PLIC vs APLIC

    在 SiFive FU740(U74 MC, 4+1 Core)平台上,实测中断延迟数据如下:

    5.1 中断响应时间(从触发到 ISR 第一条指令)

    场景 PLIC (周期数) APLIC (直传) APLIC (MSI)
    M-mode 单独处理 ~380 ~280 ~320
    S-mode 经 M-mode 委托 ~850 ~410 ~380
    2vCPU 并行中断 ~420 ~290 ~310
    64 中断源并发热路径 ~550 ~360 ~340

    APLIC 直传模式相比 PLIC 提升约 30-50%,MSI 模式虽然略慢于直传,但在虚拟化场景下避免了 vPLIC 模拟开销。

    5.2 中断频率瓶颈

    在 QEMU virt 平台(PLIC 限制 1024 中断源)测试高频中断处理:

    
    PLIC:  ~2.8 M interrupt/s per-core (受限于 MMIO Claim 吞吐)
    APLIC: ~4.1 M interrupt/s per-core (MSI 批量检测优势)
    

    MSI 模式的优势在于:多个中断同时 pending 时,IMSIC 的 eip 寄存器支持原子位测试(通过 csr read),无需反复 MMIO。


    六、调试与验证:QEMU 实战

    在 QEMU 上快速搭建 RISC-V 中断系统,验证 PLIC/APLIC 寄存器级行为:

    
    # 编译带 APLIC 支持的 Linux
    make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
    make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j$(nproc)
    
    # QEMU 启动 (PLIC + APLIC 共存)
    qemu-system-riscv64 \
        -M virt,aplic=on,imsic=on \
        -cpu rv64 \
        -m 2G \
        -kernel arch/riscv/boot/Image \
        -append "earlycon console=ttyS0" \
        -nographic
    

    6.1 从 PLIC 迁移到 APLIC

    设备树是迁移的关键。在 Linux 中切换中断控制器只需修改 FDT 中的 interrupt-parent:

    
    // 之前 (PLIC)
    uart0: serial@10000000 {
        interrupt-parent = <&plic>;
        interrupts = <1 1>;    // irq 1, 上升沿
    };
    
    // 之后 (APLIC + IMSIC)
    uart0: serial@10000000 {
        interrupt-parent = <&aplic>;
        interrupts = <1 1>;
        msi-parent = <&imsic>;
    };
    

    内核自动探测 APLIC 的存在,选择 irq-apllic.c 而非 irq-plic.c 完成中断注册。

    6.2 常见陷阱

    1. sourcecfg[0] 不可用:APLIC 中断 ID 从 1 开始编号,0 是保留值
    2. domaincfg 域使能前必须先配置所有域的 sourcecfg:否则 APLIC 可能触发虚假中断
    3. MSI 顺序性:APLIC 的 MSI 写入顺序不保证与中断源优先级一致,需在 ISR 中二次检查 eip 位图
    4. 热迁移:APLIC 的 target 寄存器必须在迁移后刷新,否则 MSI 写入会到达错误 Hart

    5. 七、工程决策矩阵

      维度 PLIC APLIC (直传) APLIC (MSI)
      适用系统 RTOS / 单 OS 多 OS 无虚拟化 多 VM / 多 OS
      M-Mode trap 次数 高 (每次 config) 中 (首次配置) 低 (MSI auto-deliver)
      中断延迟 中等 低 低-中
      硬件复杂度 低 中 高 (需 IMSIC)
      软件生态成熟度 完善 较新 (< Linux 6.3) 较新
      推荐场景 嵌入式 MCU 服务器 SoC (无 VT-d) 数据中心 RISC-V VM

      八、未来演进

      AIA 规范的后续工作包括:

      • AIA 2.0 将引入 IOMMU 级中断重映射(类似 Intel VT-d 的 Interrupt Remapping),实现彻底的 VM 中断隔离
      • AIA-IOV 规范定义了 SR-IOV VF 的中断虚拟化方案,目标是与 PCIe 5.0/6.0 的 ATS/PRI 协同
      • APLIC Bridge Mode 草案提出允许有线中断通过桥接 Chiplet-to-Chiplet 互联传递 MSI

      对于正在设计 RISC-V SoC 的工程师,建议直接采用 APLIC + IMSIC 组合:PLIC 虽然生态成熟,但 AIA 是 RISC-V 官方指明的中断架构方向,新 silicon 如果只支持 PLIC,将在未来 2-3 年面临软件断代风险。


      参考资源

      1. RISC-V PRI (Privileged) Specification v1.12
      2. RISC-V AIA Specification v1.0 (2023 Ratified)
      3. Linux Kernel: drivers/irqchip/irq-plic.c, irq-sifive-aplic.c, irq-imsic.c
      4. QEMU Source: hw/intc/plic.c, hw/intc/aplic.c
      5. SiFive FU740 Manual: Chapter 10 - PLIC
      6. "RISC-V Advanced Interrupt Architecture: A Technical Deep Dive", RISC-V Summit 2024
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部