来构建一个属于自己的ALU!Nand2Tetris(计算机系统要素) Unit 2 学习笔记

起初是在CS自学指南里找到的这门课(依据基本原理构建现代计算机:从与非门到俄罗斯方块),感觉还不错而且零基础,是一个比较好的入门课程。同时全英教学可以逆向让我捉襟见肘的英语水平稍微强一点,多理解一个单词就是胜利(

课程链接:https://www.coursera.org/learn/build-a-computer

Unit 2.1 Binary Numbers(二进制数)

二进制就是两个数进一位 所以只有 0 和 1 两个数字

但是只有0和1能做什么东西呢?

image-20251127203733344

如果 将0和1两个一组就将有四种可能 三个一组将有八种可能 以2的n次方进行推演

image-20251127204118138

二进制也可以用来表示十进制 这个一带而过就好啦

这节课就是讲二进制数的转换 没别的东西 一带而过了 这个太熟悉了

image-20251127204244120

Unit 2.2 Binary Addition(二进制加法)

上节课主要讲解了如何使用二进制来表示数字,但是要用这些数字做什么操作呢? 例如加法减法乘法?

image-20251207174807842

这节课主要是做加法 现在已经会了将二进制数和十进制数互相转换 可以通过先转换为十进制数然后进行加法 然后再转换回来的方法实现二进制加法

image-20251207175051157

但是这终究不是计算机做的事情 这只是因为我们比较擅长十进制加法从而解决的方法

计算机不通过十进制中转,而是直接进行二进制加法,其原理与我们小学学的竖式加法完全相同,只是基数从10变成了2。

正常十进制的加法 在同位满十进一

image-20251207204501660

二进制也是如此 只不过是满二进一而已 可能一开始会有点不习惯 之后用多啦计算会很快

image-20251207204620523

溢出

  • 当两个大数相加,结果超出了字长(如16位)能表示的范围时,会发生溢出
  • 计算机的通常做法:忽略最高位的进位
  • 数学上解释:这是在执行模2ⁿ加法(n为字长)
  • 示例:在4位系统中,1111(15) + 0001(1) = 0000(0),因为进位被丢弃

例如在这里 最高位的一被忽略了

image-20251207232959982

从位操作到完整加法器

计算机加法的硬件实现分为三个关键阶段,从最简单的位操作开始,逐步构建完整的加法系统。

做一个简单的示例 我们是如何取1+1 得到0和1的进位的

image-20251214213712746

我们来只关注加法输入以及加法输出

image-20251214213817916

我们可以发现将两个输入定义为a和b 结果就会得到a和b的和以及a与b的进位 如下图所示 这也就成功设计出来半加器了

**半加器(Half Adder)**它处理最简单的情况——两个位相加,不考虑前级进位。

image-20251214214009555

假如我们定义一个c,这是前一个步骤的进位(可以是0或1) 来和这里的a和b一起相加

image-20251214214146005

这时候就实现了全加器 由两个相加的数a和b以及前一位的进位c 得到和以及进位

全加器(Full Adder)。现实中的加法需要考虑前一级的进位,全加器正是为此设计。它有3个输入(a, b, c_in)和2个输出(sum, c_out),能够处理所有8种可能的输入组合。

image-20251214214246033

Unit 2.3 Negative Numbers(负数)

目前只讨论过如何使用正数,可是计算机肯定也要进行负数运算,这是我数电课都没学过的知识目前(

举一个例子,这个是四位二进制数进行穷举排列,可以得到从0到15的十进制正整数。

image-20251216215416203

原码(符号位)

这里其实就用了一个比较简单的方法(我以为会很难呢没想到这么简单 用了一位二进制数作为判断正负的位置,例如我们定义第一位为0是正数 反之则为负数

image-20251216215628739

但是这样确实会出现问题 例如出现了-0这种逆天的数字

image-20251216215801623

补码(Two’s Complement)

因为要明确处理加减号等诸多不方便的因素这个方式还是被弃用了 后来用了一个叫**补码(Two’s Complement)**的东西:用 2^N 减去 |x| 来表示负数 -x

总共有 16 个数(0~15 的二进制)。

  • 我们把大于等于 8 的数(8~15)重新解释为负数:

    • -115(即 16 - 1)表示 → 1111
    • -313(即 16 - 3)表示 → 1101
    • -88(即 16 - 8)表示 → 1000

这样就可以很好的解决掉-0的问题 而且还延申出了一个概念就是补码 与之相对的刚刚是原码

image-20251217214528462

例子:-2 + (-3)

  • -2 → 14 (1110)
  • -3 → 13 (1101)
  • 相加:1110 + 1101 = 1 1011(进位1丢弃)
  • 结果:1011 = 11(补码值)→ -5

image-20251217215334413

Unit 2.4 Arithmetic Logic Unit(算术逻辑单元)

ALU 的基本概念

冯诺依曼写的一篇论文中阐述了计算机是怎么构造的 也叫冯诺依曼架构 如下图表

image-20260227191354397

注意到里面的CPU 是由ALU和Control组成的 先只关注ALU(算术逻辑单元) 一般国内的教材会叫它运算器(Arithmetic Logic Unit

image-20260227191711900

抽象的一个比喻是 它可以接受多Bit的输入 例如Input1和Input2 第三个输入为计算的函数 输出为ALU计算的结果

ALU通常执行常见计算 例如整数加法 乘法 除法等等

image-20260227191912810

硬件与软件的权衡

  • 在设计 ALU 时,设计者需要决定将哪些功能固化在硬件中(如加法、乘法),哪些功能留给软件实现。
  • 硬件实现速度更快,但会增加复杂度;软件实现更灵活,但性能可能较低。

The Hack ALU 具体设计

image-20260227192515459

输入与输出

  • 两个 16 位数据输入:xy
  • 一个 16 位数据输出:out
  • 六个控制位(zx, nx, zy, ny, f, no),用于指定要执行的操作。
  • 两个 1 位状态输出:zr(结果为零)和 ng(结果为负)。

六个控制位共组合出来18个不同的运算(其实可以做更多 但重要的就这些) 真值表如下

如果ALU没有实现乘除法,后续可以通过软件层来补充这个功能。这是理解计算机系统分层设计的关键。

image-20260227193054615

ALU 支持 18 种不同的运算,包括:

  • 常数(0, 1, -1)
  • 直接输出 xy
  • 按位取反(!x, !y
  • 负数(-x, -y
  • 算术运算(x+1, y+1, x-1, y-1, x+y, x-y, y-x
  • 逻辑运算(x & y, x | y

The Hack ALU 内部逻辑

现在了解ALU的接口和简单的功能了 但是内部的设计对于我们来讲还算是一个黑盒 只知道怎么用 不知道怎么实现

image-20260227193552003

ALU 六个控制位的具体含义

image-20260227193705369

控制位的作用

  1. zx(零 x):如果为 1,则将 x 设置为 0。(相当于置0)
  2. nx(非 x):如果为 1,则将 x 按位取反。
  3. zy(零 y):如果为 1,则将 y 设置为 0。(相当于置0)
  4. ny(非 y):如果为 1,则将 y 按位取反。
  5. f(功能选择):如果为 1,则计算 x + y(加法);如果为 0,则计算 x & y(按位与)。
  6. no(非输出):如果为 1,则将最终结果按位取反。

控制位的顺序性

  1. zx/zy 先执行
    • 先根据 zx 决定是否将 x 置为 0
    • 先根据 zy 决定是否将 y 置为 0
  2. nx/ny 后执行
    • 再根据 nx 决定是否将(已处理的)x 取反
    • 再根据 ny 决定是否将(已处理的)y 取反
  3. f 执行
    • 根据 f 选择做加法(x + y)或按位与(x & y)
  4. no 最后执行
    • 根据 no 决定是否将最终结果取反

如何通过控制位选择运算

  • 通过设置六个控制位的二进制值,可以指定 ALU 执行特定的运算。
  • 示例:
    • 计算 !x(按位取反 x):控制位为 001100,ALU 会先处理 x(保持不变),将 y 置零并取反,然后计算 x & y,最后对结果取反,得到 !x
    • 计算 y - x:控制位为 000111,ALU 会先对 y 取反,然后计算 x + (-y),最后取反,实际上实现了减法。

状态输出 zrng

  • zr:如果计算结果为 0,则 zr = 1;否则为 0。
  • ng:如果计算结果为负数,则 ng = 1;否则为 0。
  • 这两个输出在后续的计算机架构设计中(如条件跳转)非常有用。

Unit 2.5 Project 2 Overview(项目 2 概述)

可用资源

  • 基础模块: 你可以自由使用在项目1中构建的所有芯片(如与门、或门、多路器等)。
  • 实现方式: 在硬件描述语言(HDL)中,将这些芯片作为部件进行连接,以构建更复杂的功能。

需要构建的五个芯片(组合逻辑芯片系列)

半加器(HalfAdder)

  • 功能: 将两个二进制位(a, b)相加,产生“和(Sum)”与“进位(Carry)”。
  • 实现提示: 仔细观察真值表会发现,Sum和Carry的输出分别等同于你已经构建过的两个逻辑门(异或门/与门)的输出。

image-20260305014341158

全加器(FullAdder)

  • 功能: 将三个二进制位(a, b, c)相加,产生“和(Sum)”与“进位(Carry)”。
  • 实现提示: 一个常用的方法是使用两个半加器结合一些额外的逻辑门(“胶水逻辑”)来构建。当然,也欢迎使用其他任何有效的HDL实现。

image-20260305014354871

16位加法器(16-bit Adder)

  • 功能: 对两个16位的数字进行加法运算。
  • 实现提示: 可以通过串联16个全加器来实现。将低位全加器产生的进位输出连接到相邻高位的进位输入,依次从最低位向最高位传播。根据芯片规范,最高位的进位输出被直接忽略

image-20260305014414100

增量器(Incrementor)

  • 功能: 这是一个简化的加法器,功能是将输入的16位数值加1。
  • 实现提示: 可以使用你已构建的加法器芯片来实现。在HDL中,可以使用关键字 falsetrue 来表示单比特的 01

image-20260305014432636

算术逻辑单元(ALU)

  • 功能: 这是最核心、最重要的芯片。它能根据控制位执行一系列预定义的算术和逻辑运算。
  • 实现提示:
    • 预计可以使用16位加法器和项目1中构建的各种芯片组合而成。

image-20260305014452352

Programming Assignment: Project 2(编程作业: 项目 2)

1
2
3
准备提交时,请将您编写的所有 *.hdl 文件打包成一个名为 project2.zip 的压缩文件(打包文件本身,不要放在任何子文件夹内),然后提交。您提交的次数不限,成绩将是您所有提交成绩的最大值,因此您不会因为再次提交而丢分。

如果您以审核员身份参加课程,您可以使用硬件模拟器中的测试脚本自行检查您的工作。如果您选择证书选项,请在此提交您的项目压缩文件。

需要构建五个芯片

  1. HalfAdder(半加器)- 计算两个位的和与进位
  2. FullAdder(全加器)- 计算三个位的和与进位
  3. Add16(16位加法器)- 对两个16位数进行加法运算
  4. Inc16(16位增量器)- 对16位数加1
  5. ALU(算术逻辑单元)- Hack计算机的核心运算单元

HalfAdder(半加器)

image-20260305014341158

这里可以看到有a和b的输入 还有sum(和)和carry(进位)的输出

我们聚焦到输出就可以 carry使用一个与门链接 sum使用一个Xor门链接

如果没有思路 回顾一下上一个单元做的And门和Xor门真值表就好啦

image-20260307010925139

image-20260307010942128

最终写出来的hdl代码如下

1
2
3
4
5
6
7
8
9
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-20260307011853271

FullAdder(全加器)

image-20260305014354871

一开始我的思路是关注中心在于输出与输入的关系

先专注一下sum 会发现需要满足A B C都为1 或者 A B C其中只能有一个为1的情况 sum值才为1

这里也就是Sum=A异或B异或C XOR还有奇校验的功能 如果是奇数个1 就会输出1

如果不考虑化简的情况下 可以用与门和非门一点点实现 但是那样的hdl就会写的太长了

其实把注意力观察到半加器与全加器的关系就能很快解决 全加器无非就是实现了进位相加的操作

观察全加器与半加器Sum的不同 其实就是少了一个c(来着上一位的进位)这个值

如果将两个全加器的Sum按顺序拼接起来 就可以实现全加器的Sum(和)运算

让一个半加器的输入为a b 输出为sumab 然后这里的sumab 正好可以作为下一个半加器的输入和c进行半加器运算 最后得到的Sum的输入就为 A B C啦

用代码块生成了一个大概的流程图 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                  ┌─────────────┐
A ────────────────┤ │
│ 半加器1 │
B ────────────────┤ │
└─────────────┘

│ Sum_AB (A⊕B)

┌─────────────┐
C ────────────────┤ │
│ 半加器2 │
Sum_AB ───────────┤ │
└─────────────┘

│ Sum (A⊕B⊕C)

最终结果

然后看一下Carry 也就是进位 其实可以观察到只需要三个值大于等于两个为1就可以 类似于三人举手表决电路

这时候看空下来的接口 第一个半加器的Carry的效果为A & B 第二个半加器的Carry 相当于(A⊕B)& C

化成最小项表达式正好符合真值表 如果将这两个Carry 用 或门连接 就相当于实现了全加器 最后hdl文件如下

1
2
3
4
5
6
7
8
9
10
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= sumab, carry= carry1);
HalfAdder(a= sumab, b= c, sum= sum, carry= carry2);
Or(a= carry1, b= carry2, out= carry);
}

测试成功!全加器也设计成功啦!!!

16位加法器(16-bit Adder)

image-20260310214256327

这个我感觉类似于总线的概念 用十六个全加器应该就可以实现 将carry的输出作为下一位的输入即可 一开始的为false 大概开头如下

1
2
3
4
5
6
7
8
CHIP Add16 {
IN a[16], b[16];
OUT out[16];

PARTS:
FullAdder(a= a[0], b= b[0], c= false, sum= out[0], carry= carry0);
FullAdder(a= a[1], b= b[1], c= carry0, sum= out[1], carry= carry1);
}

然后一直循环去写 就能实现十六位加法器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CHIP Add16 {
IN a[16], b[16];
OUT out[16];

PARTS:
FullAdder(a=a[0], b=b[0], c=false, sum=out[0], carry=carry0);
FullAdder(a=a[1], b=b[1], c=carry0, sum=out[1], carry=carry1);
FullAdder(a=a[2], b=b[2], c=carry1, sum=out[2], carry=carry2);
FullAdder(a=a[3], b=b[3], c=carry2, sum=out[3], carry=carry3);
FullAdder(a=a[4], b=b[4], c=carry3, sum=out[4], carry=carry4);
FullAdder(a=a[5], b=b[5], c=carry4, sum=out[5], carry=carry5);
FullAdder(a=a[6], b=b[6], c=carry5, sum=out[6], carry=carry6);
FullAdder(a=a[7], b=b[7], c=carry6, sum=out[7], carry=carry7);
FullAdder(a=a[8], b=b[8], c=carry7, sum=out[8], carry=carry8);
FullAdder(a=a[9], b=b[9], c=carry8, sum=out[9], carry=carry9);
FullAdder(a=a[10], b=b[10], c=carry9, sum=out[10], carry=carry10);
FullAdder(a=a[11], b=b[11], c=carry10, sum=out[11], carry=carry11);
FullAdder(a=a[12], b=b[12], c=carry11, sum=out[12], carry=carry12);
FullAdder(a=a[13], b=b[13], c=carry12, sum=out[13], carry=carry13);
FullAdder(a=a[14], b=b[14], c=carry13, sum=out[14], carry=carry14);
FullAdder(a=a[15], b=b[15], c=carry14, sum=out[15], carry=carry);
}

跑完发现没问题!完事!

image-20260310215343476

增量器(Incrementor)

image-20260310215604453

用十六位加法器实现就可以 这里的关键是 设置最低为1为是true 剩余的为0是false hdl写法是这样的

1
2
3
4
5
6
7
8
CHIP Inc16 {
IN in[16];
OUT out[16];

PARTS:
//// Replace this comment with your code.
Add16(a = in, b[0]= true , b[1..15]= false, out = out);
}

测试一下!过辣

image-20260310220203771

算术逻辑单元(ALU)

image-20260310220537721

这是应该目前最难的项目了 要手搓一个这么复杂的 支持如此多功能的芯片

这里要用到之前课中的一些概念

控制位的作用

  1. zx(零 x):如果为 1,则将 x 设置为 0。(相当于置0)
  2. nx(非 x):如果为 1,则将 x 按位取反。
  3. zy(零 y):如果为 1,则将 y 设置为 0。(相当于置0)
  4. ny(非 y):如果为 1,则将 y 按位取反。
  5. f(功能选择):如果为 1,则计算 x + y(加法);如果为 0,则计算 x & y(按位与)。
  6. no(非输出):如果为 1,则将最终结果按位取反。

控制位的顺序性

  1. zx/zy 先执行
    • 先根据 zx 决定是否将 x 置为 0
    • 先根据 zy 决定是否将 y 置为 0
  2. nx/ny 后执行
    • 再根据 nx 决定是否将(已处理的)x 取反
    • 再根据 ny 决定是否将(已处理的)y 取反
  3. f 执行
    • 根据 f 选择做加法(x + y)或按位与(x & y)
  4. no 最后执行
    • 根据 no 决定是否将最终结果取反

处理输入逻辑

我的思路是根据这个芯片的顺序性来一点一点去设计例如如果zx或zy为1 用一个与门连接 直接置零就ok啦 但后来发现有更好用的芯片 就是Mux 数据选择器 可以实现多路的数据选择(其实在这里有点类似于 if语句的感觉 如果为1就同行这个 不为1就通行那个)

1
2
Mux16(a= x, b= false, sel= zx, out= x0);
Mux16(a= y, b= false, sel= zy, out= y0);

然后进行第二步操作即可 用一个Not16的门 如果为1就取反 不为1就正常下一步

用Not门加Mux门即可实现 实现的思路和刚刚基本一致

1
2
3
4
Not16(in= x0, out= notx0);
Not16(in= y0, out= noty0);
Mux16(a= x0, b= notx0, sel= nx, out= x1);
Mux16(a= y0, b= noty0, sel= ny, out= y1);

现在基本了解逻辑之后就很简单了 这个f无非就是先做好对应的与和加芯片连起来 然后最后用Mux作为If判断 就可以实现

1
2
3
Add16(a= x1, b= y1, out= x1addy1);
And16(a= x1, b= y1, out= x1andy1);
Mux16(a= x1andy1, b= x1addy1, sel= f, out= foutput);

然后然后最后一样的逻辑进行Not取反即可

1
2
Not16(in= foutput, out= notfoutput);
Mux16(a= foutput, b= notfoutput, sel= no, out= out);

最后我们再实现两个输出接口即可!

状态输出的配置

状态输出 zrng

  • zr:如果计算结果为 0,则 zr = 1;否则为 0。
  • ng:如果计算结果为负数,则 ng = 1;否则为 0。

看起来第二个接口ng好实现一点 有一个原码补码反码的概念

最高位如果是0就是正数 反之最高位为1为负数

我一开始是直接设想去读out输出的最高位引脚 但是失败了 回显如下(不能将输出作为输入)

image-20260312002220356

然后我就想是不是可以通过将原先的输入进行套娃 先将out作为一个临时的输出定义为tempout 然后tempout再嵌套两层非门再输出到out 来实现这个功能
但是又出现了新的报错 大意是内部信号的总线信号只能整体访问 不能单独使用(

image-20260312002644470

那就代表这时候就得拆分这个信号了 也就是用之前做的芯片进行拆分 用Or8Way就挺好用(8输入或门 只要有一个输入为1,输出就为1)

这时候看到一个网上的方案 是先实现zr 然后ng自然就实现成功了 感觉很好用借鉴一下思路

因为外部接口没办法直接输入 所以还是要创建一个temp作为临时中转的效果

最后用Or门作为实现0和1的转换 用Mux门做一个简单的if判断即可

1
2
3
4
5
Or8Way(in= out07, out= tempzr1);
Or8Way(in= out815, out= tempzr2);
Or(a= tempzr1, b= tempzr2, out= tempzr);
Mux(a= true, b= false, sel= tempzr, out= zr);
Mux(a= false, b= true, sel= out15, out= ng);

最后验证一下 成功!

最后还是有改错 有个地方的Mux在选择上反了 然后截屏中没有改过来 但是代码块修改啦 所以文档中文字部分没问题!

image-20260312004341746

最后HDL文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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:
Mux16(a= x, b= false, sel= zx, out= x0);
Mux16(a= y, b= false, sel= zy, out= y0);

Not16(in= x0, out= notx0);
Not16(in= y0, out= noty0);
Mux16(a= x0, b= notx0, sel= nx, out= x1);
Mux16(a= y0, b= noty0, sel= ny, out= y1);

Add16(a= x1, b= y1, out= x1addy1);
And16(a= x1, b= y1, out= x1andy1);
Mux16(a= x1andy1, b= x1addy1, sel= f, out= foutput);

Not16(in= foutput, out= notfoutput);

Mux16(a= foutput, b= notfoutput, sel= no, out= tempout);
And16(a= tempout, b= true, out[15]= out15, out[0..7]=out07, out[8..15]=out815, out=out);

Or8Way(in= out07, out= tempzr1);
Or8Way(in= out815, out= tempzr2);
Or(a= tempzr1, b= tempzr2, out= tempzr);
Mux(a= true, b= false, sel= tempzr, out= zr);
Mux(a= false, b= true, sel= out15, out= ng);
}

最后交一下作业!发现完全没问题!

说实话 课程拖了太久了 好像要过期了 我得赶快做完了(

image-20260312004906045