RISC V::中断与异常处理 -- PLIC 介绍

本文目标

  • 认识 PLIC, IRQ 与 ISR
  • 综合先前所学,应用在实际案例上

进入正题

PIC

PIC (Programmable Interrupt Controller) 是一个特殊用途的电路,可以帮助处理器处理不同来源的 (同时) 发生的中断请求。它有助於确定 IRQ 的优先级,让 CPU 执行切换到最合适的中断处理常式 (ISR)。

Interrupt

先重新复习 RISC-V 的中断种类,可细分为几大项:

  • 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
    小於或是等於这个阀值的 IRQ 会被 PLIC 无视,如果我们将范例改成:
*(uint32_t *)PLIC_MTHRESHOLD(hart) = 10;

这样系统就不会处理 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 更具规模。

Reference


<<:  【Day 23】JavaScript 条件(三元)运算子

>>:  9 结束这回合

Day03. 进入No code/Low code 的世界- 安装 Blue Prism

可曾想过一家企业有着IT与非IT背景的团队(例如:行销部门)共事, 企业如何运用这些人才解决这每日堆...

【Day 4】机器学习基本功(二) --- Regression

如何找到一个函式(function)?(上) 接下来会以李宏毅老师在影片中讲的例子来做说明整理。 寻...

Day14 NodeJS-NPM I

终於进入NodeJS中最为人知的套件管理系统NPM了,不讳言的当初对NodeJS一知半解的我对於No...

17 - Visual Studio Code - 代码编辑器与它的插件

一般功能丰富的 IDE ,都会针对它所支援的语言提供许多强大的辅助功能,例如 PyCharm ,但它...

Day 21 - 背景 Gradient 使用

欢乐的时光总是过得特别快,不知不觉连假就要结束了,不过威尔猪也太悲催,为了铁人赛,中秋节还要在电脑...