系列文章目录
第一篇 【学习e200处理器日志】万事开头难—取指操作-CSDN博客
文章目录
目录
前言
本系列是为了记录在学习《手把手教你设计CPU——RISC-V处理器》的过程中的一些新的以及思考,同时把一些内容进行了简化便于理解,如果小伙伴对处理器感兴趣的话务必拜读一下,能够让你对处理器架构有一个大体的了解。
一、译码操作
取指后的流水线是“译码”。因指令信息编码在有限长度指令字(16 位或 32 位)中,需译码提取信息,常见信息包括:
指令所需读取的操作数寄存器索引。例如指令ADD R1,R2,R3(将 R2 和 R3 的值相加,结果存入 R1)中,源寄存器索引是 R2 和 R3 的编号(如寄存器编号2和3)。
需写回的寄存器索引。例如在ADD R1,R2,R3中,目标寄存器索引是 R1 的编号(如寄存器编号1)。
指令类型及操作等其他信息 。如算术逻辑指令(ADD, SUB)、分支指令(BEQ, BNE)、加载/存储指令(LW, SW)等。
二、执行操作
在经典五级流水线中,完成“译码”并从通用寄存器组读取操作数后的下一级流水线是“执行”。“执行”是将指令按具体操作类型发送给相应运算单元执行。常见运算单元有:算术逻辑运算单元(ALU)、整数乘除单元、浮点运算单元,还有特殊运算单元。
三、发射与派遣
3.1 发射派遣的概念
执行阶段需解决指令发射给运算单元执行及写回的顺序问题,涉及“派遣”和“发射”两个概念:
派遣指的是将要执行的指令发送给不同的运算单元,但是如果遇到需要多周期运行的运算单元时,需要去进行排队,这就是“发射”的意义,从排队到开始执行这个阶段就叫做发射。
根据每个周期发射指令个数,可分为“单发射”(每个周期只能发射一条指令)和“多发射”(每个周期能发射多条指令,如“双发射”“三发射”“四发射” )处理器。
3.2 发射派遣的类型
这也衍生出很多流派,根据指令发射、执行、写回的先后次序来定义:顺序发射、顺序执行、顺序写回; 顺序发射、乱序执行、顺序写回; 顺序发射、乱序执行、乱序写回以及顺序派遣、乱序发射、乱序执行、乱序写回。
很显然最后一种是最复杂的,其原理就是顺序将指令发送给不同的运算单元的等待队列,然后根据那一条指令先解决数据依赖性(也就是相关性,后面会说,反正就是符合要求)便可以先发送到执行单元进行执行,然后写会的时候也是谁先完成谁先写会。其他的顺序指的就是需要等待其他先输入的长周期指令运行完才执行下一步操作。
3.3 乱序操作原理
不同处理器有不同的乱序写回实现方式:
部分处理器配备重排序缓存(ROB):运算单元执行完后,结果先写回ROB,再由ROB按顺序写回通用寄存器组(Regfile),相当于一个中转站重新排队,这是典型乱序写回,性能好,但存在ROB面积大、数据腾挪两次导致动态功耗增加的问题。
有的处理器使用统一物理寄存器组:动态管理逻辑寄存器组映射关系,运算单元执行完后结果乱序写回物理寄存器组,数据仅腾挪一次,更省电,但控制更复杂,相当于设计了一个名单,名单的顺序是写入的顺序,但是谁先完成谁先在名单上打勾,操作起来更为复杂 。
部分处理器无ROB和统一物理寄存器组也支持乱序写回:运算单元执行完后,若与其他运算单元指令无数据相关性,可直接写回Regfile ,这种就不多说了。
四、冲突
蜂鸟 E200 执行阶段的重要职能是维护并解决流水线冲突,包括资源冲突和数据冲突(如 WAR、RAW 等数据相关性)。资源冲突常出现在指令派给不同执行单元时,例如除法单元执行指令需数十个周期,后续除法指令需等前一指令完成释放该单元,否则会出现冲突。
而数据冲突主要是由于数据相关性造成的,首先需要注意的概念就是处理器中有两种指令,一种是单周期指令,可以很快交付,一种是长指令,需要多周期才能交付。
因此,在处理器运行的过程中,需要检查是否和之前派遣过但是没有交付的指令存在数据相关性,比如这条指令需要没交付长指令的运算结果。这种冲突有三种:
WAR(Write-After-Read)相关性
在蜂鸟E200微架构中,指令按顺序派遣和按顺序写回。因此写入的操作肯定不会在派遣之前 ,所以不会产生WAR相关性导致的数据冲突。
RAW(Read-After-Write)相关性
对于正在派遣且处于流水线第二级的指令,如果之前派遣的是长指令(需多个周期写回结果),正在派遣的指令则有可能产生与前序长指令的RAW相关性。
比如指令A先对某个寄存器进行写操作,指令B后续要读取该寄存器的值,如果A是长指令还未完成写回,B就有可能产生RAW相关性冲突。
WAW(Write-After-Write)相关性
同样对于处于流水线第二级正在派遣的指令,假设之前派遣的单周期执行指令(也处于流水线第二级写回)已经完成执行并写回Regfile,那么正在派遣的指令不会产生和前序单周期指令的WAW相关性造成的数据冲突。如果前一个指令是长指令,依然会引起该相关性。
假设指令A和指令B都要对同一个寄存器进行写操作,若A先完成写回,B再进行写回就不会因为WAW相关性而产生数据冲突。
五、RISC-V的执行特点
为了简化处理器结构,实现低功耗小面积,其硬件实现有以下几个特点:
规整的指令编码格式,其主要表现在指令的索引等性质特点都在固定的位数,是的译码操作得到简化。
优雅的16位指令,其每一个16位指令都有对应的32位指令,因此在执行的时候无需关注起是否是16位还是32位数。
精简的指令个数,该指令集数目非常简洁,一共只有40多个,简化执行单元的复杂度。
整数指令都是两操作数,这样可以简化操作数读取和数据相关性检测部分的硬件设计。
六、E200处理器执行的实现
蜂鸟E200是两级流水线架构,其“译码”“执行”“交付”和“写回”功能均处于流水线的第二级 ,由EXU单元完成。
蜂鸟E200的EXU单元的工作流程如下:首先将IFU通过IR寄存器发送给EXU的指令进行译码和派遣,然后通过译码出的操作数寄存器索引(Index)读取Regfile,同时维护指令的数据相关性,最后根据分析将指令派遣(Dispatch)给不同的运算单元执行。后续的操作还有指令交付以及将指令运算的结果写回Regfile。
4.1 译码Decode
译码模块主要就是对IR寄存器中的指令进行译码,其代码主要是以组合逻辑构成,下面是其编译后的输出,包括该指令的操作数索引,结果寄存器索引,以及使用的立即数的值等信息。
下面是其输入输出端口:
端口类型 | 端口名称 | 位宽 | 描述 |
输入待解析数据 | i_instr | 输入 | 待译码的指令 |
i_pc | 输入 | 当前指令的程序计数器值 | |
i_prdt_taken | 输入 | 分支预测是否被采用的信号 | |
i_misalgn | 输入 | 取指时的地址未对齐信号 | |
i_buserr | 输入 | 取指时的总线错误信号,意味着 | |
i_muldiv_b2b | 输入 | 乘法/除法的连续操作信号 | |
dbg_mode | 输入 | 调试模式信号 | |
解析数据 | dec_rs1x0 | 输出 | 指示源寄存器1是否为x0的信号 |
dec_rs2x0 | 输出 | 指示源寄存器2是否为x0的信号 | |
dec_rs1en | 输出 | 源寄存器1使能信号 | |
dec_rs2en | 输出 | 源寄存器2使能信号 | |
dec_rdwen | 输出 | 目的寄存器写使能信号 | |
dec_rs1idx | 输出 | 源寄存器1的索引 | |
dec_rs2idx | 输出 | 源寄存器2的索引 | |
dec_rdidx | 输出 | 目的寄存器的索引 | |
dec_info | 输出 | 译码后的信息总线 | |
dec_imm | 输出 | 译码后的立即数 | |
dec_pc | 输出 | 译码后的程序计数器值 | |
错误信息 | dec_misalgn | 输出 | 译码阶段检测到的地址未对齐信号 |
dec_buserr | 输出 | 译码阶段检测到的总线错误信号 | |
dec_ilegl | 输出 | 非法指令信号 | |
判断 | dec_mulhsu | 输出 | 指示指令是否为有符号 - 无符号乘法高位指令 |
dec_mul | 输出 | 指示指令是否为乘法指令 | |
dec_div | 输出 | 指示指令是否为有符号除法指令 | |
dec_rem | 输出 | 指示指令是否为有符号取余指令 | |
dec_divu | 输出 | 指示指令是否为无符号除法指令 | |
dec_remu | 输出 | 指示指令是否为无符号取余指令 | |
dec_rv32 | 输出 | 指示指令是否为RV32指令集的指令 | |
dec_bjp | 输出 | 指示指令是否为分支或跳转指令 | |
dec_jal | 输出 | 指示指令是否为无条件跳转并链接指令 | |
dec_jalr | 输出 | 指示指令是否为跳转并链接寄存器指令 | |
dec_bxx | 输出 | 指示指令是否为条件分支指令 | |
跳转 | dec_jalr_rs1idx | 输出 | 跳转并链接寄存器指令中源寄存器1的索引 |
dec_bjp_imm | 输出 | 分支或跳转指令的立即数 |
output dec_rs1x0, //该指令源操作数1的寄存器索引为x0
output dec_rs2x0, //该指令源操作数2的寄存器索引为x0
output dec_rs1en, //该指令需要读取源操作数1
output dec_rs2en, //该指令需要读取源操作数2
output dec_rdwen, //该指令需要写结果操作数
output ['E203_RFIDX_WIDTH-1:0'] dec_rs1idx, //该指令源操作数1的寄存器索引
output ['E203_RFIDX_WIDTH-1:0'] dec_rs2idx, //该指令源操作数2的寄存器索引
output ['E203_RFIDX_WIDTH-1:0'] dec_rdidx, //该指令结果寄存器索引
output ['E203_DECINFO_WIDTH-1:0'] dec_info, //该指令的其他信息,将其打包成为一组
//宽信号,称之为信息总线(info bus)
output ['E203_XLEN-1:0'] dec_imm, //该指令使用的立即数的值
同时也用于生成后续模块的总线接口,下面是对BLP模块的实例。
//生成BJP单元所需的信息总线(Info Bus)。
wire bjp_op = dec_bjp | rv32_mret | (rv32_dret && (~rv32_dret_ilgl)) | rv3
2_fence fencei;
wire ['E203_DECINFO_BJP_WIDTH-1:0] bjp_info_bus;
assign bjp_info_bus['E203_DECINFO_GRP_BJP] = 'E203_DECINFO_GRP_BJP;
assign bjp_info_bus['E203_DECINFO_RV32] = rv32;
assign bjp_info_bus['E203_DECINFO_BJP_JUMP] = dec_jal | dec_jalr;
assign bjp_info_bus['E203_DECINFO_BJP_BPRDT] = i_prdt_taken;
assign bjp_info_bus['E203_DECINFO_BJP_BEQ] = rv32_beq | rv16_beqz;
assign bjp_info_bus['E203_DECINFO_BJP_BNE] = rv32_bne | rv16_bnez;
最终在使用一个多路选择器将这些总线信号根据类型进行选择。
//以下逻辑是典型的5输入并行多路选择器(使用Verilog assign语法编码And-Or逻辑)
assign dec_info =
( {'E203_DECINFO_WIDTH{alu_op}} && {'E203_DECINFO_WIDTH-'E20
3_DECINFO_ALU_WIDTH{1'b0}},alu_info_bus)
|| ( {'E203_DECINFO_WIDTH{amoldst_op}} && {'E203_DECINFO_WIDTH-'E20
3_DECINFO_AGU_WIDTH{1'b0}},agu_info_bus)
|| ( {'E203_DECINFO_WIDTH{bip_op}} && {'E203_DECINFO_WIDTH-'E20
3_DECINFO_BJP_WIDTH{1'b0}},bjp_info_bus)
|| ( {'E203_DECINFO_WIDTH{csr_op}} && {'E203_DECINFO_WIDTH-'E20
3_DECINFO_CSR_WIDTH{1'b0}},csr_info_bus)
|| ( {'E203_DECINFO_WIDTH{muldiy_op}} && {'E203_DECINFO_WIDTH-'E20
3_DECINFO_CSR_WIDTH{1'b0}},muldiy_info_bus)
;
可以看到,译码这一模块有很高的复杂度,后续我会结合附录A的内容详细解释。
4.2 整数通用寄存器组
RISC-V的整数指令都是单操作数或者两操作数指令,且蜂鸟E200属于单发射的微架构,因此Integer-Regfile模块只需要支持最多两个读端口,一个用于操作数1,另一个用于操作数2。同时,蜂鸟E200的写回策略是按顺序每次写回一条指令的微架构,因此Integer-Regfile模块只需要支持一个写端口。
该模块的位置在整个系统的最后面,用于数据的写回操作,下面是该模块的系统框图:
如图所示,Integer-Regfile的写端口逻辑将输入的结果寄存器索引和各自的寄存器号进行比较,产生写使能信号,被使能的通用寄存器即将写数据写入寄存器(x0由于是常数,因此无须写入)。
Integer-Regfile的每个读端口都是一个纯粹的并行多路选择器,多路选择器的选择信号即读操作数的寄存器索引,这几个通用寄存器就是x0到x31,比如ADD x1,x2,x3表示的就是x1=x2+x3,这时候就需要读取x2和x3的数据,这时候将x2和x3的索引值指向多路选择器,读端口就可以顺利的读出数据了。
该部分代码很简单,主要是加了锁存器的宏定义,使用锁存器和D触发器各有各的好处,这里需要注意一下:
module e203_exu_regfile(
input [`E203_RFIDX_WIDTH-1:0] read_src1_idx,
input [`E203_RFIDX_WIDTH-1:0] read_src2_idx,
output [`E203_XLEN-1:0] read_src1_dat,
output [`E203_XLEN-1:0] read_src2_dat,
input wbck_dest_wen,
input [`E203_RFIDX_WIDTH-1:0] wbck_dest_idx,
input [`E203_XLEN-1:0] wbck_dest_dat,
output [`E203_XLEN-1:0] x1_r,
input test_mode,
input clk,
input rst_n
);
wire [`E203_XLEN-1:0] rf_r [`E203_RFREG_NUM-1:0];
wire [`E203_RFREG_NUM-1:0] rf_wen;
`ifdef E203_REGFILE_LATCH_BASED //{
// Use DFF to buffer the write-port
wire [`E203_XLEN-1:0] wbck_dest_dat_r;
sirv_gnrl_dffl #(`E203_XLEN) wbck_dat_dffl (wbck_dest_wen, wbck_dest_dat, wbck_dest_dat_r, clk);
wire [`E203_RFREG_NUM-1:0] clk_rf_ltch;
`endif//}
genvar i;
generate //{
for (i=0; i<`E203_RFREG_NUM; i=i+1) begin:regfile//{
if(i==0) begin: rf0
// x0 cannot be wrote since it is constant-zeros
assign rf_wen[i] = 1'b0;
assign rf_r[i] = `E203_XLEN'b0;
`ifdef E203_REGFILE_LATCH_BASED //{
assign clk_rf_ltch[i] = 1'b0;
`endif//}
end
else begin: rfno0
assign rf_wen[i] = wbck_dest_wen & (wbck_dest_idx == i) ;
`ifdef E203_REGFILE_LATCH_BASED //{
e203_clkgate u_e203_clkgate(
.clk_in (clk ),
.test_mode(test_mode),
.clock_en(rf_wen[i]),
.clk_out (clk_rf_ltch[i])
);
//from write-enable to clk_rf_ltch to rf_ltch
sirv_gnrl_ltch #(`E203_XLEN) rf_ltch (clk_rf_ltch[i], wbck_dest_dat_r, rf_r[i]);
`else//}{
sirv_gnrl_dffl #(`E203_XLEN) rf_dffl (rf_wen[i], wbck_dest_dat, rf_r[i], clk);
`endif//}
end
end//}
endgenerate//}
assign read_src1_dat = rf_r[read_src1_idx];
assign read_src2_dat = rf_r[read_src2_idx];
4.3 CSR 寄存器
RISC-V的架构中定义了控制和状态寄存器(CSR),用于配置或记录一些运行的状态。CSR寄存器是处理器核内部的寄存器,使用自己的地址编码空间,与存储器寻址的地址区间完全无关系。CSR寄存器的访问采用专用的CSR读写指令,包括CSRRW、CSRRS、CSRRC、CSRRWI、CSRRSI、CSRRCI指令,这个在后面的文章中会解答。
下面是改模块的端口说明:
端口类型 | 端口名称 | 位宽 | 描述 |
nonflush_cmt_ena | 输入 | 非刷新提交使能信号 | |
eai_xs_off | 输出 | 可能是用于控制某个外部加速器(EAI)相关功能关闭的信号 | |
CSR控制 | csr_ena | 输入 | CSR(控制和状态寄存器)访问使能信号,当该信号有效时,允许对CSR进行操作 |
csr_wr_en | 输入 | CSR写使能信号,用于指示当前操作是对CSR进行写操作 | |
csr_rd_en | 输入 | CSR读使能信号,用于指示当前操作是对CSR进行读操作 | |
csr_idx | 输入 | CSR索引,用于指定要访问的具体CSR寄存器 | |
csr_access_ilgl | 输出 | CSR访问非法信号,当对CSR的访问不符合规则时,该信号有效 | |
tm_stop | 输出 | 可能用于控制跟踪模块(TM)停止工作的信号 | |
core_cgstop | 输出 | 核心时钟门控停止信号,用于控制核心的时钟门控 | |
tcm_cgstop | 输出 | 紧耦合内存(TCM)时钟门控停止信号,用于控制TCM的时钟门控 | |
itcm_nohold | 输出 | 指令紧耦合内存(ITCM)无保持信号,可能用于控制ITCM的保持状态 | |
mdv_nob2b | 输出 | 可能与某些数据验证或传输相关的无突发到突发(no burst - to - burst)信号 | |
read_csr_dat | 输出 | 从CSR中读取的数据 | |
wbck_csr_dat | 输入 | 写回到CSR的数据 | |
core_mhartid | 输入 | 核心的硬件线程ID,用于标识不同的硬件线程 | |
中断信号 | ext_irq_r | 输入 | 外部中断请求信号 |
sft_irq_r | 输入 | 软件中断请求信号 | |
tmr_irq_r | 输入 | 定时器中断请求信号 | |
status_mie_r | 输出 | 状态寄存器中的全局中断使能位 | |
机器模式 | mtie_r | 输出 | 机器模式定时器中断使能位 |
msie_r | 输出 | 机器模式软件中断使能位 | |
meie_r | 输出 | 机器模式外部中断使能位 | |
wr_csr_nxt | 输出 | 下一次要写入CSR的数据 | |
调试 | wr_dcsr_ena | 输出 | 写调试控制和状态寄存器(DCSR)使能信号 |
wr_dpc_ena | 输出 | 写调试程序计数器(DPC)使能信号 | |
wr_dscratch_ena | 输出 | 写调试暂存寄存器(DSCRATCH)使能信号 | |
dcsr_r | 输入 | 调试控制和状态寄存器的当前值 | |
dpc_r | 输入 | 调试程序计数器的当前值 | |
dscratch_r | 输入 | 调试暂存寄存器的当前值 | |
dbg_mode | 输入 | 调试模式信号,当该信号有效时,系统进入调试模式 | |
dbg_stopcycle | 输入 | 调试停止周期信号,可能用于在调试模式下控制系统停止运行的周期 | |
模式 | u_mode | 输出 | 表示系统处于用户模式 |
s_mode | 输出 | 表示系统处于监督模式 | |
h_mode | 输出 | 表示系统处于虚拟化模式 | |
m_mode | 输出 | 表示系统处于机器模式 | |
提交 | cmt_badaddr | 输入 | 提交的错误地址 |
cmt_badaddr_ena | 输入 | 提交错误地址使能信号 | |
cmt_epc | 输入 | 提交的异常程序计数器值 | |
cmt_epc_ena | 输入 | 提交异常程序计数器使能信号 | |
cmt_cause | 输入 | 提交的异常原因 | |
cmt_cause_ena | 输入 | 提交异常原因使能信号 | |
cmt_status_ena | 输入 | 提交状态寄存器使能信号 | |
cmt_instret_ena | 输入 | 提交指令执行计数使能信号 | |
cmt_mret_ena | 输入 | 提交MRET(机器模式返回)使能信号 | |
计数器 | csr_epc_r | 输出 | CSR中存储的异常程序计数器值 |
csr_dpc_r | 输出 | CSR中存储的调试程序计数器值 | |
csr_mtvec_r | 输出 | CSR中存储的机器模式陷入向量基地址 |
在每一个模块中(ALU等计算模块)都会产生CSR总线数据于CSR通信,以ALU为例,下面是有关ALU的接口代码:
input csr_ena, //来自于ALU的CSR读写使能信号
input csr_wr_en, //CSR写操作指示信号
input csr_rd_en, //CSR读操作指示信号
input [12 - 1:0] csr_idx, //CSR寄存器的地址索引
output ['E203_XLEN - 1:0] read_csr_dat, //读操作从CSR寄存器模块中读出的数据
input ['E203_XLEN - 1:0] wbck_csr_dat, //写操作写入CSR寄存器模块的数据
同时,对CSR中的一些寄存器进行写入操作也是通过CSR总线进行配置,下面是以其中一个寄存器MTVEC,下面是对其进行配置的代码:
//0x305 MRW mtvec Machine trap-handler base address.
wire sel_mtvec = (csr_idx == 12'h305);
wire rd_mtvec = csr_rd_en & sel_mtvec;
`ifdef E203_SUPPORT_MTVEC //{
wire wr_mtvec = sel_mtvec & csr_wr_en;
wire mtvec_ena = (wr_mtvec & wbck_csr_wen);
wire [`E203_XLEN-1:0] mtvec_r;
wire [`E203_XLEN-1:0] mtvec_nxt = wbck_csr_dat;
sirv_gnrl_dfflr #(`E203_XLEN) mtvec_dfflr (mtvec_ena, mtvec_nxt, mtvec_r, clk, rst_n);
wire [`E203_XLEN-1:0] csr_mtvec = mtvec_r;
`else//}{
// THe vector table base is a configurable parameter, so we dont support writeable to it
wire [`E203_XLEN-1:0] csr_mtvec = `E203_MTVEC_TRAP_BASE;
`endif//}
assign csr_mtvec_r = csr_mtvec;
最后读操作输出的结果是由一个大型的多路选择器实现的,可以看到其一共有30个左右的寄存器,每一个寄存器都代表着一个状态或者信息的记录。
assign read_csr_dat = `E203_XLEN'b0
//| ({`E203_XLEN{rd_ustatus }} & csr_ustatus )
| ({`E203_XLEN{rd_mstatus }} & csr_mstatus )
| ({`E203_XLEN{rd_mie }} & csr_mie )
| ({`E203_XLEN{rd_mtvec }} & csr_mtvec )
| ({`E203_XLEN{rd_mepc }} & csr_mepc )
| ({`E203_XLEN{rd_mscratch }} & csr_mscratch )
| ({`E203_XLEN{rd_mcause }} & csr_mcause )
| ({`E203_XLEN{rd_mbadaddr }} & csr_mbadaddr )
| ({`E203_XLEN{rd_mip }} & csr_mip )
| ({`E203_XLEN{rd_misa }} & csr_misa )
| ({`E203_XLEN{rd_mvendorid}} & csr_mvendorid)
| ({`E203_XLEN{rd_marchid }} & csr_marchid )
| ({`E203_XLEN{rd_mimpid }} & csr_mimpid )
| ({`E203_XLEN{rd_mhartid }} & csr_mhartid )
| ({`E203_XLEN{rd_mcycle }} & csr_mcycle )
| ({`E203_XLEN{rd_mcycleh }} & csr_mcycleh )
| ({`E203_XLEN{rd_minstret }} & csr_minstret )
| ({`E203_XLEN{rd_minstreth}} & csr_minstreth)
| ({`E203_XLEN{rd_counterstop}} & csr_counterstop)// Self-defined
| ({`E203_XLEN{rd_mcgstop}} & csr_mcgstop)// Self-defined
| ({`E203_XLEN{rd_itcmnohold}} & csr_itcmnohold)// Self-defined
| ({`E203_XLEN{rd_mdvnob2b}} & csr_mdvnob2b)// Self-defined
| ({`E203_XLEN{rd_dcsr }} & csr_dcsr )
| ({`E203_XLEN{rd_dpc }} & csr_dpc )
| ({`E203_XLEN{rd_dscratch }} & csr_dscratch)
;
4.4 指令的发射派遣
在E200处理器中,发射和派遣都叫做派遣。蜂鸟E200执行阶段的派遣机制特点如下:
指令交付路径:所有指令都要派遣给ALU,并通过ALU与交付模块的接口进行交付。
长指令处理:若是长指令,需通过ALU进一步发送至相应长指令运算单元。例如长指令类型的Load/Store指令,通过ALU的AGU子单元被进一步发送至LSU单元执行(LSU就是专门的长指令处理单元)。
实际派遣位置:实际的派遣功能发生在ALU内部。因为E200的译码模块在译码时已根据执行运算单元进行分组,并译码出相应指示信号,所以能按照指示信号将指令派遣给相应运算单元 。
该模块的输入包括两部分,一个是译码模块输出的数据,另一个便是其他模块的输出。输出部分也包括两个部分,一个是输入到ALU单元,另一个便是输入到OITF单元。
这里我对该系统的输入输出信号做一个简单的描述,读者可以试着对里面的一些缩写理解一下,比如imm对应立即数,rs1表示操作数1等,这方便我们对后续的一些模块代码进行理解。
信号类型 | 信号名称 | 信号方向 | 含义解释 |
Decode | wfi_halt_exu_req | 输入 | 这是一个请求信号,用于请求EXU(执行单元,Execution Unit)进入WFI(Wait For Interrupt,等待中断)的暂停状态。 |
wfi_halt_exu_ack | 输出 | 这是对wfi_halt_exu_req请求的响应信号。 | |
oitf_empty | 输入 | 表示OITF为空的信号。 | |
amo_wait | 输入 | ALU模块等待信号 | |
disp_i_valid | 输入 | 输入的握手信号 | |
disp_i_ready | 输出 | 输出的握手信号 | |
disp_i_rs1x0 | 输入 | 表示指令源操作数1的寄存器索引是否为x0。 | |
disp_i_rs2x0 | 输入 | 表示指令源操作数2的寄存器索引是否为x0。 | |
disp_i_rs1en | 输入 | 用于指示是否需要读取源操作数1。 | |
disp_i_rs2en | 输入 | 用于指示是否需要读取源操作数2。 | |
disp_i_rs1idx | 输入 | 表示源操作数1的寄存器索引。 | |
disp_i_rs2idx | 输入 | 表示源操作数2的寄存器索引。 | |
disp_i_rs1 | 输入 | 表示源操作数1的值。 | |
disp_i_rs2 | 输入 | 表示源操作数2的值。 | |
disp_i_rdwen | 输入 | 用于指示是否需要将指令运算的结果写回寄存器。 | |
disp_i_rdidx | 输入 | 表示写回结果的目标寄存器索引。 | |
disp_i_info | 输入 | 包含了指令的其他相关信息。作为信息总线(info bus)。 | |
disp_i_imm | 输入 | 表示指令中使用的立即数的值。 | |
disp_i_pc | 输入 | 表示当前指令的程序计数器(PC)值。 | |
disp_i_misalgn | 输入 | 用于指示数据访问时的对齐错误(misalignment)。 | |
disp_i_buserr | 输入 | 表示总线错误(bus error)。 | |
disp_i_ilegl | 输入 | 用于指示指令非法(illegal instruction)。 | |
ALU | disp_o_alu_longpipe | 输入 | 指示当前指令是否需使用ALU的长流水线资源 |
disp_o_alu_ready | 输入 | 由ALU反馈,高电平时表示ALU准备好接收新数据 | |
disp_o_alu_buserr | 输出 | 表示总线错误,数据传输过程中出错时置高电平 | |
disp_o_alu_ilegl | 输出 | 指示指令非法,检测到非法指令时置高电平 | |
disp_o_alu_misalgn | 输出 | 指示数据访问时的对齐错误,地址未按预期对齐时置高电平 | |
disp_o_alu_imm | 输出 | 指令中使用的立即数的值,参与指令运算 | |
disp_o_alu_info | 输出 | 包含指令其他相关信息,作为信息总线辅助ALU执行指令 | |
disp_o_alu_itag | 输出 | 来源是oitf | |
disp_o_alu_pc | 输出 | 当前指令的程序计数器值,用于如跳转指令目标地址计算等 | |
disp_o_alu_rdidx | 输出 | 写回结果的目标寄存器索引,ALU据此将结果写回相应寄存器 | |
disp_o_alu_rdwen | 输出 | 指示是否需要将运算结果写回寄存器,高电平表示执行写回操作 | |
disp_o_alu_rs1 | 输出 | 源操作数1的值,作为指令运算的一个输入操作数发送给ALU | |
disp_o_alu_rs2 | 输出 | 源操作数2的值,作为指令运算的另一个输入操作数发送给ALU | |
disp_o_alu_valid | 输出 | 表示派遣到ALU的数据有效,ALU可接收处理后续数据 |
通过RTL视图可以看出来,该模块主要以assign线型连接为主,因此代码逻辑简单,如下所示:该部分的输入主要是译码模块的输出,因此可以说是译码模块与ALU模块相连,但为了代码完整性或者以后方便添加子模块,保留了派遣模块的主体。
assign disp_o_alu_rs1 = disp_i_rs1_msked;
assign disp_o_alu_rs2 = disp_i_rs2_msked;
assign disp_o_alu_rdwen = disp_i_rdwen;
assign disp_o_alu_rdidx = disp_i_rdidx;
assign disp_o_alu_info = disp_i_info;
assign disp_o_alu_imm = disp_i_imm;
assign disp_o_alu_pc = disp_i_pc;
assign disp_o_alu_itag = disp_oitf_ptr;
assign disp_o_alu_misalgn= disp_i_misalgn;
assign disp_o_alu_buserr = disp_i_buserr ;
assign disp_o_alu_ilegl = disp_i_ilegl ;
派遣模块中还会处理流水线冲突的问题,包括资源冲突和数据相关性造成的数据冲突。 派遣模块还会在某些特殊情况下将流水线的派遣阻塞 ,而阻塞流水线的原因便是数据冲突。
可以看到,在disp_condition允许派遣的信号中使用了~dep,只有没有发生相关性冲突的时候才能派遣。
wire raw_dep = ((oitfrd_match_disprs1) |
(oitfrd_match_disprs2) |
(oitfrd_match_disprs3));
wire waw_dep = (oitfrd_match_disprd);
wire dep = raw_dep | waw_dep;
assign wfi_halt_exu_ack = oitf_empty & (~amo_wait);
wire disp_condition =
...
& (~dep)
...
4.5 OITF相关性检测
为了避免冲突,需要进行相关性检测,这便是OITF模块存在的意义。
OITF模块本质上来说就是一个先进先出的fifo。在流水线派遣点,每次派遣长指令会在 OITF 分配表项,存储其源操作数和结果寄存器索引;在写回点,按顺序写回长指令后会去除其在 OITF 的表项。保证长指令按顺序写回需借助 OITF 功能。OITF 存储已派遣但未写回的长指令信息。每条指令派遣时,会将自身源操作数和结果寄存器索引与 OITF 各表项比对,判断是否与未写回长指令产生 RAW、WAW 相关性。
下面是改模块的端口信号说明:
端口类型 | 端口名称 | 位宽 | 描述 |
派遣 | dis_ready | 输出 | 派遣就绪信号,表明 OITF 是否准备好接收新指令 |
dis_ena | 输入 | 派遣使能信号,控制是否允许新指令派遣到 OITF | |
dis_ptr | 输出 | 派遣指针,指示新指令在 OITF 中的存储位置 | |
disp_i_rs1en | 输入 | 派遣指令源寄存器 1 有效信号 | |
disp_i_rs2en | 输入 | 派遣指令源寄存器 2 有效信号 | |
disp_i_rs3en | 输入 | 派遣指令源寄存器 3 有效信号 | |
disp_i_rdwen | 输入 | 派遣指令写使能信号 | |
disp_i_rs1fpu | 输入 | 派遣指令源寄存器 1 浮点寄存器标识信号 | |
disp_i_rs2fpu | 输入 | 派遣指令源寄存器 2 浮点寄存器标识信号 | |
disp_i_rs3fpu | 输入 | 派遣指令源寄存器 3 浮点寄存器标识信号 | |
disp_i_rdfpu | 输入 | 派遣指令目的寄存器浮点寄存器标识信号 | |
disp_i_rs1idx | 输入 | 派遣指令源寄存器 1 索引编号 | |
disp_i_rs2idx | 输入 | 派遣指令源寄存器 2 索引编号 | |
disp_i_rs3idx | 输入 | 派遣指令源寄存器 3 索引编号 | |
disp_i_rdidx | 输入 | 派遣指令目的寄存器索引编号 | |
disp_i_pc | 输入 | 派遣指令的程序计数器值 | |
匹配 | oitfrd_match_disprs1 | 输出 | 派遣指令源寄存器 1 与 OITF 中目的寄存器匹配信号 |
oitfrd_match_disprs2 | 输出 | 派遣指令源寄存器 2 与 OITF 中目的寄存器匹配信号 | |
oitfrd_match_disprs3 | 输出 | 派遣指令源寄存器 3 与 OITF 中目的寄存器匹配信号 | |
oitfrd_match_disprd | 输出 | 派遣指令目的寄存器与 OITF 中目的寄存器匹配信号 | |
oitf_empty | 输出 | OITF 空标志信号 | |
返回 | ret_ena | 输入 | 返回使能信号,控制是否允许指令从 OITF 中退出 |
ret_ptr | 输出 | 返回指针,指示可以从 OITF 中退出的指令位置 | |
ret_rdidx | 输出 | 返回的目的寄存器索引 | |
ret_rdwen | 输出 | 返回的写使能信号 | |
ret_rdfpu | 输出 | 返回的浮点寄存器标识信号 | |
ret_pc | 输出 | 返回的程序计数器值 |
下面这段代码描述了该模块的FIFO特性,使用generate语块批量生成寄存器,并且使用计数器等方式记录fifo的地址等信息,在最下面将正在派遣的指令的源操作数以及结果操作数寄存器索引和各表项中的结果寄存器索引进行比较。
genvar i;
generate //{
for (i=0; i<`E203_OITF_DEPTH; i=i+1) begin:oitf_entries//{
assign vld_set[i] = alc_ptr_ena & (alc_ptr_r == i);
assign vld_clr[i] = ret_ptr_ena & (ret_ptr_r == i);
assign vld_ena[i] = vld_set[i] | vld_clr[i];
assign vld_nxt[i] = vld_set[i] | (~vld_clr[i]);
sirv_gnrl_dfflr #(1) vld_dfflrs(vld_ena[i], vld_nxt[i], vld_r[i], clk, rst_n);
//Payload only set, no need to clear
sirv_gnrl_dffl #(`E203_RFIDX_WIDTH) rdidx_dfflrs(vld_set[i], disp_i_rdidx, rdidx_r[i], clk);
sirv_gnrl_dffl #(`E203_PC_SIZE ) pc_dfflrs (vld_set[i], disp_i_pc , pc_r[i] , clk);
sirv_gnrl_dffl #(1) rdwen_dfflrs(vld_set[i], disp_i_rdwen, rdwen_r[i], clk);
sirv_gnrl_dffl #(1) rdfpu_dfflrs(vld_set[i], disp_i_rdfpu, rdfpu_r[i], clk);
assign rd_match_rs1idx[i] = vld_r[i] & rdwen_r[i] & disp_i_rs1en & (rdfpu_r[i] == disp_i_rs1fpu) & (rdidx_r[i] == disp_i_rs1idx);
assign rd_match_rs2idx[i] = vld_r[i] & rdwen_r[i] & disp_i_rs2en & (rdfpu_r[i] == disp_i_rs2fpu) & (rdidx_r[i] == disp_i_rs2idx);
assign rd_match_rs3idx[i] = vld_r[i] & rdwen_r[i] & disp_i_rs3en & (rdfpu_r[i] == disp_i_rs3fpu) & (rdidx_r[i] == disp_i_rs3idx);
assign rd_match_rdidx [i] = vld_r[i] & rdwen_r[i] & disp_i_rdwen & (rdfpu_r[i] == disp_i_rdfpu ) & (rdidx_r[i] == disp_i_rdidx );
end//}
endgenerate//}
对应的在派遣模块中,如果发生了冲突,会阻塞流水线暂停运行,等待数据处理完毕后再进行读取,这一块的代码见派遣模块。
4.6 ALU模块
蜂鸟 E200 ALU 单元包含多个功能子单元,各子单元功能如下:
-
普通 ALU 运算:执行普通 ALU 指令(逻辑运算、加减法、移位指令)。
-
访存地址生成:负责 Load、Store 和相关扩展指令的地址生成。
-
分支预测解析:进行 Branch、Jump 指令的结果解析与执行。
-
CSR 读写控制:执行 CSR 指令。
-
多周期乘除法器:执行乘法和除法指令。
4.6.1 普通 ALU 运算
首先需要声明的是,所有的子模块代码里均没有加法器等运算操作,起都是通过调用另一个模块的加法器进行工作,极大程度减小了面积以及功耗。Regular ALU 模块由组合逻辑构成,本身无运算数据通路,主要逻辑是依据普通 ALU 指令类型向共享运算数据通路发起操作请求,并取回计算结果。
下面是该模块的端口说明,可以看到主要是以共享请求为主,就是使用共享运算通路进行数据计算:
端口类型 | 端口名称 | 位宽 | 描述 |
握手 | alu_i_valid | 输入 | 输入握手有效信号,表明输入数据和控制信息有效 |
alu_i_ready | 输出 | 输入握手就绪信号,指示单元准备好接收输入 | |
alu_o_valid | 输出 | 输出握手有效信号,表明输出数据和状态信息有效 | |
alu_o_ready | 输入 | 输出握手就绪信号,指示外部模块准备好接收输出 | |
输入数据 | alu_i_rs1 | 输入 | 源寄存器 1 的数据 |
alu_i_rs2 | 输入 | 源寄存器 2 的数据 | |
alu_i_imm | 输入 | 立即数 | |
alu_i_pc | 输入 | 程序计数器的值 | |
alu_i_info | 输入 | ALU 指令的解码信息 | |
写回 | alu_o_wbck_wdat | 输出 | 写回的数据 |
alu_o_wbck_err | 输出 | 写回错误信号 | |
alu_o_cmt_ecall | 输出 | 系统调用提交信号 | |
alu_o_cmt_ebreak | 输出 | 断点提交信号 | |
alu_o_cmt_wfi | 输出 | 等待中断提交信号 | |
共享请求 | alu_req_alu_add | 输出 | 共享 ALU 数据通路加法运算请求 |
alu_req_alu_sub | 输出 | 共享 ALU 数据通路减法运算请求 | |
alu_req_alu_xor | 输出 | 共享 ALU 数据通路异或运算请求 | |
alu_req_alu_sll | 输出 | 共享 ALU 数据通路逻辑左移运算请求 | |
alu_req_alu_srl | 输出 | 共享 ALU 数据通路逻辑右移运算请求 | |
alu_req_alu_sra | 输出 | 共享 ALU 数据通路算术右移运算请求 | |
alu_req_alu_or | 输出 | 共享 ALU 数据通路或运算请求 | |
alu_req_alu_and | 输出 | 共享 ALU 数据通路与运算请求 | |
alu_req_alu_slt | 输出 | 共享 ALU 数据通路有符号小于比较运算请求 | |
alu_req_alu_sltu | 输出 | 共享 ALU 数据通路无符号小于比较运算请求 | |
alu_req_alu_lui | 输出 | 共享 ALU 数据通路加载高位立即数运算请求 | |
alu_req_alu_op1 | 输出 | 共享 ALU 数据通路的操作数 1 | |
alu_req_alu_op2 | 输出 | 共享 ALU 数据通路的操作数 2 | |
alu_req_alu_res | 输入 | 共享 ALU 数据通路的运算结果 |
使用何种运算的请求在info总线中定义,代码如下:
// The NOP is encoded as ADDI, so need to uncheck it
assign alu_req_alu_add = alu_i_info [`E203_DECINFO_ALU_ADD ] & (~nop);
assign alu_req_alu_sub = alu_i_info [`E203_DECINFO_ALU_SUB ];
assign alu_req_alu_xor = alu_i_info [`E203_DECINFO_ALU_XOR ];
assign alu_req_alu_sll = alu_i_info [`E203_DECINFO_ALU_SLL ];
assign alu_req_alu_srl = alu_i_info [`E203_DECINFO_ALU_SRL ];
assign alu_req_alu_sra = alu_i_info [`E203_DECINFO_ALU_SRA ];
assign alu_req_alu_or = alu_i_info [`E203_DECINFO_ALU_OR ];
assign alu_req_alu_and = alu_i_info [`E203_DECINFO_ALU_AND ];
assign alu_req_alu_slt = alu_i_info [`E203_DECINFO_ALU_SLT ];
assign alu_req_alu_sltu = alu_i_info [`E203_DECINFO_ALU_SLTU];
assign alu_req_alu_lui = alu_i_info [`E203_DECINFO_ALU_LUI ];
4.6.2 AGU模块
访存地址生成(AGU)模块主要负责 Load、Store 和“A”扩展指令的地址生成,以及“A”扩展指令的微操作拆分与执行。也就数据从寄存器中加载以及存储等操作,该模块需要与地址寄存器组进行联合,等待后续章节进行讲解。
4.6.3 分支预测解析模块
该模块主要就是用于分支跳转部分,这个同样也在后续会讲解。
4.6.4 CSR读写控制
CSR读写控制(CSR-CTRL)模块,主要负责CSR读写指令的执行,包括CSRRW、CSRRS、CSRRC、CSRRWI、CSRRSI以及CSRRCI指令。
该模块的端口说明如下:
端口类型 | 端口名称 | 位宽 | 描述 |
握手 | csr_i_valid | 输入 | 输入握手有效信号,表明输入数据和控制信息有效 |
csr_i_ready | 输出 | 输入握手就绪信号,指示模块准备好接收输入 | |
csr_o_valid | 输出 | 输出握手有效信号,表明输出数据和状态信息有效 | |
csr_o_ready | 输入 | 输出握手就绪信号,指示外部模块准备好接收输出 | |
输入数据 | csr_i_rs1 | 输入 | 源寄存器 1 的数据 |
csr_i_info | 输入 | CSR 指令的解码信息 | |
csr_i_rdwen | 输入 | CSR 写使能信号 | |
csr_ena | 输出 | CSR 使能信号 | |
csr_wr_en | 输出 | CSR 写使能信号 | |
csr_rd_en | 输出 | CSR 读使能信号 | |
csr_idx | 输出 | CSR 索引 | |
csr_access_ilgl | 输入 | CSR 访问非法信号 | |
需要读写的数 | read_csr_dat | 输入 | 从 CSR 寄存器读取的数据 |
wbck_csr_dat | 输出 | 要写回到 CSR 寄存器的数据 | |
EAI | csr_sel_eai | 输出 | 选择 EAI 的信号 |
eai_xs_off | 输入 | EAI 的关闭信号 | |
eai_csr_valid | 输出 | EAI CSR 操作的握手有效信号 | |
eai_csr_ready | 输入 | EAI CSR 操作的握手就绪信号 | |
eai_csr_addr | 输出 | EAI CSR 操作的地址 | |
eai_csr_wr | 输出 | EAI CSR 写使能信号 | |
eai_csr_wdata | 输出 | 要写入 EAI CSR 寄存器的数据 | |
eai_csr_rdata | 输入 | 从 EAI CSR 寄存器读取的数据 | |
输出数据 | csr_o_wbck_wdat | 输出 | CSR 写回的数据 |
csr_o_wbck_err | 输出 | CSR 写回错误信号 |
代码部分使用纯组合逻辑,基本就是解码,然后根据解码的结果控制写入或读出的数据以及相关变量,这里就不放代码了。
4.6.5 多周期乘除法
上过学的都知道,无论是惩罚还是除法都可以使用多周期的加减迭代移位操作完成,这个也不例外。如果要完成一个乘法或者是除法操作,就要频繁调用共享加法器来实现操作。下面是乘除法的原理图示:
MDV模块在乘除法运算上有如下特点:乘法操作采用基-4 Booth编码减少周期数,对无符号乘法进行一位符号扩展后按有符号数运算,需17个迭代周期;除法操作用普通加减交替法,同样对无符号数扩展后按有符号数运算,需33个迭代周期。因结果有比特精度问题,还可能需额外1个周期判断余数矫正、1个周期进行商和余数矫正,最多共36个周期。
下面是该模块的端口说明:
端口类型 | 端口名称 | 位宽 | 描述 |
握手信号 | muldiv_i_valid | 输入 | 输入握手有效信号,表明输入数据和控制信息有效 |
muldiv_i_ready | 输出 | 输入握手就绪信号,指示单元准备好接收输入 | |
muldiv_o_valid | 输出 | 输出握手有效信号,表明输出数据和状态信息有效 | |
muldiv_o_ready | 输入 | 输出握手就绪信号,指示外部模块准备好接收输出 | |
输入数据 | muldiv_i_rs1 | 输入 | 源寄存器 1 的数据 |
muldiv_i_rs2 | 输入 | 源寄存器 2 的数据 | |
muldiv_i_imm | 输入 | 立即数 | |
muldiv_i_info | 输入 | 乘除法指令的解码信息 | |
muldiv_i_itag | 输入 | 指令标签 | |
muldiv_i_longpipe | 输出 | 长流水线标识信号 | |
flush_pulse | 输入 | 刷新脉冲信号 | |
写回 | muldiv_o_wbck_wdat | 输出 | 乘除法运算的写回数据 |
muldiv_o_wbck_err | 输出 | 乘除法运算写回错误信号 | |
共享请求 | muldiv_req_alu_op1 | 输出 | 共享 ALU 数据通路的操作数 1 |
muldiv_req_alu_op2 | 输出 | 共享 ALU 数据通路的操作数 2 | |
muldiv_req_alu_add | 输出 | 共享 ALU 数据通路的加法运算请求信号 | |
muldiv_req_alu_sub | 输出 | 共享 ALU 数据通路的减法运算请求信号 | |
muldiv_req_alu_res | 输入 | 共享 ALU 数据通路的运算结果 | |
共享缓存区 | muldiv_sbf_0_ena | 输出 | 共享缓冲区 0 的使能信号 |
muldiv_sbf_0_nxt | 输出 | 共享缓冲区 0 的下一个值 | |
muldiv_sbf_0_r | 输入 | 从共享缓冲区 0 读取的数据 | |
muldiv_sbf_1_ena | 输出 | 共享缓冲区 1 的使能信号 | |
muldiv_sbf_1_nxt | 输出 | 共享缓冲区 1 的下一个值 | |
muldiv_sbf_1_r | 输入 | 从共享缓冲区 1 读取的数据 |