[ 2537 ns] Error!!! reference: PC = 0xbfc408b0, wb_rf_wnum = 0x10, wb_rf_wdata = 0x00000001 mycpu : PC = 0xbfc408b0, wb_rf_wnum = 0x10, wb_rf_wdata = 0xxxxxxxxx
直接运行后出现如上报错,因为该任务是让使用阻塞技术解决流水线冲突,所以猜测是因为发生了写后读的冲突,所以我们要让需要结果的指令在译码流水级一直等待,直到产生结果的指令将结果写入到通用寄存器堆,才可以进入到下一级的执行流水级中。
首先捋一下整体思路,为了解决上面出现的问题,我们需要判断处于流水线不同阶段的指令是否存在会引发冲突的“写后读”相关关系。具体描述为:处于译码流水级的指令具有来自非0号寄存器的源操作数,那么这些源操作数中任何一个的寄存器号与当前时刻处于执行级、访存级或写回级的指令的目的操作数的寄存器号(非0号)相同,则表明处于译码级的指令与执行级、访存级或写回级的指令存在会引发冲突的“写后读”相关关系。
接下来详细介绍实现过程:
首先,为了实现处于译码级指令中来自非0号寄存器的源操作数与执行级、访存级、写回级指令的目的操作数的寄存器号(非0号)进行比较,我们需要在ID_stage.V的顶层模块中添加下面四个输入端口:
module id_stage(
input [ 4:0] EXE_wdest, // EXE级要写回寄存器堆的目标地址号
input [ 4:0] MEM_wdest, // MEM级要写回寄存器堆的目标地址号
input [ 4:0] WB_wdest, // WB级要写回寄存器堆的目标地址号
input IF_over //对于分支指令,需要该信号
)
有了输入端口还需要有输出端口才能把各级流水线的写回寄存器堆的目的地址号给传输进来,所以分别在执行级、访存级、写回级添加的代码如下:
首先是在执行级(EXE_stage.v)
//在执行级顶层模块添加输出端口,便于将值传递到译码级
module exe_stage(
output [4:0] EXE_wdest // EXE级要写回寄存器堆的目标地址号
);
//然后添加EXE_wdest的代码逻辑
assign EXE_wdest = es_dest & {5{es_valid}};
//es_dest是执行级写寄存器堆的目标地址号,为了保证寄存器号的值有效所以要& es_vaild(执行级有效信号)
访存级(MEM_stage.v)
//在访存级顶层模块添加输出端口,便于将值传递到译码级
module mem_stage(
output [ 4:0] MEM_wdest // MEM级要写回寄存器堆的目标地址号
);
//然后添加MEM_wdest的代码逻辑
assign MEM_wdest = ms_dest & {5{ms_valid}};
写回级(WB_stage.v)
//在写回级顶层模块添加输出端口,便于将值传递到译码级
module wb_stage(
output [ 4:0] WB_wdest // WB级要写回寄存器堆的目标地址号
);
//然后添加WB_wdest的代码逻辑
assign WB_wdest = ws_dest & {5{ws_valid}};
经过上面的努力现在执行级、访存级、写回级流水线的写回寄存器堆的目的地址号以及可以传输到译码级了,下面就是在译码级实现源操作数的寄存器号与目的操作数的寄存器号进行比较,并生成控制逻辑将流水线阻塞。
在上面我们已经在ID_stage.V的顶层模块中添加了 EXE_wdest、MEM_wdest、WB_wdest、 IF_over四个输入端口,现在我们来实现译码阶段的代码逻辑,在ID_stage.v中添加下面代码:
assign inst_imm_zero = inst_lui;
//阻塞
//依据源寄存器号分类
wire inst_imm_zero; //立即数0扩展,即不从寄存器堆读rt的数据
wire inst_no_rt; //指令rt域非0,且不是从寄存器堆读rt的数据
assign inst_no_rt = inst_addiu | inst_sltu
| load_op | inst_imm_zero | inst_jal ;
·
wire rs_wait;
wire rt_wait;
assign rs_wait = (rs!=5'd0)
& ((rs==EXE_wdest) | (rs==MEM_wdest) | (rs==WB_wdest));
assign rt_wait = ~inst_no_rt & (rt!=5'd0)
& ((rt==EXE_wdest) | (rt==MEM_wdest) | (rt==WB_wdest));
wire inst_jbr; //所有分支跳转指令,即没有寄存器的目的操作数
assign inst_jbr = inst_jal | inst_beq | inst_bne |inst_jr ;
//ds_ready_go描述当前拍状态,值为1表示数据在ds级已经处理完成,可以传递到后一级流水。值为0表示还没有执行完成。
//所以当我们判断出来有写后读冲突后要将这条指令阻塞在译码流水级,即ds_ready_go的值应置0。
assign ds_ready_go = ds_valid & ~rt_wait & ~rs_wait & (~inst_jbr | IF_over);
//对于分支跳转指令,只有在IF执行完成后,才可以算ID完成;
//否则,ID级先完成了,而IF还在取指令,则next_pc不能锁存到PC里去,
//那么等IF完成,next_pc能锁存到PC里去时,jbr_bus上的数据已变成无效,
//导致分支跳转失败
//(~inst_jbr | IF_over)即是(~inst_jbr | (inst_jbr & IF_over))
下面需要实现IF_over的控制逻辑,因为我们在ID_stage.v定义的是输入端口input IF_over,所以我们要在取指模块(IF_stage.v)中定义IF_over的输出端口并实现IF_over的控制逻辑:
//在取指级定义IF_over的输出端口便于将值传递到ID_stage
module if_stage(
output reg IF_over // IF模块执行完成
);
//添加IF_over的控制逻辑
//-----{IF执行完成}begin
//由于指令rom为同步读写的,
//取数据时,有一拍延时
//即发地址的下一拍时钟才能得到对应的指令
//故取指模块需要两拍时间
//故每次PC刷新,IF_over都要置0
//然后将fs_valid锁存一拍即是IF_over信号
//与sourcecode代码中不一样的地方是将if (~reset || fs_allowin) 改成了if (reset || fs_allowin),因为sourcecode代码中好像是低电平有效,而我们这个好像是高电平有效
always @(posedge clk)
begin
if (reset || fs_allowin) //fs_allowin值为1表示可以接受上级流水传来的数据,值为0表示不能接受
begin
IF_over <= 1'b0;
end
else
begin
IF_over <= fs_valid;
end
end
//如果指令rom为异步读的,则fs_valid即是IF_over信号,
//即取指一拍完成
//-----{IF执行完成}end
至此我们的主要代码逻辑已经添加完毕了,不过我们在各级流水线的顶层模块中都添加了输入/输出变量,千万不要忘记在CPU的顶层模块中按定义顺序声明它们,下面打开mycpu_top.v添加如下代码:
//ID与EXE、MEM、WB交互
wire [ 4:0] EXE_wdest;
wire [ 4:0] MEM_wdest;
wire [ 4:0] WB_wdest;
// IF模块执行完成
wire IF_over;
在IF_stage模块根据定义的顺序添加调用:
if_stage if_stage(
.IF_over (IF_over)
);
在ID_stage模块根据定义的顺序添加调用:
// ID stage
id_stage id_stage(
.IF_over (IF_over)
);
在EXE_stage模块根据定义的顺序添加调用:
exe_stage exe_stage(
.EXE_wdest (EXE_wdest)
);
在MEM_stage模块根据定义的顺序添加调用:
mem_stage mem_stage(
.MEM_wdest (MEM_wdest)
);
在WB_stage模块根据定义的顺序添加调用:
wb_stage wb_stage(
.WB_wdest (WB_wdest)
);
至此完成时间任务二。