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

来构建一个属于自己的ALU!Nand2Tetris(计算机系统要素) Unit 2 学习笔记
Xiaozhi_z起初是在CS自学指南里找到的这门课(依据基本原理构建现代计算机:从与非门到俄罗斯方块),感觉还不错而且零基础,是一个比较好的入门课程。同时全英教学可以逆向让我捉襟见肘的英语水平稍微强一点,多理解一个单词就是胜利(
Unit 2.1 Binary Numbers(二进制数)
二进制就是两个数进一位 所以只有 0 和 1 两个数字
但是只有0和1能做什么东西呢?

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

二进制也可以用来表示十进制 这个一带而过就好啦
这节课就是讲二进制数的转换 没别的东西 一带而过了 这个太熟悉了

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

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

但是这终究不是计算机做的事情 这只是因为我们比较擅长十进制加法从而解决的方法
计算机不通过十进制中转,而是直接进行二进制加法,其原理与我们小学学的竖式加法完全相同,只是基数从10变成了2。
正常十进制的加法 在同位满十进一

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

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

从位操作到完整加法器
计算机加法的硬件实现分为三个关键阶段,从最简单的位操作开始,逐步构建完整的加法系统。
做一个简单的示例 我们是如何取1+1 得到0和1的进位的

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

我们可以发现将两个输入定义为a和b 结果就会得到a和b的和以及a与b的进位 如下图所示 这也就成功设计出来半加器了
**半加器(Half Adder)**它处理最简单的情况——两个位相加,不考虑前级进位。

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

这时候就实现了全加器 由两个相加的数a和b以及前一位的进位c 得到和以及进位
全加器(Full Adder)。现实中的加法需要考虑前一级的进位,全加器正是为此设计。它有3个输入(a, b, c_in)和2个输出(sum, c_out),能够处理所有8种可能的输入组合。

Unit 2.3 Negative Numbers(负数)
目前只讨论过如何使用正数,可是计算机肯定也要进行负数运算,这是我数电课都没学过的知识目前(
举一个例子,这个是四位二进制数进行穷举排列,可以得到从0到15的十进制正整数。

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

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

补码(Two’s Complement)
因为要明确处理加减号等诸多不方便的因素这个方式还是被弃用了 后来用了一个叫**补码(Two’s Complement)**的东西:用 2^N 减去 |x| 来表示负数 -x
总共有 16 个数(0~15 的二进制)。
我们把大于等于 8 的数(8~15)重新解释为负数:
-1用15(即 16 - 1)表示 →1111-3用13(即 16 - 3)表示 →1101-8用8(即 16 - 8)表示 →1000
这样就可以很好的解决掉-0的问题 而且还延申出了一个概念就是补码 与之相对的刚刚是原码

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

Unit 2.4 Arithmetic Logic Unit(算术逻辑单元)
ALU 的基本概念
冯诺依曼写的一篇论文中阐述了计算机是怎么构造的 也叫冯诺依曼架构 如下图表

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

抽象的一个比喻是 它可以接受多Bit的输入 例如Input1和Input2 第三个输入为计算的函数 输出为ALU计算的结果
ALU通常执行常见计算 例如整数加法 乘法 除法等等

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

输入与输出:
- 两个 16 位数据输入:
x和y。 - 一个 16 位数据输出:
out。 - 六个控制位(
zx,nx,zy,ny,f,no),用于指定要执行的操作。 - 两个 1 位状态输出:
zr(结果为零)和ng(结果为负)。
六个控制位共组合出来18个不同的运算(其实可以做更多 但重要的就这些) 真值表如下
如果ALU没有实现乘除法,后续可以通过软件层来补充这个功能。这是理解计算机系统分层设计的关键。

ALU 支持 18 种不同的运算,包括:
- 常数(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)
The Hack ALU 内部逻辑
现在了解ALU的接口和简单的功能了 但是内部的设计对于我们来讲还算是一个黑盒 只知道怎么用 不知道怎么实现

ALU 六个控制位的具体含义

控制位的作用:
zx(零 x):如果为 1,则将x设置为 0。(相当于置0)nx(非 x):如果为 1,则将x按位取反。zy(零 y):如果为 1,则将y设置为 0。(相当于置0)ny(非 y):如果为 1,则将y按位取反。f(功能选择):如果为 1,则计算x + y(加法);如果为 0,则计算x & y(按位与)。no(非输出):如果为 1,则将最终结果按位取反。
控制位的顺序性:
- zx/zy 先执行
- 先根据
zx决定是否将 x 置为 0 - 先根据
zy决定是否将 y 置为 0
- 先根据
- nx/ny 后执行
- 再根据
nx决定是否将(已处理的)x 取反 - 再根据
ny决定是否将(已处理的)y 取反
- 再根据
- f 执行
- 根据
f选择做加法(x + y)或按位与(x & y)
- 根据
- no 最后执行
- 根据
no决定是否将最终结果取反
- 根据
如何通过控制位选择运算
- 通过设置六个控制位的二进制值,可以指定 ALU 执行特定的运算。
- 示例:
- 计算
!x(按位取反 x):控制位为001100,ALU 会先处理x(保持不变),将y置零并取反,然后计算x & y,最后对结果取反,得到!x。 - 计算
y - x:控制位为000111,ALU 会先对y取反,然后计算x + (-y),最后取反,实际上实现了减法。
- 计算
状态输出 zr 和 ng
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的输出分别等同于你已经构建过的两个逻辑门(异或门/与门)的输出。

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

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

增量器(Incrementor)
- 功能: 这是一个简化的加法器,功能是将输入的16位数值加1。
- 实现提示: 可以使用你已构建的加法器芯片来实现。在HDL中,可以使用关键字
false和true来表示单比特的0和1。

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

Programming Assignment: Project 2(编程作业: 项目 2)
1 | 准备提交时,请将您编写的所有 *.hdl 文件打包成一个名为 project2.zip 的压缩文件(打包文件本身,不要放在任何子文件夹内),然后提交。您提交的次数不限,成绩将是您所有提交成绩的最大值,因此您不会因为再次提交而丢分。 |
需要构建五个芯片
- HalfAdder(半加器)- 计算两个位的和与进位
- FullAdder(全加器)- 计算三个位的和与进位
- Add16(16位加法器)- 对两个16位数进行加法运算
- Inc16(16位增量器)- 对16位数加1
- ALU(算术逻辑单元)- Hack计算机的核心运算单元
HalfAdder(半加器)

这里可以看到有a和b的输入 还有sum(和)和carry(进位)的输出
我们聚焦到输出就可以 carry使用一个与门链接 sum使用一个Xor门链接
如果没有思路 回顾一下上一个单元做的And门和Xor门真值表就好啦


最终写出来的hdl代码如下
1 | CHIP HalfAdder { |
验证电路 发现真值表成功对应

FullAdder(全加器)

一开始我的思路是关注中心在于输出与输入的关系
先专注一下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 | ┌─────────────┐ |
然后看一下Carry 也就是进位 其实可以观察到只需要三个值大于等于两个为1就可以 类似于三人举手表决电路
这时候看空下来的接口 第一个半加器的Carry的效果为A & B 第二个半加器的Carry 相当于(A⊕B)& C
化成最小项表达式正好符合真值表 如果将这两个Carry 用 或门连接 就相当于实现了全加器 最后hdl文件如下
1 | CHIP FullAdder { |
测试成功!全加器也设计成功啦!!!
16位加法器(16-bit Adder)

这个我感觉类似于总线的概念 用十六个全加器应该就可以实现 将carry的输出作为下一位的输入即可 一开始的为false 大概开头如下
1 | CHIP Add16 { |
然后一直循环去写 就能实现十六位加法器
1 | CHIP Add16 { |
跑完发现没问题!完事!

增量器(Incrementor)

用十六位加法器实现就可以 这里的关键是 设置最低为1为是true 剩余的为0是false hdl写法是这样的
1 | CHIP Inc16 { |
测试一下!过辣

算术逻辑单元(ALU)

这是应该目前最难的项目了 要手搓一个这么复杂的 支持如此多功能的芯片
这里要用到之前课中的一些概念
控制位的作用:
zx(零 x):如果为 1,则将x设置为 0。(相当于置0)nx(非 x):如果为 1,则将x按位取反。zy(零 y):如果为 1,则将y设置为 0。(相当于置0)ny(非 y):如果为 1,则将y按位取反。f(功能选择):如果为 1,则计算x + y(加法);如果为 0,则计算x & y(按位与)。no(非输出):如果为 1,则将最终结果按位取反。
控制位的顺序性:
- zx/zy 先执行
- 先根据
zx决定是否将 x 置为 0 - 先根据
zy决定是否将 y 置为 0
- 先根据
- nx/ny 后执行
- 再根据
nx决定是否将(已处理的)x 取反 - 再根据
ny决定是否将(已处理的)y 取反
- 再根据
- f 执行
- 根据
f选择做加法(x + y)或按位与(x & y)
- 根据
- no 最后执行
- 根据
no决定是否将最终结果取反
- 根据
处理输入逻辑
我的思路是根据这个芯片的顺序性来一点一点去设计例如如果zx或zy为1 用一个与门连接 直接置零就ok啦 但后来发现有更好用的芯片 就是Mux 数据选择器 可以实现多路的数据选择(其实在这里有点类似于 if语句的感觉 如果为1就同行这个 不为1就通行那个)
1 | Mux16(a= x, b= false, sel= zx, out= x0); |
然后进行第二步操作即可 用一个Not16的门 如果为1就取反 不为1就正常下一步
用Not门加Mux门即可实现 实现的思路和刚刚基本一致
1 | Not16(in= x0, out= notx0); |
现在基本了解逻辑之后就很简单了 这个f无非就是先做好对应的与和加芯片连起来 然后最后用Mux作为If判断 就可以实现
1 | Add16(a= x1, b= y1, out= x1addy1); |
然后然后最后一样的逻辑进行Not取反即可
1 | Not16(in= foutput, out= notfoutput); |
最后我们再实现两个输出接口即可!
状态输出的配置
状态输出 zr 和 ng
zr:如果计算结果为 0,则zr = 1;否则为 0。ng:如果计算结果为负数,则ng = 1;否则为 0。
看起来第二个接口ng好实现一点 有一个原码补码反码的概念
最高位如果是0就是正数 反之最高位为1为负数
我一开始是直接设想去读out输出的最高位引脚 但是失败了 回显如下(不能将输出作为输入)

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

那就代表这时候就得拆分这个信号了 也就是用之前做的芯片进行拆分 用Or8Way就挺好用(8输入或门 只要有一个输入为1,输出就为1)
这时候看到一个网上的方案 是先实现zr 然后ng自然就实现成功了 感觉很好用借鉴一下思路
因为外部接口没办法直接输入 所以还是要创建一个temp作为临时中转的效果
最后用Or门作为实现0和1的转换 用Mux门做一个简单的if判断即可
1 | Or8Way(in= out07, out= tempzr1); |
最后验证一下 成功!
最后还是有改错 有个地方的Mux在选择上反了 然后截屏中没有改过来 但是代码块修改啦 所以文档中文字部分没问题!

最后HDL文件如下
1 | CHIP ALU { |
最后交一下作业!发现完全没问题!
说实话 课程拖了太久了 好像要过期了 我得赶快做完了(

