Featured image of post CS61c_7 — RISC-V Processor Design

CS61c_7 — RISC-V Processor Design

这个部分我们将开始构建一个RISC-V处理器,实现软件和硬件的交互。

The CPU

  • Processor (CPU): the active part of th computer that does all the work(data manipulation and decision-amking)
  • Datapath: portion of the processor that contains hardware necessary to perform operations specified by the instruction set architecture (ISA)
  • Control: portion of the processor that tells the datapath what needs to be done to execute the instructions
1
2
根据前面的知识,我们知道RISC32大概有30多条指令(整数),我们可以设计三十多个datapath来分别执行这些指令。
但是这样是十分低效的,我们可以设计一个通用的datapath来执行所有指令,这样就需要一个控制器来告诉datapath如何执行每条指令。

Five stages of the Datapath

这个和x86的流水线是类似的(

  • Stage 1: Instruction Fetch (IF)
  • Stage 2: Instruction Decode (ID)
  • Stage 3: Execute (EX) - ALU (Arithmetic-Logic Unit)
  • Stage 4: Memory Access (MEM)
  • Stage 5: Write Back to Register (WB)

上图!

5 stages of the Datapath
1
2
3
这里有一个细节是我们将指令内存和数据内存分开了,
但是实际上他们就是一个物理内存的,为什么呢?
我们可以在cpu中设计两个单独的cache来存放经常使用的指令和数据,一个叫imem,一个叫dmem,这样就可以提高访问速度了。

Some Combinations

  • Register: 注意有一个write-enable信号,只有当这个信号为1时,寄存器才会被写入数据。
  • Register File: 在RB32i中,我们有32个寄存器,每个寄存器都是32位的,所以我们需要一个32x32的寄存器文件。
  • 之前在x86中我们会发现寄存器文件一般都是two-read,single-write的,这里也一样,原因就是我们使用到的指令一般都是2个源寄存器和1个目的寄存器。
Register File
  • 我们可以看到register file的细节,值得注意的是我们如何判断哪一个register写和读呢,可以注意到上方的三个n=5的输入,他们都是0-31的数,代表了目标寄存器的编号。

  • 内存:和register file类似,如图所示:

Memory

R-Type Add Datapath

很显然我们不可能一开始就直接搞出一个全通用的datapath来执行所有的指令,我们先从最简单的add开始。 首先我们要回忆一下add指令的格式:

1
2
3
add rd, rs1, rs2
funct7 |  rs2 |  rs1 | funct3 | rd  | opcode
31-25  | 24-20| 19-15|  14-12 | 11-7| 6-0

这里需要控制模块加一个enable-write信号来允许rd的写入。

add datapath

Sub Datapath

sub指令和add指令几乎一样,只有func7不同。 大部分的path都可以复用,唯一需要改变的是在alu上添加一个控制信号来区分add和sub。 其他的R-Format指令也是一个道理,先decode出对应的func序号,然后在alu上添加一个控制信号来区分不同的指令。

Datapath With Immediate

I-Format 格式:

1
2
immediate | rs1 | funct3 | rd  | opcode
31-20     |19-15| 14-12  | 11-7| 6-0
I-Format Datapath

我们使用了一个mux来选择是使用立即数还是rs2中的值,控制逻辑需要设置一个信号Bsel来选择。 同时,我们需要生成一个32位的立即数,这个过程叫做immediate generation,我们需要根据指令的格式来生成对应的立即数。 在i-type指令中,我们已经有了12位的立即数,只需要进行符号扩展就行了。 至于rs2,依然会自动read,但是不选择。 这里我们才能深刻领会各种指令格式为什么需要对齐,只有对齐寄存器和imm,我们才能使用同一个datapath来decode不同类型的指令。

Load is also Immediate Type

load指令也是i-type的一种,只不过多了一个memory access的阶段,我们需要在datapath中添加一个memory access的模块来处理load指令。

Load Datapath
  • 无非是在将要写会寄存器之前加一个mux选择是用alu的结果还是memory access的结果,都是由控制逻辑的信号来决定的。
  • 当然我们还有一些其他的load指令类型,比如lb, lh, lbu, lhu等,他们的datapath也是在load的基础上进行的,需要添加一些gate、mux来处理不同的情况。

adding sw instruction

sw指令是s-type的一种,格式如下:

1
2
31-25     | 24-20| 19-15| 14-12  | 11-7     | 6-0
imm[11:5] | rs2  | rs1  | funct3 | imm[4:0] | opcode
Store Datapath
  • 由于save指令需要将目标寄存器的值写入内存,所以我们需要将rs2的值连一条线到memory access模块的输入端。
  • 同时由于S-type的立即数是分成两部分的,所以我们需要修改immediate generation的逻辑来生成正确的立即数。
Immediate Generation
  • 这里我们会发现5-10位的立即数都是一样的,直接使用指令的25-30位即可,然后31位的进行符号扩展,至于0-4位的立即数一个来源是指令的20-24位,一个来源是指令的7-11位,我们可以用一个mux来选择。
  • 至于sb,sh等其他的store指令我们完全可以复用当前的datapath,当然需要加一些控制来防止写入不应该写入的内存。

Branches

  • 首先我们要回忆一下branch指令的格式:
1
2
     31-25        | 24-20| 19-15| 14-12  | 11-7             | 6-0
imm[12],imm[10:5] | rs2  | rs1  | funct3 | imm[4:1],imm[11] | opcode
  • 之前我们说过由于risc-v32中的指令基本上都是32位4字节的,但是考虑到一些压缩指令的存在(2字节),所以这里我们默认imm的第零位是0,然后多存了一个imm[12]扩大了imm的范围。
Branch Datapath
  • 我们将添加一个新的组件Branch Comp,在sr1的选择端添加一个mux(Asel)来选择是用rs1还是pc的值,同时pc的输入端也要添加一个mux来选择是用pc+4还是pc+imm。
  • 在Branch Comp中,我们有6种已知的比较方式(beq, bne, blt, bge, bltu, bgeu),最后两个是前者的无符号版本,而前面4个都是可以通过取反得到另一个的,如果我们想得到bne的结果,只需要在control logic中取反即可,bge同理。至于具体如何计算beq和blt的结果,我们可以在alu中添加一个sub来计算rs1-rs2的结果(符号相同的情况下),如果结果为0则beq为真,如果结果小于0则blt为真?这个对于有符号整数来说是可以的,对于无符号整数呢?其实可以搞一个逐位比较器?
Branch immediate generation
Branch immediate generation
  • 这两个图充分展示了risc-v指令的设计之美,如果我们按照常规的方式(第一张图)来安排imm的位数,那么你会发现这里每一位都需要左移一位,这就导致了虽然我们的B-type和S-type类似,但是每一位的imm都不确定,需要一个mux来选择,这个的复杂度是不可接受的,于是伟大的risc-v设计者顺水推舟的在指令中直接左移了一位(第二个图),仅仅是把imm[11]换到了第0位,这样其他的位都是原封不动的,只有imm[11]需要mux来选择,大大简化了imm的生成逻辑。

Jalr 特殊的跳转

  • 以jalr指令为例子,指令格式如下: (属于是I-type的一种)
1
2
31-20     | 19-15| 14-12  | 11-7| 6-0
immediate | rs1  | funct3 | rd  | opcode
  • jalr指令的功能是将pc+4的值写入rd寄存器,然后将rs1+imm的值作为新的pc值。
  • 由于jalr的指令格式和I-type一样,所以他的imm生成不用担心,所以rs1+imm的计算也可以复用,而将这个计算值传递给pc的线路我们在刚才的branch中国已经设计好了,直接把PCsel控制设置成taken即可,唯一需要添加的就是将这个pc+4的值传递到rd寄存器,我们可以添加一条线从pc+4的输出端到writeback的输入端,同时把writeback的mux设置成3种选择:alu的结果,memory access的结果,pc+4的值。其中2对应pc+4,其实也完全可以用mux级联替代这里的3选1mux。
Jump Datapath

J-Format for Jump Instruction

  • J-type指令的格式如下:
1
2
31        | 30-21       | 20     | 19-12      | 11-7| 6-0
imm[20]   | imm[10:1]   | imm[11]| imm[19:12] | rd  | opcode
  • jal指令干了两件事:1是将pc+4的值写入rd寄存器(这个我们在jalr中已经实现),2是将pc+imm的值作为新的pc值(也在branch中实现了),唯一需要改变的就是的imm的生成逻辑(比I-type复杂一些)。

Adding U-Types

  • U-type指令的格式如下:
1
2
31-12     | 11-7| 6-0
immediate | rd  | opcode
  • lui指令将20位高位立即数加载到目标寄存器中,并将低12位清零。
  • auipc 指令将20位高位立即数加上当前pc的值,并将结果存储到目标寄存器中。
  • 所有的硬件组件都已经设计好了,只需要修改Imm Gen的逻辑即可,直接加一个mux来选择高20位绕过符号扩展等一些复杂的判断似乎是最简单的办法了。

Summary

  • 恭喜🎉,通过复杂的设计,我们得到了一个可以执行任何一条risc-v32指令的处理器!
  • 但其实我们有一个重要的东西还没有实现,那就是control logic,之前我们都是假设有一个控制逻辑来告诉datapath如何执行指令,但是我们还没有设计这个控制逻辑,下一部分我们将设计这个控制逻辑来完成整个处理器的设计。
Licensed under CC BY-NC-SA 4.0
啊啊啊啊啊啊啊
使用 Hugo 构建
主题 StackJimmy 设计