课程 nand2tetris Part-I 回顾

⭐⭐⭐⭐⭐推荐

课程脉络

布尔逻辑

关键的理论保证:任何一个真值表,都能用布尔表达式来实现

这一章以与非门为起点,构建了基础了逻辑门,Not、And、Or、Xor, 并且实现了选择器(Mux)和分发器(DMux)。

两个想法:

  1. 用我自己的话说,Mux就是赛马比赛,分半区,一个个地选择局部冠军,最后到总冠军;DMux就是在二叉树中的接力赛,一层层传递,但是每层只有一个节点包含输入值。

  2. Mux启示,在布尔逻辑层,if的实现是两个分支都计算,然后选择结果,而不是像高级语言那样,判断条件后选择分支去计算结果。

算术逻辑单元

构建的逻辑是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
半加器 两个bit,(a, b),计算出(进位,结果)

|
v

全加器 三个bit,(进位,a,b),计算出(进位,结果)

|
v

加法器 串联全加器,每个全加器给出一位结果,并向下一个全加器传入进位

|
v

算术逻辑单元 给出控制位,控制计算行为,比如计算a+b, !a, -a, a & b等
这是CPU芯片能力的体现,乘除法、浮点计算可以有专门的单元

内存

构建的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DFF 原语

|
v

Bit寄存器 Mux + DFF + load控制位

|
v

16位寄存器

|
v

RAM N个16位寄存器 + address, N=8,64, 512, 4K,...

\_ PC 程序计数器

寄存器

区分了两类元件,combinationalsequential

关键在于元件是否含有触发器(flip-flop),在本课程中,特指DFF,Data Flip Flop。包含的就属于sequential

DFF的特点是,它的输出,是前一个时刻的输入,表达式为out(t) = in(t-1)

所谓时刻,是指计算机都有一个全局的时钟脉冲,高-低-高-低的循环往复,tick, tock, tick, tock。而触发器总会被高电平或者低电平所”触发”而产生输出,而这个输出,像前面说的,就是上一个脉冲时,输入是多少。

正是因为这样的特性,sequential可以把输出接到输入上,因为他们实际上是不同时刻的值,不会造成问题。而combinational的输入和输出是在同一时刻确定的,所以不能形成有效的loop,比如之前的And、Or、加法器等等。输出接输入loop的意义是,DFF可以记住自己的值,也就是在时间(tick-tock)流动过程中,保持输出值不变。

寄存器就是在利用DFF,增加一个load的控制,显示控制是否让DFF记住新的值,换个说法就是读/写模式的切换。如下图

1
2
3
4
5
6
7
8
                |
| load
in v
------------>+--+--+ +-----+ out
| Mux +----> + DFF + --------------->
-->+-----+ +-----+ |
| |
----------------------------

load=1的时刻,寄存器的输出仍然时上一次储存的值,但是Mux已经选择了in成为DFF的输入,所以下一个时刻,寄存器将输出in

RAM

字:用16位的寄存器来造内存,16就是字长

RAM由多个寄存器组成,输入为loadaddress[16],输出为out[16]

address能够指定了读写具体发生在哪一个寄存器上

RAM的全称为Random Access Memory,含义是,任意指定address,都能在相同的单位时间内完成读写。这是MuxDMux下层电路的能力。

RAM的叠加过程很简单,MSB指定大区,剩下的指定具体位置,然后递归,在每个大区里面执行一样的动作。

Counter

程序计数器三个基本控制位:

  • reset 起始数字归0
  • load 写入新的起始数字
  • inc +1输出

优先级依次降低,实现上可以反向计算结果,然后选择结果输出

下面一张图,更清晰地理解,上一时刻的状态决定当前时间输出的特性:

GzfRkq.png

机器码和汇编

机器码是给CPU看的,一串01010101001, 要包含三方面的信息:

从哪来?干什么?到哪去?

  1. 操作数在哪里,怎么读取它? 比如操作数直接在寄存器里,或者在主存某个位置
  2. 要执行什么操作? 比如之前造的ALU支持6位的控制位,那么机器码至少要包含这6个控制位,ALU才能读懂要做什么事情。
  3. 当前指令执行完后,下一条执行什么? 没有指定,就顺序执行,否则就按照指令中的规则跳转
1
2
3
000010101 0010101 01010
--------- ------- -----
指令 操作数 跳转

所谓寻址,是看待操作数的0101的不妨方式

  • 立即数,操作数本身就是数据,实际上没有必要寻址 e.g. LOADI R1, 67 // R1 <- 67
  • 寄存器寻址,操作数是寄存器的地址,从寄存器中读取数据 e.g. Add R2, R1, R3/ / R2 <- R1 + R3
  • 直接寻址,数据在主存中,操作数就是数据在主存中的地址,到内存中读取得到数据 e.g. LOAD R1, 67 // R1 <- M[67]
  • 间接寻址,操作数是一个地址,读取其内容(主存、寄存器)后,才得到数据在内存中的地址,再从内中得到数据 e.g. LOAD* R2, R1 // R2 <- M[R1]

所以间接寻址,就可以看作是C语言中的指针的实现

指令包含加减法、布尔操作等

跳转指根据ALU的计算结果(>0, <0 ...)来触发跳转

汇编语言就是建立起字符串和底层01001的映射关系,方便人来书写。并且支持一定的符号解析功能。符号解析在第六章-构建汇编器,有仔细说明

制作CPU

CPU要能理解上一节的机器码,因此它的主要功能有:

  1. 解析机器码,转化为内部CPU的控制位
  2. 读取、储存操作数,因此需要内部寄存器作为CPU的组件
  3. 执行计算,因此需要一个ALU作为组件
  4. 判断是否跳转,输出下一条指令的位置,因此需要一个Counter作为组件

在Hack中,内存的操作数的读取是用A-instruction读取的,所以没有同步的问题

编写汇编器

总体上,汇编器是简单的文本处理程序,翻译为二进制格式。

只是会因为增加更多的数据类型以及支持符号解析而变得复杂一些。

本课程中的符号分为标签和变量。标签是机器码指令的地址。变量是内存地址。变量对应的具体的内存地址的值不重要,一般按照地址空间递增就好了。高级语言的变量其实也是这样,变量就是某个内存地址,变量的值,是那个地址中的内容。

流程是两次扫描。第一次解析全局标签。第二次生成机器码。

补充

课程配套简直完美。教材语言简单直接,Project环境完备操作方便反馈清晰,每个Project难度适中,对我来说刚刚好,做起来很顺畅,做完有收获有成就感。

总体脉络写的是通用的知识点,HACK本身的一些笔记和想法,放在了gitbub上备份存档


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!