RISC-V 指令集介绍(一)

发布时间 2023-12-31 05:30:05作者: 吴建明wujianming

RISC-V 指令集介绍(一)

RISC-V的历史

RISC- V 最早源自 2010 年夏天美国加州大学伯克利分校 Krste Asanović 教授 主持的一个关于开源计算机系统的研究项目。该项目得到了美国国防高级研究计 划局(Defense Advanced Research Projects Agency,DARPA)的资助,后来成为 RISC-V 的前身 [ 这里顺便提一句,国际互联网 Internet 的前身 ARPANET(Advanced Research Projects Agency Network,高级研究计划局网络)也是由 DARPA 资助的 ]。 RISC-V 中的字母 V 表示第五代的意思,所以发音时应该发作“RISC-Five”, 表示它师承于伯克利分校之前开发的一系列 RISC 指令集。根据 RISC-V 的族 谱,RISC-V 之前四代指令集都产生于 20 世纪 80 年代。当然,RISC-V 在其形成 过程中,也从其他各种流行的指令集(MIPS、SPARC、ARM 等)中吸取了经验教训。

在 RISC-V 问世之际,移动计算主要由 ARM 处理器把持,而 Intel 公司的 x86 处理器则占据了大部分的桌面计算市场,RISC-V 的出现给这两大巨头带来了挑 战。与这两大巨头的指令集不同的是,RISC-V 是一个自由和开放的指令集,它的 标准化工作由 RISC-V 基金会主持,该组织目前有超过 100 个会员,并在不断扩大 之中。对任何想要用 RISC-V 设计实现处理器的公司与个人,他们都不会受到来自 RISC-V 基金会的限制,也无须向 RISC-V 基金会支付授权费用。基金会各会员公 司也承诺不会就 RISC-V 的基本议题向其他成员发起诉讼。

由于 RISC-V 没有上面提到的这些限制,因此很快得到了开源社区的大力拥护。 面对 RISC-V 的攻城略地,ARM 也开始予以反击。2018 年夏,ARM 上线了一个 名为 riscv-basics.com 的网站,对 RISC-V 发起舆论战。但是这种做法很快受到了 来自各方的诟病,甚至连 ARM 自己的员工都对此做法表示不满。迫于各方压力,ARM 很快就关闭了该网站。 另外,为了促进 RISC-V 的产业化,RISC-V 的主要开发成员还于 2015 年成立了一家叫 SiFive 的初创公司,向市场提供各类 RISC-V 的处理器内核,以及相关的 软件工具和开发套件。

8051的CISC指令集与RISC-V的比较

8051 指令集简介 提到 RISC(Reduced Instruction Set Computer,精简指令集计算机),就必然 也会提到 CISC(Complex Instruction Set Computer,复杂指令集计算机)。在许多 嵌入式系统中得到广泛使用的 8051 单片机,便是 CISC 指令集的典型代表。笔者 在开始设计 RISC-V 处理器之前,也曾做过一款 1T(单时钟周期)8051 处理器的 设计,所以对这两类不同类型的指令集都有深入了解。这里笔者愿意将这些体会做 个总结,并由此来反映 RISC-V 在技术设计上的优势。 可能有许多读者对 8051 单片机早已熟悉,该单片机是由美国 Intel 公司于 20 世纪 80 年代推出的一款 8 位单片机。由于该单片机方便易用,许多公司都推出了 第三方的兼容设计。直到今天,8051 单片机依然被许多嵌入式系统所选用。 然而在 20 世纪 80 年代该单片机刚刚问世时,半导体的制造工艺还只能达到 μm 级,处理器所能达到的时钟频率偏低。而且当时硬件设计语言还处于起步阶段, 也缺乏自动设计的工具,软件多以手工汇编编程为主。这就导致流水线设计的优势 无法得到发挥,并且每条指令需要多个时钟周期才能完成。由于上述原因,当时的 指令集设计往往具有以下特点: (1)尽量在每条指令中实现更多的功能。例如 8051 的 CJNE 指令,就 需要在一条指令中依次实现:

 ① 与累加器做减法。

② 修改进位标示。

③ 将结果做相等比较。

④ 根据比较结果决定是否跳转。

(2)指令集庞大,以实现更多的复杂功能。例如 8051 虽然是 8 位单片机, 其指令集却包含高达 255 种不同的指令和格式。

(3)由于以上两点,导致变长指令的出现,以提高内存利用率。8051 的 指令就有单字节、双字节与三字节三种不同的种类,而且除了对指令解码以外, 没有其他的手段帮助判定指令长度。

(4)寻址方式众多。例如在 8051 指令集中,对数值的操作包括如下方式:

① 立即数寻址。将常数包含在指令中。

② 直接寻址。将内存地址包含在指令中。

③ 间接寻址。将内存地址放入寄存器中,然后将寄存器地址包含在指令中。

④ 寄存器寻址。将操作数放入寄存器中,然后将寄存器地址包含在指令中。

由于众多的寻址方式,同一个功能在指令集中就可能对应多种指令格式。例如 在 8051 指令集中,光是一个加法指令就有 12 种不同格式。类似地,跳转指令也存 在多种的寻址方式和指令格式。 8051 指令集的特点,很大程度上也代表了当时众多 CISC 指令集的共同特点。 这种特点是与当时半导体制造水平和软件发展水平相匹配的。随着半导体加工工艺 的不断进步和软件开发水平的提高,流水线和高时钟频率的设计开始在处理器设计 中流行,汇编语言也开始被 C/C++ 这类高级编程语言所替代。尽管 8051 是一个非 常长寿的指令集,自问世近 40 年,依然被业界广泛采用,但是今天市面上出现的 8051 处理器,却早已和它们的祖先大不一样了。 8051 的第一代产品,其时钟频率只有 12 MHz,每个指令需要 12 个时钟周期 才能完成。而今天我们所使用的 8051 处理器,都是增强型处理器,除了有更丰富 的外围设备外,其增强之处主要表现在:

(1)时钟频率大幅提高。

(2)指令的吞吐率大幅提高,对大部分的指令,都可以做到在单个时钟 周期内完成即我们通常说的 1T 8051。

(3)在软件上,支持 C 语言的开发环境。 换句话说,今天的增强型 8051 处理器,虽然其指令集还是 40 年前的那个 指令集,但是其内部实现却早已经在原型基础上进行了 RISC 改造(实际上, 类似的 RISC 改造也同样发生在 Intel 的 x86 处理器上)。

说明:由于指令集设计的缺陷,这种对 CISC 指令集的 RISC 实现不可避免 地要在硬件上付出一定的代价。下面就以笔者主持设计的 PulseRain FP51-1T MCU 为例,对此具体加以说明。

8051 指令集对处理器设计的负面影响 PulseRain FP51-1T MCU 是美国 PulseRain Technology 公司推出的一款针对 FPGA 的 8 位微控制器,其内部的处理器内核是一个增强型 8051,可以对大部分 的 8051 指令实现 1T 吞吐率,并且在 FPGA 上可以实现很高的时钟频率(在 Intel MAX10 C8 级器件上主频可以达到 100 MHz)。 8051 的流水线实现如图 3-1 所示,该处理器的内部有一个 5 级流水线,包含 指令读取、指令解码(一)、数据内存读取、指令解码(二)和指令执行。尽管该 处理器在 FPGA 上有优秀的性能表现,然而由于 8051 指令集本身的缺陷,使得设 计者不得不以额外的逻辑资源为代价来换取更高的性能。最后的结果就是与同样时 钟频率的 RISC-V 处理器相比,8 位 8051 内核居然比 32 位 RISC-V 内核消耗更多 的逻辑资源,占用更大的芯片面积,而更大的芯片面积意味着更加耗电。对 FPGA 器件来说,这些还不是一个太大的问题,但是对专用芯片(ASIC),特别是移动 设备的专用芯片来说,更多的耗电往往意味着更短的电池寿命(Battery Life),这可能也是 Intel x86 处理器始终无法在移动设备市场上打开局面的原因之一。

 

 

图1. 8051 的流水线实现

具体来说,8051 指令集的特点会对处理器的 RISC 实现产生如下负面影响:

1)尽量在每条指令中实现更多的功能 为了在实现这些复杂功能的同时保持高吞吐率,流水线的设计者不得不花更多 的时间规划流水线的各级。即便如此,有些指令依然无法实现单周期吞吐,例如上 文提到的 CJNE 指令,就需要两个时钟周期。 另外,现代的 8051 处理器开发,早已经采用 C 语言代替了早期的汇编语言。 而高级语言的编译器往往很难把这类复杂、多功能机器指令的威力全部发挥出来, 有违当初指令集的设计初衷。 当然,指令集复杂这个特点也并非一无是处。由于 CISC 指令集的指令复杂,也使得其代码密度(Code Density)一般要优于同等字宽的 RISC 处理器。

2)庞大的指令集 庞大的指令集必然导致指令的解码阶段变得更为复杂,需要耗费更多的逻辑资 源。读者可能已经注意到,在图 3-1 所示的 5 级流水线中,指令集被分为两部分, 对它们各自的解码分别占用了流水线的一级。这样设计的原因之一就是为了在庞 大指令集下实现高吞吐率、高时钟频率,而不得不做出的妥协。同样时钟频率的 RISC-V 处理器,由于指令集比较精简,就无需做这样的妥协,从而大大节省了逻 辑资源,简化了流水线设计。

3)由于以上两点,导致变长指令的出现,以提高内存利用率 8051 的指令有单字节、双字节和三字节三种不同的种类,除解码(Decode)外, 没有其他的手段帮助判定指令长度。这种变长的指令结构,导致指令之间的边界很 难判定,甚至有可能导致内存的非对齐读取(Unaligned Memory Access),从而对 流水线的取指器(Instruction Fetch)设计带来挑战。 幸运的是,8051 的内存架构是哈佛架构,其代码与数据在不同的地址空间中 分开存放。这就使得代码存储部分可以单独做一些优化设计。在图 3-1 中左边部分 的片上代码内存,实际上被分成 4 个 8 位宽的存储体,这样对代码内存的一次读取 就可以得到 4 字节,从而保证至少可以有一条完整的指令。然而即便如此,由于 8051 指令集没有其他辅助手段来帮助判定指令长度,为了确定指令的边界,8051 的取指器不得不为此花费比 RISC-V 更多的逻辑资源。

4)众多的寻址方式 由于 8051 存在众多的寻址方式,使得指令集中的许多指令都可以访问内存。 这导致流水线的数据冲突(Data Hazard)很难判断,有时不得不通过硬件自动插入 空操作(Null Operation,NOP)来保持数据的正确和完整。这样既消耗了逻 辑资源,又降低了流水线的效率,从而对功耗和性能造成双重打击。 说明:虽然 8051 指令集有其历史局限性,但是 8051 处理器却由于其短小精悍、 性价比高,一直为笔者所钟爱。其虽历四十载,依然廉颇未老,不乏拥趸。

RISC-V 指令集对处理器设计的正面影响

8051 指令集的缺陷,在 RISC-V 中都得到了避免,具体说明如下。

1. 引入指令长度编码

8051 指令集除了对指令解码以外,没有其他的辅助手段帮助判定指令长度, 而 RISC-V 则可以通过指令的低位部分来判断指令的长度,被称为指令长度编码 (Instruction Length Encoding)。图 3-2 展示了 16 ~ 64 位指令的编码方式。64 位 以上的编码方式,可以在 RISC-V 官方标准中找到。

 

图2.RISC-V指令长度编码

指令长度编码的引入,大大简化了流水线取指器的设计,在取指时,硬件只 需要集中优化边界对齐的内存读取就可以了。而对非对齐的访问,则可以通过 产生异常,让软件处理器来处理。这样既节省了逻辑资源,又不影响处理器的性能。

2. 指令集规模较小,指令格式规整

尽管不是 8 位指令集,RISC-V 的指令集规模却比 8051 这样的 8 位指令集要小 许多。RISC-V 的 32 位基础整数指令集只有 47 条指令,即使算上 8 条乘除法扩展 指令,其指令总数也不到 8051 指令集规模的 1/4。指令集的小巧使得指令的解码器 变得简单,更无须像图1 中那样将指令集分成两部分来分别解码。 同时,RISC-V 的指令格式也非常规整,除了指令长度编码总是处在指令低位 以外,在不同指令格式之间,操作码、源寄存器和目标寄存器总是位于相同的位置 上。例如在 RISC-V 32 位基础整数指令集中(RV32I),操作码总是占用低 7 位, 而源寄存器 1 和 2(rs1,rs2)则分别占据 15 ~ 19 位与 20 ~ 24 位。目标寄存器(rd) 则占用 7 ~ 11 位(位索引以 0 为参考起点)。这种规整的指令格式进一步简化了 指令解码器和指令执行器的设计。

3. 每条指令实现单个功能 与 CISC 指令集的设计思想截然相反,RISC-V 指令集中的每条指令只集中于 优化实现单个的功能,这种将复杂任务通过多个单功能的指令来实现的做法也一直 是 RISC 指令集的指导思想。因为这样可以简化流水线的设计,从而能实现更高的 时钟主频,最终可以让 RISC 获得比 CISC 更佳的总体性能。

4. 内存访问只能通过 LOAD/STORE 与 8051 指令集中,具有众多的寻址方式不同,在 RISC-V 指令集中,对内存 的读写只能通过 LOAD 指令和 STORE 指令实现。而其他的指令,都只能以寄存器 为操作对象。没有了复杂的内存寻址方式,使得流水线对数据冲突(Data Hazard) 可以及早做出正确的判断,并通过流水线各级之间的转送加以处理,而不需要插入 空操作(NOP),极大提高了代码的执行效率。当然,这一特点也是 RISC 指令集 的共有特点之一。 至此我们可以看到,CISC 指令集的那些历史局限性,在 RISC-V 指令集中都得 到了突破。下面的章节会将 RISC-V 与其他的主流 RISC 指令集做对比,并展示其 设计上的考量与取舍。

RISC-V与其他RISC指令集的比较

根据 Andrew Waterman 的博士论文(Andrew Waterman 是 RISC-V 创始人之一, Krste Asanović 教授的学生),RISC-V 在当初的设计目标中和嵌入式处理器相关 的部分如下:

(1)指令集规模小,要求模块化并可扩展。

(2)指令集设计独立于具体的处理器实现。

(3)支持 16 位与 32 位混合编程,以提高代码密度。

(4)对 C/C++ 等编程语言提供硬件支持。

(5)将用户指令集(User-Level ISA)和特权架构(Privileged Architecture)

做正交分割(Orthogonalize),即不同特权架构的处理器可以在应用二进制 接口(Application Binary Interface,ABI)层面做到代码互相兼容。 基于以上的设计目标,RISC-V 对其他主流指令集的利弊都做了一番深入的研 究,并做出了以下改进:

(1)将指令集分为基础指令集与扩展指令集。在处理器实现时,基础指 令集是强制要求的,但扩展部分可选。 这样的安排在设计之初就为未来的历史演进留下了余地,避免了其他的 指令级随着历史演进而愈来愈臃肿的问题(例如 ARM 指令集,在 ARMv7 中 仅整数指令集部分就包含高达 600 条指令)。

(2)去除了对跳转指令延迟槽的支持。 延迟槽(Delay Slot)在许多通用的 RISC 指令集(如 MIPS 和 SPARC) 中都包含,甚至在专用数字信号处理器(例如 TI 的 C5x DSP)上也有支持。

延迟槽的目的是提高流水线的利用率,当跳转发生时,硬件不得不清空流水线, 重新设置指令取指器。而这时,如果那些紧随在跳转指令之后进入流水线的 指令(即延迟槽中的指令)可以继续被完全执行,而不被丢弃,则跳转指令 的开销就可以被降低。 延迟槽实际上是把一部分工作量转移给了软件,而且严重限制了处理器 的实现方式,所以 RISC-V 对此做了舍弃。不过,RISC-V 的设计者将比较和 跳转做了紧密的结合,对跳转指令的效率问题给出了另外一种解决方案。

(3)取消对寄存器窗口的支持。 在函数调用时,编译器往往会插入开场白(Prologue)和收场白(Epilogue) 代码来传递参数,并保存寄存器到栈上。当函数嵌套层次比较深时,这种开 场白和收场白代码的开销就显得很可观。为了降低函数调用中的这部分开销, 在加州大学伯克利分校设计的第一代 RISC 处理器和后来 SUN 公司的 SPARC 处理器当中,都引入了寄存器窗口的设计,也就是在处理器中包含了多套通 用寄存器。当函数调用发生时,主调函数(Caller)和被调函数(Callee)共 享现有的这套通用寄存器,同时硬件还会给被调函数分配一套新的通用寄存 器。这样在函数嵌套调用时,每次调用都无须再保存寄存器到栈上,从而大 大降低了开场白和收场白的代码开销。 说明:这种设计的结果就是硬件开销变得很大,而且实际使用起来的效果并 不理想,特别是当通用寄存器被耗尽时,其处理会变得非常麻烦和缓慢。因 此 RISC-V 对此弃之不用,而代以类似 IBM S/390 中的毫码程序(Millicode Routine)的办法。

(4)支持 16 位指令扩展,并支持 16 位与 32 位混合编程。 与 ARM 等其他指令集不同的是,RISC-V 的 16 位指令只是一个扩展, 并不是一个单独的指令集。而且每条 16 位指令都可以翻译成一条对应的 32 位指令,从而简化了指令解码器的设计。

RISC-V基础指令集(RV32I与RV32E)

RISC-V 的官方标准主要分成两部分:用户指令集(User-Level Instruction Set Architecture)与特权架构(Privileged Architecture)。

RISC-V 用户指令分类如图 3-3 所示,RISC-V 的用户指令集分为基础整数指令 集(Base Integer Instruction Set)和扩展指令集(Extension)。根据处理器字长的 不同,基础整数指令集又有 32 位、64 位和 128 位之分。而扩展指令集则有 16 位 压缩指令(C,Compressed Instructions)、硬件乘除法(M,Integer Multiplication and Division)、取指隔离(Zifencei,Instruction Fetch Fence)等多种不同的扩展。 考虑本书的主题主要是针对嵌入式系统开发,所以对 64 位和 128 位的指令将不予 讨论。在本章节会主要讨论 RISC-V 32 位整数指令集(RV32I)和 32 位嵌入式指令。

 

图3. RISC-V用户指令分类

RV32I 与 RV32E 基础指令集简介

在 RISC-V 标准刚刚推出时,32 位的基础指令集只有 RV32I,即 32 位整数指 令集。后来考虑嵌入式系统资源稀缺的情况,又制定了 RV32E 基础指令集,这里 的字母 E 即代表嵌入式(Embedded)。RV32I 和 RV32E 的主要区别是在通用寄存 器的数量上,在 RV32I 中,总共有 32 个 32 位宽的通用寄存器,而 RV32E 只支持 16 个 32 位宽的通用寄存器。另外 RV32E 仅支持 M、A、C 三种指令扩展。

上述 RV32I 与 RV32E 的区别对 ASIC 的设计实现是有着实际意义的,在 ASIC 实现中,寄存器通常是通过触发器来实现的。对于面积优化的 RV32I 设计,移除 16 个通用寄存器大约可以节省 25% 的芯片面积;而对于 FPGA 实现来说,移除 这 16 个寄存器并不能带来资源上面的节省。因为在 FPGA 当中,通用寄存器可以 使用片上内存(Block Memory,BRAM)来实现。以 Intel MAX10 FPGA 为例,其 BRAM 是 M9K 类型,即每片 9Kb。而将 32 个 32 位通用寄存器用内存实现,只需 要 1 024 位,会占用一片完整的 M9K BRAM。即使减少通用寄存器数量,其占用 的 M9K 数量却依然还是一片,不会减少。基于这一点,将重点讨论 RV32I。

RISC-V 地址空间 RISC-V 的地址空间如图 3-4 所示。RISC-V 总共有 3 个独立的地址空间。

1. 内存地址空间

内存地址空间可以用来分配给代码、数据,或者作为寄存器的内存映射(Memory Mapped Registers)。在物理实现时,代码和数据可以共用存储(von Neumann, 冯·诺依曼架构),也可以分别存储(Harvard,哈佛架构)。和其他的处理器一样, RISC-V 的处理器也是通过程序计数器(Program Counter,PC)来指示当前正在执 行的指令地址的。 在寄存器的内存映射部分,大部分的外围设备寄存器都会被映射到这个空间, 其中也包括机器模式的定时器(Mtime)和定时器触发值(Mtimecmp)。

2. 通用寄存器

RV32I 指令集包含 32 个通用寄存器,而 RV32E 只有 16 个这样的寄存器。

 

控制与状态寄存器

在 RISC-V 的特权架构部分还对控制与状态寄存器(Control Status Register, CSR)做了定义,并单独分配了 12 位的地址空间。在用户指令集中,则专门定义 了 Zicsr 指令集扩展来对 CSR 进行操作。

RV32I 通用寄存器与函数调用约定

RV32I 基础指令集总共定义了 32 个 32 位的通用寄存器。它们分别被标记为 x0 ~ x31。其中零号寄存器 x0 是只读寄存器,其值永远为零。 RISC-V 的设计目标之一就是对 C/C++ 等高级语言提供硬件支持,并保持不同 处理器之间在 ABI 层面的相互兼容。RISC-V 的用户指令标准还对函数调用约定 (Calling Convention)做了标准化,也就是对函数调用时,哪些寄存器需要保存, 还对寄存器具体的职能分配做了规定(因为 RV32E 只有 16 个通用寄存器,所以RV32E 的 ABI 和 RV32I 的 ABI 不兼容)。在用汇编语言编写时,这 32 个寄存器 的名称也根据其在调用约定中的职能而被重新命名。具体如表1 所示。

表1 函数调用约定的寄存器分配

 

注意:需要指出的是,除了硬件指令集会对函数调用约定产生影响外,高级 语言的编译器也会对其有影响。 例如,对下面的函数:

void dummy(int a,int b,int c,int d,int e);

不同的编译器可能对函数参数压栈的顺序有不一致的理解。有的会从左到右,以 a、b、c、d、e 的顺序压栈;有的则反之,从右到左压栈。这种编译器在函数调 用约定上的不一致在 C 和 C++ 语言混合编程时经常发生。当 C++ 模块直接调用 C 语言模块时,链接器会给出警告或报错。通常的做法是在调用时,把 C 语言的 函数用 C++ 关键字 extern "C" 加以修饰说明,从而给编译器以明确的指示。

RV32I 指令格式

RV32I 基本指令格式如图 3-5 所示,RV32I 的基本指令格式只有 4 种,分别是寄存 器类型(R-TYPE)、短立即数类型(I-TYPE)、内存存储类型(S-TYPE)、高位立 即数类型(U-TYPE)。

 

RV32I基本指令格式

为了方便跳转指令,RV32I 还包含两种衍生格式 B-TYPE(Branch,条件跳转) 与 J-TYPE(Jump,无条件跳转)。B-TYPE 衍生于 S-TYPE,B-TYPE 除了立即数 的位排列与 S-TYPE 不一样外,其他的格式都与 S-TYPE 一样。J-TYPE 也是通过 类似的方式衍生于 U-TYPE。用这种方式衍生新格式的目的是便于硬件产生目标 地址。 上面这些格式,除 R-TYPE 外,其他的格式都需要把最高位(第 31 位)做符 号扩展,以产生一个 32 位的立即数,作为指令的操作数。

图 5 所示的这些指令格式非常规整,其操作码、源寄存器和目标寄存器总是 位于相同的位置上,简化了指令解码器的设计。

RV32I 算术与逻辑指令

1. 立即数指令

1)立即数加法 RV32I 立即数加法的定义如图 3-6 所示。这里特别要指出的是,和许多其他的指令集不同,在 RV32I 当中并没有专门的状态寄存器和标记位来记录加法溢出。 对加法溢出的判断是通过在加法指令之后安排比较和跳转指令来实现的。对符号数 加法来说,只有正数加正数,或者负数加负数的情况才有可能发生溢出,所以溢出 可以通过符号位(与零比较)来判断。而对无符号数来说,其和应该不小于被加数, 所以溢出也可据此判断。

 

图6. 立即数加法 ADDI

从 RV32I 对溢出标记的舍弃,也可以看出 RISC-V 非常强调指令集的简洁,极 力减少不必要的硬件或指令,秉承了 RISC 指令集将复杂操作通过多条简单指令来 实现的原始设计理念,可以说是不忘初心(这里实际上涉及一个更加复杂的话题, 即 RISC-V 在设计时对条件编码(Condition Code)的舍弃)。具体的细节会在后 续章节加以讨论。

从 ADDI 指令也可以衍生出空操作指令(NOP)。对 RISC-V 指令集,编译器 一般会把ADDI中的立即数、源寄存器、目标寄存器都置为零,当作空操作指令使用。

2)立即数比较

RV32I 的立即数比较指令如图 7 所示。无论是符号数比较还是无符号 数比较,图 7 中的 12 位立即数都应该通过符号位扩展变为 32 位立即数, 然后根据指令 12 ~ 14 位中的功能定义,来决定比较方式符号数(SLTI)/ 无符号数(SLTIU)。

 

图7. 立即数符号数/无符号数比较

比较时,如果源寄存器中的值小于该 32 位立即数,则将目标寄存器置为 1; 否则置为零。由此,还可以通过 SLTIU 产生一个衍生指令:

 

SEQZ 指令可以很方便地根据源寄存器中的值产生零位标志,而无须添加额 外的硬件或指令。

3)立即数逻辑操作

RV32I 的立即数逻辑操作如图 8 所示。细心的读者可能已经注意到了, RV32I 中并没有定义逻辑反操作(NOT)。实际上,逻辑反操作可以通过 XORI 来 实现(只需将 XORI 指令中的立即数置为全 1 即可)。

 

图8. 立即数逻辑操作

4)立即数移位操作

RV32I 的立即数移位操作如图 9 所示。对逻辑左移操作,需要在最低有效位 (Last Significant Bit,LSB)补零。对逻辑右移操作,需要在最高有效位(Most Significant Bit,MSB)补零。而对算术右移操作,则需要在高位做符号位扩展。同 时,为了指令集的简洁,RV32I 中没有包括循环移位指令,因为循环移位可以通过 移位指令和其他指令的组合来实现。

 

图9. 立即数移位操作

SRLI 与 SRAI 的唯一编码区别是第 30 位。在处理器硬件实现上述移位指令时, 硬件只需判断此位便可加以区分。然而,为了兼容性测试的需要,RISC-V 官方提 供了一个非法指令(Illegal Instruction)的软件测试,当硬件遇到非法指令时产生 异常。为了通过该测试,硬件设计时需要将图 39 中的高 7 位都考虑进去。