从零开始写RISC-V处理器【4】硬件篇(2)
本帖最后由 sky 于 2020-9-18 09:14 编辑从零开始写RISC-V处理器【1】前言
从零开始写RISC-V处理器【2】浅谈Verilog
从零开始写RISC-V处理器【3】硬件篇(1)
从零开始写RISC-V处理器【4】硬件篇(2)
从零开始写RISC-V处理器【5】硬件篇(3-完)
项目地址:https://gitee.com/liangkangnan/tinyriscv
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行,不发生跳转。
其他跳转指令的执行是类似的,这里就不再重复了。
4.7 访存
由于tinyriscv只有三级流水线,因此没有访存这个阶段,访存的操作放在了执行模块中。具体是这样的,在译码阶段如果识别出是内存访问指令(lb、lh、lw、lbu、lhu、sb、sh、sw),则向总线发出内存访问请求,具体代码(位于id.v)如下:
第2~4行,译码出内存加载指令,lb、lh、lw、lbu、lhu。
第5行,需要读寄存器1。
第6行,不需要读寄存器2。
第7行,写目的寄存器使能。
第8行,写目的寄存器的地址,即写哪一个通用寄存器。
第9行,发出访问内存请求。
第19~21行,译码出内存存储指令,sb、sw、sh。
第22行,需要读寄存器1。
第23行,需要读寄存器2。
第24行,不需要写目的寄存器。
第26行,发出访问内存请求。
问题来了,为什么在取指阶段发出内存访问请求?这跟总线的设计是相关的,这里先不具体介绍总线的设计,只需要知道如果需要访问内存,则需要提前一个时钟向总线发出请求。
在译码阶段向总线发出内存访问请求后,在执行阶段就会得到对应的内存数据。
下面看执行阶段的内存加载操作,以lb指令为例,lb指令的作用是访问内存中的某一个字节,代码(位于ex.v)如下:
第2~4行,译码出lb指令。
第5~10行,将没有涉及的信号置为默认值。
第11行,得到访存的地址。
第12行,由于访问内存的地址必须是4字节对齐的,因此这里的mem_raddr_index的含义就是32位内存数据(4个字节)中的哪一个字节,2’b00表示第0个字节,即最低字节,2’b01表示第1个字节,2’b10表示第2个字节,2’b11表示第3个字节,即最高字节。
4.8 回写
第14、17、20、23行,写寄存器数据。
由于tinyriscv只有三级流水线,因此也没有回写(write back,或者说写回)这个阶段,在执行阶段结束后的下一个时钟上升沿就会把数据写回寄存器或者内存。
需要注意的是,在执行阶段,判断如果是内存存储指令(sb、sh、sw),则向总线发出访问内存请求。而对于内存加载(lb、lh、lw、lbu、lhu)指令是不需要的。因为内存存储指令既需要加载内存数据又需要往内存存储数据。
第2~4行,译码出sb指令。
第5~8行,将没有涉及的信号置为默认值。
第9行,写内存使能。
第10行,发出访问内存请求。
第11行,内存写地址。
第12行,内存读地址,读地址和写地址是一样的。
第13行,mem_waddr_index的含义就是写32位内存数据中的哪一个字节。
第15、18、21、24行,写内存数据。
sb指令只改变读出来的32位内存数据中对应的字节,其他3个字节的数据保持不变,然后写回到内存中。
4.8 跳转和流水线暂停
跳转就是改变PC寄存器的值。又因为跳转与否需要在执行阶段才知道,所以当需要跳转时,则需要暂停流水线(正确来说是冲刷流水线。流水线是不可以暂停的,除非时钟不跑了)。那怎么暂停流水线呢?或者说怎么实现流水线冲刷呢?tinyriscv的流水线结构如下图所示。
其中长方形表示的是时序逻辑电路,云状型表示的是组合逻辑电路。
在执行阶段,当判断需要发生跳转时,发出跳转信号和跳转地址给ctrl(ctrl.v)模块。
ctrl模块判断跳转信号有效后会给pc_reg、if_id和id_ex模块发出流水线暂停信号,并且还会给pc_reg模块发出跳转地址。
在时钟上升沿到来时,if_id和id_ex模块如果检测到流水线暂停信号有效则送出NOP指令,从而使得整条流水线(译码阶段、执行阶段)流淌的都是NOP指令,已经取出的指令就会无效,这就是流水线冲刷机制。
下面看ctrl.v模块是怎么设计的。ctrl.v的输入输出信号如下表所示:
可知,暂停信号来自多个模块。对于跳转(跳转包含暂停流水线操作),是要冲刷整条流水线的,因为跳转后流水线上其他阶段的其他操作是无效的。对于其他模块的暂停信号,一种最简单的设计就是也冲刷整条流水线,但是这样的话MCU的效率就会低一些。
另一种设计就是根据不同的暂停信号,暂停不同的流水线阶段。比如对于总线请求的暂停只需要暂停PC寄存器这一阶段就可以了,让流水线上的其他阶段继续工作。看ctrl.v的代码:
第3~6行,复位时赋默认值。
第8行,输出跳转地址直接等于输入跳转地址。
第9行,输出跳转标志直接等于输入跳转标志。
第11行,默认不暂停流水线。
第13、14行,对于跳转操作、来自执行阶段的暂停、来自中断模块的暂停则暂停整条流水线。
第16~18行,对于总线暂停,只需要暂停PC寄存器,让译码和执行阶段继续运行。
第19~21行,对于jtag模块暂停,则暂停整条流水线。
跳转时只需要暂停流水线一个时钟周期,但是如果是多周期指令(比如除法指令),则需要暂停流水线多个时钟周期。
4.9 总线
设想一下一个没有总线的SOC,处理器核与外设之间的连接是怎样的。可能会如下图所示:
可见,处理器核core直接与每个外设进行交互。假设一个外设有一条地址总线和一条数据总线,总共有N个外设,那么处理器核就有N条地址总线和N条数据总线,而且每增加一个外设就要修改(改动还不小)core的代码。
有了总线之后(见本章开头的图2_1),处理器核只需要一条地址总线和一条数据总线,大大简化了处理器核与外设之间的连接。
目前已经有不少成熟、标准的总线,比如AMBA、wishbone、AXI等。
设计CPU时大可以直接使用其中某一种,以节省开发时间。但是为了追求简单,tinyriscv并没有使用这些总线,而是自主设计了一种名为RIB(RISC-V Internal Bus)的总线。
RIB总线支持多主多从连接,但是同一时刻只支持一主一从通信。RIB总线上的各个主设备之间采用固定优先级仲裁机制。
RIB总线模块所在的源文件:rtl/core/rib.v
RIB总线模块的输入输出信号如下表所示(由于各个主、从之间的信号是类似的,所以这里只列出其中一个主和一个从的信号):
RIB总线本质上是一个多路选择器,从多个主设备中选择其中一个来访问对应的从设备。
RIB总线地址的最高4位决定要访问的是哪一个从设备,因此最多支持16个从设备。
仲裁方式采用的类似状态机的方式来实现,代码如下所示:
第3行,主设备请求信号的组合。
第7~13行,切换主设备操作,默认是授权给主设备1的,即取指模块。从这里可以知道,从发出总线访问请求后,需要一个时钟周期才能完成切换。
第18~66行,通过组合逻辑电路来实现优先级仲裁。
第20行,默认授权给主设备1。
第24~35行,这是已经授权给主设备0的情况。
第25、28、31行,分别对应主设备0、主设备2和主设备1的请求,通过if、else语句来实现优先级。
第27、30行,主设备0和主设备2的请求需要暂停流水线,这里只需要暂停PC阶段,让译码和执行阶段继续执行。
第3647行,这是已经授权给主设备1的情况,和第2435行的操作是类似的。
第4859行,这是已经授权给主设备2的情况,和第2435行的操作是类似的。
注意:RIB总线上不同的主设备切换是需要一个时钟周期的,因此如果想要在执行阶段读取到外设的数据,则需要在译码阶段就发出总线访问请求。
完
页:
[1]