中斷(中斷返回)本質上也是一種跳轉,只不過還需要附加一些讀寫CSR寄存器的操作。
RISC-V中斷分為兩種類型,一種是同步中斷,即ECALL、EBREAK等指令所產生的中斷,另一種是異步中斷,即GPIO、UART等外設產生的中斷。
- 中斷號保存在
mcause
寄存器中,最高位是 1 說明是同步異常,否則是中斷 mepc
儲存中斷前執行指令的地址,調用mret
返回后會執行其中的地址- 對於
RISCV
而言,當前運行的狀態保存在mstatus
寄存器中
MPP
位記錄當前機器模式的特權等級,0 是用戶級,1 是內核級,2 保留,3 是機器級,權限最高MPIE
記錄觸發中斷前的MIE
位的值,MIE
(Machine Interrupt Enable)位為 1 的時候,中斷才會觸發
RISCV
不支持中斷嵌套,即中斷觸發之后會將 mstatus
的 mie
位置 0
中斷處理的第一條指令地址存儲在 mtvec
中,mie
寄存器(不是mstatus
寄存器中的mie
位)控制哪些中斷可以被觸發,只有對應位置置一的中斷號的中斷會觸發。
中斷處理完成之后需要返回,從機器模式的中斷返回需要調用 mret
指令,它會 將 PC 設置為 mepc,通過將 mstatus 的 MPIE 域復制到MIE 來恢復之前的中斷使能設置,並將權限模式設置為 mstatus 的 MPP 域中的值。
對於中斷模塊設計,一種簡單的方法就是當檢測到中斷(中斷返回)信號時,先暫停整條流水線,設置跳轉地址為中斷入口地址,然后讀、寫必要的CSR寄存器(mstatus、mepc、mcause等),等讀寫完這些CSR寄存器后取消流水線暫停,這樣處理器就可以從中斷入口地址開始取指,進入中斷服務程序。
下面看tinyriscv的中斷是如何設計的。中斷模塊所在文件:rtl/core/clint.v
輸入輸出信號列表如下:
先看中斷模塊是怎樣判斷有中斷信號產生的,如下代碼:
第3~4行,復位后的狀態,默認沒有中斷要處理。
第6~7行,判斷當前指令是否是ECALL或者EBREAK指令,如果是則設置中斷狀態為S_INT_SYNC_ASSERT,表示有同步中斷要處理。
第8~9行,判斷是否有外設中斷信號產生,如果是則設置中斷狀態為S_INT_ASYNC_ASSERT,表示有異步中斷要處理。
第10~11行,判斷當前指令是否是MRET指令,MRET指令是中斷返回指令。如果是,則設置中斷狀態為S_INT_MRET。
下面就根據當前的中斷狀態做不同處理(讀寫不同的CSR寄存器),代碼如下:
第1023行,當CSR處於S_CSR_IDLE時,如果中斷狀態為S_INT_SYNC_ASSERT,則在第11行將CSR狀態設置為S_CSR_MEPC,在第12行將當前指令地址保存下來。
在第1323行,根據不同的指令類型,設置不同的中斷碼(Exception Code),這樣在中斷服務程序里就可以知道當前中斷發生的原因了。
第24~28行,目前tinyriscv只支持定時器這個外設中斷。
第30~31行,如果是中斷返回指令,則設置CSR狀態為S_CSR_MSTATUS_MRET。
第34~48行,一個時鍾切換一下CSR狀態。
接下來就是寫CSR寄存器操作,需要根據上面的CSR狀態來寫。
第11~15行,寫mepc寄存器。
第17~21行,寫mcause寄存器。
第23~27行,關閉全局異步中斷。
第29~33行,寫mstatus寄存器。
最后就是發出中斷信號,中斷信號會進入到執行階段。
有兩種情況需要發出中斷信號,一種是進入中斷,另一種是退出中斷。
9~12行,寫完mstatus寄存器后發出中斷進入信號,中斷入口地址就是mtvec寄存器的值。
第13~15行,發出中斷退出信號,中斷退出地址就是mepc寄存器的值。
編寫一個 BOOT
mret
指令
為了使 hart 跑在監管者模式下,我們必須使用 mret
。
參考 RISC-V 的相關資料,在處理 mret
指令時,PC 值會從 mepc
寄存器取得。因此,我們必須將 main
函數的地址存入 mepc
寄存器。
mstatus 寄存器
剛開始執行代碼一定是機器模式,但是我們總不能一直讓 hart 在機器模式下運行;此外,全局中斷使能位也需要我們控制。這些都可以在 mstatus 寄存器上找到,關於 mstatus 寄存器,RISC-V 特權架構 和 RISC-V 中文手冊上都有詳細介紹。在此就略寫幾句。
當進入 main 函數時,hart 最好要進入監管者模式。因為 main 函數事實上是我們操作系統內核最主要的函數之一,此外,我們也希望中斷能被打開。對照 mstatus 寄存器的位圖,我們可以在對應位域置 1 ,來打開中斷或者記錄信息等。
比如,我們想先打開機器模式的中斷使能,那么我們需要:
將 mstatus.MIE 位置為 1 ,因為它代表機器模式全局下的中斷使能
將 mstatus.MPIE 位置為 1 ,它代表了在中斷/異常發生前,機器模式全局下的中斷使能(我們肯定不想在中斷/異常發生一次后,使能就失效了吧)
我們還要將 mstatus.MPP 位置為 01,它代表了中斷/異常發生前,代碼運行的模式。之所以置為 01(監管者模式),是為了在執行 mret 的時候進入監管者模式。結合之前所說的,寫下如下代碼:
li t0, (0b01 << 11) | (1 << 7) | (1 << 3) csrw mstatus, t0
# 讓其它(非0號)硬件線程掛起,跳轉至 3 csrr t0, mhartid bnez t0, 3f csrw satp, zero //關閉mmu
這里是讀取處理器的核心號碼(mhartid),我們只需要使用 0 號核心進行初始化操作,非 0 的核心會跳轉到后面掛起
# 先初始化 li t0, (0b11 << 13) | (0b11 << 11) | (1 << 7) csrw mstatus, t0 la t1, kernel_init csrw mepc, t1 la t2, m_trap_vector csrw mtvec, t2 li t3, 0xaaa csrw mie, t3 la ra, 4f mret
這里出現一個關鍵的指令 csrw
意思是寫入狀態控制寄存器。每個核心都有一系列狀態控制寄存器,可以參考 RISCV 手冊。下方列出的是 mstatus
狀態寄存器的每個位的情況。
- 使
FS
置位,可以開啟浮點運算(不開啟的話使用浮點數會報錯) - 使
MPIE
置位,手冊里的說法是,這個位儲存中斷前MIE
的值,當我們從中斷返回后MPIE
會放到MIE
中 - 使
MPP
置0b11
,MMP
標志着當前的特權級別, mepc
放置m_trap_vector
函數的地址,出發中斷后會跳轉到m_trap_vector
(放在src/asm/trap.S
中)- 調用
mret
之后,會執行mepc
中的地址,即kernel_init
函數
Rust 初始化函數
#[no_mangle] extern "C" fn kernel_init(){ } #[no_mangle] extern "C" fn kernel_start(){ }