RVOS的trap

看 xv6 的代码有点迷惑了,对于需要看 qemu 手册和 RISC-V 手册的实现不太懂,来汪辰老师的 RVOS 课程补一补,讲的真好,很精妙的设计!

05 trap和中断

mtvec 的 两种模式

  • dircet:

  • vector:一张中断向量表,处理效率更高

分清什么是硬件自动执行的,什么是我们手动来实现的,才知道怎么code

处理 trap 的时候我们可以修改 mepc 的值达到改变 mret 返回地址的目的

mtval辅助 mcause来告诉我们异常或者中断更加详细的信息

mpp占两位,因为可能是 m,s,u 这三个特权级要用 2 bit表示

没有 upp (因为他只有一种可能:自己陷入自己)

RISC-V Trap 处理流程

image-20240510202113155

top half是不受我们控制的,硬件发生的过程

  • 把 mstatus 的 MIE 值复制到 MPIE 中,清除 mstatus 中的 MIE 标 志位,效果是中断被禁止。
  • 设置 mepc ,同时 PC 被设置为 mtvec。(需要注意的是,对于 exception, mepc 指向导致异常的指令;对于 interrupt,它指向被 中断的指令的下一条指令的位置。
    • 为什么要这样设置? 发生异常的时候相当于给了你一个机会,你可以执行你的异常处理函数,你可以在这里面解决掉你的异常,例子:缺页异常
    • 而中断一般是外设产生的,要去执行一段外设的逻辑,回来的话回到下一条,达到对应用程序看来好像什么都没有发生过的效果
  • 根据 trap 的种类设置 mcause,并根据需要为 mtval 设置附加信息
  • 将 trap 发生之前的权限模式保存在 mstatus 的 MPP 域中,再把 hart 权限模式更改为 M(也就是说无论在任何 Level 下触发 trap, hart 首先切换到 Machine 模式)。

所以要使用 s 模式的话这些也是一样的吗?

bottom-half:软件需要做的事,以下是在汇编中写的

  • 保存当前控制流的上下文信息
  • 调用 c 语言的 trap handler
  • 从 trap handler 返回, mepc 的值有可能需要调整
  • 恢复上下文
  • 执行 mret 指令返回到 trap 之前的状态

mret指令:还有sret/uret

  • 当前 hart 的权限级别 = mstatus.MPP;mstatus.MPP = U(如果 hart 不支持 U 则为 M)(恢复hart trap前的权限级别)
  • mstatus.MIE = mstatus.MPIE; mstatus.MPIE=1 (恢复hart trap前的中断使能)
  • pc = mepc (跳转到 trap 前的执行流)

很有意思的点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;

if (cause & MCAUSE_MASK_INTERRUPT) {
/* 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:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
// panic("OOPS! What can I do!");
return_pc += 4;
}

return return_pc;
}

试试第25,26行:分别注释掉的效果

只使用 25 行:panic死循环

只使用 26 行:改变了 return 到了下一条指令,那么可以返回这里异常的下一条指令

06 外部设备中断

中断分类

  • 本地中断
    • 软件中断
    • 时钟中断
  • 全局中断
    • 外部中断:外设产生的中断

问题:前面提到的 mstatus.mie 和这里的 mie 寄存器什么关系?

mstatus. mie 控制全局的中断使能状态,而 mie 寄存器中的各个位控制具体的中断源是否被使能

比如你要打开时钟中断(mie里的MTIE):要保证 mstatus.mie = 1 && mie.MTIE = 1

寄存器

mie:打开或者关闭 M/S/U 模式下对应的 External/Timer/Software 中断

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

mie可以理解为是用来写的,mip可以理解为是用来读的

PLIC介绍

为什么需要PLIC,因为外部设备很多,不可能对每一个外设都设置一个引脚,来告诉 cpu 这个外设产生中断了,于是需要一个中断代理,这样我们只需要一个引脚,简化了cpu的设计复杂度

PLIC每次只放一个中断进来

image-20240510212629762

中断源介绍:

1
2
3
4
5
6
7
8
9
//https://github.com/qemu/qemu/blob/master/include/hw/riscv/virt.h
enum {
UART0_IRQ = 10,
RTC_IRQ = 11,
VIRTIO_IRQ = 1, /* 1 to 8 */
VIRTIO_COUNT = 8,
PCIE_IRQ = 0x20, /* 32 to 35 */
VIRT_PLATFORM_BUS_IRQ = 64, /* 64 to 95 */
};
  • 0 预留不用

PLIC编程接口-寄存器

priority 设置 某一路中断源的优先级

  • 内存映射地址:base + id * 4
  • 每个 PLIC 中断源对应一个寄存器,用于配置该中断源的优先级
  • QEMU-virt 支持 7 个优先级。 0 表示对该中断源禁用中断。 其余优先级,1 最低,7 最高
  • 如果两个中断源优先级相同,则根据中断源的 ID 值进一步 区分优先级,ID 值越小的优先级越高
1
2
3
4
#define PLIC_PRIORITY(id) (PLIC_BASE + (id) * 4)

//plic_init() 设置 UART0_IRQ 这个中断源的优先级
*(uint32_t*)PLIC_PRIORITY(UART0_IRQ) = 1;

pending :指示某一路中断源是否发生

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

Enable:针对某个 hart 开启或者关闭某一路中断源

  • 每个 Hart 有 2 个 Enable 寄存器 (Enable1 和 Enable2)用 于针对该 Hart 启动或者关闭某路中断源
  • 每个中断源对应 Enable 寄存器的一个 bit,其中 Enable1 负责控制 1 ~ 31 号中断源;Enable2 负责控制 32 ~ 53 号中断源。 将对应的 bit 位设置为 1 表示使能该中断源,否则表示关闭该中断源
1
2
#define PLIC_MENABLE(hart, id) (PLIC_BASE + 0x2000 + (hart) * 0x80 + ((id) / 32) * 4)
*(uint32_t*)PLIC_MENABLE(hart, UART0_IRQ)= (1 << (UART0_IRQ % 32));

Threshold :针对某个 hart 设置中断源优先级的阈值

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

Claim/Complete:这两个是同一个寄存器, 每个 hart 一个

  • Claim:对该寄存器执行读操作,获取当前 hart 发生的最高优先级的中断源 id,claim成功后会清除对应的 pending 位
  • Complete:对该寄存器执行写操作,通知 PLCI 这个中断处理已经结束,可以接受下一个了

uart 为例

以 uart 这个中断源为例,我们如果要处理一个中断源,应该怎么编程

  1. 在 plic_init 阶段

    • 中断源:设置该中断源优先级、设置 enable 寄存器使能该终端源
    • hart:设置 threshold 阈值,使能 mie,mstatus
  2. 具体 uart 的中断处理逻辑

    • trap处理具体的 cause 时,加入一个外部中断的处理
    • 在外部中断处理里面加入判断,首先通过 plic_claim 获取 irq,如果 irq 是 uart 的 id 就执行它的中断流程
    • 然后完成之后记得调用 plic_complete
  3. 注意在 uart_init 里面需要打开 uart 自身可以接受中断

总结:编程时需要注意有的寄存器是 hart 级别的,有的寄存器是全局的,需要区别

07定时器中断

CLINT (Core Local INTerrupt)

CLINT编程接口

mtime :real-time 计时器

  • 系统保证该计数器的值始终按照一个固定的频率递增
  • 上电复位时,硬件负责将mtime的值恢复为0

mtimecmp

  • 每个 hart 一个 mtimecmp 寄存器,64-bit
  • 上电复位时,系统不负责设置 mtimecmp 的初值,所以要你自己来

这个寄存器的用处就是用来触发时钟中断:

  • 当 mtime >= mtimecmp 时,CLINT 会产生一个 timer 中断。 如果要使能该中断需要保证全局中断打开并且 mie.MTIE 标志位 置 1
  • 当 timer 中断发生时,hart 会设置 mip.MTIP,程序可以在 mtimecmp 中写入新的值清除 mip.MTIP

image-20240510223837290

Tick:

  • os 中最小的时间单位
  • Tick 的单位(周期)由硬件定时器的周期决定(通常为 1~ 100 ms)

RVOS的trap
http://example.com/2024/05/10/RVOS的trap/
Author
Jianhui Yin
Posted on
May 10, 2024
Licensed under