背景
让我们按照 From Nand to Tetris 里 Project 4 的要求,来完成下列的设计。
- 乘法(
Mult.asm) I/O处理(Fill.asm)
说明
我是在阅读了《计算机系统要素 (第2版)》 第 4 章的内容才去完成 Project 4 的。读者朋友在完成 Project 4 时,如果遇到不明白的地方,可以参考这本书中的描述。另外在 From Nand to Tetris 的网站上也能找到和 Project 4 的介绍(点击下图红色箭头所指位置,会来到 Project 4: Machine Language,我从中获益颇多 👍)
虽然本文是按照 伪代码 -> asm 代码 的顺序来进行描述的,但我自己在做 Project 4 时,是直接写 asm 代码的。直接写 asm 代码花费了不少时间,而且 debug asm 代码也花费了不少时间,如果读者朋友一下子写不出完整的 asm 代码,或者写完后没有立刻通过测试,也不要气馁,我觉得这个项目还是有些难度的。
正文
Project 4 中需要完成两个 asm 程序。
1 乘法(Mult.asm)
第一个 asm 程序与乘法有关,具体描述请参考下图(相关内容来自 《计算机系统要素 (第2版)》 第 55 页)
前往 Nand to Tetris Online IDE 的 Assembler 页面
在 Source 面板中打开 /projects/04/Mult 里的 Mult.asm 文件 ⬇️
打开这个文件后,效果如下 ⬇️ (一开始只有注释)
我们在页面上可以直接进行编辑。一种实现思路是这样的 ⬇️
- 要计算 ,而
- 将中间结果(以及最终结果)保存在 中,也就是说, 的变化过程是
- 参与求和操作的 的个数记录在 里
伪代码
用伪代码可以这样表示 ⬇️
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 ⬇️
转化后的效果如下图所示 ⬇️
在 Project 4 中,我们不用关心 Symbol Table 面板。点击 Binary Code 面板中的 CPU Emulator 按钮(位置如下图所示),就会来到 另一个页面
在新的页面中,我们可以找到 Run 按钮(具体位置如下图所示),点击它就可以进行测试
上述代码可以通过测试,具体效果如下图所示 ⬇️
2 I/O 处理 (Fill.asm)
第二个 asm 程序与 I/O有关,具体描述请参考下方两张图片(相关内容来自 《计算机系统要素 (第2版)》 第 55 页和第 56 页)
前往 Nand to Tetris Online IDE 的 Assembler 页面
在 Source 面板中有 Load file 按钮(具体位置如下图所示)
点击这个按钮之后,可以选择 /projects/04/Fill 里的 Fill.asm 文件 ⬇️
打开这个文件后,效果如下 ⬇️ (一开始内容为空)
我们在页面上可以直接进行编辑。
伪代码
一种实现思路是这样的 ⬇️ (这里写的是伪代码)
// 这里写的是伪代码
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 ⬇️
转化后,会看到这样的效果 ⬇️
在 Project 4 中,我们不用关心 Symbol Table 面板。点击 Binary Code 面板中的 CPU Emulator 按钮(位置如下图所示),就会来到 另一个页面
在新的页面中,我们可以找到 Run 按钮(具体位置如下图所示),点击它就可以进行测试。
请注意:
- 如果把控制运行速度的那个按钮放在
Slow和Fast中间的话,程序运行会非常慢,我个人的建议是让按钮和Fast的距离小于按钮和Slow的距离 - 需要允许键盘输入(当你看到
Disable Keyboard时,键盘输入是允许的)
以上两点都展示在下图里了 ⬇️
点击 Run 之后,随便在键盘上按一些键,就能看到有的像素变为黑色了。示例效果如下 ⬇️
参考资料
- 《计算机系统要素 (第2版)》 中的第 4 章
- From Nand to Tetris 网站提供的
- Project 4: Machine Language (很有帮助👍)