Nand2Tetris_Part1_04

接着 2 年前的课程继续学习😂...

Unit 4.1 Machine Languages: Overview

本小节主要介绍与机器语言相关的一些基本认识,大胡子老师提到了图灵祖师爷和冯诺伊曼祖师爷😂。不得不说,只要是跟计算机相关的东西,一定要介绍这 2 位祖师爷。
实际上,这个小节主要介绍的内容是讲内存与 CPU 如何沟通,通俗来讲,就是在讲计算机是如何执行指令的。因为 CPU 只认识 0 和 1 的比特流,所以内存需要讲这些比特流传给 CPU,由 CPU 计算好了之后在传给 RAM。而那些比特流就是指令(Instruction),而 CPU 计算的结果就是人需要的数据(Data)。当然了,CPU 传给 RAM 的数据依然是比特流,转换为人能看懂的东西,还需要特定的软件来阅读。

最后,大胡子老师又讲了一点跟机器语言相关的东西,不过这机器语言看着挺像汇编的,虽然也没学过汇编...😂

Unit 4.2 Machine Language: Elements

依然延续上一节和机器语言相关的内容,不过这一小节讲的更多的是机器语言的语法,或者说,是这一条指令在计算机(Hack)内如何在执行。

Addressing Modes

机器语言的指令都是地址模式(Addressing Mode,也不知道这么翻译对不对?)的,按照大胡子老师的讲解,机器语言可以直接作用于寄存器,大概就是这个意思。
举了四个例子:

1
2
3
4
5
6
7
8
9
10
11
// Register
Add R1, R2 // R2 <- R2 + R1

// Direct
Add R1, M[200] // Mem[200] <- Mem[200] + R1

// Indirect
Add R1, @A // Mem[A] <- Mem[A] + R1

// Immediate
Add 73, R1 // R1 <- R1 + R3

这难道看着不就是汇编吗?😂

Unit 4.3 The Hack Computer and Machine Language

本小节主要讲解即将要做的 Hack Computer 的工作原理和在 Hack 上用的 Machine Language 的一些语法。
Hack 中的机器语言的执行过程是:
hardware1
实际上也就是 CPU 读取保存在 instruction memory 内的指令(instructions),然后加载保存在 data memory 中的数据,计算后,将结果保存在 data memory 中。
实际上,这个图还可以画成:
hardware2
这样会更清晰一些。
Hack 是个 16 的计算机,所以这些硬件的总线宽度也是 16-bits 的。

The A-instruction

直译是 A-指令,实际上就是 Hack 的机器语言中的一种语法,利用@来表示地址,后面跟上数字,这个数字代表内存地址。
只不过,这个语法需要借助 A 寄存器来完成。所以实际上,Hack 中这条语句的执行过程是先将@后的地址存入 A 寄存器,而 A 寄存器中的地址值,会被当作RAM[A]来使用,实际上 M 就是 RAM[A],基本语法:

1
2
@0
M=1 // RAM[0] = 1

The C-instruction

C-指令,这是个复合语句,有点类似 C/C++ 中的 for 循环,基本语法:

1
2
3
4
dest = comp; jump   // both dest and jump are optional
comp = 0, 1, -1, D... // read the book get the others
dest = null, M, D, MD... // read the book get the others
jump = null, JGT, JEQ... // read the book get the others

老师举得例子这里就不写了。

Unit 4.4 Hack Language Specification

本小节继续在讲与 Hack Language 相关的内容,Hack program 就是用 Hack machine language 写的一系列指令,实际上这与其他程序语言是一样的。接着,老师又讲了讲符号指令(Symbolic instructions)和二进制代码在 Hack Language 中的对应关系(这些查书都可以查到,理解起来也比较简单)。

Unit 4.5 Input/Output

本小节主要介绍了在 Hack 中如何使用两个 I/O 设备。
首先是 Screen,Hack 中的 Screen 是一个 8K 的内建芯片(built-in chip),实际上是一个二维矩阵,512 列,256 行,这个矩阵的每一个元素代表一个 bit,直接通过地址来操控这些 bit。对应的,按照地址的索引方式有两个:
$ Screen[32 * row + col / 16] $ 和 $ RAM[16384 + 32 * row + col / 16]$,二者本质上是一样的,只不过前者直接从代表 Screen 的那部分内存开始索引,而后者从整个内存单元的开头进行索引。从这里也可以看出,Screen 在内存中的开始地址是 16385。
第二个介绍的键盘的使用,也是一个内建芯片(built-in chip),但是这个内建芯片没有输入,只有输出,对应的输入其实就是你现在用的键盘,每按一下不同的按键,点击 tick,就可以产生不同的效果。

Unit 4.6 Hack Programming Part 1

本小节是讲解 Hack Machine Language 的第一课,主要内容是 Hack Machine Language 是如何操作寄存器和内存的。只需要按照具体的语法书写指令,就可以操作寄存器和内存了。但是这样写出的程序,存在一个问题,那就是没法结束,老师给的建议是完成功能后,在结尾写一个循环。

Unit 4.7 Hack Programming Part 2

本小节是讲解 Hack Machine Language 的第二课,主要内容是 Hack Machine Language 中的分支(Branching)、变量(Variables)和迭代(Iteration)三种结构的用法,这三种结构都是由前面提到的 A 指令和 C 指令构成的。

Branching

分支结构会用到类似 C 语言中go to关键字的功能,会跳转到一行开始执行。这里,又引出了行号的概念,也就是可以直接写成@8,这个数字 8 就代表跳到第 8 行开始执行。为了提高可读性,可以写成@label,只需要再在后面的某一行写上(label)即可,其中的 label 是可以写成有意义的字符的。

Variables

变量的用法和分支中 label 的用法是类似的写法都是@label,如果没有在后面的语句中写上(label),那么汇编器就会认为这个 label 是个变量。
另外,变量的实际内存地址是从地址 16 开始的,这个地址位于 RAM 芯片上。

Iteration

同样,循环结构也是由 A 指令和 C 指令构成。老师没有具体讲解循环指令的写法,给了一个累加的示例程序自己看。然后就是书写这类程序的建议,先写好伪代码,然后再写成机器语言代码。

Unit 4.8 Hack Programming Part 3

这是最后一节课了,终于要讲完了哈...😂
这节课提到了指针的概念,为了解释指针的使用,老师举了一个例子。高级语言中的数组在低级语言中是不存在的,那低级语言是如何来完成对应的功能呢?答案是用指针完成的,数组名 arr 就是首地址,然后用一个 i 代表偏移量,也就有:$ arr + i = arr[i] $,显然这与 C 语言是一致的(果然,C 语言学的好,这方面很容易理解)。

后面又提到了之前提到的两个设备:屏幕和键盘,分别演示了两个 demo 程序,算是讲解了在 CPU Emulator 中如何使用这两个设备。

Unit 4.9 Project 4 Overview

这周的任务比较少,只有两个任务。
第一个任务是用机器语言写一个乘法程序,能正确获得 2 个数的乘法结果即可;第二个任务是用机器语言写一个交互程序,每当按下键盘的按键时,模拟器的屏幕就会变黑,松开按键就会还原。

Mult

从思路上来讲,乘法的实现很简单,ALU 是有加法运算功能的,所以写一个循环,重复加上相同的数字,最后将结果保存在指定的寄存器中即可。
先看下简单的加法是怎么写的:

1
2
3
4
5
6
7
8
9
// D = R0
@R0
D=M
// D = D + R1
@R1
D=D+M
// R2 = D
@R2
M=D

老师还讲过一个从 1 加到 100 的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Adds 1 + ... + 100
@i
M=1
@sum
M=0
(LOOP)
@i
D=M
@100
D=D-A
@END
D;JGT
@i
D=M
@sum
M=D+M
@i
M=M+1
@LOOP
0;JMP
(END)
@END
0;JMP

这个程序已经把完成这个任务需要的循环写好了,照着这个改一改就可以了,合理偷懒😘。
改完之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @i
M=0
@R2
M=0
(LOOP)
@R0
D=M
@i
D=D-M
@END
D;JLE // D < 0, jump to END
@R1
D=M
@R2
M=D+M
@i
M=M+1
@LOOP
0;JMP
(END)
@END
0;JMP

用测试脚本试了下,没有问题。需要说明的是,循环的写法有很多种,不用局限于一种。
话说回来,这段代码的可读性是真的差。单独看这段代码,或者隔一段时间来看,应该完全不知道写的是啥...

Fill

这个问题可以分成 2 个子问题:

  1. 如何使屏幕全部变黑?
  2. 如何获取键盘输入?

先考虑第一个问题。
在考虑这个问题之前,先研究一下老师的视频中的示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    @R0
D=M
@n
M=D

@i
M=0

@SCREEN
D=A
@address
M=D // address = 16384 (base address of the Hack screen)

(LOOP)
@i
D=M
@n
D=D-M
@END
D;JGT // if i>n go END

@address
A=M
M=-1

@i
M=M+1
@32
D=A
@address
M=D+M // address = address + 32
@LOOP
0;JMP // go to LOOP

(END)
@END // program's end
0;JMP

这个程序最终实现的效果是,在屏幕上打印了一个 16 个像素宽,$RAM[0]$ 个长度的黑色矩形。从这个程序中,可以得出几个结论:

  1. 内存中一个地址指向的 16 位寄存器就代表了屏幕中 16 个像素点,把这个内存单元的值改为 -1,就可以使这 16 个像素点变黑。
  2. 地址每加 1,就会跳过 16 个像素点,也即每移动一个寄存器,就会跳过 16 个像素点。
  3. 屏幕的一行由 32 个内存单元(寄存器)来映射这些像素点。

有了这些结论,要想得到一个完全变黑的屏幕,只需要从 16834 这个地址开始,把接下来共 8192 个内存单元的值全置为 -1 即可。(实际上,16384 + 8192 = 24576,这个地址就是映射为键盘的地址,同时也可以知道为什么老师要说屏幕是 8K 的。)
修改下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    @8192
D=A
@n
M=D

@i
M=1

@SCREEN
D=A
@address
M=D // address = 16384 (base address of the Hack screen)

(LOOP)
@i
D=M
@n
D=D-M
@END
D;JGT // if i>n go END

@address
A=M
M=-1

@i
M=M+1
@1
D=A
@address
M=D+M // address = address + 32
@LOOP
0;JMP // go to LOOP

(END)
@END // program's end
0;JMP

实际上,只需要设置循环次数为 8192 次,i 的递进值改为 1 即可。运行后,就可以实现屏幕变黑的效果了。

现在考虑第二个问题。
有了前面的思考,目前已经知道了地址为 24576 的内存单元保存的是按键的值。简言之对应两种情况:

  1. 按键了,值不为 0。
  2. 未按键,值为 0。

那么,可以根据这个情况,分别执行将屏幕变黑和变白的子代码块(这个语言没有函数的概念,不知道怎么描述了...😂)。
修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
(PRIMARYLOOP)
@KBD
D=M
@0
D=D-A
@WHITE
D;JEQ
@BLACK
0;JMP

(BLACK)
@8192
D=A
@n
M=D

@i
M=1

@SCREEN
D=A
@address
M=D // address = 16384 (base address of the Hack screen)

(BLOOP)
@i
D=M
@n
D=D-M
@BEND
D;JGT // if i>n go BEND

@address
A=M
M=-1

@i
M=M+1
@1
D=A
@address
M=D+M // address = address + 1
@BLOOP
0;JMP // go to BLOOP

(BEND)
@PRIMARYLOOP
0;JMP

(WHITE)
@8192
D=A
@n
M=D

@i
M=1

@SCREEN
D=A
@address
M=D // address = 16384 (base address of the Hack screen)

(WLOOP)
@i
D=M
@n
D=D-M
@WEND
D;JGT // if i>n go WEND

@address
A=M
M=0

@i
M=M+1
@1
D=A
@address
M=D+M // address = address + 1
@WLOOP
0;JMP // go to WLOOP

(WEND)
@PRIMARYLOOP
0;JMP

上面的代码实际上是两个重复的部分,而主循环程序也不过 8 行,主要是背后的逻辑太简单了,就是一个条件语句,两个分支,把所有情况都写出来即可。不过,因为机器语言有很多跳跃语句,所以用于跳跃的 label 一定要写的清晰明了一点,不然容易把自己搞混。

Unit 4.10 Perspective

又到了这周的问题时间:

  1. Hack Machine Language 和现实中的机器语言有什么区别?
    • Hack Machine Language 很简单,相比之下,现实中的机器语言提供了更丰富的指令和功能,比如乘法和除法。但是,这些功能也可以在 Hack 这台计算机的更高层完美实现。
  2. 所有的机器语言是不是都要拥有改变寄存器的值和获取某个寄存器的地址这两个功能?
    • 实际上,现实的机器语言不仅仅只提供了这两个基础功能。
  3. 人们用机器语言写程序时,会遇到困难吗?
    • (这问题问的😂)实际上,人们并不直接用机器语言写程序,而是直接用高级语言写程序,再用编译器将高级语言翻译成机器语言。但是,如果要提高程序的性能,就必须要能看懂高级语言被翻译后的机器语言。对应的,这个课程的第二部分,也会学习与编译器相关的内容,并试着去编写一个编译器。

这周的主题是机器语言,相比前面的内容,这一章终于跟电路联系的不是那么紧密了。这一章的难点更多的是在语言的语法方面,好在老师给的示例程序很多,借用一下,就可以很好的完成作业了。


Buy me a coffee ? :)
0%