本章目標#
- 認識 PLIC (Platform-Level Interrupt Controller)、IRQ 與 ISR
- 綜合先前所學,應用在實際案例上
PIC#
PIC(Programmable Interrupt Controller)是一種特殊用途的電路,能幫助處理器處理來自不同來源、可能同時發生的中斷請求。它會判斷 IRQ 的優先級,讓 CPU 切換到最合適的中斷處理常式(ISR)。
中斷種類複習#
先重新複習 RISC-V 的中斷(Interrupt)種類,可細分為幾大項:
- Local Interrupt
- Software Interrupt
- Timer Interrupt
- Global Interrupt
- External Interrupt
各種中斷的 Exception Code 也都有被規格書詳細的定義:

Exception code 會被紀錄在 mcause 暫存器當中。
若我們要讓運行在 RISC-V 中的系統程式支援中斷處理,也需要設定 MIE register 的域值:
// Machine-mode Interrupt Enable
#define MIE_MEIE (1 << 11) // external
#define MIE_MTIE (1 << 7) // timer
#define MIE_MSIE (1 << 3) // software
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);PLIC#
大致複習了先前介紹的中斷處理後,讓我們回到本章的重點 PLIC。
PLIC(Platform-Level Interrupt Controller)就是為了 RISC-V 平台所打造的 PIC。實際上,會有多個中斷源(鍵盤、滑鼠、硬碟……)接上 PLIC,PLIC 會判別這些中斷的優先級,再分配給處理器的 Hart(RISC-V 中 hardware thread 的最小單位)進行中斷處理。
IRQ#
在電腦科學中,中斷是指處理器接收到來自硬體或軟體的訊號,提示發生了某個事件,應該被注意,這種情況就稱為中斷。通常,在接收到來自外圍硬體的非同步訊號,或來自軟體的同步訊號之後,處理器將會進行相應的硬體/軟體處理。發出這樣的訊號稱為進行中斷請求(IRQ)。 – Wikipedia
以 QEMU 中的 RISC-V 虛擬機器 Virt 為例,它的原始碼 ↗就定義了不同中斷的 IRQ:
enum {
UART0_IRQ = 10,
RTC_IRQ = 11,
VIRTIO_IRQ = 1, /* 1 to 8 */
VIRTIO_COUNT = 8,
PCIE_IRQ = 0x20, /* 32 to 35 */
VIRTIO_NDEV = 0x35 /* Arbitrary maximum number of interrupts */
};當我們在撰寫作業系統時,就可以利用 IRQ 的代號去判別外部中斷的類型,處理鍵盤輸入、磁碟讀寫的問題。
PLIC 的 Memory Map#
至於我們到底該如何與 PLIC 進行溝通呢?PLIC 是採取 Memory Map 的機制,它會將一些重要的資訊映射到 Main Memory 當中,如此一來,我們就可以透過存取記憶體的方式做到與 PLIC 的溝通。
我們可以繼續看到 Virt 的原始碼 ↗,它定義了 PLIC 的虛擬位置:
static const MemMapEntry virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 },
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
[VIRT_UART0] = { 0x10000000, 0x100 },
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FW_CFG] = { 0x10100000, 0x18 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 },
};每一個 PLIC 的中斷源都會由一個暫存器作為代表,將 PLIC_BASE 加上暫存器的偏移量 offset,我們就可以知道暫存器映射到主記憶體的位置:
0xc000000 (PLIC_BASE) + offset = Mapped Address of register讓 mini-riscv-os 支援外部中斷#
首先看到 plic_init(),該函式定義在 plic.c:
void plic_init()
{
int hart = r_tp();
// QEMU Virt machine support 7 priority (1 - 7),
// The "0" is reserved, and the lowest priority is "1".
*(uint32_t *)PLIC_PRIORITY(UART0_IRQ) = 1;
/* Enable UART0 */
*(uint32_t *)PLIC_MENABLE(hart) = (1 << UART0_IRQ);
/* Set priority threshold for UART0. */
*(uint32_t *)PLIC_MTHRESHOLD(hart) = 0;
/* enable machine-mode external interrupts. */
w_mie(r_mie() | MIE_MEIE);
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
}看到上面的範例,plic_init() 主要做了這些初始化動作:
- 設定 UART_IRQ 的優先權:因為 PLIC 可以管理多個外部中斷源,我們必須為不同的中斷源設定優先順序,當這些中斷源衝突時,PLIC 才會知道要先處理哪個 IRQ。
- 針對 hart0 開啟 UART 中斷。
- 設定 threshold:小於或是等於這個閥值的優先級會被 PLIC 無視。如果我們將範例改成:
*(uint32_t *)PLIC_MTHRESHOLD(hart) = 1;由於先前的程式碼 *(uint32_t *)PLIC_PRIORITY(UART0_IRQ) = 1; 將 UART_IRQ 的優先級設為 1,在 Threshold 相等的情況下,系統並不會處理 UART 的 IRQ。
- 開啟外部中斷與 Machine mode 下的全局中斷:需要注意的是,本專案原先是在
trap_init()開啟 Machine mode 下的全局中斷,在這次的修改後,我們改讓plic_init()負責。
除了 PLIC 需要做初始化以外,還有 UART 需要做初始化設定,像是設定 baud rate 等動作,
uart_init()定義在lib.c中,有興趣的讀者可以自行查閱。
修改 Trap Handler#
+----------------+
| soft_handler() |
+-------+----------------+
|
+----------------+-------+-----------------+
| trap_handler() | | timer_handler() |
+----------------+ +-----------------+
|
+-------+-----------------+
| exter_handler() |
+-----------------+先前 trap_handler() 只有支援時間中斷的處理,這次我們則是要讓它支援外部中斷的處理:
/* In trap.c */
void external_handler()
{
int irq = plic_claim();
if (irq == UART0_IRQ)
{
lib_isr();
}
else if (irq)
{
lib_printf("unexpected interrupt irq = %d\n", irq);
}
if (irq)
{
plic_complete(irq);
}
}因為本次的目標是讓作業系統能夠處理 UART IRQ,所以透過上面的程式碼不難發現我們只對 UART 做處理:
/* In lib.c */
void lib_isr(void)
{
for (;;)
{
int c = lib_getc();
if (c == -1)
{
break;
}
else
{
lib_putc((char)c);
lib_putc('\n');
}
}
}lib_isr() 的原理也相當簡單,只是重複的偵測 UART 的 RHR 暫存器有沒有收到新的資料,如果沒有(c == -1)則跳出迴圈。
與 UART 有關的暫存器都定義在
riscv.h之中,這次為了支援lib_getc()添加了一些暫存器位址,大致內容如下:
#define UART 0x10000000L
#define UART_THR (volatile uint8_t *)(UART + 0x00) // THR:transmitter holding register
#define UART_RHR (volatile uint8_t *)(UART + 0x00) // RHR:Receive holding register
#define UART_DLL (volatile uint8_t *)(UART + 0x00) // LSB of Divisor Latch (write mode)
#define UART_DLM (volatile uint8_t *)(UART + 0x01) // MSB of Divisor Latch (write mode)
#define UART_IER (volatile uint8_t *)(UART + 0x01) // Interrupt Enable Register
#define UART_LCR (volatile uint8_t *)(UART + 0x03) // Line Control Register
#define UART_LSR (volatile uint8_t *)(UART + 0x05) // LSR:line status register
#define UART_LSR_EMPTY_MASK 0x40 // LSR Bit 6: Transmitter empty; both the THR and LSR are empty
本次提交的修改內容大致如上,其中還有一些實作細節沒有特別提出,建議有興趣的讀者可以直接 trace 原始碼,相信會更有收穫。有了這些基礎,之後可以添加像是:
- virtio driver 與 file system
- system call
- mini shell
等功能,讓 mini-riscv-os 更具規模。