基于RISC-V指令集的CPU设计和FPGA实现(二)

RISC-V 指令集分析

其实接下来的指令介绍我刚开始不太确定要不要写,因为对于 CPU 设计和实现而言略有拖沓,但是熟练掌握指令格式还是对写 CPU 有帮助的。接下来的部分假如学过 RISC-V 指令集的可以跳过,并且也只是简单介绍下指令的特点和作用,并不会细讲,建议拿 RISC-V 指令集尝试实现一下 C 语言程序。(或者体会我们将一百多行 C 编译出来的机器码和汇编码做手动解释的痛苦,不过我当时是用 Java 程序自动完成的,得亏 RISC-V 指令集还算工整)

RISC-V指令为32位等长指令(不考虑其他短指令),其指令分为 R、I、S、B(SB)、U、J(UJ)型,结构较为工整。

基于RISC-V指令集的CPU设计和FPGA实现(二)_第1张图片 

 

  • rs1 操作寄存器 1,一般是从中取出数据

  • rs2 操作寄存器 2,一般是从中取出数据

  • opecode 指令代码,用来代表是哪种指令(一般是大的类型,如 R、I、S、B、U、J型)

  • funct7funct3 主要用来表明指令的具体功能(一般是小的类型,如有无符号)

  • rd 目的寄存器,一般是要写入的寄存器

  • imm 立即数,后面的类似数组的调用即为类似数组的调用 : ),不过要注意的是有一部分类型(B,J型指令的立即数空了一位)

基于RISC-V指令集的CPU设计和FPGA实现(二)_第2张图片 

 

分析各个指令(详见RISC-V的指令说明书),易知 opecode 能够分辨指令的类型和部分同类型指令,同类型指令主要靠 funct3funct7 分辨其操作不同。比如对于有符号操作和无符号操作,其差别仅为 funct7 的一位区别。

但是在实际写代码的时候是否要用这些特点就不好说了,一方面是用了之后可读性下降,而且判断不全面也会出现指令误判的可能,另一方面是用了是否能提升性能有待商榷,毕竟Vivado的综合能力还是蛮强的。

注意:

  • 对于立即数扩展,还需要注意部分指令的位数有略去低位的现象。比如对于 J 型指令,略去最低一位是因为 RISC-V 指令集的指令都是 16 位往上的,按字节寻址的时候并不需要考虑最低一位,这样可以节省空间表示更大的范围。

  • 对于寄存器 x0(alias: zero),它的值恒为 0,即使你对它执行写入操作。

这里不会对RISC-V指令做过多的介绍,对于更多的指令细节,可以查看网站 RISC-V Instruction Set Specifications — riscv-isa-pages documentation (msyksphinz-self.github.io)

R 型指令

31 - 25 24 - 20 19 - 15 14 - 12 11 - 7 6 - 0
含义 funct7 rs2 rs1 funct3 rd opecode

对于 R 型指令而言,基本上是用来进行 寄存器操作 的。举个栗子:

add 指令,指令格式:add x2, x0, x1,就是将 x0x1 的寄存器内容相加,然后写入寄存器 x2

在上面的载入指令栗子中,x2rdx0rs1x1rs2

I 型指令

31 - 20 19 - 15 14 - 12 11 - 7 6 - 0
含义 imm[11: 0] rs1 funct3 rd opecode

对于 I 型指令,一般是用来进行 立即数和寄存器操作 的。举个栗子:

  • addi 指令,指令格式:addi x2, x0, 20,就是将 x0 的寄存器内容和立即数 20 相加,然后写入寄存器 x2

    在上面的栗子中,x2rdx0rs120imm

  • 还有载入指令的指令格式略为特殊

    lw 指令,指令格式 lw x2, 20(x1) ,首先,x1 寄存器内存放的数据作为一个地址(主存地址或者外设地址 [ 假如统一编址的话 ] ),那 20(x1) 就意味着将 20 + num[x1] 作为取出数据的地址,然后将这个数据存入到 x2 里面。

    在上面的载入指令栗子中,x2rdx1rs120imm

  • 还有 jalr 指令(无条件跳转),指令格式 jalr x2, 20(x1) ,跟载入指令相同,将 20 + num[x1] 作为要跳转的地址,然后将 PC + 4 存入 x2 寄存器里面

    在上面的跳转指令栗子中,x2rdx1rs120imm

S 型指令

31 - 25 24 - 20 19 - 15 14 - 12 11 - 7 6 - 0
含义 imm[11: 5] rs2 rs1 funct3 imm[4: 0] opecode

对于 S 型指令,一般是用来进行 存储操作 的。举个栗子:

sw 指令,指令格式:sw x2, 20(x1)。首先,x1 寄存器内存放的数据作为一个地址(主存地址或者外设地址 [ 假如统一编址的话 ] ),那 20(x1) 就意味着将 20 + num[x1] 作为存储数据的地址,然后将寄存器 x2 的内容存放在那里。

在上面的栗子中,x2rs2x1rs120imm

这时候可能有眼尖的同学发现了,为啥存储指令跟加载指令不搁一块儿捏。因为加载指令有目的寄存器rd(要写入数据的寄存器),结构跟 I 型很像,就归为 I 型了。但是存储指令,它不用写入寄存器呀,同时,它除了要地址rs1,它还要数据 rs2 呀,所以是这样子滴。

B 型指令

31 - 25 24 - 20 19 - 15 14 - 12 11 - 7 6 - 0
含义 imm[12 | 10: 5] rs2 rs1 funct3 imm[4: 1| 11] opecode

对于 B 型指令,一般是用来进行 条件跳转 的。举个例子:

bge 指令,指令格式:bge x1, x2, Label,这里的 Label,本质上是一个立即数,只是写汇编代码的时候我们可以用类似 C 语言的写法写成一个标记,编译器会自动将其转化为指令地址(立即数)

 if (x1 > x2)
     PC = Label;
 else 
     PC = PC + 4;

U 型指令

31 - 12 11 - 7 6 - 0
含义 imm[31: 12] rd opecode

对于 U 型指令,一般用于加载长立即数,举个栗子:

lui 指令,指令格式 lui x1, 0xFFFFF,就是将 0xFFFFF 这 20 位,载入到 x1 寄存器中,当然会覆盖掉原值,这时候 x1 寄存器为 0xFFFFF000 [32 位]

可能有眼尖的同学发现了,要它干嘛捏。

其实,你会发现 I 型指令只能操作 12 位立即数,但是我的寄存器有 32 位呀,这不糟践了吗,要是弄个数老得做个加法然后做个移位,多大罪过捏。

比如我要弄一个 32 位立即数 0xFEDCBA98,那只需要这样写:

 lui     x1, 0xFEDCC
 addi    x1, x1, -1384       # 0x568

可能有脑子尖的同学发现了,怎么这数那么别扭捏,不是 0xFEDCB 咩,怎么就变成这副模样了。

因为,对于 I 型指令而言,它的立即数是有符号的,你要加上一个十二位的数字,第十二位就是符号位,所以是个负数,需要用补码转换得到。同时由于是负数,所以高位会减 1,因此要是 0xFEDCC 减 1 才能是 0xFEDCB

J 型指令

31 - 12 11 - 7 6 - 0
含义 imm[20 | 10: 1 | 11 | 19: 12] rd opecode

对于 J 型指令,一般用于无条件跳转,举个栗子:

jal 指令,指令格式: jal x2, Label,这里的 Label 跟 B 型指令含义相同,但是到二进制码会略去最低位(当然汇编的数字是正常写的,编译器会帮你处理) ,将 Label 作为要跳转的地址,然后将 PC + 4 存入 x2 寄存器里面

在上面的跳转指令栗子中,x2rdLabelimm

好了,基础知识就讲那么多,下一篇就要开始单周期CPU设计和实现

你可能感兴趣的:(RISC,CPU,大数据)