RISC-V 指令集介绍(三)

发布时间 2024-01-02 08:26:20作者: 吴建明wujianming

RISC-V 指令集介绍(三)

4. 16 位整数计算压缩指令

C Extension 中制定了 2 条 压 缩 指 令, 来 生 成 整 数 常 量(Integer ConstantGeneration Instruction)。它们的定义如图31 所示,它们对应的 32 位指令可以在 表11 中找到。其中,C.LI 指令中的立即数需要做符号扩展,而 C.LUI 中的立即 数则是非零的无符号数。

 

图31. C Extension中的常数生成指令

表 11. 常数生成压缩指令对应的 32 位指令

 

另外,C Extension 中还定义了 2 条立即数加法指令,3 条立即数移位指令和 1 条立即数逻辑指令,其定义如图32 所示。它们对应的 32 位指令可以在表12 中找到,其中 C.ADDI4SPN 指令默认的源寄存器是 x2(sp 栈指针),以便基于栈 指针的计算。

 

图32. C Extension中的寄存器-立即数指令

表12. 寄存器 - 立即数压缩指令对应的 32 位指令

 

同时,和立即数指令相对应,C Extension 中也定义了寄存器 - 寄存器操作的压 缩指令,其定义如图33 所示。它们对应的 32 位指令可以在表13 中找到,其 中的寄存器复制指令 C.MV 实际上是一条将源寄存器 1 默认为 x0 的加法指令。

 

图33 C Extension中的寄存器-寄存器指令

表13 寄存器 - 寄存器压缩指令对应的 32 位指令

 

5. 其他的 16 位压缩指令 (Miscellaneous)

1)C.NOP,16 位空操作指令

和 32 位的空操作指令类似,C Extension 中也利用目标寄存器为零的加法指令来衍生出空操作指令,即 c.nop = c.addi x0,0 = addi x0,x0,0

2)16 位非法操作指令(Illegal Instruction)

和 32 位指令集不同的是,C Extension 专门将全零的编码定义为非法操作指令, 以方便利用硬件异常来处理被零初始化的代码内存。

3)16 位软件断点指令(C.EBREAK)

C Extension 中也为 16 位压缩指令集定义了对应的软件断点指令,其机器代码 为 16'h9002。

6. 函数调用的开场白和收场白

在讨论函数调用约定时,曾经提到通用寄存器 x5 既可以作为临时寄存器(t0), 又可以作为替代链接寄存器(见表1)。

之所以在 RISC-V 中引入 16 位压缩扩展指令集,其初衷就是为了降低代码量, 提高代码密度。而在代码的每个函数调用的开始,往往都需要编写代码,将当前寄 存器值保存到堆栈上。当函数返回时,也需要编写代码,将之前保存的寄存器值从 堆栈恢复到寄存器。这两部分代码,分别称为开场白与收场白。由于函数的调用和 返回在大部分代码中都会频繁出现,压缩指令集的设计者自然会希望将这部分代码 的代码量降至最低。

由此,各种 RISC 指令集和处理器的设计者们给出了不同的解决方案。在加州 大学伯克利分校设计的第一代 RISC 处理器(RISC I)和后来 SUN 公司的 SPARC 处理器当中,都采用了寄存器窗口的解决方案。但是寄存器窗口使得硬件开销变得 非常大,而实际使用效果却并不理想,特别是当通用寄存器被耗尽时,其处理非常 麻烦和缓慢,最终为后来的设计者所放弃。

后来的设计者如 ARM 公司,则在压缩指令集中引入了 Load-Multiple 和 StoreMultiple 指令。这些指令可以在一个指令周期内,将内存中多个地址连续的存储字 载入多个寄存器,或反之将多个寄存器中的内容在一个指令周期内写入内存的连续 地址。使用这些指令,自然可以极大降低开场白与收场白的代码量。根据 Andrew Waterman 在其博士论文中提供的统计数据,使用 Load-Multiple 和 Store-Multiple 以 后,可以将 Linux 内核的代码量降低 8% 左右。

然而,RISC-V 的设计者再三斟酌后,决定忍痛割爱,不在 C Extension 中支持 Load-Multiple 与 Store-Multiple,其原因主要如下:

(1)在前文提到的所有 16 位压缩指令,都可以在 32 位指令集中找到对 应的指令。也就是说,每一条16位压缩指令,都是其对应的32位指令的简写版。 如果引入 Load-Multiple 与 Store-Multiple 指令,则会打破这一原则。

(2)在 3.3 节中提到,RISC-V 的设计目标之一就是希望指令集设计独立 于具体的处理器实现。而引入 Load-Multiple 与 Store-Multiple,会在一定程度 上束缚处理器设计者的手脚,有违 RISC-V 的设计初衷。

(3)对那些有 MMU(Memory Management Unit,内存管理单元)的处 理器,会有虚拟地址(Virtual Address)和物理地址(Physical Address)两种 不同的地址空间。程序在虚拟地址上运行,需要访问内存时,再通过 MMU 将虚拟地址转换为物理地址。这就导致在虚拟地址空间里连续的地址,转 换为物理地址后可能不再连续。如果要在这种系统上实现 Load-Multiple 与 Store-Multiple,会大大增加硬件异常处理的难度。

因此,RISC-V 的设计者别出心裁,在借鉴了 IBM S/390 计算机毫码程序之后, 提出了如下的方案:

(1)由于开场白和收场白的工作仅仅只是将数据在堆栈和寄存器之间移 动的,这些工作完全可以用共享的代码来实现。开场白和收场白本身也可以 用类似函数调用的方式来实现。

(2)然而,这种函数调用是一种特殊的函数调用,因为:

① 这种调用本身不再需要开场白和收场白。

② 由于原先普通函数调用的返回地址需要使用 x1(ra)来存放,调用开 场白和收场白时则不能再使用 x1(ra),而需要在函数调用约定中另外再分 配一个寄存器。这个寄存器就是 x5(t0/ 替代链接寄存器)。

(3)在软件实现时,可以做如下操作:

① 把原先的开场白代码,用 jal t0,shared_prologue 代替,以调用共享的 开场白代码,并将返回地址存入 x5(t0)。

 ② 在共享的开场白代码中,用一连串的 c.swsp 指令将需要保存的寄存器 值(其中也包括 x1(ra))推入堆栈中,最后用 c.jr t0 指令返回。

 ③ 把原先的收场白代码,用 jal x0,shared_epilogue 代替。

 ④ 在共享的收场白代码中,用一连串的 c.lwsp 指令,将之前保存在堆栈 上的寄存器值恢复到对应的寄存器中(其中也包括 x1(ra)),最后用 c.jr ra 结束整个函数调用。

根据 Andrew Waterman 提供的统计数据,使用以上类似毫码程序的方案以后, 可以将 Linux 内核的代码量降低 7.5% 左右,其表现基本上和之前提到的 LoadMultiple/Store-Multiple 方案相当,但是却避免了 Load-Multiple/Store-Multiple 方案 带来的缺点。

RISC-V特权架构

如前所述,RISC-V 的设计者将其官方标准分成了两部分:用户指令集与特权 架构,其目的是希望不同特权架构的处理器可以在 ABI 互相兼容。换句话说,支 持同一用户指令集的处理器可以根据实际需求而在特权架构的设计上采取不同的 策略。

将在后文介绍的软核处理器属于 MCU(Microcontroller Unit,微处理器单 元)的范畴,本章将会重点讨论以下的内容:

● 特权层级,特别是机器模式(Machine Mode, M-Mode)。

● 控制状态寄存器。

● 机器层级指令集。

● 异常和中断。

● 调试。

特权层级

RISC-V 处理器中的软件代码都是在硬件线程上运行的。为了加强对操作系统 和信息安全的支持,RISC-V 替 HART 定义了 3 种工作模式(见图34):机器模式、 超级用户模式(Supervisor Mode,S-Mode)和普通用户模式(User Mode,U-Mode)。 每种模式分别对应一个特权层级(Privilege Levels)。其中机器模式的特权层级最高, 而普通用户模式的特权层级最低。在高特权层级运行的代码比在低特权层级的代码 拥有更多的权限,受到的约束也比低特权层级的代码要少。

 

图34. 特权层级

在处理器设计时,机器模式是强迫要求实现的。其他的两个模式,处理器设计 者则可以选择性地加以实现。一般来说,小规模的嵌入式系统只需要机器模式就可 以了,而对信息安全有特殊要求的系统,则可能需要机器模式加普通用户模式。运 行类似 UNIX 这样大型操作系统的处理器,则需要实现以上所有的模式。

UNIX 和信息安全不在本书的讨论范围之内,剩余部分将会集中于机器 模式的讨论。

控制状态寄存器

RISC-V 在特权架构部分单独定义了一个控制状态寄存器的地址空间,并分配 了 12 位地址来做索引。在这 12 位地址当中,最高的两位 [11:10] 被用来指示寄存 器的读写权限。如果这两位是 2'b11 的话,则表示该寄存器是只读寄存器;否则, 该寄存器既可以被读取,又可以被写入。地址位 [9 :8] 表示有权访问该寄存器的最 低特权层级。对要讨论的机器模式 CSR,这两位都是 2'b11。在表14 中列出了本 书会涉及的所有 CSR 寄存器。

表14 本书所涉及的 CSR 寄存器列表

 

表14 看起来有点长,但这只是众多 CSR 寄存器中的一小部分。读者如果想 了解 RISC-V 完整的 CSR 寄存器列表,则可以查找 RISC-V 的官方标准。本章就对 表14 中的寄存器做仔细讨论。

1. mvendorid 寄存器

为了对不同厂商设计生产的 RISC-V 处理器加以区分,RISC-V 在其特权架构 标准部分制定了 mvendorid 寄存器,用来存储厂商标识代码。对 RV32 来说,这是 一个 32 位的只读寄存器,它的取值实际上是衍生于

JEDEC 厂商标识代码。 JEDEC 是联合电子设备工程委员会(Joint Electron Device Engineering Council) 的 英 文 缩 写, 目 前 的 名 称 为 JEDEC 固 态 技 术 协 会(Solid State Technology Association)。它是一个在1958年成立的行业协会组织,其总部位于美国弗吉尼亚州。 所有的电子产品生产厂商都可以向 JEDEC 付费申请得到一个 JEDEC 的厂商标识代 码(即 JEDEC 厂商标识代码)。

JEDEC 的厂商标识代码分为两部分:第一部分是 Bank 域(Bank Field);第 二部分是只有一字节的 Offset 域(Offset Field)。对于这个 8 位的 Offset 域,其最 高位是奇数校验码,其余 7 位对应 Bank 域里面的厂商标识。如果 Bank 域的值是 n, Offset 域的值是 m,那么其对应的完整的 JEDEC 厂商标识代码应该是将 0x7F 重复 n-1 遍,然后再在后面接上 m。

例如 JEDEC 给美国 PulseRain Technology 公司分配厂商标识的文件中有如下 文字:

the following JEDEC Manufacturer ID number has been assigned to your company:

94 decimal(bank 11)

0101 1110 binary

5E hex

由此,n=11,m=0x5E 其完整的 JEDEC 厂商标识代码就是:

0x7F-0x7F-0x7F-0x7F-0x7F-0x7F-0x7F-0x7F-0x7F-0x7F-0x5E

由于 JEDEC 提供的厂商标识远超出了 32 位寄存器可以表示的范围,RISC-V 在特权架构标准中定义了一个方法,用来在 JEDEC 厂商标识代码的基础上衍生出 一个 32 位的数值,然后固化于 mvendorid 寄存器中。根据 JEDEC 提供的 n 与 m 数值,RISC-V32 位厂商标识代码可以用如下公式产生:

Vendor ID=((n-1)<<7)+(m&0x7F)

根据上文 n=11,m = 0x5E ,可以得出 PulseRain Technology 的 RISC-V 厂商 标识代码为 0x55E。

2. marchid(体系架构标识代码)

根据 RISC-V 官方标准,marchid 在 RV32 下是一个 32 位的只读寄存器,用来 存放 HART 所对应的体系架构的标识代码。对于开源的架构来说,这个寄存器的 值由 RISC-V 基金会负责在全球分配,其最高位必须是 0。对于商业公司所研发的 架构来说,其值由具体的商业公司来分配,但是其最高位必须为 1,其余的位不能 全为零。这样,如果将该寄存器和 mvendorid 寄存器一起使用,则可以唯一地标识 HART 的体系架构。

如果处理器设计者选择不支持这个寄存器,则应该返回零值。

3. mimpid(实现标识代码)

根据RISC-V官方标准,mimpid寄存器在RV32下也是一个32位的只读寄存器, 其主要的目的是标明处理器的版本号。该寄存器的格式完全由处理器设计者自行决 定,如果处理器设计者选择不支持该寄存器,则应该返回零值。

4. mhartid(硬件线程标识)

在 RISC-V 的术语中,每个处理器核可以包含有多个硬件线程,称作 HART (Hardware Thread)。每个 HART 都有自己的程序计数器和寄存器空间,独立顺 序运行指令。mhartid 寄存器用来给这些 HART 编号索引。在多处理器系统中, HART 的编号无须连续,但是必须至少有一个 HART 必须被编号为零。

5. misa(指令集寄存器)

由于 RISC-V 指令集标准涵盖多种字长(32 位 /64 位 /128 位),并包含多种指令集扩展(如 16 位压缩指令集扩展 C,乘除法扩展 M 等)。misa 寄存器的目的 是为了向软件告知处理器具体支持的字长和扩展,以方便软件的可移植性。本寄存 器各位完整的定义可以在 RISC-V 官方标准中找到。对于只支持单个 HART 和机器 模式的 RV32 处理器来说,如表15 所示的这些位值得关注。

表15 指令集寄存器

 

6. mstatus(硬件线程状态寄存器)

mstatus 寄存器用来标识和控制 HART 的操作状态,其各位完整的定义可以在 RISC-V 官方标准中找到。对于只支持单个 HART 和机器模式的处理器来说,需要 注意表16 中的两个位。

表16 硬件线程状态寄存器

 

7. mscratch(草稿寄存器)

在 RV32 下,这是一个 32 位的读写寄存器。除了被用来作为 CSR 寄存器操作 的读写测试以外,它还可以被操作系统作为暂存空间。

8. 与中断和异常有关的 CSR 寄存器

RISC-V 中还定义了多个 CSR 寄存器用来处理中断与异常,其中与机器模式相 关的部分主要如下:

● mtvec(machine trap vector base-address register,机器模式异常向量基地址 寄存器)。

● mip(machine interrupt register, pending interrupt,机器模式中断等待 寄存器)。

● mie(machine interrupt register, interrupt enable,机器模式中断使能寄存器)。

● mcause(machine cause register,机器模式异常原因寄存器)。

● mepc(machine exception program counter,机器模式异常 PC 寄存器)。

● mtval(machine trap value register,机器模式异常值寄存器)。

9. 计数器

作为一种硬件性能监测的手段,RISC-V 在其特权架构部分定义了一系列 计数器,用来记录从某一时间点开始后处理器已运行的时钟周期数和已执行的 指令数。具体来说,其主要包括如下的 CSR 寄存器。

1)mcycle 与 mcycleh RISC-V 中为机器模式定义了一个 64 位的 cycle 寄存器,用来记录机器已 经运行的时钟周期数。这个寄存器的低 32 位和高 32 位分别存放在 mcycle 和 mcycleh 中。

对 RV32 来说,由于无法一次性地将 mcycle 和 mcycleh 同时读取出来,为 了保证 64 位数据的完整性,需要在寄存器的读取方式上做一些处理。一种解法 是要求软件总是先读取 mcycle,紧接着再读取 mcycleh。软件读取 mcycle 时, 硬件同时将当时的 mcycleh 值保存下来,并在下次读取时提供该值。而另外一 种解法则如代码 3-1 所示(参见 RISC-V 官方标准,将 cycle 寄存器的值读入到 x3:x2 中)。

代码3-1 64位cycle寄存器的读取

again: rdcycleh x3

rdcycle x2

rdcycleh x4

bne x3, x4, again

2)minstret 与 minstreth

RISC-V 还为机器模式定义了一个 64 位的 instret 寄存器,用来记录机器已经完 成的指令数(The Number of Instructions Retired)。该寄存器的低 32 位和高 32 位 被分别存放在 minstret 与 minstreth 中。

同 cycle 寄存器一样,当在 RV32 中读取该寄存器时,也会面临保持 64 位数据 完整性的问题。其解法也与上述读取 cycle 寄存器的解法相同。

3.6.3  定时器

RISC-V 在设计时也对 RTC(Real Time Clock,实时时钟)的实现做了考虑。 实际上,要在真正意义上实现一个实时时钟,需要以下几部分的硬件支持:

(1)需要一个时钟定时器(Timer),运行在固定的频率。

(2)需要能有办法获取时间基准,以用来计算实时时间(Wall Clock)。 简单地说,就是要能有办法获取当前精确的日期与时间。在桌面系统中,这个 时间基准可以通过网络从专门的时间服务器获取。在许多嵌入式系统里,这个 时间基准可以通过 GPS 获取。

(3)需要有办法能保持时钟定时器的不间断运行。这意味着 RTC 需要 有自己的电源域,这样即使处理器的其他部分进入深度休眠状态,RTC 可以 依然保持运行。

而且 RTC 的电源域一般还需要有替代电源,如电池等。 为此,RISC-V 在其特权架构部分为机器模式定义了两个 64 位的寄存器: mtime 与 mtimecmp。同时,为了方便 RTC 的独立运行,减小实现 RTC 的硬件开销,让多个 HART 能共享 RTC,RISC-V 中将这两个寄存器定义为内存映射寄存器(见 图4),以映射到内存空间中(而不是像 mcycle 这样定义为 CSR)。而这两个 64 位寄存器在内存空间中的地址,则由具体的实现决定,RISC-V 标准中并没有对 它们的地址做硬性规定。

1. mtime 寄存器

mtime 是时钟定时器。一般来说,它应该以比较精确的石英晶体振荡器为时钟 源,并以固定的频率做计数。然而,这个固定的频率具体是多少,RISC-V 中并没 有作出明确规定。许多系统将该频率设置为 32.768 kHz,因为 32.768 kHz 的晶振 非常容易获得,而且 32.768 kHz 频率较低,适合做休眠时钟。另外,32 768 又是 2 的整数次幂,很容易由 32.768 kHz 产生周期为 1s 的时钟。

提示:笔者在 RV32 的设计实践中发现,如果 mtime 的运行频率不能被处理 器的主时钟频率整除的话,则可能会给软件,特别是嵌入式操作系统的运行 带来额外的开销。因为操作系统在做任务调度时,需要对时间片有精确的计 算。如果处理器的主时钟频率不是 mtime 运行频率的整数倍(如 mtime 运行 于 32.768 kHz,而处理器主时钟频率为 100 MHz),则操作系统可能需要做 非常复杂的 64 位除法运算。出于性能的考虑,这时操作系统往往要求处理 器能支持硬件乘除法扩展(即支持 M Extension)。而对同样的嵌入式操作系 统,如果处理器的主时钟频率是 mtime 运行频率的整数倍(如 mtime 运行于 1 MHz,而处理器主时钟频率为 100 MHz),则处理器只要支持基础整数指 令集即可。

2. mtimecmp 寄存器

mtimecmp 的主要作用便是将其与 mtime 的值做比较。当 mtime 的值大于或等 于 mtimecmp 时,便可触发产生时钟中断。

由于 mtimecmp 是一个 64 位的寄存器,在 RV32 系统中至少需要两条写指令 才能完成对其的更新。而部分更新的 mtimecmp 寄存器值可能会误触发产生时钟中 断。对此,通常的处理方法有两种:

(1)在更新 mtimecmp 之前禁止时钟中断(Disable Timer Interrupt)。在 mtimecmp 更新完毕后再重置并允许时钟中断。

(2)采用 RISC-V 官方标准中建议的汇编代码序列(代码 3-2)。

假设需要写入 mtimecmp 的低 32 位存放于寄存器 a0 中,而高 32 位存放于寄 存器 a1 中,如代码 3-2 所示。

代码3-2 mtimecmp的写入

li t0, -1 # 将0xFFFFFFFF写入寄存器t0

sw t0, mtimecmp # 将mtimecmp的低32位置为0xFFFFFFFF

sw a1, mtimecmp + 4 # 设置mtimecmp的高32位

sw a0, mtimecmp # 设置mtimecmp的低32位

注意:上面的汇编代码需要被完整并严格地顺序执行。编译器的优化,中断 服务程序(Interrupt Service Routine,ISR)的插入,或者高端处理器的乱序执 行都可能对上面代码的正确性产生影响。

3.6.4  中断与异常

1. 中断与异常的比较 我们知道,软件并不总是按照其原先计划好的步骤运行,在多数情况下,软件 在执行过程中总会发生一些意外,使得处理器不得不暂停现有的软件执行步骤,转 而去做其他的额外处理。这种意外事件主要分为两种情况。

(1)这种意外事件是由软件执行本身引发的。常见的情形包括:

● 软件在执行过程中访问了一个不存在的 CSR 寄存器。

● 软件在访问内存时没有按照字长对齐。

● 遭遇断点或者操作系统调用。

这种由软件本身引起的意外事件通常被称作异常(许多处理器会将被零 除也作为一种异常,不过 RISC-V 的除法指令是不会产生异常的)。

(2)这种意外事件是由独立于软件运行的外部事件引发的。 这种由外部事件导致的意外通常被称作中断。在单个 HART 的机器模式下, 中断主要来源于两个地方:

① 定时器中断。

② 来自处理器核外部的中断,主要由外围设备产生。

在实际的硬件处理中,中断和异常的处理非常相近。

2. RISC-V 的中断控制器结构

在中小规模的嵌入式系统中,一般都会对中断信号的电气特性做直接处理。 具体地说,中断信号的电气特性一般有两种:电平触发(Level Trigger)和边沿触 发(Edge Trigger),而且一般以电平触发居多。对于多个中断源的情况,可以简 单地将它们线或(Wired-OR)在一起,作为共享中断,如图35 所示。对于中 断延迟要求比较高的情形,也可以用专门的中断向量控制器(Vectored Interrupt Controller,VIC)来处理,如图36 所示。

 

图35 共享中断

 

图36 中断向量控制器 在共享中断的情况下,处理器核在收到中断信号后,需要在中断处理程序中逐 个查询外围设备,以确定中断源,因此其中断相应的延迟较大,其优点是硬件设计 比较简单。对于像中断优先级、中断嵌套等问题,则大多都交由软件来处理。

为了减小中断延迟,许多中高端的嵌入式处理器都会在处理器核之外放置一 个中断向量控制器,如图36 所示。中断向量控制器在向处理器核提供中断信号 的同时,还会提供与中断源相对应的 ISR(Interrupt Service Routine,中断服务程 序)的入口地址。这些 ISR 的地址便组成了中断向量表(Interrupt Vector Table, IVT)。同时,中断向量控制器一般还会支持中断优先级、中断屏蔽等设置。 与共享中断相比,中断向量控制器的硬件开销较大,但是软件处理则相对简单 直接。

以上说的这两种方式都称为带外中断,其中断信号和数据是分开独立的。然而, 随着系统规模的日益增大和高速串行数据传输的不断发展,点对点的拓扑结构变得 流行起来。PCI-Express 总线便是其典型代表之一,它采用的中断机制被称为消息 告知中断(Message Signaled Interrupt,MSI),这是一种带内中断的中断机制。在 这种中断方式下,设备通过向某个指定的地址写入特殊的消息来发送中断信号。而 外围设备也通过交换矩阵(Switch)和处理器核相连。MSI 中断机制的优点是其可 扩展性比较好,缺点是其软硬件都比较复杂。

这种 MSI 中断机制和交换矩阵的思路显然也影响了 RISC-V 的设计者。在 RISC-V 标准中,对 RISC-V 的外部中断控制定义为 PLIC(Platform-Level Interrupt Controller,平台级中断控制器),其结构如图37 所示。

 

图37 PLIC的结构

从图37 可以看出,PLIC 的设计考虑到了多个 HART 的情况。图37 中门户的作用主要是将中断源来的中断电气信号转换为 MSI,然后交由交换矩 阵来处理。交换矩阵可以被软件配置,以对中断优先级和中断屏蔽等做出设定。 门户的另外一个作用是当来自某个中断源的中断正在被处理时,阻止接收同一 中断源的后续中断。

对某个 HART 来说,如果中断发生,交换矩阵会通知 HART,而这种通知的 方式可以有多种实现方式。对于复杂的系统,这种通知本身就可以是 MSI;对于相 对简单的系统,这种通知可以是简单的硬连线,直接连接到 HART 内部中断寄存 器的等待中断位上。

HART 在收到来自交换矩阵的中断通知后,需要读取对应的读取 / 完成寄存器 来确定中断源。读取 / 完成寄存器是一个内存映射寄存器,当其被读取时,会返回 中断源的 ID(Indentifier)。同时,读取寄存器的动作也会被 PLIC 认定为对中断的读取,从而修正 PLIC 中中断等待的状态。

当 HART 结束对中断的处理后,需要将刚才处理完成的中断源 ID 再写入读取 / 完成寄存器。PLIC 在收到这个写入动作后,会修改门户的状态,以允许接收对应 中断源的后续中断。

注意:这里要提醒读者注意的是,PLIC 只包括对外部中断的处理。为方便 RTC 的实现,RISC-V 标准中还专门定义了时钟定时器。而时钟定时器的中断 属于局部中断(Local Interrupt),其在 HART 中有专门的寄存器位对应。其 他的局部中断还包括软件中断和处理器设计者的自定义局部中断。这些 Local Interrupt 会在后文讨论中断相关的寄存器时做详细讨论。在 SiFive 公司的 Freedom E31 处理器中,将这些同局部中断相关的寄存器(时钟定时器寄存 器、软件中断寄存器等)统称为核局部中断寄存器(Core Local Interruptor, CLINT)。所以对 HART 来说,其完整的中断拓扑结构如图38 所示。

 

图38 外部中断和局部中断

这里笔者想借此机会对 PLIC/CLINT 的设计发表一些个人见解,供读者 参考:

(1)RISC-V 的设计者对大规模的多处理器系统做了很多考量。从图37 可以看出,RISC-V 的外部中断控制器有很好的可扩展性。然而,对于单个 HART 的仅支持机器模式的处理器核来说,这种结构显得比较复杂。同时和 图36 相比,这种结构并不是纯粹的中断向量控制器结构。软件依然需要通 过读取内存映射寄存器来确定中断源,而不是由硬件支持的中断向量表来直 接调用 ISR。所以 PLIC 依然会有比较大的中断延迟。

(2)在图38 的架构下,如果需要进一步减少中断延迟,则可以通过 CLINT 中的自定义局部中断来实现。然而,在讨论相关的 CSR 寄存器时可以 发现,RISC-V 对自定义局部中断的向量化处理并没有很完整的定义,向量编 码可扩展的空间也非常有限,从而给 VIC 的实现造成了障碍。这就导致在目 前的架构下,RISC-V 处理器的设计者不得不做一些额外的非标准设计来适应 对中断延迟有严格要求的场合。

(3)在本书撰写之际,RISC-V 基金会发布的最新官方标准包括用户 指令集 20190608 版和特权架构 20190608 版。上述对外部中断和局部中断的 处理架构的讨论,都是基于这两个官方标准和之前的较早版本。然而,随着 RISC-V 在嵌入式系统中的应用和普及,RISC-V 的设计者可能也意识到了目 前中断处理架构的不足。所以 SiFive 公司又提议了一个新的中断处理的架构 标准,叫作 CLIC(Core-Local Interrupt Controller,核局部中断控制器),并 将其用在了 SiFive 公司的 E20 处理器上。CLIC 的结构如图39 所示。

CLIC 可以被看作是 PLIC 和 CLINT 的合并与简化。图39 的外部中断主要 是用来在大规模系统与更高层级的 PLIC 相连的。实际上大部分的外设都可以直接 被连接到 CLIC 上。同时 CLIC 架构标准中还定义了一些新的 CSR 寄存器,例如 mtvt(machine trap vector table,机器模式异常向量表)等,用来加强对中断向量的支持。

 

图39 CLIC结构图

提示:在本书撰写之际,CLIC 架构标准还只是处在提议和草稿阶段。由于其 目前还不是 RISC-V 的官方标准,本书在后续章节将不会对其再做进一步的 深入讨论。有兴趣的读者可以在 GitHub 上找到更多的相关内容。

3. RISC-V 中断和异常的触发

在 RISC-V 中,对中断和异常的处理方式非常相近。二者一般都可以被称作异 常情况。对于单个 HART 的机器模式,当异常情况发生时,硬件一般要经历以下 的处理步骤:

(1)确定中断是否被屏蔽。 对于单个 HART 的机器模式,下面两个 CSR 寄存器会影响中断的屏蔽。

 ① mstatus 寄存器中的 mie 位(见表16),这是全局的中断使能位。但 是该位不会屏蔽异常处理。

 ② mie(machine interrupt register,interrupt enable,机器模式中断使能寄 存器)寄存器中的相关位。在 RV32 下,mie 寄存器是一个 32 位的可读写寄 存器,其与机器模式相关的位如表17 所示。

表17 mie 寄存器的位定义

 

这里对 RISC-V 特权架构中定义的软件中断做一下讨论。在 RISC-V 中,机 器模式软件中断的主要目的是提供一种手段,用来在多 HART 系统中中断其他的 HART。为此,处理器的设计者需要在 CLINT 部分提供一个内存映射寄存器(或 寄存器位),称为 msip(machine software interrupt pending,机器模式软件中断等 待寄存器)。对 msip 的写操作会触发软件中断。

(2)确定异常情况发生的原因。

当中断或异常发生时,处理器需要正确填写 CSR 寄存器 mcause 中的相 关内容。对于 RV32 来说,机器模式异常原因寄存器 mcause 是一个 32 位的 可读写寄存器(这意味着软件也可以修改其内容)。mcause 的 MSB,即位 31 被用来标识这个异常情况是中断还是异常。如果是中断,则该位应该被置 为 1;如果是异常,则该位应被置为 0。mcause 剩下的位被用来作为异常编码。 虽然在标准中称其为异常编码,但其也包括中断的情况(在中断情况下,异 常编码实际上是中断源编号)。在目前的标准中,只用到了其中的低 4 位。 对于单个 HART 的机器模式,如果异常情况是中断,则相关中断源编号如 表18 所示。如果异常情况是异常,则对应的异常种类编码如表19 所示(细 心的读者也许会发现,表17 与表18 的位定义是一样的)。

表18 mcause 的中断源编号

 

表19 mcause 的异常种类编号

 

(3)确定异常情况发生的地址。

对于机器模式,RISC-V 在其特权架构标准中定义了 mepc(machine exception program counter,机器模式异常程序计数器)寄存器,用来存放异常 情况发生时的程序计数器的值。对于异常来说,当前触发异常的指令的 PC 值 是一个重要参数,所以 mepc = PC。而对中断来说,mepc 值则会被中断处理 程序末尾的 MRET(M-Return)指令用来作为中断返回地址。所以,mepc 需 要存放下一条指令的地址。

(4)确定与异常情况相关的参数。

为了帮助异常情况的处理,RISC-V 还在其特权架构标准的机器模式中定 义了 mtval 寄存器,以提供与异常情况相关的参数。在 RV32 下,mtval 是一 个 32 位的可读写寄存器。当内存访问出现异常时,对应的内存读写地址应该 被保存在这个寄存器里。

(5)改变 PC 值,调用中断 / 异常处理程序,并设置相应的中断比特状态位。

对于机器模式,RISC-V 在其特权架构标准中定义了 mtvec 寄存器,用来 确定异常情况处理程序的地址,在 RV32 下,这是一个 32 位的可读写寄存器。 其中的低两位用来确定中断模式,其余高 30 位被用来作为基地址(BASE)。 中断模式的定义如表20 所示。

表20 mtvec 的中断模式定义

 

由于 mcause 中的异常编码目前只有 4 位,其大部分已被占用,而 RISC-V 官方标准中也没有定义专门的 CSR 来支持中断向量表(Interrupt Vetor Table, IVT),所以表20 中的向量模式并不能很好地对来自外设的中断进行类似 VIC 这样的向量化支持。目前还在草案和提议阶段的 CLIC 标准将改变这一状况,在目 前的 CLIC 标准提议草案中,已经对表20 中的模式 2 与模式 3 做了扩充。

为对应中断的情况,硬件还需要将 mip 寄存器中的相应位设置为等待。mip 寄 存器中的位定义如表21 所示。很显然,表21 中的位定义与表17 中的位定义 是一一对应的。

表21 mip 寄存器的位定义

 

4. RISC-V 中断和异常的返回

在机器模式下,当异常情况处理程序完成所有操作后,需要调用 MRET (M-Return,机器返回指令)指令返回。MRET 指令的定义如图40 所示。当处 理器遇到 MRET 指令时,应将 PC 值置为 mepc 寄存器中的值,这样指令从之前被 异常情况打断的地方继续执行。

 

图40 MRET指令

5. WFI(中断等待指令,Wait for Interrupt)

为了给操作系统多提供一个调度的方法,RISC-V 在其特权架构标准中还定义 了中断等待指令,如图41 所示,当处理器遇到该指令时,则进入停顿状态,直 到中断的发生。

 

图41 WFI指令

当中断发生时,处理器会设置 mepc = PC + 4(即 WFI 之后的那条指令的地址)。 在机器模式下,当中断处理结束,MRET 返回时,则会将 PC 设置为 mepc 的值, 从而使得处理器会执行 WFI 之后的那条指令。

6. 环境调用与断点

为了给操作系统和软件调试提供更多调度和中断的方式,RISC-V 标准中还定 义了环境调用指令 ECALL(Environment Call)和断点指令 EBREAK(Environment Breakpoint),它们的定义如图23 所示。当处理器遇到 ECALL 或 EBREAK 指令 时,都会产生异常。其中 ECALL 在机器模式下的异常编码是 11,而 EBREAK 的 异常编码是 3(参见表19)。

RISC-V 的特权架构标准中特别强调,当遇到 ECALL 和 EBREAK 指令时,应 该将 mepc 寄存器(此处仅讨论机器模式)的值设置为当前指令的地址,而不是下 一条指令的地址。细心的读者也许会问:“如果是这样,当异常处理结束时,调用 MRET 指令时,岂不是又回到了原来的 ECALL/EBREAK 指令,陷入重复执行的死 循环?”对此,笔者就以 GDB 中的软件断点的操作为例来具体解释。

相信许多读者都对 GDB(GNU Debugger)不陌生。当我们需要在 GDB 中设 置软件断点时,一般的做法是在 GDB 命令行中键入“break 断点地址”。当处理 器执行到该断点地址时,软件中断被触发后,我们可以检查寄存器的值或读取内存中的内容,然后用 continue(继续执行)命令来继续程序的执行。在这些调试操作 的背后,GDB 到底做了些什么?

如图42 所示,当在 GDB 命令行中键入“break 断点地址”后,调试器会将 内存对应内存地址中的指令换作 EBREAK 指令。

 

图42 软件中断

随后,当处理器运行到对应的断点地址时,会触发 EBREAK 断点异常,进入 调试器事先准备好的断点异常处理程序中。在这里,用户可以查看寄存器和内存中 的内容,以帮助调试。

当用户完成寄存器和内存内容查看后,可以在 GDB 命令行中键入“continue” 以继续运行程序。但是在继续运行之前,GDB 会将内存中的 EBREAK 再替换回原 先的指令,以避免调试器可能带来的副作用。这就是为什么 mepc 应该被设置为断 点地址,而不是指向断点地址之后的那条指令。