话接上文: 造个计算机--1、设计运算器
还从老冯的这张结构图说起来,上面的文章里面说完了图里面运算器的实现。万里长征迈出了第一步(理论上的,实际要做的时候,还有更多的细节问题,想想都汗啊。。), 接下来看看怎么把整个虚线框里面的内容实现。
从图上可以看到,运算器这块只负责读入数据,进行计算,然后输出数据。 黄色的是数据流,灰色的是指令流,这是啥意思呢?
让计算过程自动起来---机器指令
再看看当年无数大牛为之疯狂的牛郎星8800(有名有姓就有盖茨、乔帮主),看看这一闪一闪亮晶晶的小红灯。
巨牛的牛郎星 8800怎么算一把呢?睁大眼睛看看,见证奇迹的时刻到了:
闪亮你眼睛的加法看明白这个是什么意思不?翻译一下:
这是上面界面的一个逻辑示意图, 在这个操作界面上,左边的那个是做加法还是减法的控制器,现在板下来的表示是做加法。 有两排数字开头,开关扳上表示1,扳下来表示0。图中表示的是10110111(183)+ 00010110(22),下面闪闪的红灯就是计算结果的输出设备了,看来在老的体系结构里面,红灯也可以表示输出啊, 亮灯的表示是1,不亮的表示是0, 输出的结果是11001101(205), 第一个是溢出位,暂时不用管了。
要是觉得看二进制计算麻烦的话,推荐打开windows系统自带的计算器,在查看里面选择程序员型,那么你就会发现一个二进制、八进制、十进制、十六进制的切换,不需要自已算,在任何进制下输入数据以后,直接点击别的进制,就自动完成转换计算。
这样的机器虽然能够解决基本计算的问题,但是说实在的,确实非常不好用。比如现在需要做一个连续加的操作,假设我们希望先把三个数字加在一起,然后把另两个数字加在一起,最后再把另外三个数加在一起。如果是只有上面的运算器部分的话,我们需要把这些数字都写在纸上面,然后一个个地把数据按照二进制的格式输入进去,并根据计算结果显示的情况把数据抄下来,然后再继续进行计算。在这个过程中,需要不断地把数据操作过程在计算机外记录下来,就不停的balabala看小红灯了。
那么有没有办法让计算过程自动进行呢?懒惰是人类进步的根源,这样的方法必须要有啊!
首先,我们需要一种叫作内存的东西,能够把数据存储在计算机里面,并且能够保持一定的时间。可以把内存理解为一个一个的小房间,每个小房间都有一个门牌号,这就是地址,地址表示的是数据存储的位置,每个地址对应的数据有如数据的住所。内存的主要作用就是能够对数据进行存储、读取和修改。(关于内存的实现,除了上述提到的基本逻辑门的组合以外,还需要加上触发器设计,这部分相对独立,稍后再说。)
累加求和在内存中实现过程如图所示,这是一个在内存中计算求和的过程,在这里,为了表示方便,把里面关于二进制的表述都换成了我们较为熟悉的10进制,实际在计算机里面存储的都是二进制。在这里,每一个格子表示一个内存地址单元,里面存放的是相应的数据,左边是这些内存单元的地址编号,基本上所有的地址编号都是从0开始的,因此该图表示的是在内存的第0000号单元格中存放的数据是27,0001号单元格的数据是12,……。
为了完成上图所示的求和的操作,我们需要进行的操作是:
把地址0000中的数读取到加法器中(读取)
•把地址0001中的数加到加法器中(加)
•把地址0002中的数加到加法器中(加)
•把加法器中的数保存到地址0003中(保存)
•把地址0004中的数读取到加法器中(读取)
把地址0005中的数加到加法器中(加)
•把加法器中的数保存到地址0006中(保存)
•把地址0007中的数读取到加法器中(读取)
•把地址0008中的数加到加法器中(加)
•把地址0009中的数加到加法器中(加)
•把加法器中的数保存到地址00010中(保存)
•停止加法器的自动计算工作(停止)
在这里,我们需要用到4种操作,读取,加,保存,停止,对于这四种操作,假如我们想让计算机来自动执行的话,我们可以给这几个操作本身编码成数字。
操作编码对照表这样编码只是为了方便,并没有特别的原因。通过相应的转换以后,上述的相应计算操作即可编码成如图 所示的操作过程,存放在以1000 开始的内存地址中。
编码过程但是,实际上这样的编码序列还是无法自动运行,因为前面的每个操作都需要指定操作数据地址,因此,假设我们规定每个操作命令加上操作数据的地址为三个内存单元,并命名为指令,那么整个计算过程的编码如图 所示。
整个计算的编码过程这样计算机就可以根据存储在内存中的指令一条条地往下执行直到遇到停机指令,这样就可以让整个计算过程自动执行,从而让计算机根据写好的指令完成我们想要指定的计算步骤。
上述四个基本指令只是用于这样的连续累加所涉及的一些操作的示意,真正通用的计算机在进行运算时,需要设计更多的硬件来实现相应更多的指令。一个计算机系统支持的全部指令称为指令集。
遥想当年,当Intel和AMD公司还处于热火朝天的撕逼大战的时候,那个时候CPU的卖点都是说我有牛X的指令集,一个说我有MMX指令集,一个说我有3DNow指令集。现在好像这些都不是卖点了,好像也没有人在再在中关村以赛扬冒充奔腾I 装到电脑里坑外行。这一回头,想到当年青葱的装机岁月,都TMD的快二十年过去了,岁月真是被狗啃了一样的快啊。此处略去某中年猥亵大叔发呆发痴碎碎念xxxx字。
存还是不存 ? --内存
前面提到了关于内存的实现, 虽然这个东西要稍麻烦一点,如果要引入各种名词还得解释一番,本着最简单的剃刀原则,还是从实际要用的说起。 从哪里呢?还是从逻辑门吧。
D触发器示意图这个东西叫D触发器,由一个非门,两个与门和两个或非门组成,这个图和前面的图略有不同的,在两个输出的地方,都有一个线把数据重新接回到另外的一个输入或非门上去。或非门起到的作用就是对或的结果取反,也就是说只有在两个输入都是0的时候,或非门的输出结果才是1,在该图里面Q(念Q非,简书里面写得时候没有上在带横线的标记,凑合用中横线表示一下)位置的输出成为Q的输入,而Q的输出成为Q输入,这样意味着,两者之中任何一个为1的时候,另外一个就为0。
D触发器起到的主要作用是这样的:当时钟输入为1时,Q端输出与数据端输入是相同的。但当时钟输入变为0时,Q端输出将维持原来的数据端输入,再改变数据端输入不会影响Q端输出,直到时钟输入再次变为1为止。
怎么实现的呢?看下面这两张图,图里红色线条表示为1,黑色表示为0,
保持位为0,不允许数据端数据到Q 保持位为1,允许数据端输出到Q上图仔细看一下,Q的输出来源两个输入,这两个输入一个来自
Q,另外一个来自于前面与门的输出,当保持位为0时,从上面那个与门的输出为0,所以Q的输出就由Q来决定,如果在上一次的数据输入的时候,Q是1的话,Q就为0,00得1,Q的输出就为1,如果Q原来是0的话,Q为1,那么01得0,Q的输出就是0,这样就保持住Q在上一次的输出数据。具体更细的内容,可以参考《编码的奥秘》里面第14章反馈与触发器的内容。
所以其把数据端简写为D,保持位,或者叫时钟位,叫为clk输出结果的表示是这样的,
D触发器在这里为什么要把保持位称为时钟位呢,因为有了这个东西,我们能够像一闪一闪的电子表一样,在每一次闪烁的时候,产生计算结果,并显示出来。回想一下,从电子表、各种家电的控制面板,所有的结果全部都是在某个时间间隔上发生变化的,其实手机、电脑也都是这样的,只是因为时间间隔太短了,所以我们无法感觉。
有一个词叫CPU的主频,比如1G(10的9次方)的cpu,说得是其在一秒钟内cpu发生计算的次数,每一次的时钟周期就是用1秒除了1G, 1ns(纳秒),佛经里面说一弹指六十刹那,一刹那九百生灭,如果这里按照1秒弹指两次算的话,一秒之间,十万八千生灭, 比起一秒之间,CPU的十亿次计算,差了孙悟空翻一个筋斗云那么远的倍数,足见其强大的计算能力。
番外,请直接Pass,愿开脑洞者可细思 : 这里有个话外的问题, 就算CPU强悍到这个程度,计算结果也是在某个时钟周期上发生变化的,而我们世界,无时无刻不在变化着的,这样的变化,存不存在最小的时间单元呢?十万八千生灭,每一生灭之间,一个生命从生到死,走完一个轮回,轮回之下,时间焉能不再分?分而再分,可至无穷,无穷何在?如果存在最小的时间单元,那么意味着宇宙在某个时间间隔上是保持不变的,如果不存在的话,芝诺悖论就存在了,细思极恐!
扯回来,这样我们就得到这样一个触发器,图中三角符号表示有时钟信息输入,
D触发器示意图然后我们再加一个选择器,就得到了传说中的法宝--> 寄存器,
1位(bit)的寄存器这个寄存器可以实现本文最开始的时候提到的功能,即数据的小房间,如果load为1的话,out=in,如果load位为0的话,out始终保持为上一次时钟输入的数据, 这样的就实现了可以写入数据,可以存住数据的功能。
这个东西整了半天,能够存住多少数呢? 只能存住一个数,还是二进制数,也就是只能够存0或者1,不过没有关系,做整个电脑,最重要也是最核心的就是功能的实现,规模,那都不是事,有一个了那就有无数个。
w位的寄存器这里是一个w位的寄存器, 在本项目里面,是16位的计算机,所以需要的是一次能够存16位数寄存器,这样一次才能够存住16位的数字,这里是指存住一个数,也就是提到的一个指令或者一个数字。对应前面画的一个小方格。
然后,我们再堆啊堆,就能够堆出一个500M, 一个G,更多的内存,
内存的实现示意图在这里有三个输入,一个输出, address是地址,表示要对这些寄存器的某个进行操作,load是控制位,表示是读还是写,如果是写就把in的数据写到该位置上,如果是读的话,就把该位置的数据读出来,然后输出。
这里面有三角符号,就是时钟信号,时钟周期下,在时钟位上的输入信号是这样的,
时钟信号在这里,一个时钟周期是由两个部分组成的,在上面一半的时候,输入是为1,在下面这一半的时候,输入是0。暂时先不care关于信号,后面我们还需要再细细的要交道。
OK,内存就是这样了,存还是不存,这是个问题,取决于load位的控制,load由谁控制呢? 控制器。
CPU 的初步实现
OK,前面插了一杆子关于内存的实现,现在重新回到关于指令的问题上来。 还以刚才提到的连续对三个数进行相加的操作为例,简单说一下我们需要涉及到的指令。
在整个加法以过程中,除了停止指令以外,主要涉及的指令分为两大类,一类是对内存进行操作的指令,load和store,主要命令cpu对内存进行读和写操作,另外一个就是add指令,这个是在运算器内主要完成操作, 其实现在想想,所有实际CPU的操作也就是三类(内存访问、运算、其它,当然在我们设计的这个简化的CPU里面,只需要两类,不需要其它)。一般来说,完成一次加法操作涉及到三个数, 加数、被加数和求和结果(话说从小学开始一直到现在我都搞不懂加数和被加数分别是哪个,只知道+号两边的数相加,好像对于学习也没有什么影响,那么为什么需要这种概念呢?根据奥卡姆原则,这种东西就根本不需要存在,好吧,又扯远了。。),在实际的CPU里面,这些数据一般都是放在一个紧挨着运算器的位置,叫寄存器,然后CPU就不需要通过长长的总线跑到内存里面去把数都取过来再算,因为对于CPU强大的计算能力来说,数据在路上来回跑得时间,都够它算个几百次了。
再重新观察一下这个计算过程,
再看计算过程1) 从内存的0000位置,取一个数,假如是1,放到加法器,那么CPU需要知道我要取的位置是0000,然后还得有个地方存取回来的这个数1 ,
2) 然后再从内存的0001位置,取另外一个数,假设是2,然后相加得3,那么CPU需要知道我要取的位置是0001,然后还得有个地方存计算结果的这个数3,
3) 然后再从内存的0002位置,取另外一个数,假设是3,然后相加得6,那么CPU需要知道我要取的位置是0002,然后还得有个地方存计算结果的这个数6,
4) 然后把6这个数,存在内存0003的位置,那么CPU需要知道我要存的位置是0003
所以在我们设计的这个CPU里面,实际上只需要两个额外的寄存器就可以了,一个叫D(data),一个叫A(address),D用来指上面存的那个数,A用来存上面提到的内存的位置(专业术语叫地址,一说这个,又回到当年看黄皮汇编书的时代,那里好像光XXXX寻址就有七个方式, 让人根本是不明觉厉,话说很久以后我才明白寻址就是寻找地址的意思,然后告诉你找地址有七种方式,要知道那才是8086/8088的CPU指令啊,面对“相对基址变址寻址方式”这样的鬼东西,让人一看就很绝望和崩溃),所以在我们项目里面,寻址这种鬼东西,还是滚远点吧。
整个计算过程的结构如图所示,在CPU内部有两个寄存器A和D, 为了专门把对于A进行读写的指令分开来,在这里用一个单独的符号来表示A里面的内存位置,用什么符号呢? 就用@吧,比如@0004,就是把0004这个数放到A寄存器,然后运算的结果放到D里面,图里面在左边有一个M寄存器,这里不是一个固定位置,实际就是A寄存器所指的内存位置的数,在这里M=8。如果执行@0000,那么A=0000, M=1, 如果执行@0002,那么A=0002, M=3。
设计CPU的结构OK,根据现在设计的CPU,我们重新用我们设计的两种指令再把上面的过程编译一遍,一种是@指令,就是取址指令,我们称为A指令,另外的就是计算指令,简单点就叫C指令。
1) 从内存的0000位置,取一个数,
@0000 //说明,把A寄存器设置为0000, 这个时候M指的是A指向位置的数,所以M=1
D=M //把M的值,送给D寄存器
step 12) 然后再从内存的0001位置,取另外一个数,然后相加
@0001 //A指向0001, M=2
D=D+M //这个时候把D和M相加以后结果存到D里面,这个时候D=3
step 23) 然后再从内存的0002位置,取另外一个数,然后相加
@0002 //A指向0002, M=3
D=D+M //这个时候把D和M相加以后结果存到D里面,这个时候D=6
step 34) 然后把6这个数,存在内存0003的位置
@0003 //A指向0003, M为某个原来的数
M=D //这个时候把D的结果给M,也就是相当于把0003位置的这个数改成计算结果。
step 4好吧,到了这里,我们已经从最简单的逻辑门开始,一步步的走到了一个终于可以叫CPU的东西出来了(尽管像爱因斯坦的第一个小板凳一样,虽然图森破,但毕竟能跑起来,btw,长大以后才有人告诉我们,正如无数曾经的儿童教育故事一样,爱因斯坦小板凳这个故事也是编的。)
到了这里,我们的星辰大海的征途,总算是迈出了重要的一步,接下来这个简易的CPU如何能够做更多的处理,变成一个计算机呢, 且听下回分晓。
网友评论