五级流水线(带冒险和异常处理)
项目结构
multistage_pipeline
|-> data: IM和DM读入的地方
|-> figure: 设计图纸,我高兴时可能会写写使用说明
|-> Python: 一些加速生产的脚本
|-> mips_code: 用来测试的mips汇编源代码
|-> sim: 放激励文件的文件夹
|-> src
|-> Controller: 控制器
|-> DataPath: 数据通路
|-> Hazard: 旁路模块与冒险检测模块
|-> Pipe: 流水线寄存器
|-> Utils: 其余组件
|-> myCpu.v: 顶层模块
|-> README.md: Current
|-> .gitignore 上传文件过滤列表
设计图纸(2021.5.17更新)
日志
2021.5.17
从前两次的设计中,体会到一点:一个正确的,完备的思路对于一个不小的项目来说是多么的重要。因此,这次为了轻松愉悦的开发过程,我花了许久(6个小时左右)设计了最终带转发,冒险和异常检测的五级流水线图纸。并且吸取了部分DUAN学长的FPGA项目习惯。
祝成功!
2021.5.18
在实现乘除法指令(MULT,DIV,MULTU,DIVU
)时,我发现了一个问题,那就是乘除法指令的输出是一个63位的数字,虽然实际上乘除法只是改变专用寄存器的值,但是我对专用寄存器的设计使得我每个时钟周期只能改变$hi
和$lo
中的某一个。我的电路无法在一个时钟周期内一次性改变$hi
和$lo
。总得满足我的乘除法指令吧?怎么办呢?我的做法比较简单粗暴,我为ALU
增加了一个输出接口prod
,这个接口值和MIPS32指令手册上描述乘法除法中的prod的含义一样,为了让它能够成功抵达regfile
,并成功写入,我做出了如下的改变:
ALU
增加新的输出接口值prod
,当然,相应的后续流水线寄存器中也会添加暂存prod的接口。- 选择目的操作数寄存器编号的
mux1
增加一个选项:34,不同于0-33,34不代表任何一个寄存器的编号,而是告诉我们的regfile,你要拆分并读入流水线存入的prod值。 regfile
增加一个输入接口MEM_WB_prod
,代表接受最后一个流水线寄存器的存储的prod值。然后在regfile
内部,我增加了写寄存器的逻辑:增加一个34号,如果接受到的目的操作数编号(在我的设计中为MEM_WB_mux1_out
)为34,则将prod拆成两块送给$hi
和$lo
。
是否有更好的解决方案呢?
2021.5.26
果然,在实现57条指令的过程中,我发现了一个问题,由于我对专用寄存器$hi
和$lo
的是走的专线,所以导致这两个家伙没有进入我的数据冒险系统中。也就是说,如果我的上一条指令改变了专用寄存器的值,而下一条指令又需要使用专用寄存器的值,那么就会发生错误,因为此时下一条指令拿到的$hi
或者$lo
并不是最新的。为了解决这个问题,我采用了简单粗暴的方法:为EX阶段的保存专用寄存器的线(ID_EX_low_out
和ID_EX_high_out
)增加旁路。需要注意的是,我的垃圾设计的缺点这个时候就体现出来了,因为存储最新的专用寄存器值得地方可以是专用寄存器专用线,也可以是prod,所以嘛。。。在写旁路时旁路单元输出旁路信号的逻辑会有一点点小的改变,旁路选择的选项也从原本的3个(EX_MEM阶段的源操作数值、MEM_WB阶段的源操作数值、原本的值,也就是ID_EX阶段源操作数的值)变为5个(增加了EX_MEM阶段的prod和MEM_WB阶段的prod),因此旁路单元输出的旁路信号的位数从原本的2位变成3位。
2021.5.26
中间事情真的很多,所以只能断断续续地更新了。在我的想法下,今天完成了57条指令的全数据通路和控制器的连接,接下来就只剩调试了。有时间我得将已经被我改得面目全非的设计图纸的visio原文件修改同步更新一下了。
2021.5.30
开始进行整体测试,今天先测试所有的R型指令,遇到了pc启动的问题,我将reset设置为了上升沿,并且将pc.v
修改如下:
// initial
always @(posedge reset) begin
pc_out <= initial_addr;
end
always @(posedge clock && reset == 1) begin
if (!OR1_out) // OR1_out represents stall or not
pc_out <= npc_out;
end
也就是将初始化和持续更新分开来。
除此之外,用来用来软堵塞pc的OR1_out
和OR2_out
一开始都是x,会使得pc在启动时不更新,所以需要对两个冒险模块初始化其输出信号,使得一开始输出的都是不堵塞的信号。
2021.5.31
5月的最后一天,我已经完成了所有R型指令的测试、旁路测试和冒险测试。debug过程中发现一个问题:和之前一样,尽量别写形如posedge clock or posedge reset
,请把初始化的always块另外写,因为我这边默认是reset上升沿触发模块的初始化操作,但是如果你将reset信号与别的信号混在一起写,就会导致模块永远处于更新状态,对于我的regfile
,这意味着,每当后续的上升沿来临时,寄存器堆中所有的寄存器都会清零。
第二个值得记载的bug就是有关branch类型指令的问题,在pro1和pro2中的branch信号中,我最终的目标地址是通过branch当前的相对位移>>2
和基地址相加得到的,但是pro3在我的设计中,branch在ID阶段得到的pc_add_out
是当前IF阶段的pc_add_out
,而不是随着当前这条branch指令得到的。因此在使用branch跳转时使用的pc_add_out
必须减四才能正确。
第三个值得记载的bug
对于跳转指令的堵塞,有一个很隐蔽的bug,在控制冒险时,当我们为了branch指令的操作数而软阻塞ID阶段和IF阶段的指令时,由于使用的是延迟槽的机制,虽然我们阻塞了ID和IF,但是判断是否冲刷的模块还是在正常工作。所以如果堵塞的branch指令是生效,是需要跳转的,那么在堵塞的第一个周期,IF_ID流水线寄存器中保存的ID阶段的指令就会被冲刷成空指令,这会使得第二个堵塞周期结束,电路回归正常时,branch会去解析空指令,从而使得pc变成0(这根据你提供的空指令的后16位决定,我使用的是addi $t0, $zero, 0
,其后16位为0).所以有必要控制堵塞IF_ID流水线寄存器的信号和冲刷IF_ID流水线寄存器的信号的优先级:当且仅当IF_ID流水线寄存器不被堵塞时,IF_ID才能执行冲刷的操作。根据这样的认识,我们可以只修改代码逻辑而不添加额外的信号得修复这个bug:
always @(posedge clock)
begin
if (OR4_out)
IF_ID_im_out = NOP; // use NOP to flush and the pc_add_out won't be used later, so we don't care about pc_add_out there
else if (!OR2_out) // update iff IF_ID_Write
begin
IF_ID_im_out = im_out;
IF_ID_pc_add_out = pc_add_out;
end
end
更改之后
always @(posedge clock)
begin
if (!OR2_out) // update iff IF_ID_Write
begin
if (OR4_out)
IF_ID_im_out = NOP; // use NOP to flush and the pc_add_out won't be used later, so we don't care about pc_add_out there
else
IF_ID_im_out = im_out;
IF_ID_pc_add_out = pc_add_out;
end
end