2022北航计算机组成原理 Project 6

流水线CPU(存储器外置)设计文档


设计草稿及模块安排

整体模块架构

mips

D_Controller
E_Controller
M_Controller
W_Controller

Blocker

D_Controller
E_Controller
M_Controller

F_PC
FD_REG
D_GRF
D_ext
D_cmp
D_NPC
DE_REG
E_ALU
EM_REG
M_DM_IN
M_DM_OUT
MW_REG


模块安排

mips

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
i_inst_rdata32outi_inst_addr 对应的 32 位指令
m_data_rdata32outm_data_addr 对应的 32 位数据
i_inst_addr32outF级的pc
m_data_addr32out数据存储器读写地址
m_data_wdata32out数据存储器待写入数据
m_data_byteen4out写数据存储器的字节使能信号
m_inst_addr32outM级的pc
w_grf_we1outGRF 写使能信号
w_grf_addr5outGRF 中待写入寄存器编号
w_grf_wdata32outGRF 中待写入数据
w_inst_addr32outW级pc

mips模块是整个系统的主模块,其主要承担整体架构中各分模块以及cpu与外置指令、数据存储器的线路连接任务,其中对流水线D,E,M,W每一级调用模块Controller来输出控制信号并以此为依据控制转发,并调用模块Blocker处理阻塞情况。

F_PC

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
en1in使能信号
next_pc32in下一条执行指令地址
pc32out当前执行指令地址

F_PC即F级的指令取址器,定位执行指令的地址;由于流水线CPU可能存在的阻塞处理,这里使用en端实现,需阻塞则置为0,暂停更新pc值

FD_REG

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
flush1in刷新信号,置1则输出全0
en1in使能信号
F_pc32inF级当前执行指令的地址
F_instr32inF级当前的执行指令
D_pc32outD级当前需执行指令的地址
D_instr32outD级当前需执行的指令

FD_REG即保存前一周期F级得到的指令及状态并在本周期将其传送到D级的寄存器,其中引入flush即刷新信号也是为了服务于阻塞机制,但在目前的指令集下,其在FD_REG中无作用,将在DE_REG中介绍;需要注意的是,阻塞发生时,该寄存器的en也应置为0

D_GRF

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
pc32inW级指令地址
A15in读取的寄存器1编号
A25in读取的寄存器2编号
A35in需写入的寄存器编号
WD32in需写入寄存器的数据
RD132out从寄存器1读出的数据
RD232out从寄存器2读出的数据

D_GRF即D级的寄存器文件,值得注意的是其中pc、A3和WD来自W级,且可能产生冒险行为,这里采用寄存器内部转发来解决W级的回写与D级读寄存器地址冲突的情况,采用外部转发解决E,M与D级产生的数据冲突;具体操作见冲突处理一节。

D_ext

管脚名位数in/out功能解释
imm1616in16位立即数输入
extop1in扩展方式控制:0:0扩展;1:符号扩展
imm3232out扩展后的32位立即数

D_ext即安排在D级的立即数扩展模块

D_cmp

管脚名位数in/out功能解释
rs_data32in从寄存器1读出的数据(可能是转发过来的)
rt_data32in从寄存器2读出的数据(可能是转发过来的)
cmpop3in选择比较方式,对应beq、bne等指令
jump1out根据指令比较方式和比较结果决定是否跳转,跳转则置1

D_cmp即D级的比较器,为了减少判断跳转指令可能带来的流水线上的无效指令,将分支判断提前到D级,那么即使发生跳转,需要作废的指令只有F级,此时若跳转也约定F级指令不作废,即得到延时槽

D_NPC

管脚名位数in/out功能解释
NPCop3in根据指令选择的下一条指令地址的操作选择信号
F_pc32in当前F级的指令地址
D_pc32in当前D级的指令地址
b_jump1in来自D_cmp的跳转判断信号
imm1616in16位地址偏移量
imm2626in26位伪直接寻址的指令地址
rs_data32in从寄存器1读出的数据(可能是转发过来的)
next_pc32out经判断计算得到的下一条执行指令的地址

D_NPC即D级的指令更新器,值得注意的是若处理b这一类指令满足条件应在跳转至D_pc + 4 + {{14{imm16[15]}},imm16,2'b00},而若不需要跳转则下一条指令地址为F_pc+4。值得注意的一点是若imm16 = 0则由于延时槽的存在且D_pc+4 = F_pc该跳转指令的下一条指令会被执行两次

DE_REG

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
flush1in刷新信号,置1则输出全0
en1in使能信号
D_pc32inD级正在执行的指令地址
D_instr32inD级正在执行的指令
D_rs_data32in从寄存器1读出的数据(可能是转发过来的)
D_rt_data32in从寄存器2读出的数据(可能是转发过来的)
D_imm3232in在D级被扩展得到的32位立即数
D_b_jump1in在D级经判断得到的b指令跳转信号
E_pc32outE级需执行的指令地址
E_instr32outE级需执行的指令
E_rs_data32out传递到E级的寄存器1数据
E_rt_data32out传递到E级的寄存器2数据
E_imm3232out传递到E级的32位立即数
E_b_jump1out传递到E级的b指令跳转信号

DE_REG即保存前一周期D级得到的指令及状态并在本周期将其传送到E级的寄存器,需要注意的是只要处于阻塞状态,该寄存器的flush置1,即在流水线中产生“气泡”,“气泡”随流水线传递,达到等待直至阻塞状态解除的目的。

E_ALU

管脚名位数in/out功能解释
A32in操作数1
B32in操作数2
ALUCtrl4inALU运算控制信号
ALUResult32out运算结果

E_ALU即安排在E级的运算单元。

E_MDU

管脚名位数in/out功能解释
A32in操作数1
B32in操作数2
Start1in乘除运算开始信号
Busy1out乘除运算正在进行信号
HI32out高位寄存器
LO32out低位寄存器

E_MDU即安排在E级的乘除相关指令(mult, multu, div, divu, mfhi, mflo, mthi, mtlo)处理单元。乘除法部件的执行乘法的时间为 5 个时钟周期,执行除法的时间为 10 个时钟周期(包含写入内部的 HI 和 LO 寄存器);通过只能有效 1 个时钟周期的 Start 信号来启动乘除法运算,通过 Busy 输出标志来反映这个延迟。

EM_REG

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
flush1in刷新信号,置1则输出全0
en1in使能信号
E_pc32inE级正在执行的指令地址
E_instr32inE级正在执行的指令
E_ALUResult32inE级ALU的运算结果
E_MDUResult32inE级MDU的运算结果(HI/LO)
E_rt_data32inE级保存的寄存器2数据
E_imm3232inE级保存的32位立即数
E_b_jump1inE级保存的b指令跳转信号
M_pc32outM级需执行的指令地址
M_instr32outM级需执行的指令
M_ALUResult32out传递到M级的ALU的运算结果
M_MDUResult32out传递到M级的MDU的运算结果
M_rt_data32out传递到M级的寄存器2数据
M_imm3232out传递到M级的32位立即数
M_b_jump1out传递到M级的b指令跳转信号

EM_REG即保存前一周期E级得到的指令及状态并在本周期将其传送到M级的寄存器。

M_DM_IN

管脚名位数in/out功能解释
DMop3in存取数据存储器方式选择信号
MemAddr32in存取数据存储器地址
dataIn32in待处理的写入外置存储器的数据
WriteEnable1in写存储器使能信号
m_data_byteen4out写数据存储器的字节使能信号
m_data_wdata32out写入外置存储器的数据

M_DM_IN即安排再M级的外置数据存储器写入数据的预处理器。

M_DM_OUT

管脚名位数in/out功能解释
DMop3in存取数据存储器方式选择信号
MemAddr32in存取数据存储器地址
m_data_rdata32in从外置存储器读出的数据
dataOut32out处理后的读出数据

M_DM_OUT即安排再M级的外置数据存储器读出的数据的处理器。

MW_REG

管脚名位数in/out功能解释
clk1in系统时钟信号
reset1in复位信号
flush1in刷新信号,置1则输出全0
en1in使能信号
M_pc32inM级正在执行的指令地址
M_instr32inM级正在执行的指令
M_ALUResult32inM级保存的ALU的运算结果
M_MDUResult32inM级保存的MDU的运算结果
M_dataOut32inM级读出的存储器数据
M_b_jump1inM级保存的b指令跳转信号
W_pc32outW级需执行的指令地址
W_instr32outW级需执行的指令
W_ALUResult32out传递到W级的ALU的运算结果
W_MDUResult32out传递到W级的MDU的运算结果
W_dataOut32out传递到W级的存储器读出数据
W_b_jump1out传递到W级的b指令跳转信号

MW_REG即保存前一周期M级得到的指令及状态并在本周期将其传送到W级的寄存器。

Controller

管脚名位数in/out功能解释
instr32in32位指令
b_jump1inb指令跳转信号
rs5out指令的21-25位,寄存器1编号
rt5out指令的16-20位,寄存器2编号
rd5out指令的11-15位,寄存器3编号
shamt5out指令的6-10位,多用于移位指令的位移量
imm1616out指令的0-15位,16位立即数
imm2626out指令的0-25位,26位立即数
ALUCtrl4outALU运算操作选择信号
ALU_Asel2outALU操作数1选择信号
ALU_Bsel2outALU操作数2选择信号
MDUCtrl3outMDU运算选择信号
MDU_Start1out乘除运算开始信号
cmpop3out比较操作选择信号
extop1out位拓展操作选择信号
NPCop3out指令地址更新操作选择信号
DMop3out存储器读/写数据操作选择信号:字/半字/字节
DM_WriteEnable1out外置数据存储器写使能信号
GRF_WriteEnable1out寄存器文件写使能信号
GRF_A35out寄存器文件写数据地址
WDSel2out寄存器写入数据选择信号
load1out读取数据存储器指令识别信号
store1out写入数据存储器型指令识别信号
cal_r1out寄存器操作计算指令识别信号
cal_i1out立即数操作计算指令识别信号
shift_s1out固定位移指令识别信号
shift_v1out可变位移指令识别信号
branch1outb型跳转指令识别信号
j_reg1out从寄存器获取跳转地址的跳转指令识别信号
j_imm1out以立即数为跳转地址的跳转指令识别信号
j_link1out跳转并链接指令识别信号
mul_div1out乘除指令识别信号
mt1out向 HI/LO 寄存器写入指令信号
mf1out从 HI/LO 寄存器读出数据写入寄存器文件指令识别信号

Controller即通用控制器,在mips模块中,在D,E,M,W每一级被调用,接收在相应级所需的状态信息,达到分布式译码的目的,以但前级的指令和状态为依据,发出控制信号,操作数据通路,控制转发操作;在Blocker模块中,在D,E,M级调用,发出指令识别信号以计算 Tuse 和 Tnew ,为是否阻塞提供依据。

Blocker

管脚名位数in/out功能解释
D_instr32inD级正在执行的指令
E_instr32inE级正在执行的指令
M_instr32inM级正在执行的指令
E_MDU_Start1inMDU开始执行乘除指令信号
E_MDU_Busy1inMDU正在执行乘除指令信号
ifBlock1out阻塞操作使能信号,需阻塞置1

Blocker即阻塞控制器,根据Controller的译码结果,计算D_Tuse_rs,D_Tuse_rt,E_Tnew,M_Tnew(由于当前指令集W_Tnew恒为0故先不作计算),再结合寄存器读写信息使用AT模型控制输出的阻塞信号。


冲突处理

在流水线CPU中,由于多条指令同时存在于流水线上,且在同一周期内执行不同的指令操作,这可能引发由于硬件资源重叠和指令间依赖性而导致的冲突问题;当前指令集下冲突有以下2种可能:

  • 寄存器文件中的寄存器被同时读写
  • 后面指令在需要使用数据时,前面供给的数据还没有存入寄存器堆

本节主要讨论阻塞和转发处理冲突的情况判断和实现方式。


阻塞操作

阻塞,顾名思义使流水线停在某一指令,需等待某种条件解除阻塞状态。

何时阻塞
后面指令(记为B)在需要使用数据时,前面指令(记为A)供给的数据还没有产生并写入流水级寄存器,这时转发无源头 (转发的源头都是流水级寄存器存储的数据,故不能认为数据产生后可被立即转发) ,唯一的方法是让B等待,直到A在流水线某一级产生所需数据时解除,再考虑转发或直接使用。
这里采用Tuse–Tnew模型判断。

  • Tuse:某指令位于 D 级的时候,再经过多少个时钟周期就必须要使用相应的数据。
  • Tnew:位于某个流水级的某个指令,它经过多少个时钟周期可以算出结果并且存储到流水级寄存器里。

具体各指令的Tuse,Tnew参见文件夹内表格Tuse&&Tnew。

由此,可以得到结论 Tuse < Tnew 且读写地址重叠(均不为$0) 时必须进行相应阻塞操作。

加入乘除指令后,由于乘除槽的存在,MDU在进行运算时,后续的乘除相关指令应被阻塞在D级,故需要额外为此添加一个阻塞信号。

当前指令集需阻塞的情况代码表示如下:

    wire E_ifBlock_rs = (E_GRF_A3 == D_rs && D_rs != 0) && (D_Tuse_rs < E_Tnew);
    wire E_ifBlock_rt = (E_GRF_A3 == D_rt && D_rt != 0) && (D_Tuse_rt < E_Tnew);
    wire M_ifBlock_rs = (M_GRF_A3 == D_rs && D_rs != 0) && (D_Tuse_rs < M_Tnew);
    wire M_ifBlock_rt = (M_GRF_A3 == D_rt && D_rt != 0) && (D_Tuse_rt < M_Tnew);
    wire E_ifBlock_MDU = (D_mul_div | D_mf | D_mt) && (E_MDU_Busy | E_MDU_Start);

    assign ifBlock = E_ifBlock_rs | E_ifBlock_rt | M_ifBlock_rs | M_ifBlock_rt | E_ifBlock_MDU;

如何阻塞
自此,我们得到了阻塞信号ifBlock
以此为依据操作阻塞情况的数据通路:

  • 将F_IFU和FD_REG的使能信号(en)置为0,不再更新并发送新的指令信号,达到使进入流水线的指令滞留在D级的目的。
  • 将DE_REG的刷新信号(flush)置为1,使向E级发送的指令为nop,即产生“气泡”填充流水线;注意,仅需在此寄存器刷新,因为只要处于阻塞状态,该寄存器不断产生“气泡”,而这些“气泡”随时钟周期向后移动填充流水线各级。
  • 对于不同指令,当 Tuse >= TnewE_MDU_Busy = 0, E_MDU_Start = 0时解除阻塞状态,使能信号置为1,刷新信号置为0,开始考虑转发,继续流水。

具体代码实现如下:

    assign PC_en = !ifBlock;

    assign FD_REG_en = !ifBlock;
    assign DE_REG_en = 1;
    assign EM_REG_en = 1;
    assign MW_REG_en = 1;

    assign FD_REG_flush = 0;
    assign DE_REG_flush = ifBlock;
    assign EM_REG_flush = 0;
    assign MW_REG_flush = 0;

转发操作

转发,即将先进入流水线的指令产生的数据根据条件发送给后进入的指令。

何时转发
前面指令供给的数据,而后面指令在需要使用数据时,前面供给的数据已经产生且写入流水级寄存器但还没有存入寄存器堆,导致后面的指令在GRF中取不到正确的值,故当两个流水级出现读写寄存器的重叠时,(在无需阻塞或阻塞完成时)应考虑转发。

如何转发
在当前指令级下,仅存在:

  • W向D级转发(寄存器内自转发)
  • E,M向D级转发
  • M,W向E级转发
  • W向M级转发
    具体转发关系见文件夹下表格 hazard_and_relocate

具体代码实现如下:

    //寄存器内自转发
    assign RD1 = (A1==0) ? 0 : 
                 (A1==A3 && A1!=0) ? WD :
                 regFile[A1];

    assign RD2 = (A2==0) ? 0 : 
                 (A2==A3 && A2!=0) ? WD :
                 regFile[A2];

    //向D级转发
    assign D_Forward_rs_data = (D_rs == 0) ? 0 :
                               (D_rs == E_GRF_A3) ? E_WD : 
                               (D_rs == M_GRF_A3) ? M_WD :
                                D_rs_data;
    
    assign D_Forward_rt_data = (D_rt == 0) ? 0 :
                               (D_rt == E_GRF_A3) ? E_WD : 
                               (D_rt == M_GRF_A3) ? M_WD :
                                D_rt_data;

    //向E级转发
    assign E_Forward_rs_data = (E_rs == 0) ? 0 :
                               (E_rs == M_GRF_A3) ? M_WD :
                               (E_rs == W_GRF_A3) ? W_WD :
                               E_rs_data;

    assign E_Forward_rt_data = (E_rt == 0) ? 0 :
                               (E_rt == M_GRF_A3) ? M_WD :
                               (E_rt == W_GRF_A3) ? W_WD :
                               E_rt_data;

    //向M级转发
    assign M_Forward_rt_data = (M_rt == 0) ? 0 :
                               (M_rt == W_GRF_A3) ? W_WD :
                               M_rt_data;

值得注意的是,这种转发方式的正确性是由阻塞机制转发优先级决定的。
所谓优先级即向每一级转发时,依次沿流水线检索此级的下级,满足条件即转发,若都无需转发,则采用本级读出的寄存器值,这在代码中有所体现。
但可能存在这样两个问题
1.如需要向D级转发某数据,此数据在E级产生但未写入流水级寄存器,直至下个时钟上升沿才写入EM_REG(M级),则按优先级优先转发了DE_REG(E级)保存的数据,这是否会导致错误?
:实际上不会,由于阻塞机制的存在,当数据在E级产生但未写入流水级寄存器时,流水线被阻塞,指令停滞,此时转发的数据也起不到作用,待到下个时钟上升沿才写入EM_REG(M级),转发来自M级的数据会直接将错误值覆盖,阻塞状态解除,流水线正常执行。

2.如果转发到的寄存器在此指令期间不被读(可以不转发)将一个值存入其中会不会有影响?
:不会,若该寄存器不被读,则其要么被写,要么不参与本指令的执行,那么对于第一种情况,该指令写入时会将原本转发的值覆盖;对于第二种情况,该寄存器在流水级中的表现实际是相当于提前被写入了(实际上写入还是要到转发的源头指令的W级)。


测试方案

自动随机生成测试程序

#include <cstdio>
#include <algorithm>
#include <queue>
#include <map>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <set>
#include <unordered_map>
#include <vector>
#include <ctime>
typedef long long ll;
using namespace std;
unsigned int grf[32] = {0};
int reg[] = {0, 1, 2, 3, 31, 1, 2, 3, 31, 1, 2, 3, 31, 1, 2, 3, 1};
int hi, lo;
int dm[3075];
#define R reg[rand() % 16]
#define R_nz reg[rand() % 16+1]
#define I ((rand() + rand()) % 40) * 4
#define B (rand() % 650)
#define TYPE rand()%10 + 1

void Div(int rs, int rt)
{
	printf("div $%d,$%d\n", rs, rt);
}

void divu(int rs, int rt)
{
	printf("divu $%d,$%d\n", rs, rt);
}

void mult(int rs, int rt)
{
	printf("mult $%d,$%d\n", rs, rt);
}

void multu(int rs, int rt)
{
	printf("multu $%d,$%d\n", rs, rt);
}

void mfhi(int rs)
{
	printf("mfhi $%d\n", rs);
}

void mflo(int rs)
{
	printf("mflo $%d\n", rs);
}

void mthi(int rs)
{
	printf("mthi $%d\n", rs);
}

void mtlo(int rs)
{
	printf("mtlo $%d\n", rs);
}

void addu(int rs, int rt, int rd)
{
	printf("addu $%d,$%d,$%d\n", rd, rt, rs);
	if (rd)
		grf[rd] = grf[rs] + grf[rt];
}
void subu(int rs, int rt, int rd)
{
	printf("subu $%d,$%d,$%d\n", rd, rt, rs);
	if (rd)
		grf[rd] = grf[rs] - grf[rt];
}
void ori(int rs, int rt, int imm)
{
	printf("ori $%d,$%d,%d\n", rt, rs, imm);
	if (rt)
		grf[rt] = grf[rs] | imm;
}
void lui(int rs, int rt, int imm)
{
	printf("lui $%d,%d\n", rs, imm);
	if (rs)
		grf[rs] = 1u * imm << 16;
}
void lw(int rs, int rt)
{
	int imm = I;
    subu(rs, rs, rs);
	printf("lw $%d,%d($%d)\n", rt, imm, rs);
	grf[rt] = dm[imm / 4];
}
void sw(int rs, int rt)
{
	int imm = I;
    subu(rs, rs, rs);
	printf("sw $%d,%d($%d)\n", rt, imm, rs);
	dm[imm / 4] = grf[rt];
}
int jump[1010];
void beq(int rs, int rt, int casenum)
{
	int jaddr = B;
	while (jump[jaddr])
		jaddr = B;
	printf("beq $%d,$%d,endsubtest%d\n", rs, rt, casenum);
    printf("nop\n");
}

int main() {
    srand(time(NULL));
    freopen("mips_code.asm", "w", stdout);
    printf("ori $1, $0, %d\n", I);
    //printf("lui $1, %d\n", I);
    printf("ori $2, $0, %d\n", I);
    //printf("lui $2, %d\n", I);
    printf("ori $3, $0, %d\n", I);
    //printf("lui $3, %d\n", I);
    printf("ori $31, $0, %d\n", I);

    int T = 15;
    for(int i = 1; i <= T; i++) {
        addu(4, R, R);
        printf("jal subtest%d\n", i);
        printf("nop\n");
        printf("back%d:\n", i);
    }

    printf("\n");
    printf("endtest:\n");
	printf("beq $0, $0, endtest\nnop\n\n"); 

    for(int i = 1; i <= T; i++) {
        printf("subtest%d:\n", i);
        int last_k;
        for(int j = 1; j <= 15; j++) {
            grf[0] = 0;
            int k = TYPE;
            if(k == 1 || k == 4 || k == 5) {
                int r = rand()%4 + 1;
                if(r == 1) addu(R, R, R);
                else if(r == 2) subu(R, R, R);
                else if(r == 3) ori(R, R, I);
                //else if(r == 4) lui(R, R, I);
            } else if(k == 2 || k == 6) {
                int r = rand()%2 + 1;
                if(r == 1) lw(R%4, R);
                else if(r == 2) sw(R%4, R);
            } else if((k == 3 || k == 7) && (last_k != 3 || last_k != 7)) {
                beq(R, R, i);
            } else if(k == 8 || k == 9 || k == 10) {
            	int r = rand()%5 + 1;
            	if(r == 1) mult(R, R);
                else if(r == 2) multu(R, R);
                else if(r == 3) Div(R_nz, R_nz);
                else if(r == 4) divu(R_nz, R_nz);
                else if(r == 5) mfhi(R_nz);
                else if(r == 6) mflo(R_nz);
                else if(r == 7) mthi(R_nz);
                else if(r == 8) mtlo(R_nz);
			}
            last_k = k;
        }
        printf("endsubtest%d:\n", i);
        printf("la $ra, back%d\n", i);
        printf("jr $ra\n");
        printf("nop\n");
        printf("\n\n");
    }

    return 0;
}


思考题解答

1、为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
答:
乘除法的运算时间远大于其他运算,若将其整合进ALU,则需要在一个周期内支持包括乘除运算在内的所有运算,整个CPU的时钟周期大大延长;将其独立出来可使用多个时钟周期进行乘除运算。
独立的HI,LO寄存器便于存放可能产生的64位数据或商和余数,简化了CPU的数据通路,便于并行指令。

2、真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。
答:
乘法:首先CPU会初始化三个通用寄存器用来存放被乘数,乘数,部分积的二进制数,部分积寄存器初始化为0,然后在判断乘数寄存器的低位是低电平还是高电平(0/1):如果为0则将乘数寄存器右移一位,同时将部分积寄存器也右移一位,在位移时遵循计算机位移规则,乘数寄存器低位溢出的一位丢弃,部分积寄存器低位溢出的一位填充到乘数寄存器的高位,同时部分积寄存器高位补0,如果为1则将部分积寄存器加上被乘数寄存器,再进行移位操作。 当所有乘数位处理完成后部分积寄存器做高位乘数寄存器做低位就是最终乘法结果。

除法:首先CPU会初始化三个寄存器,用来存放被除数,除数,部分商。余数(被除数与除数比较的结果)放到被除数的有效高位上。CPU做除法时和做乘法时是相反的,乘法是右移,除法是左移,乘法做的是加法,除法做的是减法。 首先CPU会把被除数bit位与除数bit位对齐,然后在让对齐的被除数与除数比较(双符号位判断)。 这里说一下什么是双符号位判断: 比如01-10=11(前面的1是符号位) 1-2=-1 计算机通过符号位和后一位的bit位来判断大于和小于,那么01-10=11 就说明01小于10,如果得数为01就代表大于,如果得数为00代表等于。 如果得数大于或等于则将比较的结果放到被除数的有效高位上然后在商寄存器上商:1 并向后多看一位 (上商就是将商的最低位左移1位腾出商寄存器最低位上新的商) 如果得数小于则上商:0 并向后多看一位 然后循环做以上操作当所有的被除数都处理完后,商做结果被除数里面的值就是余数。

3、请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?
答:
由于乘除槽的存在,MDU在进行运算时,后续的乘除相关指令应被阻塞在D级,故需要额外为此添加一个阻塞信号。

    wire E_ifBlock_MDU = (D_mul_div | D_mf | D_mt) && (E_MDU_Busy | E_MDU_Start);

在MDU中设置周期计数变量 cnt ,乘除运算开始则进行计数,通过对cnt的值的判断来判定MDU的工作状态,具体实现如下:

            if(cnt == 0)begin
                if(Start)begin
                    Busy <= 1;
                    
                    // 根据指令类型设定cnt 并执行相应的运算操作
                    case (MDUCtrl)
                        `MDUCtrl_mult:begin
                            cnt <= 5;
                            {hi,lo} <= $signed(A)*$signed(B); 
                        end
                        
                        `MDUCtrl_multu:begin
                            cnt <= 5;
                            {hi,lo} <= A*B;
                        end

                        `MDUCtrl_div:begin
                            cnt <= 10;
                            lo <= $signed(A)/$signed(B);
                            hi <= $signed(A)%$signed(B);  
                        end

                        `MDUCtrl_divu:begin
                            cnt <= 10;
                            lo <= A/B;
                            hi <= A%B;
                        end
                    endcase
                end
                else
                    cnt <= 0; 
            end
            else if(cnt == 1) begin
                HI <= hi;   
                LO <= lo;       //将运算结果写入HI/LO
                Busy <= 0;
                cnt <= 0;   
            end
            else begin
                cnt <= cnt-1;   //更新cnt
            end

4、请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)
答:
以 mips_tb 中这段对数据存储器的写入处理为例:

    always @(*) begin
        fixed_wdata = data[m_data_addr >> 2];
        fixed_addr = m_data_addr & 32'hfffffffc;
        if (m_data_byteen[3]) fixed_wdata[31:24] = m_data_wdata[31:24];
        if (m_data_byteen[2]) fixed_wdata[23:16] = m_data_wdata[23:16];
        if (m_data_byteen[1]) fixed_wdata[15: 8] = m_data_wdata[15: 8];
        if (m_data_byteen[0]) fixed_wdata[7 : 0] = m_data_wdata[7 : 0];
    end

可见字节使能信号处理写指令实现十分得清晰,避免了大量的位拼接和分条件写入的情况,对于按字、半字、字节写入的实现通用,统一性好。

5、请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
答:
不是,是一字,然后再依据字节使能信号做处理。处理大量单字节数据,如字符串中的字符时按字节读和按字节写的效率会高于按字读和按字写。

6、为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
答:

  • 指令分类
    assign load = lw | lb | lbu | lh | lhu;
    assign store = sw | sb | sh;
    assign cal_r = add | sub | addu | subu | _and | _or | _xor | _nor | 
                   sll | srl | sra | sllv | srlv | srav | slt | sltu;
    assign cal_i = lui | ori | addi | addiu | andi | xori | slti | sltiu;
    assign shift_s = sll | srl | sra;
    assign shift_v = sllv | srlv | srav;
    assign branch = beq | bne | blez | bgtz | bltz | bgez;
    assign j_imm = j | jal;
    assign j_link = jal | jalr;
    assign j_reg = jr | jalr;
    assign mul_div = mult | multu | div | divu;
    assign mt = mthi | mtlo;
    assign mf = mfhi | mflo;

在Controller模块中对指令进行特性划分以简化译码操作。

  • 解耦合
    将数据通路的阻塞,转发分离;将数据通路中零散的选择信号规整到mips主模块中。降低模块间的耦合度,详见整体模块架构。

7、在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
答:
数据冲突情况主要沿用P5的情况分析,因为做了指令的归类,故未做太大的修改。需要注意的是乘除指令的加入:
在Blocker中需将乘除指令的Tuse,Tnew纳入考虑;
在mips中需要添加MDUResult(HI/LO)对写入寄存器以及转发的影响。
解决方法即扩展P5的阻塞和转发模块。
乘除指令相关的测试:

//(1) busy
ori $t0,11
ori $t1,12
multu $t0,$t1
nop
mflo $t1
mfhi $t2
mtlo $t2
mthi $t1


//(2)start
ori $t0,11
ori $t1,12
divu $t0,$t1
mflo $t1
mfhi $t2
mtlo $t2
mthi $t1


//(3)测试乘除法
ori $t0,2
ori $t1,-3
mult $t0,$t1
mfhi $a0
mflo $a1
multu $t0,$t1
mthi $t0
mfhi $a2
mflo $a3
sw $a2,0($0)
sb $a3,2($0)
ori $t0,4
ori $t1,5
mtlo $t0
mult $t0,$t1
mfhi $a0
mflo $a1
multu $t0,$t1
mfhi $a2
mflo $a3
sh $a2,2($0)
sb $a3,13($0)
ori $t0,-3
ori $t1,-5
mult $t0,$t1
mfhi $a0
mflo $a1
multu $t0,$t1
mfhi $a2
mflo $a3
sw $a2,16($0)
sb $a3,15($0)
ori $t0,25
ori $t1,-5
div $t0,$t1
mfhi $a0
mflo $a1
divu $t0,$t1
mfhi $a2
mflo $a3
mthi $a3
sw $a2,4($0)
sw $a3,8($0)
ori $t0,2
ori $t1,5
div $t0,$t1
mfhi $a0
mflo $a1
divu $t0,$t1
mthi $a1
mtlo $a2
mfhi $a2
mflo $a3
sb $a2,1($0)
sw $a3,12($0)
ori $t0,-999
ori $t1,-5
div $t0,$t1
mfhi $a0
mthi $a0
mflo $a1
mtlo $a0
divu $t0,$t1
mfhi $a2
mflo $a3
sw $a2,0($0)
sb $a3,4($0)

8、如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
答:
生成随机样例的同时使用了特殊的策略:

  • 限制使用寄存器,仅使用$0,$1,$2,$3,$31,以提高数据冲突的频率。
  • 将test分为几个subtest,用jal,beq指令将其串联,避免了跳转指令地址不合理的情况
  • 指令绝对数目多,在长时间内多组指令轮测可覆盖绝大部分情况。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vanthon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值