吃透Chisel语言.39.Chisel实战之单周期RISC-V处理器实现(上)——需求分析和初步设计

Chisel实战之单周期RISC-V处理器实现(上)——需求分析和初步设计

需求分析

首先明确我们要做的是什么,这个在标题里面已经说明了,我们要做的是一个单周期RISC-V处理器

但光是个短语不足以支撑我们开展项目,我们需要对项目目标做进一步的明确,也就是需求分析。

关于指令集架构(ISA)

设计一个处理器的依据是指令系统规范,也就是ISA的规范,不严谨地来说就是该指令集架构的机器语言的规范,即计算机软件和硬件的接口。而设计处理器是在ISA规范的基础上,对微体系结构进行设计,所以经典的教材《计算机体系结构:量化研究方法》中就将计算机体系结构描述为指令集架构(ISA)和微体系结构的结合。

因此,我们第一步就是要明确,我们这个项目支持的指令系统规范是什么!

既然要做一个RISC-V处理器,那必然是要支持RISC-V指令集,我们可以在官网找到该指令集的规范文件:

Specifications - RISC-V International (riscv.org)

指令集规范中包含了非特权指令集(当前版本规范为Unprivileged Spec v. 20191213)、特权指令集(当前版本规范为Privileged Spec v. 20211203),以及一些仍然处于其他阶段的扩展规范。

我们这个项目的目标很简单,不需要支持特权指令,更不需要支持拓展指令(比如向量拓展、位操作拓展等),仅需要支持非特权指令集就行了,而且是它的一个子集

非特权指令集有以下基本子集:

基本集 说明 版本 状态
RVWMO WMO即Weak Memory Ordering,是RISC-V的内存一致性模型 2.0 正式批准
RV32I 最基本的RISC-V 32位整数指令集 2.1 正式批准
RV64I 最基本的RISC-V 64位整数指令集,可以看作是RV32I到64位的拓展 2.1 正式批准
RV32E 面向嵌入式微控制器的基本的RISC-V 32位整数指令集,可以看作是RV32I的精简 1.9 草案
RV128I 最基本的RISC-V 128位整数指令集,可以看作是RV32I、RV64I到128位的拓展 1.7 草案

既然是基本子集,那就必须得有一个,我们的单周期处理器是单线程的、顺序执行的,因此不需要考虑内存一致性模型。我们作为例子也只需要实现最简单的32位版本,因此考虑将RV32I作为我们项目的基本指令集。而RV32E虽然是RV32I的精简,但仍然处于草案阶段,就也不考虑了。

注意:这里的xx位指的是地址空间的位数。

还有一些拓展指令子集:

拓展集 说明 版本 状态
M 整数乘法、除法拓展 2.0 正式批准
A 原子指令拓展 2.1 正式批准
F 单精度浮点数拓展 2.2 正式批准
D 双精度浮点数拓展 2.2 正式批准
Q 四精度浮点数拓展 2.2 正式批准
C 压缩指令拓展 2.0 正式批准
Counters 计数器、定时器、性能计数器拓展 2.0 草案
L 十进制浮点数拓展 0.0 草案
B 位操作拓展 0.0 草案
J 对动态转译语言的支持拓展,动态转译也叫JIT,此拓展用于支持动态检查和垃圾回收 0.0 草案
T 事务性内存操作拓展 0.0 草案
P Packed-SIMD指令拓展 0.2 草案
V 向量拓展 0.7 草案
Zicsr 控制和状态寄存器拓展 2.0 正式批准
Zifencei 指令抓取栅栏拓展 2.0 正式批准
Zam 非对齐原子内存操作拓展 0.1 草案
Ztso RVTSO(Total Store Ordering)内存一致性模型拓展,是RVWMO的变体 0.1 草案

RV32I加上A拓展就可以支持操作系统了,但我们不需要,其他拓展更是不需要了。

我们可以根据需求选取需要的基本子集和拓展集,实现符合应用场景的处理器。由于我们的需求很简单,那么自然我们经过上面的分析,就可以得到结论:

我们只需要支持RV32I基本指令集!不需要其他任何拓展!

当然了RV32I中的指令我们也并非都需要,在后续的实现中我们还会进行少许的取舍。

关于微体系结构(Microarchitecture)

CPU的设计无非只有两部分,一个是数据通路,另一个就是逻辑控制,不管如何我们先确定我们在微体系结构上的需求。

如果你学习过《计算机体系结构:量化研究方法》或其他类似的教材,那肯定知道很多现代处理器中常见的技术,比如Cache、流水线、分支预测、SIMD等等,想想就让人害怕。不过好消息是我们这个项目暂时不涉及这些,不信我们先捋一捋:

  1. 内存层级方面:现代处理器都有Cache之类的东西,用来构成内存层级,然后用一些复杂的策略来保证Cache的命中率,而我们的项目中不需要内存层级,直接从内存访问指令、数据啥的就行;
  2. 指令集并行(ILP)方面:这里引入了流水线技术,紧接着为了解决数据冒险,引入了转发技术和动态调度技术(比如记分牌算法和Tomasulo算法),跟着一起出现的还有分支预测那些,另一方面引入了多发射技术,似乎越来越超纲了,但是我们不需要,我们是单周期CPU,没有流水线,而且我们一次只执行一条指令,也没有多发射,根本就没有指令集并行;
  3. 数据级并行(DLP)方面:显然我们不需要,因为我们不支持向量拓展,跳过;
  4. 线程级并行(TLP)方面:我们是单核CPU,不支持多线程,更不存在线程之间共享内存,也没有Cache啥的,因此我们同样也不需要这个,复杂的内存一致性模型完全不用考虑;

还有什么地址转换啥的,我们也不需要

捋完了可以发现,我们什么优化技术都不需要用上,简简单单就实现一个朴素的单周期RISC-V处理器就行了!

初步设计

需求分析结束之后,就可以开始我们的初步设计了!

RV32I指令集分析

通过上面的分析,我们只需要把RV32I中我们需要的指令给支持了就行了,那么我们从分析RV32I中的指令开始

RV32I指令集指令有6种类型:

吃透Chisel语言.39.Chisel实战之单周期RISC-V处理器实现(上)——需求分析和初步设计_第1张图片

其中:

  1. R类型即寄存器(Register)类型,有三个操作数,两个源操作数均来自寄存器(rs1和rs2),目的操作数为寄存器(rd);
  2. I类型即立即数(Immediate)类型,有三个操作数,两个源操作数分别来自立即数(imm)和寄存器(rs1),目的操作数为寄存器(rd);
  3. S类型即存储(Store)类型,有三个操作数,均为源操作数,其中寄存器rs1和立即数imm运算得到存储的地址,rs2寄存器的值为被存储的数;
  4. B类型即分支(Branch)类型,有三个操作数,均为源操作数,其中两个寄存器(rs1和rs2)的值用于比较,立即数imm的值为分支目的地址的偏移量;
  5. U类型即无符号立即数(Unsigned immediate)类型,有两个操作数,立即数imm为源操作数,rd为目的操作数,此指令用于将立即数加载到指定寄存器rd;
  6. J类型即跳转(Jump)类型,有两个操作数,立即数imm为源操作数,用于计算跳转目的地址,rd为目的操作数,用于记录跳转前指令的下一条指令的地址;

进一步地,我们分析RV32I中的所有指令,共计四十条,如下表所示:

吃透Chisel语言.39.Chisel实战之单周期RISC-V处理器实现(上)——需求分析和初步设计_第2张图片

指令格式是比较规整的,除了ECALLEBREAK以外,均显然符合上面的六种指令格式类型。

可以按照指令的功能对指令进行分类:

  1. 直接跳转类:包括JALJALR
  2. 条件分支类:包括BEQBNEBLTBGEBLTUBGEU
  3. 加载/存储类:包括LBLHLWLBULHUSBSHSW
  4. 算术逻辑运算和位运算类:包括所有加法、减法,按位与、或、异或,逻辑左移、逻辑右移、算术右移相关指令;
  5. 比较指令类:包括SLTISLTIUSLTSLTU
  6. 其他指令类:FENCEECALLEBREAK

再依次对这几个类指令的行为进行分析:

  1. 直接跳转类需要对PC寄存器的值进行直接修改,同时写一个寄存器;
  2. 条件分支类首先需要进行比较,然后根据比较结果选择是否修改PC寄存器;
  3. 加载/存储类需要访问数据存储,加载只读取数据,存储只写入数据;
  4. 算术逻辑运算和位运算类会对操作数进行运算,然后将结果写入目的寄存器;
  5. 比较指令类与算术逻辑运算和位运算类一致,但操作变成了比较;
  6. 其他指令中,由于不需要维护内存一致性和连续性,因此我们不需要实现FENCE,同样,由于不涉及环境调用中断和调试调试中断,所以我们暂时也不需要实现ECALLEBREAK

数据通路和控制逻辑的初步设计

根据上面的分析,我们设计的CPU中应该至少需要包含以下组件:

  1. 指令内存(MemInst):接收一个32位的指令地址,读取出指令;
  2. PC寄存器(PCReg):为指令内存提供指令地址,每个时钟周期地址都会+4,当前指令为跳转时,下一条指令为跳转目的地址,当前指令为分支指令且分支成功时,下一条指令为分支目标地址;
  3. 通用寄存器堆(Registers):可读可写的寄存器,接收寄存器号,为运算单元提供操作数,接收运算结果或从数据内存读取到的值;
  4. 数据内存(MemData):根据加载/存储地址,加载或存储数据,加载或存储依赖于译码器的译码;
  5. 指令译码器(Decoder):对指令进行译码,解析得到立即数、操作码、寄存器号等信息;
  6. 运算单元(ALU):根据操作数和操作码进行运算,运算结果写到寄存器,分支指令时将比较结果发送给PC,加载存储指令时计算地址;

这些组件只描述了数据通路,要使得CPU能正常运行,还需要良好的逻辑控制

控制逻辑需要根据译码结果对数据通路进行控制,可能需要以下几个方面:

  1. ctrlJump:指令是否为跳转指令?如果是,需要给控制信号到PC,要求在下一时钟周期修改为跳转目的地址;
  2. ctrlBranch:指令是否为分支指令?如果是,根据运算单元的比较结果(分支与否),决定是否让PC在下一个时钟周期跳转的分支目标地址;
  3. ctrlRegWrite:指令是否需要写寄存器?如果是,将运算单元的结果或从数据内存中读取的值写入寄存器;
  4. ctrlLoad:指令是否为加载指令?如果是,写入寄存器的值来源应该是数据内存;
  5. ctrlStore:指令是否为存储指令?如果是,将寄存器中的值写入数据内存;
  6. ctrlALUSrc:指令的操作数2是立即数还是寄存器值?根据此选择操作数2的值;
  7. ctrlJAL:指令是否为JAL指令?如果是,操作数1的值应当为PC寄存器的值;
  8. ctrlOP:为ALU指定具体的操作,加?减?或者其他啥?

这些控制信号的生成和传输我们统一由控制器(Controller)完成。

上面的说明并不详尽,只作为初步设计,但我们也很难在开始的时候就考虑到所有细节,更多的细枝末节需要在设计、调试、修改的迭代中完善,但至少上面的内容足够我们开始实现了。

最后放上初步设计的草图:

吃透Chisel语言.39.Chisel实战之单周期RISC-V处理器实现(上)——需求分析和初步设计_第3张图片

再次说明,上面的设计是不完备的,比如目前还未考虑到加载/存储时是字节、半字还是字,虽然只是一个信号的问题,但足以体现还有很多不完善的地方,在实现中迭代设计是很有必要的。接下来,我们就将基于这个不完备的设计开始我们的项目开发!

你可能感兴趣的:(吃透Chisel语言!!!,risc-v,Chisel,单周期,CPU设计实现,RV32I)