
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 里核對寄存器讀寫邏輯很管用。這個在規格書里很容易被忽略但排查硬件描述和軟件訪問字節序不一致時能省下至少半天時間。