Skip to main content

用RTL实现最简单的处理器(日记)(已完结)

这是b线的第二个任务(第一个是pa2),链接如下用RTL实现最简单的处理器 | 官方文档 (oscc.cc)

我现在基本上就是边学边做边写博客,这可以让我有一个明确的目标,但是可能存在很多理解上的错误以及我走的歧路。

我对数电还掌握得不好,于是我根据这些链接来复习:Verilator 使用指南 – USTC CECS 2023,以及南大数电实验的讲义,还有ddca(rv版),还有C++ 编程指南 – USTC CECS 2023

一.预备阶段

1.修改Makefile

首先,为了之后能自动仿真并且也能在nvboard上面跑,我决定先修改我的Makefile,并且写两份激励代码,实现键入make sim来自动仿真(也就是不接入nvboard),并且键入make nvb可以接入nvboard来仿真(其实我在预学习阶段就想这么做了)

为了做到这一步,我打算对我的Makefile做大手术,将两者剥离写入另外两个.mk文件中(其实我对于我的makefile早就看不顺眼了)

经过我的一番改造,成功把两者剥离,幸好没出奇怪的bug(我之前include的位置写错了,然后出过奇怪的bug),我就不把代码放上来了,毕竟makefile每个人有每个人自己的想法。

2.学习c++和verilator仿真

我现在其实还是不太会c++,完全是靠c去理解的,在写这么一个大项目之前我有必要了解一下c++,中科大的c++编程指南对我挺友好,也是从一个只会c的角度来理解c++,我通过其学习了函数重载,DPI-C等的一些基本概念。

二,处理器基本架构

说实话,我从没学过数电,从没学过处理器,对于处理器微架构更是一无所知,我仅根据我的理解和想象画出了如下架构图

这是我的第一版cpu架构版图,很简单,就是把讲义上说的各个模块放在图纸上然后稍微连一下线展示其中的逻辑关系

其实我就可以分模块实现然后连线了,但是在将每个模块真正实现之前,很有必要去修改一下我的架构,我觉得ysyx的讲义讲的太少了,确实让我不知所措,很怕到时候写完发现整体的思想思路有问题,于是我还是打算学习一下微架构,我看打算看南大数电实验十,和ddca第七章(对不起老师,我没有“独立解决未知的问题”,但是我是不会直接把别人的照搬的,我会在我的第一版上面做修改和添加,我感觉这个过程也是将自己的思想与前人的思想对比,发现自己的不足),看完ddca7.1节后,我学到了某些思想,如下:

1.handle一个复杂系统的一个方法是模块化,它将微架构分为两部分,第一是datapath,第二是control unit (对比我的第一版,我将两者混用并且没有十分简略,显得十分混乱没有逻辑)。

2.以状态机的视角来设计,先设计这个系统中的state的元件,然后用组合逻辑电路来计算state。

3.提醒我要注意时序,我现在设计的电路是单周期的,也就是一个周期执行一条指令,如何在设计中体现出单周期。

我将这三种思想来改进我的第一版

所以以下是我的第二版

这样看起来确实舒服了很多,我在做这个图的时候有一个最大的感觉,就是我是在连电路,而不是在描述一件事(做到这一点我只转化了一下视角:用状态机的视角而不是逻辑和行为的视角),而我的第一版特别像一个流程图。接下来我将来讲解一下我的设计。

我不太理解讲义上给的IFU模块具体的作用,我认为有点多余(可能是学艺不精),于是在我的cpu2.0中我就把它给删掉了,并且图中有两个EXU,但是这两个实际上是一个。

整体的思路是

1.从rising edge of clk开始,pc寄存器更新为nextpc(更新pc)

2.并直接将地址传给inst memory(这是一块充满inst的内存块),其根据地址输出指令到IDU中(取指)

3.IDU进行指令译码,译出操作码,各种操作数,将这些全部传给EXU模块(译码)

4.假设执行的指令是addi,首先EXU在GPR中根据寄存器地址取出rs1的值,(请注意,在这里我需要保证的是状态机的state在每个rising edge改变,所以上方的clk仅仅控制写操作,所以读操作是组合的,不受clk控制),再将rs1和imm放入ALU中运算,运算完将结果返回给EXU,EXU将WE3改成1,将写的数据放在WD3,地址放在A3。至此一个clk结束,在下一个rising edge时,写的数据将被写入。至此addi指令执行完毕。(执行)(回过头来看这里,我发现我对于EXU的实现还是一个行为建模的思想,而不是连电路的思想,于是在第三版我直接删掉了EXU)

接下来我将稍微分析一下时序,时序这个东西我有点忘记了,基本上就是,取指,译码,执行这些步骤的关键路径的时间加起来要小于clk,对于处理器架构和微架构的评价,有这么一个式子

这个余老师在b站上有讲过,我就不详述了。

之后我看了ddca 7.3节,就是讲单周期处理器设计的,在这一节,我终于搞明白了处理器微架构的设计思想,什么叫做把datapath和control unit分开,datapath包括state的元件,ALU等等,他们放置在电路中只要一通电datapath就开始运转了,但是,为了实现不同的指令功能,就需要很多个control signal,他们可以帮助控制哪些datapath是可以通过的哪些不能,比如控制alu的功能,写使能是否开启,再打个比方,即使指令中没有立即数,但是计算立即数以及存储立即数的datapath依旧存在,这时候就需要控制信号来挑选哪一条路径放行,然后,将这一系列的control signal合并起来,形成一个集合,就是control unit,control unit的输入是opcode,function3,function7,一系列的flag等等,输出则是control signal的集合,将这一系列的control signal连接到各个元件上,就实现了功能选择。(我只能说ddca讲的太好了)

基于上面的原理以及思想,我就可以对我的cpu2.0再进行改版升级,我的cpu2.0虽然已形成框架,但是再某些模块还是太抽象了,连线也不够,也没有贯穿datapath和control unit的思想,我将针对这些问题进行改进

以下架构图就是可以实现addi这一条指令的架构图,是cpu第三版

上述架构是仅可实现addi时的架构图,由于只需要实现一条指令,所以只要其实datapath就够了,没有必要有control signal来选择,但是为了之后的扩展,我暂时设定了三个个control signal,分别是Regwrite,ALUControl,IMMType,第一个控制GPR的写使能,用于把addi的rd的值写进去,设置为1即可,第二个控制立即数编码,最后一个控制alu的行为,设置为000(加法)即可。Data Memory由于无需与内存交互就暂时放在一边闲置了

我将对其的实现做一个简单的描述。

1.pc首先由于reset信号设置为0x80000000(reset指令我没画,但是是存在的),并计算出下一条指令卡在nextpc上

2.PC连上inst mem找到指令并输出,由于指令中的数据只会包含寄存器和立即数,并且由于rv32i的友好设置,将所有的放在同一个位置上,所以可以直接将其接入GPR和IMM(立即数扩展模块),注意,由于立即数的位置根据指令类型有所差异,所以传给IMM的包括了31到7位的指令,并之后将通过control signal进行选择,addi就是选择31:20,并进行符号扩展到32位

3.将寄存器取出的值和立即数扩展后的值进行相加操作

4.结果写回GPR中,在addi指令执行时,WE3始终为1,由Regwrite信号控制

其实如果你们也曾经看过ddca的话,应该发现我就是按照他的架构模板实现的,因为它写得太好了我找不到不用他实现的理由,但是我在其中也有我自己的尝试,比如我的第二版就是没有参考别人的架构图实现的框架(虽然说画的跟s一样)

finished(发表一下我对这个任务的看法,作为一个真正的初学者,完全由自己画出架构图还是太困难了,不仅需要深刻的了解数字电路,还需要有想象力与创造力,毕竟回头看这个架构简直是十分简单,但是要是不知道处理器的设计思想,不模块化设计,不深刻理解rv32指令集,不深刻掌握数电,真正从白纸做起,参照我的第一版(那根本就不是数字电路,而是一个流程图),还是有点难度的,可能只是因为我菜吧)

三,在NPC中实现addi指令

接下来就是按照上述的架构图先一个模块一个模块的实现,然后再在顶层模块连线,最后仿真。

立一个flag,我尽量不写出always块

emmm,在imm模块,遇到了第一个困难,它用行为建模实在是太直观了,在immtypr是000时,imm = {…..}。这直接一个简简单单的case语句就能实现行为选择,但是说好的尽量不写always块,我看看能否不用case语句实现。

这是我第一次数据流建模,预学习的数电实验我全是用行为建模的,想了很久,灵光一现,我的思路是先处理所有type的立即数编码,放在一些wire变量中,然后通过一个mux来挑选选择哪个放行。虽然很简单,但是我却深刻的理解了行为建模和数据流建模的思维方式完全不同,感觉数据流建模不存在什么功能选择,就是把每个结果先算出来再选择,而行为建模则是先选择再算(一点浅显理解)。代码如下:

因为现在只有i type的指令(addi嘛),我暂时没有用immtype信号,并且没接入mux选择器,这些之后再说。

之后的alu,由于我数电实验时是用的行为建模方式,所以我还得全部自己写一遍数据流建模(这就是偷懒的后果)并且之前的数电实验是以补码为前提的,但是rv指令集是存在unsigned类型的。

这是ddca上对于一个简单alu的实现,我的RTL代码也会根据这个进行实现,当然这个alu迟早是要扩展功能的。并且这个加减法没必要用mux,参考南大数电实验alu的图,用异或就行。

里面用了选择器模板,没有用任何行为建模,我还无法保证是正确的,只有全部写完仿真才行。

之后一个是IDU,由于现在只需要实现一条指令并且此时的control signal较少,我就直接赋值了,之后要引入一些decoder来完成选择性的赋值,代码如下:

接下来是编写顶层模块了,这个顶层模块太长了,但其主要就是把上面单个模块按照我的cpu3.0连线而已,要注意的是我还将一个inst和pc指令拉到了顶层模块,为的是在c++代码中实现内存访问时能访问这两个信号。

最后就是编写c++仿真代码,在仿真代码中实现内存。代码如下:

我实在不会写c++代码,所以我大多都是c语言风格写的。我写了四条指令,其实这指令的opcode没啥用,因为我暂时还没有实现idu的全部功能。

这四条指令分别的结果是(x0始终为0)

1.x1 = x0 + 5;

2.x2 = x0 + 1;

3.x2 = x0 + 2;

4.x2 = x1 + 5

最后x1 = 5,x2 = A。波形如下

完美完成!挺顺利的。

四,让程序决定仿真何时结束

成功,最主要的就是使用dpi-c机制,在IDU模块检测到是ebreak指令就调用npc_trap函数,由于需要调用函数,我就写了一个always块,没办法,要调用函数不得不写一个always块。

五,结语

通过这个任务我一方面知识上掌握了基本的cpu微架构实现方式,更重要的是在思维上,有了一种数据流建模的思想,不管是做任何大型的数电,不可避免的需要多功能,但是一个通电启动的静态电路是通过什么进行功能选择的呢,就是将datapath和control unit分开,datapath需要实现所有功能,而control unit就进行功能选择(类似于开关一样的东西),所以说control unit是对于datapath的一个结果进行选择,比如alu有加法减法移位比较….最终选择哪种结果就取决于control unit,不管是设计大到cpu还是小到一个alu,idu,都需要这种思想。最后,就是很重要的状态机思想,在时序逻辑电路中,状态机的state是一切的核心,应该围绕其进行设计,这是一个设计cpu微架构不错的切入点。总之,我写这个写了3天左右,收获还是很大的,并且,可以看到我上面很多文字都十分愚蠢幼稚,那都是我一步步摸索的痕迹,我也不打算删掉了。就这样吧。

完结撒花!(之后就要准备上大学第一次期末考试啦,没时间做ysyx了,祝我好运吧hhh)