ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RISC-V中断控制器从PLIC到AIA的迁移实战:APLIC与IMSIC配置要点

RISC-V中断控制器从PLIC到AIA的迁移实战:APLIC与IMSIC配置要点 最近给一颗RISC-V SoC做linux中断控制器适配设备树里原本的PLIC节点整个被换成了AIA规范下的APLIC和IMSIC。干之前我以为就是个“寄存器换皮”的事真正动手才发现从PLIC的编程模型切到APLIC/IMSIC牵扯到中断投递路径、MSI地址计算、设备树中断属性、还有驱动里claim/complete的时机几乎每一层都得重过一遍。这篇文章把我从“拿PLIC经验硬套AIA”到“真正调通中断”的过程梳理了一遍重点放在设备树改造、APLIC侧编程、IMSIC侧收中断以及实践里踩过的几个坑。适合正在做RISC-V中断控制器适配、外设中断驱动移植或者准备把老SoC里的PLIC往AIA架构迁移的工程师。1. 从PLIC迁到AIA动手前必须想清楚三件事1.1 PLIC到底卡在哪里AIA又怎么解传统PLIC在RISC-V生态里用了很多年核心作用是把多个外设中断线的优先级仲裁好再定向投递给某个hart。它的模型很简单外设触发中断线PLIC检查使能位和优先级然后把中断挂起软件读claim寄存器拿到中断号处理完写complete。这套流程本身没什么问题但有几个硬伤。第一是不支持MSI。PCIe、NVMe这类现代设备都依赖MSI/MSI-X做中断设备直接写一个消息到目标地址就能产生中断。PLIC只认电平边沿没法接住这种“内存写触发中断”的形式导致很多高速外设被迫退化成INTx线。第二是虚拟化支持太弱。PLIC没有“域”的概念没法把中断源按特权级别切分。在虚拟机场景下hypervisor要把外设中断直接投递给guestPLIC做不了只能靠软件转发延迟很高。第三是路由粒度太粗。PLIC虽然可以把同一个中断源定向到多个hart但具体到“哪个特权模式”、“哪个中断文件”它管不了。AIA就是为了解决这三件事出来的。AIA定义了两类新硬件APLIC负责收集平台级外部中断源IMSIC负责接收MSI并递交给hart。再加上一组新的CSR和间接寄存器机制从根上补上了MSI和虚拟化的缺口。1.2 迁移前先确认三件事CPU、特权级、工具链不要一上来就改设备树先确认平台条件。AIA是一个完整的架构扩展不是只换个中断控制器节点就能用。我的经验是开工前把下面三个问题查清楚。第一CPU核是否在硬件里实现了AIA扩展。AIA不是说APLIC支持MSI就行CPU侧也要配套支持IMSIC的访问和对应CSR。如果CPU本来就不支持AIA那配了APLIC/IMSIC也白搭。怎么看到数据手册里查有没有IMSIC基址看看miselect、mireg这些CSR是不是真实存在或者看看SoC里的CPU中断输入有没有接IMSIC的触发信号。最直接的办法是QEMU里跑一下AIqemu的virt机型从7.2开始就默认带AIA。第二确认你要跑哪个特权级的中断。如果只是M模式裸机或者RTOS中断文件用file0就行如果是Linux这种跑S模式主要用file1。这个选择直接决定IMSIC中断文件页的计算也决定设备树里interrupts-extended怎么写。第三工具链和内核版本。AIA在Linux内核里是最近几个版本才逐步完善的APLIC和IMSIC的irqchip驱动也是这样。如果内核版本太老建议把drivers/irqchip/irq-riscv-aplic.c和irq-imsic.c相关补丁合进来不然就得自己写驱动了。交叉工具链的话binutils和gcc对AIA CSR的支持不太一样老工具链可能连mvien、mvip这些寄存器名都不认识编译时报错。2. APLIC和IMSICAIA中断架构的全景拆解2.1 APLIC不是“PLIC改名”它是PLIC升级版加MSI生成器我刚看APLIC这个名字时以为它就是把PLIC寄存器规范重新整理了一遍。实际看下来APLIC的定位更像是“带MSI生成功能的高级平台级外部中断汇集点”。它仍然挂在所有外设中断线和CPU中间但投递方式从唯一一种变成了两种。一种是Direct模式跟PLIC非常像APLIC把中断源仲裁后通过某条中断线直接送给指定hart。软件通过APLIC的claim寄存器读中断号处理完再complete。好处是迁移成本低老驱动逻辑几乎不动。另一种是MSI模式这是AIA真正的价值所在。APLIC收到外设中断源事件后不再“拉线”通知CPU而是根据target寄存器里配置的目标地址主动发起一次地址写入。这个写入的目标就是某个IMSIC的中断文件页地址等价于设备自己写MSI。CPU看到的是MSI进来的外部中断APLIC在里面充当了“老设备到MSI世界的翻译官”。APLIC还引入了一个PLIC完全没有的概念域Domain。一个APLIC可以被切成多个域每个域拥有独立的使能、优先级、目标和模式配置。虚拟化场景下可以把一组设备中断归给M-mode域另一组归给S-mode域还有一组归给guest域。普通系统里一般只用域0照葫芦画瓢就行。2.2 IMSIC给每个hart配一个“MSI信箱”IMSIC全称是Incoming MSI Controller我习惯叫它“MSI信箱”。每个hart都有自己独立的IMSIC它负责接收APLIC或者设备直接发来的MSI写事务再把中断挂起位置位。IMSIC内部不是简简单单一个“收到就置位”的盒子它把MMIO地址空间切成了多个中断文件Interrupt File。每个文件占一页4KB不同文件对应不同特权模式。常规设计中file0是M-mode外部中断file1是S-mode外部中断再往上可能还有VS-mode等guest中断文件。一个MSI能投递到哪个文件完全取决于写的是哪个页面的地址。中断文件里每个中断号占4字节偏移。比如file1里中断号5对应的MSI写入地址就是IMSIC基地址 1 * 4KB 5 * 4。这个“页内偏移等于中断号乘4”的设计让MSI投递变得非常轻量设备写一次内存事务就把中断送进某个hart了不需要软件参与。2.3 一次完整MSI中断的路径画在脑子里的图理解AIA中断最有效的方法就是把一次MSI中断从头到尾走一遍。我调稳定之后自己画了一条路径外设中断源触发APLIC的pending位置1。APLIC检查该中断源的使能位和优先级通过域配置找到目标。如果域配置为MSI模式APLIC按照target寄存器中预写的地址发起对IMSIC的写入。IMSIC在对应中断文件页上收到这次写事务把该文件中对应中断号的pending位置位同时把这个外部中断映射到hart的mip/sie对应位。CPU的中断控制器发现外部中断pending且使能跳转到mtvec/stvec。软件进入中断处理读取APLIC的claim寄存器得到中断号。处理完外设中断写APLIC的complete寄存器清掉这条pending。中断返回。对比传统PLIC路径最大的区别在第3和第4步中断信号不是靠一根线“拉高”送进hart的而是变成了一次内存写。这条路径带来的收益是中断源和hart之间不再需要长距离的物理线连设备/MSI控制器可以通过总线路由自由选择目标hart这对多核异构SoC特别友好。3. 实操第一步设备树与启动阶段的适配3.1 芯片点灯先过“设备树关”从PLIC迁移到AIA设备树是第一个动手点。我先把原来PLIC的节点摘出来plic: interrupt-controllerc000000 { compatible riscv,plic0; reg 0x0 0xc000000 0x0 0x400000; interrupts-extended cpu0 11, cpu1 11; interrupt-controller; #interrupt-cells 1; riscv,max-priority 7; riscv,ndev 64; };改成APLIC节点后主要差异是compatible变成riscv,aplic中断属性变成两段式中断号加触发类型aplic: interrupt-controllerc000000 { compatible riscv,aplic; reg 0x0 0xc000000 0x0 0x4080; interrupts-extended cpu0 3, cpu1 3; interrupt-controller; #interrupt-cells 2; riscv,num-sources 64; riscv,children imsic0 imsic1 imsic2 imsic3; riscv,delegate imsic0 1, imsic1 1, imsic2 1, imsic3 1; };注意interrupts-extended里的3是CLINT/PLIC时代就沿用的外部中断号。在RISC-V里外部中断对应的irq号一直是3S-mode external11是机器外部中断的irq号这里不要想当然改。初次上手时最容易犯的错就是把interrupts-extended里的中断号改成APLIC内部的中断源号完全不是一码事。3.2 每核挂一个IMSIC节点APLIC节点改好还不够IMSIC节点必须跟着上而且得给每个hart都挂一个。参考写法imsic0: msi-controller24000000 { compatible riscv,imsics; reg 0x0 0x24000000 0x0 0x4000; msi-controller; interrupts-extended cpu0 11; riscv,guest-bits 0; riscv,file-bits 2; };reg里的地址IMSIC设备树节点通常要求把整个harts的中断文件空间都映射出来驱动会按hart编号计算各自的基地址。riscv,file-bits表示中断文件个数的log2riscv,guest-bits是guest中断文件用的位数不做虚拟化直接写0。改完设备树后还要留意中断号编码。传统PLIC因为只能传一个中断号所以#interrupt-cells 1AIA为了表达“中断号触发方式”一般用2。所有外设子节点的interrupts属性都得跟着改比如UART原来是interrupts 10现在要写成interrupts 10 1后面的1是上升沿触发。别小看这个细节漏改一个节点启动日志里就会多一个“no irq domain found”的报错。3.3 启动日志里认一认AIA的痕迹设备树改完之后内核启动日志是验证第一道关卡。正常能看到[ 0.000000] riscv-aplic: applying MSI-mode domain to IMSIC0或者[ 0.000000] irq: no irq domain found for interrupt-controllerc000000 !第一条说明APLIC进入了MSI模式并且和IMSIC绑定成功。第二条是典型的节点没配对或者compatible写错。内核启动后还可以检查/proc/interrupts看设备中断是不是挂到了imsic控制器下而不是plic。如果这里还是显示plic多半是驱动没有走新的irqchip路径需要查一下interrupt-parent是不是还在指向老PLIC节点。4. 实操第二步APLIC寄存器编程要点4.1 先认识APLIC的寄存器族APLIC的寄存器体系比PLIC复杂不少但走一遍后会发现套路很清晰。最常用的是下面这几个寄存器名作用domaincfg配置域模式Direct/MSI、域使能等每个域一组sourcecfg配置每个中断源的触发方式电平/边沿、归属域等target配置中断源的目标hart和MSI目标地址/中断号source设置/清除中断源的pending状态可软件触发测试interrupt-enable中断源使能位图pending只读查看哪个中断源正在pendingclaimi读一次返回当前最高优先级待处理中断号并隐式清除complete写中断号表示处理完成这些寄存器不是所有都用到最小配置只需要domaincfg、sourcecfg、target、interrupt-enable和claim/complete。先拿着这几个动手跑通了再扩展。4.2 Direct模式配置流程先求“能亮”迁移初期个人强烈建议先把全部中断源配成Direct模式。为什么因为Direct模式下APLIC的claim/complete流程和PLIC几乎一样老驱动思路能直接复用先让功能跑起来再逐步切MSI。一个典型Direct模式配置流程伪代码大概是#define APLIC_DOMAINCFG 0x0000 #define APLIC_SOURCECFG 0x0004 #define APLIC_TARGET 0x3000 #define APLIC_IRQ_ENABLE 0x2000 #define APLIC_CLAIMI 0x1C00 #define APLIC_COMPLETE 0x1C04 // 1. 配置domaincfg域0选择Direct模式 writel(0, APLIC_BASE APLIC_DOMAINCFG); // 2. 配置sourcecfg中断源1上升沿触发 writel(0x1, APLIC_BASE APLIC_SOURCECFG 4 * 1); // 3. 配置target目标hart0外部中断号53 // 这里写的是domaincfg或目标配置寄存器各芯片实现略有差异 writel(0x00000000, APLIC_TARGET 4 * 1); // 4. 使能中断源1 writel(0x1, APLIC_IRQ_ENABLE 0 * 4); // 5. 中断处理函数里claim再complete int irq readl(APLIC_BASE APLIC_CLAIMI); // 按照irq分发处理... writel(irq, APLIC_BASE APLIC_COMPLETE);注意Direct模式下target寄存器的内容通常前半部分是hart ID后面是中断号。各家SoC对域和中断号字段的偏移定义不完全一样必须对照芯片手册。写错目标hart的中断号轻则中断收不到重则打到别的核上特别难排查。4.3 MSI模式配置流程给设备配一个“投递地址”Direct模式验证通则后再切MSI模式。MSI模式和Direct模式唯一的“物理区别”在于APLIC拿到中断源事件后不再走线而是改写IMSIC的地址。所以配置上只需要把domaincfg的模式位改成MSI同时target寄存器里写的不再是“目标hart编号”而是一个完整的目标MSI地址。以4核SoC为例假设IMSIC0的M-mode中断文件基地址是0x24000000S-mode中断文件基地址是0x24004000我们要把中断源1投递给hart0的S-mode外部中断号16那么MSI目标地址就是0x24004000 16 * 4 0x24004040把这个地址写进APLIC的target寄存器domaincfg切到MSI模式使能中断源整个投递路径就建立了// 切换域0为MSI模式 writel(0x1, APLIC_BASE APLIC_DOMAINCFG); // 中断源1的目标MSI地址hart0的S-mode file 0x24004000 中断号16*4 writel(0x24004040, APLIC_TARGET 4 * 1); // 使能中断源1 writel(0x1, APLIC_IRQ_ENABLE 0 * 4);这里最关键也是最容易错的地方就是中断文件页的选择。S-mode中断文件不是“默认页”在IMSIC地址布局里每个文件占一页M-mode文件通常是第0页S-mode文件在第1页。地址算错一位中断就送进了M-mode信箱S-mode这里死活等不到。4.4 优先级、使能与中断控制别只盯着APLICAIA比PLIC多了一条新的优先级配置路径。传统PLIC有priority寄存器APLIC也保留了类似的域优先级我一开始只调这个发现MSI模式下完全不生效。后来查资料才知道AIA还把中断优先级下沉到了IMSIC/CPU侧通过miselect选择索引mireg读写优先级。只有两边都配置过中断优先级才能完整约束。具体用法大致是// 选择要操作的间接寄存器索引这里对应中段优先级寄存器 write_csr(miselect, 0x30); // 写入优先级值 write_csr(mireg, 0x10);优先级这块建议按“平台优先级为主、CSR优先级为辅”的思路配。外设之间争抢CPU响应顺序主要看APLIC的优先级仲裁CPU内部是否允许被更高优先级打断再看mireg。裸机里如果不用嵌套中断CSR优先级可以统一配低少踩很多坑。5. 实操第三步IMSIC侧的中断投递与处理5.1 IMSIC的地址空间不搞懂这个后面全白搭IMSIC的地址空间是最绕的一块但它其实就一个核心思想每个hart一个IMSIC每个IMSIC一组中断文件每个文件一页4KB每4字节一个中断号。以4核CPU为例IMSIC基地址从0x24000000开始假定每个IMSIC占8个4KB页那么hart基地址M-mode文件地址S-mode文件地址hart00x240000000x240000000x24004000hart10x240080000x240080000x2400C000hart20x240100000x240100000x24014000hart30x240180000x240180000x2401C000这个地址一算很多事就通了为什么APLIC只要写一次MSICPU就立刻收到中断。因为它本质就是往某个hart专属的“信箱页面”里写了一个字。设备侧直接写MSI的话逻辑也一样设备驱动里配置好这个地址就能做到“写内存即中断”。5.2 自己动手写一个MSI验证IMSIC通不通调IMSIC时别急着接真实外设先拿软件写一个MSI验证链路。我记得最有效的一个测试是在初始化完成后手动往S-mode中断文件地址写值uint32_t *msi_addr (uint32_t *)0x24004040; // hart0 S-mode中断文件中断号16 *msi_addr 0xdeadbeef; // 内容无所谓地址决定了中断如果写下去之后CPU的中断处理函数马上被触发就说明IMSIC的接收链路没问题。这个技巧用来区分“是IMSIC没通”还是“是外设中断源没到APLIC”特别方便。我迁移前期一度怀疑APLIC配置不对结果用手写MSI一测IMSIC早就通了问题出在APLIC的source连接上。5.3 中断处理函数里claim和complete的顺序AIA的claim流程和PLIC有个小差别特别容易踩。传统PLIC的做法是先读claim拿中断号处理完写complete。AIA在MSI模式下如果APLIC生成MSI给了IMSIC那APLIC侧的pending和IMSIC侧的pending是两回事。第一次切换时我只清了APLIC的complete结果IMSIC侧中断一直挂起中断处理函数反复进入死循环。后面总结出的稳定流程是读取APLIC的claimi寄存器拿到中断号这同时清了APLIC侧pending。根据中断号处理外设。写APLIC的complete完成APLIC侧清除。如果有必要再操作IMSIC的对应位把文件内的pending清掉。具体清IMSIC pending的方式由驱动和芯片实现共同决定有的在读取IMSIC的claim时自动清有的要求写同一个地址。这块必须查芯片手册不能想当然。5.4 向量中断和间接寄存器能迟点用就迟点用AIA的完整能力还包括向量中断每个中断源可配置单独入口和通过miselect/mireg访问的高级寄存器。这些功能对降低中断延迟很有帮助但迁移初期建议一律不启用。我的体会是向量中断需要修改mtvec/stvec的向量模式并维护一张完整的中断入口表一旦把优先级和向量表配错排查成本成倍上升。先把基本MSI流程跑通等系统稳定了再逐项开启。不过间接寄存器本身一定要认识因为内核AIA驱动经常会用到它们来判断当前是哪个中断被激活。至少知道miselect是索引mireg是数据面后续看日志不再懵。6. 迁移踩坑实录中断号对不上、中断丢失、优先级失效6.1 中断号突然“漂移”了原来是不止一套编号迁移后第一件怪事原来PLIC下设备中断号是10APLIC配好后驱动居然等不到但是/porc/interrupts里面明明显示有中断在pending。后来我一层层打日志才定位到问题出在中断编号体系不一致。在PLIC时代Linux把PLIC的中断源编号直接映射成了Linux irq number中断号10就是设备树里写的10。到AIA以后APLIC的中断源号先要映射成Linux irq number中间多了一层。如果驱动和设备树里对不上或者设备树节点里interrupts属性仍然沿用旧的#interrupt-cells 1写法就会出现中断号错位。解决思路很朴素先读cat /proc/interrupts看当前设备实际拿到的irq号再反查APLIC的source编号把设备树里的interrupts属性改成一致。别迷信“我原来PLIC是多少AIA就应该是多少”。6.2 MSI中断“发了一下就没下文”八成是文件页选错另一个高频问题是中断触发了一次之后再也触发不了或者干脆一次都不触发。我自己的案例就栽在IMSIC中断文件页计算上。当时裸机测试往0x24004000写数据能进中断我以为配置没问题。结果一接真实外设APLIC的MSI模式怎么配都不触发。最后对照手册发现IMSIC的S-mode中断文件页基地址在我的芯片上是0x24004000但APLIC的target寄存器里除了地址还有一个字段专门用来指示这个MSI要写入哪个中断文件的文件编号。我target里填的是文件页的物理地址可APLIC按文件编号去索引两边对不上MSI发到了文件0M-modeS-mode自然收不到。查这种问题最笨也最有效的方法是先用手写MSI打到一个horn看它响应的是M-mode还是S-mode。如果响应的是M-mode说明文件编号填错了往S-mode文件号调整就行。6.3 优先级配置“看着生效实则没生效”AIA的优先级机制分散在APLIC、IMSIC和CPU CSR三侧很容易出现“改了A侧B侧没配”的情况。我一次测试中想提高某外设中断的优先级在APLIC的域优先级寄存器里写了大值结果还是被另一个低优先级中断打断。深挖后发现APLIC域优先级只决定了多个pending中断源之间的仲裁顺序真正决定“CPU当前中断能否被更高优先级中断抢占”的是AIA在CSR侧的优先级寄存器。要支持嵌套中断必须把抢占优先级配到mireg对应的字段里同时打开mstatus的中断全局使能。光改APLIC做不到抢占。如果要快速验证优先级建议先把所有中断的CSR优先级调成一样只留APLIC域优先级做仲裁看中断源之间的服务顺序是否符合预期。这样能减少干扰项。6.4 老驱动还想走PLIC过渡期怎么兼容如果迁移的SoC里既有AIA控制器又保留了PLIC兼容路径会有一个比较尴尬的阶段新设备树把控制器切成了APLIC但某个老驱动还是按PLIC claim/complete流程写死的中断处理。这种驱动轻则中断行为怪异重则直接死循环。过渡期我的做法是加一段“兼容检测”中断处理函数里先判断当前控制器的状态寄存器如果是APLIC且MSI模式就按AIA流程走否则退回PLIC流程。从工程进度上这个兼容层能让你不用一次性改完所有驱动先把主线功能跑起来再逐步替换。等全部驱动都适配好兼容层删掉即可。现象排查方向中断完全没有进来APLIC的domaincfg是否设成了MSIIMSIC中断文件编号CPU中断总使能中断进来一次后不再来complete没写干净或者IMSIC侧pending没清中断号对不上设备树#interrupt-cells是否改为2interrupts触发类型字段是否匹配优先级行为不对APLIC域优先级和miregCSR优先级是否都配置多核时中断打到错误的核target寄存器里的hart ID是否写对设备树interrupts-extended顺序是否和核编号一致7. 迁移后的体验与一点个人建议折腾完这轮迁移我最大的感受是AIA并没有发明一套全新的中断哲学它只是把“中断就是一次内存事件”这个想法贯彻到底了。PLIC解决的是“多源仲裁”IMSIC解决的是“精准投递”APLIC夹在中间负责把老的线中断翻译成新的MSI语言。理解了这条链路后大部分寄存器配置都能推导出来而不是死记地址。给后面迁移的人三个实操建议。一先Direct后MSI。Direct模式是最接近PLIC的迁移初期先用它把外设中断全部点通再切MSI每一步都有清晰的可验证目标。二手写MSI是IMSIC的试金石。在任何“中断没进来”的情况下先手动往目标中断文件页写一个值能瞬间区分问题在IMSIC侧还是APLIC侧。三设备树别照抄。不同SoC的IMSIC基地址、中断文件页布局、target寄存器位域都可能有差异设备树里的地址和属性必须以芯片手册为准。我见过太多直接把别家板子的APLIC/IMSIC节点搬过来然后中断全部乱套的案例。迁移之后Linux系统里看不到PLIC的打印/proc/interrupts呈现的也是imsic相关的控制器路径。这个“消失了”的过程恰恰说明中断架构真正完成了换代。如果后续要做多核负载均衡、虚拟化直通、或者PCIe MSI中断的深度优化AIA这套结构会越用越顺手。
返回列表