话接上文: 造个计算机--2、设计CPU
关于造个计算机,现在已经说到了第三回了,前面提到了怎么设计运算器和内存,怎么在CPU内设立寄存器,实现把连续操作的三个数相加变成了可以描述的指令,现在我们就差输入和输出,就能够形成一个完整的冯氏结构的计算机了。
这张图描述的计算机的硬件整体结构,我们再重新看一下,咱们项目计划要完成的一个完整的计算机结构,应该是这样的:
从图上可以看到,在冯氏的结构里面,我们实现的只是硬件部分的设计,暂时先不考虑如何在这个计算机上进行软件编程(我们的目标是能够实现在这个计算机上通过编程实现原来红白机上那种小游戏,比如敲砖块什么的,当然,那些内容在软件部分实现),完整的计算机硬件体系,我们已经完成从了逻辑门到运算器、内存的设计,这一节我们需要把运算器、内存、控制器、输入和输入设备,整个连接起来,完成的是一个整体的计算机硬件体系结构。
在这里面我们引入一个叫体系结构的名词,具体的内容,可以参考百度百科,计算机体系结构
对于我们要做的这个模型级的小项目而言,是否理解里面的那些术语,然并卵。 简单来说,就是前面一再说到的老冯结构图的一个完整实现。在这个体系结构上,我们支持机器指令是什么,严格意义上来说,计算机没有软件也是完成可以工作的,当然在今天没有这些我们无法想像,但是退回五六十年前,当时的计算机可就只有硬件部分,所有的程序都是靠手动输入的。
让指令跑起来
下面我们来看一看,前面设计的这些命令是如何工作的,前面举例提到如何实现三个数相加的指令(见造个计算机--2,设计CPU),
@0000
D=M
@0001
D=D+M
@0002
D=D+M
@0003
M=D
实际上,这些命令严格上来说,并不是机器指令,而是一种汇编语言,这些汇编语言在我们的计算机上,对应的机器指令是这样的:
汇编代码 |机器指令 |
-----|-----|-----
@0000|0000 0000 0000 0000
D=M |1111 1100 0001 0000
@0001|0000 0000 0000 0001
D=D+M|1111 0000 1001 0000
@0002 |0000 0000 0000 0010
D=D+M|1111 0000 1001 0000
@0003 |0000 0000 0000 0011
M=D |1110 0011 0000 1000
那我们怎么样把这些指令输入到计算机里面,让计算机能够执行呢?
答案是 打孔的纸带。
打孔纸带
话说打孔纸带的源起,来自于IBM的创始人赫尔曼·霍尔瑞斯,他于1888年发明自动制表机——首个使用打孔卡技术的数据处理机器。自动制表机用于1890年以及后续的美国人口普查,并获得巨大成功。就是靠当初卖的这些大型的制表机,IBM的百年帝国才得以开始发家。
最初的打孔不是纸带,是卡片形式,是这样的
后来在计算机上用于输入输出的时候,就演变成这样的了
,
有没有一种久违的熟悉感油然而生,想到了答题卡没有?
在这个纸带上,其中打孔代表1,不打代表0。最初的计算程序,都是用手工用打孔器把每一条指令打在穿孔纸带上,然后再输入到计算机里面去。纸带打孔器上有八个孔。根据预先约定的位置,用一个钢顶针在给定的孔位把计算机一条指令在纸带的一行上钻出几个孔来,有点像给皮带打孔。一个程序少说也有几百上千条指令,所用的纸带足有好几米甚至十几米长,没有个三五天的时间是打不完的。
上研究生的时候,导师跟我们说起来他年轻的时候,是要申请机房的计算时间的,拿了一大卷纸带过去以后,去掉卡纸,拿错了纸带等原因以外,算出来的结果有错误的话,在里面寻找错误就是一个让人崩溃的事情,所以当年老一辈的程序员都有很强的能够在纸上把代码写出来一次通过的能力。与现在各种的IDE相比,那简单就是长矛与钢炮的区别。
于是上面的8行机器指令,落在纸带上就变成这样了, 这里用●表示打孔,○表示没有,那么上面的语句就变成这样的纸带命令。机器在运行的时候,就依次的读入纸带,通过打孔的信息,获得相对应的机器指令然后运行。
汇编代码 | 机器指令 | 打孔纸带 |
---|---|---|
@0000 | 0000 0000 0000 0000 | ○○○○○○○○ ○○○○○○○○ |
D=M | 1111 1100 0001 0000 | ●●●●●●○○ ○○○●○○○○ |
@0001 | 0000 0000 0000 0001 | ○○○○○○○○ ○○○○○○○● |
D=D+M | 1111 0000 1001 0000 | ●●●●○○○○ ●○○●○○○○ |
@0002 | 0000 0000 0000 0010 | ○○○○○○○○ ○○○○○○●○ |
D=D+M | 1111 0000 1001 0000 | ●●●●○○○○ ●○○●○○○○ |
@0003 | 0000 0000 0000 0011 | ○○○○○○○○ ○○○○○○●● |
M=D | 1110 0011 0000 1000 | ●●●○○○●● ○○○○●○○○ |
是按照什么样的规则,把汇编代码翻译成机器指令,并对应到纸带形状的呢?
--按照每台机器给定的指令集。
这就是我们接下来要讲的。
Hack计算机指令集
上面做为示例介绍的几个语句,只是我们这台计算机需要支持指令集中的少数一些。严格来说,一台计算机,之所以不同于别的计算机,就在于其支持的指令集的不同。
本项目的计算机,到目前为止,里面的指令部分都是来自于《计算机系统要素》这本书,因此遵守作者的命名,我们设计的这台计算机,正式的名字叫Hack(这个名字很酷啊,因为造这台计算机的就叫Hacker了。。)。 这是一台基于冯氏结构的16位计算机(一次计算的时候,操作的是对象是16位的二进制数据)。
现在我们正式完整的看一下Hack支持的指令,
A指令
第一部分是上面的@指令,叫地址指令,也称为A指令,在上面的语句中,@0000, @0001,@0010,@0011,这四句都是A指令。A指令的第一位是0,后面是具体的数值。
C指令
第二部分就是上面的除了@指令以外的其它指令,叫计算指令,也称为C指令。C指令是整个计算机的重点,几乎包括所有要做的事情,整个计算指令,基本上就是在回答三个问题:
- 计算什么
- 计算后结果存在什么地方
- 下一步做什么
在上面的语句D=M ,D=D+M,M=D中,涉及到了前两个问题,对于第三个问题,如果所有的计算问题都是按照顺序依次执行的话,那就不需要考虑。但是实际上程序的执行还有另外两种结构, 选择(比大小)和循环(如从1加到100),所以需要回答这个问题。
针对上面的三个问题,C指令的第一位是1,表示这是C指令,第二位和第三位默认为了,但是没有用。后面的十三位分三个区域。
其中第一个区域是计算区域comp( compute的缩写),一共是7位,第一位a=0时表示选择计算表左边的计算指令,a=1时代表选择右边的计算指令,后面6位代表了具体选择的指令。
在这些计算指令里面,可以对照一下在 造个计算机--1、设计运算器 里面的提到的那些计算指令,在这里全部都实现了。
第二个区域是目的区域dest(destination的缩写), 一共是三位,表示根据comp指令计算出来的结果应该存储的位置。
d1 d2 d3 | 汇编代码 | 目的地(存储后计算的位置) |
---|---|---|
0 0 0 | Null | 代表空,不存在任何地方 |
0 0 1 | M | 存到Memory【A】,存到A寄存器指定位置的内存处 |
0 1 0 | D | 存到D寄存器 |
0 1 1 | MD | 存到Memory【A】和D寄存器 |
1 0 0 | A | 存到A寄存器 |
1 0 1 | AM | 存到A寄存器和Memory【A】 |
1 1 0 | AD | 存到A寄存器和D寄存器 |
1 1 1 | AMD | 存到A寄存器、Memory【A】和D寄存器 |
第三个区域是跳转区域jump, 一共是三位,根据comp指令计算出来的结果,判断是否应该发生跳转,第一位规定,如果计算结果小于0时,发生跳转。第二位规定,如果计算结果等于0时发生跳转,第三位规规定,当计算结果大于0时发生跳转,具体情况如表示。jump跳转指令一旦不为0,表示的意思是跳转到由A寄存器指定位置的指令继续执行。
以上面的D=D+M为例解释,看看这条命令,用Hack计算机的指令应该怎么表示,
首先,由于是一条计算指令,所以第一位就是1,第二位和第三位不管。
然后看表现形式,其中 dest 是D, comp是 D+M, 省略了jump部分。
参考上面的comp指令表,D+M是在右边, 所以第一位a=1, 对应位置处的指令是000010,所以comp部分是 1000010。
根据dest表,因为目的是存到D寄存器,所以dest部分是010。
由于没有针对计算结果的跳转,所以 jump为000。
因此,D=D+M 汇编代码对应的Hack指令为 1111000010010000。同样可以翻译 D=M 对应的Hack指令为1111110000010000,M=D对应的Hack指令为1110001100001000。
通过一个从1加到100的C语言实现和我们的Hack机器汇编语言的实现例子,再深入的了解一下Hack机器的指令集。
看看用我们前面定义的Hack语言实现,
这是汇编到机器指令的对应关系,在这里i和sum,是数据变量的地址, LOOP和END代表着该条指令在内存中存储的位置,在Hack计算机里面这两部分内存是分开的。
汇编代码 |机器指令 |
-----|-----|-----
@i |0000000010000000
M=1 |1110111111001000
@sum |0000000010000001
M=0 |1110101010001000
(LOOP) |
@i |0000000010000000
D=M |1111110000010000
@100 |0000000001100100
D=D-A |1110010011010000
@END|0000000000010010
D;JGT|1110001100000001
@i |0000000010000000
D=M |1111110000010000
@sum |0000000010000001
M=D+M |1111000010001000
@i |0000000010000000
M=M+1|1111110111001000
@LOOP|0000000000000100
0;JMP |1110101010000111
(END)|
@END |0000000000010010
0;JMP|1110101010000111
这一串的神秘的0101就是从1加到100的Hack计算机上的运行的程序了,从前一个笑话,说是有台高级的计算机出了问题,请来了特别专业的工程师来维修,结果因为没有键盘,于是直接接了一个终端,然后就开始不断的0101往里面输入了几个小时,然后就修好了,其原理大抵如此。
计算机整体结构
OK,前期的准备工作基本上结束了,现在咱们来完整的看一下咱们的Hack计算机的结构。
内存
让我们从左到右看去,首先是内存,在我们的Hack计算机里面,内存是分成了数据内存和指令内存两部分的,数据内存里面放着在计算过程中操作的变量、数组等内容。而指令内存就是上面翻译完了以后机器指令存放的位置。
在上例中,i和sum都是变量,存在10000000开始的位置,而上面的十多行代码,都存在从0开始的位置上, 所以在@LOOP的时候是在100位置,即第4行的位置,同理END是在第18行,即二进制的10010处。
CPU
关于CPU,从最开始的与或非门一直在讨论这个问题,在这个图里面,运算器不论,在寄存器里面,除了在一篇里面提到的A和D寄存器以外,还多一个PC寄器,这个东东其实就是在执行程序时,CPU需要知道下一条要运行的指令的地址,在运行的时候,主要就是这两种情况, 1) 如果运算的结果没有跳转,那么pc里面的数就加1,指向下一条指令 2)如果当前指令包括了跳转指令,那么cpu将pc 更新为要跳转到的位置, 如上例中的 D;JGT指令,如果成立的话,就需要跳到18行去,这个时候PC就设置为18,在运行完这条指令以后,cpu就根据这个数据,自动跳转到18行,然后就接下来运行。
关于控制器,其实严格意义上来说,并不是一个独立的组件,而是起到控制作用,主要执行的任务包括1)指令解析:把读出来指令能够按照前面定义的A指令和C指令进行解析。 2) 指令执行: 发信号指示计算机的各个部分需要做的工作 3)读取下一条指令。
输入和输出
整体的计算机必须要包括输入和输出了。最经典的输入输出结构,就是键盘和显示器了。在 造个计算机--1、设计运算器里面也提到了,计算机的输入和输出设备是有很多的,实际的计算机在出厂的时候,并不知道它将来会接多少的外接设备,那么怎么解决这个问题呢?在Hack计算机这里,我们采了一个取巧的方法,实际上,所有的输入和输出的过程,我们都只操作内存。
显示器:
Hack计算机包括一个黑白的屏幕,256x512像素,屏幕的内容是由某片连续的内存构成的,物理屏幕上的每一行从屏幕的左上角开始,在内存中用32个连续的16位字节表示(32x16=512),用1表示屏幕为黑色,用0表示为白色, 可以看到图中的第三位和第四位是1,所以对应的屏幕上两个点就是黑色的。通过这些点的标黑白,就可以显示相应的字符等,图中右下角为字符“A”的像素的表示。
键盘:
键盘的原理与显示器的原理基本上相同,也是对应到一部分内存,只不过与显示器不同,显示器是从内存中读取数据进行显示,而键盘是把在键盘上敲击的键,写入到对应的内存单元中去。
从这张图里面可以看到上述描述的过程,在数据内存中专门开辟了两块内存,一部分是用于给显示器进行读取,另外一部分是供键盘进行输入。
这样设计最大的好处在于,对于CPU来说,输入和输出设备就不care了,我只需要把我想要输出的内容写到某个地方,外面的输出设备就会自已去那里读,然后进行外部的输出。 而对于输入设备,CPU只需要不断的去某个对应该设备的内存去取回来就行了。 这样的话,理论上就可以随便接各种各样的设备了。那么输入和输出设备又是怎么映射到这样的内存位置的呢?在实际的计算机中,有一个词叫驱动程序。在我们的Hack计算机里面,我们暂时不讨论,在实践篇的时候,再详细展开。
回顾
OK, 到了这里,关于计算机系统的硬件部分的原理已经基本上介绍完成(虽然里面忽略了很多,很多),考虑到做为一个项目的阶段性目标,接下来的内容将是对硬件的实践,争取能够实现在实际的硬件机器上把机器指令跑起来,然后再考虑本文开始提到的红色部分。
关于实践部分,如果有兴趣的话,可以报名参与,一起做一个。
请持续关注, 别走开,稍后将会更精彩,敬请期待 实践篇。