在开篇中我们对RISC-V及其SoC软核工程进行了简单介绍,现在来介绍取指模块:
tinyriscv这个SoC工程的内核cpu部分,采用经典的三级流水线结构进行设计,即大家所熟知的:取值—>译码—>执行三级流水线。
另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。
目录
0 RISC-V SoC注解系列文章目录
1. CPU结构解析
2. 取值结构注解
2.1 pc_reg.v(时序逻辑电路)
2.2. if_id.v(时序逻辑电路)
2.3 rom.v
零、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模块(终篇)
在注解工程中的“取指”代码之前,先回顾一下CPU执行指令的简略过程(以三级流水线为例):
CPU内部主要由:
以上三部分组成。
那程序在CPU中是怎么执行的呢?程序执行过程中涉及到的组件在这个tinyriscv工程中是怎么对应的呢?
简化的CPU执行程序的过程如下:
程序经编译器编译后生成二进制指令码,烧写程序即烧写程序对应的二进制指令码,烧写程序到硬件系统中后,这些二进制指令码就保存在系统的硬盘中,当你打开一个程序的时候,就把对应的指令码+数据加载到了内存里,其中指令部分被随后加载到了CPU的缓存里面,然后CPU根据程序计数器,即程序指针PC指向的缓存地址,把存储的指令从缓存取到指令寄存器里面保存,这里是取指;你取过来的指令,是由操作码和地址码组成的,分别表示执行什么操作和对谁操作,但是这些指令需要控制单元里面的一个叫做译码器的东西来分析,分析这些取来的指令码到底是什么意思,然后根据分析出来的内容指定下一步的行动计划:去哪里找什么部件执行什么操作,这是译码。 随后数据寄存器把数据从内存里面加载到算术逻辑单元,进行运算,并把结果传回数据寄存器,这就是执行。
以上就是三级流水线CPU的执行过程。在此过程中涉及到的组件与这个tinyriscv工程的对应关系如下:
程序:嵌入式代码,如C代码;
编译器:gnu工具链;
硬件系统:嵌入此工程所编写的SoC软核的硬件平台,如FPGA;
硬盘:可以是上述FPGA板卡的板载flash,烧写代码时,可以将程序烧写固化在flash中,这样板卡掉电后,程序还会存在于flash中,在下次上电后,可从flash中读取;
内存:工程中对应rom.v外设,工程中提供的代码烧写方式是直接烧录在rom中的,板卡掉电后rom中的数据会丢失,需要重新烧写代码;
缓存:此工程比较简洁,没有使用缓存结构,将代码烧写进内存rom中后,cpu内核直接与rom进行通信,从rom中取出指令码,省去了缓存这一层级;
程序指针PC:本质上是一个32bit的寄存器,用于存放指令的地址,在工程的pc_reg.v文件中定义为reg[`InstAddrBus] pc_o;
指令寄存器:存放根据程序指针PC取出的32bit指令码,在工程中定义为inst_i;
回到tinyriscv工程中,首先看这个RISC-V内核的取指部分:涉及到pc_reg.v、if_id.v、rom.v等模块:
取指模块的整体框图如下所示:
取指模块的详细介绍如下:
主要功能为:对指令存储器的地址信号,进行复位、跳转、暂停、地址递增等操作,即是对指令的地址进行处理,用于产生PC寄存器的值,该值会被用作指令存储器的地址信号,用来从rom中读取指令内容;
pc_reg模块有3个主要输入,分别是:跳转标志jump_flag_i,跳转地址jump_addr_i,和流水线暂停标志hold_flag_i,这三个输入信号从crtl.v模块接入,还有1个由jtag调试模块接入的复位标志jtag_reset_flag_i,用于jtag下载调试。
1个输出信号pc_o,即输出指令的地址,输出给总线,通过总线将指令寄存器的地址信号送入ROM,之后可从ROM中,把具体的指令给if_id模块。
主要功能为:取指,并将从rom中读取的指令打一拍后送到译码模块,即将取指数据(三级流水的第一级)打一拍,送入下一级(译码)。
此处插播一条内容:为什么CPU内核运行,要有流水线操作,比如此处的三级流水(取指----译码----执行)?
答案播报(以下播报内容引用源代码作者的博客):
这与数字电路中的时序有关,用以下这个模型为例:
其中对时序影响最大的是上图中的组合逻辑电路。所以要避免时序问题,最简单的方法减小组合逻辑电路的延时。组合逻辑电路里的串联级数越多延时就越大,实在没办法减小串联级数时,可以采用流水线的方式将这些级数用触发器隔开。要设计处理器的话,流水线是绕不开的。当然你也可以抬杠说:”用状态机也可以实现处理器啊,不一定要用流水线。采用流水线设计方式,不但可以提高处理器的工作频率,还可以提高处理器的效率。但是流水线并不是越长越好,流水线越长要使用的资源就越多、面积就越大。在设计一款处理器之前,首先要确定好所设计的处理器要达到什么样的性能(或者说主频最高是多少),所使用的资源的上限是多少,功耗范围是多少。如果一味地追求性能而不考虑资源和功耗的话,那么所设计出来的处理器估计就只能用来玩玩,或者做做学术研究。tinyriscv采用的是三级流水线,即取指、译码和执行,设计的目标就是要对标ARM的Cortex-M3系列处理器。(播报结束)
输入信号:
输出信号:
若流水线暂停信号hold_flag_i有效,需要冲刷流水线,则设置inst_i、inst_addr_i、int_flag_i为默认值(32'h00000001、32‘d0、8'h0),然后传递给下一级模块。
要功能为:存储烧写的指令码,并根据PC寄存器的值向外输出指令码;
reg[`MemBus] _rom[0:`RomNum - 1];
定义一个32*4096的二维数组,作为存储数据的空间。即存储32bit的指令码,最多可以存储4096条指令码,4096这个维度即指令码对应的地址。
在数据存入和读出的过程中:
always @ (posedge clk) begin
if (we_i == `WriteEnable) begin
_rom[addr_i[31:2]] <= data_i;
end
end
注意到_rom的地址索引为addr_i[31:2],即指令地址addr_i的高30位,这是为什么呢?
在计算机体系结构中,一个字节可以存储8bit数据,而内存空间映射的地址,一个地址对应一个8bit数据,即一个字节的数据;一条32bit的指令码,由4个字节组成,在内存空间中占用4个地址,这也是为什么在pc_reg.v模块中,pc的值每次累加4'h4的原因:
else begin
pc_o <= pc_o + 4'h4;
end
但是在rom中,定义的_rom存储空间是32*4096的,4096这个维度是32bit指令码的索引,即地址,结构如下:
_rom中一个地址对应的是32bit数据,所以实际映射地址的4个地址的数据(4*8bit)对应_rom数组存储结构中1个地址的数据(32bit),但是_rom中的数据又是紧凑存储的,而pc程序指针数值上是以4为间隔递增,即pc值的低2位pc[1:0]是以4为周期变化,不能直接作为rom的索引地址,而pc[1:0]由0加4,对应32bit数据,pc[31:2]也递增1表示,与_rom索引变化一致,所以使用pc地址的高30位作为rom的指令索引地址。
另外,在实际移植到FPGA的过程中,需要注意所用FPGA芯片的资源容量,适当的调整这个二维数组的大小。