从零开始实现一个基于RISC-V的流水线处理器 (完) :流水线中断及数据转发

前言

有一段时间没更了,主要是家里键盘出问题了,在维修过程中我干脆去关注其他项目了,处理器就先放了放,现在键盘修好了,就赶紧回来补上。

整个处理器的编写满打满算一共花了两周时间,最终完成的设计大概是本科毕业论文的水平。这个处理器大约能模拟出处理器核的行为特征,然而实际上要完成一个真正的处理器,难度是要大很多的:不仅要设计处理器核,也要设计总线来考虑处理器与外围设备的交互,提高处理器核心性能的cache等也是必须要加上的。但我们在这里就先不去关注这些内容了,可能以后有空了会加上。

摘要

我们将流水线中断分为两种,一种是需要流水线暂停一个时钟周期的数据依赖冒险,一种是需要清空流水线的控制冒险(分支冒险)。此外,在特殊情况下我们也需要将目前指令计算出的结果转发到下一级或下两级。

在本文中,我们完善了处理器的流水线中断及数据转发机制,使处理器成为了一个真正可以运行我们定义的指令集中的所有指令的单元。

转发机制

相信在前面的文章中大家已经了解了处理器的流水线设计:在处理器中,同一时刻有处于不同指令阶段的多条指令在同时执行。

下图给出了我们设计的处理器的流水线机制。我们可以看到,指令1的操作是$3 = $1 + $2,我们在ID级译码出指令的操作,在EX级算出result = $1 + $2,并将result寄存一个时钟周期,直到WB级才进行写回。那么如果指令2的操作要用到指令1的目标寄存器怎么办?例如,我们可以定义指令2的操作为$4 = $3 - $2,那么,我们将要在指令2的EX阶段使用的$3中的数据应当就是指令1被执行完毕后,向$3中写入的新值了。如果按正常指令顺序执行的话,指令2所取出的值就是错误的旧值,但我们又不能一直等到指令1全部执行完后再对指令2所需的数据进行取值,因此我们必须创建一种解决方案,使每次进入ALU进行运算的都是正确的数据。

于是我们可以创建这样的转发机制:在每条指令的EX级开始时都进行一个检测,看本条指令的源寄存器与上两条指令的目标寄存器是否相同(由下面流水线的图也可以看出,一条指令的数据会影响到接下来的两条数据),如果相同,则把处于MEM / WB级的上一条 / 上两条的指令的数据转发过来,再在本条指令的ALU前做一个MUX,决定最终进入ALU运算的是原始的rs1 / rs2的数据还是转发过来的新数据。
转发系统

转发模块的代码如下所示:

`timescale 1ns / 1ns
//============================================================================================
// Engineer: 
// McEv0y
// Module Name:   forward
// Description:
// 处理器中负责转发的模块,forward_a和forward_b分别是用来表明rs1_data和rs2_data是否应该进行转发的握手信号。
// forward = 2'b00时,不需要进行转发。
// forward = 2'b01时,需要转发处在wb阶段的运算结果。
// forward = 2'b10时,需要转发处在mem阶段的运算结果。
//============================================================================================
module forward
  (
   input                regs_write_ex,
   input                regs_write_mem,
   input                regs_write_wb,
   input      [4 : 0]   rs1_addr_ex,
   input      [4 : 0]   rs2_addr_ex,
   input      [4 : 0]   rd_addr_mem,
   input      [4 : 0]   rd_addr_wb,
   output reg [1 : 0]   forward_a,
   output reg [1 : 0]   forward_b
  );
//-- 判断rs1是否是上两条指令的目标寄存器, mem的优先级大于wb.
//--------------------------------------
  assign forward_a[1] = regs_write_ex && regs_write_mem && (rs1_addr_ex == rd_addr_mem);
  assign forward_a[0] = regs_write_ex && regs_write_wb && !((rs1_addr_ex == rd_addr_mem) && regs_write_mem) && (rs1_addr_ex == rd_addr_wb);

//-- 判断rs2是否是上两条指令的目标寄存器.
//--------------------------------------
  assign forward_b[1] = regs_write_ex && regs_write_mem && (rs2_addr_ex == rd_addr_mem);
  assign forward_b[0] = regs_write_ex && regs_write_wb && !((rs2_addr_ex == rd_addr_mem) && regs_write_mem) && (rs2_addr_ex == rd_addr_wb);

endmodule

数据依赖

上面提到的数据依赖的情况是在执行Load指令以外的I-Type指令和所有的R-Type指令时出现的。如果我们考虑Load指令,又会有怎样的情况呢?

除了Load指令外,其余需要检测是否转发的指令的EX段都可以计算出将要向目标寄存器写入的新数据。但Load指令不同:它的EX段只是为了计算RAM的地址,在MEM段才能真正从RAM中取出将要向目标寄存器写入的寄存器。这时候,如果下一条指令刚好是需要转发数据的指令,那就有问题了,因为如果我们还在指令1的MEM段开始时进行转发,那么转发出的就是地址而非数据,由此会产生错误。

我们的解决方案就是,当且仅当第n条指令是Load指令,且第n + 1条指令的源寄存器与第n条指令的目标寄存器是同一个寄存器时,在流水线中生成一个气泡,气泡的作用是使流水线的运行暂停一个周期,即处于ID段的第n + 1条指令先不进入EX段,处于IF段的第n + 2条指令也先不取指令,等到第n条指令完成MEM段,即把要更新的数据从RAM中取出来之后,下面的操作再正常进行,这样使用正常的转发系统就可以直接把MEM段的数据转发到ID段了。
数据依赖
数据依赖需要增加一个气泡产生模块,这个模块的作用就是检测Load指令与其下条指令之间是否存在数据依赖现象,如存在数据依赖,则产生气泡。

具体代码如下:

`timescale 1ns / 1ns

module dependence_detect
  (
   input            ram_read_ex,
   input  [4 : 0]   rd_addr_ex,
   input  [4 : 0]   rs1_addr_id,
   input  [4 : 0]   rs2_addr_id,
   output           stall,
   output           pc_en
  );
   assign stall = ram_read_ex && ((rd_addr_ex == rs1_addr_id) || (rd_addr_ex == rs2_addr_id));
   assign pc_en = ~stall;
endmodule

流水线冲刷

我们在前面的文章也已经提到了,当指令类型为跳转指令时,其执行的过程为:

IF -> ID -> JUMP

也就是说,在ID的下一个周期处理器就会进行跳转,这就会带来一个问题:在决定跳转的这个时钟上升沿,处理器的ID级也刚好接收到原pc + 4的地址,这样显然是不对的。既然进行了跳转,那么我们的下一条指令肯定是从跳转地址对应的rom中取的,因此我们需要定义一个重置pc地址寄存器的使能信号flush.
flush = j || jr || branch
即只要检测到处理器即将进行跳转,就令flush = 1,这样在下个上升沿到来时pc + 4的地址就不会被送往ID的decoder了。

顶层模块

这样,我们完善了处理器的运行机制,使之成为了一个完整可用的模块。新的顶层模块代码如下。

`timescale 1ns / 1ps
///
// Engineer: 
// McEv0y
// Create Date: 2020/08/11 09:23:16
// Module Name: CPU
///


module CPU
 #(
    parameter DATA_WIDTH = 32
  )
  (
    input clk
  );
//=====================================================================================
//==    模块中所用信号的定义
//=====================================================================================
  wire                    j, jr, branch;
  wire                    stall, pc_en, flush;
  wire [DATA_WIDTH-1 : 0] branch_addr, jr_addr, jump_addr;
  wire [DATA_WIDTH-1 : 0] i_imm_id, jump_offset_id, b_offset_id, store_offset_id;
  wire [4 : 0]            rs1_addr_id, rs2_addr_id, rd_addr_id;
  wire [DATA_WIDTH-1 : 0] oprand_a,
                          oprand_b,
                          oprand_b_temp,
                          alu_result_ex;
  wire [1 : 0]            forward_a,
                          forward_b;
//=====================================================================================
//==    IF
//=====================================================================================
  reg  [DATA_WIDTH-1 : 0] pc_in;
  wire [DATA_WIDTH-1 : 0] pc_out;
  assign branch_addr = pc_out + b_offset_ex;
  assign jr_addr     = rs1_data_ex;
  assign jump_addr   = pc_out + jump_offset_ex;

  always@(*)
  begin
    case({
   j, jr, branch})
      3'b000: pc_in = pc_out + 4;
      3'b001: pc_in = branch_addr;
      3'b010: pc_in = jr_addr;
      3'b100: pc_in = jump_addr;
      default: pc_in = 0;
    endcase
  end
  
  myreg cpu_pc (
  • 18
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值