..
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00
2023-10-06 15:30:42 +08:00

五级流水线(带冒险和异常处理)

项目结构

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-3334不代表任何一个寄存器的编号而是告诉我们的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_outID_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_outOR2_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