本文目标
PIC (Programmable Interrupt Controller) 是一个特殊用途的电路,可以帮助处理器处理不同来源的 (同时) 发生的中断请求。它有助於确定 IRQ 的优先级,让 CPU 执行切换到最合适的中断处理常式 (ISR)。
先重新复习 RISC-V 的中断种类,可细分为几大项:
各种中断的 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 (Platform-Level Interrupt Controller) 就是为了 RISC-V 平台所打造的 PIC 。
实际上,会有多个中断源 (键盘、滑鼠、硬碟...) 接上 PLIC,PLIC 会判别这些中断的优先级,再分配给处理器的 Hart (RISC-V 中 hardware thread 的最小单位) 进行中断处理。
在电脑科学中,中断是指处理器接收到来自硬体或软件的讯号,提示发生了某个事件,应该被注意,这种情况就称为中断。 通常,在接收到来自外围硬体的非同步讯号,或来自软件的同步讯号之後,处理器将会进行相应的硬体/软件处理。发出这样的讯号称为进行中断请求 (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 进行沟通呢?
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
首先,看到 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()
主要做了这些初始化动作:
*(uint32_t *)PLIC_MTHRESHOLD(hart) = 10;
这样系统就不会处理 UART 的 IRQ 了。
trap_init()
开启 Machine mode 下的全局中断,在这次的修改後,我们改让 plic_init()
负责。除了 PLIC 需要做初始化以外,还有 UART 需要做初始化设定,像是设定 baud rate 等动作,
uart_init()
定义在lib.c
中,有兴趣的读者可以自行查阅。
+----------------+
| 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 原始码,相信会更有收获。
有了这些基础,之後可以添加像是:
mini-riscv-os
更具规模。
<<: 【Day 23】JavaScript 条件(三元)运算子
可曾想过一家企业有着IT与非IT背景的团队(例如:行销部门)共事, 企业如何运用这些人才解决这每日堆...
如何找到一个函式(function)?(上) 接下来会以李宏毅老师在影片中讲的例子来做说明整理。 寻...
终於进入NodeJS中最为人知的套件管理系统NPM了,不讳言的当初对NodeJS一知半解的我对於No...
一般功能丰富的 IDE ,都会针对它所支援的语言提供许多强大的辅助功能,例如 PyCharm ,但它...
欢乐的时光总是过得特别快,不知不觉连假就要结束了,不过威尔猪也太悲催,为了铁人赛,中秋节还要在电脑...