From Nand to Tetris 里的 Project 5 (Memory 部分)

135 阅读4分钟

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

  1. Memory
  2. CPU
  3. Computer

本文只涉及 Memory 的实现

说明

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

image.png

正文

前往 Nand to Tetris Online IDE,选择 Project 5 里的 Memory ⬇️

image.png

我们的目标是实现 Memory (数据存储器),注释中的相关描述如下 ⬇️

/**
 * The complete address space of the Hack computer's memory,
 * including RAM and memory-mapped I/O. 
 * The chip facilitates read and write operations, as follows:
 *     Read:  out(t) = Memory[address(t)](t)
 *     Write: if load(t-1) then Memory[address(t-1)](t) = in(t-1)
 * In words: the chip always outputs the value stored at the memory 
 * location specified by address. If load=1, the in value is loaded 
 * into the memory location specified by address. This value becomes 
 * available through the out output from the next time step onward.
 * Address space rules:
 * Only the upper 16K+8K+1 words of the Memory chip are used. 
 * Access to address>0x6000 is invalid and reads 0. Access to any address
 * in the range 0x4000-0x5FFF results in accessing the screen memory 
 * map. Access to address 0x6000 results in accessing the keyboard 
 * memory map. The behavior in these addresses is described in the Screen
 * and Keyboard chip specifications given in the lectures and the book.
 */
  • 输入是

    • in[16]in[16]
    • loadload
    • address[15]address[15]
  • 输出是

    • out[16]out[16]
addressaddress 的范围应该访问哪里说明
0address<163840\le address \lt 16384RAM\text{RAM}16384=214=0x400016384=2^{14}=\text{0x4000}
16384address<2457616384\le address \lt 24576Screen\text{Screen}16384=214=0x400016384=2^{14}=\text{0x4000}
24576=214+213=0x600024576=2^{14}+2^{13}=\text{0x6000}
address=24576address = 24576Keyboard\text{Keyboard}24576=214+213=0x600024576=2^{14}+2^{13}=\text{0x6000}
address>24576address\gt 24576(此时的 addressaddress 无效)24576=214+213=0x600024576=2^{14}+2^{13}=\text{0x6000}

1 如果 addressRAM 的范围内

1.1 判断 address 是否在 RAM 的范围内

当且仅当 0address<163840\le address \lt 16384 时,addressaddressRAM\text{RAM} 的范围内。由于 addressaddress1515 位的,这就等价于 address[14]=0address[14]=0address[0..13]address[0..13] 这些位可以是任意值。当 addressaddressRAM\text{RAM} 的范围内时,addressaddress 的模式可以表示如下 ⬇️ (下图中用 ? 表示这一位可以是任意值)

mermaid-diagram-2026-01-21-224131.png

可以用一个 非门 来判断 addressaddress 是否在 RAM\text{RAM} 的范围内。

Not(in=address[14],out=inRamRange)\text{Not}(in= \text{address[14]}, out= \text{inRamRange})

这个 非门 的输出是 inRamRange\text{inRamRange}

  • inRamRange\text{inRamRange}true\text{true} 时: addressaddress RAM\text{RAM} 的范围内
  • inRamRange\text{inRamRange}false\text{false} 时: addressaddress 不在 RAM\text{RAM} 的范围内

1.2 判断是否要对 RAM 进行 load 操作

我们可以用一个 Mux\text{Mux} 来结合 inRamRange\text{inRamRange}load\text{load},将这个 Mux\text{Mux} 的输出记为 loadRAM\text{loadRAM},用 loadRAM\text{loadRAM} 判断是否对 RAM 进行 load 操作 ⬇️

Mux(a=false,b=load,sel=inRamRange,out=loadRAM)\text{Mux}(a= \text{false}, b= \text{load}, sel= \text{inRamRange}, out= \text{loadRAM})

现在可以将 in,loadRAM,address\text{in},\text{loadRAM},\text{address} 连接到 RAM\text{RAM} 上了(我们只需要 addressaddress 的低 1414 位,即 address[0..13]\text{address[0..13]}),将 RAM\text{RAM} 的输出记为 ramOut\text{ramOut} ⬇️

RAM16K(in=in,load=loadRAM,address=address[0..13],out=ramOut)\text{RAM16K}(in= \text{in}, load= \text{loadRAM}, address= \text{address[0..13]}, out= \text{ramOut})

1.3 判断是否要使用 ramOut\text{ramOut}

我们需要根据 inRamRange\text{inRamRange} 的值来判断是否使用 ramOut\text{ramOut} ⬇️

  • inRamRange\text{inRamRange}true\text{true} 时: 使用 ramOut\text{ramOut}
  • inRamRange\text{inRamRange}false\text{false} 时: 忽略 ramOut\text{ramOut}

因此可以用 Mux16\text{Mux16} 来选择是否使用 ramOut\text{ramOut},这个 Mux16\text{Mux16} 的输出记为 outCandidate1\text{outCandidate1} ⬇️

Mux16(a=false,b=ramOut,sel=inRamRange,out=outCandidate1)\text{Mux16}(a= \text{false}, b= \text{ramOut}, sel= \text{inRamRange}, out= \text{outCandidate1})

1.4 将这些代码合在一起

RAM 相关的代码合起来是这样的 ⬇️

// When and only when address[14] == 0, RAM is loaded
Not(in= address[14], out= inRamRange);
Mux(a= false, b= load, sel= inRamRange, out= loadRAM);
RAM16K(in= in, load= loadRAM, address= address[0..13], out= ramOut);
Mux16(a= false, b= ramOut, sel= inRamRange, out= outCandidate1);

2. 如果 addressScreen 的范围内

2.1 判断 address 是否在 Screen 的范围内

当且仅当 16384address<2457616384\le address \lt 24576 时, addressaddressScreen\text{Screen} 的范围内。由于 addressaddress1515 位的,这就等价于以下两个条件都成立 ⬇️

  • address[14]=1\text{address[14]}=1
  • address[13]=0\text{address[13]}=0

address[0..12]\text{address[0..12]} 这些位可以是任意值。当 addressaddressScreen\text{Screen} 的范围内时,addressaddress 的模式可以表示如下 ⬇️ (下图中用 ? 表示这一位可以是任意值)

mermaid-diagram-2026-01-21-224051.png

可以用一个 非门 来判断 address[13]=0\text{address[13]}=0 是否成立(这个非门的输出记为 notA13\text{notA13}) ⬇️

Not(in=address[13],out=notA13)\text{Not}(in= \text{address[13]}, out= \text{notA13})

在此基础上,可以用一个 与门 来判断以下两者是否都成立

  • address[14]=1\text{address[14]}=1
  • address[13]=0\text{address[13]}=0

这个与门的输出记为 inScreenRange\text{inScreenRange} ⬇️

And(a=address[14],b=notA13,out=inScreenRange)\text{And}(a= \text{address[14]}, b= \text{notA13}, out= \text{inScreenRange})

2.2 判断是否要对 Screen 进行 load 操作

我们可以用一个 Mux\text{Mux} 来结合 inScreenRange\text{inScreenRange}load\text{load}

Mux(a=false,b=load,sel=inScreenRange,out=loadScreen)\text{Mux}(a= \text{false}, b= \text{load}, sel= \text{inScreenRange}, out= \text{loadScreen})

现在可以将 in,loadScreen,address\text{in},\text{loadScreen},\text{address} 连接到 Screen\text{Screen} 上了(我们只需要 addressaddress 的低 1313 位,即 address[0..12]\text{address[0..12]}),将 Screen\text{Screen} 的输出记为 screenOut\text{screenOut} ⬇️

Screen(in=in,load=loadScreen,address=address[0..12],out=screenOut)\text{Screen}(in= \text{in}, load= \text{loadScreen}, address= \text{address[0..12]}, out= \text{screenOut})

2.3 判断是否要使用 screenOut\text{screenOut}

我们需要根据 inScreenRange\text{inScreenRange} 的值来判断是否使用 screenOut\text{screenOut} ⬇️

  • inScreenRange\text{inScreenRange}true\text{true} 时: 使用 screenOut\text{screenOut}
  • inScreenRange\text{inScreenRange}false\text{false} 时: 忽略 screenOut\text{screenOut}

因此可以用 Mux16\text{Mux16} 来选择是否使用 screenOut\text{screenOut},这个 Mux16\text{Mux16} 的输出记为 outCandidate2\text{outCandidate2} ⬇️

Mux16(a=false,b=screenOut,sel=inScreenRange,out=outCandidate2)\text{Mux16}(a= \text{false}, b= \text{screenOut}, sel= \text{inScreenRange}, out= \text{outCandidate2})

2.4 将这些代码合在一起

Screen 相关的代码合起来是这样的 ⬇️

// When and only when (address[14] == 1) and (address[13] == 0), Screen is loaded
Not(in= address[13], out= notA13);
And(a= address[14], b= notA13, out= inScreenRange);
Mux(a= false, b= load, sel= inScreenRange, out= loadScreen);
Screen(in= in, load= loadScreen, address= address[0..12], out= screenOut);
Mux16(a= false, b= screenOut, sel= inScreenRange, out= outCandidate2);

3 如果 addressKeyboard 的范围内

通过如下方式可以获取 Keyboard 的输出 ⬇️ 记这个输出为 kbOut\text{kbOut}

Keyboard(out=kbOut)\text{Keyboard}(out= \text{kbOut})

3.1 判断 address 是否在 Keyboard 的范围内

当且仅当 address=24576address=24576 时,addressaddressKeyboard\text{Keyboard} 的范围内。当 addressaddress 在 Keyboard\text{Keyboard} 的范围内时,addressaddress 的模式可以表示如下 ⬇️

mermaid-diagram-2026-01-21-223905.png

我们需要判断以下两个条件是否都成立

  • address[13]=1\text{address[13]}=1address[14]=1\text{address[14]}=1
  • address[0..12]\text{address[0..12]} 都是 00

可以用一个 与门 来判断 address[13]=1\text{address[13]}=1address[14]=1\text{address[14]}=1 是否同时成立 ⬇️

And(a=address[13],b=address[14],out=high2BitAnd)\text{And}(a= \text{address[13]}, b= \text{address[14]}, out= \text{high2BitAnd})

在此基础上,还需要判断 address[0..12]\text{address[0..12]} 是否都为 00。如果逐位去判断,看起来比较繁琐。可以这样考虑,如果 address[0..12]\text{address[0..12]} 都为 00 的话,记

sum=address[0..14]+111n times13 个 1\text{sum}=\text{address[0..14]} + \underbrace{11\cdots 1}_{n\rm\ times}^{\text{13 个 1}}
=11000n times13 个 0+111n times13 个 1=11\underbrace{00\cdots 0}_{n\rm\ times}^{\text{13 个 0}}+\underbrace{11\cdots 1}_{n\rm\ times}^{\text{13 个 1}}
=111n times15 个 1=\underbrace{11\cdots 1}_{n\rm\ times}^{\text{15 个 1}}
=0111n times15 个 1=0\underbrace{11\cdots 1}_{n\rm\ times}^{\text{15 个 1}}

那么 sum[15]=0\text{sum}[15]=0。满足 address[13]=1\text{address[13]}=1address[14]=1\text{address[14]}=1addressaddress 共有 2132^{13} 个。这些 addressaddress111n times13 个 1\underbrace{11\cdots 1}_{n\rm\ times}^{\text{13 个 1}} 的结果汇总如下 ⬇️

1515 位的 address\text{address}address\text{address} 视为 1616十进制表示的address\text{address}address\text{address}111n times13 个 1\underbrace{11\cdots 1}_{n\rm\ times}^{\text{13 个 1}} 之后得到的 1616 位的 sumsum用十六进制表示 sumsum
1100000000000002110000000000000_2011000000000000020110000000000000_22457624576011111111111111120111111111111111_20x7FFF\text{0x7FFF}
1100000000000012110000000000001_2011000000000000120110000000000001_22457724577100000000000000021000000000000000_20x8000\text{0x8000}
\cdots\cdots\cdots\cdots\cdots
1111111111111112111111111111111_2011111111111111120111111111111111_23276732767100111111111111021001111111111110_20x9FFE\text{0x9FFE}

对这些 addressaddress 而言,只有 1100000000000002110000000000000_2 算出的 sumsum,其 sum[15]=0sum[15]=0。可以用一个 与门 来判断以下两者是否都成立(如果以下两者同时成立,则说明 address=1100000000000002\text{address}=110000000000000_2)

  • high2BitAnd=true\text{high2BitAnd}=\text{true}
  • sum[15]=0\text{sum}[15]=0

将这个 与门 的输出记为 inKbRange\text{inKbRange},对应的实现如下 ⬇️

Add16(a[0..14]=address,b[0..12]=true,out[15]=msbIsOne)\text{Add16}(a[0..14]= \text{address}, b[0..12]= \text{true}, out[15]= \text{msbIsOne})
Not(in=msbIsOne,out=msbIsZero)\text{Not}(in= \text{msbIsOne}, out= \text{msbIsZero})
And(a=high2BitAnd,b=msbIsZero,out=inKbRange)\text{And}(a= \text{high2BitAnd}, b= \text{msbIsZero}, out= \text{inKbRange})

3.2 判断是否要使用 kbOut\text{kbOut}

我们需要根据 inKbRange\text{inKbRange} 的值来判断是否使用 kbOut\text{kbOut} ⬇️

  • inKbRange\text{inKbRange}true\text{true} 时: 使用 kbOut\text{kbOut}
  • inKbRange\text{inKbRange}false\text{false} 时: 忽略 kbOut\text{kbOut}

因此可以用 Mux16\text{Mux16} 来选择是否使用 kbOut\text{kbOut},这个 Mux16\text{Mux16} 的输出记为 outCandidate3\text{outCandidate3} ⬇️

Mux16(a=false,b=kbOut,sel=inKbRange,out=outCandidate3)\text{Mux16}(a= \text{false}, b= \text{kbOut}, sel= \text{inKbRange}, out= \text{outCandidate3})

3.3 将这些代码合在一起

Keyboard 相关的代码合起来是这样的 ⬇️

Keyboard(out= kbOut);
// When and only when address is 110000000000000 (there are two '1' and thirteen '0'),
// keyboard is loaded
And(a= address[13], b= address[14], out= high2BitAnd);
Add16(a[0..14]= address, b[0..12]= true, out[15]= msbIsOne);
Not(in= msbIsOne, out= msbIsZero);
And(a= high2BitAnd, b= msbIsZero, out= inKbRange);
Mux16(a= false, b= kbOut, sel= inKbRange, out= outCandidate3);

4 将 RAM/Screen/Keyboard 的输出整合在一起

  • RAM\text{RAM} 有关的候选输出在 outCandidate1\text{outCandidate1}
  • Screen\text{Screen} 有关的候选输出在 outCandidate2\text{outCandidate2}
  • Keyboard\text{Keyboard} 有关的候选输出在 outCandidate3\text{outCandidate3}

我们可以用两个 Or16\text{Or16}outCandidate1,outCandidate2,outCandidate3\text{outCandidate1},\text{outCandidate2},\text{outCandidate3} 进行或运算 ⬇️

Or16(a=outCandidate1,b=outCandidate2,out=outTemp)\text{Or16}(a= \text{outCandidate1}, b= \text{outCandidate2}, out= \text{outTemp})
Or16(a=outTemp,b=outCandidate3,out=out)\text{Or16}(a= \text{outTemp}, b= \text{outCandidate3}, out= \text{out})

将上面的代码都整合到一起,得到完整的 hdl 代码如下 ⬇️

CHIP Memory {
    IN in[16], load, address[15];
    OUT out[16];

    PARTS:
    // When and only when address[14] == 0, RAM is loaded
    Not(in= address[14], out= inRamRange);
    Mux(a= false, b= load, sel= inRamRange, out= loadRAM);
    RAM16K(in= in, load= loadRAM, address= address[0..13], out= ramOut);
    Mux16(a= false, b= ramOut, sel= inRamRange, out= outCandidate1);
    
    // When and only when (address[14] == 1) and (address[13] == 0), Screen is loaded
    Not(in= address[13], out= notA13);
    And(a= address[14], b= notA13, out= inScreenRange);
    Mux(a= false, b= load, sel= inScreenRange, out= loadScreen);
    Screen(in= in, load= loadScreen, address= address[0..12], out= screenOut);
    Mux16(a= false, b= screenOut, sel= inScreenRange, out= outCandidate2);
    
    Keyboard(out= kbOut);
    // When and only when address is 110000000000000 (there are two '1' and thirteen '0'),
    // keyboard is loaded
    And(a= address[13], b= address[14], out= high2BitAnd);
    Add16(a[0..14]= address, b[0..12]= true, out[15]= msbIsOne);
    Not(in= msbIsOne, out= msbIsZero);
    And(a= high2BitAnd, b= msbIsZero, out= inKbRange);
    Mux16(a= false, b= kbOut, sel= inKbRange, out= outCandidate3);
    
    Or16(a= outCandidate1, b= outCandidate2, out= outTemp);
    Or16(a= outTemp, b= outCandidate3, out= out);
}

这样的代码可以通过仿真测试,具体测试步骤如下。先点击 Run\text{Run} 按钮,开始测试

image.png

测试过程中,会看到以下提示

Click the Keyboard icon and hold down the 'K' key (uppercase) until you see the next message...

image.png

我们把 Chip Memory 这个面板(在屏幕中间)滑动到最底部,确保键盘的输入是被允许的(当你能看到 "Disable Keyboard" 时,就处于正确的状态) ⬇️ 此时通过键盘输入 K,就可以继续进行测试了

image.png

之后会看到 Screen 区域出现两个短的横线,在屏幕最下方会有如下的提示

Two horizontal lines should be in the middle of the screen. Hold down 'Y' (uppercase) until you see the next message ...

image.png

此时用键盘输入 Y,就可以继续进行测试。之后会看到如下的提示

Simulation successful: The output file is identical to the compare file

这就说明测试通过了 ⬇️ image.png

其他

文中展示 RAM/Screen/Keyboard 地址范围的那些图是如何画出来的?我以 Keyboard 为例,来进行说明。

在 mermaid.live 页面可以绘制 block 图。具体的语法可以参考 Block Diagrams Documentation 一文。用以下代码可以画出对应的图 ⬇️

block
    block
        columns 15
        p14["[14]"] p13["[13]"] p12["[12]"] p11["[11]"] p10["[10]"]
        p9["[9]"] p8["[8]"] p7["[7]"] p6["[6]"] p5["[5]"]
        p4["[4]"] p3["[3]"] p2["[2]"] p1["[1]"] p0["[0]"]
        b14["1"] b13["1"] b12["0"] b11["0"] b10["0"]
        b9["0"] b8["0"] b7["0"] b6["0"] b5["0"]
        b4["0"] b3["0"] b2["0"] b1["0"] b0["0"]
    end

style p14 fill:#fff,stroke:#fff;
style p13 fill:#fff,stroke:#fff;
style p12 fill:#fff,stroke:#fff;
style p11 fill:#fff,stroke:#fff;
style p10 fill:#fff,stroke:#fff;
style p9 fill:#fff,stroke:#fff;
style p8 fill:#fff,stroke:#fff;
style p7 fill:#fff,stroke:#fff;
style p6 fill:#fff,stroke:#fff;
style p5 fill:#fff,stroke:#fff;
style p4 fill:#fff,stroke:#fff;
style p3 fill:#fff,stroke:#fff;
style p2 fill:#fff,stroke:#fff;
style p1 fill:#fff,stroke:#fff;
style p0 fill:#fff,stroke:#fff;

style b14 fill:#0f0;
style b13 fill:#0f0;
style b12 fill:#ff0;
style b11 fill:#ff0;
style b10 fill:#ff0;
style b9 fill:#ff0;
style b8 fill:#ff0;
style b7 fill:#ff0;
style b6 fill:#ff0;
style b5 fill:#ff0;
style b4 fill:#ff0;
style b3 fill:#ff0;
style b2 fill:#ff0;
style b1 fill:#ff0;
style b0 fill:#ff0;

image.png

参考资料