RISC-V 指令集介绍(二)

发布时间 2024-01-01 05:19:29作者: 吴建明wujianming

RISC-V 指令集介绍(二)

32 位立即数构建与地址生成

通过对图5 的观察可以发现,U-TYPE 指令中的立即数有 20 位,而 I-TYPE 指令中的立即数有 12 位。32 位立即数可以通过一条 U-TYPE 指令和一条 I-TYPE 指令来联合构建。图10 中的 LUI(Load Upper Immediate,高位立即数载入)指 令即是为此目的而设计的,该指令会将其所携带的 20 位立即数载入目标寄存器的 高位,而将目标寄存器的低 12 位置零。如果在 LUI 指令之后紧随一条 ADDI 指令, 则可以继续构建目标寄存器的低 12 位,从而拼接出完整的 32 位立即数。该 32 位 立即数也可以作为 32 位的地址使用。

 

 图10. 立即数和立即地址构建指令

根据 RISC-V 这种“20+12=32”的立即数指令格式,可以把 RISC-V 的内存空 间想象成一个分页的结构,其每个页面的大小为 212=4 096 字节,而页地址则有 20 位。图10 中的 AUIPC(Add Upper Immediate to PC,高位立即数加 PC)指令就 是为了移动页地址而设计的,和其他的 U-TYPE 指令一样,AUPIC 也会将其携带 的 20 位立即数作为高位,而将低 12 位置零,以生成一个完整的 32 位数。然后该 32 位数会与当前指令计数器(32 位寄存器)的值相加,并将结果存入目标寄存器 (RV32I 也用 PC 来存放当前活跃指令的内存地址)。

RISC-V 的设计目标之一就是为高级语言提供硬件支持,而有了 AUIPC 指 令,可以很容易构建相对 PC 的寻址方式,从而实现独立于地址的代码(Position Independent Code,PIC)。如果要将相对于当前地址 0x1234 字节的内容载入 x4 寄存器,则可以通过 AUIPC 指令用如下的代码实现:

aupic x4, 0x1 # PC + 0x1000 => x4

lw x4, 0x234(x4) # (x4 + 0x234) => x4

如果不使用 AUIPC 指令,则需要采用如下的变通办法:

jal x4, 0x4 # PC + 4 => x4, 同时也无条件跳转至 PC + 4

lui x5, 0x1 # 0x1000 => x5

add x4, x4, x5 # x4 + x5 => x4, x4的值变为 PC + 0x1004

lw x4, 0x230(x4) # (PC + 0x1004 + 0x230) => x4

虽然上面的变通办法也可以达到目的,但是它有以下缺点:

(1)代码晦涩冗长,而且需要借助额外的寄存器 x5。

(2)跳转指令(Jump and Link,JAL)可能会误导流水线的运行,使 得流水线执行清空动作。在某些采用 BTB(Branch Target Buffer,分支目 标缓冲区)(用来记录之前发生过跳转的指令的 PC 值和目标地址)来做跳 转预测的处理器上,上面的跳转指令会在 BTB 中留下记录条目,但对跳转预 测却并无帮助,因为目标地址等同于下一条顺序执行的指令地址。 由此可见,AUIPC 的引入极大地减轻了编译器的负担。

注解:跳转预测

为了进一步提高流水线的运行效率,处理器的设计者往往会在取指器中 加入跳转预测的模块。跳转预测常用的模块部件有:

① BHT (Branch History Table,跳转历史表 ),利用 PC 的末几位地址, 来记录之前发生的跳转历史,作为动态跳转预测的依据。BHT 有时也叫 BHB (Branch History Buffer)。

② BTB,当跳转发生时,BTB 会记录下跳转指令的地址和其目标地址 (Target Address)。当该 PC 值再次被遇到时,则可以将之前记录的目标地址作为指令读取的地址。一般来讲,最终真正的目标地址都会在流水线比较靠 后的阶段才能确定,如果最后确定的目标地址与之前记录的地址不一致,则 宣告跳转预测失败,清空流水线重新取指令。否则,预测成功,流水线不会 有停顿。

③ RAS (Return Address Stack,返回地址栈 ),RAS 与 BTB 有些类似, RAS主要是用来对跳转返回指令提供预测地址。当程序遇到函数调用指令时, 会把函数的返回地址存入 (push) 到 RAS 中。当取指器认为当前指令是一条跳 转返回指令时,就会做退栈动作 (pop),并把之前存在 RAS 栈顶的地址作为 下一条指令的读取地址。之后,在流水线比较靠后的阶段,当最终的目标地 址被确定时,如果目标地址与 RAS 提供的预测地址不吻合,则预测失败,清 空流水线重新取指令;否则,预测成功,流水线不会有停顿。

寄存器 - 寄存器指令 寄存器 - 寄存器指令包括加减法(见图11)、数值比较(见图12)、逻辑 操作(见图13)与移位操作(见图14)。这些指令的功能和前面的立即数指 令相似,唯一的区别是立即数指令中的立即数被替换为源寄存器 2(寄存器 - 寄存 器指令中包含减法指令,而立即数操作则没有定义减法)。

 

图11. 寄存器加减法

 

图12. 寄存器数值比较

 

图13. 寄存器逻辑操作

 

图14. 寄存器移位操作

由于这些相似性,本书对寄存器 - 寄存器指令不再赘述,读者如有疑问,可以 查阅 RISC-V 官方标准中的相关部分。

控制转移指令 RISC-V 中的转移控制指令(Control Transfer Instructions)主要包括以下两类:

(1)无条件跳转(Unconditional Jump)。

(2)有条件跳转(Conditional Branches)。

不过和其他指令集相比,RISC-V 的跳转指令设计得非常有特色:

(1)RISC-V 中并没有专门的函数调用指令,函数的调用是通过设置跳 转指令中的寄存器来实现的,这样减小了指令集的规模。

(2)RISC-V 将比较和跳转相结合,而且利用条件跳转来对“溢出”“零值”等做出判断。RISC-V 本身并不专设状态寄存器来做溢出位、加法进位、 零值标识等,从而简化了处理器结构。

(3)RISC-V 为无条件跳转指令专门定义了一种 J-TYPE 的指令格式,而 该格式衍生于 U-TYPE。J-TYPE 只是在 U-TYPE 的基础上,对立即数的某些 位做了位置调整。对有条件跳转指令,RISC-V 也做了类似的处理,在 S-TYPE 的基础上衍生出了 B-TYPE。这些细微的调整,可以在一定程度上降低硬件设 计的开销。

1. 无条件跳转指令 (Unconditional Jump)

1)直接跳转 JAL(Jump and Link,跳转与链接)

JAL 指令如图15 所示。RISC-V 为 JAL 指令专门定义了 J-TYPE 格式。将 图15 和图5 中的 U-TYPE 比较,可以发现除了立即数的某些位做了位置调整 以外,其他都没有变化。JAL 指令会把其携带的 20 位立即数做符号位扩展,并左 移一位,产生一个 32 位的符号数。然后将该 32 位符号数和 PC 相加来产生目标地 址(这样,JAL 可以作为短跳转指令,跳转至 PC±1 MB 的地址范围内)。

 

图15. JAL 指令

同时,JAL 也会把紧随其后的那条指令的地址存入目标寄存器中。这样,如果 目标寄存器是零,则 JAL 等同于 GOTO 指令;否则,JAL 可以实现函数调用的功能。

2)间接跳转 JALR(Jump and Link Register,跳转与链接寄存器) JALR 指令如图 16 所示。JALR 指令会把所携带的 12 位立即数和源寄存器 相加,并把相加的结果末位清零,作为新的跳转地址。同时,和 JAL 指令一样, JALR 也会把紧随其后的那条指令的地址存入目标寄存器中。

 

图16. JALR指令

JAL 指令受其指令格式所限,只能实现 PC±1 MB 的短跳转。而通过如下的指 令序列将 JALR 指令和 LUI/AUIPC 指令相结合,可以实现全地址空间的跳转:

lui ra, 立即数(20位)

jalr ra, ra, 立即数(12位)

或者 auipc ra, 立即数(20位)

jalr ra, ra, 立即数(12位)

2. 动态预测返回地址

尽管 RISC-V 指令集本身并没有对 JAL 或 JALR 中目标寄存器的取值做出限制, 但是根据前面提到的函数调用约定(Calling Convention),JAL/JALR 常用的目标 寄存器有 x1(ra,返回地址)和 x5(t0,替代链接寄存器)。对普通的函数调用, x1(ra)会被用来存放返回地址。然而,表 1 的调用约定中还定义了 x5(替代链 接寄存器),其作用是:

(1)在使用压缩扩展指令集(Compressed Instruction Extension)时,方 便将函数调用的开场白和收场白作为公共的函数调用,从而到达提高代码密 度(Code Density)的目的。对 x5(替代链接寄存器)的具体用法,会在后续 有关“压缩指令扩展”的章节做详细讨论。

(2)对于协程(Coroutine)这种需要实现堆栈切换的情况,利用 x5(替 代链接寄存器)可以帮助实现快速的 Coroutine 调用与返回(见表2)。

表 2 JALR 指令的 RAS 操作

 

 

RISC-V 的函数调用约定将 JAL/JALR 在调用和返回时可使用的寄存器限制在 x1 或 x5 上,为动态预测返回地址创造了条件。在 3.4.5 节的注解部分,提到了跳 转预测常用的三种模块:BHT、BTB 与 RAS。将 RAS 与无条件跳转指令相结合, 则可以很好地实现返回地址的动态预测,具体做法如下:

(1)JAL 指令的 RAS 操作。 根据函数调用约定,当 JAL 的目标寄存器值是 x1 或者 x5 时,可以判定其是 在做函数调用。这时可以把返回地址(紧随其后的那条指令的地址)压入 RAS。

(2)JALR 指令的 RAS 操作。 JALR 指令既可以作为函数调用指令,又可以作为调用返回指令,其 RAS 的操作方式如表 2 所示。

注解:协程

在多任务处理中(Multi-task),协程(Coroutine)可以被看作一种合作式 (Collaborative),非抢断式(Non-preemptive)的线程(Thread)。图17 展示了两个协程互相调用的情形,其中每一个节点都是一个退让(Yield)操作。 这样的话,协程 A 和 B 会轮流按照 1、2、3、4、5 的顺序执行。

 

图17. 协程

为了实现协程,往往需要做堆栈的切换。把 alternative link register和表2 相结合,把 x1(ra) 与 x5(替代链接寄存器)分别分配给图17 中的协程 A 和 B,则可以提高跳转预测的准确率,以实现协程的快速调用和返回。在图17 中除第一个退让外,其他的退让操作既可以看作从一个协程返回另外一个协 程,又可以看作从一个协程调用另外一个协程。这就是为什么表 2 中的第 4 行既有出栈操作,又有入栈操作。

注解:宏操作合并

在处理器设计中,可以设计一个硬件模块,在指令解码阶段之前,通过 对已取指令序列的观察,将其中某些前后相邻的简单指令合并为一条复杂的 指令(可以是处理器内部定义的专门指令),以提高指令执行效率,这种做 法称为宏操作合并(Macro-Op Fusion)。最后所提到的 LUI+JALR 或 AUIPC+JALR 指令序列,就是被宏操作合并的典型例子。表2 中的最后一行, 就是为了提高这两个指令序列被宏操作合并之后的跳转预测准确率而设计的。 这里要注意的是,宏操作合并是处理器设计实现的方法,与指令集本身 无关。另外,Intel 公司是宏操作合并技术的发明者,对该项技术拥有专利。

3. 条件编码

在讨论有条件跳转指令(Conditional Branches)之前,有必要先介绍一下条 件编码(Condition Code)的概念。实际上,以笔者多年从事嵌入式设计的经验来 看,由于各种技术指标之间的相互抵触(例如数字设计的 AT2 边界限制),设计的 过程更多的是在各个技术方案之间寻求妥协和取舍,以求达到总体最优的过程。 RISC-V 的设计者也是基于各种综合的考量,最终决定舍弃条件编码,而代之以将条件判断与跳转指令直接相结合的方案,具体如下。

在传统的 RISC 设计中,设计者往往会安排一个状态寄存器,在其中放置各 类的标志位[例如溢出标志(Overflow)、零标志(Zero Flag)、进位标志(Carry)等]。 在某条指令更改了这些标志位之后,后续的指令会根据更改后的标志位来决定是 否需要被执行。为此,这些标志位会被编码(即条件编码),并成为指令编码的 一部分。 在 ARMv8 指令集中,就在指令编码中专门分配了 4 位,用来做条件编码,以 表示比较相等(结果为零)、溢出等状态。对“ if(a == 10){…}”这样的高级代 码,编译器的常用处理方式是在一个计算指令之后跟随一个条件执行指令,如下面 的伪代码所示:

subtraction (register – 10) # 减法,结果可以被丢弃

branch if zero flag is not set # 如果不相等则跳转

注意:上面的代码序列实际上包含了状态寄存器的使用。第一条的减法指令 会影响状态寄存器中的零标志位,而第二条的跳转指令中的条件编码包含有 零标志判断,但是不包含对通用寄存器的读取。

使用条件编码的优点是可以让条件跳转指令变得相对比较简单(不涉及对通用 寄存器的读取,只涉及标志位),这样跳转条件就可以在流水线比较靠前的阶段被 判断出来。但是这样做的缺点是条件编码需要在指令编码中占用一定的位,而且需 要在条件跳转指令之前安排另外一条指令用来设置标志位,降低了代码密度。 同时,硬件也需要有专门的状态寄存器,记录各种标志位。

RISC-V 的设计者则另辟蹊径,将上面标志位设置指令合并到条件跳转指令 中去,在条件跳转指令中直接读取通用寄存器做判断,这种做法的优点是:

● 没有条件编码,节省下的位可以被指令编码派作其他用途,从而减小指令 集规模。

● 只要一条指令就可以实现上面需要两条指令来实现的功能,提高了代码密度。

● 不需要专门的状态寄存器来记录各种标志位,降低了硬件的开销。

这种做法的缺点是:由于需要在条件跳转指令中直接读取通用寄存器,跳转条 件要在流水线中比较靠后的阶段才能判定。RISC-V 的设计者认为,目前跳转预测 的准确度(预测跳转是否发生)和精确度(预测跳转目标地址)都已经大幅提高, 将跳转条件的判定在流水线中后移并不会给性能带来太大的负面影响。权衡利弊, RISC-V 最终还是舍弃了条件编码。

4. 有条件跳转指令

RV32I 的有条件跳转指令总共有 6 条,其定义如图18 所示(图中网格标 记的深色部分为立即数)。RISC-V 为有条件跳转指令专门定义了新的指令格式 B-TYPE,其衍生于图5中的S-TYPE。通过将图18与图5中的S-TYPE作比较, 可以发现 B-TYPE 只是在 S-TYPE 的基础上对立即数的某些位做了顺序调整。

 

图18. 有条件跳转指令 有条件跳转指令会将源寄存器 1 中的值和源寄存器 2 中的值做比较,如果满 足表3 中的比较条件则跳转,其目标地址产生的办法如下:

有条件跳转指令会把其携带的 12 位立即数做符号位扩展,并左移 1 位,来产 生一个 32 位的立即数。该立即数会和当前的程序计数器值相加,来产生最终的目 标地址。这样的话,有条件跳转指令能跳转至 PC±4 KB 的地址范围内。

表 3. 有条件比较跳转指令

 

另外,将源寄存器 1 和源寄存器 2 对换,则可以从表3 里的最后 4 条指令产 生另外 4 条衍生指令,即 BGT(符号数比较,大于则跳转)、BGTU(无符号数比 较,大于则跳转)、BLE(符号数比较,不大于则跳转)、BLEU(无符号数比较, 不大于则跳转)。

5. 静态跳转预测

RISC-V 的设计者没有使用条件编码,而是选择让有条件跳转指令(Conditional Branches)直接对寄存器进行比较,导致跳转条件要在流水线中比较靠后的阶段才 能判定。为了减少这种设计策略对处理器性能的负面影响,RISC-V 对跳转预测非 常重视。除了动态跳转之外,针对有条件跳转指令,RISC-V 还对软硬件设计者做 出了如下的建议,以帮助提高静态跳转的准确率:

(1)在软件设计中不要用条件永远为真的有条件跳转指令(例如 BEQ x0,x0)来代替无条件跳转指令,以减少对分支预测不必要的干扰。

(2)对于有条件跳转指令,其顺序分支应存放比较常用的代码,而其跳 转发展应存放不太常用的代码。对高级语言的循环指令来说,这意味着循环 体应放在顺序分支上。

(3)和高级语言的循环指令相关的另外一条对处理器硬件设计的建议就 是:对于向前跳转的指令(目标地址大于 PC),应静态预测该跳转不发生。 对向后跳转的指令(目标地址小于 PC),应静态预测该跳转会发生。如果和 上一条建议相结合使用的话,则这种静态预测的策略非常符合大多数循环体的实际状况(重复 N 次,然后退出循环)。软件设计者在做优化设计时,也 应该将此条考虑在内。对有条件跳转指令,由于目标地址的产生非常简单直 接(PC+ 立即数),所以不需要很大的硬件开销就可以实施这条指令。

6. B-TYPE 和 J-TYPE 中的立即数

RV32I 中为 JAL 指令和有条件跳转指令分别定义了 J-TYPE 与 B-TYPE 格式。 这两种格式实际上各自衍生于 U-TYPE 与 S-TYPE。J-TYPE 除了立即数的位排列与 U-TYPE 不一样外,其他的格式都与 U-TYPE 一样。B-TYPE 也是通过类似的方式衍 生于 S-TYPE。RISC-V 的设计者做出这种安排的主要原因是为了减小硬件的开销, 现说明如下。

为了实现 16 位地址的边界对齐,JAL 指令和有条件跳转指令都需要将其所携 带的立即数做左移一位的操作。如果不在指令格式上做些处理的话,那么最后生成 的 32 位立即数,其位的位置和 U-TYPE 与 S-TYPE 相比,会因位置的偏差而没有 对齐,这就会增加指令解码器的硬件开销。而衍生定义 J-TYPE 和 B-TYPE 的目的, 就是为了将移位后的大部分位保留在原来的位置上。

将图5 中的 I-TYPE、S-TYPE 和 U-TYPE 与图15 及图18 做比较,可以 将处理器中有关 32 位立即数生成的各位组成归纳如表4 所示。

表4. 立即数位组成

 

续表

 

从表4 可以看出,在最终需要产生的 32 位立即数中,有多于 80% 的位(26/32) 其来源最多只有两个,从而极大降低了立即数生成的硬件开销。而这一优势,都是 拜衍生格式 B-TYPE 与 J-TYPE 所赐。

内存载入与存储指令

1. 小端模式

对于字长超过 8 位的系统,其数据在内存中的存放方式主要有两种:一种是小 端模式(Little Endian,小字节序或低字节序),即对每个字长的数据,将低位的 字节存放在内存地址的低位部分;另外一种存放方式是大端模式(Big Endian,大 字节序或高字节序),即对每个字长的数据,将高位的字节存放在内存地址的低位 部分。虽然从技术角度来说,这两种存放方式各有利弊,但是考虑到目前大部分的 商用系统都采用小端模式,所以 RISC-V 的设计者也决定将小端模式定为 RISC-V 的标准模式。

2. 载入指令

和传统的 RISC 指令集一样,RISC-V 避免了 CISC 指令集中那种通过多种寻址方式 访问内存的做法,而将内存的访问仅限于载入(Load)和存储(Store)指令。RISC-V 中 的内存载入指令包括单字节、双字节和 32 位三种字长。同时对单字节和双字节指令, 根据其符号位处理方式的不同(符号位扩展或零位填充),又可分为符号数载入 与无符号数载入。为此,RISC-V 中定义了 5 条不同的载入指令,如图19 所示。

 

图19. 内存载入指令

在图19 所示的内存载入指令中,需要将其所携带的 12 位立即数作符号扩展, 变为一个 32 位的符号数,然后将该 32 位符号数同源寄存器相加,以产生内存的读 取地址。内存读取完成后,从内存读取的数据最终会被存入目标寄存器。 内存的读取可能会导致硬件异常,具体的细节会在后续章节讨论。 3. 存储指令 和载入指令相类似,RISC-V 中还根据字长的不同,分别定义了三种内存存储 指令,如图20 所示。其内存写入地址的产生也和载入指令类似,即将其所携带 的 12 位立即数作符号扩展并同源寄存器 1 相加。而需要写入内存的数据则存放于 源寄存器 2 当中。

 

图20. 内存存储指令

同内存读取一样,内存的写入操作也可以导致硬件异常。

4. 非对齐的内存读写 (Misaligned Memory Access)

和许多其他的系统一样,内存读写地址都是字节地址,如果软件操作不慎, 就可能引起非对齐的内存读写,导致内存操作无法在一个读写周期内完成。对于 这种情况,RISC-V 中并没有强制规定应对之道,而是交由处理器的设计者来自行 决定。通常来说,处理器的设计者可以有硬件处理和软件处理两种解决策略。

(1)处理器的设计者可以选择以硬件方式来处理非对齐的内存读写,除 了增加硬件开销之外,还需要考虑读写操作的原子化问题。由于非对齐的内 存读写往往需要两个读写周期才能完成,如果处理器硬件在设计时不加以特 殊处理,则可能会由于外部中断等原因,导致在这两个读写周期之间被插入 其他的操作,使内存读写不再是一个原子化的操作。RISC-V 本身对这种情况 并没有做强制规定,而是由软硬件开发者自行决定。

(2)处理器的设计者也可以选择不在硬件层面去处理,而代之让硬件产 生异常,然后交由软件处理。

RV32I 内存同步指令

RISC-V 的设计者在设计之初就考虑到了并行处理的问题,在 RISC-V 的术 语中,每个处理器核可以包含多个硬件线程,称作硬件线程(Hardware Thread, HART)。每个 HART 都有自己的程序计数器和寄存器空间,独立顺序运行指令。 不同的 HART 会共享同一个内存地址空间,从这一点上来说,HART 和 Intel 处理 器中的超线程(Hyper Thread)非常类似。

在 RISC-V 中,当这些不同的 HART 需要做内存访问同步时,需要显式地 (Explicitly)使用 FENCE 指令来同步数据。由于多 HART 的问题不在本书讨论 的范围之内,所以本书对 FENCE 指令不再做进一步阐述。然而,即使是顺序执 行指令的单核单硬件线程处理器,其也会有内存同步的问题,具体主要有以下两种情形:

(1)自修改代码(Self-modifying Code),即程序在执行过程中,会动 态修改自己的指令存储内存。无论在工业界还是学术界,这种做法都是颇有 争议的。甚至有些极端的看法认为只有病毒软件才需要自修改代码,而且自 修改代码往往会带来安全上的隐患,所以不建议使用。

(2)软件载入过程。软件载入的一般情况是处理器上电后会先运行引导 加载程序(Bootloader),然后引导加载程序会把其他软件当作数据载入到内 存中,接着跳转至载入地址,运行新载入的软件。在这个过程当中,处理器 可能存在指令内存预读取、指令缓存、流水线等一系列对内存同步有复杂影 响的活动,在新软件被运行之前,需要采取措施,以保证内存同步。

不论是上面哪一种情况,为了实现内存同步,处理器都先要运行某个 固定的指令序列来清空缓存、刷新流水线等。这个固定的指令序列被称作 内存屏障(Memory Barrier)。内存屏障通常是由处理器设计者提供给软 件设计者的。在 RISC-V 中,定义了指令同步命令 FENCE.I(该指令属于 Zifencei 扩展),用来发挥内存屏障的作用。

由于只讨论单处理核的情况,所以在所涉及的范围内,FENCE 与 FENCE.I 的实现并没有太大的区别,其定义如图21 所示。

 

图21. 内存同步指令

控制与状态寄存器指令 RISC-V 中除了内存地址空间和通用寄存器地址空间外,还定义了一个独立 的控制与状态寄存器(Control Status Register,CSR)地址空间,其地址宽度是 12 位。随着设计目标的不同,每个处理器实际实现的 CSR 可能会有所不同,因此 RISC-V将CSR的定义放在了特权架构部分,不过这些CSR的访问方式却是一致的, RISC-V 将访问这些 CSR 的指令定义在了用户指令集中(Zicsr 指令集扩展)。

如图22 所示,RISC-V 中一共定义了 6 条访问 CSR 的命令,其具体功能如表 5 所示。

 

图22. 控制与状态寄存器(CSR)操作指令

表 5. CSR 命令的功能定义

 

在表5 中定义的这些命令还包含如下规定:

(1)表5 中定义的操作都是原子操作。

(2)对从 CSR 中读取的值都应该做零位扩展(将高位未使用部分置零)。

(3)对立即数,也应将未使用的高 27 位置零。

(4)如果目标寄存器为 x0 的话,则 CSRRW 和 CSRRWI 应避免读取 CSR,避免产生不必要的副作用。

(5)如果源寄存器为 x0,则 CSRRS、CSRRC 应避免对 CSR 写入操作。

(6)如果立即数为零,则 CSRRSI、CSRRCI 应避免对 CSR 写入操作。

在具体实现时,上述的这 6 条指令可以由硬件实现,也可以为了减小硬件开销, 而选择让硬件产生异常,转而由软件来处理。

环境调用与软件断点

如图 23 所示,RISC-V 中还定义了两条指令(ECALL 和 EBREAK),以实现操作系统的系统调用与软件断点。

 

图23. 环境调用与软件断点指令

基础指令集的面积优化方案

RV32I 中共包含 47 条指令,分为 6 类,其各类包含的指令条目数如表 6 所示。

表 6. RV32I 指令条目数

 

如果处理器设计者想要极力减小硬件逻辑开销,可以选择性地将表 6 中最 后的 3 类共 10 条指令做简化实现,即将 FENCE 指令用 NOP 来代替,将 CSR 指 令和 ECALL、EBREAK 合并解码(这些指令的操作码部分都是一样的)并产生异 常,然后交由软件处理。这样的话,指令集实际需要实现的总指令数变为 47–2–6 – 2+1=38 条。对面积(Area)优化的处理器设计,可以采取这种简化的方案。

RISC-V扩展指令集

RISC-V 将指令集分成基础指令集和扩展指令集(Extension)。基础指令集已 经在前文中得到阐述,而扩展指令集则有 M(乘除法扩展)、C(压缩指令扩展)、 A(原子操作扩展)、F(单精度浮点扩展)、D(双精度浮点扩展)等众多的标准 扩展。除此之外,RISC-V 还允许处理器设计者添加非标准的扩展。

由于扩展众多,在编译器编译代码时,需要把目标处理器具体支持的指令扩展 告诉编译器。以 GCC(GNU C Compiler)为例,其在编译代码时,往往需要软件 工程师提供以下两个选项:-march 和 -mabi。

(1)-march 选项被用来告知 GCC 目标处理器的基础指令集和扩展,对 32 位基础指令集 RV32I,常用的选项有:

① -march=rv32i,仅支持基础 32 位整数指令集(RV32I)。

② -march=rv32im,支持 RV32I + 乘除法扩展。

③ -march=rv32imc,支持 RV32I + 乘除法扩展 + 16 位压缩指令扩展。

(2)-mabi 选项被用来告知 GCC 其应该生成的 ABI,对 32 位基础指 令集,常用的选项有:

 ① -mabi=ilp32,C 语言中的 int、long 和指针类型都是 32 位,浮点参数 通过整数寄存器传递。

 ② -mabi=ilp32f,-mabi=ilp32d,浮点参数通过浮点寄存器传递。

出于实际考虑,只讨论标准扩展中的 32 位乘除法扩展和 16 位压缩 指令扩展部分。前者对数值计算非常重要,后者则能大大提高代码密度。

乘除法扩展(M Extension)

如图 24 所示,RISC-V 中根据乘数和被乘数的类型(有符号数 / 无符号数) 和结果的截取范围(高 32 位 / 低 32 位),分别定义了 4 条 32 位的乘法指令, 其结果也是 32 位。

 

图24. 乘法指令

同样,根据除数和被除数的类型(符号数 / 无符号数),RISC-V 中定义了 4 条 32 位除法和余数指令,如图25 所示。

 

图25. 除法和余数指令

RISC-V 舍弃了条件编码和状态标志位,而采用有条件跳转指令来帮助判断溢 出等状态,除法指令也沿袭了这一设计思路。在 RISC-V 的除法中,无论被除数和 除数的值是什么,都不会让硬件产生异常。不过,下面两种情况是需要特别判断的。

(1)除数为零。 在这种情况下,商应为 32 位全 1(32'hFFFFFFFF),而余数应等于被除数。

(2)符号数除法溢出。 这种情况只会发生在被除数为 -231 而除数为 -1 的情况。由于补码(Two’s Complement)中正数和负数的不对称性,231无法用32位符号数表示,导致溢出。 在这种情况下,商应为 32'h80000000,而余数则为 0。

对 FPGA 实现来说,由于大部分的 FPGA 器件中都带有硬件乘法器,乘法指 令可以直接用硬件乘法器来实现。而对除法和余数指令,既可以采用传统的移位相 减的办法,又可以采用 SRT(Sweeney Robertson and Tocher)除法等实现快速除法。

压缩指令集扩展

为了提高代码密度,RISC-V 中引入了 16 位的压缩指令扩展(C Extension)。 和 32 位指令集 RV32I 相比,C Extension 的引入可以将代码密度提高 40% 左右。 RISC-V 的 C Extension 对 32 位、64 位和 128 位的指令集都做了扩展,所以被 统称为 RVC。本书将只讨论其中对 32 位指令集的扩展部分,即 RV32C,并且讨论 也将集中于整数指令集部分。 1. C Extension 的格式

和许多 RISC 指令集不同,RISC-V 的 16 位压缩指令只是一个扩展(Extension), 而不是一个独立的指令集。在 RV32C 中的每一条指令,实际上都可以被转化为一条完 整的 32 位指令。RV32C 只是对部分 32 位指令的一种简写方式,从而将纯 32 位代码 转化为 16 位和 32 位的混合方式。这样做的好处是处理器如果需要支持 C Extension, 只需要修改指令取指器和指令解码器就可以了,大大简化了处理器的设计。

C Extension 中总共定义了 8 种指令格式,如图 26 所示。作为一种压缩指令 扩展,C Extension 的指令格式中对立即数和寄存器都做了一些限制。

 

图26. C Extension指令格式

(1)立即数的位数被缩减。

(2)寄存器的寻址有 3 位和 5 位两种方式。对 3 位的寄存器寻址(图26 中深色标记部分),其仅限于部分通用寄存器(x8~x15,3'b000对应x8,3'b111 对应 x15)。由表 1 可以看到,x8 ~ x15 都是函数调用时必须用到的寄存器, 临时寄存器没有被包含其中。

(3)如果指令同时涉及源寄存器和目标寄存器,则二者必须相等(如 图26 中所示的 CB 格式,尽管在图26 中只标注了源寄存器,但是实际上 在某些具体的指令中,也包含源寄存器和目标寄存器相同的情形,图32 和 表 12 中采用的是 CB 格式的 C.ANDI 指令)。

2. 16 位载入与存储压缩指令

C Extension 中定义了两种 LOAD 指令,如图27 所示。一种是基于栈指针(x2)的 C.LWSP 指令,另一种是基于寄存器的 C.LW 指令。它们对应的 32 位指令可以 在表7 中找到。

 

图27. C Extension中的LOAD指令

表7. 压缩 LOAD 指令对应的 32 位指令

 

图27 中的立即数(无符号数)都被左移 2 位,然后才被当作位移量。为此, 图27中的立即数都做了一些位的重新安排,其原因和衍生出B-TYPE和J-TYPE 的原因是一样的,都是为了减小处理器实现的硬件开销,此处不再赘述。

另外,图27 中的 C.LW 指令,其寄存器只有 3 位表示。为了和 5 位的寄存 器加以区分,在其名称后加了单引号(rs1' 与 rd',而不是 rs1 与 rd)。

和 16 位载入压缩指令相类似,C Extension 中也分别基于栈指针(x2)和寄存 器定义了两种存储指令,即 C.SWSP 和 C.SW。它们的定义如图28 所示,对应的 32 位指令可以在表8 中找到。由于 STORE 指令格式和前文的 LOAD 指令非常类 似,这里就不再进一步展开。

 

图28. C Extension中的STORE指令

表 8. 压缩 STORE 指令对应的 32 位指令

 

3. 16 位跳转压缩指令

C Extension 中定义了 4 条无条件跳转压缩指令,其定义如图 29 所示,它们 对应的 32 位指令可以在表 9 中找到。在这 4 条指令中,C.J 和 C.JR 都不会保 存返回地址(默认目标寄存器为零),而 C.JAL 和 C.JALR 则默认目标寄存器为 x1(ra)。同时,C.JAL 和 C.JALR 的返回地址是 PC+2,而不是之前 32 位指令中 的 PC+4。

 

图29. C Extension中的无条件跳转指令

表9. 无条件跳转压缩指令对应的 32 位指令

 

另外,C Extension 中还定义了 2 条有条件跳转压缩指令,其定义如图 30 所示, 它们对应的 32 位指令可以在表 10 中找到。这两条指令都默认源寄存器 2 为 x0。

 

图30. C Extension中的有条件跳转指令

表 10. 有条件跳转压缩指令对应的 32 位指令