三、RISC-V SoC内核——执行 代码讲解

tinyriscv这个SoC工程的内核cpu部分,采用经典的三级流水线结构进行设计,即大家所熟知的:取值—>译码—>执行三级流水线。

另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。

上一篇博文中注释了译码模块,现在来介绍执行模块:

目录

0 RISC-V SoC注解系列文章目录

1. 执行模块的注解

2. ex.v(组合逻辑电路)

2.1 执行代码解析

2.2 乘法操作注解

3. ram.v


0 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内核注解——总线

八、RISC-V SoC外设注解——GPIO

九、RISC-V SoC外设注解——SPI接口

十、RISC-V SoC外设注解——timer定时器

十一、RISC-V SoC外设注解——UART模块(终篇)

1. 执行模块的注解

RISC-V内核的执行部分:涉及到ex.v、ram.v、div.v等模块。其中div.v会在下一章节详细叙述。

执行部分详细结构图:

三、RISC-V SoC内核——执行 代码讲解_第1张图片

 接下来分模块对执行结构进行注解:

2. ex.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   // 跳转目的地址  

2.1 执行代码解析

(可以参考原作者的博客):

下面以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行,从内存中读出的数据进行了无符号位拓展,因为操作的是无符号数,不存在符号位,所以高位直接补零。

2.2 乘法操作注解

接下来对执行模块的乘法操作进行注解,乘法操作的主体代码部分如下:

// 处理乘法指令(硬件中所有数据,以补码的形式存储!!!)  

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]中,具体指令格式如下

三、RISC-V SoC内核——执行 代码讲解_第2张图片

 `INST_MULHU:无符号数*无符号数,将乘积的高位写入x[rd]中,具体指令格式如下

三、RISC-V SoC内核——执行 代码讲解_第3张图片

 `INST_MULHSU:有符号数*无符号数,将乘积的高位写入x[rd]中,具体指令格式如下

三、RISC-V SoC内核——执行 代码讲解_第4张图片

 `INST_MULH:有符号数*有符号数,将乘积的高位写入x[rd]中,具体指令格式如下

三、RISC-V SoC内核——执行 代码讲解_第5张图片

 对乘法操作进行逐步拆解分析(以指令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为例,相乘过程如下:

三、RISC-V SoC内核——执行 代码讲解_第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 

3. ram.v

功能:存储临时数据。

代码解析见取指章节中对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

你可能感兴趣的:(RISC-V,risc-v,soc,fpga开发)