tinyriscv这个SoC工程的内核cpu部分,采用经典的三级流水线结构进行设计,即大家所熟知的:取值—>译码—>执行三级流水线。
另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。
上一篇博文中注释了译码模块,现在来介绍执行模块:
目录
0 RISC-V SoC注解系列文章目录
1. 执行模块的注解
2. ex.v(组合逻辑电路)
2.1 执行代码解析
2.2 乘法操作注解
3. ram.v
零、RISC-V SoC软核笔记详解——前言
一、RISC-V SoC内核注解——取指
二、RISC-V SoC内核注解——译码
三、RISC-V SoC内核注解——执行
四、RISC-V SoC内核注解——除法(试商法)
五、RISC-V SoC内核注解——中断
六、RISC-V SoC内核注解——通用寄存器
七、RISC-V SoC内核注解——总线
八、RISC-V SoC外设注解——GPIO
九、RISC-V SoC外设注解——SPI接口
十、RISC-V SoC外设注解——timer定时器
十一、RISC-V SoC外设注解——UART模块(终篇)
RISC-V内核的执行部分:涉及到ex.v、ram.v、div.v等模块。其中div.v会在下一章节详细叙述。
执行部分详细结构图:
接下来分模块对执行结构进行注解:
主要功能为:
1.根据当前是什么指令(加减乘除,移位等操作)执行对应的操作,比如add指令,则将寄存器1的值和寄存器2的值相加。注意:其他指令的执行是类似的,需要注意的是没有涉及的信号要将其置为默认值,if和case情况要写全,避免产生锁存器。
2.如果是跳转指令,则发出跳转信号。
3.如果是内存加载指令,则读取对应地址的内存数据。
接口信号及注解:
// from id
input wire[`InstBus] inst_i, // 指令内容
input wire[`InstAddrBus] inst_addr_i, // 指令地址
input wire reg_we_i, // 是否写通用寄存器
input wire[`RegAddrBus] reg_waddr_i, // 写通用寄存器地址
input wire[`RegBus] reg1_rdata_i, // 通用寄存器1输入数据
input wire[`RegBus] reg2_rdata_i, // 通用寄存器2输入数据
input wire csr_we_i, // 是否写CSR寄存器
input wire[`MemAddrBus] csr_waddr_i, // 写CSR寄存器地址
input wire[`RegBus] csr_rdata_i, // CSR寄存器输入数据
input wire int_assert_i, // 中断发生标志
input wire[`InstAddrBus] int_addr_i, // 中断跳转地址
input wire[`MemAddrBus] op1_i,
input wire[`MemAddrBus] op2_i,
input wire[`MemAddrBus] op1_jump_i,
input wire[`MemAddrBus] op2_jump_i,
// from mem
input wire[`MemBus] mem_rdata_i, // 内存输入数据
// from div
input wire div_ready_i, // 除法运算完成标志
input wire[`RegBus] div_result_i, // 除法运算结果
input wire div_busy_i, // 除法运算忙标志
input wire[`RegAddrBus] div_reg_waddr_i,// 除法运算结束后要写的寄存器地址
// to mem
output reg[`MemBus] mem_wdata_o, // 写内存数据
output reg[`MemAddrBus] mem_raddr_o, // 读内存地址
output reg[`MemAddrBus] mem_waddr_o, // 写内存地址
output wire mem_we_o, // 是否要写内存
output wire mem_req_o, // 请求访问内存标志
// to regs
output wire[`RegBus] reg_wdata_o, // 写寄存器数据
output wire reg_we_o, // 是否要写通用寄存器
output wire[`RegAddrBus] reg_waddr_o, // 写通用寄存器地址
// to csr reg
output reg[`RegBus] csr_wdata_o, // 写CSR寄存器数据
output wire csr_we_o, // 是否要写CSR寄存器
output wire[`MemAddrBus] csr_waddr_o, // 写CSR寄存器地址
// to div
output wire div_start_o, // 开始除法运算标志
output reg[`RegBus] div_dividend_o, // 被除数
output reg[`RegBus] div_divisor_o, // 除数
output reg[2:0] div_op_o, // 具体是哪一条除法指令
output reg[`RegAddrBus] div_reg_waddr_o,// 除法运算结束后要写的寄存器地址
// to ctrl
output wire hold_flag_o, // 是否暂停标志
output wire jump_flag_o, // 是否跳转标志
output wire[`InstAddrBus] jump_addr_o // 跳转目的地址
(可以参考原作者的博客):
下面以add指令为例说明,add指令的作用就是将寄存器1的值和寄存器2的值相加,最后将结果写入目的寄存器。代码如下:
... `INST_TYPE_R_M: begin if ((funct7 == 7'b0000000) || (funct7 == 7'b0100000)) begin case (funct3) `INST_ADD_SUB: begin jump_flag = `JumpDisable; hold_flag = `HoldDisable; jump_addr = `ZeroWord; mem_wdata_o = `ZeroWord; mem_raddr_o = `ZeroWord; mem_waddr_o = `ZeroWord; mem_we = `WriteDisable; if (inst_i[30] == 1'b0) begin reg_wdata = reg1_rdata_i + reg2_rdata_i; end else begin reg_wdata = reg1_rdata_i - reg2_rdata_i; end ... end ...
第2~4行,译码操作。
第5行,对add或sub指令进行处理。
第6~12行,当前指令不涉及到的操作(比如跳转、写内存等)需要将其置回默认值。
第13行,指令编码中的第30位区分是add指令还是sub指令。0表示add指令,1表示sub指令。
第14行,执行加法操作。
第16行,执行减法操作。
其他指令的执行是类似的,需要注意的是没有涉及的信号要将其置为默认值,if和case情况要写全,避免产生锁存器。
下面以beq指令说明跳转指令的执行。beq指令的编码如下:
beq指令的作用就是当寄存器1的值和寄存器2的值相等时发生跳转,跳转的目的地址为当前指令的地址加上符号扩展的imm的值。具体代码如下:
...
`INST_TYPE_B: begin
case (funct3)
`INST_BEQ: begin
hold_flag = `HoldDisable;
mem_wdata_o = `ZeroWord;
mem_raddr_o = `ZeroWord;
mem_waddr_o = `ZeroWord;
mem_we = `WriteDisable;
reg_wdata = `ZeroWord;
if (reg1_rdata_i == reg2_rdata_i) begin
jump_flag = `JumpEnable;
jump_addr = inst_addr_i + {{20{inst_i[31]}}, inst_i[7], inst_i[30:25], inst_i[11:8], 1'b0};
end else begin
jump_flag = `JumpDisable;
jump_addr = `ZeroWord;
end
...
end
...第2~4行,译码出beq指令。
第5~10行,没有涉及的信号置为默认值。
第11行,判断寄存器1的值是否等于寄存器2的值。
第12行,跳转使能,即发生跳转。
第13行,计算出跳转的目的地址。
第15、16行,不发生跳转。
其他跳转指令的执行是类似的,这里就不再重复了。
访存和写回操作相关代码及有关概念的解析请参考原作者博客。
从零开始写RISC-V处理器
从零开始写RISC-V处理器 | liangkangnan的博客 (gitee.io)https://liangkangnan.gitee.io/2020/04/29/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%86%99RISC-V%E5%A4%84%E7%90%86%E5%99%A8/需要注意的是,在写回操作中,往内存中写数据的时候,需要先读出内存中的数据,进行修改后,再写进去。(例如:sb指令只改变读出来的32位内存数据中对应的字节,其他3个字节的数据保持不变,然后写回到内存中。)
RV32I 的 Load 和 Store 指令除了提供 32 位字(lw,sw)的加载和存储外, RV32I 还支持加载有符号和无符号字节和半字(lb,lbu,lh,lhu)和存储字节和半字(sb,sh)。有符号字节和半字符号扩展为 32 位再写入目的寄存器。即使是自然数据类型更窄,低位宽数据也是被扩展后再处理,这使得后续的整数计算指令能正确处理所有的 32 位。在文本和无符号整数中常用的无符号字节和半字,在写入目标寄存器之前都被无符号扩展到 32 位。
加载有符号半字指令lh:
`INST_LH: begin
jump_flag = `JumpDisable;
hold_flag = `HoldDisable;
jump_addr = `ZeroWord;
mem_wdata_o = `ZeroWord;
mem_waddr_o = `ZeroWord;
mem_we = `WriteDisable;
mem_req = `RIB_REQ;
mem_raddr_o = op1_add_op2_res;
if (mem_raddr_index == 2'b0) begin
reg_wdata = {{16{mem_rdata_i[15]}}, mem_rdata_i[15:0]};
end else begin
reg_wdata = {{16{mem_rdata_i[31]}}, mem_rdata_i[31:16]};
end
end
在`INST_LH指令中,第11行和13行,从内存中读出的数据进行了符号位拓展,原因如下:对于小于32位的立即数来说,如果将立即数(负数)直接存入32位寄存器(低位),那么最高位将会自动补0,将会改变立即数据的数值(因为每次判断正负的时候,程序直接每次直接判断的是最高位)。
加载无符号半字指令lhu:
`INST_LHU: begin
jump_flag = `JumpDisable;
hold_flag = `HoldDisable;
jump_addr = `ZeroWord;
mem_wdata_o = `ZeroWord;
mem_waddr_o = `ZeroWord;
mem_we = `WriteDisable;
mem_req = `RIB_REQ;
mem_raddr_o = op1_add_op2_res;
if (mem_raddr_index == 2'b0) begin
reg_wdata = {16'h0, mem_rdata_i[15:0]};
end else begin
reg_wdata = {16'h0, mem_rdata_i[31:16]};
end
end
在`INST_LHU指令中,第11行和13行,从内存中读出的数据进行了无符号位拓展,因为操作的是无符号数,不存在符号位,所以高位直接补零。
接下来对执行模块的乘法操作进行注解,乘法操作的主体代码部分如下:
// 处理乘法指令(硬件中所有数据,以补码的形式存储!!!)
always @ (*) begin
//根据opcode和funct7确定该指令为乘法操作
if ((opcode == `INST_TYPE_R_M) && (funct7 == 7'b0000001)) begin
case (funct3)//根据funct3确定具体是哪一种乘法指令
`INST_MUL, `INST_MULHU: begin
mul_op1 = reg1_rdata_i;
mul_op2 = reg2_rdata_i;
end
//mulhsu指令将操作数寄存器rsl与rs2中的32位整数相乘,
//其中rsl当作有符号数、rs2当作无符号数,将结果的高32位写回寄存器rd中
`INST_MULHSU: begin
//符号数乘无符号数,将符号数取反加1
mul_op1 = (reg1_rdata_i[31] == 1'b1)? (reg1_data_invert): reg1_rdata_i;
mul_op2 = reg2_rdata_i;
end
//mulh指令将操作数寄存器rsl与rs2中的32位整数当作有符号数相乘
//结果的高32位写回寄存器rd中。
`INST_MULH: begin
//有符号数乘法,将两个操作数都取反加1
mul_op1 = (reg1_rdata_i[31] == 1'b1)? (reg1_data_invert): reg1_rdata_i;
mul_op2 = (reg2_rdata_i[31] == 1'b1)? (reg2_data_invert): reg2_rdata_i;
end
default: begin
mul_op1 = reg1_rdata_i;
mul_op2 = reg2_rdata_i;
end
endcase
end else begin
mul_op1 = reg1_rdata_i;
mul_op2 = reg2_rdata_i;
end
end
首先简单介绍一下代码中涉及到的四条乘法指令。
`INST_MUL:无符号数*无符号数,将乘积的低位写入x[rd]中,具体指令格式如下
`INST_MULHU:无符号数*无符号数,将乘积的高位写入x[rd]中,具体指令格式如下
`INST_MULHSU:有符号数*无符号数,将乘积的高位写入x[rd]中,具体指令格式如下
`INST_MULH:有符号数*有符号数,将乘积的高位写入x[rd]中,具体指令格式如下
对乘法操作进行逐步拆解分析(以指令mulh为例,其他指令类似):
Step1:
`INST_MULH: begin
//有符号数乘法,将两个操作数都取反加1
mul_op1 = (reg1_rdata_i[31] == 1'b1)? (reg1_data_invert): reg1_rdata_i;
mul_op2 = (reg2_rdata_i[31] == 1'b1)? (reg2_data_invert): reg2_rdata_i;
end
Step2:首先判断乘数和被乘数是否是负数,如果是负数则需要对负数进行转换:
assign reg1_data_invert = ~reg1_rdata_i + 1;
assign reg2_data_invert = ~reg2_rdata_i + 1;
Step3:将上述转换后的结果相乘
assign mul_temp = mul_op1 * mul_op2;
Step4:如果是两个正数或者两个负数相乘,则直接返回Step3的结果;如果为一正一负相乘,则将Step3的结果取反加一。
assign mul_temp_invert = ~mul_temp + 1; //将负数的补码取反加1。
以-2×3=-6为例,相乘过程如下:
将结果的高位存储到寄存器中:
//mulh指令将操作数寄存器rsl与rs2中的32位整数当作有符号数相乘
//结果的高32位写回寄存器rd中。
`INST_MULH: begin
jump_flag = `JumpDisable;
hold_flag = `HoldDisable;
jump_addr = `ZeroWord;
mem_wdata_o = `ZeroWord;
mem_raddr_o = `ZeroWord;
mem_waddr_o = `ZeroWord;
mem_we = `WriteDisable;
case ({reg1_rdata_i[31], reg2_rdata_i[31]})
2'b00: begin
reg_wdata = mul_temp[63:32];
end
2'b11: begin
reg_wdata = mul_temp[63:32];
end
2'b10: begin
reg_wdata = mul_temp_invert[63:32];
end
default: begin
reg_wdata = mul_temp_invert[63:32];
end
endcase
end
功能:存储临时数据。
代码解析见取指章节中对rom的介绍,两段代码是一摸一样的
一、RISC-V内核结构——取指:https://blog.csdn.net/weixin_42294124/article/details/123033894?spm=1001.2014.3001.5502
`include "../core/defines.v"
// ram module
module ram(
input wire clk,
input wire rst,
input wire we_i, // write enable
input wire[`MemAddrBus] addr_i, // addr
input wire[`MemBus] data_i,
output reg[`MemBus] data_o // read data
);
reg[`MemBus] _ram[0:`MemNum - 1];
//写RAM
always @ (posedge clk) begin
if (we_i == `WriteEnable) begin
//从内核中输入的地址,是以0,4,8,C间隔变化,
//为了将数据按顺序依次存放在RAM(0,1,2,3...)地址中,
//所以采用地址的高30位,作为输入地址。
_ram[addr_i[31:2]] <= data_i;
end
end
//读RAM
always @ (*) begin
if (rst == `RstEnable) begin
data_o = `ZeroWord;
end else begin
data_o = _ram[addr_i[31:2]];//将RAM中地址addr_i[31:2]中的数据读出
end
end
endmodule