# RISC-V Platform-Level Interrupt Controller(PLIC) 探討
>[name=Jacky Shia][time=Oct 1,2022] [Jacky](https://www.linkedin.com/in/%E8%9C%83%E8%A9%B0-%E5%A4%8F-b854191b7/)
---
相信有接觸過embedded system都對interrupt不陌生,就以筆者來說,只知道有中斷發生,CPU進入到相對應的ISR(Interrupt Service Routine),要做的事情就只是針對中斷訊號來做處理,但這之間硬體做了些什麼事情卻無從而知.此篇文章為以RISC-V架構探討中斷發生,硬體是如何處置,這樣軟體思維上就能更清楚掌控中斷的處理.
[參考內容](https://github.com/riscv) :
>* RISC-V Platform-Level Interrupt Controller Specification
>* Volume I: User-Level ISA
>* Volume II: Privileged Architecture
---
## 本文大綱
[toc]
---
## Target and Hart
RISC-V把hardware thread稱為hart,代表一個CPU內核中可能有多個hart,每一個hart又可以再接上M-mode externel interrupt & S-mode externel interrupt,不同的mode視為不同的context,意味著不同特權下,中斷來源是可以區分的,然而不同的中斷接線來源會直接影響CSR,產生不同的系統行為,後續[特權模式下中斷處理](https://hackmd.io/BZZXfv2DTl6xFQC-yHKuSQ?both#%E7%89%B9%E6%AC%8A%E6%A8%A1%E5%BC%8F%E4%B8%8B%E4%B8%AD%E6%96%B7%E8%99%95%E7%90%86)會進一步探討,後面內容就先以單純M-mode情況下來進行說明。
:::info
:bulb: Target這一詞是包含hart,並且包含CPU以外裝置(ex. DMA)
:::
讀者到這裡可能開始好奇PLIC內可以設定這麼多中斷來源,但只接兩條線到CPU,這是怎麼做到的? 後續[Hardware Architecture](https://hackmd.io/BZZXfv2DTl6xFQC-yHKuSQ?both#PLIC-Hardware-Architecture)提到實際上的應用。

>* RISC-V Platform-Level Interrupt Controller Specification
---
## PLIC Hardware Architecture
PLIC register table :

>* RISC-V Platform-Level Interrupt Controller Specification
* Interrupt Enables registers,讀者可以把文中提及的context視為target,意味著每一個中斷可以決定在那些target上執行。
* RISC-V寫到可以支援到15872 targets,每個context占了128bytes,但實際上並不會有這麼多接口,每多一個就等於是chip面積的增加,所以要考量到實際狀況。

>* RISC-V Platform-Level Interrupt Controller Specification
:::warning
:bulb: PLIC 內總共有1024中斷源設置,其中source 0是hardwired to 0。
:::
接下來就是要從block diagram中了解PLIC該如何去設定
1. 所有外部中斷觸發,會先抵達Gateway,由Gateway決定要不要送入PLIC內的pending array,最後由PLIC送出level訊號通知target。
2. 這之間會影響PLIC送出中斷訊號到target有三個數值會影響
* Enable register,若enable bit = 0,則不會送出訊號
* Priority register,若priority = 0,則不會送出訊號
* Priority Threshold value,若priority<threshold value,則不會送出訊號
3. claim的操作來取回相對應interrupt ID,同時清除pending array。
4. complete的操作為通知gateway,這時相同中斷來源才能再次被記錄在pending array
:::info
:bulb: 中斷來源沒有被Enable或者Priority = 0情況下,claim/complete是會被忽略的
:::

>* RISC-V Platform-Level Interrupt Controller Specification
---
## PLIC Interrupt handle flow
這個章節的探討會分成三個部分,
* Single Target Interrupt Handle
* Multi-Target Interrupt Handle
* Peepmtive Handle
---
### Single Target Interrupt Handle
在介紹PLIC中斷如何交握之前,先說明CPU內核中會使用到相對應的CSR(Control and Status Registers),如先前所說明,目前內容都只先針對M-mode下進行探討。
```
mtvec : 當進入異常時,會是fetch mtvec所指向的地址並繼續進行(這裡沒有探討vector interrupt使用)
mcause : 紀載異常原因,用來判斷是否為外部中斷
mtval : 紀載異常原因的位置,或讀寫數值
mepc : 進入異常前PC所指向的地址,執行mret後會返回mepc
mstatus : MPIE紀錄MIE當前數值,當執行mret時將MPIE存回MIE(global interrupt enable)
mie : MEIE外部中斷開關
mip : MEIP外部中斷等待
```
>* Volume II: Privileged Architecture
:::success
在==Volume II: Privileged Architecture==中提到Trap,Trap觸發的定義包含了中斷(interrupt)與例外狀況(exception),中斷又可以細分為內部中斷(local interrupt)與外部中斷(external interrupt),PLIC所做的事情就是將外部中斷轉換成內部中斷給CPU進行處理。
:::
有了上述準備可以開始探討CPU與PLIC之間中斷的交握與處裡
---
#### 中斷處理前的前置作業
* CPU 必須設定mtvec到trap handler
* CPU CSR中mstatus.MIE與mie.MEIE開啟
* PLIC內enable相對應中斷
---
#### 中斷觸發到通知CPU
* 中斷觸發進入到gateway決定是否送入PLIC core
* PLIC根據設定決定是否通知CPU

>* RISC-V Platform-Level Interrupt Controller Specification
---
#### CPU接收後處理流程
* CPU 收到interrupt通知後mip.MEIP舉起
* CPU 紀錄當前mepc
* CPU fetch mtvec進入trap,將mstatus.MIE存進mstatus.MPIE,清除mstatus.MIE
* CPU claim Interrupt ID進入ISR(interrupt service routine)
* CPU interrupt handle
* CPU send complete with interrupt ID
* CPU 執行mret,mstatus.MPIE存回mstatus.MIE,回到mepc位置

>* Volume II: Privileged Architecture
然而context switch階段必要保留的GPR(General Purpose Register)就會是在CPU接受中斷後先進行push stack動作,結束ISR並且send complete後進行pop stack。
---
### Multi-Target Interrupt Handle
綜觀上述的內容,讀者可以更清楚了解到在CPU上interrupt操作,衍伸到multi-target概念上也大致相同,但有幾個議題需要討論:
* 假設PLIC上一個中斷源在多個target enable情況下,PLIC會一次notice所有符合設定的target,此時所有target接受到訊號後同時claim ID後會發生什麼事?

>* RISC-V Platform-Level Interrupt Controller Specification
RISC-V允許所有target去race interrupt ID,在[你所不知道的memory map](https://hackmd.io/BZZXfv2DTl6xFQC-yHKuSQ?both#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84Memory-map)會探討bus的實際狀況,當某個target得到ID後,pending array是會被清空的,這樣的話其他target得到的數值會是==0==,0在PLIC內定義就是沒有interrupt,這是特例情況,當所有target進入trap handle後要取得ID,但沒搶到資源情況得到0,這時就必須要對0做特殊處理,後續還會提及另一個例子。
* 考慮到affinity,interrupt可以選擇在某個target上作執行
---
### Peepmtive Handle
Note: local interrupt會根據mcause數值而有優先權分別
前面提及內容,跳過Priority Thresholds register,現在回顧到先前內容。
* CPU claim Interrupt
* claim相當於告訴PLIC正在執行interrupt的交握,此時更新priority threshold value為目前處理中斷的優先權,下一筆中斷沒有大於threshold value就不會被PLIC送出。
* CPU interrupt handle
* CPU complete with interrupt ID
* 需要將threshold value更新為執行中斷前的數值
當中斷被PLIC送進CPU時會相當於local interrupt,此時mcause也會被更新成對應數值,此時MEIP也是舉起的。
當對PLIC發出claim會清除tip訊號,此時MEIP也會清除,這時高優中斷又被PLIC送進CPU時,MEIP再度舉起,CPU就會重複進入mtvec所設定地址。

>* Volume II: Privileged Architecture

>* Volume II: Privileged Architecture
Local interrupt會根據號碼不同而有優先權之分,而有先後的執行順序,此時是hart的行為,就不是PLIC所控制的。
---
## 你所不知道的Memory map
現在假設使用的裝置上的PLIC位置為0xE3000000,現在看以下一段程式碼 :
```clike=
typedef struct plic plic_t
plic_t *dev_plic = (plic_t *)(0xE3000000);
dev_plic->int_enable = 0x2000;
```
0xE3000000這個位置對於軟體來說就是直接用一個pointer指向這個位置就可以對這個位置修改,但硬體上是怎麼訂出的個區間,這要參考到何謂bus matrix。
參考下圖簡易[bus matrix](https://blog.csdn.net/uiojhi/article/details/112855204),通常會有不只一個的master,這個master通常是CPU或者是DMA,之間通訊採用[AMBA](https://developer.arm.com/Architectures/AMBA) 的AXI、AHB、APB,細部也可以參考SMP架構。
實際在RTL level時,會設定slave的base address,這就是所謂的memory map address,當軟體對於指標進行操作,就會由CPU發出到達bus matrix,經由仲裁後才會由PLIC接收,當對一個device的操作勢必會產生latency。
:::warning
:bulb:Device access在系統上會設置inorder避免亂序
:::
當多個master發出request或者CPU內核發出多個讀寫指令,bus matrix會呈現塞車的現象,未處理的項目會存在FIFO中等待,對於inorder的access來說,不同處理器架構下是可能會blocking pipeline。
針對塞車的狀況,大多的仲裁器會採用[round-robin](http://eportfolio.lib.ksu.edu.tw/~4000E039/blog?node=000000005)來分配bus service的狀況。
回到先前討論claim ID的部分,即使core 0先執行claim ID的動作,但未必會馬上到bus上取得PLIC內的ID,有可能會是其他core先抵達bus matrix,這也要取決於CPU內bus busy狀況。

---
接下來討論一個實際案例,讀者是否有思考過如果已經在PLIC內enable的中斷源,經過一段時間後去作disable,這之間的探討非常細微,考量到bus matrix的latency,假設軟體端認知指令執行後,disable動作應該已經完成了,這就大錯特錯。
* Disable指令很有可能還停留在bus matrix時,interrupt發生
* CPU進入中斷處理後,disable訊息抵達PLIC
* CPU準備claim ID,但此中斷已經被disable
* CPU這時claim ID只會得到0,意味著如果沒有對於no interrupt value 0處理,系統就可能異常
這就是軟體開發需要對於bus matrix有一定概念的原因。
---
## 特權模式下中斷處理
最後一個部分開始前一樣先介紹對應CSR,此章節就比較貼近作業系統模式切換,但當更了解單一模式下中斷處理後,對於特權模式的切換馬上就能駕輕就熟。
```
stvec : 當進入異常時,會是fetch stvec所指向的地址並繼續進行(這裡沒有探討vector interrupt使用,與MMU)
scause : 紀載異常原因,用來判斷是否為外部中斷
stval : 紀載異常原因的位置,或讀寫數值
sepc : 進入異常前PC所指向的地址,執行sret後會返回mepc
sstatus : SPIE紀錄SIE當前數值,當執行sret時將SPIE存回SIE(global interrupt enable)
sie : SEIE外部中斷開關
sip : SEIP外部中斷等待
```
>* Volume II: Privileged Architecture
:::warning
:warning: S/U-mode的狀態下不能對M-mode CSR操作
:::
首先S-mode下的CSR與M-mode下的大同小異,針對中斷處理上RISC-V提供了delegate的機制,意味著將S/U-mode下觸發的中斷委任給當前模式下處理,現在重新看以下ISA與CSR :
```
mret : M-mode下執行的return
sret : S-mode下執行的return
```
```
mstatus.MPP : 紀錄跳到trap前的privilege level
sstatus.SPP : 紀錄跳到trap前的privilege level
```
先前提及到的mret刻意跳過mstatus.MPP的變化,現在說明這個register是如何操作。
觀察到mideleg這個CSR,SEI bit是設定是否要將S-mode下觸發的外部中斷委任給S-mode自行處理
* 委任下中斷任務處理
* 委任後進入到trap,sstatus.SPP會記錄當前模式
* 此時中斷異常紀錄在scause,中斷等待紀錄在sip
* 執行sret後恢復成sstatus.SPP所記錄的模式

>* Volume II: Privileged Architecture
* 如果沒有委任又是怎麼樣的行為
* 進入到trap會跳回M-mode,變成mstatus.MPP紀錄先前模式
* 中斷狀況記錄在mcause,中斷等待紀錄在mip.SEIP
* 執行mret恢復成mstatus.MPP模式
結合到U-mode情況下,作業系統是如何呼叫system-call,RISC-V提供ecall作為一個呼叫system-call的方式,同時M-mode delegate U-mode下發生的interrupt給S-mode代為處理,則就是呼叫system-call的行為本身。
在沒有設定delegate情況,高權限中斷可以打斷低權限正在執行的程序,如M-mode中斷打斷S-mode下所執行的程序,然而hart的限制,最多只能有兩層堆疊。
---
```筆者第一次分享自己所學經驗貢獻開源社群,文筆有待加強,技術上有什麼地方需要糾正與交流的,隨時都歡迎與我linkedin聯繫。```