Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

CLINT的SBI调用链实现

CLINT的SBI调用链实现

本文主要讲述我在实现SBI调用链实现CLINT定时时遇到的问题,以及如何解决。

需要使用到以下文件:

  • RISCV-SBI-Spec.pdf
  • RISCV_ABI.pdf
  • RISCV-Privileged.pdf

涉及到的章节:

  • SBI:
    • Chapter 1 Introduction
  • ABI:
    • Chapter 1.1 Integer Register Convention
  • Privileged:
    • Chapter 3.1.5. Hart ID (mhartid) Register
    • Chapter 3.1.6.1 Privilege and Global Interrupt-Enable Stack in mstatus register
    • Chapter 3.1.8. Machine Trap Delegation (medeleg and mideleg) Registers
    • Chapter 3.1.9. Machine Interrupt (mip and mie) Registers
    • Chapter 3.1.13. Machine Scratch (mscratch) Register
    • Chapter 3.1.15. Machine Cause (mcause) Register
    • Chapter 3.2.1. Machine Timer (mtime and mtimecmp) Registers

1. 引言

本文默认读者已经了解简单的RISC-V架构,以及RISC-V SBI和OpenSBI。

在前面实现OS内核的时候,我选择的是-bios none的启动方式,也就是意味着无法使用OpenSBI,所以我的打算就是自行实现SBI调用链,尽量往工程实践中靠拢。

2. 简要

在阅读RISCV-SBI-Spec.pdf,简介中简单描述了SBI的调用

|    User Mode          |
         |
         |     System Call
         ↓
|    Supervisor Mode    |
         |
         |     SBI Call
         ↓
|    Machine Mode       |

我们可以看到SBI的调用是从S模式进入到M模式,然后实现SBI的功能后,再返回到S模式。

所以,在这里就会涉及到特权级切换。

同时我们也要知道,在RISCV中的CLINT是一个时钟中断控制器,当他发生时钟中断时,会发送Machine Timer Interrupt,让我们的OS进入到M模式(在没有进行中断委托的时候)。

所以,因为CLINT的时钟中断,我们也就会涉及到中断,委托中断的设置。

3. 总体思路

首先是中断,及中断委托的问题。

我们在先进入mstart函数时,处于M模式下,通常这个情况下,会进行基础的配置和中断委托。

涉及到CLINT中的Machine Timer Interrupt,所以在开始的配置过程中,我们要将M模式下的Machine Timer Interrupt委托到S模式。

同时要将M模式下的全局中断打开,也就是mie中的信息。

RISCV-Privilegedmie中有比较详细的介绍,我们要开启MTIE中断使能位,不过需要保证的是,在开启mstatus中的MIE中断使能位时,一定要有处理Machine timer Interrupt的函数,也就是需要M态的中断处理入口及处理函数,否则的话,OS会直接因为中断无法处理,一直陷入中断,导致OS直接挂起。

当然,其中的Supervisor timer interrupt同时也需要委托到S模式,因为M模式下会处理Machine timer Interrupt,我们想要实现到S模式下的中断,就需要去变通一下,也就是在M模式处理Machine timer interrupt时,将MIP中的STIP中断挂起位置1,使其可以在S模式下,可以响应被委托的Supervisor timer interrupt中断。

所以简化一点:

  1. 设置sie.STIE,使其响应Supervisor timer interrupt,开启sstatus.SIE

  2. 通过SBI设置下一次的中断时间。

  3. 时间到达后,M模式响应Machine timer interrupt,处理中断,并将下一次的响应时间设置到无限远(等待设置下一次响应时间),并置mip.STIP让M模式中断后,让S模式响应Supervisor timer interrupt
  4. S模式响应Supervisor timer interrupt,在处理函数中,通过SBI设置下一次的中断时间
  5. SBI通过ecall进入M模式中断处理函数,处理Environment call from S-mode异常,通过传入的参数,设置下一次的响应时间,并清除mip.STIP中断挂起位,防止S模式中断死循环。
  6. 返回2,进行循环。

4. 需要注意的细节

  • x模式下,xstataus.xie,xie,xip的区别及作用。

首先,先说xie和xip的区别,首先xie是控制中断使能位,xip控制中断挂起位,xie控制对应模式是否想管,xip控制对应模式是否触发中断。而xstatus.mie管理是否启动xie寄存器。

所以可以简化为

if(xstatus.mie && (mie & mip))

还有一些细节,就需要去看RISCV-Privileged中的miemip的章节。

  • CLINT的配置

CLINT的mie,mstatus的配置要及时在M模式下进行配置,否则在S模式下配置,而且没有中断委托和处理的话,会导致系统直接挂起。

5. 具体实现

void pre_timerinit() {
  uint64 mie = r_mie();
  mie &= ~(MIE_MSIE | MIE_MEIE);
  mie |= MIE_MTIE;
  w_mie(mie);

  //
  w_mcounteren(0xffff);
}

void timerinit() {
  kprintf("Enable time interrupts...\n");

  w_sie(r_sie() | SIE_STIE);
  sbi_set_timer(r_time() + 1000000);
  w_sstatus(r_sstatus() | SSTATUS_SIE);
  kprintf("Enabled\n");
}

pre_tiemrinit是需要在M模式下进行配置,而且需要先实现w_stvec,并及时实现处理Machine timer interrupt,否则会因为设置完mie后,CLINT触发Machine timer interrupt没有处理函数导致OS挂起

w_mcounterrn的配置就是为了实现在S模式下访问time寄存器,可以直接去看RISCV-Privilege中相关的内容

tiemrinit放在S模式下及时初始化即可。

void s_trap_handler(void) {
  uint64 sc = r_scause();
  uint64 epc = r_sepc();
  uint64 tval = r_stval();
  // kprintf("sc: %p\n", (void *)sc);

  if ((sc >> 63) == 1) {
    uint64 cause = sc & ((1ULL << 63) - 1);
    // determine if it's a timer interrupt
    // wo notify S state by setting SIP_SSIP (Software Interrupt Pending)
    if (cause == 5) {
      // timer interrupt
      // set timer for next interrupt
      sbi_set_timer(r_time() + 1000000);
      kprintf("Tick\n");
      return;
    }
  }
}
void m_trap(uint64 mcause, uint64 mepc, uint64 *regs) {
  // Check whether the most significant bit is ahn exception or an interrupt
  int is_interrupt = (mcause >> 63) & 1;

  uint64 code = mcause & ((1ULL << 63) - 1);
  if (is_interrupt) {
    if (code == 7) {
      uint64 *mtimecmp = (uint64 *)CLINT_MTIMECMP(r_mhartid());
      *mtimecmp = -1ULL; // Set the distance to infinity

      w_mip(r_mip() |
            MIP_STIP); // Key: Set STIP to allow S to receive scause = 5 }
    return;
  } else {
    if (code == 9) {         // ecall from S
      uint64 eid = regs[16]; // a7
      uint64 fid = regs[15]; // a6
      uint64 arg0 = regs[9]; // a0 = stime_value

      if ((eid == SBI_EID_TIME && fid == SBI_FID_SET_TIMER) ||
          (eid == SBI_EID_LEGACY_SET_TIMER)) {

        uint64 *mtimecmp = (uint64 *)CLINT_MTIMECMP(r_mhartid());
        *mtimecmp = arg0;

        regs[9] = SBI_SUCCESS; // a0
        regs[10] = 0;          // a1
      } else {
        regs[9] = -2; // SBI_ERR_NOT_SUPPORTED
        regs[10] = 0;
      }

      w_mip(r_mip() & ~MIP_STIP);

      w_mepc(mepc + 4);
      return;
    } else {
      while (1)
        ;
    }
  }
}

这就是M模式下处理中断函数和S模式下处理中断函数。

路径就是:

M模式下编号为7的machine timer interrupt中断,S模式下的编号为5的Supervisor timer interrupt中断,sbi触发的编号为9的Environment call from S-mode异常,然后循环

记住不要把编号为9的异常委托到S模式下处理,否则M模式无法接收到中断,而导致M无法处理SBI的ecall调用