编码 隐匿在计算机软硬件背后的语言
《编码 隐匿在计算机软硬件背后的语言》笔记
2. 编码与组合
通过一张.
与-
的树形图,译码过程更直观方便:
5. 双向莫斯电码系统
下图,包含的元素包括:电压、开关、灯泡、接地。开关合上后,电压与接地形成电子通路,电子流经灯泡,使灯泡发光。
6. 电报机与继电器
1844 年,第一条电报线路在华盛顿假设完成,传输了第一条消息What hath God wrought
。下图是电报机的原理,一端电报按键控制另一端的电磁铁。
为了解决长距离电线,电阻增大,信号衰退的问题,设计出了继电器,原理如下。
7. 十进制计数法
阿拉伯数字系统,符号(0123456789ABCDEF 等) + 位置(个位、十位、百位)组合表示一个数字。
8.十进制的替代品 其他进制数
八进制
八进制乘法表:
四进制
二进制
二进制加法:
二进制乘法:
9.二进制数
商品的条形码:
扫码时,要么正向扫,依据编码表得到
0 51000 01251 7
这串数字逆向扫,依据逆向的编码表得到:
7 15210 00015 0
这串数字
10. 逻辑与开关
11. 门电路
与门:
或门:
非门:
或非门:
与非门:
异或门:
12. 二进制加法器
加法计算是计算机需要实现的唯一工作。如果可以实现加法器,那么就可以利用加法器来实现减法、乘法和除法计算。
二进制的加法表:
用逻辑电路来实现上述加法表:
就构成了一个半加器,它只能用来算 1 位二进制数的,也就是说,它只能计算出两个任意二进制数的最低位。
对于上图二进制加法,右数第二位的相加后,还需要与低位产生的进位再次相加,才能得到结果,所以设计出了全加器:
一个 8 位的二进制加法器
操作界面图:
上面的开关是人操作的,用于输入两个二进制数,确定两个二进制数后,下面的指示灯的明灭代表输出的二进制数的0
和1
。
实现:
最低位:没有进位,所以进位输入接地,表示0
中间位数:全加器依次相联
最高位:进位接灯泡
合在一起就是:
8 位加法器简化:
再次简化:
因此我们可以很容易画出 16 位加法器的设计图:
13. 如何实现减法
理论上来说可以参考加法器的设计方法和思路,设计出一个减法器。但是这样子的话,计算机的运算器实现里面就有两种电路的存在,一种加法器电路,一个减法器电路。这样的实现必定是浪费材料和空间的。那么能不能找到一种方法,让减法也可以通过加法器完成。
在自然数学中,16 - 8 = 8
可以写成16 + (-8) = 8
,23 - 87 = -64
可以写成23 + (-87) = -64
,那么参考加法器的构造:
于是,我们有了:
0001 0000(16) - ?(-8) = 0000 1000(8)
0001 0111(23) - ?(-87) = ?(-64)
加法器的 B 输入是一个负数,那么这个负数的二进制表示是什么,才能够使上述等式成立,并且输出的数据,我们通过 8 位加法器的灯也能读懂。
我们可以先来考虑一个简单的:
0000 0001(1) + ?(-1) = 0000 0000(0)
同时我们也知道,加法器中:
0000 0001(1) + 1111 1111(255) = 1 0000 0000(256)
上面式子中的1
是进位,如果我们不看进位的值,即将进位舍去,那么我们使用1111 1111
这个二进制数作为-1
的二进制表示:
0000 0001(1) + 1111 1111(-1) = 1 0000 0000(0)
同理,我们可以:
0000 1000(8) + ?(-8) = 1 0000 0000(0)
推算可知,能够符合加法器运算的,-8
的二进制是1111 1000
。
观察下,该二进制也可以写成1111 0111 + 0000 0001
,也就是8
的二进制数的每一位取反后,再加上1
。
根据以上讨论,我们可以得出下表:
二进制数 表示的十进制数
1000 0000 -128
1000 0001 -127
1000 0010 -126
...
1111 1101 -3
1111 1110 -2
1111 1111 -1
0000 0000 0
0000 0001 1
0000 0010 2
0000 0011 3
...
0111 1110 126
0111 1111 127
可以验证下,上表的二进制数,可以通过加法器的运算正确的表示 8 进制加减法。当执行126 + 125
或者-123 - 78
这样的运算时,8 位加法器的进位的值为1
,表示这个算式产生的结果已经无法使用 8 位二进制数表示。所以对于 8 位加法器,只能够表达有限整数范围内的算数运算。这个有限范围指的是,A 输入、B 输入以及最后的计算结果都要落在-128 ~ 127
这个范围内,否则就是溢出错误。
我们将这种方式推广到 64 位加法器,这样能够表达的运算范围就非常大了。
14 反馈与触发器
蜂鸣器
开关连通后,电路闭合(图二),电磁铁把弹簧拉下从而电路断开(图三),电磁铁失去磁力弹片回到原来位置,电路又闭合(图四),周而复始。
改良后的蜂鸣器:
振荡器
左边图是取反器(非门),如果将它的输出连接到输入,就构成了一个蜂鸣器(图中),逻辑图见右上角,这段线路的输出在周期时间内一直在0
与1
之间震荡。每秒钟震荡的次数称为赫兹,是计量单位。
RS 触发器
下图是使用或非门构建的一个触发器电路,上面的开关闭合使点亮灯泡,当它断开时,灯泡保持亮状态:
下图,当下面开关闭合时,灯泡关闭,当开关断开时,灯泡仍然保持关闭状态:
这就是触发器电路,当两个开关都断开时,电路处于两个稳定状态之一,也就是说它有记忆性,可以记住最近一次时哪个开关闭合了。
上面是触发器最简单的一种,称为R-S
触发器,也叫作R-S
锁存器。
RS 触发器可以实现,在R
与S
都为0
情况下(开关都关闭):
通过
S
位设置为1
来将Q
设置为1
,即便S
位再次变为0
,Q
的输出依然保持为1
同理,通过将
R
设置为1
,来将Q
重置为0
D 位触发器
有了 RS 触发器作为基础,我们再来构思这样一种触发器,有一个数据端与一个保持位作为输入,有一个输出端:
当保持位为
1
时,输出的就是数据端的输入(0
或1
)当保持位被置为
0
时,无论数据端的值是什么,都不会再改变输出端的值
观察 RS 触发器表与 D 位触发器表:Q
的值与S
的值是一致的,当S
与R
的都为0
时,Q
的值是不变的。
利用 RS 触发器电路作为基础来实现 D 位触发器:使用与门实现保持位,当保持位置为0
时,RS 触发器的输入就都是0
,不再影响Q
的值。
上图中,保持位决定置位能不能修改Q
的值,当保持位为1
时,置位的值决定了Q
的值,现在的问题是,如何去掉无用的R
位。观察下 RS 触发器表,可知使用一个非门,就可以实现:
当保持位接入到一个时钟(振荡器)上,简写为Clk
,数据端简写为D
,下图即为 D 型触发器表:
8 位锁存器
如果将 8 个 D 位触发器连接起来,就构成了一个 8 位锁存器,它可以用来存储一个 8 位二进制数据了:
改进加法器
参考之前章节的加法器,有 A,B 两排输入,有灯泡作为输出,现在想让 8 位存储器存下加法器的输出,并且可以选择作为下一次加法的输入:
上图中2-1
的选择器实现如下:
有了 8 位锁存器后,我们可以考虑将加法机器的第二排开关去掉,理由是:
第一次使用第一排开关输入的值可以存储到锁存器中,然后再次使用第一排开关输入一个数,该数与存储器中的数相加后,再次存入 8 位锁存器。
如果使用递推逻辑的话,第一次输入时,应该是该数与锁存器中的
0
相加后再次存入锁存器。
所以我们对 D 位触发器做一点小修改,使得无论输入端是什么,Q
强制输出0
:
优化后的加法机器如下:
你可能会发现这个加法器比前面那个好用,尤其是当你需要加上一长串数字时。
刚开始时,按下清零开关,这个操作使锁存器输出为
0
,并熄灭了所有的灯泡,同时使加法器的B
端输入全为0
接着,通过开关输入第一个加数,闭合“相加”开关,则此加数反映在灯泡上
再输入第二个数并再次闭合“相加”开关,由开关输入的 8 位操作数加到前面的结果上,其和输出到灯泡。
如此反复,可以连加很多数。
电平触发 与 边沿触发
上述的触发器都是电平触发式的,意思是只有在时钟端输入从0
变到1
后(即高电平时),数据端输入的值才能保存在锁存器中,并且在时钟端输入为 1 期间,数据端输入的任何改变都将反应在 Q 上。对于一般的应用,电平触发已经够用了。
边沿触发对输出的改变条件要求更加严格,对于边沿触发器而言,只有当时钟从0
变到1
的瞬间,输出才会改变。而当时钟输入维持为1
时,数据端输入的改变也不会影响输出。
边沿触发的 D 型触发器是由两级 R - S 触发器按如下方式连接而成的
非工作状态:
现在使数据端输入为1
,这改变了第一级触发器状态,因为时钟信号取反后为1
。但第二级仍保持不变,因为时钟
端输入仍为0
。
现在把时钟输入变为1
,这就引起第二级触发器改变,使 Q 端输出变为 1。
与前面不同的是现在无论数据端输入如何变化(如变为0
),它也不会影响Q
端的输出值。
边沿触发的 D 型触发器的功能表需要一个新符号来表示这种从 0 到 1 的瞬时变化,即用一个向上指的箭头:
计数器的构造
之前提到的振荡器中,它的频率为 1 秒震荡的次数,那么这个次数如何来统计呢?思路是电路实现一个计数器,连接到振荡器上。那么这个计数器如何构建呢?
我们使用上述的边沿触发器,它与振荡器的连接方式如下:
刚开始时,CLK
与D
的输入都是0
,然后Q
的输出就是0
,Q扛
就是1
,带动D
变成1
.所以最初始的状态就是右表格中的第一行,之后时钟开始震荡,各个输入输出端的值变化如右表。
画成更直观的时间折线图如下:
可以很直观的看到,Q
的震荡频率是CLK
的一半。这种电路称为分频器。
如果将多个分频器串联呢:
如果给高低电平标上0
和1
,并且竖着从下往上读的话,明显就是一个计数器了:
如果使用更多的分频器,就可以实现更大位数的计数,下面是一个 8 位计数器:
将振荡器连到 8 位计数器上,当计数器总和达到11111111
时,它又会返回到00000000
。计算振荡器频率的方法就是:
把计数器的输出连到 8 个灯泡上。当所有输出为 0 时(即没有一个灯泡点亮),启动一个秒表;当所有灯泡都点亮时,停住秒表。这就是振荡器循环 256 次所需要的时间。
所以边沿 D 位触发器还需要一个清零开关,同时再加一个预置开关(当开启时,Q 变为 1,Q 杠变为 0),电路设计如下:
15. 字节与十六进制
略,这部分很简单。
16. 存储器组织
D 位触发器就可以作为保存一位信息的存储器,它的输入时钟输入变成了写入信号、数据端变为了数据输入端,而Q
则是保存的信息位。想要在保存一位数据,写入信号应该先置为 1 后为 0。
8 位锁存器
左边为上述触发器的简化图,通过并联在一起就组成了一个可以保存 8 个位的存储器。
再简化:
8-1 选择器
如果上述 8 位锁存器的输出接入 8 个灯泡,那么很容易知道每个位存入的值,但如果灯泡只有一个呢?
可以通过 3 个开关来控制显示哪一位的值。
图中的这部分电路就是8-1
选择器,可以用它来控制输出哪一位的值。
电路实现如下:
3-8 译码器
同样的道理,我们想实现通过 3 个开关来控制写入 8 位中指定的位置。称为3-8
译码器的电路。
下面就是 8 位锁存器的完整电路:
s0 s1 s2
通常称为地址,电路简化为:
这种电路成为读/写存储器
,通常也叫做随机访问存储器RAM
。称它为随即是因为可以通过改变地址,就可以从 8 个锁存器中的任意一个读出或者写入数据。
锁存器阵列
可以通过将两个8-1 RAM
连接起来,使它们按照相同的方法来寻址:
这样的话,一个地址可以有两个位置来存储数据,即可以存储和读取 4 种状态00
01
10
11
。这种电路称为8x2 RAM
阵列。
如果想用8-1 RAM
来构造一个16-1 RAM
呢?即有 16 个地址,可以随机存储16
位数据。电路如下:
上图中增加的选择
输入,其实就是地址线路。
通过第一个阵列,我们知道了如何将让一个地址存储多位数据,所以可以扩建出8x8 RAM
,即一个地址可以存储和读取8 x 8 = 256
种状态。
通过第二个阵列,我们知道如何拓展地址数目,如果我们将地址线加到 10 条呢,就可以有1024
个地址可以用。
所以我们很容易就可以构建出一个1024x8 RAM
存储器。如下图:
那么上面这个存储器能够存多大容量呢?一位数据就是1 bit
,八位数据就是1 B
。1B = 8bit
。所以1024x8 RAM
能存储的数据量为: 1024 B
,也就是1kB
。
1980 年代的 PC 机的存储器的配置都是64KB
的,地址范围是0x0000 ~ 0xFFFF
,电路图如下:
16 位存储器机器
A0 - A15
是地址输入开关,操作开关表示好一个数据后,下面的指示灯会立即显示出当前地址所存储的8
位数据值。
D0 - D7
是数据输入开关,操作开关表示好一个数据后,拨动一次写入开关( 0 -> 1 -> 0
),就可以将该数据写入上诉地址。
接管位
设置为 0,就表示从外部接受输入,所有A
和D
开关不再起作用,设置为1
即表示使用A
和D
开关输入的数据。
电路图如下:
17. 自动操作
这章是最难理解的,有了开关作为输入,灯泡作为输出,加法器作为运算工具,存储器作为存储器。如何将它们结合在一起实现:在存储器的 100 个地址里存入 100 个数字,然后通过加法器将它们依次累加,计算一个和,然后再次存入某个存储器位值?
基本的电路如何实现?
加载一个数到加法器
将一个数存入到存储器
基本电路实现后,我们怎么使用这些基本电路?人们创造出了指令,一条指令对应基本电路的某些操作。
指令有二进制表示,也有人可读的汇编代码表示。
下图为第 14 章实现的加法机器,它的使用参考 14 章。
这个机器最大的问题是: 如果想把 100 个二进制数加起来,你就得坐在加法机前耐着性子输入每一个数字并累加起来。当你完成时,却发现有两个数字是错误的,你只好又重复全部的工作。
现在如果使用上一章的存储器,代替加法器中的8
位开关输入,并且使用计数器代替代替存储器中的地址输入,计数器自动将累加后的值作为 8 位数据的存储地址。
上图中电路的用法如下:
先通过"清零"开关,将 8 位锁存器和 16 位计数器都置为 0
通过存储器的控制面板,将 100 个整数的值,写入到存储器的
0 - 99
这 100 个地址处打开振荡器开关,它振荡时
0->1
,计数器就加 1 作为地址,然后存储器输出该地址的值给 8 位加法器加法器将该地址的值(A 输入)与目前锁存器中的值(B 输入)相加,输出给 8 位锁存器
锁存器也接收到振荡器
0->1
的信号时,它将来自加法器的输出存入锁存器(这是边沿触发)振荡器继续振荡,依次将存储器中的
0 - 99
地址处的值,累加到了 8 位锁存器中,用户可以从灯泡上读取最后的结果
将 100 个数加成 50 个数
如果想将存储器中的 100 个数,每个相加,然后存回存储器中,最终得到 50 个数呢?
首先,8 位锁存器的灯泡输出可以去掉,直接连回存储器。通过存储器指定地址来查看计算后的结果。
现在我们想要做到的是: 我们希望能随心所欲地控制累加数字的个数,以及加完后的和存储在 RAM 的地址。
比如下图: 先加 3 个数,再加 2 个数,再加 3 个数。
我们怎样来设计一个自动加法器,能完成下面的工作?
从存储器中传送一个字节到累加器中,这时该数字存储在 8 位锁存器中,这个操作叫装载(Load)
从存储器中传送一个字节加到累加器中,这个操作叫 Add
从累加器中取出结果,保存(Store)到存储器中
让自动加法器停止工作(Halt)
对于上述案例,自动加法器所做的工作应该如下:
把地址
0000h
中的数装载到累加器中把地址
0001h
中的数加到累加器中把地址
0002h
中的数加到累加器中把累加器中的数保存到地址
0003h
中把地址
0004h
中的数装载到累加器中把地址
0005h
中的数加到累加器中把累加器中的数保存到地址
0006h
中把地址
0007h
中的数装载到累加器中把地址
0008h
中的数加到累加器中把地址
0009h
中的数加到累加器中把累加器中的数保存到地址
000Ah
中停止自动加法器的工作
怎样完成这些工作呢?只是简单地键入一组数到RAM
中并期望自动加法器来正确操作是不可能的。对于RAM
中的每个数字,我们还需要一个数字代码来表示自动加法器所要做的工作:装载,加,保存或停止。
代码与数据分别存储在不同的 RAM 中的自动加法器
最容易的方法是将这些代码存储在一个完全独立的RAM
阵列(称为代码阵列)。由这个阵列来控制自动加法器对数据所在RAM
(称为数据阵列)所要进行的操作。
那么这个代码RAM
里存的是什么呢?如下图:
对比下就知道,代码RAM
中的每个地址的代码对应数据RAM
中一个数据,或者表示要存入到该地址的数据。左图中这样的数字代码通常称为指令码或者操作码,它们指示电路执行某种操作。
另外,为了实现装载指令(Store
),数据RAM
的输出有时候也要作为 8 位锁存器的输入,所以还需要加一个2-1
数据选择器,综上所述,电路设计如下:
上面电路中,代码RAM
输出到数据RAM
的部分暂时未讨论,我们先来考虑这么一个问题:代码RAM
和数据RA M
阵列同时、顺序地从0000h
开始寻址。代码RAM
中的每条指令对应于数据RAM
中相同地址的存储单元。一
旦Store
指令使某个数据保存在数据RAM
中,这个数就不能再被装载到累加器中。
那么我们能不能做到,由代码RAM
自己控制要操作的数据RAM
的地址,而不是一一对应?
答案就是,在每条代码指令后面跟一个地址,假设地址为 16 位的,图示如下:
实现上述功能的关键设计就是,将代码RAM
中的数据输出到 3 个 8 位锁存器中,参考如下:
现在我们再来思考一个问题,两个RAM阵列
使得自动加法器的体系结构变得尽可能清晰、简单。但既然已经决定每条指令占 3 个字节,我们是否可以将代码阵列省去,直接在数据阵列里面存下操作指令?答案是可以的。
电路参考如下:
下图显示指令和数据输入在一个RAM
阵列中,怎样把两个 8 位数相加再减去第三个数:
现在有了一个新问题,指令与数据都存储在一个RAM
中的,本来指令的地址是连续的,但是可能出现这种情况,数据将连续的指令隔开了,这如何处理?
答案是增加一个jump
指令,执行该指令时,直接跳到下一条指令的地址处继续执行。为了支持这个指令,电路改动如下:当执行jump
指令时,高低位地址输出到16位计数器
,从而改变下一条指令执行的地址。
还有一条比较重要的指令叫条件转移指令,它用来实现乘法。具体不细讲了。
现在我们的计算机到这里就构建的差不多了,它有一个64kB
的阵列作为存储器,控制面板上的开关和灯泡是输入输出设备,而自动加法器就是 CPU,它的累加器宽度为 8 位,它是一个简单的锁存器,用来在处理机内部保存数据,8 位加法器和 8 位反向器一起被称为算术逻辑单元ALU
,16 位计数器叫做程序计数器PC
,它的寻址范围是 16 位。
处理器可以响应的操作码(如指装载和存储的10h
和11h
)叫作机器码,或机器语言。我们要用很长的短语表示机器所执行的指令,通常,机器码都分配指定了用大写字母表示的短的助记符,这些助记符有 2 或 3 个字符。下面是一系列可能的上述计算机所能识别的机器码的助记符:
人们使用这样助记符编程:
LOD A,[1003h] # 把1003h处的值装载到累加器中
ADD A,[001Eh] # 把累加器中的内容保存到地址1003h处
0000h:LOD A,[1005h] # 表示某一指令存储在某一地址
1002h: 00h,1Ch # 表示了一些存储在某一地址的数据
下面是一个乘法程序的例子:
写代码时最好不要用真实的数字地址,因为它们是会变的。例如,如果要把数字存储到地址2000h~2005h
处,需要重写许多语句。较好的方法是使用标号来指定存储单元,这些标号是简单的单词,或类似于单词的东西。
18. 从算盘到芯片
1947 年 12 月 16 日,当贝尔实验室的两个物理学家John Bardeen
(1908—1991)和Walter Brattain
在装配一个不同类型的放大器时,所有的一切都改变了。这种新型放大器由锗片 — 一种称作半导体的元素 — 和一条金箔构成。一个星期后,他们给他们的上司William Shockley
(1910—1989)进行了演示。这就是第一个晶体管,一种被人们称为 20 世纪最伟大的发明的器件。
锗和硅元素称为半导体,并不是因为它们的导电性是导体的一半,而是因为它们的导电性可以用多种方法来控制。半导体最外层有 4 个电子,是最外层所能容纳电子最大数目的一半。在纯半导体中,原子彼此非常稳固地结合在一起,具有与金刚石相似的晶状结构。这种半导体不是好的导体。
但是半导体可以掺杂,意思是与某种杂质相混合。半导体很容易与其他杂质结合而变得不纯。有一类杂质为原子的结合提供额外的电子,这种半导体叫 N 型半导体(Negative
); 另一种类型的杂质掺杂生成 P 型半导体。
两个 N 型半导体中夹一个 P 型半导体可制成放大器,称作 NPN 晶体管,对应的三部分分别是集电极(Collector)、基极(Base)和发射极( Emitter )。下面是一个NPN
晶体管的示意图:
使用晶体管构建 与门
和 或门
19. 两种典型的微处理器
1971 年,第一个微处理器是Intel4004
,有 2300 个晶体管。
Intel8080
是具有重大历史意义的微处理器,它是8
位处理器,可寻址64KB
存储空间。
了解微处理器最好方法可能是在系统方式下测试其完整的指令集。
一个 8 位处理器最多有 256 条指令,每个指令对应某个 8 位值。Intel8080
有 244 条指令,但如果想使用它来做乘法和除法,仍然需要写一小段程序来实现。
21. 总线
总线是提供给计算机中每块电路板的数字信号的集合,这些信号分为 4 类:
地址信号:这些信号由微处理器提供,常用来寻址
RAM
单元,也可用来寻址连接到计算机上的其他部件数据输出信号:也由微处理器提供,用来写入数据到
RAM
或其他设备。要仔细推敲输入input
和输出output
的含义。数据输出信号是从微处理器输出,变成RAM
和其他设备的数据输入信号数据输入信号:是由计算机的其余部分提供,由微处理器读入的信号。数据输入信号通常来自于
RAM
的输出,也即表示微处理器读入存储器内容。但是其他部件也提供数据输入信号给处理器。控制信号。由各种各样的信号组成,通常与计算机的特定处理器的控制信号一致。控制信号可来自于微处理器或从其他部件传送到微处理器。例如,微处理器可用一个控制信号来指示它要写一些数据到某一存储器地址
23. 定点数与浮点数
浮点加法的关键是有效数相加,因而用的技巧是用两个数的指数部分确定有效数如何移位。假设要做以下加法:
(1.1101 × 2^5 ) + ( 1.0010 × 2^2 ) = 1.1111010 x 2^5
指数部分的不同表明第二个数必须进行移位。实际上,需要进行11101000
和10010
的整数加法。
两个浮点数的相乘是把有效数部分像整型数那样相乘并把两个整型指数相加。