P3-单周期CPU(Logisim实现)
仅凭阅读本文,您不可能系统地学会如何搭建单周期CPU。即便如此,您读完本文应该也有以下几点收获:了解用Logisim搭建CPU时的一种并不优秀的实现方法,以及这种方法是如何进一步优化的;了解课上测试的坑在哪里(本文以各种更新,“upd”的形式给出了相当多的真实遇到过的bug),了解课上测试的形式,让准备更有针对性。
另外,roife同学讲解了P3-P7的具体实现的思路,且阅读体验要更好。需要的话请移步他的博客,内容仅供参考。笔者由于水平非常拉胯,自愧写的cpu难登大雅之堂,如果后面有大佬看到本文,请轻喷。
upd:16进制码导入电路时文件头为v2.0 raw,还有就是不要用未定义的指令。
关于课上测试,可以看[这个学姐的博客](https://www.cnblogs.com/Happy-Huan/p/14055025.html)二战已过
作为一个刚接触培养工程能力课程的萌新,不能缺少重来一次的勇气(第一版十个小时造完了,一堆bug加没法很好地拓展),只有搭完了第一遍,才能真正体会到会遇到哪些问题,才会体会到高老板课件中的精髓,才能在第二版中更好地布线,命名以及合理使用tunnel。
坑点和技巧总结在后面,前面一部分搭建过程纯属个人深夜胡言乱语,请无视。
先看完那几页要求!
当时还没学懂理论,先搭建了一下各个部件。GRF不用说了,肯定是用P0搭好的,CPU可以重新来过,但是GRF必须复用。注意GRF要有Reset,总Reset按下的时候PC、DM、GRF都要回到原始状态(助教在讨论区说了)。IFU(取指令单元)我拆成了PC与IM两个子电路。注意PC有Reset,IM用的是ROM,这些只要仔细读过要求应该不会出问题。32bitALU也没什么特别的,只是考虑到为了方便以后扩展(比如加运算指令),我的ALUOp用了3位,也就是说ALU支持8种运算,目前只有无符号加减以及或运算。对于DM(我也不知道叫啥,反正是内存),注意其输入地址为5位(32bit*32要求),注意有reset,注意MemWrite的作用是为1的时候写入数据,是数据从外面进入内存,而不是从内存写出去!至此,我们上课讲的基础元件已经搭好了。但是我推荐大家搭建一个万能分线器,它的顶层长这样:
功能大家可以望文生义一下,有了他之后的顶层布线大家亦可以想象,会十分简洁,需要的信号只需要一条线就能引出来。
下面从数据通路开始连接,只靠课本或者自己老师(我的理论课老师不是老板)的课件对于7条指令的CPU也不会麻烦,但指令多了之后工程化方法会很好地让我们加指令与加元件。我是先从addu开始连的通路,具体连法课本或课件已经讲得很清楚了,不再详述。然后连lw和sw,随后又补上lui指令。此处lui指令我没有放入ALU,而是单独在顶层里面加了一个16位逻辑左移移位器,然后把这个结果和ALU的结果做一个多路选择,选择信号叫做AorS_sel,当然,这个选择出来的结果最后还是免不了与从内存中读出来的数据做另一个多路选择,信号叫做MemtoReg.在加这些I型指令的时候,务必仔细阅读MIPS手册,看看到底是0扩展还是符号扩展,需不需要左移两位之类的。最后我加上了beq,为了使得线不乱,我在IFU那里的算PC+4+offset处用了tunnel进行简化布线。
数据通路不难画,控制器需要想想,并且好好查表,造表。我把控制器分成了两部分,一个只看OpCode的部分,另一个是需要综合第一部分给出的ALUOp信号以及Func信号去生成ALUcontrol信号(位数自己看着来)。两部分的结构都是与或门阵列,第一部分是OpCode六位以及其取反的战场,第二部分是ALUOp及其取反以及Func及其取反的战场。为了连线,我们需要先查mips手册,连接与门阵列,然后再造表连接或门阵列。关于造表的顺序,下面简单说一下。先搬过来教程中的
我开始是个铁憨憨,横着填的表,这是一种麻烦的填表顺序,因为每填一行,需要把所有指令的数据通路都想一遍,想得脑壳疼。第二次搭建的时候我考虑竖着填表,每填一列只需要把该指令的通路走一遍,思考量从O(n^2)降低到O(n),还不容易错。表中数据的确定是由自己的设计决定的,不能与教程中给出的数据苟同。另外,对于R型指令,所有的R型指令在与门阵列中合成一个与门就行,叫if R,因为R型指令最终干什么,还是取决于Func,所以在第一部分控制器中所有R型指令统统归为一类即可。对于if R的结果,考虑到R型指令必然写入Regfile,所以是要和RegWrite连接的。至于第一部分中的ALUOp,值的情况取决于ALU的设计以及自己的规定,一种规则是000为加法,001为减法,010为或,011为比较,100为取决于Func字段。第一部分控制器的大体思路已经讲完了,下面考虑第二部分控制器的搭建思路。我第一次搭建第二部分的时候,由于考虑不周+没看高老板课件,出了严重的漏洞,至于哪里错了待会儿再讨论。一种正确(应该正确吧,等课上就知道了)的方案是仍然用与或门阵列。与或门阵列没看懂?对于R型指令,考虑一个例子:比如addu,它的Func是100001,需要在ALUOp为100时才会生效,所以,我们要把这对应的9条线(1取原信号,0取取反之后的信号)都连到addu的与门上才行。如图:
这样的话,如果ALUOp信号不是100的话,就说明不是R型指令,addu自然不能生效。相信有了这个例子,能更好地理解与或门中的与部分。由于学艺不精,我最开始不会搭这里,在室友提醒下才发现和第一部分控制器如出一辙(脑子笨不会融会贯通啊)有了这个例子,相信其他情况也容易类比实现。但是讲真的,第二部分控制器我设计的不好,用到一个常量0来辅助,要是加指令的话,我需要去掉0,并且思考如何扩展使得信号不会乱。这里是个雷,不知道周四能不能炸死自己。两部分都结束之后,控制器也就搭完了。控制信号我统统用tunnel连到各处的,这样感觉更加可读。
就这样,CPU就搭完了,它出生时是长这样的:
太难看,但是如果按照我写的思路搭的话,第二遍就是这样:
增加了信号的名字,并刻意加大骨干元件大小之后,看起来终于像一个CPU了
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
以下并非胡言乱语。
刚刚发现弱测结果终于出来了,WA了第三个点。看了讨论区,发现自己的MemAddr取错了(图中32 to 5封装起来的内部错了)。为了保证4的倍数,类似于PC部分的取址,应该取2-6位而不是0-4位!!!讨论区万能!!!这其实也是我的一个知识盲点:现代计算机按字节编址,如果写sw $s1,20($s2)的话,是存在第五个字里(32位机器)。而DM中一个地址是一个字,所以要除以4,即取2-6位!!
关于测试:http://shell-storm.org/online/Online-Assembler-and-Disassembler/ 反汇编转化网站,选择mips,bigendian,然后disassemble就行了。
更新:对于beq指令,在写测试程序的时候,判断相等一定要是两个寄存器里的数据判断是否相等,不能判断寄存器和立即数是否相等,这样会导致测试程序在电路上跑不出正确结果,但MARS上无所谓,MARS比较聪明,帮我们善后了。
更新:关于加指令时的完备思路:(这个思路可能仅适用于我画的CPU,因为我的控制器部分设计得非常糟糕,导致ALU控制器的功能并不纯净!但大家的CPU可能没有我这个特点)先跑一遍数据通路,看看是不是需要加mux和其他的元件(一般得加),在需要增加mux的地方在表中做一个标记,表示需要增加新的控制信号,然后看控制器,如果是I,J型指令,就先把这条指令的名字加到主控制器的与逻辑中,如果是R型指令,就把这条指令的名字加入到ALU控制器的与逻辑中。然后看看需不需要加新的控制信号,再之后连接控制信号或逻辑(注意,以前的控制信号也可能因为多了一条指令而又多了一种生效的情况),最后,把控制信号加到对应的mux上,写一个简单的程序验证一下,看看相应的寄存器以及内存中的值改变得对不对。
更新:如何将PC值直接置为0x00003000:考虑p0中所用的寄存器同步复位的方法。这里把图搬过来加深一下印象:
把0改成0x00003000就好了(课下测试部分关于PC的复位,用同步复位或者异步复位似乎都可以通过,也就是说,测试电路似乎保证了clear一直保持到时钟上升沿到来)
结果就相当于押到了一半的题。
结果就死在了异步复位到0x3000,彰显了菜鸡本色
那么,究竟怎么异步复位到0x3000呢?
要实现异步复位,一个必要条件是必须把reset接到寄存器上,否则,做不到异步。
考虑上面那个电路:在按下复位之后,寄存器被清零,0=0,所以MUX选择了0x3000输出,当下一个时钟到来之后,寄存器的值不再为0,这时候便是输出0x3004了,之后的过程就是在这个基础上不断加4,也就是实现了异步复位。这个方法不知道大家有没有在斐波那契那个问题中使用过,我真的是下了场之后才想到自己之前用这个电路做过斐波那契数列那个题,于是心中各种不甘心。。。
安利思考题的网站:https://wenku.baidu.com/view/f5db168fcc7931b764ce1545.html 可以参考,但最好还是自己想+查资料
更新:深夜总结:ALU选择信号3位还是不太够(可能是我的实现方式不好),所以考虑重新交一个4位选择信号的ALU,整个电路需要改动的地方挺多的,改之前先留一个保命版本确保自己能参加课上测试。万能分线器为了支持跳转指令,需要进行一定的增加。这些坑必须在测试之前解决掉,不然到时候改到怀疑人生。
带符号运算:按照指令集的意思,是溢出的话输出溢出标志,并且不写回数据。所以考虑加一个overflow,在ALU中,增加新输出溢出判断,溢出判断只有在真溢出时才为1。这个东西取完反之后和寄存器写使能做与运算作为“新”写使能(溢出时不写回去)
B开头的各种转移指令:类似于beq,需要增加多路器,控制信号可以考虑取反后再用一下
更新:当时在考场上发现最后提交的CPU中有一处明显错误,是调整与门的接口数量时导致的,有一根线没连上,不过还好我考试之前一直在想这个事情,上了考场先把那个错改过来了。这里提醒大家,当逻辑门的输入数量为偶数时,门最中间是没有输入口的,为奇数时最中间才是输入口。所以在盲改输入个数之后一定拖动着看看!
upd:课上测试的时候,先加最容易加的指令,比如运算指令,加这种指令的时候出错的概率比较小,可以加完之后交上去测一测,这样大概是能知道自己课下有没有bug的。课上新指令的bug和课下bug在错误测试点的编号上**可能**会有一定的分布上的差异(比如乱错或者只有前几个点错等),注意是**可能**!18级的P3P4具有那种特点,但P5P6就明显不是这样了。
upd:关于GRF,P0测得很弱,直接用P0的GRF的话建议仔细看看有没有错误,我第一次P3的时候旁边的同学因为GRF连错了一根线所以挂了。
11月13号更新:刚刚回顾了P3,发现这里的总结仍然有遗漏的地方,下面进行一些补充。
1.关于评测机的显然和实际不符的报错:讨论区中有人问过为什么评测机爆出来的自己出错部分的代码的行为反汇编之后正确的,但评测机却要求一个八竿子打不着的行为,和这条指令完全对不上。事实上,这是因为评测机报错报的是我们的电路在这条指令处出错,这条指令,是我们的电路目前执行的指令,报错有两种原因吧,第一种,是这条指令执行的时候我们有值赋值错误(这个大家应该容易发现),第二种,就是我们当前根本不应该执行这条指令,但却执行了它,即可能是跳转发生了错误,这时候我们就要仔细的检查一下自己的跳转指令有没有出问题。
2.关于添加指令的一些问题:首先,课下别自己添加太多指令,因为课上需要加一些奇怪的组合指令,这些在指令集里找不到,如果我们课下加了太多指令,课上的修改空间就会变得十分紧张;其次,加指令的时候别拘泥于理论课上的一些内容,比如理论课上加跳转指令j时,是多加了一个多路选择器,即用两个多路选择器选择三个信号,这样短期收益的确不错,并且我甚至倾向于推荐在课上连接电路的时候用这个方法来节省时间,但是到了P4,我再用这种多加多路选择器时发现加指令变得比较困难了:需要定义更多的导线,而导线的命名很大程度上影响了我们出bug的几率(我P4课下导线定义了60多行,费尽千辛万苦改导线,cpu勉强能用,但是我还是选择等P3重测完了之后重新再写一遍)其实我们完全可以把多路选择器选择信号位数增加,这样更符合实际的cpu的构造。
课上测试(11月14日重测版)
第一题,仍然是加一条跳转指令,为了避免和7号测试的jal重复,这次让加bal指令,看指令集容易理解,数据通路在jal的基础上几乎不用修改。推荐课下除了要求的指令之外,把jal,jr,jalr加进去,这样除非遇到一些奇怪的组合指令,否则就基本上只是加多路选择器和选择信号的工作量了。因为第一次复位踩坑挂掉,这次吸取了经验很快做掉了。
第二题,取2的对数指令,结果向下取整。指令集中仍然是以循环的方式给出的,但是也同样和第一次求前导1个数一样,可以利用BitFinder做出来:最高位1的所在位数(从零开始)就是取2的对数并且向下取整的结果。说实话重测有第一次的经验真的可以快很多很多,不会傻乎乎去搭建状态机了(加状态机目测也不对,可能就不是单周期能跑完的指令了),也没有考试快结束时才知道要用BitFinder的令人窒息的操作了。
第三题,添加lbu指令,这个也是指令集里面的,对着指令集的说明去好好想想,不难做出来。最后快俩小时一直提交不上去,不知道最后有没有对,我的疑问在大小端存储上,场上不清楚取一个字节是从左往右还是从右往左取,不过这并不是问题,这种提交不过,那另一种肯定能过了(只要没别的毛病)。
以后,我可以吸取进度最靠前的室友的经验了(但愿他能挺到最后)