RISC-V 入门-Trap

Trap 简介

控制流(Control Flow)和 Trap

  • 控制流(Control Flow)
    从给处理器加电开始,直到你断电为止,程序计数器假设一个值的序列
    $$a_0,a_1,\dotsb,a_{n-1}$$
    每个$a_k$都是指令的地址,每次从$a_{k}$到$a_{k+1}$的过渡称为控制转移,而这样的控制转移序列叫做处理器的控制流。
  • 异常控制流(Exceptional Control Flow, ECF)
    系统也必须能够对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定要和程序的执行相关。比如,一个硬件定时器定期产生信号,这个事件必须得到处理。包到达网络适配器后,必须存放在内存中。程序向磁盘请求数据,然后休眠,直到被通知说数据已就绪。现代系统通过使控制流发生突变来对这些情况做出反应。我们把这些突变称为异常控制流。
    • exception
    • interrupt

RISC-V 把 ECF 统称为 Trap

RISC-V Trap 处理中涉及的寄存器

寄存器 全称 用途说明
mtvec Machine Trap-Vector Base-Address 它保存发生异常时处理器需要跳转到的地址。
mepc Machine Exception Program Counter 当 trap 发生时,hart 会将发生 trap 所对应的指令的地址值(pc)保存在 mepc 中。
mcause Machine Cause 当 trap 发生时,hart 会设置该寄存器通知我们 trap 发生的原因。
mtval Machine Trap Value 它保存了 exception 发生时的附加信息:譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身,对于其他异常,它的值为 0。
mstatus Machine Status 用于跟踪和控制 hart 的当前操作状态(特别地,包括关闭和打开全局中断)。
mscratch Machine Scratch Machine 模式下专用寄存器,我们可以自己定义其用法,譬如用该寄存器保存当前在 hart 上运行的 task 的上下文(context)的地址。

mtvec(Machine Trap-Vector Base-Address)

WARL: Write Any Values, Read Legal Values

  • BASE:trap 入口函数的基地址,必须保证四字节对齐;

  • MODE:进一步用于控制入口函数的地址配置方式:

    • Direct,所有异常和中断发生后,PC都跳转到BASE指定的地址处;

      通常中断处理函数内部会有switch case条件语句,通过不同的中断采用不同的处理方式。

      reg_t trap_handler(reg_t epc, reg_t cause)
      {
          reg_t return_pc = epc;
          reg_t cause_code = cause & 0xfff;
          
          if (cause & 0x80000000) {
              /* Asynchronous trap - interrupt */
              switch (cause_code) {
              case 3:
                  uart_puts("software interruption!\n");
                  break;
              case 7:
                  uart_puts("timer interruption!\n");
                  break;
              case 11:
                  uart_puts("external interruption!\n");
                  break;
              default:
                  uart_puts("unknown async exception!\n");
                  break;
              }
          } else {
              /* Synchronous trap - exception */
              printf("Sync exceptions!, code = %d\n", cause_code);
              panic("OOPS! What can I do!");
              //return_pc += 4;
          }
      
          return return_pc;
      }
    • Vectored,异常的处理方式同上,但是中断的入口地址以数组方式排列;

      trap_vector:
          # save context(registers).
          csrrw t6, mscratch, t6 # swap t6 and mscratch
          reg_save t6
      
          # Save the actual t6 register, which we swapped into
          # mscratch
          mv t5, t6  # t5 points to the context of current task
          csrr t6, mscratch # read t6 back from mscratch
          sw t6, 120(t5) # save t6 with t5 as base
      
          # Restore the context pointer into mscratch
          csrw mscratch, t5
      
          # call the C trap handler in trap.c
          csrr a0, mepc
          csrr a1, mcause
          call trap_handler
      
          # trap_handler will return the return address via a0.
          csrw mepc, a0
      
          # restore context(registers).
          csrr t6, mscratch
          reg_restore t6
      
          # return to whatever we were doing before trap.
          mret

      MODE可取值如下:

采用Vectored方式效率更高。

mepc(Machine Exception Program Counter)

trap发生时,pc会被替换为 mtvec设定的地址,同时hart 会设置mepc为当前指令或者下一条指令的地址(处理异常时,mepc 为当前指令的地址,处理中断时,mepc 为下一条指令的地址)。

当我们需要退出trap 时可以调用特殊的 mret 指令,该指令会将mepc中的值恢复到pc中(实现返回的效果);

在处理 trap 的程序中我们可以修改 mepc 的值达到改变mret 返回地址的目的。

mcause(Machine Cause)

trap 发生时,hart 会设置该寄存器通知我们 trap 发生的原因。

最高位 Interrupt 为 1 时标识了当前 trapinterrupt,否则是exception

剩余的 Exception Code 用于标识具体的interrupt或者exception 的种类。

mtval(Machine Trap Value)

trap 发生时,除了通过mcause 可以获取exception的种类 code 值外,hart 还提供了 mtval 来提供exception 的其他信息来辅助我们执行更进一步的操作。

具体的辅助信息由特定的硬件实现定义,RISC-V 规范没有定义具体的值。但规范定义了一些行为,譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身等。

mstatus(Machine Status)

寄存器各个位可以大致分为以下三类,其中x可以为U,S,M。表示用户模式以及两种特权模式。

  • xIE(x=M/S/U): 分别用于打开(1)或者关闭(0)M/S/U 模式下的全局中断。当 trap 发生时,hart会自动将 xIE 设置为 0。

  • xPIE(x=M/S/U):当 trap 发生时用于保存 trap 发生之前的 xIE 值。

  • xPP(x=M/S):当 trap 发生时用于保存 trap 发生之前的权限级别值。注意没有 UPP。因为异常只会从低权限向高权限跳转,通常低权限如user模式,会被置于上方,高权限如内核一般都会画在下方,这也解释了异常,中断处理为什么叫trap,因为是向下陷入的过程。

  • 其他标志位涉及内存访问权限、虚拟内存控制等,暂不考虑。

Trap 处理流程

主要为 Exception,下一章详解 Interrupt。

初始化

trap的基地址写入寄存器,

Top Half

  1. mstatusMIE 值复制到 MPIE 中,清除 mstatus中的 MIE 标志位,效果是中断被禁止。
  2. 设置mepc,同时PC被设置为 mtvec。(需要注意的是,对于exceptionmepc指向导致异常的指令;对于 interrupt,它指向被中断的指令的下一条指令的位置。)
  3. 根据 trap 的种类设置 mcause,并根据需要为mtval设置附加信息。
  4. trap 发生之前的权限模式保存在 mstatusMPP 域中,再把hart 权限模式更改为 M(也就是说无论在任何 Level 下触发traphart 首先切换到 Machine 模式)。

Bottom Half

  1. 保存(save)当前控制流的上下文信息(利用 mscratch);
  2. 调用 C 语言的 trap handler;
  3. trap handler 函数返回,mepc的值有可能需要调整;
  4. 恢复(restore)上下文的信息;
  5. 执行MRET指令返回到 trap之前的状态。
trap_vector:
 # save context(registers).
 csrrw t6, mscratch, t6 # swap t6 and mscratch
 reg_save t6

 # Save the actual t6 register, which we swapped into
 # mscratch
 mv t5, t6  # t5 points to the context of current task
 csrr t6, mscratch # read t6 back from mscratch
 sw t6, 120(t5) # save t6 with t5 as base

 # Restore the context pointer into mscratch
 csrw mscratch, t5

 # call the C trap handler in trap.c
 csrr a0, mepc
 csrr a1, mcause
 call trap_handler

 # trap_handler will return the return address via a0.
 csrw mepc, a0

 # restore context(registers).
 csrr t6, mscratch
 reg_restore t6

 # return to whatever we were doing before trap.
 mret

退出 trap:编程调用 MRET 指令

针对不同权限级别下如何退出 trap 有各自的返回指令xRET(x = M/S/U)。以在 M 模式下执行mret 指令为例,会执行如下操作:

  • 当前 Hart 的权限级别 = mstatus.MPPmstatus.MPP = U(如果 hart 不支持 U 则为 M)
  • mstatus.MIE = mstatus.MPIE; mstatus.MPIE = 1
  • pc = mepc

中断

中断分类

  • 本地(Local)中断

    • 软中断software interrupt
    • 定时器中断 timer interrupt
  • 全局(Global)中断

    • 外部中断 externel interrupt

RISC-V 中断编程中涉及的寄存器

寄存器 全称 用途说明
mie Machine Interrupt Enable 用于进一步控制(打开和关闭)software interrupt/timer interrupt/external interrupt
mip Machine Interrupt Pending 它列出目前已发生等待处理的中断。

mie(Machine Interrupt Enable)

打开(1)或者关闭(0)M/S/U 模式下对应的 External/Timer/Software 中断。

mip(Machine Interrupt Pending)

获取当前 M/S/U 模式下对应的 External/Timer/Software 中断是否发生。

中断处理流程

中断处理

  1. mstatusMIE 值复制到 MPIE 中,清除 mstatus 中的 MIE 标志位,效果是中断被禁止。
  2. 当前的 PC 的下一条指令地址被复制到 mepc 中,同时 PC 被设置为mtvec。注意如果我们设置 mtvec.MODE = vetcoredPC =mtvec.BASE + 4 × exception-code
  3. 根据 interrupt 的种类设置 mcause,并根据需要为 mtval 设置附加信息。
  4. trap 发生之前的权限模式保存在 mstatusMPP 域中,再把hart 权限模式更改为 M

退出中断

以在 M 模式下执行 mret 指令为例,会执行如下操作:

  • 当前 Hart 的权限级别 = mstatus.MPP; mstatus.MPP= U(如果 hart 不支持 U 则为 M)
  • mstatus.MIE = mstatus.MPIE; mstatus.MPIE = 1
  • pc = mepc

PLIC(Platform-Level Interrupt Controller)

PLIC 简介

HART 只能处理一个中断,PLIC 相当于一个控制中心,它通过中断类型,优先级等等来选出一个需要处理的中断。协调多个中断,服务一个 HART。

enum {
    UART0_IRQ = 10, //Interrupt Source ID
    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 */
};
  • Interrupt Source ID 范围:1 ~ 53(0x35)
  • 0 预留不用

PLIC本身也是一个外设,RISC-V 规范规定,PLIC 的寄存器编址采用内存映射(memory map)方式。每个寄存器的宽度为 32-bit。

具体寄存器编址采用 base + offset 的格式,且 base 由各个特定platform 自己定义。针对 QEMU-virt,其 PLIC 的设计参考了FU540-C000base0x0c000000

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_ACLINT_SSWI] = {  0x2F00000,        0x4000 },
    [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 编程接口 - 寄存器

Priority

功能:设置某一路中断源的优先级
内存映射地址:BASE + (interrupt-id) * 4

  • 每个 PLIC 中断源对应一个寄存器,用于配置该中断源的优先级。
  • QEMU-virt 支持 7 个优先级。0 表示对该中断源禁用中断。其余优先级,1 最低,7 最高。
  • 如果两个中断源优先级相同,则根据中断源的 ID 值进一步区分优先级,ID 值越小的优先级越高。

Pending

功能:用于指示某一路中断源是否发生
内存映射地址:BASE + 0x1000 + ((interrupt-id) / 32) * 4

  • 每个 PLIC 包含 2 个 32 位的 Pending 寄存器,因为总共有 54 个中断源,每一个 bit 对应一个中断源,如果为 1 表示该中断源上发生了中断(进入Pending 状态),有待 hart 处理,否则表示该中断源上当前无中断发生。
  • Pending 寄存器中断的 Pending 状态可以通过claim 方式清除。
  • 第一个 Pending 寄存器的第 0 位对应不存在的 0 号中断源,其值永远为 0。

Enable

功能:针对某个 hart 开启或者关闭某一路中断源
内存映射地址:BASE + 0x2000 + (hart) * 0x80

  • 每个 Hart 有 2 个 Enable 寄存器(Enable1Enable2)用于针对该Hart 启动或者关闭某路中断源。
  • 每个中断源对应 Enable 寄存器的一个 bit,其中Enable1 负责控制 1 ~ 31 号中断源;Enable2 负责控制 32 ~ 53 号中断源。将对应的 bit 位设置为 1 表示使能该中断源,否则表示关闭该中断源。

Threshold

功能:针对某个 hart 设置中断源优先级的阈值
内存映射地址:BASE + 0x200000 + (hart) * 0x1000

  • 每个 Hart 有 1 个 Threshold 寄存器用于设置中断优先级的阈值。
  • 所有小于或者等于(<=)该阈值的中断源即使发生了也会被 PLIC 丢弃。特别地,当阈值为 0 时允许所有中断源上发生的中断;当阈值为 7 时丢弃所有中断源上发生的中断。

Claim/Complete

功能:如下
内存映射地址:BASE + 0x200004 + (hart) * 0x1000

  • ClaimComplete 是同一个寄存器,每个 Hart 一个。
  • 对该寄存器执行读操作称之为 Claim,即获取当前发生的最高优先级的中断源IDClaim 成功后会清除对应的 Pending 位。
  • 对该寄存器执行写操作称之为 Complete。所谓 Complete指的是通知PLIC 对该路中断的处理已经结束。
void external_interrupt_handler()
{
 int irq = plic_claim(); //

 if (irq == UART0_IRQ){
        uart_isr();
 } else if (irq) {
  printf("unexpected interrupt irq = %d\n", irq);
 }
 
 if (irq) {
  plic_complete(irq); //
 }
}

CLINT (Core Local INTerruptor)

定时器中断,属于本地中断的一种,由芯片内部CLINT设备产生的中断。

  • RISC-V 规范规定,CLINT 的寄存器编址采用内存映射(memory map)方式。
  • 具体寄存器编址采用base + offset的格式,且 base 由各个特定 platform 自己定义。针对 QEMU-virt,其 CLINT 的设计参考了 SFIVEbase0x2000000

CLINT 编程接口 - 寄存器 (Timer 部分)

mtime

功能:real-time 计数器(counter)
内存映射地址:BASE + 0xbff8

  • 由晶振产生,系统全局唯一,在 RV32RV64 上都是 64-bit。系统必须保证该计数器的值始终按照一个固定的频率递增。
  • 上电复位时,硬件负责将 mtime 的值恢复为 0。

mtimecmp

功能:定时器比较寄存器
内存映射地址:BASE + 0x4000 + (hart) * 8)

  • 每个 hart 一个 mtimecmp 寄存器,64-bit。

  • 上电复位时,系统不负责设置 mt`imecmp 的初值。

  • mtime >= mtimecmp 时,CLINT 会产生一个 timer 中断。如果要使能该中断需要保证全局中断打开并且mie.MTIE 标志位置 1

  • timer 中断发生时,hart 会设置 mip.MTIP,程序可以在 mtimecmp 中写入新的值清除mip.MTIP

时钟节拍 tick

  • 操作系统中最小的时间单位;
  • Tick 的单位(周期)由硬件定时器的周期决定
    (通常为 1 ~ 100ms);
  • Tick 周期越小,也就是1s内产生的中断越多,系统的精度越高,但开销越大。