流水线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 | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
i_inst_rdata | 32 | out | i_inst_addr 对应的 32 位指令 |
m_data_rdata | 32 | out | m_data_addr 对应的 32 位数据 |
i_inst_addr | 32 | out | F级的pc |
m_data_addr | 32 | out | 数据存储器读写地址 |
m_data_wdata | 32 | out | 数据存储器待写入数据 |
m_data_byteen | 4 | out | 写数据存储器的字节使能信号 |
m_inst_addr | 32 | out | M级的pc |
w_grf_we | 1 | out | GRF 写使能信号 |
w_grf_addr | 5 | out | GRF 中待写入寄存器编号 |
w_grf_wdata | 32 | out | GRF 中待写入数据 |
w_inst_addr | 32 | out | W级pc |
mips模块是整个系统的主模块,其主要承担整体架构中各分模块以及cpu与外置指令、数据存储器的线路连接任务,其中对流水线D,E,M,W每一级调用模块Controller来输出控制信号并以此为依据控制转发,并调用模块Blocker处理阻塞情况。
F_PC
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
en | 1 | in | 使能信号 |
next_pc | 32 | in | 下一条执行指令地址 |
pc | 32 | out | 当前执行指令地址 |
F_PC即F级的指令取址器,定位执行指令的地址;由于流水线CPU可能存在的阻塞处理,这里使用en端实现,需阻塞则置为0,暂停更新pc值。
FD_REG
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
flush | 1 | in | 刷新信号,置1则输出全0 |
en | 1 | in | 使能信号 |
F_pc | 32 | in | F级当前执行指令的地址 |
F_instr | 32 | in | F级当前的执行指令 |
D_pc | 32 | out | D级当前需执行指令的地址 |
D_instr | 32 | out | D级当前需执行的指令 |
FD_REG即保存前一周期F级得到的指令及状态并在本周期将其传送到D级的寄存器,其中引入flush即刷新信号也是为了服务于阻塞机制,但在目前的指令集下,其在FD_REG中无作用,将在DE_REG中介绍;需要注意的是,阻塞发生时,该寄存器的en也应置为0。
D_GRF
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
pc | 32 | in | W级指令地址 |
A1 | 5 | in | 读取的寄存器1编号 |
A2 | 5 | in | 读取的寄存器2编号 |
A3 | 5 | in | 需写入的寄存器编号 |
WD | 32 | in | 需写入寄存器的数据 |
RD1 | 32 | out | 从寄存器1读出的数据 |
RD2 | 32 | out | 从寄存器2读出的数据 |
D_GRF即D级的寄存器文件,值得注意的是其中pc、A3和WD来自W级,且可能产生冒险行为,这里采用寄存器内部转发来解决W级的回写与D级读寄存器地址冲突的情况,采用外部转发解决E,M与D级产生的数据冲突;具体操作见冲突处理一节。
D_ext
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
imm16 | 16 | in | 16位立即数输入 |
extop | 1 | in | 扩展方式控制:0:0扩展;1:符号扩展 |
imm32 | 32 | out | 扩展后的32位立即数 |
D_ext即安排在D级的立即数扩展模块
D_cmp
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
rs_data | 32 | in | 从寄存器1读出的数据(可能是转发过来的) |
rt_data | 32 | in | 从寄存器2读出的数据(可能是转发过来的) |
cmpop | 3 | in | 选择比较方式,对应beq、bne等指令 |
jump | 1 | out | 根据指令比较方式和比较结果决定是否跳转,跳转则置1 |
D_cmp即D级的比较器,为了减少判断跳转指令可能带来的流水线上的无效指令,将分支判断提前到D级,那么即使发生跳转,需要作废的指令只有F级,此时若跳转也约定F级指令不作废,即得到延时槽。
D_NPC
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
NPCop | 3 | in | 根据指令选择的下一条指令地址的操作选择信号 |
F_pc | 32 | in | 当前F级的指令地址 |
D_pc | 32 | in | 当前D级的指令地址 |
b_jump | 1 | in | 来自D_cmp的跳转判断信号 |
imm16 | 16 | in | 16位地址偏移量 |
imm26 | 26 | in | 26位伪直接寻址的指令地址 |
rs_data | 32 | in | 从寄存器1读出的数据(可能是转发过来的) |
next_pc | 32 | out | 经判断计算得到的下一条执行指令的地址 |
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 | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
flush | 1 | in | 刷新信号,置1则输出全0 |
en | 1 | in | 使能信号 |
D_pc | 32 | in | D级正在执行的指令地址 |
D_instr | 32 | in | D级正在执行的指令 |
D_rs_data | 32 | in | 从寄存器1读出的数据(可能是转发过来的) |
D_rt_data | 32 | in | 从寄存器2读出的数据(可能是转发过来的) |
D_imm32 | 32 | in | 在D级被扩展得到的32位立即数 |
D_b_jump | 1 | in | 在D级经判断得到的b指令跳转信号 |
E_pc | 32 | out | E级需执行的指令地址 |
E_instr | 32 | out | E级需执行的指令 |
E_rs_data | 32 | out | 传递到E级的寄存器1数据 |
E_rt_data | 32 | out | 传递到E级的寄存器2数据 |
E_imm32 | 32 | out | 传递到E级的32位立即数 |
E_b_jump | 1 | out | 传递到E级的b指令跳转信号 |
DE_REG即保存前一周期D级得到的指令及状态并在本周期将其传送到E级的寄存器,需要注意的是只要处于阻塞状态,该寄存器的flush置1,即在流水线中产生“气泡”,“气泡”随流水线传递,达到等待直至阻塞状态解除的目的。
E_ALU
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
A | 32 | in | 操作数1 |
B | 32 | in | 操作数2 |
ALUCtrl | 4 | in | ALU运算控制信号 |
ALUResult | 32 | out | 运算结果 |
E_ALU即安排在E级的运算单元。
E_MDU
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
A | 32 | in | 操作数1 |
B | 32 | in | 操作数2 |
Start | 1 | in | 乘除运算开始信号 |
Busy | 1 | out | 乘除运算正在进行信号 |
HI | 32 | out | 高位寄存器 |
LO | 32 | out | 低位寄存器 |
E_MDU即安排在E级的乘除相关指令(mult, multu, div, divu, mfhi, mflo, mthi, mtlo
)处理单元。乘除法部件的执行乘法的时间为 5 个时钟周期,执行除法的时间为 10 个时钟周期(包含写入内部的 HI 和 LO 寄存器);通过只能有效 1 个时钟周期的 Start 信号来启动乘除法运算,通过 Busy 输出标志来反映这个延迟。
EM_REG
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
flush | 1 | in | 刷新信号,置1则输出全0 |
en | 1 | in | 使能信号 |
E_pc | 32 | in | E级正在执行的指令地址 |
E_instr | 32 | in | E级正在执行的指令 |
E_ALUResult | 32 | in | E级ALU的运算结果 |
E_MDUResult | 32 | in | E级MDU的运算结果(HI/LO) |
E_rt_data | 32 | in | E级保存的寄存器2数据 |
E_imm32 | 32 | in | E级保存的32位立即数 |
E_b_jump | 1 | in | E级保存的b指令跳转信号 |
M_pc | 32 | out | M级需执行的指令地址 |
M_instr | 32 | out | M级需执行的指令 |
M_ALUResult | 32 | out | 传递到M级的ALU的运算结果 |
M_MDUResult | 32 | out | 传递到M级的MDU的运算结果 |
M_rt_data | 32 | out | 传递到M级的寄存器2数据 |
M_imm32 | 32 | out | 传递到M级的32位立即数 |
M_b_jump | 1 | out | 传递到M级的b指令跳转信号 |
EM_REG即保存前一周期E级得到的指令及状态并在本周期将其传送到M级的寄存器。
M_DM_IN
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
DMop | 3 | in | 存取数据存储器方式选择信号 |
MemAddr | 32 | in | 存取数据存储器地址 |
dataIn | 32 | in | 待处理的写入外置存储器的数据 |
WriteEnable | 1 | in | 写存储器使能信号 |
m_data_byteen | 4 | out | 写数据存储器的字节使能信号 |
m_data_wdata | 32 | out | 写入外置存储器的数据 |
M_DM_IN即安排再M级的外置数据存储器写入数据的预处理器。
M_DM_OUT
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
DMop | 3 | in | 存取数据存储器方式选择信号 |
MemAddr | 32 | in | 存取数据存储器地址 |
m_data_rdata | 32 | in | 从外置存储器读出的数据 |
dataOut | 32 | out | 处理后的读出数据 |
M_DM_OUT即安排再M级的外置数据存储器读出的数据的处理器。
MW_REG
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
clk | 1 | in | 系统时钟信号 |
reset | 1 | in | 复位信号 |
flush | 1 | in | 刷新信号,置1则输出全0 |
en | 1 | in | 使能信号 |
M_pc | 32 | in | M级正在执行的指令地址 |
M_instr | 32 | in | M级正在执行的指令 |
M_ALUResult | 32 | in | M级保存的ALU的运算结果 |
M_MDUResult | 32 | in | M级保存的MDU的运算结果 |
M_dataOut | 32 | in | M级读出的存储器数据 |
M_b_jump | 1 | in | M级保存的b指令跳转信号 |
W_pc | 32 | out | W级需执行的指令地址 |
W_instr | 32 | out | W级需执行的指令 |
W_ALUResult | 32 | out | 传递到W级的ALU的运算结果 |
W_MDUResult | 32 | out | 传递到W级的MDU的运算结果 |
W_dataOut | 32 | out | 传递到W级的存储器读出数据 |
W_b_jump | 1 | out | 传递到W级的b指令跳转信号 |
MW_REG即保存前一周期M级得到的指令及状态并在本周期将其传送到W级的寄存器。
Controller
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
instr | 32 | in | 32位指令 |
b_jump | 1 | in | b指令跳转信号 |
rs | 5 | out | 指令的21-25位,寄存器1编号 |
rt | 5 | out | 指令的16-20位,寄存器2编号 |
rd | 5 | out | 指令的11-15位,寄存器3编号 |
shamt | 5 | out | 指令的6-10位,多用于移位指令的位移量 |
imm16 | 16 | out | 指令的0-15位,16位立即数 |
imm26 | 26 | out | 指令的0-25位,26位立即数 |
ALUCtrl | 4 | out | ALU运算操作选择信号 |
ALU_Asel | 2 | out | ALU操作数1选择信号 |
ALU_Bsel | 2 | out | ALU操作数2选择信号 |
MDUCtrl | 3 | out | MDU运算选择信号 |
MDU_Start | 1 | out | 乘除运算开始信号 |
cmpop | 3 | out | 比较操作选择信号 |
extop | 1 | out | 位拓展操作选择信号 |
NPCop | 3 | out | 指令地址更新操作选择信号 |
DMop | 3 | out | 存储器读/写数据操作选择信号:字/半字/字节 |
DM_WriteEnable | 1 | out | 外置数据存储器写使能信号 |
GRF_WriteEnable | 1 | out | 寄存器文件写使能信号 |
GRF_A3 | 5 | out | 寄存器文件写数据地址 |
WDSel | 2 | out | 寄存器写入数据选择信号 |
load | 1 | out | 读取数据存储器指令识别信号 |
store | 1 | out | 写入数据存储器型指令识别信号 |
cal_r | 1 | out | 寄存器操作计算指令识别信号 |
cal_i | 1 | out | 立即数操作计算指令识别信号 |
shift_s | 1 | out | 固定位移指令识别信号 |
shift_v | 1 | out | 可变位移指令识别信号 |
branch | 1 | out | b型跳转指令识别信号 |
j_reg | 1 | out | 从寄存器获取跳转地址的跳转指令识别信号 |
j_imm | 1 | out | 以立即数为跳转地址的跳转指令识别信号 |
j_link | 1 | out | 跳转并链接指令识别信号 |
mul_div | 1 | out | 乘除指令识别信号 |
mt | 1 | out | 向 HI/LO 寄存器写入指令信号 |
mf | 1 | out | 从 HI/LO 寄存器读出数据写入寄存器文件指令识别信号 |
Controller即通用控制器,在mips模块中,在D,E,M,W每一级被调用,接收在相应级所需的状态信息,达到分布式译码的目的,以但前级的指令和状态为依据,发出控制信号,操作数据通路,控制转发操作;在Blocker模块中,在D,E,M级调用,发出指令识别信号以计算 Tuse 和 Tnew ,为是否阻塞提供依据。
Blocker
管脚名 | 位数 | in/out | 功能解释 |
---|---|---|---|
D_instr | 32 | in | D级正在执行的指令 |
E_instr | 32 | in | E级正在执行的指令 |
M_instr | 32 | in | M级正在执行的指令 |
E_MDU_Start | 1 | in | MDU开始执行乘除指令信号 |
E_MDU_Busy | 1 | in | MDU正在执行乘除指令信号 |
ifBlock | 1 | out | 阻塞操作使能信号,需阻塞置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 >= Tnew 或 E_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指令将其串联,避免了跳转指令地址不合理的情况
- 指令绝对数目多,在长时间内多组指令轮测可覆盖绝大部分情况。