写了这么久的代码,第一次思考计算机是怎么认识自己写的代码并执行的
一个代码到底是怎么执行起来的?CPU内部到底是怎么工作的?
大家都知道计算机是二进制,即 0 和 1,但计算机中的 0 和 1 到底是什么?
就是低电平和高电平的意思,0 代表低电平,1 代表高电平。比如 0.2V 是低电平的话,那么 5V 可能就是高电平了。一般两者都有一个阈值,当电压大于某个阈值时,即是高电平;当电平小于某个阈值时,即是低电平。计算机中的 0 和 1 是为了理解方便,给低/高电平取的别名。
同时两种称呼分别代表了数字电路和模拟电路。
数字电路是电路是以“0”和“1”及相应的逻辑符号来表示的,如下图:
模拟电路是电路中以电压高低和电流等参数来表示的,如下图所示:
可以看做建筑施工图和建筑实物图的关系:数字电路主要是表现其逻辑和功能,模拟电路是搞定采用什么材料什么方式来实现数字电路想要达到的结果!
高低电平如何实现的?
二极管是用半导体材料(硅、硒、锗等)制成的一种电子器件,具有单向导电性。
一个二极管的实物图:
逻辑电路图(即抽象的)
电流可以从正(+)极流向负(-)极,此时处于导通状态;但反过来却不行,此时处于截止状态。这就是单向导电性!
由于单向导电性,二极管就像是一个开关:
当处于导通状态时,开关闭合,两边电压大小一致,如正极 (+) 电压为 5.2V,那么负极 (-) 也为 5.2V。
当处于截止状态时,开关断开,两边电压大小不一致,如负极(-)为 5.2V,正极 (+) 电压为 0V。
与门
通过二极管可以获得“0”和“1”,利用这个特性,我们可以制作一些有趣的电路,比如【与门】
通过小学 1 年级的知识,我们可以知道,此时 uA、uB 只要有一个是 0V,那 uY 就会和 0V 直接导通,导致 uY 也变成 0V。只有 uA、uB 都是 10V,uY 也才是10V。
并且可以把电路进行封装,不关心具体的二极管、电阻这些元器件,统一用 & 符号表示,就是上图右侧的描述。
这个装置成为【与门】,把有电压的地方计为 1,0V 电压的地方计为 0。至于具体几 V 电压,那不重要。
或门
再来分析一个或门:当输入中至少有一个“1”时,输出为“1”,若全为“0”,则输出“0”。
刚刚的与门展示的是两个输入,现在来看看四个输入!
当 A、B、C、D 四个输入都是输入低电平 0 时,四个二极管都处于截止状态,此时输出即为低电平 0。
当其中任意一个不为低电平时,若A为高电平 1,此时第一个二极管导通,输出即为 A 的电平,即高电平 1。
或门在数字电路中还可以表示为:
其他还有【非门】和【异或门】,跟这个都差不多。都可以用二极管或者三极管做出来,实际并不是用二极管三极管做的,因为它们太费电了。实际是用场效应管(也叫MOS管)做的。
运算离不开逻辑运算,也就是门电路,常见的逻辑运算有与、或、非、异或、同或。它们的真值表与逻辑符号如下。
然后我们就可以用门电路来做 CPU了。当然做 CPU 还是挺难的,我们先从简单的开始:加法器。
对于一个简单的加法器而言有两个输入(A/B)和一个输出(Sum)和一个进位(C)。
输入 A | 输入 B | 输出 Sum | 进位 C |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
只看输入 A、B 和 输出 Sum,发现是异或门!
只看输入 A、B 和 进位 C,发现是与门!
也就是说加法器可以由异或门和与门构成!如下图所示:
到这里,我们已经做出了一个最简单的 CPU,但是现在这个只能称为“半加器”,最多只能计算 1+1,如何更进一步,比如 1+2, 2+2?
这就需要在原本的加法器增加一个进位接口,如下就是“全加器”:
两个半加器电路可以实现一个全加器。
前一个半加器将用于将 A、B 相加以产生部分和。
后一个半加器用于将 CIN 与前一个半加器产生的和相加,以获得最终的 S 输出。
任何半加器逻辑产生进位,就会有一个输出进位。因此,COUT 将是半加器进位输出。
每次都这么画实在太麻烦了,我们简化一下
将两个加法器串联在一起,就得到一个可以进行 2 位数(例如 3 + 2)的加法器。
进行 A + B 计算,例如 3 + 2 的和,3 的二进制是 11 (A1A0),2 的二进制是 10 (B1B0),
先算 A0 + B0,输出 C1 = 0,S0 = 1,再计算 A1 + B1 + C1,输出 S1 = 0,C2 = 1,最终的结果由C2、S1、S0 拼成 1012 = 5。
要几位加法就用几个加法器串联,如四位(15+15):
现在我们就有了一个 4 位加法器,已经达到了小学 2 年级的水平,是不是特别给力?
减法器其实是本质还是通过加法器实现的,比如 5 - 3,其实就是 5 + (-3)。
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
5 | 0000 0101 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 | 1111 1101 |
先将十进制转为补码,再将补码输入全加器,这样就做到了减法计算。
做完加法器我们再做个乘法器吧,当然乘任意10进制数是有点麻烦的,我们先做个乘2的吧。
乘2就很简单了,对于一个2进制数数我们在后面加个 0 就算是乘 2 了,比如
5 = 101(2)
10 = 1010(2)
所以我们只要把输入都往前移动一位,再在最低位上补个零就算是乘2了。
那乘 3 呢?简单,先位移一次(乘 2)再加一次。乘 5 呢?先位移两次(乘 4)再加一次。
所以一般简单的 CPU 是没有乘法的,而乘法则是通过位移和加算的组合来通过软件来实现的。
现在假设我们有加法器了,也有一个位移1位的模块了。把这两个模块串联起来你就能算 (A + B ) * 2
了!激动人心,已经差不多到了小学 3 年级水平。
那我要是想算 A * 2 + B
呢?
简单,把加法器模块和位移模块的接线改一下就行了,改成 A 先过位移模块,再进加法器就可以了。
改个程序还得重新接线?
所以你以为呢?编程就是把线来回插啊。
早期的计算机就是这样编程的,几分钟就算完了但插线好几天。而且插线是个细致且需要耐心的工作,所以那个时候的程序员都是清一色的漂亮女孩子,穿制服的那种。
虽然和美女作伴是个快乐的事,但插线也是个累死人的工作。所以我们需要改进一下,让 CPU 可以根据指令来相加或者乘2。
这个就简单了,sel 输入 0 则输出 i0 的数据,i0 是什么就输出什么。同理 sel 如果输入 1 则输出 i1 的数据。
有这个东西我们就可以给加法器和乘2模块(位移)设计一个激活针脚。
这个激活针脚输入 1 则激活这个模块,输入 0 则不激活。这样我们就可以控制数据是流入加法器还是位移模块了。
现在我们终于可以做 A × B + C
了,这就需要先保存 A×B 的结果,在与 C 相加,等等...保存?话说在计算机内部是用什么方式保存数据的呢?
由于保存数据的重要性,电路中使用何种方式可以保存数据。比如使某个器件一直输出高电平,那不就是 1 了吗?一直输入低电平,那不就是 0 了吗?这里再引入一个触发器。
这个模块的作用是存储 1bit 数据。比如上面这个,R 是 Reset,输入 1 则清零。S 是 Set,输入 1 则保存 1。RS 都输入 0 的时候,会一直输出刚才保存的内容。
注意到这个电路跟之前我们看到的都不一样了,其门电路的输出会作为自身的输入,这种结构被称为反馈电路。
我们用触发器来保存计算的中间数据(也可以是中间状态或者别的什么),1bit 肯定是不够的,不过我们可以并联,用 4 个或者 8 个来保存 4 位或者 8 位数据。这种我们称之为寄存器。
现在我们把上述用到的元器件组合起来,共有 8 个引脚,其中 4 个数据引脚,4 个指令引脚。数据引脚是可以输入数据,指令引脚是用来选择执行的操作。
我们定义,当指令引脚输入:
0001 读取数据,将数据引脚的数据读入寄存器;
0010 选择加法器,将数据引脚的数据与寄存器数据相加,结果在寄存器;
0100 选择乘法器,将寄存器的数据乘以数据引脚的数据,结果在寄存器;
1000 清空寄存器。
我们以一个计算题来举例:3 × 5 + 6,输入依次为:
0001 0011 # 读取数字 3
0100 0101 # 选择乘法器,乘以 5
0010 0110 # 选择加法器,加 6
不错,现在我们计算出 3 × 5 + 6,可以去小学 3 年级踢馆了,呃,不过是不是有点麻烦,这还是我们只定义了 4 种指令,要是成千上万种这谁顶得住?
我们不妨对指令稍微包装一下,规定:
0001 用 MOV 表示
0010 用 SHL 表示
0100 用 ADD 表示
并且假设现在又多了一个元件可以实现十进制到二进制的转换,那么命令应该为:
MOV 3
SHL 5
ADD 6
稍微好受一点了,这就是我们每个人都精通的汇编语言,之前仅有 0 和 1 的语言称为机器语言。
太棒了,靠这台我们设计的 CPU 可以打败所有的小学生,称霸小学校园了。而且现在我们用的是 4 位 CPU,如果换成 8 位的 CPU 完全可以吊打初中生了!
实际上用程序控制 CPU 是个挺高级的想法,再此之前计算机的 CPU 都是单独设计的。
1969 年一家日本公司 BUSICOM 想搞程序控制的计算器,而负责设计 CPU 的美国公司也觉得每次都重新设计 CPU 是个挺傻X的事,于是双方一拍即合,于1970年推出一种划时代的产品,世界上第一款微处理器4004。那家负责设计 CPU 的美国公司也一步一步成为了业界巨头。它就是 Intel。