继电器是如何成为CPU的(计算机原理)
究竟是如何设计的电路,具有计算和控制的智力?
这一点也不高深。本系列文章从初中学的最简单的电路图说起,看看能不能从最初的有一个继电器的电路,画到一个简单的CPU。电路图用multisim12.0绘制,这样就可以看到效果了。
(注:虽然现代计算机是用半导体材料制作芯片的,但是电路原理和继电器材料没有不同。本人愚钝,只理解了继电器,还没有研究电子管、晶体管这些器件的工作原理。而继电器原理简单,用作讲解数字电路原理再好不过。)
从电池、开关和继电器开始
上图左上角是一个很普通的电路,由电池、开关电流表和灯泡组成。这个是初中物理学过的。
上图右上角是对左上角电路的简单变形,用VCC代替了电池,用接地符号实现了回路。相当于把电池拆开来画了。电路功能是一样的。本文此后的电路图一般都采用VCC和接地表示电源。因为你会发现只有这样的画法能够让我们方便理解之后的电路图。
然后往下看,第三个电路图,那个圈圈里圈着个K的蓝色的东西,表示的是继电器。当继电器左侧有电流通过时,右侧的电路就断开;反之则联通。(对应地,也有那种左侧有电流通过时,右侧的电路就联通;反之则断开的继电器,稍后会用到)这个电路想让你知道的是,可以用继电器实现这样的功能:控制左侧的电路的通断,即可改变右侧电路的通断。换个方式说,就是左侧的电路通断情况决定了右侧的电路的通断情况。再往抽象了说,就是左侧电路代表了'输入'(Input)这个概念,右侧电路代表了'输出'(Output)这个概念。继电器则代表了'计算'(或者'算法'、'处理'等)(CPU)这个概念。硬件就是用这种电器设备的固有物理性质提供了最初的计算能力。
再往下看第四个电路图,它和第三个的区别就在于用VCC和接地符号代替了电池。初次看电路,我们有必要经历一次从电池到VCC和接地符号的过渡。以后的电路图,我们将直接使用VCC和接地符号,请务必在此处适应这样的画法。
然后是最下边这个电路。和第四个电路相比,它用一个'非门'的符号代替了'继电器、右侧电路电源和继电器的接地'这三个符号。也就是说一个'非门'实际上就是'继电器、右侧电路电源和继电器的接地'这三个东西的组合,而且画的时候省略了电源和接地符号。
之前在学校里学数字电路,我就是想不明白逻辑门的电源在哪儿,整个电路的通路在哪儿,所以一直学不好。现在有个这个电路图,感觉一切豁然开朗。
用继电器做个与门
刚刚我们用继电器做出了'非门'。可以看到只要用一个继电器就可以了。与门的电路如下图所示。
与门由两个继电器(这两个继电器用的都是跟做非门的继电器通断情形相反的那种)构成,只有当两个继电器左侧都通电时,右侧电路才能联通。这样就实现了'与'的功能。'与门'的符号是一个躺着封口的U形。
注:本文里我做的电路图片都是GIF格式的,你可以在浏览器里看到随着开关的开闭,输入和输出电路上的灯泡是如何变化的。每个图上都有(http://bitzhuwei.cnblogs.com)标识我的博客地址,不过每个GIF图的最后一帧都去掉了这个标识。这样,看到一帧没有标识的时候,就知道下一帧将是GIF图的第一帧了。
用继电器做个或门
'非门'、'与门'做出来了,'或门'也就不难理解了,直接上图大家体会吧。(导线交叉的地方,如果有个红点,表示是联通的,否则就是互不相干的)'或门'的符号像一个子弹。
用继电器做个异或门
'异或门'的功能是:左侧的两个输入电路一个联通另一个不联通时,右侧电路联通;否则右侧电路不联通。这次先画符号,大家可以先自己尝试用继电器画'异或门',锻炼一下思维,然后再向下看答案。
答案揭晓!'异或门'的实现如下图所示。乍看很复杂,其实是有逻辑的。最右边的两个继电器其实是一个'或门',最左边两个继电器是两个'非门',中间两上两下共四个继电器,是两个'与门'。这样就好理解了。AB两个输入电路为A通、B不通的时候,整个'异或门'会通过下方的路线使灯泡亮;若A不通、B通,则会通过上方的线路使灯泡亮。其他情况灯泡都不会亮。
回到顶部(go to top)
做一些看起来可用的东西
与或非异或逻辑门电路虽然重要,却离构造CPU相距较远,做出来似乎也没什么成就感。那么我们用这些过于基础的器件,做一些有用的东西吧!
小小约定
为了表达方便,我们约定一下,逻辑门电路的输入(输出)线上有电流的时候,我们说输入为1(输出为1),否则就说输入为0(输出为0)。
振荡器
振荡器能够不断地输出0、1、0、1、0、1、0、1……这样的信号。这种东西虽然一时说不上有什么用,不过一定是有用到的时候的。振荡器做起来也简单得离谱。看下图所示电路。
把非门的输出端连到输入端上。当输出为1时,输入也就成了1,那么输出就得变成0;然后输入也跟着变成0,这样输出又变回了1。循环翻转无休无止。振荡器能翻转多快,就看继电器的反应有多快了。(现代计算机用晶体管代替了继电器,晶体管比继电器翻转速度快得多,所以能够得到的频率更高,计算机的速度更快。而且晶体管比继电器省电)话说multisim12仿真的翻转速度也够可以的,本人截图也费了不少劲。
顺便给一个用继电器做振荡器的实际应用的例子:上学的时候上下课的铃声就是用继电器做的振荡器完成的。0101信号用锤子敲打铁盔表达了出来。
加法器
刚刚约定了,用1和0表示电路的通断。电路只有通断这两种状态,所以计算机只用0和1来表示数,这就是二进制啊。关于二进制和十进制的内容别人写得太多了,我就不提啦。要计算两个多位二进制数的和,可以分别计算各个数位上的结果,而各个数位的计算方法又是相同的,即'被加数+加数=>和的值+进位的值'。这用逻辑门表示如下图。
A和B表示加数和被加数,C表示前一位计算的进位的值,C1灯泡表示本次计算的进位的值,S1灯泡表示本次计算的和的值。我找了半天,也没在multisim12里找到一位二进制数的加法器件的符号,大家看这个图就可以了。
要计算多位数,把单位数加法器首尾相连就可以了,如下图所示。这两张图是从《穿越计算机的迷雾》中拿来的。如果侵权,请告诉我,不然我就偷个懒不自己画啦。
寄存器
学汇编的时候听过这个东西,'寄存'这个词让我感觉寄人篱下可怜巴巴的。寄存器的功能是把数据(约定里说的0和1,实际就是电路的通断状态)保存下来,以后还可以取出来用。寄存器这东西比之前的器件都复杂,需要一步一步来做。
R-S触发器
先看下面这个电路,这是各种有存储功能的器件的基础。
上图展示的是在R和S开关都断开的情况下启动电路得到的结果。《穿越计算机的迷雾》里说这种情况下,哪个灯泡亮是不一定的,就看哪个逻辑门转的快了。不过我用multisim12仿真的时候,两个灯泡是在忽亮忽灭不停地闪。这可以说是非正常人类使用R-S触发器的情况。下面再看看正常人类使用R-S触发器的情况。
R-S触发器的R是Reset,意思是把Q灯泡重置为0,S是Set,意思是把Q灯泡置为1。可以看到,只闭合S,则Q亮NQ不亮;只闭合R,则Q不亮NQ亮;同时闭合RS,则Q和NQ都不亮;同时断开RS,则Q和NQ保持刚才的状态不变。
D触发器
既然要保存数据,我们就要求只在希望保存某个数据的时候存进去,否则就不理他。所以我们给R-S触发器加一个新的输入作为控制端,只有控制端为1的时候才能保存新来的数据。这里也把RS端合并为一个D端,用一个非门实现了只能使R和S有且只有一个开关是闭合的(即要么Q灯泡变为1要么Q灯泡变为0)。
D触发器的符号我也没在multisim里找到,大家看这个吧。
上升沿D触发器
我们只希望在某一瞬间把数据存起来,而不是像D触发器那样,控制端为1的整个时间端都会保存新数据。(这样最安全)于是我们在D触发器基础上设计了上升沿D触发器。这个触发器只在控制端从0变为1的瞬间存储新数据。是不是很奇妙的设计?
上升沿D触发器使用了两个D触发器,再加一个非门,就OK了。平时,我们把要存储的数据(D开关)放好。然后,当控制端CP为断开时,图中上面那个D触发器是能够存数据的,但是下面那个存不了。就是说新数据已经到了两个D触发器之间的导线上。在控制端CP闭合的瞬间,上面的D触发器无法再存新数据了,而下面的D触发器可以存新数据了,那么它存的是哪个新数据?只能是刚刚在两个D触发器之间的导线上的数据了。这个瞬间之后,新数据无法通过上面的D触发器,自然也就无法保存了。
这次我终于在multisim12里找到了上升沿D触发器的符号。
乒乓触发器
这是个有点类似振荡器的器件。两者的区别在于,振荡器是自动地改变输出,乒乓触发器是在输入一个上升沿的时候改变输出。电路图如下所示。
可以看出,乒乓触发器其实就是把上升沿D触发器的非Q输出端接到了输入端。很显然每次存的新数据总是和输出信号相反。
乒乓触发器可以用来做计数器。计数器有这样的功能:每收到一个上升沿的信号,就增加1。例如下图所示的能统计二进制的00000到11111(即0到31)这32个数。计数器的每一位计数器件都是一个乒乓触发器。灯泡亮表示1,灯泡灭表示0。
走马灯
利用上升沿D触发器还可以做'走马灯'。走马灯是每次都让前面一个灯泡亮的设备,最后一个灯泡亮过之后,又从第一个灯泡开始亮,循环往复。下图所示电路就是一个有5个灯泡循环走马的走马灯。为了在电路刚接通的时候让第一个灯亮起来,我加了两个开关S1和S2,大家可以分析一下怎么用S1和S2。这有助于加深体会上升沿D触发器的功能,并加速对更复杂器件的理解。
寄存器
终于到这个小玩意了!
能保存5位二进制数的寄存器如下图所示。其实就是5个上升沿D触发器并列起来而已。
想保存数据的时候,调整好D0、D1、D2、D3、D4五个开关(实际应用的时候就可能是其他电路的输出导线了),然后断开再闭合一下控制端的开关S5即可,数据就保存到了5个上升沿D触发器的Q端。
我这里只画了保存5位数的寄存器,是因为再画就太大了,在word里图就看不清楚了。我们知道现在计算机的寄存器已经到32位或者64位了。那就是有32或64个并列的上升沿D触发器组成一个寄存器。我们也看到了一个上升沿D触发器需要几十个继电器(晶体管),那么一个寄存器就需要上千个继电器(晶体管)了。而CPU里包含的寄存器、加法器等运算器还有各种控制器,其包含的继电器(晶体管)数目上百万也就很好理解了。内存条里的内存也是用上升沿D触发器和一个门电路组成存储一位(一个bit,8个bit是一个字节)的存储结构的,其包含的继电器(晶体管)数目可想而知有多少。
传输门
下图所示的传输门的作用是:当左边的'~1G'端输入为0时,左侧的1A4、1A3、1A2、1A1会直接传输到右侧对应的1Y*,就像一条线直接从1A*连接到1Y*一样;当左边的'~1G'端输入为1时,左侧的1A4、1A3、1A2、1A1都不能传输到右侧对应的1Y*,就像从1A*到1Y*的连线被剪断了一样。
注:本文里我做的电路图片都是GIF格式的,你可以在浏览器里看到随着开关的开闭,输入和输出电路上的灯泡是如何变化的。每个图上都有(http://bitzhuwei.cnblogs.com)标识我的博客地址,不过每个GIF图的最后一帧都去掉了这个标识。这样,看到一帧没有标识的时候,就知道下一帧将是GIF图的第一帧了。
传输门的原理很简单,就是在每个1A*到1Y*之间的连线上放个继电器而已,如下图所示。(取自《穿》)
寄存器
在上一篇已经说明了寄存器的原理,这里仅仅是为了说明'74LS194'这个带有各种无聊管脚的四位寄存器的用法。将'~CLR'、'S1'和'S0'置为1,'SR'和'SL'置为0,然后,'74LS194'就是一个简单的四位寄存器了。(本人在multisim12.0里只找到了这个靠谱的四位寄存器,凑合用吧。)
数值显示器
为了更直观地看到CPU的运算结果,我们将使用'DCD_HEX'这个东西。它能够把输入的'0101'显示为'5',把'1010'显示为'A'。
本文还要用一个四位的加法器,直接在下面这个简陋的CPU里看就好了,不再单独展示。
回到顶部(go to top)
一个简陋的CPU
现在一切就绪,可以开始设计CPU了!
CPU包括运算器和控制器两部分。我们首先做出运算器,然后逐步实现控制器,最后感受一下用机器语言编程的过程。本文实现的CPU虽然功能及其简陋,但是能够传达出当前真实CPU的原理。
运算器和手动控制器
现在我们要做的这个CPU,字长是4位,只能做两个数的加法。实现了运算器和手动版的控制器的CPU如下图所示。我们把这个版本称为version1的CPU。
上图中,'Add'是加法器,能执行两个4位数的加法运算。'RA'和'TR'是寄存器,'GAA'和'GBA'是传输门。'4''3''2''1'用来准备需要相加的数据(0到15),'KTR''KRA''KGA''KGB'是用来控制传输门通断和寄存器脉冲的开关。
在上图所示的GIF动画中,显示了'5+1+2+4'这个过程。这个过程可以分为4个步骤:①加载一个数值(Load);②加上一个数值(Add);③加上一个数值(Add);④加上一个数值(Add)。具体来说,每一个步骤要做的事情是:
指令 |
内容 |
加载一个数(Load) |
准备数据(0101); KGB↓, KGA↑; KRA↓↑ |
加一个数(Add) |
准备数据(0001); KGB↓, KGA↑; KTR↓↑; KGA↓, KGB↑; KRA↓↑ |
加一个数(Add) |
准备数据(0002); KGB↓, KGA↑; KTR↓↑; KGA↓, KGB↑; KRA↓↑ |
加一个数(Add) |
准备数据(0004); KGB↓, KGA↑; KTR↓↑; KGA↓, KGB↑; KRA↓↑ |
你可以看到,每次执行(Add)这一步,要做的事情(搬动开关)是一样的,规律性极强。这意味着可以用简化的方式控制'KTR''KRA''KGA''KGB'这几个开关的状态。经过简化的CPU就有一定的自动化控制的性质了,如下图所示。我们把这个版本的CPU称为version2的CPU。
Verison2要比刚才的verison1进化了一些。为便于理解,我们保留原来的'KTR''KRA''KGA''KGB'这四个开关(把它们挪到了右上角,因为实在没地方放了),但让它们永远保持闭合的状态。这是想说明:version2里新增的电路只是实现了更加自动化地控制'KTR''KRA''KGA''KGB'的开闭,它没有改变version1中电路的工作流程。
Version2中的'KLoad'和'KAdd'开关分别代表了'Load'和'Add'这两个指令。当'KLoad'闭合时,表示CPU要进行加载操作,这会把'4''3''2''1'上的数据存入寄存器'RA';当'KAdd'闭合时,表示CPU要进行相加操作,这会把'4''3''2''1'上的数据与'RA'当前的数据相加,然后相加的结果又存储到'RA'。
Version2中的'K0'和'K1'两个开关会依次的开闭,即两者总有一个是断开且另一个是闭合的(若出现其它情况那就是电路设计错了)。所以实际上'K0'和'K1'可以用一个'2位循环移位寄存器'代替。(为便于理解,仍然保留'K0'和'K1'这两个开关,只不过让它们永远保持闭合的状态)
Version2中,只需重复'准备指令,准备数据,执行指令(K↓↑↓↑)'这样的操作,就能完成version1中的控制功能。这需要一个从'KLoad''KAdd''K0''K1'到'KTR''KRA''KGA''KGB'的转换电路,即version2电路图中的上半部分,如下图所示。
这需要一点点设计逻辑电路的知识,本文直接列出真值表,据此即可画出转换电路。(想知道如何推导的话,请查阅《穿》或者数字电路类书籍)
Kload |
Kadd |
K0 |
K1 |
KGA |
KRA |
KTR |
KGB |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
其中'KLoad''KAdd''K0''K1'为输入,'KTR''KRA''KGA''KGB'为输出。只有如上三种输入情况下,输出部分会有1;其它的输入情况下,输出全部为0,所以就不需要列出来了。
举个例子,'KGA'='KLoad''~KAdd''K0''~K1' + '~KLoad''KAdd''K0''~K1'=('KLoad' ⊕'KAdd')'K0''~K1',据此可以画出'KGA'的转换电路。
在version2中,CPU要做的就是重复'准备指令,准备数据,执行指令(K↓↑↓↑)'这件事。其中执行指令这一步是完全重复完全自动化的(只要用振荡器替换K就可以),而准备指令和准备数据还需要手工操作。每次要执行哪个指令、要准备的数据是多少,这都是没有规律的,可改进的方法就是:把指令和数据按顺序保存到一些特别的寄存器里,需要的时候取出来用。这些特别的寄存器,就是内存。
内存
所谓饭前便后要洗手,一个完整的卫生间,除了有若干坑位,还得有洗手池配套。类似的,学习CPU的结构原理,也得把'内存'牵出来溜溜,否则无法说明计算机编程的本质。CPU+内存才是一个完整的计算机(核心),才能展现出CPU的功能。由此也能联系到,将鼠标键盘显示器等称为'外部'设备的道理。
最基本的内存单元能够存储和读写一个位(bit),是由一个上升沿D触发器和传输门组成,如下图所示。传输门电路我没有找到只有一位的,拿这个四位GAA的凑合看吧。
内存单元的符号如下图所示。(取自《穿》)
把8个bit单元的读写端分别连起来,就可以存储一个字节(8bit)。下图是能够存储5bit的内存单元。(取自《穿》)此图所示的结构,我们称之为'一层'。
存储4bit的一层的符号如下图所示。这时我学会了multisim12里的'层次块'这个东西,下图就是用层次块画的,这样可以将复杂的电路封装起来,省地方了,还能复用。所以说模块化的思想在硬件设计里就有了。('用层次块替换…'和VS里的'Extract Method…'功能是何其类似!)
用一层一层的内存单元,即可构成存储器。对于有8层的存储器,需要3个bit表示需要读写的层数(23=8)。地址译码器的作用是:输入101时,输出的第5个(从0开始计数)引脚为1,其余均为0。有了地址译码器,存储器对外只需很少的地址线(例如10根)即可使用很多层(例如1024层)。这也符合了软件设计中接口尽可能简单的原则。本文所用的RAM存储器的结构如下图所示。其中左边的'3-8translator'就是译码器。
其层次块符号如下图所示。
这个RAM有三条地址线(Addr3、Addr2和Addr1),能够表示23=8个字(层),每个字的长度是4bit。这个小小的RAM刚好够存储(5+1+2+4)这个示例的指令和数据,下面就用这个RAM继续进化CPU。
这里也顺便把'3-8translator'译码器的电路实现贴出来吧,如下图所示。
9位循环移位寄存器
在version2里用的'2位循环移动寄存器'只需要一个乒乓触发器(详见上一篇)就可以了,但是后面要做的全自动控制器,需要一个'9位循环移位寄存器',且这个寄存器要在加电时自动将第一个输出管脚置为1,其余为0。具有这样的功能的寄存器如下图所示。
如上图所示,第二行有4个D↑触发器,其中最左边的那个负责第一个输出管脚,即应该在加电时就置为1的那个管脚。这是通过左下方的电路实现的,其原理大家自己琢磨吧,无非是利用反馈电路实现了只生效一次这个功能而已。'9位循环移位寄存器'的符号如下图所示,其中'D0'是第一个输出管脚。
还有一个3bit的计数器,在上一篇里已经提过计数器,这里直接上图。
其层次块表示如下图所示。
自动控制器
有了内存,我们就要把指令和数据存进去。存储数据很好理解,是多少就写入多少。存指令之前,我们需要为每条指令分配一个4位的编码,比如0000表示Load,1111表示Add,然后用这个指令码控制'KLoad'和'KAdd'的开闭,所以这又是一个转换电路。有了这个转换电路,就可以只用'K'开关来完成'准备指令、准备数据、执行'这些操作了。全部自动化的CPU如下图所示。我们将这个版本的CPU称为version3的CPU。
这里的'ALU'是用层次块表示的version1里的电路,因为不这样的话,电路太大,而且不容易重点突出自动控制器的工作流程。同样的,'ShiftRegister9bit'、'Translator'、'Counter3bit'、'RAM8F4bit'都是层次块表示的电路。
'ShiftRegister9bit'会依次将输出端置为1,这可以从上方的三个数值显示器看出来。'Counter3bit'是3bit的计数器。'RAM8F4bit'是8层(每层4bit)的内存。'Translator'实现了控制信号的自动控制,'Translator'的内部实现如下图所示。
Version3所示的下半部分展示了译码电路,即把代表指令的4bit信号转换为指令信号的电路。由于我们指定Load和Add指令的代码(0000和1111)都是很规则的,所以译码电路也比较简单,如下图所示。
Version3的CPU用'9位循环移位寄存器'等器件实现了'取指令、分析指令,取数、执行'的全部自动化,其中取指令、分析指令、取数、执行分别占用了移位寄存器的t0- t2、t3、t4-t6、t7-t8这9个阶段。
注:如果用振荡器替换了'K',这个CPU会不停地运转下去,这样就得不到我们想要的结果0xC(十进制的12)了。所以还需要添加'停机'指令。不过到这里已经说清了CPU的控制器是如何一步步实现自动化的,不再继续讲述如何添加新的指令。
最原始的机器语言编程
在给出上文的自动控制器里,我们只说了从内存里取指令和数据,而没有说这些指令和数据是如何写进去的。其实写进去的过程就是(机器语言)编程的过程。最简单的,你可以用拨动开关的方式,调整好要写入的位置,再调整好要写入的数值,把指令和数据一个字一个字地写入内存。最初的计算机编程就是用类似这样的方式(打孔纸带)编程的。(这个过程实在无聊,本文就不展示了,有兴趣的话自己拿multisim玩玩就好~)
用助记符和一些宏来代替机器码,这就是汇编语言。用C语言这种方式封装了汇编语言的编程方法,就是面向过程编程。用C++\C# \Java这样的语言封装了面向过程的语言,就是面向对象的编程方法。用if(..){…}代替JMP指令这种东西比较容易想象,但用'封装继承多态'这种飘渺的概念代替面向过程编程就有点困难了。我在另一篇文章《用C表达面向对象语言的机制——C#版》中作了分析和总结,现在终于算是从继电器一路走到面向对象编程了。