4.4 取指 目前tinyriscv所有外设(包括rom和ram)、寄存器的读取都是与时钟无关的,或者说所有外设、寄存器的读取采用的是组合逻辑的方式。这一点非常重要! tinyriscv并没有具体的取指模块和代码。PC寄存器模块的输出pc_o会连接到外设rom模块的地址输入,又由于rom的读取是组合逻辑,因此每一个时钟上升沿到来之前(时序是满足要求的),从rom输出的指令已经稳定在if_id模块的输入,当时钟上升沿到来时指令就会输出到id模块。 取到的指令和指令地址会输入到if_id模块(if_id.v),if_id模块是一个时序电路,作用是将输入的信号打一拍后再输出到译码(id.v)模块。 . 4.5译码 译码模块所在的源文件:rtl/core/id.v 译码(id)模块是一个纯组合逻辑电路,主要作用有以下几点: 1.根据指令内容,解析出当前具体是哪一条指令(比如add指令)。 2.根据具体的指令,确定当前指令涉及的寄存器。比如读寄存器是一个还是两个,是否需要写寄存器以及写哪一个寄存器。 3.访问通用寄存器,得到要读的寄存器的值。 译码模块的输入输出信号如下表所示:
以add指令为例来说明如何译码。下图是add指令的编码格式:
可知,add指令被编码成6部分内容。通过第1、4、6这三部分可以唯一确定当前指令是否是add指令。知道是add指令之后,就可以知道add指令需要读两个通用寄存器(rs1和rs2)和写一个通用寄存器(rd)。下面看具体的代码:
第1行,opcode就是指令编码中的第6部分内容。 第3行,`INST_TYPE_R_M的值为7’b0110011。 第4行,funct7是指指令编码中的第1部分内容。 第5行,funct3是指指令编码中的第4部分内容。 第6行,到了这里,第1、4、6这三部分已经译码完毕,已经可以确定当前指令是add指令了。 第7行,设置写寄存器标志为1,表示执行模块结束后的下一个时钟需要写寄存器。 第8行,设置写寄存器地址为rd,rd的值为指令编码里的第5部分内容。 第9行,设置读寄存器1的地址为rs1,rs1的值为指令编码里的第3部分内容。 第10行,设置读寄存器2的地址为rs2,rs2的值为指令编码里的第2部分内容。 其他指令的译码过程是类似的,这里就不重复了。译码模块看起来代码很多,但是大部分代码都是类似的。 译码模块还有个作用是当指令为加载内存指令(比如lw等)时,向总线发出请求访问内存的信号。这部分内容将在总线一节再分析。 译码模块的输出会送到id_ex模块(id_ex.v)的输入,id_ex模块是一个时序电路,作用是将输入的信号打一拍后再输出到执行模块(ex.v)。 . 4.6 执行 执行模块所在的源文件:rtl/core/ex.v 执行(ex)模块是一个纯组合逻辑电路,主要作用有以下几点: 1.根据当前是什么指令执行对应的操作,比如add指令,则将寄存器1的值和寄存器2的值相加。 2.如果是内存加载指令,则读取对应地址的内存数据。 3.如果是跳转指令,则发出跳转信号。 执行模块的输入输出信号如下表所示:
下面以add指令为例说明,add指令的作用就是将寄存器1的值和寄存器2的值相加,最后将结果写入目的寄存器。代码如下:
第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的值。具体代码如下:
第2~4行,译码出beq指令。 第5~10行,没有涉及的信号置为默认值。 第11行,判断寄存器1的值是否等于寄存器2的值。 第12行,跳转使能,即发生跳转。 第13行,计算出跳转的目的地址。 第15、16行,不发生跳转。 其他跳转指令的执行是类似的,这里就不再重复了。
|