From Nand to Tetris 里的 Project 2

168 阅读4分钟

背景

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

  1. 实现半加器(HalfAdder)
  2. 实现全加器(FullAdder)
  3. 实现 1616 位加法器(Add16)
  4. 实现 1616 位自增器(Inc16)
  5. 实现算术逻辑单元(ALU)

说明

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

正文

1. 实现 HalfAdder

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

image.png

我们的目标是用 Project 1 里已经实现的各种 chip 实现一个 HalfAdder

  • 输入是
    • aa
    • bb
  • 输出是
    • sumsum
    • carrycarry

我们可以从它的真值表入手 ⬇️

aabbsumsumcarrycarry
00000000
00111100
11001100
11110011

可见

  • sum=(¬ab)(a¬b)=absum=(\neg{a}\land b) \lor (a\land \neg{b})=a\oplus b
  • carry=abcarry=a\land b

所以可以这样实现 ⬇️

CHIP HalfAdder {
    IN a, b;    // 1-bit inputs
    OUT sum,    // Right bit of a + b 
        carry;  // Left bit of a + b

    PARTS:
    And(a= a, b= b, out= carry);
    Xor(a = a, b = b, out = sum);
}

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

image.png

我们可以把 HalfAdder 看成一个同时实现了 \oplus\land 运算的 chip

2. 实现 FullAdder

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

image.png

我们的目标是用 Project 1 里已经实现的各种 chip,以及刚才实现的 HalfAdder 来实现一个 FullAdder

  • 输入是
    • aa
    • bb
    • cc
  • 输出是
    • sumsum
    • carrycarry

我们还是从真值表入手 ⬇️

aabbccsumsumcarrycarry
0000000000
0000111100
0011001100
0011110011
1100001100
1100110011
1111000011
1111111111

通过观察真值表,可以将 sumsumcarrycarry 写成对应的析取范式 DNF(Disjunctive Normal Form) 的形式

  • sum=(¬a¬bc)(¬ab¬c)(a¬b¬c)(abc)sum=(\neg{a}\land \neg{b}\land c)\lor (\neg{a}\land b\land \neg{c})\lor(a\land\neg{b}\land \neg{c})\lor(a\land b\land c)
  • carry=(¬abc)(a¬bc)(ab¬c)(abc)carry=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor(a\land b\land \neg{c})\lor(a\land b\land c)

从上面的析取范式出发,我们可以只用 And/Or/Not 来实现 FullAdder。但有没有更简洁的实现方式呢?

基于 DNF 来化简

一种思路是从 DNF 出发,将其进行化简。

化简 sumsum
sum=(¬a¬bc)(¬ab¬c)(a¬b¬c)(abc)sum=(\neg{a}\land \neg{b}\land c)\lor (\neg{a}\land b\land \neg{c})\lor(a\land\neg{b}\land \neg{c})\lor(a\land b\land c)

把含有 cc 的两个最小项(minterm)合并,把含有 ¬c\neg{c} 的两个最小项合并,于是得到

sum=((ab)c)((ab)¬c)sum=((a\odot b)\land c) \lor ((a\oplus b) \land \neg{c})

根据

  • \oplus 运算的性质: xy=(x¬y)(¬xy)x\oplus y=(x\land \neg{y}) \lor (\neg{x} \land y)
  • \odot 运算的性质: xy=¬(xy)x\odot y=\neg{(x\oplus y)}

可以将 sumsum 化简如下

sum=(ab)csum=(a\oplus b)\oplus c
=abc=a\oplus b\oplus c
化简 carrycarry
carry=(¬abc)(a¬bc)(ab¬c)(abc)carry=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor(a\land b\land \neg{c})\lor(a\land b\land c)

将含有 aba\land b 的两个最小项(minterm)合并,将另外两个最小项合并,可以得到

carry=(ab)((ab)c)carry=(a\land b)\lor((a\oplus b)\land c)

完整的代码如下 ⬇️

CHIP FullAdder {
    IN a, b, c;  // 1-bit inputs
    OUT sum,     // Right bit of a + b + c
        carry;   // Left bit of a + b + c

    PARTS:
    // Calculationtion for sum
    Xor(a = a, b = b, out = abXor);
    Xor(a = abXor, b = c, out = sum);

    // Calculation for carry
    And(a= a, b= b, out= p1);
    And(a= abXor, b= c, out= p2);
    Or(a= p1, b= p2, out= carry);
}

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

image.png

上面对 sumsumcarrycarry 的化简都需要一些技巧,而且(我觉得)挺容易写错的,那么有没有其他的化简方式呢?我们也可以通过利用 HalfAdder 来进行化简。

利用 HalfAdder 来进行化简

我们回忆一下 HalfAdder 中的 sumsumcarrycarry 是怎样的 ⬇️ (为了和 FullAddersumsumcarrycarry 进行区分,我把 HalfAdder sumsumcarrycarry 分别改写为的 sumhasum_{ha}carryhacarry_{ha}

  • sumha=(¬ab)(a¬b)=absum_{ha}=(\neg{a}\land b) \lor (a\land \neg{b})=a\oplus b
  • carryha=abcarry_{ha}=a\land b

为了便于区分,我把 FullAdder 的 sumsumcarrycarry 分别改写为 sumfasum_{fa}carryfacarry_{fa}。 先看 sumfasum_{fa}sumhasum_{ha} 是否有关联。

  • sumfa=(¬a¬bc)(¬ab¬c)(a¬b¬c)(abc)sum_{fa}=(\neg{a}\land \neg{b}\land c)\lor (\neg{a}\land b\land \neg{c})\lor(a\land\neg{b}\land \neg{c})\lor(a\land b\land c)
  • sumha=absum_{ha}=a\oplus b
如何表示 sumsum

一个思路是看看能否将 sumfasum_{fa}sumhasum_{ha} 表示出来,我试了试,觉得有些繁琐,而且不容易想到。另一个思路是,我们从 sumfasum_{fa}sumhasum_{ha} 背后的含义入手。 sumhasum_{ha} 表示 a,ba,b 的和(忽略进位),所以 sumha=absum_{ha}=a\oplus b

  • sumha=0sum_{ha}=0 时,只有 c=1c=1sumfa=1sum_{fa}=1 才会成立
  • sumha=1sum_{ha}=1 时,只有 c=0c=0sumfa=1sum_{fa}=1 才会成立

所以 sumfa=sumhacsum_{fa}=sum_{ha}\oplus c

HalfAdder 的作用就是对输入 a,ba,b 分别执行 \oplus\land 运算,所以 sumfasum_{fa} 可以通过拼接两个 HalfAdder 来实现 ⬇️

  • 第一个 HalfAddera,ba,b 为输入,它的
    • sumfa1=absum_{fa1}=a\oplus b
    • carryha1=abcarry_{ha1}=a\land b
  • 第二个 HalfAddersumfa1sum_{fa1}cc 为输入,它的
    • sumha2=sumfa1csum_{ha2}=sum_{fa1}\oplus c
    • carryha2=sumfa1ccarry_{ha2}=sum_{fa1}\land c
如何表示 carrycarry
carryfa=(¬abc)(a¬bc)(ab¬c)(abc)carry_{fa}=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor(a\land b\land \neg{c})\lor(a\land b\land c)
=(¬abc)(a¬bc)((ab¬c)(abc))=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor((a\land b\land \neg{c})\lor(a\land b\land c))
=(¬abc)(a¬bc)(ab)=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor(a\land b)
=(¬abc)(a¬bc)carryha=(\neg{a}\land b\land c)\lor(a\land \neg{b}\land c)\lor carry_{ha}
=((ab)c)carryha=((a\oplus b)\land c)\lor carry_{ha}
=(sumhac)carryha=(sum_{ha}\land c)\lor carry_{ha}

考虑到 HalfAdder 的作用就是对输入 a,ba,b 分别执行 \oplus\land 运算,所以 carryfacarry_{fa} 可以通过拼接两个 HalfAdder 再加上一个 Or 运算 来实现 ⬇️

  • 第一个 HalfAddera,ba,b 为输入,它的
    • sumfa1=absum_{fa1}=a\oplus b
    • carryha1=abcarry_{ha1}=a\land b
  • 第二个 HalfAddersumfa1sum_{fa1}cc 为输入,它的
    • sumha2=sumfa1csum_{ha2}=sum_{fa1}\oplus c
    • carryha2=sumfa1ccarry_{ha2}=sum_{fa1}\land c

那么

carryfa=(sumha1c)carryha1=carryha2carryha1carry_{fa}=(sum_{ha1}\land c)\lor carry_{ha1}=carry_{ha2}\lor carry_{ha1}

至此我们可以用 HalfAdderOr 来实现 FullAdder。具体的代码如下 ⬇️

CHIP FullAdder {
    IN a, b, c;  // 1-bit inputs
    OUT sum,     // Right bit of a + b + c
        carry;   // Left bit of a + b + c

    PARTS:
    HalfAdder(a= a, b= b, sum= sumHa, carry= carryHa);
    HalfAdder(a= sumHa, b= c, sum= sum, carry= temp);
    Or(a= carryHa, b= temp, out= carry);
}

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

image.png

3. 实现 1616 位加法器(Add16)

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

image.png

我们的目标是用 Project 1 里已经实现的各种 chip,以及刚才实现的 HalfAdder/FullAdder 来实现一个 Add16

  • 输入是
    • a[16]a[16]
    • b[16]b[16]
  • 输出是
    • out[16]out[16]

要实现的逻辑如下 ⬇️

16-bit adder: Adds two 16-bit two's complement values.
The most significant carry bit is ignored.

我写了如下的 java 程序来生成对应的 HDL 代码

import java.util.StringJoiner;

public class Adder16HdlCodeGenerator {
    public static void main(String[] args) {
        String template = """
                CHIP Add16 {
                    IN a[16], b[16];
                    OUT out[16];
                
                    PARTS:
                %s
                }
                """;
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        joiner.add("    FullAdder(a= a[0], b= b[0], c= false, sum= out[0], carry= c0);");
        for (int i = 1; i < 16; i++) {
            String line = String.format("    FullAdder(a= a[%s], b= b[%s], c= c%s, sum= out[%s], carry= c%s);", i, i, i - 1, i, i);
            joiner.add(line);
        }
        System.out.printf(template, joiner);
    }
}

请将以上 java 代码保存为 Adder16HdlCodeGenerator.java。用以下命令可以编译 Adder16HdlCodeGenerator.java 并运行其中的 main 方法。

javac Adder16HdlCodeGenerator.java
java Adder16HdlCodeGenerator

运行结果如下

CHIP Add16 {
    IN a[16], b[16];
    OUT out[16];

    PARTS:
    FullAdder(a= a[0], b= b[0], c= false, sum= out[0], carry= c0);
    FullAdder(a= a[1], b= b[1], c= c0, sum= out[1], carry= c1);
    FullAdder(a= a[2], b= b[2], c= c1, sum= out[2], carry= c2);
    FullAdder(a= a[3], b= b[3], c= c2, sum= out[3], carry= c3);
    FullAdder(a= a[4], b= b[4], c= c3, sum= out[4], carry= c4);
    FullAdder(a= a[5], b= b[5], c= c4, sum= out[5], carry= c5);
    FullAdder(a= a[6], b= b[6], c= c5, sum= out[6], carry= c6);
    FullAdder(a= a[7], b= b[7], c= c6, sum= out[7], carry= c7);
    FullAdder(a= a[8], b= b[8], c= c7, sum= out[8], carry= c8);
    FullAdder(a= a[9], b= b[9], c= c8, sum= out[9], carry= c9);
    FullAdder(a= a[10], b= b[10], c= c9, sum= out[10], carry= c10);
    FullAdder(a= a[11], b= b[11], c= c10, sum= out[11], carry= c11);
    FullAdder(a= a[12], b= b[12], c= c11, sum= out[12], carry= c12);
    FullAdder(a= a[13], b= b[13], c= c12, sum= out[13], carry= c13);
    FullAdder(a= a[14], b= b[14], c= c13, sum= out[14], carry= c14);
    FullAdder(a= a[15], b= b[15], c= c14, sum= out[15], carry= c15);
}

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

image.png

4. 实现 1616 位自增器(Inc16)

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

image.png

我们的目标是用 Project 1 里已经实现的各种 chip,以及刚才实现的 HalfAdder/FullAdder/Add16 来实现一个 Inc16

  • 输入是
    • in[16]in[16]
  • 输出是
    • out[16]out[16]

要实现的逻辑如下 ⬇️

16-bit incrementer:
out = in + 1

一个直观的做法是使用上一步实现的 Add16,具体的代码如下 ⬇️

CHIP Inc16 {
    IN in[16];
    OUT out[16];

    PARTS:
    Add16(a = in, b[0]= true, b[1..15] = false, out = out);
}

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

image.png

5. 实现算术逻辑单元(ALU)

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

image.png

我们的目标是用 Project 1 里已经实现的各种 chip,以及刚才实现的 HalfAdder/FullAdder/Add16/Inc16 来实现一个 ALU

这次的输入项比较多,我用一个表格来进行说明

输入说明
x[16]x[16]16 位的输入
y[16]y[16]16 位的输入
zxzx用于表示是否要将 xx 置零
(我想 zx 大约是 zero x 的缩写吧)
nxnx用于表示是否要将 xx 取反
(我想 nx 大约是 negate x 的缩写吧)
zyzy用于表示是否要将 yy 置零
(我想 zy 大约是 zero y 的缩写吧)
nyny用于表示是否要将 yy 取反
(我想 ny 大约是 negate y 的缩写吧)
ff用于表示 out=x+yout=x+y 还是 out=xyout=x\land y
(f=1f=1 时为前者,f=0f=0 时为后者)
nono用于表示是否要将 outout 取反
(no=1no=1 时需要执行取反操作)

我把输出项的相关信息也整理成一个表格了 ⬇️

输出说明
out[16]out[16]16 位的输出
zrzr用于表示 outout 是否等于 00
(如果 out=0out=0,则 zr=1zr=1,否则 zr=0zr=0)
ngng用于表示 outout 是否小于 00
(如果 out<0out\lt 0,则 ng=1ng=1,否则 ng=0ng=0)

我们要实现的功能如下 ⬇️

/**
 * ALU (Arithmetic Logic Unit):
 * Computes out = one of the following functions:
 *                0, 1, -1,
 *                x, y, !x, !y, -x, -y,
 *                x + 1, y + 1, x - 1, y - 1,
 *                x + y, x - y, y - x,
 *                x & y, x | y
 * on the 16-bit inputs x, y,
 * according to the input bits zx, nx, zy, ny, f, no.
 * In addition, computes the two output bits:
 * if (out == 0) zr = 1, else zr = 0
 * if (out < 0)  ng = 1, else ng = 0
 */
// Implementation: Manipulates the x and y inputs
// and operates on the resulting values, as follows:
// if (zx == 1) sets x = 0        // 16-bit constant
// if (nx == 1) sets x = !x       // bitwise not
// if (zy == 1) sets y = 0        // 16-bit constant
// if (ny == 1) sets y = !y       // bitwise not
// if (f == 1)  sets out = x + y  // integer 2's complement addition
// if (f == 0)  sets out = x & y  // bitwise and
// if (no == 1) sets out = !out   // bitwise not

这次的功能比较复杂。由于以下控制位是依次起作用的,所以我们一个一个来看应该如何实现它们

  • zxzx
  • nxnx
  • zyzy
  • nyny
  • ff
  • nono

11 步:实现 zxzx 的功能

  • zx=1zx=1 时,需要对 xx 执行清零操作
  • zx=0zx=0 时,不修改 xx

我们可以用一个 Mux16 (在 Project 1 里已经实现了 Mux16)来实现这样的功能,具体的代码如下 ⬇️ (因为下一步可能会对 xx 进行取反操作,所以把这一步的输出称为 x0

Mux16(a= x, b= false, sel= zx, out= x0);

22 步:实现 nxnx 的功能

  • zx=1zx=1 时,需要对 xx 执行取反操作(上一步将最新的 xx 值记录在 x0 里了)
  • zx=0zx=0 时,不修改 xx(上一步将最新的 xx 值记录在 x0 里了)

我们可以先用 Not16x0 进行取反操作得到 notX0,然后再用一个 Mux16x0/notX0 中选择一个作为输出(称这一步的输出为 x1)。对应的代码如下 ⬇️

Not16(in= x0, out= notX0);
Mux16(a= x0, b= notX0, sel= nx, out= x1);

33 步:实现 zyzy 的功能

  • zy=1zy=1 时,需要对 yy 执行清零操作
  • zy=0zy=0 时,不修改 yy

33 步和第 11 步类似,我们直接写代码吧 ⬇️

Mux16(a= y, b= false, sel= zy, out= y0);

44 步:实现 nyny 的功能

  • ny=1ny=1 时,需要对 yy 执行取反操作(上一步将最新的 yy 值记录在 y0 里了)
  • zy=0zy=0 时,不修改 yy(上一步将最新的 yy 值记录在 y0 里了)

44 步和第 22 步类似,我们直接写代码吧 ⬇️

Not16(in= y0, out= notY0);
Mux16(a= y0, b= notY0, sel= ny, out= y1);

55 步:实现 ff 的功能

  • f=1f=1 时,out=x+yout=x+y (之前几步将最新的 x,yx,y 值分别记录在 x1/y1 里了)
  • f=0f=0 时,out=xyout=x\land y(之前几步将最新的 x,yx,y 值分别记录在 x1/y1 里了)

我们可以先用

  • Add16 实现 x+yx+y 的操作(称其结果为 addResult
  • And16 实现 xyx\land y 的操作(称其结果为 andResult

然后用一个 Mux16 决定选择 addResult/andResult 中的哪一个作为输出(称这一步的输出为 out0)。具体的代码如下

And16(a= x1, b= y1, out= andResult);
Add16(a = x1, b = y1, out = addResult);
Mux16(a= andResult, b= addResult, sel= f, out= out0);    

66 步:实现 nono 的功能

  • no=1no=1 时,对 outout 进行取反操作(上一步将最新的 outout 值记录在 out0 里了)
  • no=0no=0 时,不修改 outout(上一步将最新的 outout 值记录在 out0 里了)

对应的代码如下 ⬇️

Not16(in= out0, out= notOut0);
Mux16(a= out0, b= notOut0, sel= no, out= out);

至此,实现 ALU 的前 66 个步骤我们都已经完成了。 完整的代码如下(我加了点注释)

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute (out = x + y) or (out = x & y)?
        no; // negate the out output?
    OUT 
        out[16], // 16-bit output
        zr,      // if (out == 0) equals 1, else 0
        ng;      // if (out < 0)  equals 1, else 0

    PARTS:
    // x0 is the x we get after applying zx
    Mux16(a= x, b= false, sel= zx, out= x0);
    
    // x1 is the x we get after applying nx
    Not16(in= x0, out= notX0);
    Mux16(a= x0, b= notX0, sel= nx, out= x1);
    
    // y0 is the y we get after applying zy
    Mux16(a= y, b= false, sel= zy, out= y0);
    
    // y1 is the y we get after applying ny
    Not16(in= y0, out= notY0);
    Mux16(a= y0, b= notY0, sel= ny, out= y1);
    
    // out0 is the out we get after applying f 
    And16(a= x1, b= y1, out= andResult);
    Add16(a = x1, b = y1, out = addResult);
    Mux16(a= andResult, b= addResult, sel= f, out= out0);
    
    // out is the out we get after applying "no" 
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, out= out);
}

上述代码的仿真结果如下 ⬇️

image.png

观察仿真结果后,我们会发现,outout 这个输出没有问题,但是 zrzrngng 的结果,很多是错误的。这是因为我们还没有实现 zrzrngng 的逻辑

77 步:实现 zrzr 的功能

  • out=0out=0 时,zr=1zr=1
  • out0out\ne 0 时,zr=0zr=0

因为 outout 一共有 1616 位,所以当 outout 的值为 00 时,out[0],out[1],...,out[15]out[0],out[1],...,out[15] 全都是 00。这 1616 位可以分位高 88 位和低 88 位 ⬇️

image.png

如果 outout 同时满足以下两个条件,那么 outout 的所有位(共 1616 位)都是 00

  • 其高 88 位全都是 00,并且
  • 其低 88 位全都是 00

我们可以把第 66 步的代码调整成这样 ⬇️ ⬇️

    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, out= out, 
    out[0..7]= low8, out[8..15]= high8);
    
    Or8Way(in= high8, out= high8Or);
    Or8Way(in= low8, out= low8Or);
    Or(a= high8Or, b= low8Or, out= orResult);
    Not(in= orResult, out= zr);

88 步:实现 ngng 的功能

  • out<0out\lt 0 时,ng=1ng=1
  • out0out\ge 0 时,ng=0ng=0

215-2^{15} 这个负数为例,当 outout 的值等于 215-2^{15} 时,它的每一位是这个样子 ⬇️

image.png

一般地,当 out<0out\lt 0 时,out[15]=1out[15]=1;当 out[15]=1out[15]=1 时,out<0out\lt 0

所以我们只需要判断 out[15]out[15] 的值是否为 11,就知道 ngng 的值应该是什么了。 我们可以把第 66 步的代码调整成这样 ⬇️

    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, out= out, out[15]= ng);

我们把对 zrzrngng 的计算的代码调整一下 ⬇️

  • outout 的高 88 位和低 88 位分别进行 Or 运算(得到的结果是 high8,low8high8,low8),在此基础上计算 zrzr 的值(在下图的 6868 行至 7373 行)
  • outout 的最高位赋值给 ngng(在下图的 6868 行)

image.png

现在可以把 88 个步骤的代码合并在一起了,完整的代码如下 ⬇️

// 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/2/ALU.hdl
/**
 * ALU (Arithmetic Logic Unit):
 * Computes out = one of the following functions:
 *                0, 1, -1,
 *                x, y, !x, !y, -x, -y,
 *                x + 1, y + 1, x - 1, y - 1,
 *                x + y, x - y, y - x,
 *                x & y, x | y
 * on the 16-bit inputs x, y,
 * according to the input bits zx, nx, zy, ny, f, no.
 * In addition, computes the two output bits:
 * if (out == 0) zr = 1, else zr = 0
 * if (out < 0)  ng = 1, else ng = 0
 */
// Implementation: Manipulates the x and y inputs
// and operates on the resulting values, as follows:
// if (zx == 1) sets x = 0        // 16-bit constant
// if (nx == 1) sets x = !x       // bitwise not
// if (zy == 1) sets y = 0        // 16-bit constant
// if (ny == 1) sets y = !y       // bitwise not
// if (f == 1)  sets out = x + y  // integer 2's complement addition
// if (f == 0)  sets out = x & y  // bitwise and
// if (no == 1) sets out = !out   // bitwise not

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute (out = x + y) or (out = x & y)?
        no; // negate the out output?
    OUT 
        out[16], // 16-bit output
        zr,      // if (out == 0) equals 1, else 0
        ng;      // if (out < 0)  equals 1, else 0

    PARTS:
    // x0 is the "x" we get after applying zx
    Mux16(a= x, b= false, sel= zx, out= x0);
    
    // x1 is the "x" we get after applying nx
    Not16(in= x0, out= notX0);
    Mux16(a= x0, b= notX0, sel= nx, out= x1);
    
    // y0 is the "y" we get after applying zy
    Mux16(a= y, b= false, sel= zy, out= y0);
    
    // y1 is the "y" we get after applying ny
    Not16(in= y0, out= notY0);
    Mux16(a= y0, b= notY0, sel= ny, out= y1);
    
    // out0 is the "out" we get after applying f 
    And16(a= x1, b= y1, out= andResult);
    Add16(a = x1, b = y1, out = addResult);
    Mux16(a= andResult, b= addResult, sel= f, out= out0);
    
    // out is the "out" we get after applying "no"
    // out1 is equal to out
    // assign value to zr and ng based on out
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, out= out, 
    out[0..7]= low8, out[8..15]= high8, out[15]= ng);
    
    Or8Way(in= high8, out= high8Or);
    Or8Way(in= low8, out= low8Or);
    Or(a= high8Or, b= low8Or, out= orResult);
    Not(in= orResult, out= zr);
}

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

image.png

实现 ALU 的其他思路

我觉得还有其他思路可以实现 ALU 中的第 77 步(即 zrzr),利用 ngng,我们可以判断出 outout 的最高位是否是 11。如果 outout 的最高位是 00 的话,那么会有两种可能 ⬇️

  • outout 表示 00
  • outout 表示一个正整数

我们要做的事情就是区分上面的两种可能。

思路 11: 借助 Inc16 来判断

x=00000000000000002,y=11111111111111112x=0000000000000000_2, y=1111111111111111_2。注意到

  • xx 取反会得到 yy,而 y+1=xy+1=x (在补码加法的意义下)
  • 对满足 00000000000000002<n011111111111111120000000000000000_2 \lt n \le 0111111111111111_2 的任意正整数 nn 而言,(n)+1(\sim n)+1 得到的结果的最高位是 11

基于上述观察,我们可以这样判定 zrzr。 如果以下两个条件同时满足,那么 zrzr11,否则 zrzr00

  • out[15]out[15]00
  • Inc16(Not16(out))\text{Inc16}(\text{Not16}(out)) 的结果的最高位是 00

所以我们可以这样来设计 zrzr ⬇️ (因为第 66 步的代码也做了小的调整,所以下面的代码包含了第 66 步)

    // out is the "out" we get after applying "no"
    // out1 is equal to out
    // assign value to ng
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, 
    out= out, out= out1, out[15]= ng, out[15]= zr1);

    // assign value to zr
    Not16(in= out1, out= notOut);
    Inc16(in= notOut, out[15]= zr2);
    Or(a= zr1, b= zr2, out= zr3);
    Not(in= zr3, out= zr);

完整的代码如下

// 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/2/ALU.hdl
/**
 * ALU (Arithmetic Logic Unit):
 * Computes out = one of the following functions:
 *                0, 1, -1,
 *                x, y, !x, !y, -x, -y,
 *                x + 1, y + 1, x - 1, y - 1,
 *                x + y, x - y, y - x,
 *                x & y, x | y
 * on the 16-bit inputs x, y,
 * according to the input bits zx, nx, zy, ny, f, no.
 * In addition, computes the two output bits:
 * if (out == 0) zr = 1, else zr = 0
 * if (out < 0)  ng = 1, else ng = 0
 */
// Implementation: Manipulates the x and y inputs
// and operates on the resulting values, as follows:
// if (zx == 1) sets x = 0        // 16-bit constant
// if (nx == 1) sets x = !x       // bitwise not
// if (zy == 1) sets y = 0        // 16-bit constant
// if (ny == 1) sets y = !y       // bitwise not
// if (f == 1)  sets out = x + y  // integer 2's complement addition
// if (f == 0)  sets out = x & y  // bitwise and
// if (no == 1) sets out = !out   // bitwise not

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute (out = x + y) or (out = x & y)?
        no; // negate the out output?
    OUT 
        out[16], // 16-bit output
        zr,      // if (out == 0) equals 1, else 0
        ng;      // if (out < 0)  equals 1, else 0

    PARTS:
    // x0 is the "x" we get after applying zx
    Mux16(a= x, b= false, sel= zx, out= x0);
    
    // x1 is the "x" we get after applying nx
    Not16(in= x0, out= notX0);
    Mux16(a= x0, b= notX0, sel= nx, out= x1);
    
    // y0 is the "y" we get after applying zy
    Mux16(a= y, b= false, sel= zy, out= y0);
    
    // y1 is the "y" we get after applying ny
    Not16(in= y0, out= notY0);
    Mux16(a= y0, b= notY0, sel= ny, out= y1);
    
    // out0 is the "out" we get after applying f 
    And16(a= x1, b= y1, out= andResult);
    Add16(a = x1, b = y1, out = addResult);
    Mux16(a= andResult, b= addResult, sel= f, out= out0);
    
    // out is the "out" we get after applying "no"
    // out1 is equal to out
    // assign value to ng
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, 
    out= out, out= out1, out[15]= ng, out[15]= zr1);

    // assign value to zr
    Not16(in= out1, out= notOut);
    Inc16(in= notOut, out[15]= zr2);
    Or(a= zr1, b= zr2, out= zr3);
    Not(in= zr3, out= zr);
}

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

image.png

思路 22: 借助 Add16 来判断

x=00000000000000002,y=01111111111111112x=0000000000000000_2, y=0111111111111111_2。注意到

  • x+y=yx+y=y, 而 yy 的最高位是 00
  • 对满足 00000000000000002<n011111111111111120000000000000000_2 \lt n \le 0111111111111111_2 任意正整数 nn 而言,n+yn+y 的结果的最高位是 11

基于上述观察,我们可以这样判定 zrzr。 如果以下两个条件同时满足,那么 zrzr11,否则 zrzr00

  • out[15]out[15]00
  • out+01111111111111112out+0111111111111111_2 的结果的最高位是 00

所以我们可以这样来设计 zrzr ⬇️ (因为第 66 步的代码也做了小的调整,所以下面的代码包含了第 66 步)

    // out is the "out" we get after applying "no"
    // out1 is equal to out
    // assign value to ng
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, 
    out= out, out= out1, out[15]= ng, out[15]= zr1);

    // assign value to zr
    Add16(a = out1, b[0..14] = true, b[15] = false,
    out[15] = zr2);
    Or(a= zr1, b= zr2, out= zr3);
    Not(in= zr3, out= zr);

完整的代码如下

// 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/2/ALU.hdl
/**
 * ALU (Arithmetic Logic Unit):
 * Computes out = one of the following functions:
 *                0, 1, -1,
 *                x, y, !x, !y, -x, -y,
 *                x + 1, y + 1, x - 1, y - 1,
 *                x + y, x - y, y - x,
 *                x & y, x | y
 * on the 16-bit inputs x, y,
 * according to the input bits zx, nx, zy, ny, f, no.
 * In addition, computes the two output bits:
 * if (out == 0) zr = 1, else zr = 0
 * if (out < 0)  ng = 1, else ng = 0
 */
// Implementation: Manipulates the x and y inputs
// and operates on the resulting values, as follows:
// if (zx == 1) sets x = 0        // 16-bit constant
// if (nx == 1) sets x = !x       // bitwise not
// if (zy == 1) sets y = 0        // 16-bit constant
// if (ny == 1) sets y = !y       // bitwise not
// if (f == 1)  sets out = x + y  // integer 2's complement addition
// if (f == 0)  sets out = x & y  // bitwise and
// if (no == 1) sets out = !out   // bitwise not

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute (out = x + y) or (out = x & y)?
        no; // negate the out output?
    OUT 
        out[16], // 16-bit output
        zr,      // if (out == 0) equals 1, else 0
        ng;      // if (out < 0)  equals 1, else 0

    PARTS:
    // x0 is the "x" we get after applying zx
    Mux16(a= x, b= false, sel= zx, out= x0);
    
    // x1 is the "x" we get after applying nx
    Not16(in= x0, out= notX0);
    Mux16(a= x0, b= notX0, sel= nx, out= x1);
    
    // y0 is the "y" we get after applying zy
    Mux16(a= y, b= false, sel= zy, out= y0);
    
    // y1 is the "y" we get after applying ny
    Not16(in= y0, out= notY0);
    Mux16(a= y0, b= notY0, sel= ny, out= y1);
    
    // out0 is the "out" we get after applying f 
    And16(a= x1, b= y1, out= andResult);
    Add16(a = x1, b = y1, out = addResult);
    Mux16(a= andResult, b= addResult, sel= f, out= out0);
    
    // out is the "out" we get after applying "no"
    // out1 is equal to out
    // assign value to ng
    Not16(in= out0, out= notOut0);
    Mux16(a= out0, b= notOut0, sel= no, 
    out= out, out= out1, out[15]= ng, out[15]= zr1);

    // assign value to zr
    Add16(a = out1, b[0..14] = true, b[15] = false,
    out[15] = zr2);
    Or(a= zr1, b= zr2, out= zr3);
    Not(in= zr3, out= zr);
}

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

image.png

其他

outout 的值为 00 或者 215-2^{15} 时,每一位的值是如何画出来的?

我是通过 mermaid.live 页面来绘制那些图的。其中 out=0out=0 时,对应的代码如下 ⬇️ (out=215out=-2^{15} 时的代码也大同小异,就不赘述了)

block
    block
        columns 16
            name15["[15]"] name14["[14]"] name13["[13]"] name12["[12]"] 
            name11["[11]"] name10["[10]"] name9["[9]"] name8["[8]"] 
            name7["[7]"] name6["[6]"] name5["[5]"] name4["[4]"] 
            name3["[3]"] name2["[2]"] name1["[1]"] name0["[0]"] 

            value15["0"] value14["0"] value13["0"] value12["0"] 
            value11["0"] value10["0"] value9["0"] value8["0"]
            value7["0"] value6["0"] value5["0"] value4["0"]
            value3["0"] value2["0"] value1["0"] value0["0"] 
    end

style value0 fill:#ffff00;
style value1 fill:#ffff00;
style value2 fill:#ffff00;
style value3 fill:#ffff00;
style value4 fill:#ffff00;
style value5 fill:#ffff00;
style value6 fill:#ffff00;
style value7 fill:#ffff00;
style value8 fill:#ffff00;
style value9 fill:#ffff00;
style value10 fill:#ffff00;
style value11 fill:#ffff00;
style value12 fill:#ffff00;
style value13 fill:#ffff00;
style value14 fill:#ffff00;
style value15 fill:#ffff00;

style name0 fill:#d3d3d3,stroke:#fff;
style name1 fill:#d3d3d3,stroke:#fff;
style name2 fill:#d3d3d3,stroke:#fff;
style name3 fill:#d3d3d3,stroke:#fff;
style name4 fill:#d3d3d3,stroke:#fff;
style name5 fill:#d3d3d3,stroke:#fff;
style name6 fill:#d3d3d3,stroke:#fff;
style name7 fill:#d3d3d3,stroke:#fff;
style name8 fill:#d3d3d3,stroke:#fff;
style name9 fill:#d3d3d3,stroke:#fff;
style name10 fill:#d3d3d3,stroke:#fff;
style name11 fill:#d3d3d3,stroke:#fff;
style name12 fill:#d3d3d3,stroke:#fff;
style name13 fill:#d3d3d3,stroke:#fff;
style name14 fill:#d3d3d3,stroke:#fff;
style name15 fill:#d3d3d3,stroke:#fff;

image.png

具体的语法请参考 Block Diagrams Documentation

参考资料