一、RISC-V SoC内核——取指 代码讲解

在开篇中我们对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


0 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内核注解——总线

八、RISC-V SoC外设注解——GPIO

九、RISC-V SoC外设注解——SPI接口

十、RISC-V SoC外设注解——timer定时器

十一、RISC-V SoC外设注解——UART模块(终篇)

1. CPU结构解析

在注解工程中的“取指”代码之前,先回顾一下CPU执行指令的简略过程(以三级流水线为例):

CPU内部主要由:

  1. 寄存器(Register):多种寄存器各司其职,有程序计数器(即程序指针PC,Program Counter),用来记录要执行的指令的地址;指令寄存器,用来暂存指令内容;数据寄存器,一般为通用寄存器,用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。
  2. 控制单元(Control):以流水线的方式对指令进行取指,译码,执行,并且发出为完成每条指令所要执行的各个操作的控制信号。
  3. 算术逻辑运算单元(ALU,Arithmetic Logic Unit):用来执行定点或浮点算术运算操作、移位操作以及逻辑操作,也可执行地址运算和转换。

以上三部分组成。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_16,color_FFFFFF,t_70,g_se,x_16

    那程序在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;


2. 取值结构注解

回到tinyriscv工程中,首先看这个RISC-V内核的取指部分:涉及到pc_reg.v、if_id.v、rom.v等模块:

取指模块的整体框图如下所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 取指模块的详细介绍如下:

2.1 pc_reg.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模块。

2.2. if_id.v(时序逻辑电路)

主要功能为:取指,并将从rom中读取的指令打一拍后送到译码模块,即将取指数据(三级流水的第一级)打一拍,送入下一级(译码)。

此处插播一条内容:为什么CPU内核运行,要有流水线操作,比如此处的三级流水(取指----译码----执行)?

答案播报(以下播报内容引用源代码作者的博客):

这与数字电路中的时序有关,用以下这个模型为例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 其中对时序影响最大的是上图中的组合逻辑电路。所以要避免时序问题,最简单的方法减小组合逻辑电路的延时。组合逻辑电路里的串联级数越多延时就越大,实在没办法减小串联级数时,可以采用流水线的方式将这些级数用触发器隔开。要设计处理器的话,流水线是绕不开的。当然你也可以抬杠说:”用状态机也可以实现处理器啊,不一定要用流水线。采用流水线设计方式,不但可以提高处理器的工作频率,还可以提高处理器的效率。但是流水线并不是越长越好,流水线越长要使用的资源就越多、面积就越大。在设计一款处理器之前,首先要确定好所设计的处理器要达到什么样的性能(或者说主频最高是多少),所使用的资源的上限是多少,功耗范围是多少。如果一味地追求性能而不考虑资源和功耗的话,那么所设计出来的处理器估计就只能用来玩玩,或者做做学术研究。tinyriscv采用的是三级流水线,即取指、译码和执行,设计的目标就是要对标ARM的Cortex-M3系列处理器。(播报结束)

输入信号:

  1. 指令内容inst_i,从rom.v中读取得地址,读取地址是组合逻辑;
  2. 指令地址inst_addr_i,从pc_reg.v模块接入,向下一级流水译,即码模块传递;
  3. 流水线暂停标志hold_flag_i,信号位宽为3bit,从ctrl.v模块传入,根据不同的暂停信号,选择是否暂停整条流水线;
  4. 外设中断输入信号int_flag_i,由time.v模块传入,用于定时器中断发生时,暂停主程序流水线,转而执行中断操作。

输出信号:

  1. 外设中断输入的输出信号int_flag_o, 如果复位或者暂停流水线信号无效,则外设(timer)中断输入信号int_flag_i被打一拍后送到clint.v模块(clint.v模块是中断管理模块,后续会讲到);
  2. 指令内容inst_o,如果复位或者暂停流水线信号无效,则将由rom输入的指令内容inst_i打一拍输出到id.v译码模块和clint.v模块;
  3. 指令地址inst_addr_o, 如果复位或者暂停流水线信号无效,则指令地址inst_addr_i被打一拍,输出到id.v译码模块和clint.v模块;

        若流水线暂停信号hold_flag_i有效,需要冲刷流水线,则设置inst_i、inst_addr_i、int_flag_i为默认值(32'h00000001、32‘d0、8'h0),然后传递给下一级模块。

2.3 rom.v

要功能为:存储烧写的指令码,并根据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

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_12,color_FFFFFF,t_70,g_se,x_16

        但是在rom中,定义的_rom存储空间是32*4096的,4096这个维度是32bit指令码的索引,即地址,结构如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_9,color_FFFFFF,t_70,g_se,x_16

        _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芯片的资源容量,适当的调整这个二维数组的大小。

  • 32
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小wang的IC自习室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值