《CPU自制入门》笔记 —— ID段理论基础、数据解码器(1)

在完成IF阶段后,流水线的下一个阶段为ID阶段。

在ID阶段中,会实现对指令的解码并生成必要的信号。数据的直通、Load冒险的检测、分支的判断都会在这一阶段进行。

1. 理论基础

1. 数据冒险

所谓的数据冒险是指在流水线运行过程中,由于指令执行所需要的数据还未准备好所造成的冒险。在这种情况下,下一条指令所需要的数据并没有准备好,就会导致下一条指令无法执行,从而导致流水线的堵塞。

因此,为了避免这种情况的产生,我们采用了一种名为数据直通的方法。如图:

在图中,我们假设在流水线中,运算结果的写回是在WB中。而在下一条指令中,在ID阶段就需要上一条指令的结果,如果按照正常流水线的执行就需要将下一条指令的执行延迟多个指令周期,使得流水线效率大幅度降低。而运算结果的产生是在EX阶段,从图中观察可以得到,在EX阶段执行时,下一条指令正好执行到ID阶段,因此,我们就可以在ID和EX阶段增加一条通路,直接将EX阶段产生的结果输送到下一条指令的ID阶段中,这就是数据的直通。

但数据的直通并不是万能的,它也有力不从心的时候。最典型的情况就是Load冒险产生时数据的直通就无法避免阻塞的发生。

所谓的Load冒险是指在流水线执行过程中所用的数据需要使用Load指令从内存中获取的情况。由于内存的读写发生在MEM阶段,因此,下一条指令数据的获取必须在上一条指令的MEM阶段才能执行。而如果按照正常情况,当MEM执行时,下一条指令的EX阶段已经执行过去了,这是必须要避免的。

在这种情况下,实践已经证明Load冒险是无法避免的,但我们可以采用数据直通的方式将MEM中的数据采用直通直接送入下一条指令的ID阶段,使得流水线仅仅阻塞一个周期。在这一个指令周期中,如让其传输无效数据即可。

在流水线中出现一个指令周期无效的情况被称为流水线冒泡。

2. 控制冒险

所谓控制冒险是指无法确定下一条指令而导致的冒险。

这种情况一般发生在跳转指令的时候。由于在这一条指令执行完前跳转分支无法确定,导致下一条指令无法执行,就会导致控制冒险。

控制冒险根本上无法避免。但我们可以将分支判断尽可能安排在流水线的前端,这样可以减少因分支指令造成的无效指令的数目,例如如果我们将分支在ID阶段判断出来,就可以只产生一条无效指令,但如果在MEM阶段判断出来,就会产生三条无效指令,降低流水线效率。

在上面已经提到,控制冒险会产生流水线冒泡,冒泡会导致流水线效率下降,但这又无法避免,我们应该怎样做呢?

我们通常会采用延迟分支的方法。所谓的延迟分支是指分支指令之后的指令总是在分支目标指令之前执行。而分支指令之后的位置称为延迟槽,在函数调用的时候,这个位置经常用于对行参的传递。 这样就会消除一个冒泡,提高了流水线的效率。

在这里插入图片描述

2. 指令解码器

在了解了ID阶段需要的基础知识后,我们开始ID阶段的构建工作。首先进行指令解码器的编写。

指令解码器的作用是在ID阶段对指令分解出各个指令字段、生成地址、数据以及控制信号。而上文提到的数据直通、Load冒险检查、分支判定等也在这个模块中执行。

module定义阶段的代码如下:

`include "cpu.h"
`include "isa.h"
`include "stddef.h"
`include "nettype.h"
`include "global_config.h"

module decoder (

	//IF阶段寄存器
	output reg [`WordAddrBus] if_pc,    //程序计数器
	output reg [`WordDataBus] if_insn,    //指令寄存器
	input reg if_en,   //流水线数据有效标志位
	
	//GPR接口
	input wire [`WordDataBus] gpr_rd_data_0,    //读取数据0
	input wire [`WordDataBus] gpr_rd_data_1,    //读取数据1
	output wire [`RegAddrBus] gpr_rd_addr_0,    //读取地址0
	output wire [`RegAddrBus] gpr_rd_addr_1,    //读取地址1
	
	//来自ID阶段的数据直通
	input wire id_en,    //流水线数据有效标志位
	input wire [`RegAddrBus] id_dst_addr,    //写入地址
	input wire id_gpr_we_,    //写入有效
	input wire [`MemOpBus] id_mem_op,    //内存操作
	
	//来自EX阶段的数据直通
	input wire ex_en,    //流水线数据有效
	input wire [`RegAddrBus] ex_dst_addr,    //写入地址
	input wire ex_gpr_we_,    //写入有效
	input wire [`WordDataBus] wx_fwd_data,    //数据直通
	
	//来自MEM阶段的数据直通
	input wire [`WordDataBus] mem_fwd_data,
	
	//控制寄存器接口
	input wire exe_mode,    //执行模式
	input wire [`WordAddrBus] creg_rd_data,    //读取的数据
	input wire [`RegAddrBus] creg_rd_addr,    //读取的地址
	
	//解码结果
	output reg	[`AluOpBus]		 alu_op,    //ALU操作码
	output reg	[`WordDataBus]	 alu_in_0,    //ALU输入0
	output reg	[`WordDataBus]	 alu_in_1,    //ALU输入1
	output reg	[`WordAddrBus]	 br_addr,    //分支地址
	output reg	br_taken,    //分支成立
	output reg	br_flag,    //分支标志位
	output reg	[`MemOpBus]	mem_op,    //内存操作
	output wire	[`WordDataBus]	mem_wr_data,    //内存写入数据
	output reg	[`CtrlOpBus]	ctrl_op,    //控制操作
	output reg	[`RegAddrBus]	dst_addr,    //通用寄存器写入数据
	output reg	gpr_we_,    //通用寄存器写入有效
	output reg	[`IsaExpBus]	exp_code,    //异常代码
	output reg	ld_hazard    //Load冒险
	
	
);

在模块中,我们首先实现的是内部信号的生成以及输出赋值。代码如下:

	/* 指令字段进行分解操作 */
	wire [`IsaOpBus] op = if_insn[`IsaOpLoc];    //分离操作码
	wire [`RegAddrBus] ra_addr = if_insn[`IsaRaAddrLoc];    //分离寄存器Ra地址
	wire [`RegAddrBus] rb_addr = if_insn[`IsaRbAddrLoc];    //分离寄存器Rb地址
	wire [`RegAddrBus] rc_addr = if_insn[`IsaRcAddrLoc];    //分离寄存器Rc地址
	wire [`IsaImmBus] imm = if_insn[`IsaImmLoc];    //分离立即数数据
	
	/* 对立即数进行操作 */ 
	//符号扩充
	wire [`WordDataBus] imm_s = {{`ISA_EXT_W{imm[`ISA_IMMMSB]}}, imm};
	//0扩充
	wire [`WordDataBus] imm_u = {{`ISA_EXT_W{1'b0}}, imm};
	
	/* 寄存器读取地址 */
	assign gpr_rd_addr_0 = ra_addr;    //通用寄存器读取地址0
	assign gpr_rd_addr_1 = rb_addr;    //通用寄存器读取地址1
	assign creg_rd_addr = ra_addr;    //控制寄存器读取地址
	
	/* 通用寄存器的读取数据 */ 
	reg [`WordDataBus] ra_data;    //无符号Ra
	wire signed [`WordDataBus] s_ra_data = $signed(ra_data);    //有符号Ra
	reg [`WordDataBus] rb_data;    //无符号Rb
	wire signed [`WordDataBus] s_rb_data = $signed(rb_data);    //有符号Rb
	assign mem_wr_data = rb_data;    //内存写入数据
	
	/* 地址操作 */
	wire [`WordAddrBus] ret_addr = if_pc + 1'b1;    //返回地址
	wire [`WordAddrBus] br_target = if_pc + imm_s[`WORD_ADDR_MSB:0];    //分支目标地址
	wire [`WordAddrBus] jr_target = ra_data[`WordAddrLoc];    //跳转目标地址

在这一部分中,首先我们对指令的各个字段进行分解,将指令操作码、寄存器地址、立即数等分离出来,以助于后面的操作。

在之后,我们假设存在立即数,由于我们CPU在运算过程使用的数据字长为32位,而我们的立即数仅为16位,这就需要我们对其进行扩充,如果是无符号数,则在前面补零,否则补符号位。但为了减少判断带来的损失,我们直接将两种补法都进行一遍。

之后部分为将寄存器地址进行输出。

通用寄存器的读取数据部分主要是定义数据以及各个数据之间的关系,其中具体数据由后面部分读取出来。

在本部分中,分为无符号和有符号两种数据,其中$signed()作用是将无符号数转换为有符号数。在Verilog中,reg、wire中定义的变量默认存储的是无符号数,需要始终这个函数进行转换。

最后部分用于各项地址生成。

其中,返回的地址由于延迟分支的存在,实际返回的地址应该是分支指令的下下条指令,而在IF阶段,PC中的值已经加一,则实际上还需要将PC中的值再加一。

在分支目标地址产生中,跳转目标相对于PC的偏移地址存储在指令的立即数部分,而我们地址为30位,立即数为32位,我们需要截取[29:0]位进行分支目标地址的生成。

而如果是跳转指令(如JMP)等,一般跳转的目标地址会保存在Ra中。而我们的地址使用的是字编址,因此需要使用Ra内数据的高30位地址。

在分支目标地址产生中,跳转目标相对于PC的偏移地址存储在指令的立即数部分,而我们地址为30位,立即数为32位,我们需要截取[29:0]位进行分支目标地址的生成。

而如果是跳转指令(如JMP)等,一般跳转的目标地址会保存在Ra中。而我们的地址使用的是字编址,因此需要使用Ra内数据的高30位地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值