tinyriscv这个SoC工程的内核cpu部分,采用经典的三级流水线结构进行设计,即大家所熟知的:取值—>译码—>执行三级流水线。
另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。
上一篇博文中注释了中断模块,现在来介绍通用寄存器reg.v模块:
目录
0 RISC-V SoC注解系列文章目录
1. reg在内核中的位置
如下图,绿色的方块是通用寄存器reg,从位置上可以看出,通用寄存器在功能上,主要承担译码和执行部分的临时数据存储:
2. RISC-V通用寄存器
RISC-V架构支持32位或者64位的架构,32 位架构由RV32表示,其每个通用寄存器的宽度为32比特; 64位架构由RV64表示,其每个通用寄存器的宽度为64比特。
RISC-V架构的整数通用寄存器组,包含32个(I 架构)或者16个(E架构)通用整数寄存器,其中整数寄存器0被预留为常数0,其他的31个(I架构)或者15个(E架构)为普通的通用整数寄存器。
如果使用浮点模块(F或者D),则需要另外一个独立的浮点寄存器组,包含32个通用浮点寄存器。如果仅使用F模块的浮点指令子集,则每个通用浮点寄存器的宽度为32比特;如果使用了D模块的浮点指令子集,则每个通用浮点寄存器的宽度为64比特。
在流水线中能够尽快地读取通用寄存器组,往往是处理器流水线设计的期望之一,这样可以提高处理器性能和优化时序。这个看似简单的道理在很多现存的商用RISC架构中都难以实现,因为经过多年反复修改不断添加新指令后,其指令编码中的寄存器索引位置变得非常凌乱,给译码器造成了负担。
得益于后发优势和总结了多年来处理器发展的经验,RISC-V的指令集编码非常规整,指令所需的通用寄存器的索引都被放在固定的位置,如下所示。因此指令译码器可以非常便捷地译码出寄存器索引,然后读取通用寄存器组。
RV32I 规整的指令编码格式
3. reg.v
3.1 输入输出接口:
input wire clk,
input wire rst,
// from ex
input wire we_i, // 写寄存器标志
input wire[`RegAddrBus] waddr_i, // 写寄存器地址
input wire[`RegBus] wdata_i, // 写寄存器数据
// from jtag
input wire jtag_we_i, // 写寄存器标志
input wire[`RegAddrBus] jtag_addr_i, // 读、写寄存器地址
input wire[`RegBus] jtag_data_i, // 写寄存器数据
// from id
input wire[`RegAddrBus] raddr1_i, // 读寄存器1地址
// to id
output reg[`RegBus] rdata1_o, // 读寄存器1数据
// from id
input wire[`RegAddrBus] raddr2_i, // 读寄存器2地址
// to id
output reg[`RegBus] rdata2_o, // 读寄存器2数据
// to jtag
output reg[`RegBus] jtag_data_o // 读寄存器数据
注意,上述代码注释中的的寄存器1不是指x1寄存器,寄存器2也不是指x2寄存器。而是指一条指令里涉及到的两个寄存器(源寄存器1和源寄存器2)。一条指令可能会同时读取两个寄存器的值,所以有两个读端口。又因为jtag模块也会进行寄存器的读操作,所以一共有三个读端口。
3.2. 主要功能
程序中定义了一个宽度位32位,深度位32位的寄存器regs。
reg[`RegBus] regs[0:`RegNum - 1]; //32*32 //reg[31:0]位宽 regs[0:31] 深度
1.写寄存器:将ex或者jtag中的数据,寄存在寄存器regs中;
2.读寄存器(组合逻辑):
读寄存器的地址来自译码id模块,并将从寄存器中读到的数据,送给译码id模块(regs)。
读寄存器的地址来自jtag模块,并将从寄存器中读到的数据,送给jtag模块(jtag读寄存器)。
3.3. 代码注释
// 写寄存器 写寄存器操作来自执行模块。
always @ (posedge clk) begin
if (rst == `RstDisable) begin//`define RstDisable 1'b1
// 优先ex模块写操作 写使能为高,并且写寄存器地址不为0时,因为寄存器x0是只读寄存器并且其值固定为0。
if ((we_i == `WriteEnable) && (waddr_i != `ZeroReg)) begin
//将ex模块中的写数据和地址,写入寄存器相应的地址当中
regs[waddr_i] <= wdata_i;
//否则当 jtag的写使能为高,并且写寄存器地址不为0时,因为寄存器x0是只读寄存器并且其值固定为0。
end else if ((jtag_we_i == `WriteEnable) && (jtag_addr_i != `ZeroReg)) begin
//将jtag模块中的写数据和地址,写入寄存器相应的地址当中
regs[jtag_addr_i] <= jtag_data_i;
end
end
end
//读寄存器
// 读寄存器1 读寄存器操作来自译码模块,并且读出来的寄存器数据也会返回给译码模块。
//assign rdata1_o = (raddr1_i == `ZeroReg) ? `ZeroWord : ((raddr1_i == waddr_i && we_i == `WriteEnable) ? wdata_i : regs[raddr1_i]);
always @ (*) begin
if (raddr1_i == `ZeroReg) begin //如果读地址为0,因为寄存器x0是只读寄存器并且其值固定为0。
rdata1_o = `ZeroWord;//因此输出数据为32'h0
//第二条指令依赖于第一条指令的结果。为了解决这个数据相关的问题。
//如果读地址等于写地址,并且正在写操作,则直接将要写的值返回给读操作。
end else if (raddr1_i == waddr_i && we_i == `WriteEnable) begin
rdata1_o = wdata_i;
end else begin //如果没有数据相关,则返回要读的寄存器的值。
rdata1_o = regs[raddr1_i];
end
end
注意:由于流水线的原因,当前指令处于执行阶段的时候,下一条指令则处于译码阶段。由于执行阶段不会写寄存器,而是在下一个时钟到来时才会进行寄存器写操作,如果译码阶段的指令需要上一条指令的结果,那么此时读到的寄存器的值是错误的。比如下面这两条指令:add x1, x2, x3 、add x4, x1, x5 第二条指令依赖于第一条指令的结果。为了解决这个问题,如果读寄存器等于写寄存器,则直接将要写的值返回给读操作。
// 读寄存器2 读寄存器操作来自译码模块,并且读出来的寄存器数据也会返回给译码模块。
//assign rdata2_o = (raddr2_i == `ZeroReg) ? `ZeroWord : ((raddr2_i == waddr_i && we_i == `WriteEnable) ? wdata_i : regs[raddr2_i]);
always @ (*) begin
if (raddr2_i == `ZeroReg) begin
rdata2_o = `ZeroWord;
//如果读地址等于写地址,并且正在写操作,则直接返回写数据
end else if (raddr2_i == waddr_i && we_i == `WriteEnable) begin
rdata2_o = wdata_i;
end else begin
rdata2_o = regs[raddr2_i];
end
end
// jtag读寄存器
//assign jtag_data_o = (jtag_addr_i == `ZeroReg) ? `ZeroWord : regs[jtag_addr_i];
always @ (*) begin
if (jtag_addr_i == `ZeroReg) begin
jtag_data_o = `ZeroWord;
end else begin
jtag_data_o = regs[jtag_addr_i];
end
end