注意:个人学习笔记要点记录,后续会完善更改,参考《从零开始写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指令的作用就是当寄存器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个字节的数据保持不变,然后写回到内存中。