怎么从软件到硬件
这个问题对于我来说很难理解。即使我本身是程序员,即使看了很多科普视频,但仍然不知道。硬件是不可变的,软件是多变的,怎么用不变实现变化?
我偶然间看了系统推荐的游戏视频,发现了这个游戏。于是就趁着无聊玩了下。这个游戏展现了怎么从硬件到软件。
逻辑与哲学
在古希腊时,逻辑被注意到。当时被称为逻各斯,古希腊哲学集大成者亚里士多德提出了三段论,苏格拉底是人,人会死,苏格拉底会死。在那时,逻辑往往和宇宙本身的存在是分不清的,因而逻辑就被看作是通过内心真理,认识真理的途径。
而中世纪基督教的崛起,对上帝存在的证明也离不开对逻辑的重视。这时候有一种论点,逻辑就是存在。比如我说"金山不存在",既然我能说出金山这个符号,那就一定存在金山。对此有全能的上帝创造了自己不能举起来的石头的悖论。既然上帝是全能的,那他就能创造这块石头,然而他不能举起了,上帝就不是全能的。
从近代开始,哲学转向认识论。开始注意主体和语言的问题。康德在以前逻辑的基础上创造了先验逻辑。逻辑的地位降低了,逻辑不是宇宙的真理,而是我们自身的真理。宇宙可以违反逻辑,但我们只能认识符合自己逻辑的东西。
到了费希特和黑格尔那里,又创造了辩证逻辑。辩证逻辑相对于形式逻辑来说,更难理解。
这涉及逻辑最重要的一个定律,同一律
\[A=A \]
比如常见的关于苹果和手机的违反同一律的笑话,"我买了一个苹果,它不小心掉进了果汁里。现在它可以称作'果汁iPhone'了。"在不同的命题里面,"苹果",这个同一个符号指代了不同的对象。辩论中也也常常使用违反同一律的偷换概念来赢得辩论。古希腊常常用同一律来证明种子,嫩芽,花,果实都是同一个东西。今天的我和昨天的是我是同一个我。黑格尔用同一律来证明不同的历史阶段是同一个历史,而不是一个个偶然的事件。然而这个定律并没有这么简单。
现代哲学家弗雷格创造发展了现代逻辑,提出了命题逻辑,谓词逻辑(一阶逻辑),以至于n阶逻辑。维特根斯坦说凡是能够说的事情都能够说清楚凡是不可说的东西都应该保持沉默。罗素通过摹状词理论改造金山不存在这句话,变成存在一个x,x是金山,x不存在来消除能说出来金山,就一定存在金山的语言幻象。乔姆斯基提出的语言构造理论是高级编程语言的基石。
逻辑相对于宇宙和人本身的地位从古至今变换不定,但古希腊神庙上的那句话,**"认识你自己"**永不过时。
命题逻辑
一般来说,最基础的电路元件是与或非门电路。但在游戏中使用一个更加基础的门电路,与非门。这个门电路何以组合出前面那3中基础逻辑门。
接下来就从逻辑开始吧。命题逻辑属于形式逻辑的一种,也叫零阶逻辑,还有一阶逻辑......n阶逻辑。这种逻辑系统是从自然语言中提取出来的子系统。而电路就是由命题逻辑所描述。
命题逻辑包含
- 命题
比如花是红色的。输入1是真。输入2是假。 - 联结词
联结词用来连接两个命题,然后这个基于联结词命题的真假。联结词由合取、析取、否定等。比如苏格拉底是人,人会死,合取结果是苏格拉底会死。比如输入1是真,输入2是真,定义合取输出是真。
从联结词这里已经能看到一点电路的雏形了,如果我们吧他整理的更加好看的话.我们先把1定义为真,0定义为假。
输入1 | 输入2 | 输出(合取) |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 0 | 0 |
0 | 1 | 0 |
这张表叫真值表。真值表的每一格就是一个命题。而输出的每一格就是一个复合命题。这个真值表不就描述的是一个与逻辑门
吗?
谓词逻辑(一阶逻辑)
真值表不是一个表达式,要用语言说出这个真值表的逐一说出每一个单元格的每一格命题。使用一阶逻辑可以更加简洁的描述这个真值表。
谓词逻辑在命题逻辑的基础上给逻辑系统添加了量词和谓词。使得描述变得简洁。谓词逻辑看起来像是函数,只不过是特殊的逻辑函数
- 个体词
个体词就是命题中的具体事物,类似于函数中的变量x。一系列个体词的集合就构成了论域,相当于函数的定义域 - 量词
有两个量词,存在量词和全称量词。比如只要有一个输入为真,输出为真。 - 谓词
谓词就相当于函数f(x)中的f,比如苏格拉底会死中的......会死。
现在简化一些上面的真值表,使用谓词逻辑来描述下吧。
- 对于所有的输入1和输入2,输出等价于输入1合取输入2。
谓词逻辑看起来就是用来描述需求和测试电路正确性。
n阶逻辑是有漏洞的,罗素悖论指出了这问题。在自我指涉的情况下会出现逻辑悖论。就是那个理发师只给不给自己理发的人理发的命题。这出现在二阶逻辑中。
组合逻辑电路
不妨就以加法器为例子,这在电脑中是一个非常重要的元件,加法,减法,乘法都依赖于他。如果仅仅考虑一位数的情况下,加法逻辑的真值表及一阶逻辑描述是这样的
加数 | 被加数 | 和/异或 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
0+0=0
1+0=0
0+1=1
1+1=0进位1
- 一阶逻辑描述为,对于所有的加数和被加数,和等价于输入1异或输入2。
\[\forall A \forall B (P(A,B,S) \longleftrightarrow S=A \oplus B) \]
很令人惊讶,加法就是异或,但是看最后一个格子。
等等,什么时候进位也必须明确的表示出来,让我们再加一列。进位的特点是只有最后一行1+1进位,其他时候不进位。进位刚好就是合取
加数 | 被加数 | 进位/合取 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
- 一阶逻辑描述为,对于所有的加数和被加数,进位等价于输入1合取输入2。
\[\forall A \forall B (P(A,B,C_{out}) \longleftrightarrow C_{out}=A \and B) \]
如果是2位数加法,就要考虑进位了。2位数加法有四个输入,2个输出,但第一位数加法依然用等价于异或,第二位数加法则是合取(进位)和异或的共同作用,进位和被加数的地位是相等的,所以加数高位+被加数高位就相当于加数高位+进位,进位也就相当于一次加法,高位和就是加数高位+被加数高位+进位。所以这个共同作用就是加数高位异或被加数高位,再异或第一位数加法的进位。
加数高位与被加数高位的和 | 低位的进位 | 最终的和/异或 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
- 一阶逻辑描述为,对于所有的加数高位和被加数高位和进位,和等价于输入1异或输入2异或进位。
而这个高位可以替换位任意一位,只是对于第一位,进位输入总是为0,所以对于加法器的每一位的一阶逻辑描述就是
- 对于所有的加数的第n位和被加数第n位和第n位的进位,和等价于输入1异或输入2异或进位。
\[\forall A \forall B \forall C_{in}(P(A,B,C_{in},S) \longleftrightarrow S= A \oplus B \oplus C_{in}) \]
考虑了进位输入之后,产生的进位输出也有所不同。但由于进位输出本身是加法产生的,所以加数、被加数、进位输入三个中只要有两个为真就会产生进位
加数高位 | 被加数高位 | 第一位数的进位/合取 | 进位输出 |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 |
1 | 0 | 0 | 0 |
1 | 0 | 1 | 1 |
0 | 1 | 0 | 0 |
0 | 1 | 1 | 1 |
1 | 1 | 0 | 1 |
1 | 1 | 1 | 1 |
- 其一阶逻辑描述是,对于所有的加数的第n位和被加数第n位和第n位的进位,进位输出等价于输入1合取输入2的结果析取输入1合取进位输入的结果析取进位输入合取输入2的结果。
\[\forall A \forall B \forall C_{in} (P2(A,B,C_{in},C_{out}) \longleftrightarrow C_{out}= (A \and B) \or (A \and C_{in}) \or (B \and C_{in})) \]
一阶逻辑表达式转化为电路
从一阶逻辑表达式转化为电路基本上就是照着表达式读一遍。
- 先看求和的表达式
\[\forall A \forall B \forall C_{in}(P(A,B,C_{in},S) \longleftrightarrow S= A \oplus B \oplus C_{in}) \]
1.A异或B。拖一个异或门到设计图上,两个输入分别是A和B
2.再异或\(C_{in}\)。再拖一个异或门,两个输入分别是前一个异或门的输出和\(C_{in}\),这一个异或门的输出就是和
- 再看求进位的表达式
\[\forall A \forall B \forall C_{in} (P2(A,B,C_{in},C_{out}) \longleftrightarrow C_{out}= (A \and B) \or (A \and C_{in}) \or (B \and C_{in})) \]
1.\(A \and B\)。拖一个与门到设计图上,输入分别是\(A\)和\(B\)。
2.\(A \and C_{in}\)。拖一个与门到设计图上,输入分别是\(A\)和\(C_{in}\)。
3.\(B \and C_{in}\)。拖一个与门到设计图上,输入分别是\(B\)和\(C_{in}\)。
4.\((A \and B) \or (A \and C_{in})\)。拖一个或门到设计图上,输入是第一步与门和第二步与门的输出
5.\((A \and B) \or (A \and C_{in}) \or (B \and C_{in})\)。拖一个或门到设计图上,输入是第4步与门和第5步与门的输出。这个与门的输出就是进位
- 多位加法器
将上面所得到的电路复制一份,然后前一份电路的输出接到后一份电路的输入上,多位的话就依次串联。这就得到了多位加法器。
一阶逻辑表达式就类似于用代入法取展开一个函数一样,比如
\[f(x)=x+1 \]
\[y=f(f(x)=((x+1)+1)=x+2 \]
以2位加法器为例,其一阶逻辑表达式这样推出
第一位求和表达式
\[\forall A1 \forall B1 \forall C_{in1}(P(A1,B1,C_{in1},S1) \longleftrightarrow S1= A1 \oplus B1 \oplus C_{in1}) \]
第一位进位表达式
\[\forall A1 \forall B1 \forall C_{in1} (P(A1,B1,C_{in1},C_{out1}) \longleftrightarrow C_{out1}= (A1 \and B1) \or (A1 \and C_{in1}) \or (B1 \and C_{in1})) \]
第二位求和表达式
\[\forall A2\forall B2\forall C_{in}(P(A2,B2,C_{out1},S2) \longleftrightarrow S2=A2 \oplus B2 \oplus C_{out1})) \]
将第一位进位表达式带入第二位求和表达式
\[\forall A1\forall A2\forall B1\forall B2\forall C_{in}(P(A1,A2,B1,B2,C_{in1},S2) \longleftrightarrow A2 \oplus B2 \oplus ((A1 \and B1) \or (A1 \and C_{in1}) \or (B1 \and C_{in1}))) \]
第二位进位表达式
\[\forall A2 \forall B2 \forall C_{out1} (P(A2,B2,C_{out1},C_{out2}) \longleftrightarrow C_{out2}= (A2 \and B2) \or (A2 \and C_{out1}) \or (B2 \and C_{out1})) \]
将第一位进位表达式带入第二位进位表达式
\[\forall A1 \forall B1\forall A2 \forall B2 \forall C_{in1} (P(A1,B1,A2,B2,C_{in1},C_{out2}) \longleftrightarrow C_{out2}= (A2 \and B2) \or (A2 \and ((A1 \and B1) \or (A1 \and C_{in1}) \or (B1 \and C_{in1}))) \or (B2 \and ((A1 \and B1) \or (A1 \and C_{in1}) \or (B1 \and C_{in1})))) \]
一阶逻辑本来就和函数差不多,表达式变换也是很简单的。
用不变实现变化的方法,译码器和开关
典型的如同ifelse
结构的代码,代码的执行是可变的,不是在if中,就是在else中。那么电路是怎么实现的?
电路中没有可动的开关,但是译码器配合开关也能实现同样的效果。
所谓译码器,就是把一个指令数映射为一个序数。比如输入2-4译码器,输入0,第一个输出亮起,输入01也就是1,第二个输出亮起,输入10也就是2,第三个输出亮起。
每一根信号线接到一个开关控制上,这样就实现了只有某一根数据线被选通。
比如选择ALU输出加法器和还是减法器的差。
比如读取第一个寄存器的值还是读取第二个寄存器的值。
比如将ALU的计算结果缓存到哪一个寄存器
这种方式导致了电路中我们能看到的大量平行导线。在CPU,内存中都使用了这种结构。
指令跳转
循环、分支结构都离不开跳转,甚至以前还有goto语句。跳转语句是实现图灵完备的关键。也依赖于译码器。比如上一句代码写明要跳转到哪一行,也许是一个循环结构的开始,进行下一次循环。也许是分支结构的某一个分支。一方面,译码器激活跳转选择器,将要跳转的地址赋值给计数器,而不是让计数器自增为下一句代码行数。
另一方面,内存使用这个跳转数字经过内存内部的译码器,激活内存内部对应地址的可读开关,关闭其他寄存器的可读开关。这样就实现了跳转。
通用计算机
将内存,译码器,寄存器组,ALU这几个元件连接起来,就构成了通用计算机。
在时钟的驱动下,内存的某条指令经过CPU的译码器,从寄存器取数据,将数据送到ALU,ALU的内部的译码器又根据指令决定是做加法还是减法。是将结果输出到某个寄存器,还是将结果输出到某个端口,比如网口。同时这个指令也会经过一个分叉路口来到计数器那边,决定其自增或跳转,在下一个时钟刻执行下一条指令。就像经典的图灵机一样。而这个时钟的频率现在可达GHz。
事实上有些编程语言也采用类似的方式来实现的,比如brainfuck
汇编
指对于一个逻辑元件来说,指令就是几个输入,每个输入要么真,要么假。如果我们把这个输入视为一个二进制数的各个位数的话,那么输入就变成了代码。现在代码已经进入了我们视野,尽管他以数字的形式存在。但是我们已经根据计算机架构,为每个数字约定好了具体的含义。通常,我们可以借助第三方软件,或手工,通过查表的方式,将汇编代码映射为数字,放到内存中。
体系架构
代码复用,即函数是很重要的。没有函数,程序会长很多。因此有必要用电路来实现函数调用。函数的特点是层层调用,层层返回
\[y=f(g(a(x))) \]
这刚好可以用栈来实现。只需要增加一个对内存稍加改造,让一个寄存器记住现在读取到哪个位置了,控制读取只能根据这个值+1或-1就行。在栈中存储函数调用时的地址、参数,在函数返回时将栈中的数据弹出,将寄存器组还原到之前的状态。
寄存器的数量是稀少的,因此还可以再加一块内存,来存放计算过程中产生的数据,比如排序时要用大量寄存器。图灵完备游戏中采用了这种被称为哈佛架构的计算机架构。