verilog实现RISC-V流水线处理器
使用verilog语言描述的RISC-V流水线处理器,处理了数据冒险和控制冒险,github地址:流水线
RISC-V流水线数据通路
参考教材Computer Organization and Design: The Hardware/Software Interface,将整个数据通路分为五级流水线,分别为取指阶段(Instruction Fetch, IF),解析指令阶段(Instruction Decode, ID),执行阶段(EX),访存阶段(MEM),写回阶段(WB),各模块也基本按照教材设计。
接下来介绍各大模块的实现。
(作者水平有限,代码和实现中可能仍存在不少纰漏和错误,欢迎读者指出)
PC寄存器和PC加法器
PC寄存器接收一个新的PC值,如果输入的PCwrite信号为高,则在时钟上升沿将输入的新PC值保存,否则不改变原来的PC值;当发生数据冒险和控制冒险时,需要通过PCwrite信号来阻止PC值的更新。
1 | module PC_reg( |
PC加法器用于计算新的PC值并将该值输入到PC寄存器,当没有跳转指令执行时,PC加法器简单地将目前的PC值加4并写回PC寄存器,当跳转发生时,PC加法器计算出目标地址后再写回PC寄存器。五级流水线的数据通路需要在MEM阶段计算出分支目标地址,所以其取EX/MEM流水线寄存器中保存的PC值和立即数字段来计算新的PC值。
1 | module PC_adder( |
流水线寄存器
由于一个数据通路中有多条指令在执行,所以需要流水线寄存器来保存指令相关的信息,如果不用流水线寄存器保存,则当下一条指令载入数据通路时,上一条指令的PC值、控制信号等就会丢失。
五级流水线需要四个流水线寄存器:IF/ID流水线寄存器、ID/EX流水线寄存器、EX/MEM流水线寄存器、MEM/WB流水线寄存器。当流水线寄存器中一个信息在某一阶段被使用了,那么在下一个流水线寄存器中就不再需要保存该值;随着指令在流水线中的流动,也会有新的信息需要保存到流水线寄存器中。
处理数据冒险和控制冒险时需要在流水线中插入空指令,当flush信号为高时,就把寄存器中全部信息清零。
1 | module ID_EX_reg( |
控制器
控制器根据输入的指令来生成对应的控制信号,其首先比对指令中0到6位的操作码(opcode)来确定指令的类型并产生对应的信号。对于R型指令,需要根据funct3和funct7字段来确认ALU所要执行的操作,对于一些I型指令,需要根据funct3字段确定ALUControl。
1 | module Controller( |
立即数生成
I型、S型、SB型、U型和UJ型指令都需要提取立即数字段,其中SB型和UJ型指令中的立即数表示的是半字地址,需要左移一位来转换成字节地址。
1 | module ImmGen( |
寄存器堆
寄存器堆中有32个64位的寄存器,写信号为高时,将输入的数据写入rd寄存器号对应的寄存器中;同时还需要从寄存器堆中读取rs1、rs2寄存器的值并输出。
其中写入数据WD、寄存器号rd和写使能信号是来自流水线最后一级,rs1和rs2则来自IF/ID流水线寄存器。
1 | module RegFile( |
寄存器堆前递单元
由于寄存器堆是异步写(在always块执行完毕后才更新寄存器)、同步读的,而寄存器堆和ID/EX流水线寄存器都是在时钟上升沿进行更新,所以当ID阶段要读取的寄存器和WB阶段要写的寄存器相同时,会导致读取出来的值是更新前的值而不是WB阶段写回的新值。
如果采取在上升沿写入、下降沿读取寄存器堆的解决方案,ID/EX流水线寄存器由于是在上升沿更新,所以就无法取得读取出来的值。因此我采取了添加前递单元的解决方案,当要读取的寄存器等于要写入的寄存器时,就将WB阶段的写回值前递给ID/EX流水线寄存器。
1 | module Forwarding_WB( |
Comparator
比较器用于确定各种跳转指令是否满足跳转条件,并输出信号指示是否跳转
1 | module Comparator( |
ALU
ALU接受两个输入数据,然后根据ALUControl来决定要进行的计算并给出输出。
1 | module( |
RAM
使用256个8位宽的寄存器来组成RAM,即有256个字节。当MemWrite信号为高时,将传入的数据从传入的内存地址开始一个字节一个字节地往后写,读取数据时也是从给定的内存地址开始逐字节读取。
同时还要根据指令的funct3字段来确定要写入的数据长度
1 | module RAM( |
RAM前递单元
对于store指令来说,要存储到内存中的寄存器值可能依赖于前一条指令的结果,因此可能需要将MEM/WB流水线寄存器中的值前递一个阶段
1 | module Forwarding_store( |
RAM数据裁剪
对于lb
、lh
、lw
三条指令,其各自要读取的数据长度分别为1个字节、2个字节、4个字节,而从RAM中读取数据时都是直接从地址开始读取4个字节,因此需要添加一个组合逻辑模块来裁剪多读取的数据。
除了要裁剪数据,还要区分lb
、lh
、lbu
、lhu
,决定是否需要进行符号扩展。
1 | module Load_Clip( |
ALU前递单元
当一条指令的操作数依赖于前一条指令或前前一条指令的结果时会发生数据冒险,如果之前指令的写回结果在EX阶段就能得到,那么就可以通过前递来解决这种数据冒险。
当一条指令需要前一条指令的结果,可以将前一条指令的结果从EX/MEM寄存器中前递给将要进行ALU计算的指令。
当一条指令需要前前一条指令的结果,可以从MEM/WB寄存器中前递数据。
只有当EX/MEM寄存器不进行前递时才考虑MEM/WB寄存器是否进行前递,因为EX/MEM中的结果更新,写回后会覆盖MEM/WB中的结果。
1 | module Forwarding( |
冒险检测单元
load-use数据冒险不能单纯通过前递来解决,因为当指令需要使用内存中的数据时上一条指令还没有访存并得到数据,所以无法前递。
当检测到ID/EX寄存器中的指令需要读取内存,且其要写入的寄存器等于IF/ID寄存器中的指令要使用的寄存器,这时候就需要清空IF/ID寄存器,这样就插入了一条空指令,并且阻止PC寄存器更新,下一个时钟周期又会取出同一条指令加载进IF/ID寄存器中。如此,两条指令之间就间隔了一个阶段,访存的指令获得数据之后就可以将数据前递。
对于控制冒险,这里采取了最简单的延迟分支处理方式,向流水线中插入三条空指令,分支指令在Mem阶段决定好分支目标时再更新PC寄存器的值。
1 | module Hazard_unit( |