2022 buaa CO P5(Verilog 实现流水线CPU)

2022 buaa CO P5

by psfott

我的CPU整体架构

2022 buaa CO P5(Verilog 实现流水线CPU)_第1张图片

  1. 设计说明:
  • 处理器应支持如下指令集:{ add, sub, ori, lw, sw, beq, lui, jal, jr, nop }。
  • 处理器为五级流水线设计。
  • 总得来说,搭建流水线CPU具体步骤为实现数据通路及控制模块,实现转发、暂停
  1. 工程模块定义:为了实现流水线CPU的功能,我设计了如下模块,分别为F_IFU,D_CMP,D_EXT,D_GRF,D_NPC,D_REG,E_ALU,E_REG,F_IFU,HCU,M_DM,M_REG,MCU,W_REG,以及顶层模块mips.v

工程模块

F_IFU(取指单元)

  • 该模块内包含IM 和 PC,IM的容量为16KB(32bit * 4096字)
信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 异步复位信号
en I 1 使能信号(stall取反)
npc I 32 下一条要被执行指令的地址
F_instr O 32 输出当前执行的指令
pc O 32 当前执行的指令的地址
pc8 O 32 pc+8

D_CMP(B类指令比较单元)

信号名 方向 位宽 描述
rs I 32 输入的第一个数据
rt I 32 输入的第二个数据
judge O 1 判断结果

D_EXT(扩展单元)

信号名 方向 位宽 描述
imm I 16 16位立即数
EXTOP I 1 EXT功能选择信号
D_E32 O 32 扩展结果

D_GRF

  • 内部转发机制:我们将WD的数据实时映射到RD1和RD2端口上以解决数据冲突(详见后文转发部分)
信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 异步复位信号
A1 I 5 地址输入信号,指定32位寄存器中的一个,将其中的数据读入RD1
A2 I 5 地址输入信号,指定32位寄存器中的一个,将其中的数据读入RD2
A3 I 5 地址输入信号,指定32位寄存器中的一个,将其作为待写入的目标
WD I 32 输入待写入的数据
WE I 1 写使能信号
RD1 O 32 输出A1寄存器中的32位数据
RD2 O 32 输出A2寄存器中的32位数据

D_NPC

  • 计算下一指令模块
信号名 方向 位宽 描述
F_PC I 32 F级的指令的地址
D_PC I 32 D级的指令的地址
IMM I 26 beq跳转立即数 & instr_index
RA I 32 储存在寄存器中的地址数据(实现jr)
Br I 3 NPC功能选择信号
judge I 1 B类指令判断结果
npc O 32 下一条要被执行指令的地址
  • 具体实现:
assign npc   =   (Br == `Br_pc4)  ? F_pc + 4 : 
                 (Br == `Br_addr) ? {D_pc[31 : 28], imm26, 2'b0} :
                 (Br == `Br_reg)  ? RA :
                 (Br == `Br_branch && judge) ? D_pc + 4 + {{14{imm26[15]}},imm26[15 : 0],2'b0} :
                 F_pc + 4;

E_ALU

信号名 方向 位宽 描述
srcA I 32 参与ALU计算的第一个值
srcB I 32 参与ALU计算的第二个值
ALUctrl I 4 ALU操作控制信号
Result O 32 运算结果
  • 具体实现:
always@(*) begin
	case(ALUctrl)
		`ADD : Re = srcA + srcB;
		`SUB : Re = srcA - srcB;
		`AND : Re = srcA & srcB;
		`OR  : Re = srcA | srcB;
		`LUI : Re = {srcB[15 : 0],{16{1'b0}}};
		default : Re = 0;
	endcase
end

M_DM

  • DM的容量为12KB(32bit * 3072 words)
信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 异步复位信号
WE I 1 写入使能信号
A I 5 地址输入信号,指向DM的某个存储单元
D I 32 数据输入信号
PC I 32 当前指令地址
RD O 32 输出A指定存储单元中的32位数据

MCU(主控制信号)

  • 在该模块,我对当前D级的指令进行译码,并生成控制信号和T值
信号名 方向 位宽 描述 相关指令
instr I 32 当前执行的指令
rs O 5 D_rs
rt O 5 D_rt
rd O 5 D_rd
imm O 16 16位立即数
imm26 O 26 26位立即数
DMWr O 1 DM写入使能信号 store型指令
extop O 1 扩展单元选择信号
ALUop O 4 ALU功能选择信号
Bsel O 1 ALU-srcB选择信号
WDsel O 2 GRF-WD选择信号
RFWr O 1 GRF写使能信号
A3sel O 2 GRF-A3选择信号
Tuse_rs O 2 输出Tuse_rs的值
Tuse_rt O 2 输出Tuse_rt的值
Tnew_D O 2 输出D级Tnew的值

HCU(冒险控制模块)

  • 在冒险控制中,我采用的是A-T法,通过对传入的“A”(A1,A2,A3)和“T”(Tnew,Tuse)进行分析,判断当前需要进行转发(forward)还是暂停(stall),并通过组合逻辑生成相应控制信号。

  • 注意:

    • 转发优先原则:能用转发解决的冲突不要阻塞
    • 全速转发原则:Tnew和Tuse的设置要合理
  • 转发的实现:

    assign forwardAD = ( D_A1 && (D_A1 == E_A3) && RFWr_E && (WDsel_E == 2'b10) ) ? 2'b01 : // 转发E_pc8 
                       ( D_A1 && (D_A1 == M_A3) && RFWr_M && (WDsel_M == 2'b00) ) ? 2'b10 : // 转发M_AO
                       ( D_A1 && (D_A1 == M_A3) && RFWr_M && (WDsel_M == 2'b10) ) ? 2'b11 : // 转发M_pc8
                       2'b00 ; // 不转发
    assign forwardBD = ( D_A2 && (D_A2 == E_A3) && RFWr_E && (WDsel_E == 2'b10) ) ? 2'b01 : // 转发E_pc8 
                       ( D_A2 && (D_A2 == M_A3) && RFWr_M && (WDsel_M == 2'b00) ) ? 2'b10 : // 转发M_AO
                       ( D_A2 && (D_A2 == M_A3) && RFWr_M && (WDsel_M == 2'b10) ) ? 2'b11 : // 转发M_pc8
                       2'b00 ; // 不转发
    assign forwardAE = ( E_A1 && (E_A1 == M_A3) && RFWr_M && (WDsel_M == 2'b00) ) ? 2'b01 : //转发M_AO
                       ( E_A1 && (E_A1 == M_A3) && RFWr_M && (WDsel_M == 2'b10) ) ? 2'b10 : //转发M_pc8
                       ( E_A1 && (E_A1 == W_A3) && RFWr_W ) ? 2'b11 : //转发W_WD
                       2'b00 ; //不转发
    assign forwardBE = ( E_A2 && (E_A2 == M_A3) && RFWr_M && (WDsel_M == 2'b00) ) ? 2'b01 : //转发M_AO
                       ( E_A2 && (E_A2 == M_A3) && RFWr_M && (WDsel_M == 2'b10) ) ? 2'b10 : //转发M_pc8
                       ( E_A2 && (E_A2 == W_A3) && RFWr_W ) ? 2'b11 : //转发W_WD
                       2'b00 ; //不转发
    assign forwardDM = ( M_A2 && (M_A2 == W_A3) && RFWr_W && DMWr_M ) ? 1'b1 : //转发W_WD
                       1'b0  ; //不转发
  • 阻塞的实现
    wire Stall_rs_E = (Tuse_rs < Tnew_E) && (E_A3 && (E_A3 == D_A1));
    wire Stall_rt_E = (Tuse_rt < Tnew_E) && (E_A3 && (E_A3 == D_A2));
    wire Stall_E = Stall_rs_E || Stall_rt_E;

    wire Stall_rs_M = (Tuse_rs < Tnew_M) && (M_A3 && (M_A3 == D_A1));
    wire Stall_rt_M = (Tuse_rt < Tnew_M) && (M_A3 && (M_A3 == D_A2));
    wire Stall_M = Stall_rs_M || Stall_rt_M;

    assign stall = Stall_E || Stall_M;

D/E/M/W 级流水寄存器

  • 充分理解“流水”的含义,值传递
  • D级寄存器有en使能信号,E级寄存器有clr清除信号(实现阻塞)

冒险处理

  • 采用A—T法实现

转发(forward)

  • 当前面的指令要写某个寄存器但还未写入,但后面的指令需要用到该寄存器的值时,这时候会产生数据冒险,我们首先考虑进行转发。我们假设所有的数据冒险均可通过转发解决。也就是说,当某一指令前进到必须使用某一寄存器的值的流水阶段时,这个寄存器的值一定已经产生,并存储于后续某个流水线寄存器中。
  • 实现方法:我们要清楚哪些模块需要转发后的数据(需求者)和保存着写入值的流水寄存器(供应者)
  • 数据的供应者:
流水级 数据
E E_pc8
M M_AO,M_pc8
W W_AO,W_RD,W_pc8
  • 数据的需求者
接收端口 数据
D_V1 E级,M级
D_V2 E级,M级
E_srcA M级,W级
E_srcBfw M级,W级
M_WD W级
  • ​ 从上表可以看出,W级中的数据没有转发到D级,原因是我们在GRF内实现了内部转发机制。实现方法如下:
assign RD1 = ((A1 == A3) && (A3 != 5'b0) && WE) ? WD : Register[A1]; 
assign RD2 = ((A2 == A3) && (A3 != 5'b0) && WE) ? WD : Register[A2];//内部转发
  • 根据上述条件,我们可以生成上面的5个MUX选择信号,选择信号的输出值应遵循“就近原则”,及最先产生的数据最先被转发。
assign D_V1 = (fwAD == 2'b01) ? E_pc8 : 
              (fwAD == 2'b10) ? M_AO  :
              (fwAD == 2'b11) ? M_pc8 :
              RD1;

assign D_V2 = (fwBD == 2'b01) ? E_pc8 : 
              (fwBD == 2'b10) ? M_AO  :
              (fwBD == 2'b11) ? M_pc8 :
              RD2;
//···

阻塞(stall)

  • ​ 如果转发不能处理数据冒险,在这种情况下,新的数据还未来得及产生。我们只能暂停流水线,等待新的数据产生。为了方便处理,我们仅仅为D级的指令进行暂停处理。
  • 两个概念:
    • Tuse(time to use) :指令进入 D 级后,其后的某个功能部件再经过多少时钟周期就必须要使用寄存器值。
    • Tnew(time to new) :位于 E 级及其后各级的指令,再经过多少周期就能够产生要写入寄存器的结果。
  • Tuse graph
指令 Tuse_rs Tuse_rt
add/sub 1 1
ori/lui 1 X
load 1 X
store 1 2
beq 0 0
jal X X
jr 0 X
  • Tnew graph
指令 Tnew_D Tnew_E Tnew_M Tnew_W
add/sub 2 1 0 0
ori/lui 2 1 0 0
load 3 2 1 0
store X X X X
beq X X X X
jal 0 0 0 0
jr X X X X
  • 然后我们Tnew和Tuse传入HCU中,进行stall信号的计算。

  • 如果Tnew > Tuse,HCU中的stall信号值为1,并进行以下操作:

    • 冻结PC(IFU_en = ~stall)
    • 冻结D寄存器(D_en = ~stall)
    • 清空E寄存器(E_clr = stall)

测试方案:

  • 课下弱测testcode:
341c0000
341d0000
34011010
3c028723
34037856
3c0485ff
34050001
3c06ffff
3407ffff
00220820
00234820
00224022
00e00022
13910003
00000000
10000015
00000000
10220013
00000000
3402000c
00000000
00000000
00000000
0c000c1b
ac410000
1000000b
00220820
00220820
00220820
00220820
ac5f0000
8c410000
00000000
00000000
00000000
00200008
ac5f0000
1000ffff
00000000
  • 测试转发 // problems maybe
341c0000
341d0000
34011010
34041345
8c300000
ad500000

思考题

  1. 我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。
  • 因为b类指令的Tuse = 0,很容易出现T_new > T_use 的情况,阻塞可能增多。
  • 指令序列如下:
ori $a0,$0,0x0101
ori $a1,$0,0xffff
beq $a0,$a1,loop
//···
  1. 因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?
  • 因为需要考虑编译优化,jal的下一条指令是延迟槽中的指令,在jal执行前会被执行。如果回写PC+4的话,当出现“jr $ra”时,将会回到延迟槽,重复执行延迟槽中的指令。因此需要回写PC+8。
  1. 我们要求所有转发数据都来源于流水寄存器而不能是功能部件(如 DM、ALU),请思考为什么?
  • 因为流水寄存器中的储存的数据时前一级已经计算出来的数据,在当前周期内时稳定输出的。而功能部件的输出是有延迟的,如果让这些部件提供数据,有可能再其在回写数据生成前就写入了错误的数据,导致数据波动。
  1. 我们为什么要使用 GPR 内部转发?该如何实现?
  • 因为需要使得W级保存的将要写入得数据及时反馈到GRF的输出端口,从而避免数据冒险。
  • 实现如下:
assign RD1 = ((A1 == A3) && (A3 != 5'b0) && WE) ? WD : Register[A1]; 
assign RD2 = ((A2 == A3) && (A3 != 5'b0) && WE) ? WD : Register[A2];
  1. 我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?
  • 见上文
  1. 在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。
  • 计算指令:只需要更改ALU和MCU,数据通路无需扩展
  • 跳转指令:条件跳转 + 条件写,一般来说在D级就可以判断条件,所以应该在D级生成一个条件信号然后流水。
  • 条件存储:到了M级才知道具体要写入的内容,所以要stall! 具体写法如下:
wire stall_rs_E = (TuseRS < TnewE) && D_A1 && (D_lwso ? D_A1 == 5'd31 : D_A1 == E_A3);

debug心得

  1. 连线问题:由于流水级寄存器输入输出过于冗杂,肉眼debug非常考验人的专注力,所以建议先画出图再按照图连线。
  2. 检查连线时,可以在mips.v中加入 default_nettype none会有意想不到的效果
  3. 仿真波形yyds
  4. 多多与hxd们对拍测试数据,详见讨论区

附录

  1. 清空延迟槽方法:
  • 插入nop
F_instr = (!D_b_jump && D_bgezall) ? 32'd0 : im[pc[13:2] - 12'hc00];
  • 当bonall进入E级后,产生一个清空信号,这个信号会连接到DREG的reset端,然后就把在D级的延迟槽指令变成nop了,完成了清除
  1. 跳转 + 条件写:跳转指令一个好处在于它是在 D 级决定是否跳转的,也就是说在 D 级你可以获得全部的正确信息。所以我们的方案是 D 级生成一个 D_check 信号然后流水它。然后每一级根据这个信号判断写入地址/写入值之类的。
  2. 循环左移的写法:
if(B[4:0] == 5'd0) out = A;
else out = A << B[4:0] | A >> (5'd31 - B[4:0] + 5'd1);
  1. 我的课上题目
  • 计算指令:
    2022 buaa CO P5(Verilog 实现流水线CPU)_第2张图片
    归类到calc_i型计算指令并按照RTL描述更改ALU就好
  • 跳转指令:
    2022 buaa CO P5(Verilog 实现流水线CPU)_第3张图片
    • 条件跳转+条件连接
    • 我的实现方式在CMP模块增加一个b_jump信号,如果满足条件b_jump赋1,并在D级将RFWr赋1.
    • 同类题目:bpnal
  • 存储指令:
    2022 buaa CO P5(Verilog 实现流水线CPU)_第4张图片
    • 注意阻塞和转发的处理
    • 可以理解为附加题,毕竟低空飘过就可以了)

你可能感兴趣的:(2022,buaa,CO,fpga开发,单片机,嵌入式硬件)