From Nand to Tetris 里的 Project 3

107 阅读15分钟

背景

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

  1. 实现 Bit (11 位寄存器)
  2. 实现 Register (1616 位寄存器)
  3. 实现 RAM8
  4. 实现 RAM64
  5. 实现 RAM512
  6. 实现 RAM4K
  7. 实现 RAM16K
  8. 实现 PC (Program Counter, 程序计数器)

说明

我是阅读了《计算机系统要素 (第2版)》 第 3 章的内容之后才去完成 Project 1 的。读者朋友在完成 Project 3 时,如果遇到不明白的地方,可以参考这本书中的描述。

正文

1. 实现 Bit (11 位寄存器)

前往 Nand to Tetris Online IDE,选择 Project 3 里的 Bit,效果如下图所示 ⬇️

image.png

我们的目标是用 DFF(Data Flip-Flop, 数据触发器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 Bit(即,11 位寄存器)。

注释中的相关描述如下 ⬇️

/**
 * 1-bit register:
 * If load is asserted, the register's value is set to in;
 * Otherwise, the register maintains its current value:
 * if (load(t)) out(t+1) = in(t), else out(t+1) = out(t)
 */
  • 输入是
    • inin
    • loadload
  • 输出是
    • outout

书中有一些关于 11 位寄存器设计的描述 ⬇️

image.png

微信圖片_2025-12-27_102957_841.jpg

结合书中的描述,我们可以这样来实现

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    Mux(a= previousOut, b= in, sel= load, out= nextOut);
    DFF(in= nextOut, out=out, out= previousOut);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

2. 实现 Register (1616 位寄存器)

前往 Nand to Tetris Online IDE,选择 Project 3 里的 Register,效果如下图所示 ⬇️

image.png

我们的目标是用 Bit(11 位寄存器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 Register(即,1616 位寄存器)。

注释中的相关描述如下 ⬇️

/**
 * 16-bit register:
 * If load is asserted, the register's value is set to in;
 * Otherwise, the register maintains its current value:
 * if (load(t)) out(t+1) = int(t), else out(t+1) = out(t)
 */
  • 输入是
    • in[16]in[16]
    • loadload
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 Bit(11 位寄存器),既然 Register1616 位寄存器,那么可以把 Register 看成 161611 位寄存器的组合。

代码的整体结构如下 ⬇️

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Bit(in= in[0], load= load, out= out[0]);
    Bit(in= in[1], load= load, out= out[1]);
    ...
    Bit(in= in[15], load= load, out= out[15]);
}

由于这里有很多行类似的代码,自己复制粘贴加修改感觉有点浪费时间,不如用 java 代码来生成它吧。 请将以下代码保存为 RegisterHdlCodeGenerator.java

import java.util.StringJoiner;

public class RegisterHdlCodeGenerator {

    public static void main(String[] args) {
        String template = """
                CHIP Register {
                    IN in[16], load;
                    OUT out[16];
                
                    PARTS:
                %s
                }
                """;

        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        for (int i = 0; i < 16; i++) {
            String line = String.format("    Bit(in= in[%s], load= load, out= out[%s]);", i, i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);
    }
}

使用如下的命令可以编译 RegisterHdlCodeGenerator.java 并运行其中的 main 方法。

javac RegisterHdlCodeGenerator.java
java RegisterHdlCodeGenerator

运行结果如下

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Bit(in= in[0], load= load, out= out[0]);
    Bit(in= in[1], load= load, out= out[1]);
    Bit(in= in[2], load= load, out= out[2]);
    Bit(in= in[3], load= load, out= out[3]);
    Bit(in= in[4], load= load, out= out[4]);
    Bit(in= in[5], load= load, out= out[5]);
    Bit(in= in[6], load= load, out= out[6]);
    Bit(in= in[7], load= load, out= out[7]);
    Bit(in= in[8], load= load, out= out[8]);
    Bit(in= in[9], load= load, out= out[9]);
    Bit(in= in[10], load= load, out= out[10]);
    Bit(in= in[11], load= load, out= out[11]);
    Bit(in= in[12], load= load, out= out[12]);
    Bit(in= in[13], load= load, out= out[13]);
    Bit(in= in[14], load= load, out= out[14]);
    Bit(in= in[15], load= load, out= out[15]);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

3. 实现 RAM8

前往 Nand to Tetris Online IDE,选择 Project 3 里的 RAM8,效果如下图所示 ⬇️

image.png

我们的目标是用 Register(1616 位寄存器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 RAM8(即,881616 位寄存器的存储器)。

注释中的相关描述如下 ⬇️

/**
 * Memory of eight 16-bit registers.
 * If load is asserted, the value of the register selected by
 * address is set to in; Otherwise, the value does not change.
 * The value of the selected register is emitted by out.
 */
  • 输入是
    • in[16]in[16]
    • loadload
    • address[3]address[3]
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 Register(1616 位寄存器)。现在需要选择将 in[16]in[16] 输入 88Register 中的哪一个,可以使用 Project 1 里的 DMux8Way,对 out[16]out[16] 而言,我们可以借助 Project 1 里的 Mux8Way1688Register 的输出中进行选择。

代码的整体结构如下 ⬇️

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address,
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    Register(in= in, load= a, out= out0);
    Register(in= in, load= b, out= out1);
    ...
    Register(in= in, load= h, out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address, out= out);
}

由于这里有很多行类似的代码,自己复制粘贴加修改感觉有点浪费时间,不如用 java 代码来生成它吧。 请将以下代码保存为 RAM8HdlCodeGenerator.java

import java.util.StringJoiner;

public class RAM8HdlCodeGenerator {
    public static void main(String[] args) {
        String template = """
                CHIP RAM8 {
                    IN in[16], load, address[3];
                    OUT out[16];
                
                    PARTS:
                    DMux8Way(in= load, sel= address,
                    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);
                
                %s
                
                    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
                    e= out4, f= out5, g= out6, h= out7, 
                    sel= address, out= out);
                }
                """;
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        for (int i = 0; i < 8; i++) {
            String line = String.format("    Register(in= in, load= %s, out= out%s);", (char) ('a' + i), i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);

    }
}

使用如下的命令可以编译 RAM8HdlCodeGenerator.java 并运行其中的 main 方法。

javac RAM8HdlCodeGenerator.java
java RAM8HdlCodeGenerator

运行结果如下

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address,
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    Register(in= in, load= a, out= out0);
    Register(in= in, load= b, out= out1);
    Register(in= in, load= c, out= out2);
    Register(in= in, load= d, out= out3);
    Register(in= in, load= e, out= out4);
    Register(in= in, load= f, out= out5);
    Register(in= in, load= g, out= out6);
    Register(in= in, load= h, out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address, out= out);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

4. 实现 RAM64

前往 Nand to Tetris Online IDE,选择 Project 3 里的 RAM64,效果如下图所示 ⬇️

image.png

我们的目标是用 RAM8(即,881616 位寄存器的存储器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 RAM64(即,64641616 位寄存器的存储器)。

注释中的相关描述如下 ⬇️

/**
 * Memory of sixty four 16-bit registers.
 * If load is asserted, the value of the register selected by
 * address is set to in; Otherwise, the value does not change.
 * The value of the selected register is emitted by out.
 */
  • 输入是
    • in[16]in[16]
    • loadload
    • address[6]address[6]
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 RAM8,可以用类似的思路来实现 RAM64

代码的整体结构如下 ⬇️

CHIP RAM64 {
    IN in[16], load, address[6];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[3..5],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM8(in= in, load= a, address= address[0..2], out= out0);
    RAM8(in= in, load= b, address= address[0..2], out= out1);
    ...
    RAM8(in= in, load= h, address= address[0..2], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[3..5], out= out);
}

由于这里有很多行类似的代码,自己复制粘贴加修改感觉有点浪费时间,不如用 java 代码来生成它吧。 请将以下代码保存为 RAM64HdlCodeGenerator.java

import java.util.StringJoiner;

public class RAM64HdlCodeGenerator {
    public static void main(String[] args) {
        String template = """
                CHIP RAM64 {
                    IN in[16], load, address[6];
                    OUT out[16];
                
                    PARTS:
                    DMux8Way(in= load, sel= address[3..5],
                    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);
                
                %s
                
                    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
                    e= out4, f= out5, g= out6, h= out7,
                    sel= address[3..5], out= out);
                }
                """;
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        for (int i = 0; i < 8; i++) {
            String line = String.format("    RAM8(in= in, load= %s, address= address[0..2], out= out%s);", (char) ('a' + i), i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);
    }
}

使用如下的命令可以编译 RAM64HdlCodeGenerator.java 并运行其中的 main 方法。

javac RAM64HdlCodeGenerator.java
java RAM64HdlCodeGenerator

运行结果如下

CHIP RAM64 {
    IN in[16], load, address[6];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[3..5],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM8(in= in, load= a, address= address[0..2], out= out0);
    RAM8(in= in, load= b, address= address[0..2], out= out1);
    RAM8(in= in, load= c, address= address[0..2], out= out2);
    RAM8(in= in, load= d, address= address[0..2], out= out3);
    RAM8(in= in, load= e, address= address[0..2], out= out4);
    RAM8(in= in, load= f, address= address[0..2], out= out5);
    RAM8(in= in, load= g, address= address[0..2], out= out6);
    RAM8(in= in, load= h, address= address[0..2], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[3..5], out= out);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

5. 实现 RAM512

前往 Nand to Tetris Online IDE,选择 Project 3 里的 RAM512,效果如下图所示 ⬇️

image.png

我们的目标是用 RAM64(即,64641616 位寄存器的存储器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 RAM512(即,5125121616 位寄存器的存储器)。

注释中的相关描述如下 ⬇️

/**
 * Memory of 512 16-bit registers.
 * If load is asserted, the value of the register selected by
 * address is set to in; Otherwise, the value does not change.
 * The value of the selected register is emitted by out.
 */
  • 输入是
    • in[16]in[16]
    • loadload
    • address[9]address[9]
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 RAM64,可以用类似的思路来实现 RAM512

代码的整体结构如下 ⬇️

CHIP RAM512 {
    IN in[16], load, address[9];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[6..8],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM64(in= in, load= a, address= address[0..5], out= out0);
    RAM64(in= in, load= b, address= address[0..5], out= out1);
    ...
    RAM64(in= in, load= h, address= address[0..5], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[6..8], out= out);
}

由于这里有很多行类似的代码,自己复制粘贴加修改感觉有点浪费时间,不如用 java 代码来生成它吧。 请将以下代码保存为 RAM512HdlCodeGenerator.java

import java.util.StringJoiner;

public class RAM512HdlCodeGenerator {
    public static void main(String[] args) {
        String template = """
                CHIP RAM512 {
                    IN in[16], load, address[9];
                    OUT out[16];
                
                    PARTS:
                    DMux8Way(in= load, sel= address[6..8],
                    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);
                
                %s
                
                    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
                    e= out4, f= out5, g= out6, h= out7,
                    sel= address[6..8], out= out);
                }
                """;
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        for (int i = 0; i < 8; i++) {
            String line = String.format("    RAM64(in= in, load= %s, address= address[0..5], out= out%s);", (char) ('a' + i), i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);
    }
}

使用如下的命令可以编译 RAM512HdlCodeGenerator.java 并运行其中的 main 方法。

javac RAM512HdlCodeGenerator.java
java RAM512HdlCodeGenerator

运行结果如下

CHIP RAM512 {
    IN in[16], load, address[9];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[6..8],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM64(in= in, load= a, address= address[0..5], out= out0);
    RAM64(in= in, load= b, address= address[0..5], out= out1);
    RAM64(in= in, load= c, address= address[0..5], out= out2);
    RAM64(in= in, load= d, address= address[0..5], out= out3);
    RAM64(in= in, load= e, address= address[0..5], out= out4);
    RAM64(in= in, load= f, address= address[0..5], out= out5);
    RAM64(in= in, load= g, address= address[0..5], out= out6);
    RAM64(in= in, load= h, address= address[0..5], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[6..8], out= out);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

6. 实现 RAM4K

前往 Nand to Tetris Online IDE,选择 Project 3 里的 RAM4K,效果如下图所示 ⬇️

image.png

我们的目标是用 RAM512(即,5125121616 位寄存器的存储器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 RAM4K(即,409640961616 位寄存器的存储器)。

注释中的相关描述如下 ⬇️

/**
 * Memory of 4K 16-bit registers.
 * If load is asserted, the value of the register selected by
 * address is set to in; Otherwise, the value does not change.
 * The value of the selected register is emitted by out.
 */
  • 输入是
    • in[16]in[16]
    • loadload
    • address[12]address[12]
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 RAM512,可以用类似的思路来实现 RAM4K

代码的整体结构如下 ⬇️

CHIP RAM4K {
    IN in[16], load, address[12];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[9..11],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM512(in= in, load= a, address= address[0..8], out= out0);
    RAM512(in= in, load= b, address= address[0..8], out= out1);
    ...
    RAM512(in= in, load= h, address= address[0..8], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[9..11], out= out);
}

由于这里有很多行类似的代码,自己复制粘贴加修改感觉有点浪费时间,不如用 java 代码来生成它吧。 请将以下代码保存为 RAM4KHdlCodeGenerator.java

import java.util.StringJoiner;

public class RAM4KHdlCodeGenerator {
    public static void main(String[] args) {
        String template = """
                CHIP RAM4K {
                    IN in[16], load, address[12];
                    OUT out[16];
                
                    PARTS:
                    DMux8Way(in= load, sel= address[9..11],
                    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);
                
                %s
                
                    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
                    e= out4, f= out5, g= out6, h= out7,
                    sel= address[9..11], out= out);
                }
                """;
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        for (int i = 0; i < 8; i++) {
            String line = String.format("    RAM512(in= in, load= %s, address= address[0..8], out= out%s);", (char) ('a' + i), i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);
    }
}

使用如下的命令可以编译 RAM4KHdlCodeGenerator.java 并运行其中的 main 方法。

javac RAM4KHdlCodeGenerator.java
java RAM4KHdlCodeGenerator

运行结果如下

CHIP RAM4K {
    IN in[16], load, address[12];
    OUT out[16];

    PARTS:
    DMux8Way(in= load, sel= address[9..11],
    a= a, b= b, c= c, d= d, e= e, f= f, g= g, h= h);

    RAM512(in= in, load= a, address= address[0..8], out= out0);
    RAM512(in= in, load= b, address= address[0..8], out= out1);
    RAM512(in= in, load= c, address= address[0..8], out= out2);
    RAM512(in= in, load= d, address= address[0..8], out= out3);
    RAM512(in= in, load= e, address= address[0..8], out= out4);
    RAM512(in= in, load= f, address= address[0..8], out= out5);
    RAM512(in= in, load= g, address= address[0..8], out= out6);
    RAM512(in= in, load= h, address= address[0..8], out= out7);

    Mux8Way16(a= out0, b= out1, c= out2, d= out3,
    e= out4, f= out5, g= out6, h= out7,
    sel= address[9..11], out= out);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

7. 实现 RAM16K

前往 Nand to Tetris Online IDE,选择 Project 3 里的 RAM16K,效果如下图所示 ⬇️

image.png

我们的目标是用 RAM4K(即,409640961616 位寄存器的存储器) 以及 Project 1/Project 2 里已经实现的各种 chip 实现一个 RAM16K(即,16384163841616 位寄存器的存储器)。

注释中的相关描述如下 ⬇️

/**
 * Memory of 16K 16-bit registers.
 * If load is asserted, the value of the register selected by
 * address is set to in; Otherwise, the value does not change.
 * The value of the selected register is emitted by out.
 */
  • 输入是
    • in[16]in[16]
    • loadload
    • address[14]address[14]
  • 输出是
    • out[16]out[16]

我们在上一步里已经实现了 RAM4K,可以用类似的思路来实现 RAM16K。(由于 44RAM4K 可以组成一个 RAM16K,所以这次不必通过 java 代码来生成 HDL 代码)

代码如下 ⬇️

CHIP RAM16K {
    IN in[16], load, address[14];
    OUT out[16];

    PARTS:
    DMux4Way(in= load, sel= address[12..13],
    a= a, b= b, c= c, d= d);

    RAM4K(in= in, load= a, address= address[0..11], out= out0);
    RAM4K(in= in, load= b, address= address[0..11], out= out1);
    RAM4K(in= in, load= c, address= address[0..11], out= out2);
    RAM4K(in= in, load= d, address= address[0..11], out= out3);

    Mux4Way16(a= out0, b= out1, c= out2, d= out3,
    sel= address[12..13], out= out);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

8. 实现 PC (Program Counter, 程序计数器)

前往 Nand to Tetris Online IDE,选择 Project 3 里的 PC,效果如下图所示 ⬇️

image.png

我们的目标是用已经实现的各个 chip,包括 Project 1/Project 2 里已经实现的各种 chip,来实现一个 PC(Program Counter, 程序计数器)。

注释中的相关描述如下 ⬇️

/**
 * A 16-bit counter.
 * if      reset(t): out(t+1) = 0
 * else if load(t):  out(t+1) = in(t)
 * else if inc(t):   out(t+1) = out(t) + 1
 * else              out(t+1) = out(t)
 */
  • 输入是
    • in[16]in[16]
    • resetreset
    • loadload
    • incinc
  • 输出是
    • out[16]out[16]

大致可以这样构建 PC ⬇️

  1. 因为涉及 加一 操作,所以先用一个 Inc16(Project 2 里有 Inc16 这个 chip) 来实现加一的功能
  2. 在此基础上,用一个 (1616 位的) MuxRegister 的输入 outout 以及 Inc16(out) 之间做选择,选择结果称为 out1out_1
  3. 在此基础上,用一个 (1616 位的) Muxout1out_1inin 之间做选择,选择结果称为 out2out_2
  4. 在此基础上,用一个 (1616 位的) Muxout2out_200 之间做选择,选择结果称为 out3out_3,将 out3out_3 作为 Register 的输入

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

CHIP PC {
    IN in[16], reset, load, inc;
    OUT out[16];
    
    PARTS:
    Register(in= out3, load= true, out= out, out= previousOut);
    Inc16(in= previousOut, out= incOut);
    Mux16(a= previousOut, b= incOut, sel= inc, out= out1);
    Mux16(a= out1, b= in, sel= load, out= out2);
    Mux16(a= out2, b= false, sel= reset, out= out3);
}

这样的代码可以通过仿真测试,效果如下图所示 ⬇️

image.png

参考资料