计算机组成原理P5

Cdostan MVP++

流水线CPU设计文档

本次课下要求我们使用verilog来搭建一个支持十条指令的流水线CPU。可以说难度与复杂度较之前几次都上升了不少,但理清思路将CPU运行步骤印在心里后,会发现复杂度也没那么大。

构建思路

流水线CPU的搭建是在单周期CPU之上的,所以其实其整个数据通路和单周期的CPU没有太大的区别,唯一不同的就是单周期一条指令只需一个周期,而我们搭建的流水线CPU需要5个周期,这也要求我们将数据进行流水,每一级在每个周期将相应数据传到下一级。

数据通路

在此首先附上单周期的logisim图与流水线的数据通路图。

  • 单周期CPUlogisim设计图:
    单周期CPUlogisim设计图
  • 流水线数据通路图:

可以看到,若暂时不考虑冒险问题,流水线的数据通路和单周期的数据通路几乎一致,其只是将单周期的数据通路分为五个部级:F、D、E、M、W,相关部级所要做的工作根据名字一目了然。而在每个部级之间,我们加上一个流水寄存器用于传递和保存所要用的数据。什么数据需要传呢?这就要看我们下一级的需求了。但是可以知道的是,pc和相应的instructioncode是必须要一直流水下去的,其他按照需求流水即可,例如在E级还需要将ALU产生的结果等流水下去。

冒险

数据通路看起来是比较简单的,使用verilog搭建起来可能会有一定的复杂度,但我们整体的思路还算清晰。接下来扰乱我们思绪的就应该是关于冒险的处理了。流水线CPU可能会存在结构冒险、控制冒险和数据冒险三种冒险问题,但由于我们的CPU的im和mem并不是一个存储器,因此结构冒险的问题对于我们来说是不存在的,因此主要考虑剩下两种。

而我认为最重要的冒险其实是数据冒险,也就是前面一条指令要写入的寄存器和当前指令要读取的寄存器一样时,这时我们读取的数据可能不是真正的数据。其之所以重要是因为几乎每一条指令都会产生这样的冒险,因此首先来考虑它。

数据冒险

在这里,我采用的是教程里的A-T法来解决此问题。
A指的即是address,T指的即是time,在满足address和time的条件下,我们要做出相应操作来处理冒险。

  • A条件:address相同
  • T条件
    • Tuse:指令位于D级的时候,再经过多少个时钟周期就必须要使用相应的数据
    • Tnew:位于某个流水级的某个指令,它经过多少个时钟周期可以算出结果并且存储到流水级寄存器里
      在满足Tuse > Tnew 且 Tnew = 0时,我们可以将相关数据进行转发,而当Tuse < Tnew时,我们需要让指令在D级阻塞一周期。

基本思路便是如此,满足A-T的条件,我们就将相应数据进行转发,不满足我们就阻塞一周期。而相应的数据也不会太复杂,其实我们仔细思考可以发现,有写入功能的指令他们写入的数据无非就是三种,一种是jal,它会写入下一条指令的地址,一种是ALU计算型,他们回写入ALU的计算结果,一种是lw,会写入MEM的读取值,况且再结合上阻塞会发现,如果我们当前指令是与lw产生冒险,那一定会阻塞到lw流水到M级或W级,而那时便可将MEM读取值转发,因此我们注意将下一条指令的地址以及ALU的结果流水下去即可。
还有一点需要注意的是对于一些不用相关数据的指令例如jr,我们不应把他们的Tuse设为0,而是应该设为一个尽可能大的数,不然会产生无谓的阻塞,使得运行周期加长。

控制冒险

控制冒险即是分支指令(如 beq )的判断结果会影响接下来指令的执行的情况,例如beq需要判断rs和rt寄存器是否相等,而我们在单周期里是在ALU进行判断的,也就是说现在我们将在E级判断,但等判断结果出来后,F级已经又流水了两条指令了,而这时如果判断的结果是真,将会产生错误,为此,我们将beq的判断移至D级,但我们会发现F级仍会往下流水一条指令,所以这时加上延时槽便可解决这样的问题。

控制器

我们可以采用两种译码方式,一种是分布式译码,一种是集中式译码。集中式译码需要在某一级译码完成后将控制信号流水下去,这样流水的信号数量会很大,因此我采用的是分布式译码的方式,也就是在每一级对在该级的指令译码一次产生我所需要的控制信号即可,因此这样我会将控制器实例化4次。在控制器中除了单周期CPU里对应的控制信号以外,还需要加入转发的控制信号,我对Tuse和Tnew的计算也放进了控制器里。

模块搭建

IM

1
2
3
4
5
module IM(
input [13:2] addr,
output [31:0] out
);
reg [31:0] im [0:4095];

FD_reg

1
2
3
4
5
6
7
8
9
module FD_reg(
input clk,
input reset,
input fd_enable,
input [31:0] f_pc,
input [31:0] f_instr,
output reg [31:0] d_pc,
output reg [31:0] d_instr
);

GRF

1
2
3
4
5
6
7
8
9
10
11
12
module GRF(
input [4:0] a1,
input [4:0] a2,
input [4:0] a3,
input [31:0] wd,
input clk,
input reset,
input regwe,
input [31:0] pc,
output [31:0] rd1,
output [31:0] rd2
);

STALL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module Stall(
input [4:0] rs,
input [4:0] rt,
input [4:0] erd,
input [4:0] mrd,
input [4:0] wrd,
input e_regwe,
input m_regwe,
input w_regwe,
input [1:0] rs_tuse,
input [1:0] rt_tuse,
input [1:0] e_tnew,
input [1:0] m_tnew,
input [1:0] w_tnew,
output pcstop,
output stall
);

EXT

1
2
3
4
5
module EXT(
input [5:0] opt,
input [15:0] imm,
output [31:0] outimm
);

DE_reg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module DE_reg(
input clk,
input reset,
input clr,
input [31:0] d_pc,
input [31:0] d_instr,
input [31:0] d_rd1,
input [31:0] d_rd2,
input [31:0] d_imm,
input [4:0] d_grfwr,
output reg [31:0] e_pc,
output reg [31:0] e_instr,
output reg [31:0] e_rd1,
output reg [31:0] e_rd2,
output reg [31:0] e_imm,
output reg [4:0] e_grfwr
);

FWD

1
2
3
4
5
6
7
8
9
10
11
12
module FWD(
input [4:0] rs,
input [4:0] rt,
input [4:0] mrd,
input [4:0] wrd,
input mregwe,
input wregwe,
input [1:0] mtnew,
input [1:0] wtnew,
output [1:0] rs_slt,
output [1:0] rt_slt
);

ALU

1
2
3
4
5
6
7
module ALU(
input [31:0] int_a,
input [31:0] int_b,
input [4:0] aluopt,
output [31:0] out
);

EM_reg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module EM_reg(
input clk,
input reset,
input [31:0] e_pc,
input [31:0] e_instr,
input [4:0] e_grfwr,
input [31:0] e_alu,
input [31:0] e_memwd,
output reg [31:0] m_pc,
output reg [31:0] m_instr,
output reg [4:0] m_grfwr,
output reg [31:0] m_alu,
output reg [31:0] m_memwd
);

MEM

1
2
3
4
5
6
7
8
9
10
module MEM(
input clk,
input reset,
input memwe,
input loadwe,
input [13:2] addr,
input [31:0] wrdata,
input [31:0] pc,
output [31:0] rddata
);

MW_reg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module MW_reg(
input clk,
input reset,
input [31:0] m_pc,
input [31:0] m_instr,
input [4:0] m_grfwr,
input [31:0] m_aludata,
input [31:0] m_memdata,
output reg [31:0] w_pc,
output reg [31:0] w_instr,
output reg [4:0] w_grfwr,
output reg [31:0] w_aludata,
output reg [31:0] w_memdata
);

CONTROLLER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module CONTROLLER(
input [2:0] phase,
input [5:0] opt,
input [5:0] func,
output regwe,
output [1:0] regdst,
output aluslt,
output [4:0] aluopt,
output memwe,
output loadwe,
output [1:0] memreg,
output beq1,
output jal1,
output jr1,
output [1:0] D_rs_Tuse,
output [1:0] D_rt_Tuse,
output [1:0] Tnew
);

测试方案

  1. 基本指令测试(不含jal和jr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ori $1,$0,0x1011
ori $2,$1,0x0111
ori $11,$0,2
lui $3,0
lui $3,0x0111
lui $3,0xffff
ori $4,$3,0xffff
lui $3,0x0111
add $5,$3,$0
add $5,$4,$0
add $5,$3,$1
add $5,$3,$4
add $5,$4,$4
add $5,$5,$5
loop3:add $11,$11,$4
beq $11,$0,loop4
sub $6,$3,$0
sub $6,$4,$0
sub $6,$3,$1
sub $6,$3,$4
ori $7,$0,1
sub $8,$4,$7
sub $6,$4,$8
sub $6,$6,$6
sub $6,$3,$4
loop3:sw $5,4($1)
sw $6,4($2)
nop
lw $9,4($1)
lw $10,4($2)
beq $5,$10,loop1
beq $5,$9,loop2
loop1:sw $10,8($2)
loop2:beq $6,$10,loop3
loop4:sw $9,12($2)
  1. 跳转测试
1
2
3
4
5
6
7
8
9
10
11
ori $1,$0,0x301c
sw $1,4($0)
lw $31,4($0)
loop:add $2,$1,$1
jr $ra
add $3,$2,$0
add $2,$2,$2
add $4,$3,$2
jal loop
ori $5,$4,0
ori $6,$4,0

思考题

我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。

例如以下测试代码:

1
2
3
4
5
6
7
ori $1,$0,0x0011
sw $1,4($0)
lw $2,4($0)
beq $1,$2,loop
add $3,$1,$2
add $4,$1,$2
loop:add $5,$1,$2

可以发现,因为lw的原因,beq在D级需要阻塞两个周期,所以其并非总能提高效率。

因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计

由于延时槽指的是跳转指令发生时,无论如何会进行下一条指令,所以对于jal 等需要将指令地址写入寄存器的指令来说,真正的下一条指令应该是当前pc值+8

我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么?

这样可能会造成数据冲突。例如在E级进行转发的时候,当前的rd1值需要转发alu的计算结果,但这样转发后alu又会进行运算,最后得不到一个稳定的值。

我们为什么要使用 GPR 内部转发?该如何实现?

因为在当指令传入W级后,真正要写入GRF的内部发生的时间应为下一个周期,而这时若不进行内部转发,在下一个周期就会损失掉这个要写入GRF的值,导致错误。
实现方法:assign rd1 = (a1==a3&&regwe&&a1!=0)?wd:grf[a1];

我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?

主要的需求者为D级grf的两个读取数据,alu的两个计算数据以及mem的写入数据。可能来源于EM流水寄存器的alu输出,MW流水寄存器的写入数据,以及每个部级的pc值。

在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。

  • 对于R型指令,因为其操作是grf[rt]<-ALU(grf[rs],grf[rt]),因此我们其实只需对ALU进行扩展即可,其他例如冒险等都与现有的add与sub指令一样
  • 对于I型指令,其操作也更多和ALU相关,还有一些可能与MEM相关,因此可能需要在这两个地方进行扩展,同时可能还需要考虑M级对E级转发数据通路的扩展。
  • 对于跳转型指令,直接跳转应该已经被jr和jal所占,因此其他的挑战指令大多应该都涉及寄存器两个读取值之间的某些运算甚至有涉及MEM的操作,因此还需扩展这两个部件,除此之外,这些跳转可能不能再提前至D级,因此可能要增加阻塞信号,或者如果仍保持再D级判断,那么需要考虑更多的转发信号,对转发数据通路进行一定的修改。

确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。

我的译码方式是分布式译码,译码器的架构是控制信号驱动型,先通过操作码和功能码将指令译出,这是与逻辑,再通过或逻辑通过相应的指令或起来来实现控制信号的译出。这样的架构使得代码量易于压缩,但若控制器出错,相应的错误比较难以查出。

请详细描述你的测试方案及测试数据构造策略

  • 构造策略:针对基本指令,应尽可能将其每种情况考虑到,同时我们还应将不涉及转发和涉及转发的情况都考虑到,不要有该转发没转发或者不该转发转发了的情况产生。而对于跳转指令,除了跳转本身以外,我们也要考虑冒险的情况,涉及转发和不涉及转发的情况都应被考虑。
  • 测试方案:以下采用一个帮我查出bug的典型方案
1
2
3
4
5
6
7
8
9
10
11
12
ori $1,$0,0x3018
sw $1,4($0)
lw $31,4($0)
jr $ra
add $2,$1,$1
ori $3,$2,0
add $3,$2,$2
add $4,$4,$4
ori $4,$0,8
ori $5,$0,8
loop:add $6,$6,$5
beq $4,$6,loop

在本方案里,首先对1号寄存器赋值,再存入内存并将值读出到31号寄存器里,再跳转,因为延时槽的原因,跳转的下一条指令add也会被执行,执行后跳转到下一条add指令,并向下执行,最后在beq指令由于4号寄存器与6号寄存器值相同,会再次跳转,并执行,最终结束程序。在该测试样例中,考虑了很多触及转发的情况,且对于基础指令和跳转指令均有,还考虑了跳转指令为程序最后一条指令(因此帮我查出一些错误)的情况,较为全面。

  • Title: 计算机组成原理P5
  • Author: Cdostan
  • Created at : 2025-02-09 21:10:31
  • Updated at : 2025-02-10 18:18:44
  • Link: https://cdostan.github.io/2025/02/09/计组/计组P5/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments