看我怎么从一个晶体管怼到整个CPU

我们可能都听说过这样的话:“CPU是计算机的大脑,所有运算都在这里完成”,“CPU是由数十亿个晶体管组成的”,“CPU只能识别二进制,类似于1001101100这样的数据”,“所有的编程语言最终都会被转换为二进制进行处理,因为CPU只识别二进制”……。时间久了,我们会把这些话当做约定俗成的公理来看待,很少思考这是为什么。

我有着20年编程经验,虽然日常工作中使用编程解决各类问题在我看来都是信手拈来,但有几个问题一直困扰我,CPU为什么能识别二进制?它的工作原理是什么?数十亿个晶体管起到什么作用?这些问题不明白虽然不会影响我的生活、工作,但会让我无法全面认知计算机运行的原理,总感觉缺少点什么,接下来我就带着大家一起看看CPU是如何工作的。

先来看CPU的构成,其实不只是CPU,也包括GPU、NPU……等等所有的芯片都是一样的,他们都是由很多个“开关”组成的,对,就是开关,你没看错!

就好像家里的水龙头,打开,水流出来,关上,水没了。当然芯片里不是真的有水龙头,只是个比喻。早期的计算机使用的是继电器开关,在一些工业电气设备中还能看到这种器件。

典型的外观就是一大卷铜线,旁边有几个弹簧片,弹簧片上有金属触点,当铜线圈通电时会产生磁场,吸引弹簧片产生位移,从而使金属触点连接或断开,本质上说就是一个开关,只不过它控制的是电流,而不是水流。

后来人们发现这种继电器开关故障率太高,哪怕是一只虫子爬进去,也可能造成开关短路,软件编程中的“bug”一词就来源于此。所以人们又研究出了电子管开关,这种开关摒弃了机械运动部件,故障率大大降低。

现在逛一些旧家电市场,可能还会看到使用老式电子管的收音机。但电子管由于体积大,功耗高,后来基本被晶体管替代了,除了一些专业领域,比如音响功放,因为电子管制成的功率放大器可以保证高品质的原声,所以音响发烧友,都会以拥有一台优质的电子管功率放大器为荣。

最后我们看看晶体管,它也是一个电流开关,相较于电子管,它可以做得非常小,功耗非常低,所以现在的芯片都是基于晶体管开关的。

当然这张图片是普通的晶体管,并不是芯片中微型晶体管的样子,晶体管可以比较大,在一些家用电器的电路板上经常能看到这样的东西,外观样式很丰富。同时也可以做的非常小,在一个指甲盖大小的区域里能集成上百亿个晶体管。

我们说过晶体管就是一个开关,类似于水龙头,水龙头控制的是水流,之所以打开开关,有水流出,是因为有水压的存在。

同样晶体管开关控制的是电流,之所以打开开关,有电流通过,是因为有电压的存在。我们用这个符号来表示一个晶体管开关。

上面是控制端,相当于水龙头的开关,当控制端施加一个高电位(可以简单理解为有电),输入端和输出端之间的通道打开,电流由输入端流向输出端。如果控制端是低电位(相当于没电),通道关闭,输出端没有电流流出。为了描述方便,我们使用0代表没电,1代表有电,一个晶体管开关会形成下面二种状态:

也就是控制端有电(1),输出端就有电(1),控制端没电(0),输出端没电(0)。所以一个晶体管开关只能有二种状态,要么是1要么是0,而1和0就是二进制,这也就是CPU只能识别二进制的道理。

本节我们介绍了芯片为什么只能识别二进制,下节开始我们会介绍由多个晶体管开关组合形成的基本门电路,若干门电路组合又能形成更复杂的存储电路、运算电路,然后是半加器、全加器……直到形成整个CPU!

----------------------------------------------------------------------------------

上节我们介绍了CPU的基本单元是晶体管开关,接下来我们就看看这一堆开关组合在一起能做什么?首先我们需要用晶体管开关制作几种最基本的建筑材料——门电路。

这有一个晶体管开关,我们在输入端添加一个通道(细线),这条线比其他线更细,留意这点。

然后我们人为规定控制端为输入端,新加的细线为输出端。

请注意,虽然晶体管本身有固定的输出、输出、控制端,但作为使用者我们完全可以根据需要调整输入端、输出端的定义,只要输入端的变化能引发输出端变化即可。

接下来我们分析输入和输出端之间的关系,当输入端是0(0代表没电,1代表有电),主通道关闭,从左边过来的电流无法到达右边,但可以经这条细线流出,所以输出是1。即输入是0,输出是1。

接下来输入是1的话,主通道打开,电流从左边流到右边,因为主通道通了,输出端(细线)就没有电流了。

我们可以做个类比,某天早晨打开水龙头,发现没水,一看新闻,原来市政主管道被挖断了,路面上都能游泳了,所以家里就没水了,这是同样的道理,因为水都从主管道流走了,家里的小管道自然就没水了。所以输入是1,输出是0。

我们用一个表格记录输入、输出之间的关系,会发现它们之间存在相反的关系,所以这个简单的电路称为非门电路,非就是相反的意思,门是一个形象的解释,就好像一个开关,门打开,人能进出,门关上,谁也不能进出。这个非门电路以后我们经常用到,为了简单起见,我用一个符号代表这个电路。其中的NOT就是非的意思。

然后我们看第二个门电路,这有二个晶体管开关,把他们首尾相连,然后将两个晶体管的控制端当做输入,分别叫输入A、输入B,最后一个晶体管的输出端当做输出。

然后分析下输入和输出的关系,当输入A=0,输入B=1时,第一个开关主管道关闭,第二个开关主管道打开,电流无法从左边流到右边,输出=0;当输入A=1,输入B=0时,也是类似,输出=0;当输入A=0,输入B=0时,输出当然=0;而当输入A=1,输入B=1时,二个开关都打开,输出=1。

用表格整理一下,会发现一个规律,只有当A、B都是1的时候输出才是1,可以用一句话描述“当输入A=1并且输入B=1时,输出才=1”,其他的情况输出都是0。这个电路称为与门电路,与就是并且的意思。这个门电路以后会经常用到,也用一个符号代表,其中AND就是与的意思。

接下来再用二个晶体管彼此相连,但不是串联,而是并联,也就是两个输入端连在一起,两个输出端连在一起。

注意,图中那个圆弧代表二条线不是相连,因为在平面上无法画出立体图,大家可以想象一下,二条线一个在上一个在下,没有任何接触。现在我们将两个晶体管的控制端分别当做输入A、输入B,输出端不变,接下来分析输入输出之间的关系。

当输入A=0,输入B=1时,下面这个晶体管打开,电流可以走下面通道从输出端流出,输出=1;当输入A=1,输入B=0时,上面这个晶体管打开,电流可以走上面通道从输出端流出,输出=1;当输入A=0,输入B=0时,二个晶体管都关闭,输出=0;当输入A=1,输入B=1时,两个通道都打开,输出=1。

使用表格总结一下,会发现这样一个规律:“只要输入A或者输入B有一个为1,输出就=1”,这个门电路称为或门电路,为了方便描述,使用这个符号代替或门电路,其中or就是或者的意思。

有了上面三种最基本的门电路,我们就可以组合出更复杂的门电路(姑且称为二阶门电路,与基本门电路区分),种类很多,比如与非门、或非门、异或门等等,我们举其中一个例子——异或门。电路是这样的,由二个与门、一个非门、一个或门组成。

电路分析用下面四张图来表示,大家可以自己推理一下,看结果是否正确。

最后用表格统计出来:

它的规律是:当输入A和输入B不同时,输出=1。我们用这个符号来描述异或门。

其他二阶门电路我们不一一介绍了,有兴趣的读者可以自己查阅资料,下节我们来看看由这些门电路如何制作加法器,实现加法运算。

----------------------------------------------------------

我们知道CPU的主要功能就是运算,加减乘除运算的基础是加法运算,所以我们先来看如何制作一个加法器。

我们所说的加法器当然是指二进制的加法,请看下图:

左边表格是二个一位二进制数相加的情况分析(二进制没有2,只能通过进位用10描述)。目标有了,怎么实现呢?之前介绍过的异或门的逻辑与此很像(右边的表格),我们看到前三种情况的加法,异或门可以直接实现,而最后一种情况1+1的结果是0而不是10,这里面其实缺少了一个进位,我们可以增加一个与门,解决进位的问题。

电路图是这样的,其中异或门的输出作为“和的个位数”,与门的输出作为进位。最终实现的结果如下表:

可能有人会想, 1+1=10没问题,但0+0=00,0+1=01很是奇怪,能否把前面的0去掉呢?其实我们不用担心,这只是与我们的日常思维习惯有些冲突,并不影响结果的正确性,毕竟在一个数字前面加0和不加0的结果都一样。

这样我们就实现了一位二进制数的加法电路,使用这个符号表示。

为什么叫“半加器”呢?因为还不完善,因为一个完整的加法必然要考虑多位数相加,刚刚分析的只是一位数相加,如果有多位数,必然要考虑前一位数字相加后可能有进位,这个进位也要加进来,所以还可以对这个电路继续完善,直至满足多位数相加,这个完善的加法电路就称为全加器,用这个符号表示。

有了半加器、全加器,就可以实现多位二进制相加了,例如二个八位二进制数相加的电路如下:

可简化为:

减法器的过程稍显复杂,因为减法不考虑进位,但需要考虑借位,而借的这一位如何记录,如何归还都是问题,所以我们要想办法将借位化解掉。我们先从熟悉的十进制减法入手,例如35-16,用借位法很容易得出结果19,但现在我们要避免借位,怎么做呢?我们可以把式子变换一下:

35-16=35-16+100-100=35-16+99+1-100=35+(99-16)+1-100

这样99-16就不涉及到借位,因为对于二位数来说99是最大的。我们继续变换式子:

35+(99-16)+1-100=35+83+1-100=118+1-100=119-100

这时又遇到减法,但是不涉及借位,只需把百位的1去掉即可,结果是19。虽然看起来绕了很大一个弯路,但我们成功地避免了借位。

接下来我们来看二个八位二进制数相减的例子:

参考前面十进制减法的例子,把这个式子转换为:

这其中涉及到二次减法,其中:

观察一下结果会发现,差和减数是按位取反的,也就是每位0、1正好相反,这个逻辑可以使用如下电路实现:

但问题是,该电路只会对输入取反,而我们要做的是既能做加法也能做减法的电路,所以应该在减法时实现反转,改造电路如下:

当做减法时,取反端=1,才对输入取反,如果是加法,取反端=0,输入不取反。我们将这个电路简化一下,称为求补器:

有了求补器,我们就可以将第一个减法变为加上取反后的结果:

这样就只剩下最后一个减法,减去100000000,这个逻辑很简单,只需将首位变为0即可,当然是在做减法的时候。终于我们将所有的减法都化解掉了,现在我们使用加法器,再配合异或门就可以实现加减法运算。

图中有三个SUB端,这就是加减法的切换开关,当SUB=0时,进行加法运算,当SUB=1时,进行减法运算。在减法中,输入B的数据会先通过求补器进行取反,然后再和输入A相加。另外通过加法器的CI(进位输入)可以实现结果+1(因为SUB=1),最后加法器的CO进位输出(也就是结果的首位数据)通过一个异或门处理,减去1,最终得到最后的结果。

至此我们的电路已经能做加减法了,而乘法就是多次加法,除法就是多次减法,所以我们也就能实现乘除的运算了。

--------------------------------------------------------------

日常生活中有很多涉及到逻辑运算的地方,例如“如果这个周末天气好,我们也有空,就会去郊游”,这句话里就有逻辑运算,“周末天气是否好”、“我们是否有空”这是二个条件,只有当二个条件都满足时,“是否去郊游”这个结果才是肯定的,如果有一个条件不满足,郊游就泡汤了。很明显,这个逻辑与之前介绍的与门电路非常像,所以用门电路非常适合做逻辑运算,换句话说CPU不仅能做算术运算,逻辑运算更是它的拿手好戏。例如“判断一个数字是否是负数”、“判断所有输入是否为0”等等。

下面这个电路就可以判断所有输入A1-A8是否都是0,只有全部是0,最终输出才是1,否则输出就是0。

目前为止,我们将晶体管开关进行巧妙的组合,已经能够实现算术运算、逻辑运算了,按照惯例,我们将这样一堆电路进行封装,简化为一个符号,它被称为算术逻辑单元(或运算单元),简称ALU。

它具有二个8位二进制作为输入信号,同时还要告诉它做什么运算(加、减……),所以我们用一个4位二进制表示操作运算符(例如1000代表加法,1100代表减法……),输出结果也是8位二进制。与此同时,ALU还要输出一些标记,这些标记只有1位二进制,代表某种状态。例如如果输出结果为0,是否为0标志位就是1,如果输出结果为负数,是否负数标志位就是1,如果运算出现溢出(进位产生的),溢出标志位就是1。

至此我们已经创建了CPU的核心之一ALU,接下来我们还会了解计算机是如何存储数据的,最后我们会完整搭建一颗CPU!

------------------------------------------------------

上一节我们创建了算术逻辑单元ALU,它可以进行算术、逻辑运算,但计算出来的结果如何保存呢?这就需要用到存储单元,接下来我们看看用什么电路能够保存数据。

由浅入深,我们先来存储1位二进制数。先看这个改造过的或门电路。

它的特殊之处在于其输出信号会作为输入信号之一,看起来很奇怪是吧。一开始A、B都为0,输出=0,当A=1时或门输出=1,那么B=1,这时A、B都是1,输出还是1,保持不变,接下来即使将A设为0,输出依然是1。于是这个电路可以保存信号1。但我们也会发现一个问题,即这个保存是永久性的,不论A的值是什么,输出都是1,这显然不符合实际需求,保存的数据应该可以修改才对,这个问题暂时放一放,后面再解决。

接下来我们再看这个改造过的与门电路,同样它的输出信号也作为输入信号之一。

一开始将A、B都设为1,输出=1,当A设为0时,输出=0,进而B=0,输出还是0,保持不变。接下来不论A如何变化,输出始终保持为0,所以这个电路可以保存0。

然后我们开始着手解决如何修改保存内容的问题,设计如下电路:

其中“输入”端的信号就是这个电路要保存的值,“允许写入”代表是否可以修改保存值,如果是0,代表不能修改,如果是1,代表可以修改,“输出”端的信号就是保存的值。这个电路大家可以自行分析,我将四种不同的情况列在下面供大家参考。

允许写入情况,输入端信号1会保存起来

允许写入情况,输入端信号0会保存起来

禁止写入情况,电路原本保存1,不论输入端是什么都没有影响

禁止写入情况,电路原本保存0,不论输入端是什么都没有影响

我们用一个简化的符号代替这个电路,这个电路可以保存1位二进制数,并且可以随意修改,我们把它叫做锁存器。

当我们将8个锁存器放在一起,就可以保存8位二进制数了,这样一组锁存器被称为寄存器。早期电脑使用8位寄存器,即由8个锁存器组成,能保存8位二进制数,后来出现了16位寄存器、32位寄存器,到现在的电脑基本都是64位寄存器了。

接下来我们设计一个能存储256位二进制数的电路,如下图:

我们共需要16*16=256个锁存器,将他们设计成16行16列的矩阵,为了能准确存取数据,我们需要知道每个锁存器的位置,所以需要一个额外的电路确定要操作的锁存器的地址。如果要确定地址,必须知道行数、列数,一共16行、16列,所以行和列各用4位二进制数表示就行(0000代表第1行,0001代表第2行……,1111代表第16行,列也是一样的),这样就需要8位的地址输入信号。受篇幅所限,细节的电路不再画了,最终我们有了一个能存取256位二进制数的电路,用这个图形表示。

“8位地址”用于定位锁存器的位置,“允许写入”=1时,“数据”被存入某个锁存器,“允许读取”=1时,某个锁存器的数据可以被读取出来。

一个这样的电路还是没什么卵用的,还需要继续扩大规模,我们将8个相同的电路如图所示连接在一起。

这样我们一次就可以读写8位二进制数了,8位也叫做一个字节(Byte,简称B)。同样,我们简化一下,使用下图表示。

这个电路有256个地址,每个地址可以读写一个8位二进制数,一共可以保存256个字节(256B)的数据。这个容量的存储空间能够保存什么呢?很遗憾,现在的文档、图片动辄以KB、MB为单位,似乎什么也干不了。

补充一下小知识,1GB=1024MB,1MB=1024KB,1KB=1024B。

至此我们使用多个锁存器构建出了可以存取数据的内存,虽然只有256B,更大的内存也是同样的原理,下节我们就开始打造完整的CPU了!

----------------------------------------------------------

前面我们介绍了所有的基础零件,接下来就可以构建CPU了,这节的内容会比较多。

既然开始构建CPU,就少不了程序,因为CPU就是用来执行程序的。我们知道任何编程语言编写的程序最终都会转换为二进制,所以这里我们直接使用二进制的编程语言(机器语言)。比如我们让CPU计算一个加法3+14,这个加法运算用机器语言来描述的话就类似于下面这段二进制:

看着挺乱是吧,没关系,后面我们会讲解。大家只要清楚这段二进制的程序需要保存在内存里,这样CPU才能读取并执行。

不仅是程序,3和14作为运算的数据也是保存在内存里的,CPU要做的动作是从内存中某个地址读取数据3和14,然后根据程序要求(加、减……)对二个数字进行运算,运算的结果保存在内存中某个地址处。

要实现这样的功能,必须有一个约定,要让CPU能够识别相应的动作,即读取、运算、保存,所以我们建立如下约定:(称为指令表)。

指令表可以理解为是程序的解释器,当CPU拿到一段程序,例如00101110时,它必须知道这意味着什么,而指令表就能起到答疑解惑的作用。

具体来说每段程序(指令)的前四位对应着指令表中的操作码,后四位对应着指令表中的地址。比如00101110,前四位0010是操作码,它的含义是LOAD_A(见指令表),代表读取数据放入寄存器A。再看后四位1110,指令表中描述的内容是“4位内存地址”,这其实就是要读取的数据的内存地址。连在一起的含义就是“在1110这个地址处读取数据保存到寄存器A”。

接下来上电路!首先我们需要一块内存,可以直接使用上节提到的256B内存,但为了方便起见,我们假设它只有16个地址,可以保存16个8位二进制数。另外需要六个寄存器,每个可以保存一个8位二进制数,其中寄存器A-D用于临时存储和操作数据,指令地址寄存器用于记录程序运行到哪里了(程序指令的地址),指令寄存器用于存放指令内容。

接下来分析工作过程,当计算机启动时,所有寄存器初始值都是00000000,CPU开始进入第一个阶段:取指令,也就是从内存中获取指令。指令地址寄存器会连接到内存,读取地址为00000000的数据,即00101110,这个数据会保存在指令寄存器,第一个阶段结束。

第二个阶段:解码,即弄清楚指令要做什么。其实前面我们已经做了铺垫,指令内容是00101110,其中前四位0010是操作码,在指令表中对应的就是LOAD_A,指令后四位是1110,对应的是4位内存地址,整体意思就是从1110的位置读取数据保存在寄存器A。但现在还没有现成的电路能够做解码工作,所以我们需要添加一部分电路,如图。

这部分新添加的电路我们暂且称为解码电路,指令寄存器的前四位数据0010作为解码电路的输入,经过这些门电路的处理,最终会输出1。换句话说,解码电路的作用就是识别指令是否是0010(LOAD_A),只有指令是0010时,输出才是1,否则就是0。

第三个阶段:执行。解码电路的输出会连接到内存的允许读取端口,而指令寄存器的后四位1110会连接到内存的地址端口,这样就相当于允许读取内存地址为1110的数据,这个数据是00000011(十进制3)。

接下来这个数据要如何保存到寄存器A呢?我们需要让解码电路的输出同时连接到寄存器A的允许写入端口,而四个寄存器的数据输入端口要连接到内存的数据端口。当数据00000011被读取出来时,会同时发给四个寄存器,但只有寄存器A是允许写入的,所以数据就被保存在寄存器A中了。

接下来将指令地址寄存器+1,变成00000001,以便取下一条指令,后面的步骤与前面类似。需要注意的是,前面的解码电路只能识别第一条指令LOAD_A,后面每一条指令都需要单独的解码电路支持。我们把所有指令对应的解码电路和指令寄存器、指令地址寄存器等部分统一叫做控制单元

接下来我们快速分析剩余的指令,现在指令地址是00000001,所以从内存中取出00011111存入指令寄存器。前四位0001对应的指令就是LOAD_B,后四位1111是要读取的内存地址,对应的数据是00001110(十进制14),这个数据会被存放到寄存器B中。然后指令地址寄存器+1(00000010),继续取下一条指令。指令内容是10000100,前四位1000在指令表中对应的是ADD,后四位0100分别是二个寄存器的地址01和00(因为寄存器A-D只有四个,用二位二进制数就可以描述),其中00是寄存器A,01是寄存器B,所以这个指令的作用就是将寄存器A、寄存器B的值相加。涉及到加法,就要用到之前讲过的算术逻辑单元,也称运算单元(ALU)了,我们简化一下电路如下:

寄存器A、B会通过控制单元连接到运算单元的二个输入端,同时控制单元会将操作符也传递给运算单元,这样就可以计算了。计算的结果必须保存起来才行,但指令本身并未明确保存在哪里,所以这里面有个约定,运算结果会保存在指令地址中最后一个寄存器里,二个寄存器地址分别是01和00,后面就是00,也就是寄存器A,所以最终结果会通过控制单元保存到寄存器A中,这个结果是00010001(十进制17)。

指令地址寄存器+1(00000011),继续取下一条指令。指令内容是01000111,前四位0100表明指令是STORE_A,即将寄存器A的数据写入内存,内存地址就是指令的后四位0111,控制单元会发送给寄存器A允许读取信号,发送给内存允许写入信号,将寄存器A的值保存在内存中对应的位置。

终于我们完成了一句简单的程序任务,两数相加,并成功保存结果。我们会发现每个指令的读取、解码、执行,相当于一个周期,CPU就是不断的重复这个周期,进而完成各类任务。在每个周期中算术逻辑单元、控制单元、存储单元(内存)都需要密切配合,节奏不能乱,才能保证最后的结果正确。但如何确保这个节奏是恰当的呢?既不能太快,因为即使是电信号的处理也需要时间,也不能太慢,造成计算效率太低。所以有一个单独的电路在控制这个节奏,就好像一台时钟,精确的指挥各个部分有条不紊的运行。CPU都有一个重要的指标:主频,例如2.6GHz,相当于26亿次周期/秒,意味着一秒钟内CPU会执行26亿次周期,主频越高的CPU代表速度越快。我们把带有时钟电路的算术逻辑单元、控制单元、6个寄存器封装成一个相对独立的部分,这就是CPU!

至此,我们从一个简单的晶体管开关开始,一路添砖加瓦,终于打造了一个完整的CPU,当然也是最基础的CPU。相信这个系列文章能让我们对硬件与软件的结合点有了清晰的认知,我们日常所使用的各类软件都是程序指令编写出来的,每个程序的每条指令在CPU内部都会经历众多晶体管开关的处理,最终完成我们希望的任务。

展开阅读全文

页面更新:2024-02-09

标签:晶体管   寄存器   减法   加法   指令   电路   单元   内存   地址   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top