让我们按照 From Nand to Tetris 里 Project 5 的要求,来完成下列的设计。
- 1.
Memory(已完成,可以参考 From Nand to Tetris 里的 Project 5 (Memory 部分) 一文) - 2.
CPU - 3.
Computer
本文只涉及 CPU 的实现
说明
我是阅读了《计算机系统要素 (第2版)》 第 5 章的内容后才去完成 Project 5 的。读者朋友在完成 Project 5 时,如果遇到不明白的地方,可以参考书中的描述。书中第 66 页和第 67 页有如下内容,可供参考。
正文
前往 Nand to Tetris Online IDE,选择 Project 5 里的 CPU ⬇️
我们的目标是实现 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.
*/
- 输入是
- 输出是
实现 CPU 需要处理以下 3 部分的逻辑
- 指令译码
- 指令的执行
- 取指令
1 指令译码
《计算机系统要素 (第2版)》 一书第 67 页对 指令译码 有如下描述 ⬇️
如果 ,则 表示一条 A 指令。A 指令满足以下模式(? 表示对这一位的值没有限制)
如果 ,则 表示一条 C 指令。C 指令的格式如下 ⬇️
2 指令的执行
《计算机系统要素 (第2版)》 一书第 67 页对 指令的执行 有如下描述 ⬇️
- : 确定
ALU的 输入来自A寄存器的值还是来自输入的M值- 如果 ,则
ALU的 输入来自A寄存器 - 如果 ,则
ALU的 输入来自输入的M值
- 如果 ,则
- : 这些位用于确定
ALU执行怎样的计算 - (即 ): 这些位用于确定由哪些寄存器接收
ALU的输出 - (即 ): 这些位用于确定下一条要取出的指令
判断 instruction 是 A 指令还是 C 指令
- 如果 ,则 表示一条
A指令 - 如果 ,则 表示一条
C指令
基于以上逻辑,可以写出对应的 hdl 代码 ⬇️
// instruction type
DMux(in= true, sel= instruction[15], a= aType, b= cType);
对 位的判断
《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️
如果
instruction 是 C 指令,那么 就是 ,它的值决定了 ALU 的 输入来自哪里
- 如果 ,则
ALU的 输入来自A寄存器 - 如果 ,则
ALU的 输入来自输入的M值
基于以上逻辑,可以写出对应的 hdl 代码 ⬇️ (aRegister 表示 A 寄存器的输出,下文会提到)
Mux16(a= aRegisterOut, b= inM, sel= instruction[12], out= yOfAlu);
对 位的判断
《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️
对 ALU 而言,
- 它的 输入总是来自
D寄存器的输出 - 它的 输入由 (即 位) 来决定(上文已提到,这里不赘述)
这样可以写出对应的 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);
对 (即 ) 位的判断
《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️
如果 instruction 是 C 指令,那么 表示 (即 )
- 如果 ,则将计算结果存储在 中
- 如果 ,则将计算结果存储在 寄存器中
- 如果 ,则将计算结果存储在 寄存器中
请注意,以上三个判断并不是互斥的。
基于以上逻辑,可以写出对应的 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);
对 (即 ) 位的判断
《计算机系统要素 (第2版)》 一书第 49 页有如下描述 ⬇️
如果 instruction 是 C 指令,那么 表示 (即 )
- 如果 ,则当计算结果 时,要跳转
- 如果 ,则当计算结果 时,要跳转
- 如果 ,则当计算结果 时,要跳转
请注意,以上三个判断并不是互斥的。 (例如 和 可以同时成立)
基于以上逻辑,可以写出对应的 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 寄存器的 设置为 。
- 是
A指令 - 是
C指令并且从 中可以判断出需要将计算结果存储在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 寄存器的 设置为 。
- 是
C指令 - 从 中可以判断出需要将计算结果存储在
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 (即 ) 进行写操作。
- 是
C指令 - 从 中可以判断出需要将计算结果存储在
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 页对 取指令 有如下描述 ⬇️
基于以上逻辑,可以写出对应的 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 的位置
点击 Run 按钮,就可以开始测试了
如下图所示,测试通过 ⬇️
其他
用于展示 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;
具体的语法请参考 Block Diagrams Documentation