取指、译码、执行、访存、回写

注意:个人学习笔记要点记录,后续会完善更改,参考《从零开始写RISC-V处理器》。

取指:

目前tinyriscv所有外设(包括rom和ram)、寄存器的读取都是与时钟无关的,或者说所有外设、寄存器的读取采用的是组合逻辑的方式。

tinyriscv并没有具体的取指模块和代码。PC寄存器模块的输出pc_o会连接到外设rom模块的地址输入,又由于rom的读取是组合逻辑,因此每一个时钟上升沿到来之前(时序是满足要求的),从rom输出的指令已经稳定在if_id模块的输入,当时钟上升沿到来时指令就会输出到id模块。

取到的指令和指令地址会输入到if_id模块(if_id.v),if_id模块是一个时序电路,作用是将输入的信号打一拍后再输出到译码(id.v)模块

译码:

译码(id)模块是一个纯组合逻辑电路,主要作用有以下几点:

1.根据指令内容,解析出当前具体是哪一条指令(比如add指令)。

2.根据具体的指令,确定当前指令涉及的寄存器。比如读寄存器是一个还是两个,是否需要写寄存器以及写哪一个寄存器。

3.访问通用寄存器,得到要读的寄存器的值。

译码模块的输入输出信号如下表所示:

序号 信号名 输入/输出 位宽(bits) 说明
1 rst 输入 1 复位信号
2 inst_i 输入 32 指令内容
3 inst_addr_i 输入 32 指令地址
4 reg1_rdata_i 输入 32 寄存器1输入数据
5 reg2_rdata_i 输入 32 寄存器2输入数据
6 csr_rdata_i 输入 32 CSR寄存器输入数据
7 ex_jump_flag_i 输入 1 跳转信号
8 reg1_raddr_o 输出 5 读寄存器1地址,即读哪一个通用寄存器
9 reg2_raddr_o 输出 5 读寄存器2地址,即读哪一个通用寄存器
10 csr_raddr_o 输出 32 读csr寄存器地址,即读哪一个CSR寄存器
11 mem_req_o 输出 1 向总线请求访问内存信号
12 inst_o 输出 32 指令内容
13 inst_addr_o 输出 32 指令地址
14 reg1_rdata_o 输出 32 通用寄存器1数据
15 reg2_rdata_o 输出 32 通用寄存器2数据
16 reg_we_o 输出 1 通用寄存器写使能
17 reg_waddr_o 输出 5 通用寄存器写地址,即写哪一个通用寄存器
18 csr_we_o 输出 1 CSR寄存器写使能
19 csr_rdata_o 输出 32 CSR寄存器读数据
20 csr_waddr_o 输出 32 CSR寄存器写地址,即写哪一个CSR寄存器

 可知,add指令被编码成6部分内容。通过第1、4、6这三部分可以唯一确定当前指令是否是add指令。知道是add指令之后,就可以知道add指令需要读两个通用寄存器(rs1和rs2)和写一个通用寄存器(rd)。下面看具体的代码:

case (opcode)//opcode对应第6部分
...
    `INST_TYPE_R_M: begin
        if ((funct7 == 7'b0000000) || (funct7 == 7'b0100000)) begin//funct7对应第1部分
            case (funct3)//funct3对应第4部分
//第1、4、6这三部分已经译码完毕,已经可以确定当前指令是add指令
                `INST_ADD_SUB, `INST_SLL, `INST_SLT, `INST_SLTU, `INST_XOR, `INST_SR, `INST_OR, `INST_AND: begin
                     reg_we_o = `WriteEnable;//设置写寄存器标志为1
                     reg_waddr_o = rd;//设置写寄存器地址为rd
                     reg1_raddr_o = rs1;//设置读寄存器地址为rs1
                     reg2_raddr_o = rs2;
                 end
...

译码模块的输出会送到id_ex模块(id_ex.v)的输入,id_ex模块是一个时序电路,作用是将输入的信号打一拍后再输出到执行模块(ex.v)。

执行:

执行(ex)模块是一个纯组合逻辑电路,主要作用有以下几点:

1.根据当前是什么指令执行对应的操作,比如add指令,则将寄存器1的值和寄存器2的值相加。

2.如果是内存加载指令,则读取对应地址的内存数据。

3.如果是跳转指令,则发出跳转信号。

执行模块的输入输出信号如下表所示:

序号 信号名 输入/输出 位宽(bits) 说明
1 rst 输入 1 复位信号
2 inst_i 输入 32 指令内容
3 inst_addr_i 输入 32 指令地址
4 reg_we_i 输入 1 寄存器写使能
5 reg_waddr_i 输入 5 通用寄存器写地址,即写哪一个通用寄存器
6 reg1_rdata_i 输入 32 通用寄存器1读数据
7 reg2_rdata_i 输入 32 通用寄存器2读数据
8 csr_we_i 输入 1 CSR寄存器写使能
9 csr_waddr_i 输入 32 CSR寄存器写地址,即写哪一个CSR寄存器
10 csr_rdata_i 输入 32 CSR寄存器读数据
11 int_assert_i 输入 1 中断信号
12 int_addr_i 输入 32 中断跳转地址,即中断发生后跳转到哪个地址
13 mem_rdata_i 输入 32 内存读数据
14 div_ready_i 输入 1 除法模块是否准备好信号,即是否可以进行除法运算
15 div_result_i 输入 64 除法结果
16 div_busy_i 输入 1 除法模块忙信号,即正在进行除法运算
17 div_op_i 输入 3 具体的除法运算,即DIV、DIVU、REM和REMU中的哪一种
18 div_reg_waddr_i 输入 5 除法运算完成后要写的通用寄存器地址
19 mem_wdata_o 输出 32 内存写数据
20 mem_raddr_o 输出 32 内存读地址
21 mem_waddr_o 输出 32 内存写地址
22 mem_we_o 输出 1 内存写使能
23 mem_req_o 输出 1 请求访问内存信号
24 reg_wdata_o 输出 32 通用寄存器写数据
25 reg_we_o 输出 1 通用寄存器写使能
26 reg_waddr_o 输出 5 通用寄存器写地址
27 csr_wdata_o 输出 32 CSR寄存器写数据
28 csr_we_o 输出 1 CSR寄存器写使能
29 csr_waddr_o 输出 32 CSR寄存器写地址,即写哪一个CSR寄存器
30 div_start_o 输出 1 开始除法运算
31 div_dividend_o 输出 32 除法运算中的被除数
32 div_divisor_o 输出 32 除法运算中的除数
33 div_op_o 输出 3 具体的除法运算,即DIV、DIVU、REM和REMU中的哪一种
34 div_reg_waddr_o 输出 5 除法运算完成后要写的通用寄存器地址
35 hold_flag_o 输出 1 暂停流水线信号
36 jump_flag_o 输出 1 跳转信号
37 jump_addr_o 输出 32 跳转地址

下面以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
//指令编码中的第30位区分是add指令还是sub指令。0表示add指令,1表示sub指令
                     reg_wdata = reg1_rdata_i + reg2_rdata_i;//加法
                 end else begin
                     reg_wdata = reg1_rdata_i - reg2_rdata_i;//减法
                 end
        ...
     end
...

if和case情况要写全,避免产生锁存器。

下面以beq指令说明跳转指令的执行。beq指令的编码如下:

 beq指令的作用就是当寄存器1的值和寄存器2的值相等时发生跳转,跳转的目的地址为当前指令的地址加上符号扩展的imm的值。具体代码如下:

...
`INST_TYPE_B: begin
    case (funct3)//译码beq指令
        `INST_BEQ: begin
            hold_flag = `HoldDisable;
            mem_wdata_o = `ZeroWord;
            mem_raddr_o = `ZeroWord;
            mem_waddr_o = `ZeroWord;
            mem_we = `WriteDisable;
            reg_wdata = `ZeroWord;
//同add,没有涉及的信号置为默认值
            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
...

访存:

由于tinyriscv只有三级流水线,因此没有访存这个阶段,访存的操作放在了执行模块中。具体是这样的,在译码阶段如果识别出是内存访问指令(lb、lh、lw、lbu、lhu、sb、sh、sw),则向总线发出内存访问请求:

...
`INST_TYPE_L: begin
    case (funct3)
        `INST_LB, `INST_LH, `INST_LW, `INST_LBU, `INST_LHU: begin//译码
            reg1_raddr_o = rs1;//读取寄存器1
            reg2_raddr_o = `ZeroReg;//不需要读寄存器2
            reg_we_o = `WriteEnable;//写目的寄存器使能
            reg_waddr_o = rd;//写目的寄存器的地址
            mem_req = `RIB_REQ;//发出访问内存请求
         end
         default: begin
            reg1_raddr_o = `ZeroReg;
            reg2_raddr_o = `ZeroReg;
            reg_we_o = `WriteDisable;
            reg_waddr_o = `ZeroReg;
         end
    endcase
end
`INST_TYPE_S: begin
    case (funct3)
    	`INST_SB, `INST_SW, `INST_SH: begin//译码
        	reg1_raddr_o = rs1;//读取寄存器1
            reg2_raddr_o = rs2;//读取寄存器2
            reg_we_o = `WriteDisable;//不需要写目的寄存器
            reg_waddr_o = `ZeroReg;
            mem_req = `RIB_REQ;//发出访问内存请求
 	end
...

在译码阶段向总线发出内存访问请求后,在执行阶段就会得到对应的内存数据。

下面看执行阶段的内存加载操作,以lb指令为例,lb指令的作用是访问内存中的某一个字节:

...
`INST_TYPE_L: begin
	case (funct3)
		`INST_LB: begin//译码
        	jump_flag = `JumpDisable;
        	hold_flag = `HoldDisable;
        	jump_addr = `ZeroWord;
        	mem_wdata_o = `ZeroWord;
        	mem_waddr_o = `ZeroWord;
            mem_we = `WriteDisable;
//没有涉及的信号置为默认值
            mem_raddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:20]};
//得到访存的地址
            case (mem_raddr_index)
//由于访问内存的地址必须是4字节对齐的,因此这里的mem_raddr_index的含义就是32位内存数据(4个字节)中的哪一个字节,2’b00表示第0个字节,即最低字节,2’b01表示第1个字节,2’b10表示第2个字节,2’b11表示第3个字节,即最高字节
            	2'b00: begin
                	reg_wdata = {{24{mem_rdata_i[7]}}, mem_rdata_i[7:0]};//写寄存器数据
               	end
                2'b01: begin
                	reg_wdata = {{24{mem_rdata_i[15]}}, mem_rdata_i[15:8]};
               	end
                2'b10: begin
                  	reg_wdata = {{24{mem_rdata_i[23]}}, mem_rdata_i[23:16]};
              	end
              	default: begin
                  	reg_wdata = {{24{mem_rdata_i[31]}}, mem_rdata_i[31:24]};
               	end
			endcase
		end
...

回写:

由于tinyriscv只有三级流水线,因此也没有回写(write back,或者说写回)这个阶段,在执行阶段结束后的下一个时钟上升沿就会把数据写回寄存器或者内存。

需要注意的是,在执行阶段,判断如果是内存存储指令(sb、sh、sw),则向总线发出访问内存请求。而对于内存加载(lb、lh、lw、lbu、lhu)指令是不需要的。因为内存存储指令既需要加载内存数据又需要往内存存储数据。

以sb指令为例:

...
`INST_TYPE_S: begin
	case (funct3)
    	`INST_SB: begin//译码sb指令
        	jump_flag = `JumpDisable;
            hold_flag = `HoldDisable;
            jump_addr = `ZeroWord;
            reg_wdata = `ZeroWord;
//没有涉及到的指令设置为默认值
            mem_we = `WriteEnable;//写内存使能
            mem_req = `RIB_REQ;//发出访问内存请求
            mem_waddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:25], inst_i[11:7]};
            mem_raddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:25], inst_i[11:7]};
//内存写地址和读地址,两个地址是一样的
            case (mem_waddr_index)//mem_waddr_index的含义就是写32位内存数据中的哪一个字节
            	2'b00: begin
                	mem_wdata_o = {mem_rdata_i[31:8], reg2_rdata_i[7:0]};
                end
                2'b01: begin
                   mem_wdata_o = {mem_rdata_i[31:16], reg2_rdata_i[7:0], mem_rdata_i[7:0]};
               	end
                2'b10: begin
                	mem_wdata_o = {mem_rdata_i[31:24], reg2_rdata_i[7:0], mem_rdata_i[15:0]};
                end
                default: begin
                	mem_wdata_o = {reg2_rdata_i[7:0], mem_rdata_i[23:0]};
                end
          	endcase
     	end
...

sb指令只改变读出来的32位内存数据中对应的字节,其他3个字节的数据保持不变,然后写回到内存中。

你可能感兴趣的:(RISC-V处理器设计,单片机,嵌入式硬件)