From Nand to Tetris 里的 Project 4

101 阅读5分钟

背景

让我们按照 From Nand to Tetris 里 Project 4 的要求,来完成下列的设计。

  1. 乘法(Mult.asm)
  2. I/O 处理(Fill.asm)

说明

我是在阅读了《计算机系统要素 (第2版)》 第 4 章的内容才去完成 Project 4 的。读者朋友在完成 Project 4 时,如果遇到不明白的地方,可以参考这本书中的描述。另外在 From Nand to Tetris 的网站上也能找到和 Project 4 的介绍(点击下图红色箭头所指位置,会来到 Project 4: Machine Language,我从中获益颇多 👍)

image.png

虽然本文是按照 伪代码 -> asm 代码 的顺序来进行描述的,但我自己在做 Project 4 时,是直接写 asm 代码的。直接写 asm 代码花费了不少时间,而且 debug asm 代码也花费了不少时间,如果读者朋友一下子写不出完整的 asm 代码,或者写完后没有立刻通过测试,也不要气馁,我觉得这个项目还是有些难度的。

正文

Project 4 中需要完成两个 asm 程序。

1 乘法(Mult.asm)

第一个 asm 程序与乘法有关,具体描述请参考下图(相关内容来自 《计算机系统要素 (第2版)》 第 55 页)

image.png

前往 Nand to Tetris Online IDE 的 Assembler 页面

image.png

Source 面板中打开 /projects/04/Mult 里的 Mult.asm 文件 ⬇️

image.png

打开这个文件后,效果如下 ⬇️ (一开始只有注释)

image.png

我们在页面上可以直接进行编辑。一种实现思路是这样的 ⬇️

  1. 要计算 R0×R1R_0\times R_1,而 R0×R1=R0+R0++R0R1R0相加R_0\times R_1=\underbrace{R_0+R_0+\cdots +R_0}_{R_1\text{个}R_0相加}
  2. 将中间结果(以及最终结果)保存在 R2R_2 中,也就是说,R2R_2 的变化过程是 0R0×1R0×2R0×3R0×R10\to R_0\times 1\to R_0\times 2\to R_0\times 3 \cdots \to R_0\times R_1
  3. 参与求和操作的 R0R_0 的个数记录在 cntcnt

伪代码

用伪代码可以这样表示 ⬇️

var cnt = 0
RAM[R2] = 0

while (RAM[R1] != cnt) {
    RAM[R2] += RAM[R0]
    cnt++
}

asm 代码

基于上述伪代码,可以写出如下的 asm 代码 ⬇️

// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/4/Mult.asm

// Multiplies R0 and R1 and stores the result in R2.
// (R0, R1, R2 refer to RAM[0], RAM[1], and RAM[2], respectively.)
// The algorithm is based on repetitive addition.

// initialize RAM[R2] with 0
@R2
M=0

// initialize @cnt with 0
@cnt
M=0

// while R1 != cnt
(LOOP)
    // calculate R1 - cnt
    @R1
    D=M
    @cnt
    D=D-M
    @END
    D;JEQ

    // R2 += R0
    @R0
    D=M
    @R2
    M=D+M

    // cnt++
    @cnt
    M=M+1

    @LOOP
    0;JMP

(END)
    @END
    0;JMP

验证

点击 Source 面板中的 Translate all 按钮,就可以将 asm 代码转化为 binary code ⬇️

image.png

转化后的效果如下图所示 ⬇️

image.png

Project 4 中,我们不用关心 Symbol Table 面板。点击 Binary Code 面板中的 CPU Emulator 按钮(位置如下图所示),就会来到 另一个页面

image.png

在新的页面中,我们可以找到 Run 按钮(具体位置如下图所示),点击它就可以进行测试

image.png

上述代码可以通过测试,具体效果如下图所示 ⬇️

image.png

2 I/O 处理 (Fill.asm)

第二个 asm 程序与 I/O有关,具体描述请参考下方两张图片(相关内容来自 《计算机系统要素 (第2版)》 第 55 页和第 56 页)

image.png

image.png

前往 Nand to Tetris Online IDE 的 Assembler 页面

Source 面板中有 Load file 按钮(具体位置如下图所示)

image.png

点击这个按钮之后,可以选择 /projects/04/Fill 里的 Fill.asm 文件 ⬇️

image.png

打开这个文件后,效果如下 ⬇️ (一开始内容为空)

image.png

我们在页面上可以直接进行编辑。

伪代码

一种实现思路是这样的 ⬇️ (这里写的是伪代码)

// 这里写的是伪代码
while (true) {
    for (position in SCREEN to (KBD - 1)) {
        var mask = 1 // mask 中的 1 逐步从 LSB 向 MSB 移动
        while (mask > 0) {
            if (RAM[KBD] > 0) {
                RAM[position] = RAM[position] | mask
            }  else {
                RAM[position] = RAM[position] & (~mask)
            }
            mask = mask + mask // 当 mask 等于 16384 时,mask + mask 会溢出
        }
    }
}

这样的伪代码和 asm 程序还有比较大的距离,我们可以在上述伪代码的基础上,写出如下的(更接近 asm 代码的)伪代码 ⬇️

// 这里写的是更接近 asm 的伪代码
init:
var position = SCREEN // SCREEN = 16384

// 主循环
main_loop:
while (true) {
    if (position - KBD == 0) { // KBD = SCREEN + 8192
        goto init
    }
   
    var mask = 1
    
    mask_loop:
    if (RAM[KBD] > 0) {
        goto black
    }
    goto white
    
    black:
    RAM[position] |= mask
    goto move_mask
    
    white:
    RAM[position] &= ~mask
    
    move_mask:
    if (mask < 0) {
        goto inc_position
    }
    mask = mask + mask
    goto mask_loop
    
    inc_position:
    position++
}

asm 代码

在此基础上,可以写出对应的 asm 代码 ⬇️

// initialize position with SCREEN
(INIT)
    @SCREEN
    D=A
    @position
    M=D

// main loop
(MAIN_LOOP)
    // if position == KBD, then reset position to SCREEN
    @KBD
    D=A
    @position
    D=D-M
    @INIT
    D;JEQ

    // now we are sure that SCREEN <= position < KBD

    // initialize mask with 1
    @mask
    M=1

    (MASK_LOOP)
        @KBD
        D=M

        @BLACK
        D;JGT
        @WHITE
        0;JMP

        // if any key is pressed
        (BLACK)
            @position
            D=M
            A=D
            D=M
            @mask
            D=D|M
            @position
            A=M
            M=D

            @MOVE_MASK
            0;JMP

        // if no key is pressed
        (WHITE)
            @mask
            D=!M
            @invertedMask
            M=D
            @position
            D=M
            A=D
            D=M
            @invertedMask
            D=D&M
            @position
            A=M
            M=D

        (MOVE_MASK)
            @mask
            D=M
            @INC_POSITION
            D;JLT
            // move the mask 1 bit towards MSB
            @mask
            M=D+M
            @MASK_LOOP
            0;JMP

    // increment position, i.e. position++
    (INC_POSITION)
        @position
        M=M+1
        @MAIN_LOOP
        0;JMP

验证

写好 asm 代码后,在 Source 面板上点击 Translate all 就可以将 asm 代码转化为 binary code ⬇️

image.png

转化后,会看到这样的效果 ⬇️

image.png

Project 4 中,我们不用关心 Symbol Table 面板。点击 Binary Code 面板中的 CPU Emulator 按钮(位置如下图所示),就会来到 另一个页面

image.png

在新的页面中,我们可以找到 Run 按钮(具体位置如下图所示),点击它就可以进行测试。

image.png

请注意:

  1. 如果把控制运行速度的那个按钮放在 SlowFast 中间的话,程序运行会非常慢,我个人的建议是让按钮和 Fast 的距离小于按钮和 Slow 的距离
  2. 需要允许键盘输入(当你看到 Disable Keyboard 时,键盘输入是允许的)

以上两点都展示在下图里了 ⬇️

image.png

点击 Run 之后,随便在键盘上按一些键,就能看到有的像素变为黑色了。示例效果如下 ⬇️

image.png

参考资料