在長期的嵌入式開發實踐中,對中斷機制的理解始終停留在表面層次,特別當開發者長期局限于純軟件抽象層面時,對中斷機制的理解極易陷入"知其然而不知其所以然"的困境,這種認知的局限更為明顯;隨著工作需要不斷深入底層技術,對硬件機制的了解逐漸加深,并積累了大量的學習筆記。借此機會,我將這些零散的知識進行系統化梳理,既是對自身知識的復盤,也希望能為相關領域的開發者提供些許幫助和參考。關于RISC-V中斷機制的分析,本文將從硬件實現原理和軟件應用以下兩個方面來展開介紹:
RISC-V CLINT、PLIC及芯來ECLIC中斷機制分析 —— RISC-V中斷機制(一)
ECLIC中斷流程及實際應用 —— RISC-V中斷機制(二)
背景
中斷(Interrupt)機制,即處理器內核在順序執行程序指令流的過程中突然被別的請求打斷而中止執行當前的程序,轉而去處理別的事情,待其處理完了別的事情,然后重新回到之前程序中斷的點繼續執行之前的程序指令流。
中斷相關的基本知識要點:
- 打斷處理器執行的“別的請求”便稱之為中斷請求(Interrupt Request),“別的請求”的來源便稱之為中斷源(Interrupt Source),中斷源通常來自于內核外部(稱之為外部中斷源),也可以來自于內核內部(成為內部中斷源)。
- 處理器轉而去處理的“別的事情”便稱之為中斷服務程序(Interrupt Service Routine,ISR)。
- 中斷處理是一種正常的機制,而非一種錯誤情形。處理器收到中斷請求之后,需要保存當前程序的現場,簡稱為“保存現場”。等到處理完中斷服務程序后,處理器需要恢復之前的現場,從而繼續執行之前被打斷的程序,簡稱為“恢復現場”。
- 可能存在多個中斷源同時向處理器發起請求的情形,需要對這些中斷源進行仲裁,從而選擇哪個中斷源被優先處理。此種情況稱為“中斷仲裁”,同時可以給不同的中斷分配級別和優先級以便于仲裁,因此中斷存在著“中斷級別”和“中斷優先級”的概念。
芯來N級別處理器內核實現了一個“改進型內核中斷控制器(Enhanced Core Local Interrupt Controller,ECLIC)”,可用于多個中斷源的管理。N級別處理器內核中的所有類型(除了調試中斷之外)的中斷都由ECLIC統一進行管理。
1 寄存器
有詳細介紹說過ECLIC相關的寄存器,下面介紹中斷處理流程使用的CSR寄存器:用來保存控制信息。
1.1 硬件自動填寫的寄存器
-
mepc(Machine Exception Program Counter)
保存發生異常或中斷時的PC值。
如果中斷處理需要恢復到異常指令后一條指令進行執行,就需要正確判斷將 pc 寄存器加上多少字節。 -
mcause(Machine Cause Register)
記錄中斷是否是硬件中斷,以及具體的中斷原因,如:- Interrupt Bit(最高位):1 表示中斷,0 表示異常。
- Exception Code(低 31 位):具體原因編碼(如 0x0B 表示外部中斷)。
異常編碼表:
-
mtval(Machine Trap Value Register)
存儲與異常相關的附加信息(如非法地址、非法指令編碼)。- 非法地址異常:mtval 記錄訪問的非法地址。
- 非法指令異常:mtval 存儲非法指令的二進制編碼。
- 對于中斷(非異常),mtval 通常無意義,可能保留為 0
1.2 指示硬件處理中斷的寄存器
- mtvec(Machine Trap Vector Base Address Register)
設置機器模式(M-mode)下中斷和異常的入口地址基址。存儲了一個基址 BASE 和模式 MODE:- MODE 為 0 表示 直接模式,即遇到中斷便跳轉至 同一入口地址(mtvec.BASE)。
- MODE 為 1 表示 向量模式,中斷跳轉到 mtvec.BASE + 4 * cause,異常仍使用統一入口。
以上是官方中斷入口,芯來的eclic是基于clic,這里是有改動的,正常情況下mtvec是作為異常處理入口的,中斷入口是由mtvt2定義。芯來mtvec定義如下:
-
mstatus(Machine Status Register)
控制全局中斷使能及特權模式切換;如:- MIE(Machine Interrupt Enable):全局中斷開關。若為 0,所有中斷被屏蔽。
- MPP(Machine Previous Privilege):記錄中斷前的特權模式(如 M/S/U-mode),用于 mret 返回。
-
mie(Machine Interrupt Enable Register)
用來控制具體類型中斷的使能,如:- MEIE(Machine External Interrupt Enable):外部中斷(如 PLIC/ECLIC 中斷)使能。
- MTIE(Machine Timer Interrupt Enable):定時器中斷使能。
- MSIE(Machine Software Interrupt Enable):軟件中斷使能。
-
mip(Machine Interrupt Pending Register)
和 mie 相對應,標記中斷的掛起狀態(Pending Bits)- MEIP(Machine External Interrupt Pending):外部中斷掛起位。
- MTIP(Machine Timer Interrupt Pending):定時器中斷掛起位。
- MSIP(Machine Software Interrupt Pending):軟件中斷掛起位。
-
mtvt
寄存器保存中斷向量表的基址(在CLIC模式下),基址至少對齊64字節邊界 -
mtvt2
用于指示所有ECLIC中斷共享的公共基處理程序的入口地址
-
mscratch
寄存器的用處會在實現線程時起到作用,在中斷處理開始時,將當前線程的上下文指針保存到 mscratch,再從 mscratch 加載中斷棧指針;感興趣可以自行學習下。 -
msubm
芯來自定義的CSR msubm寄存器保存當前機器子模式和當前陷阱之前的機器子模式
- jalmnxti
芯來自定義的CSR jalmnxti,用來減少中斷延遲并加速中斷尾部鏈接。
jalmnxti包括mnxti(Next Interrupt Handler Address and Interrupt-Enable CSR)的所有功能,此外還包括啟用中斷、處理下一個中斷、跳轉到下一個中斷入口地址以及跳轉到中斷處理程序。(感興趣可以自行繼續深入學習下)
實際通過修改jalmnxti和ra的地址的值,在中斷嵌套和咬尾時,可以節省保存上下文(CSR和通用寄存器)的開銷,
1.4 臨時寄存器
- pushmepc
- pushmsubm
- pushmcause
芯來自定義了通過CSR指令csrrwi將msubm、mepc、mcause的值存儲在以SP為基址的內存空間中,該指令將CSR寄存器的值存儲在SP+1*4地址中。
2 ECLIC中斷處理流程
2.1 整體流程(主要以非向量中斷為例)
當一個hart發生中斷時,整個中斷流程需要軟硬協作完成,下圖是eclic中斷處理,包括:進入以及推出全部流程流程
-
硬件接收到中斷信號,硬件自動更新CSR寄存器
- 更新內核退出中斷時的返回地址,存儲在mepc(1、該地址就是中斷打斷的PC值(在中斷結束之后,回到被停止執行的程序點), 2、mepc軟件可以顯示修改)
- ①、mcause.EXCCODE存放中斷ID,以便軟件查詢;②、如果當前中斷搶占了低優先級中斷,mcause.MPIL將更新為minstatus.MIL的值,處理中斷后,將使用mcause.MPIL的值來恢復mintcause.MIL的值;③、如果是向量模式中斷,mcause.minhv的值將更新為1,在完成從中斷向量表中取出存儲的目標地址,然后再跳轉到目標地址中去后,mcause.minhv域的值清除為0
- ①、mstatus.MPIE更新為mstatus.MIE的值,mstatus.MIE置0(屏蔽所有中斷);②、處理器的當前特權模式(Privilege Mode)切換到機器模式(Machine Mode)mstatus.MPP從特權模式將切換到機器模式;
- ①、msubm.PTYP域的值被更新為中斷發生前的Machine Sub-Mode(msubm.TYP域的值)②、msubm.TYP域的值則被更新為“中斷處理模式”(反映當前的模式已經是“中斷處理模式”)
- 更新內核退出中斷時的返回地址,存儲在mepc(1、該地址就是中斷打斷的PC值(在中斷結束之后,回到被停止執行的程序點), 2、mepc軟件可以顯示修改)
-
跳轉到共享中斷入口地址,保存中斷上下文(非向量中斷)(寄存器clicintattr[i]的shv域決定中斷是向量中斷還是非向量中斷)
- 跳入到mtvt2.CMMON-CODE-ENTRY(mtvt2.MTVT2EN = 1);
- 將一些通用寄存器(ra/tp/t0-t6/a0-a7)(保存中斷上下文)保存到堆棧中;
- 將CSR mepc、mcause、msubm保存到堆棧中,確保后續的搶占中斷可以被正確處理;
- 如果被配置成為向量處理模式,則該中斷被處理器內核響應后,處理器直接跳入該中斷的向量入口(Vector Table Entry)存儲的目標地址(ISR地址)
- 如果被配置成為非向量處理模式,則該中斷被處理器內核響應后,處理器直接跳入所有中斷共享的入口地址
-
執行Nuclei自定義指令“
csrrw ra,CSR_JALMNXTI,ra
”,如果沒有待處理的中斷,則該指令將被視為空操作;否則進入下面步驟- 直接跳入該中斷的向量入口(Vector Table Entry)存儲的目標地址,即該中斷源的中斷服務程序**(Interrupt Service Routine,ISR)**中去
- 硬件置位全局中斷使能位mstatus.MIE,此時可以接受新中斷,以形成中斷嵌套。
- 把當前PC(csrrw ra,CSR_JALMNXTI,ra)寫入返回地址ra寄存器,達到JAL(Jump and Link)的效果,即:在執行完中斷handle之后,將再次執行該指令“
csrrw ra,CSR_JALMNXTI,ra
”,進而重新判斷是否有還未處理的中斷(pending),進而形成中斷咬尾
-
從中斷服務函數中返回后,軟件來恢復中斷上下文
- mstatus.MIE置0,屏蔽所有中斷,確保操作的原子性
- 從堆棧中恢復中斷前
CSR寄存器(msubm、mepc、mcause)
以及通用寄存器(ra/tp/t0-t6/a0-a7/sp)
的值
-
軟件執行mret退出中斷處理程序,硬件將自動更新CSR寄存器
- ①、mcause.MPIL的值恢復minstatus.MIL的值,minstatus.MIL的值會被恢復到中斷前的原始值;②、使用mcause.MPIE的值恢復minstatus.MIE的值,minstatus.MIE的值會被恢復到觸發中斷前的值;③、mcause.MPP特權模式從中斷模式退出,恢復為中斷前的模式,(同樣也會更新mstatus.MPIE和mcause.MPP的值,見最后NOTE)。
- 硬件將處理器Machine Sub-Mode的值恢復為msubm.PTYP域的值
- 跳轉到mepc定義的PC,繼續執行之前被中止的程序流。
NOTE:
mstatus.MPIE域和mstatus.MPP域的值與mcause.MPIE域和mcause.MPP域的值是鏡像關系,即,在正常情況下,mstatus.MPIE域的值與mcause.MPIE域的值總是完全一樣,mstatus.MPP域的值與mcause.MPP域的值總是完全一樣。
2.2 向量中斷與非向量中斷
ECLIC的每個中斷源均可以設置成向量或者非向量處理(通過寄存器clicintattr[i]的shv域
),向量處理模式和非向量處理模式二者有較大的差別
2.1章節主要是以非向量中斷為例介紹的中斷處理流程,這里把前面的一些和向量中斷不同的點再總結下,方便和向量中斷做對比
2.2.1 非向量中斷處理模式
1、非向量處理模式,則該中斷被處理器內核響應后,處理器會直接跳入到所有非向量中斷共享的入口地址,該入口地址可以通過軟件進行設置
-
如果配置CSR寄存器
mtvt2的最低位為0
(上電復位默認值),則所有非向量中斷共享的入口地址由CSR寄存器mtvec的值(忽略最低2位的值)指定。由于mtvec寄存器的值也指定異常的入口地址,因此,意味著在這種情況下,異常和所有非向量中斷共享入口地址。 -
如果配置CSR寄存器
mtvt2的最低位為1
(芯來SDK的bootloader里配置為1了,將異常和非向量中斷入口分開,不用判斷是中斷還是異常了,提升效率),則所有非向量中斷共享的入口地址由CSR寄存器mtvt2的值(忽略最低2位的值)指定。
2、如2.1章節步驟二描述,進入所有非向量中斷共享的入口地址之后,處理器會開始執行一段共有的軟件代碼
- 首先保存CSR寄存器
mepc、mcause、msubm
入堆棧。保存這幾個CSR寄存器是為了保證后續的中斷嵌套能夠功能正確,因為新的中斷響應會重新覆蓋mepc、mcause、msubm的值,因此需要將它們先保存入堆棧 - 保存若干通用寄存器(處理器的上下文)入堆棧
- 然后執行一條特殊的指令“
csrrw ra, CSR_JALMNXTI, ra
”。如果沒有中斷在等待(Pending),則該指令相當于是個Nop指令不做任何操作;如果有中斷在等待(Pending),執行該指令后處理器會:- 直接跳入該中斷的向量入口(Vector Table Entry)存儲的目標地址,即該中斷源的中斷服務程序(Interrupt Service Routine,ISR)中去。
- 在跳入中斷服務程序的同時,硬件也會同時打開中斷的全局使能,即,設置mstatus寄存器的MIE域為1。打開中斷全局使能后,新的中斷便可以被響應,從而達到中斷嵌套的效果。
- 在跳入中斷服務程序的同時,“csrrw ra, CSR_JALMNXTI, ra”指令還會達到JAL(Jump and Link)的效果,硬件同時更新Link寄存器的值為該指令的PC自身作為函數調用的返回地址。因此,從中斷服務程序函數返回后會回到該“csrrw ra, CSR_JALMNXTI, ra”指令重新執行,重新判斷是否還有中斷在等待(Pending),從而達到中斷咬尾的效果。
- 在中斷服務程序的結尾處同樣需要添加對應的恢復上下文出棧操作。并且在CSR寄存器mepc、mcause、msubm出堆棧之前,需要將中斷全局使能再次關閉,以保證mepc、mcause、msubm恢復操作的原子性(不被新的中斷所打斷)。
2.2.2 向量中斷的處理
1、如果被配置成為向量處理模式,則該中斷被處理器內核響應后,處理器會**直接跳入該中斷的向量入口(Vector Table Entry)**存儲的目標地址,即該中斷源的中斷服務程序(Interrupt Service Routine,ISR)
2、向量處理模式具有如下特點:
-
向量處理模式時處理器會直接跳到中斷服務程序,并沒有進行上下文的保存,因此,中斷響應延遲非常之短,從中斷源拉高到處理器開始執行中斷服務程序中的第一條指令,基本上只需要硬件進行查表和跳轉的時間開銷,理想情況下約6個時鐘周期。
-
對于向量處理模式的中斷服務程序函數,一定要使用特殊的
__attribute__((interrupt))
來修飾中斷服務程序函數。 -
向量處理模式時,由于在跳入中斷服務程序之前,處理器并沒有進行上下文的保存,因此,理論上中斷服務程序函數本身不能夠進行子函數的調用。
-
如果不小心調用了其他子函數,只要使用了
__attribute__((interrupt))
來修飾中斷復位函數,編譯器就會自動插入一段代碼進行上下文的保存。(實際使用中不推薦調用子函數) -
處理器在響應向量中斷后,mstatus寄存器中的MIE域將會被硬件自動更新成為0(中斷被全局關閉,從而無法響應新的中斷)。因此向量處理模式默認是不支持中斷嵌套的,為了達到向量處理模式且又能夠中斷嵌套的效果,可以在中斷服務例程里依次加入以下操作來實現中斷嵌套效果。
- 保存CSR寄存器mepc、mcause、msubm入堆棧。
- 重新打開中斷的全局使能(mstatus.MIE置1)
- 執行中斷程序內容
- 關閉中斷全局使能,恢復上下文出棧操作
3、對于向量處理模式的中斷而言,由于在跳入中斷服務程序之前,處理器并沒有進行上下文的保存,因此進行“中斷咬尾”的意義不大,因此,向量處理模式的中斷,沒有“中斷咬尾”處理能力。(除非在向量中斷復位函數里進行中斷上下文以及返回地址的處理,但這樣不如一開始就注冊為非向量中斷)
2.2.3 二者區別
這里就簡單總結對比下:
對比項 | 向量中斷(Vectored Interrupt) | 非向量中斷(Non-Vectored Interrupt) |
---|---|---|
入口地址 | 每個中斷有獨立的入口地址(由向量表定義)。 | 所有中斷共享統一入口地址。 |
開銷 | 低(硬件自動跳轉IRQ,無需軟件處理中斷上下文)。 | 高(需要從統一入口進入,需要軟件處理中斷上下文)。 |
中斷嵌套 | 支持(需要在中斷服務例程中手動處理上下文來支持) | 支持 |
中斷咬尾 | 不支持 | 支持 |
IRQ定義 | void __INTERRUPT isr_uart() { ... } | void isr_uart() { ... } |
應用場景 | 需要快速響應的高優先級外設 | 低優先級或非實時外設 |
NOTE:
非向量模式的中斷處理函數,中斷函數前面一定不要加__INTERRUPT
這個關鍵字描述,否則編譯器會用mret 指令,導致中斷提前返回了中斷前的代碼,而不是返回到common_entry 這里
而向量中斷需要使用__attribute__((interrupt))
來修飾,讓編譯器來處理該服務例程
2.3 中斷搶占和中斷咬尾
實際上通過前面的前面的介紹,對于中斷搶占和中斷咬尾,基本上已經能了解個七七八八了,這里再簡單總結下,并舉例說明下。
2.3.1 基本概念
1、中斷嵌套
處理器內核正在處理某個中斷的過程中,可能有一個級別更高的新中斷請求到來,處理器可以中止當前的中斷服務程序,轉而開始響應新的中斷,并執行其“中斷服務程序”,如此便形成了中斷嵌套(即前一個中斷還沒響應完,又開始響應新的中斷),并且嵌套的層次可以有很多層。
2、中斷咬尾
處理器內核正在處理某個中斷的過程中,可能有新中斷請求到來,但是**“新中斷的級別”低于或者等于“當前正在處理的中斷級別”**,因此,新中斷不能夠打斷當前正在處理的中斷(因此不會形成嵌套)
2.3.2 中斷嵌套處理流程
1、非向量中斷
假設中斷源30、31、32這三個中斷源先后到來,且“中斷源32的級別” > “中斷源31的級別”> “中斷源30的級別”,那么后來的中斷便會打斷之前正在處理的中斷形成中斷嵌套
2、向量中斷
假設中斷源30、31、32這三個中斷源先后到來,且“中斷源32的級別” > “中斷源31的級別”> “中斷源30的級別”,那么后來的中斷便會打斷之前正在處理的中斷形成中斷嵌套。
向量中斷不同點是,1、中斷入口不一致(中斷的服務函數地址),2、中斷服務函數里軟件實現中斷上下文處理
2.3.3 中斷咬尾處理流程
假設中斷源30、29、28這三個中斷源先后到來,且“中斷源30的級別” >= “中斷源29的級別”>= “中斷源28的級別”,那么后來的中斷不會打斷之前正在處理的中斷(不會形成中斷嵌套),但是會被置于等待(Pending)狀態。當中斷源30完成處理后,將會直接開始中斷源29的中斷處理,省掉中間的“恢復上下文”和“保存上下文”過程。
2.3.4 總結
非向量中斷總是能夠支持中斷嵌套和中斷咬尾的;
向量中斷則是可以通過在中斷服務程序中通過軟件處理來支持中斷嵌套,但不支持中斷咬尾。
另外,并未給出向量中斷和非向量中斷相互嵌套的例子,不過大家可以自行分析下。
3 實際應用
這里結合芯來開源SDK中demo_eclic代碼及qemu(RISC-V匯編學習(四)—— RISCV QEMU平臺搭建(基于芯來平臺))來演示下,向量中斷與非向量中斷以及中斷的嵌套。
中斷注冊、入口初始化及共享中斷入口流程代碼,自行參考芯來開源sdk中的驅動及BootLoader學習(參考下面源碼鏈接)。
3.1、源碼分析
芯來提供了一個軟件中斷和timer中斷的示例,源碼點擊鏈接即可查看:
Gitee: https://gitee.com/Nuclei-Software/nuclei-sdk/blob/master/application/baremetal/demo_eclic/demo_eclic.c
Github: https://github.com/Nuclei-Software/nuclei-sdk/blob/master/application/baremetal/demo_eclic/demo_eclic.c.
源碼執行結果,展示部分打印:
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 0 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run when timer interrupt finished
[IN TIMER INTERRUPT]timer interrupt end
[IN SOFTWARE INTERRUPT]software interrupt hit 0 times
[IN SOFTWARE INTERRUPT]software interrupt end
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 1 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run when timer interrupt finished
[IN TIMER INTERRUPT]timer interrupt end
[IN SOFTWARE INTERRUPT]software interrupt hit 1 times
[IN SOFTWARE INTERRUPT]software interrupt end
分析
- 程序中分別注冊了:
- 1、timer中斷:高優先級、非向量中斷
- 2、軟件中斷:低優先級、向量中斷
timer_intlevel = HIGHER_INTLEVEL;
swirq_intlevel = LOWER_INTLEVEL;// initialize timer
setup_timer();
// initialize software interrupt as vector interrupt
returnCode = ECLIC_Register_IRQ(SysTimerSW_IRQn, ECLIC_VECTOR_INTERRUPT,ECLIC_LEVEL_TRIGGER, swirq_intlevel, 0, eclic_msip_handler);
// inital timer interrupt as non-vector interrupt
returnCode = ECLIC_Register_IRQ(SysTimer_IRQn, ECLIC_NON_VECTOR_INTERRUPT,ECLIC_LEVEL_TRIGGER, timer_intlevel, 0, eclic_mtip_handler);
- 實現:
通過設置core內部的MTIMERCMP寄存器,來觸發timer中斷,之后在timer中斷服務例程里配置MSIP來觸發軟件中斷
根據打印可以發現程序這里實現了中斷咬尾效果,即:
1、低優先級中斷無法打斷高優先級中斷
2、每次都會先觸發非向量中斷(timer中斷),然后再觸發向量中斷(soft中斷)
3、在高優先級的非向量中斷退出后再次執行"csrrw ra, CSR_JALMNXTI, ra
",此時還有低優先級的中斷在pending中,就會繼續執行低優先級的軟件中斷,從而形成中斷咬尾
另外,這里把軟件中斷配置成非向量中斷(記得去掉IRQ的__INTERRUPT
修飾)效果也是一樣的,
但是把timer中斷換成向量中斷(不支持中斷咬尾)是不行的。(第一個觸發的中斷是非向量中斷可以實現中斷咬尾)
3.2 中斷嵌套實現
我們在芯來提供的demo_eclic基礎上來做些修改:
只需要打開#define SWIRQ_INTLEVEL_HIGHER 1
,就會從新定義優先級: soft中斷>timer中斷
執行代碼,部分打印如下:
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 0 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run during timer interrupt
[IN SOFTWARE INTERRUPT]software interrupt hit 0 times
[IN SOFTWARE INTERRUPT]software interrupt end
[IN TIMER INTERRUPT]timer interrupt end
-------------------
[IN TIMER INTERRUPT]timer interrupt hit 1 times
[IN TIMER INTERRUPT]trigger software interrupt
[IN TIMER INTERRUPT]software interrupt will run during timer interrupt
[IN SOFTWARE INTERRUPT]software interrupt hit 1 times
[IN SOFTWARE INTERRUPT]software interrupt end
[IN TIMER INTERRUPT]timer interrupt end
可以看到在低優先級timer中斷執行時,將被高優先級soft中斷打斷,從而形成中斷嵌套。
這里把軟件中斷配置成非向量中斷(記得去掉IRQ的__INTERRUPT
修飾)效果也是一樣的,
后者把timer中斷換成向量中斷也是可行的(1、IRQ加上的__INTERRUPT
修飾 2、IRQ里開始加上SAVE_IRQ_CSR_CONTEXT();
,結束加上RESTORE_IRQ_CSR_CONTEXT();
)。
以上就是芯來eclic中斷相關的內容,如有紕漏,還請各位看官大佬予以指出
參考:
芯來科技N級別指令集架構