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

51 阅读9分钟

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

本文只涉及 CPU 的实现

说明

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

p66.png

p67_temp.png

正文

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

image.png

我们的目标是实现 CPU (中央处理单元),注释中的相关描述如下 ⬇️

/**
 * The Hack Central Processing unit (CPU).
 * Parses the binary code in the instruction input and executes it according to the
 * Hack machine language specification. In the case of a C-instruction, computes the
 * function specified by the instruction. If the instruction specifies to read a memory
 * value, the inM input is expected to contain this value. If the instruction specifies
 * to write a value to the memory, sets the outM output to this value, sets the addressM
 * output to the target address, and asserts the writeM output (when writeM = 0, any
 * value may appear in outM).
 * If the reset input is 0, computes the address of the next instruction and sets the
 * pc output to that value. If the reset input is 1, sets pc to 0.
 * Note: The outM and writeM outputs are combinational: they are affected by the
 * instruction's execution during the current cycle. The addressM and pc outputs are
 * clocked: although they are affected by the instruction's execution, they commit to
 * their new values only in the next cycle.
 */
  • 输入是
    • inM[16]\text{inM[16]}
    • instruction[16]\text{instruction[16]}
    • reset\text{reset}
  • 输出是
    • outM[16]\text{outM[16]}
    • writeM\text{writeM}
    • addressM[15]\text{addressM[15]}
    • pc[15]\text{pc[15]}

实现 CPU 需要处理以下 3 部分的逻辑

  1. 指令译码
  2. 指令的执行
  3. 取指令

1 指令译码

《计算机系统要素 (第2版)》 一书第 67 页对 指令译码 有如下描述 ⬇️

image.png

如果 instruction[15]=0\text{instruction[15]}=0,则 instruction\text{instruction} 表示一条 A 指令。A 指令满足以下模式(? 表示对这一位的值没有限制)

mermaid-diagram-2026-01-24-120551.png

如果 instruction[15]=1\text{instruction[15]}=1,则 instruction\text{instruction} 表示一条 C 指令。C 指令的格式如下 ⬇️

mermaid-diagram-2026-01-24-150022.png

2 指令的执行

《计算机系统要素 (第2版)》 一书第 67 页对 指令的执行 有如下描述 ⬇️

image.png

  • aa: 确定 ALUyy 输入来自 A 寄存器的值还是来自输入的 M
    • 如果 a=0a=0,则 ALUyy 输入来自 A 寄存器
    • 如果 a=1a=1,则 ALUyy 输入来自输入的 M
  • cccccccccccc: 这些位用于确定 ALU 执行怎样的计算
  • dddddd (即 destdest): 这些位用于确定由哪些寄存器接收 ALU 的输出
  • jjjjjj (即 jumpjump): 这些位用于确定下一条要取出的指令

判断 instructionA 指令还是 C 指令

  • 如果 instruction[15]=0\text{instruction[15]}=0,则 instruction\text{instruction} 表示一条 A 指令
  • 如果 instruction[15]=1\text{instruction[15]}=1,则 instruction\text{instruction} 表示一条 C 指令

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

// instruction type
DMux(in= true, sel= instruction[15], a= aType, b= cType);

aa 位的判断

《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️

image.png 如果 instructionC 指令,那么 instruction[12]\text{instruction[12]} 就是 aa,它的值决定了 ALUyy 输入来自哪里

  • 如果 a=0a=0,则 ALUyy 输入来自 A 寄存器
  • 如果 a=1a=1,则 ALUyy 输入来自输入的 M

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️ (aRegister 表示 A 寄存器的输出,下文会提到)

Mux16(a= aRegisterOut, b= inM, sel= instruction[12], out= yOfAlu);

cccccccccccc 位的判断

《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️

image.png

ALU 而言,

  • 它的 xx 输入总是来自 D 寄存器的输出
  • 它的 yy 输入由 instruction[12]\text{instruction[12]} (即 aa 位) 来决定(上文已提到,这里不赘述)

这样可以写出对应的 hdl 代码 ⬇️ (dRegister 表示 D 寄存器的输出,下文会提到)

ALU(x= dRegisterOut, y= yOfAlu, 
    zx= instruction[11], 
    nx= instruction[10],
    zy= instruction[9], 
    ny= instruction[8], 
    f= instruction[7], 
    no= instruction[6], 
    out= aluOut, zr= zr, ng= ng);

dddddd (即 destdest) 位的判断

《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️

image.png

如果 instructionC 指令,那么 instruction[3..5]\text{instruction[3..5]} 表示 dddddd (即 destdest)

  • 如果 instruction[3]=1\text{instruction[3]}=1,则将计算结果存储在 RAM[A]\text{RAM[A]}
  • 如果 instruction[4]=1\text{instruction[4]}=1,则将计算结果存储在 D\text{D} 寄存器中
  • 如果 instruction[5]=1\text{instruction[5]}=1,则将计算结果存储在 A\text{A} 寄存器中

请注意,以上三个判断并不是互斥的。

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

// dest
And(a= cType, b= instruction[3], out= toM, out= writeM);
And(a= cType, b= instruction[4], out= toD);
And(a= cType, b= instruction[5], out= toA);

jjjjjj (即 jumpjump) 位的判断

《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️

image.png

如果 instructionC 指令,那么 instruction[0..2]\text{instruction[0..2]} 表示 jjjjjj (即 jumpjump)

  • 如果 instruction[0]=1\text{instruction[0]}=1,则当计算结果 >0\gt 0 时,要跳转
  • 如果 instruction[1]=1\text{instruction[1]}=1,则当计算结果 =0= 0 时,要跳转
  • 如果 instruction[2]=1\text{instruction[2]}=1,则当计算结果 <0\lt 0 时,要跳转

请注意,以上三个判断并不是互斥的。 (例如 instruction[0]=1\text{instruction[0]}=1instruction[1]=1\text{instruction[1]}=1 可以同时成立)

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

Or(a= zr, b= ng, out= compNonPositive);
Not(in= compNonPositive, out= compIsPositive);

And(a= instruction[0], b= compIsPositive, out= gtZero);
And(a= instruction[1], b= zr, out= eqZero);
And(a= instruction[2], b= ng , out= ltZero);
Or(a= gtZero, b= eqZero, out= geZero);
Or(a= geZero, b= ltZero, out= rawJump);
And(a= cType, b= rawJump, out= jump);

A 寄存器的处理

当以下两个条件中的某一个成立时(这两个条件不会同时成立),需要将 A 寄存器的 loadload 设置为 true\text{true}

  • instruction\text{instruction}A 指令
  • instruction\text{instruction}C 指令并且从 dddddd 中可以判断出需要将计算结果存储在 A 寄存器中

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

// We set "load" in A register to true,
// when either of the following condition is satisfied
// 1. instruction is A type, or
// 2. instruction is C type and we need to send the result to A
Mux16(a= aluOut, b= instruction, sel= aType, out= aRegisterInput);
Or(a= aType, b= toA, out= loadA);
ARegister(in= aRegisterInput, load= loadA, out= aRegisterOut, out[0..14]= addressM);

D 寄存器的处理

当以下两个条件都成立时,需要将 D 寄存器的 loadload 设置为 true\text{true}

  • instruction\text{instruction}C 指令
  • dddddd 中可以判断出需要将计算结果存储在 D 寄存器中

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

// When both of the following conditions are satisfied, we set "load" in D register to true
// 1. instruction is C type, and
// 2. we need to send the result to D
DRegister(in= aluOut, load= toD, out= dRegisterOut);

M 的处理

当以下两个条件都成立时,需要对 M (即 RAM[A]\text{RAM[A]}) 进行写操作。

  • instruction\text{instruction}C 指令
  • dddddd 中可以判断出需要将计算结果存储在 M

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

// When both of the following conditions are satisfied, we write M
// 1. instruction is C type, and
// 2. we need to send the result to M
Mux16(a= false, b= aluOut, sel= toM, out= outM);

3 取指令

《计算机系统要素 (第2版)》 一书第 68 页对 取指令 有如下描述 ⬇️

image.png

基于以上逻辑,可以写出对应的 hdl 代码 ⬇️

Or(a= reset, b= jump, out= noInc);
Not(in= noInc, out= toInc);

PC(in= aRegisterOut, load= jump, inc= toInc, reset= reset, out[0..14]= pc);

验证

将上文提到的各部分 hdl 组装在一起,可以得出完整的程序 ⬇️

// 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/5/CPU.hdl
/**
 * The Hack Central Processing unit (CPU).
 * Parses the binary code in the instruction input and executes it according to the
 * Hack machine language specification. In the case of a C-instruction, computes the
 * function specified by the instruction. If the instruction specifies to read a memory
 * value, the inM input is expected to contain this value. If the instruction specifies
 * to write a value to the memory, sets the outM output to this value, sets the addressM
 * output to the target address, and asserts the writeM output (when writeM = 0, any
 * value may appear in outM).
 * If the reset input is 0, computes the address of the next instruction and sets the
 * pc output to that value. If the reset input is 1, sets pc to 0.
 * Note: The outM and writeM outputs are combinational: they are affected by the
 * instruction's execution during the current cycle. The addressM and pc outputs are
 * clocked: although they are affected by the instruction's execution, they commit to
 * their new values only in the next cycle.
 */
CHIP CPU {

    IN  inM[16],         // M value input  (M = contents of RAM[A])
        instruction[16], // Instruction for execution
        reset;           // Signals whether to re-start the current
                         // program (reset==1) or continue executing
                         // the current program (reset==0).

    OUT outM[16],        // M value output
        writeM,          // Write to M? 
        addressM[15],    // Address in data memory (of M)
        pc[15];          // address of next instruction

    PARTS:
    // instruction type
    DMux(in= true, sel= instruction[15], a= aType, b= cType);

    // dest
    And(a= cType, b= instruction[3], out= toM, out= writeM);
    And(a= cType, b= instruction[4], out= toD);
    And(a= cType, b= instruction[5], out= toA);

    // We set "load" in A register to true,
    // when either of the following condition is satisfied
    // 1. instruction is A type, or
    // 2. instruction is C type and we need to send the result to A
    Mux16(a= aluOut, b= instruction, sel= aType, out= aRegisterInput);
    Or(a= aType, b= toA, out= loadA);
    ARegister(in= aRegisterInput, load= loadA, out= aRegisterOut, out[0..14]= addressM);

    // When both of the following conditions are satisfied, we set "load" in D register to true
    // 1. instruction is C type, and
    // 2. we need to send the result to D
    DRegister(in= aluOut, load= toD, out= dRegisterOut);

    // When both of the following conditions are satisfied, we write M
    // 1. instruction is C type, and
    // 2. we need to send the result to M
    Mux16(a= false, b= aluOut, sel= toM, out= outM);

    Mux16(a= aRegisterOut, b= inM, sel= instruction[12], out= yOfAlu);
    ALU(x= dRegisterOut, y= yOfAlu, 
        zx= instruction[11], 
        nx= instruction[10],
        zy= instruction[9], 
        ny= instruction[8], 
        f= instruction[7], 
        no= instruction[6], 
        out= aluOut, zr= zr, ng= ng);

    Or(a= zr, b= ng, out= compNonPositive);
    Not(in= compNonPositive, out= compIsPositive);
    
    And(a= instruction[0], b= compIsPositive, out= gtZero);
    And(a= instruction[1], b= zr, out= eqZero);
    And(a= instruction[2], b= ng , out= ltZero);
    Or(a= gtZero, b= eqZero, out= geZero);
    Or(a= geZero, b= ltZero, out= rawJump);
    And(a= cType, b= rawJump, out= jump);

    Or(a= reset, b= jump, out= noInc);
    Not(in= noInc, out= toInc);

    PC(in= aRegisterOut, load= jump, inc= toInc, reset= reset, out[0..14]= pc);
}

这样的代码可以通过仿真测试。在测试前,可以把速度按钮移动到比较接近 Fast 的位置

image.png

点击 Run 按钮,就可以开始测试了

image.png

如下图所示,测试通过 ⬇️

image.png

其他

用于展示 A 指令和 C 指令的模式的图是如何画出来的?

我是通过 mermaid.live 页面来绘制的。以 C 指令为例,对应的代码如下 ⬇️

block
    columns 1
    title["C 指令"]
    block
        columns 16
        p15["[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]"]
        
        b15["1"] b14["x"] b13["x"] b12["a"] b11["c"] b10["c"]
        b9["c"] b8["c"] b7["c"] b6["c"] b5["d"]
        b4["d"] b3["d"] b2["j"] b1["j"] b0["j"]
    end

style title fill:#fff,stroke:#fff;

style p15 fill:#fff,stroke:#fff;
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 b15 fill:#0f0;
style b14 fill:#fff;
style b13 fill:#fff;
style b12 fill:#f08;
style b11 fill:#088;
style b10 fill:#088;
style b9 fill:#088;
style b8 fill:#088;
style b7 fill:#088;
style b6 fill:#088;
style b5 fill:#f88;
style b4 fill:#f88;
style b3 fill:#f88;
style b2 fill:#0ff;
style b1 fill:#0ff;
style b0 fill:#0ff;

image.png

具体的语法请参考 Block Diagrams Documentation

参考资料