ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RISC-V中断架构演进:从PLIC到AIA的APLIC与IMSIC迁移实践

RISC-V中断架构演进:从PLIC到AIA的APLIC与IMSIC迁移实践 RISC-V 的生态这两年确实热闹但中断控制器这块很多人还停留在 PLIC 时代。我最近把一颗自研核的中断路径从传统 PLIC 整体切到了 AIA 规范下的 APLIC 加 IMSIC过程挺折腾踩了不少坑也把整个架构吃透了不少。这篇就把迁移中的设计思考、寄存器层面的细节、软件适配的难点以及调试中遇到的一些诡异问题整理出来给正在做 RISC-V 中断方案选型或者准备迁移的朋友一个参考。1. 为什么要从 PLIC 迁到 AIA老架构的四个天花板先说结论如果只是做个简单的 MCU 或者单核、低中断数量的场景PLIC 完全够用没必要折腾 AIA。但如果你在做多核应用处理器、边缘计算芯片或者要给虚拟化方案做铺垫PLIC 的短板会越来越明显。1.1 多核中断负载均衡的缺失PLIC 的设计模式是典型的全局中断分发器。它维护了一张中断源到目标 hart 的映射表通过priority、enable、threshold、claim/complete这一套机制来分发中断。问题是这个分发逻辑是集中式的所有核的中断请求都汇聚到一个 PLIC 实例上由软件通过配置寄存器决定由哪个 hart 来处理。而且老 PLIC 的仲裁粒度很粗。它只能在“把某个中断源固定分配给某个 hart”这个层面做静态路由做不到像 ARM GIC 那样按优先级动态把中断负载分摊到多个核上。多核跑 Linux 的时候如果大量外设中断都路由到 CPU0CPU0 的软中断处理压力会非常大其他核却闲着。1.2 虚拟化支持先天不足PLIC 本身就是为裸机或简单 RTOS 设计的它只知道物理中断源不理解虚拟中断的概念。虚拟化场景下虚拟机里的驱动直接操作 PLIC 寄存器会产生语义冲突Guest 认为自己在 claim 中断实际上 claim 的是物理中断这会直接导致中断状态机错乱。要支持虚拟化就得在 Hypervisor 层面做大量 Trap-and-Emulate 的软件补丁性能损耗极大。AIA 架构里专门针对虚拟化做了设计IMSIC 支持按照vgeinvirtual guest external interrupt number来区分不同虚拟机的 MSI 中断。硬件帮你把中断按虚拟机隔离好Hypervisor 只需要做轻量级的转发。1.3 中断编号空间与可扩展性限制PLIC 支持的中断源数量是有限的一般几十到一百多个而且每个中断源的上下文context有限。当 SoC 里集成的外设越来越多比如多个 PCIe 控制器、GPU、AI 加速器每个都有大量 MSI/MSI-X 中断需求时PLIC 的中断源数量会成为硬瓶颈。AIA 用对 MSI 的可编程路由天然解决这个问题每个设备可以分配独立的 MSI 向量中断数量上限非常高。IMSIC 每个 hart 有独立的msi address空间基于index的寻址方式让中断号可以做到非常大比如 2048 个 per-hart而且可以按需扩展。1.4 中断延迟和上下文开销PLIC 的 claim/complete 流程需要访问两个不同的寄存器组期间有锁的开销而且不支持硬件自动屏蔽同源中断需要软件主动处理。在 Linux 这类 OS 里PLIC 驱动通常会启用标准的 IRQ chip 回调配合handle_cascade_irq做软件重分发而这同样会引入额外的延迟。IMSIC 的 MSI 投递路径更直接外设写一个 MSI 地址IMSIC 直接把中断挂到目标 hart 的中断 pending 队列上。没有集中式仲裁的往返时间也不需要软件查表决定给哪个核延迟表现会比 PLIC 好一截。当然这个延迟差异在小系统里不明显高负载下才体现得出来。2. AIA 架构核心概念APLIC 与 IMSIC 的分工逻辑AIA 架构把传统 PLIC 的活拆成了两部分APLIC 管“线控中断”IMSIC 管“消息中断”。这两个组件通过一组新增的 CSR 和异常码与操作系统交互理解它们的分工是后面做迁移的基础。2.1 APLIC承上启下的线控中断控制器APLICAdvanced Platform Level Interrupt Controller可以理解为 PLIC 的增强版但在架构上做了大改。它的定位是处理“基于电平/脉冲的线控中断源”包括GPIO、UART、I2C、SPI 等外设的中断请求线。APLIC 支持两种中断投递模式Direct mode直通模式APLIC 直接把线控中断转成中断信号发给目标 hart。这和传统 PLIC 比较像但格式和中断号映射规则变了。MSI mode消息中断模式APLIC 收到线控中断后由硬件根据配置好的目标地址产生一个 MSI 写操作发送到对应的 IMSIC 或者外部 MSI 控制器。这个模式下APLIC 本质上是把一个电平信号“翻译”成一个 MSI 报文。这里要注意APLIC 和 PLIC 一个关键区别在于中断号的分配。传统 PLIC 用全局中断源 ID比如interrupt id 33对应某个外设软件直接按这个 ID 操作寄存器。AIA 中进入 IMSIC 的是一组基于域的index线控中断经过 APLIC 的处理后在 MSI mode 下会被赋予一个 IMSIC 域中的中断号这个号由软件在初始化时通过配置 APLIC 的 sourcecfg 寄存器来设定。2.2 IMSIC每核独立的消息中断控制器IMSICIncoming MSI Controller是 AIA 引入的重头戏。每个 hart 都有一个自己的 IMSIC 实例它只负责接收发送给自己的 MSI 中断并维护一个本地的待处理中断队列。IMSIC 有几个关键特点第一每个中断使用guest index和virtual index两个维度来区分。物理中断使用index来标识虚拟中断额外加上guest index。这样多个虚拟机可以共享同一个物理 IMSIC但各自维护自己的虚拟中断表。这个设计直击虚拟化痛点。第二IMSIC 的中断使能和优先级的维护方式完全不同。PLIC 里的每个中断源有独立的 enable bit 和 priority 寄存器IMSIC 则是通过一组setipnum、setie_num寄存器来触发或屏蔽中断。中断的优先级由软件写入中断文件interrupt file对应索引位置的优先级值来决定。第三中断的 claim/complete 流程改成了基于getipnum寄存器。软件读取getipnum会同时获得最高优先级待处理中断的编号并自动将其标记为 in-service软件写入getipnum则完成中断处理。这个流程比 PLIC 的 claim/complete 两次寄存器访问更高效。2.3 AIA 新增的 CSR 和异常码迁移中绕不开的是新增的 CSR。AIA 在原有CSR 0xbc0mstatus中的MIE等已有位之外定义了0x7c0mintstatusMachine Interrupt Translation Status0x7c1mintthreshMachine Interrupt Translation Threshold0x7c2mintvecMachine Interrupt Translation Vector0xbc0扩展了mstatus的MPIE、MPP等原有字段之外还新增了MPV等用于虚拟化的位。异常码方面AIA 定义了一个新的 trap 原因码外部中断Guest External Interrupt在mcause中编码为 13之前 PLIC 使用的是 Supervisor External Interrupt 11。同时为虚拟化增加了vsinterrupt对应的原因码。这要求在异常向量表里新增对应的处理入口。注意老的 PLIC 驱动里cause 16的判断逻辑要小心处理AIA 下中断原因码从 12 开始就出现了新的含义直接用 mask 判断会踩坑。3. 迁移实战硬件侧寄存器配置和中断路由初始化这一节是全文的核心干货。我按照实际项目的迁移顺序从硬件初始化、中断路由配置到驱动适配逐步展开。3.1 初始化 APLIC 的寄存器序列APLIC 初始化时首先要根据 SoC 的中断源布局规划好中断号。在 AIA 规范中APLIC 的寄存器布局和 PLIC 完全不同它使用了一套基于偏移的sourcecfg、domaincfg、target寄存器。以我的项目为例SoC 里有 64 个线控中断源系统有 4 个 hart。在这里我接一个典型的初始化配置过程#define APLIC_BASE_ADDR 0x0C000000 #define APLIC_SOURCECFG_BASE 0x0000 #define APLIC_DOMAINCFG 0x0004 #define APLIC_TARGET_BASE 0x1000 void aplic_init(void) { uint32_t i; uint32_t base APLIC_BASE_ADDR; // 1. 配置全局域使能 APLIC writel(0x1, base APLIC_DOMAINCFG); // 2. 对每个中断源设置目标 hart 和优先级 for (i 0; i 64; i) { // 将中断源 i 路由到 hart 0使用中断号 i1 // 高 16 位写入 target hart低 16 位写入 interrupt id index writel((0x0 16) | (i 1), base APLIC_TARGET_BASE (i * 4)); } // 3. 每个中断源使能 for (i 0; i 64; i) { writel(0x1, base APLIC_SOURCECFG_BASE (i * 4)); } }这里和 PLIC 最大的区别是target寄存器的设置。PLIC 时代你需要在enable寄存器矩阵中把对应 hart 的 bit 置 1而且中断源的 ID 是固定的。AIA 里每个中断源通过独立的 target 寄存器指定目标 hart域内和该中断源在此 hart 上对应的index灵活度高了很多。每个中断源的使能也在sourcecfg寄存器中完成而不是在独立的 enable 寄存器中。所以初始化时要注意sourcecfg是“先配置、后使能”的顺序如果先把所有中断源都使能了再改 target可能导致中断乱飞。3.2 IMSIC 地址翻译与路由配置IMSIC 的配置核心是建立一个从 MSI 地址到具体中断索引的映射关系。AIA 规定IMSIC 的 MMIO 地址空间按guest index和hart index分页。每个 hart 的 IMSIC 区域可以看到2 * (num_guest_index 1)个 4KB 页面物理和虚拟中断各自有独立的 4KB 窗口。我的项目中IMSIC 基址为0x24000000每个 hart 的 IMSIC 占 4096 字节中断索引范围是 0 到 2047。#define IMSIC_BASE_ADDR 0x24000000 #define IMSIC_HART0_OFFSET 0x0 #define IMSIC_SETIPNUM_OFF 0x0 #define IMSIC_GETIPNUM_OFF 0x0 #define IMSIC_SETIE_NUM_OFF 0x10 #define IMSIC_MAX_INDEX 2047 void imsic_init(void) { uint32_t base IMSIC_BASE_ADDR IMSIC_HART0_OFFSET; int i; // 1. 读取 IMSIC 的版本和特性确认支持的最大 index uint32_t ver readl(base 0xFFC); // 按需校验中断数量 // 2. 设置中断文件的阈值对应每个索引的优先级 // 这里通过 setie_num 寄存器逐一配置 for (i 0; i IMSIC_MAX_INDEX; i) { // 配置优先级为默认值 0x42345678 (avg) writel(0x42345678, base IMSIC_SETIE_NUM_OFF (i * 4)); } }这里要注意一个细节setie_num寄存器不只是设置使能它实际上是把一个 32 位的interrupt file entry写入到指定索引的文件条目中其中高 16 位是优先级低 16 位是控制位。这个设计和 PLIC 单独设置 priority 寄存器的方式完全不同容易弄混。3.3 从 PLIC 到 AIA 的中断向量表和入口改写中断向量表的改动可能是迁移中最需要小心的部分。PLIC 时代mtvec或stvec指向一个统一入口在 trap handler 里通过读取mcause判断中断类型如果是外部中断再去读 PLIC claim 寄存器获取中断号。AIA 的改动点第一trap 原因码变了。在 AIA 之前外部中断的mcause值如下机器模式外部中断是 11监督模式外部中断是 9虚拟化场景外部中断是 13。实际上 AIA 规范中引入了新的 trap 原因Guest External Interrupt编码为 13。在mcause判断时不能再用传统的cause 0x1F简单区分了。第二AIA 模式下正常的机器模式外部中断的mcause仍然是 11但如果有虚拟化Guest 的外部中断会走 13。如果 Hypervisor 不特殊处理Guest 的中断会直接泄漏到 Host。所以异常向量表里11 和 13 的入口都要单独处理。我实际修改的中断入口部分逻辑如下void trap_handler(uint64_t mcause, uint64_t mepc, uint64_t mtval) { uint64_t cause mcause 0x1F; uint64_t is_interrupt (mcause 63) 0x1; if (is_interrupt) { switch (cause) { case 11: // Machine External Interrupt handle_machine_external(); break; case 13: // Guest External Interrupt虚拟化场景 handle_guest_external(); break; default: handle_other_interrupt(cause); } } else { // 异常处理 do_exception(cause, mepc, mtval); } }这里有个容易踩的坑在 AIA 规范下mcause的最高位63 位代表中断/异常标志但标准 RISC-V 中mcause是一个 XLEN 位的 CSR处理时最好把第 63 位拿出来判断不要直接和0x800000000000000B这类常量比较否则 32 位和 64 位模式下容易出错。3.4 AIA 中断的软件处理和平台设备的中断映射硬件寄存器配好后软件逻辑也要跟着变。PLIC 时代的中断处理函数长这样irq plic_claim(); // 读 claim 寄存器 handle_irq(irq); // 处理中断 plic_complete(irq); // 写 complete 寄存器AIA 下IMSIC 的处理流程是irq imsic_get(); // 读 getipnum 寄存器返回最高优先级待处理中断的 index handle_irq(irq); // 处理 // 写 getipnum 时写入要完成的中断编号或者读一次也会触发完成其实这里要比 PLIC 方便因为getipnum的读操作本身就会返回一个中断 ID 并自动完成硬件级别的中断屏蔽而写getipnum则完成中断嵌套的优先级恢复。Linux 内核的 IRQ 子系统适配中要重点修改irq_chip的回调static struct irq_chip imsic_irq_chip { .name IMSIC, .irq_mask imsic_mask, .irq_unmask imsic_unmask, .irq_eoi imsic_eoi, .irq_set_affinity imsic_set_affinity, };imsic_eoi回调里需要读取getipnum或者写getipnum来告知控制器中断处理完成。但要注意getipnum是个“读即清”的寄存器在 EOI 里千万别先读一次用于调试再写一次否则第一次读就把中断清了第二次写反而会触发一个未处理中断。4. 驱动迁移过程中的常见问题与排查技巧这一部分记录的是我在实际迁移中遇到的几个坑按概率排序最后一个问题排查了整整两天。4.1 中断号映射错乱APLIC 和 IMSIC 的 index 对齐这是迁移中最容易遇到的问题。在 PLIC 里传统做法是直接把中断源 ID 作为 Linux IRQ number简单粗暴。在 ABI 中APLIC 的线控中断源经过翻译后会得到一个 index这个 index 在 IMSIC 域内是一个稀疏分布的值不一定从 0 开始连续排列。如果板级设备树里写的中断号和设备驱动里注册时使用的中断号不一致要么中断触发后软件 Acknowledge 了错误的中断要么中断永远 no one care设备一直 busy poll。我们的项目里APLIC 的target寄存器就设定了 index 和好如果两个设备配置成相同的 index后配的那个会把先配的覆盖掉。解决办法每个中断源在设备树里声明时需要开辟独立的interrupts属性并且确保 APLIC target 寄存器里写入的就是这个中断号不能复用。4.2 非 MSI 外设中断在虚拟化下丢失在虚拟化场景如果虚拟机的设备是直通的它发送的是真正的 MSI没问题。但传统线控中断APLIC 翻译成 MSI 后送到哪个 IMSIC 的哪个 guest index需要软件提前配置好guest index和 pending 文件。我遇到的一次 Bug 是虚拟机里的 UART 中断Host 能收到但 Guest 永远收不到。排查后发现IMSIC 的 guest 中断需要单独开启对应 guest index 的中断文件使能位而且要在mintstatus里使能对应的 guest 外部中断转发通道。等到把 guest index 对应的一组寄存器全配好中断才能正常注入。4.3 IMSIC 的 EOI 时机导致中断风暴在一个网络驱动的压力测试中出现了持续不断的中断风暴CPU 占用率直接被打满。后来发现原因是外设中断触发后IMSIC 的getipnum会自动 pending 当前最高优先级中断但如果软件在 EOI 之前就重新使能了该中断源外设状态没有真正清掉就会立刻再次触发中断形成风暴。解决办法是在中断处理函数的最后才写 EOI而且要先屏蔽中断源等外设状态清理完毕再解除屏蔽。这个顺序不能反反了就会丢中断或者风暴。4.4mret时机与mstatus.MPIE的交互这个问题比较隐蔽。在 PLIC 时代mret之后中断是否重新可以触发取决于mstatus.MPIE置位情况。AIA 引入了mintstatus之后多了一层翻译状态。如果mintstatus中的某个位没有正确清除mret之后新中断没有办法被响应表现就是“中断只进来一次之后就再也进不来了”。排查方式在 trap 返回之前打印mintstatus的值对比正常和异常两种情况能很快定位到是不是因为 missing EOI 或者MINTSTATUS_MI位卡住了。4.5 排查工具建议迁移和调试过程中我发现纯靠打印效率太低。推荐几个辅助手段OpenSBI 的 DT 扫描OpenSBI 已经支持 APLIC/IMSIC 的初始化用它先验证硬件寄存器读写是否正确。JTAG 直接读 IMSIC 寄存器如果 SoC 的 JTAG 能看到 IMSIC 映射的 4KB 窗口直接读getipnum看中断是否有 pending。/perf 中断事件Linux 下用perf stat -e irq:irq_handler_entry看中断频率如果中断风暴这里数字会非常吓人。逻辑分析仪抓外设 IRQ 线确认 APLIC 的输入侧信号确实翻转排除外设侧的问题。5. 我对这次迁移的一些总结和想法整个迁移过程下来我的一个直接感受是AIA 不是 PLIC 的简单升级版它是一套为虚拟化和大规模多核设计的中断分发体系。PLIC 的设计哲学是“集中管理分散响应”所有中断源汇聚到一个控制器再分发到各个 hart。AIA 的设计哲学是“源头翻译本地处理”线控中断在 APLIC 做一次翻译变成 MSI 后直接投递到目标 IMSIC由目标核本地处理中间少了集中仲裁的开销。 这也意味着驱动开发者的思维方式需要跟着变从“操作一堆全局寄存器”转向“配置好翻译路径然后按文件和索引操作”。从性能角度讲IMSIC 的直投模式延迟确实比 PLIC 低特别是在开启了 MSI 的内核里UART、网卡这些中断源的响应时间都有可感知的下降。不过如果要拿数值吹牛需要在相同主频、相同外设负载下做 A/B 对比单纯从架构推导延迟低多少并不严谨。从可维护性看AIA 的配置项比 PLIC 多对软件初始化代码的编写要求更高。但一旦配好中断路由的灵活性远超 PLIC。特别是做多核负载均衡时可以在运行时动态调整 APLIC 的 target 寄存器把不同外设中断按负载重新路由到不同的核这在 PLIC 时代是做不到的。如果让我给正在评估迁移的朋友一句话建议如果只是单核裸机跑外设PLIC 别动如果已经在做或者准备做双核以上 Linux或者有虚拟化规划那从 IP 选型阶段就把 APLIC IMSIC 方案纳入省得后续从 PLIC 迁移过来重写驱动。最后再分享一个小技巧调试 APLIC 时利用它的domaincfg寄存器里的BIGEND字节序位可以切换寄存器的字节序在大小端混合的 SoC 里核对寄存器读写逻辑很管用。这个在规格书里很容易被忽略但排查硬件描述和软件访问字节序不一致时能省下至少半天时间。
返回列表