构建五级流水线——lab1
本文最后更新于 2026年4月15日 12:29:24
我针对lab1代码进行了重构,想要更清晰地理清每个部分的功能。
一、信号定义
enable和valid:
- enable是控制寄存器是否更新,stall时表示不更新
- valid控制指令是否有效,针对指令本身,对应气泡。这个需要一直传递
pc和inst:
- pc:当前命令对应的地址
- inst:当前命令
并不是所有模块都需要这两个信号,但可以在级间寄存器中保留,便于查看波形图
clk和reset
- clk:时钟信号,用于同步
- reset:重置信号,用于告知是否要复位
这两个信号都属于全局条件,整条流水线需要同时接收,而不是通过流水线一级级往下传
二、五级流水线
if——取指
id——译码
这一步根据inst进行解码,同时从regfile那里得到寄存器的值。这是整条流水线的大脑,指导后续每个模块。这里解码出来的信号在到达所需阶段之前会一直被传递下去
ex——运算
mem——存取
wb——写回
①如何设计一个职责清晰的流水线架构?
五级流水线通过每个阶段执行不同的指令,尽量让所有模块都在工作,从而达到提速的效果.因此,流水线被拆分成了取指,解码,计算,访存,写回这五个部分,每个部分各司其职.
然而这带来一个问题,也就是当前指令并不总是能够拿到最新的值(称为写后读问题).比如add运算在ex阶段算出来结果,却要在wb阶段才能真正写回.这时,需要专门处理,使得每个模块能够拿到最新的值.
因此,自然的思路就是每个模块各自处理自己的值,使得自己能够拿到最新的值.还有一种做法,就是在decode阶段就将转发逻辑处理好.这在lab1中只涉及转发至ex阶段的情形下还能够运行.然而,后续增加了更多指令,也会有各自需要处理的数据问题.如果全部都将这些处理交给id阶段,会使得它过于臃肿.并且,负责分解职责的只有id阶段这一个部分,将它看作整个流水线的大脑并不合适.
更加合理的做法是每个部分都引入一个小型的处理装置,哪个模块需要什么,这个模块就专门处理相关的数据逻辑.比如ex阶段需要最新的数据,就专门将转发交给ex来做.根据数据的归属阶段进行功能划分,会使得整体更加清晰.
具体实现在下面详述
三、基本模块
pc
首先是pc模块,单纯接受时钟信号用于同步,接受reset用于复位,enable表示指令是否更新,next_pc是将要跳到的pc,输出cur_pc表示当前进行到哪条指令。
decoder
接着是decoder模块。它只接受机器码,并完成解码工作。
lab1中的指令有两类,第一类是R-Type (Register 类型)
- 适用指令: add, sub, and, or, xor, addw, subw
- 特点: 数据的来源是两个寄存器(rs1 和 rs2),计算结果存入目标寄存器(rd)。
32 位机器码结构:(补充)
- opcode:决定算数的type,这里就是Rtype
- rd,rs1,rs2:分别对应一个具体的寄存器编号,由于位置固定,可以直接从inst中进行切分
- funct3、funct7:决定具体的计算类型
第二类:I-Type (Immediate 类型)
- 适用指令: addi, xori, ori, andi, addiw
- 特点: 数据的来源是一个寄存器(rs1)和一个立即数
- 牺牲rs2和funct7,变为存储5+7=12位的立即数
- 需要进行符号扩展以变成64位
① 首先是用于ex阶段的:
从inst中的固定位置切分出对应代码,从而确定rd,rs的值。再分类成四种运算:i,r,iw,rw,并通过布尔运算确定i和w。接着分成四种运算,并根据具体的inst得到op,imm和we的值
② 还有wb阶段的:
id阶段的decoder和regfile都是组合逻辑,因此decoder一旦分出寄存器编号,regfile立刻便可以将数据取出
由于op写成数字不直观,引入额外的mypkg进行定义
alu
统一接受两个数进行计算,接受op来选择进行何种计算,接受w来判断是否要截断到32位,最后输出一个答案ans
regfile
同时负责id读取寄存器和wb阶段写回寄存器。这两个都是寄存器操作,因此并入一个模块。
regfile作为直接操控寄存器的模块,需要直接暴露寄存器的值用于difftest。而写入寄存器为防止毛刺需要遵守clk,也就是第一拍接收到mem_wb的信号,在下一拍才真正写入寄存器
而difftest会在mem_wb的valid信号传入的时候进行检查,来不及等regfile写入,因此需要实例化32条gprs导线将正确的值暴露出来
四、级间寄存器
这些时序寄存器起到承上启下的传递作用,让每个基本模块都能接收到它们需要的值;同时,它们全都接收clk和reset全局信号,从而可以控制整条流水线;它们还负责将valid信号传递下去,用于表示当前信号是否有效;传递pc,便于后续查错的时候检查当前指令走到哪一步了
因此,这些寄存器都默认接收clk,reset这两个信号,传递valid,pc,inst这三个信号,后续分析中不再赘述
这些寄存器还要将每个模块产生的新信号传递下去,用完的信号则不再传递
if_id
decoder需要接收它所需要的inst信号,用于解码。正好inst属于默认信号,最终这个寄存器只需要传递默认的三个信号
id_ex
这里将regfile取到的三个值进行传递
同时将解码出来的运算op,是否32位和立即数传给alu。这些信号在alu处被消耗掉之后就不再继续传递
将是否要写回,写回哪个寄存器传给wb,需要一直传递到wb阶段
同时,需要额外传递ex阶段将要用到的两个源寄存器,用于交给转发模块进行写后读冲突的判断ex_mem
需要将alu计算出来的结果传递下去
继续传递wb的是否写回和写回哪个mem_wb
继续传递alu的计算结果(虽然可能是写入内存而不是写入寄存器的)
继续传递wb的是否写回和写回哪个
五、特殊模块
1. forward
转发逻辑的机制:
如何计算转发逻辑:可以数时钟上升沿的个数
ex阶段使用一个MUX,从id,EX/MEM,MEM/WB这三个来源中选取一个来源进行后续的计算.
alu包含五种基本运算:add,sub,and,or,xor
decoder只将所有指令区分为上面这五种中的一种,因为是否使用立即数或者32位与实际做出的计算无关
需要处理三种转发:
EX/MEM → EX
1
2add x5, x1, x2
add x6, x5, x3上一条实际改动的寄存器x5,直接作为下一条指令的输入寄存器(rs1或者rs2)。这时,上一条指令的计算结果在EX/MEM寄存器中,并且已经经过时钟从输入走到了输出.此时,将ex/mem输出的数据转发至ex模块是正确的
MEM/WB → EX
1
2
3add x5, x1, x2
xor x7, x8, x9
sub x6, x3, x5sub指令需要用的x5在一条指令之前被使用过了,此时sub走到ex阶段,x5的新值走到了mem/wb的输出口上,将这个值转发至ex即可
WB → ID / regfile 同周期旁路
1
2
3
4add x5, x1, x2
add x10, x11, x12
sub x13, x14, x15
add x6, x5, x3add指令用到的x5在两条指令之前被使用,此时x5的值同样处在wb阶段.不同之处在于,此时wb使用regfile进行写回,而同时,id阶段通过regfile读取数据.然而,写数据是需要时间的.此时,我们可以在regfile中设计一种转发,使得id能够直接读取到最新的数据.
这里的转发不再是针对ex阶段,而是由于写入数据需要时间,因此id阶段读取数据会有延迟导致的
为什么不像前两个转发一样,把regfile也开放给ex,从而让ex再去读取regfile中最新的数据呢?这打破了ex单纯负责计算的职能.同时,经典的regfile只有两个读取端口,再增加更多的端口会带来更高的能耗,并降低主频
这三种转发正好按照从高到低的优先级顺序,即优先考虑最近的指令,从而保证指令一定是最新的
完成转发模块
ex阶段可能会涉及到两个寄存器的读取,因此要分别针对这两个寄存器进行转发判断,这里只分析其中一个。
创建两位二进制信号forward1,根据对应的写后读冲突为其赋值,并指示ex阶段需要选择其中哪个数据
转发模块只输出两个forward信号
2. ex
这个模块需要通过先通过MUX和forward信号选出正确的rs数据
同时,由于立即数计算的原因,alu的第二个操作数还需要根据“是否使用立即数”额外判断一次
最后交给内部的alu阶段进行计算
六、连接
1. include与import
include:直接将对应文件粘贴进来
common.sv文件中定义了一系列简写,mypkg中,我们定义了一些enum枚举类型。对于用到这些自定义类型的文件需要再开头将它们include进来。额外地,对于ex部分还需要引入alu模块的include;对于core需要将所有模块include进来,因为它是顶层连线。
import:在module后面添加,将import的package引入当前作用域,从而可以直接使用里面的名字
2. 顶层模块——core
difftest
它是否进行检查由commit_valid决定,也就是mem_wb的valid_out。当mem_wb将wb数据在第一拍传入regfile时,difftest直接根据这个valid信号进行检查。
为什么不等regfile真的把数据写进去呢?因为下一拍就是下一个指令进入wb了。如果此时difftest再去检测上一条指令对应的寄存器状态,如果检测到错误,就会误报下一条指令出现错误,让查错变得很麻烦。实例化
实例化部分设计大量的同名信号,将它们分级处理比较合适,即:
- pc,if_id,ibus
- decoder,regfile,id_ex
- ex,alu,forward,ex_mem
- mem,mem_wb
- wb,regfile
对于所有级间寄存器,统一采用"{各自名字}_{各自接口名}_(in/out)"的形式进行命名;对于同级信号,只会在内部使用(后续通过级间寄存器进行传递才会造成改名)。这种情况造成的改名额外通过assign语句进行。最终得到分组如下:
pc / if_id / ibus
- pc_enable
- pc_next_pc
- pc_cur_pc
- if_id_valid_in/out
- if_id_pc_in/out
- if_id_inst_in/out
decoder / regfile / id_ex
- decoder_rd_id
- decoder_rs1_id
- decoder_rs2_id
- decoder_imm
- decoder_op
- decoder_use_imm
- decoder_use_w
- decoder_write_regs
- regfile_rs1_num
- regfile_rs2_num
- regfile_gprs
- id_ex_*_in/out
ex / alu / forward / ex_mem
- forward_forward1
- forward_forward2
- ex_alu_result
- ex_mem_*_in/out
mem / mem_wb
- mem_alu_ans
- mem_write_regs_in
- mem_write_reg_id_in
- mem_write_num_out
- mem_write_regs_out
- mem_write_reg_id_out
- mem_wb_*_in/out
wb / regfile
- wb_write_num_in/out
- wb_write_regs_in/out
- wb_write_reg_id_in/out
3. difftest
四个difftest分别起到不同作用:
- DifftestInstrCommit指示当前指令的提交状态
- DifftestArchIntRegState显示寄存器中的值
- DifftestTrapEvent判断指令是否走到指定位置,并通过a0寄存器的值是否为0来判断是否hit good trap