美文网首页
编码:自动加法器(二)

编码:自动加法器(二)

作者: 汪小鱼 | 来源:发表于2021-10-27 07:28 被阅读0次

1 前言

  本文是基于《编码》、《穿越计算机的迷雾》两部著作进行读后整理的记录性博客。对书中较为重要的内容进行归纳整理进行二次创作,略去了繁琐的讲述细节,力求简明扼要。


编码:一种由若干符号和规则组成的系统,用来向计算机表述指令。

2 正文

2.1 累加器

  在前面的文章中已经介绍了自动加法器,并结合我们的需求进行构建,但就目前而言,我们构造的自动加法器还存在一些问题。本文我们接着上文的设备,对我们的设备进行优化。

  在之前的介绍中,我们每个操作码在存储器中占 1 个字节。现在除了 Halt 操作码外,我希望每一个指令在存储器中仅占据 3 个字节的空间,其中第一个字节为代码本身,另外的两个字节用来存放 1 个 16 位存储器单元地址。

例如我们的 Load 指令,后两个字节保存的地址用来指明数据 RAM 阵列的一个存储单元,该单元存放的是需要被加载到累加器中的字节。对于Add,Subtract,Add with Carry,Subtract with Borrow 指令来说,该地址指明的存储单元所保存的是要从累加器中加上或减去的字节。对于 Store 指令来说,该地址指明的是累加器中的内容将要保存到的存储单元地址。

  当加法器对两个数进行求和时,为了执行这个操作,需要按下面的方式设置代码 RAM 阵列和数据 RAM 阵列。

  在改进的自动加法器中,每条指令(除了 Halt 指令)需要 3 个字节。

  每一条指令的代码(除了 Halt 指令)后跟两个字节,用来指明数据 RAM 阵列中 16 位的存储地址。

  有了这种指令附带地址的设定,我们之前的存储方式就可以不用那么规整了。在前面的介绍中,我们针对计算分别保存低字节和高字节的计算结果。如下图所示,是我们之前的一个示例,76ABh + 232Ch,就计算时高低字节分开存储并计算。

  现在,我们可以采用一种更加合理的方式来保存这两个操作数及其运算结果,可能会把它们保存到我们从未用到过的存储区域。

  这 6 个存储单元不必像上图中这样全都连在一起,它们可以分散在整个 64 KB 数据 RAM 阵列的任意位置。为了把这些地址中的数相加,代码 RAM 阵列中的指令必须用以下方式设置。

  可以看到,保存在地址 4001h 和 4003h 处的两个低字节数先执行加法,其结果保存在 4005h 地址处。两个高字节数(分别保存在 4000h 和 4002h 处)通过 Add withCarry 指令相加,其结果保存在地址 4004h 处。如果去掉 Halt 指令并向代码 RAM 中加入更多指令,随后的计算可以通过引用地址很方便地使用原来的那些操作数及其结果。

  实现该设计的关键是把代码 RAM 阵列的数据输出到 3 个 8 位锁存器中。每个锁存器保存该 3 字节指令的一个字节。第一个锁存器保存指令代码本身,第二个锁存器保存地址的高字节,第三个锁存器保存地址的低字节。第二个和第三个锁存器的输出构成了数据 RAM 阵列的 16 位地址

  从存储器中取出指令的过程称为取指令(instruction fetch)。在我们设计的加法器中,每一条指令的长度是 3 个字节。因为每次从存储器取回一个字节,所以取每条指令需要的时间为 3 个时钟周期(对应三个锁存器的时钟信号)。此外,一个完整的指令周期需要 4 个时钟周期。这些变化必然使得控制信号更加复杂。机器响应指令码做一系列操作的过程称为执行(execute)指令

这里同前面文章介绍的类似,省略了对代码对电路的实际连接,实现这些并不复杂。这里为了突出重点,对部分内容进行了省略。其实,我们完全可以根据指令对应的操作码,设计出连接各个位置的电路结构。

  在前面构建设备的过程中我们使用了两种 RAM 阵列,一个用来存放指令码,另一个用来存放操作数据,这种设计使得我们设计的加法器的结构清晰。但现在我们使用 3 个字节长的指令格式,而且第二个和第三个字节用来指明了操作数的存储地址,因此就没有必要再使用两个独立的 RAM 阵列。操作码和操作数可以存放在同一个 RAM 阵列。

  为了实现这个设计,我们需要一个 2-1 选择器来确定如何对 RAM 阵列寻址。通常,和前面的方式相同,我们用一个 16 位的计数器来计算地址。数据 RAM 阵列的输出仍然连接到 3 个锁存器,分别用来保存指令代码及其对应操作数的 16 位地址,其 16 位的地址输出是 2-1 选择器的第二种输入。地址被锁存后,可以通过选择器将其作为 RAM 阵列的地址输入。

通常,指令从 0000h 开始存放,这是因为当计数器复位后从该位置访问 RAM 阵列。最后的 Halt 指令存放在 000Ch 地址。我们可以把这 3 个操作数及它们的运算结果保存在RAM 阵列的任何地址。下图演示了如何把两个 8 位数相加,然后从结果中再减去一个 8 位数。


  假设在使用的过程中,我们发现需要在原来的结果中再加两个数,你可以向存储器中输入一些新的指令以替代原有的指令,我们也可以选择在原指令的地址后增加一些新的指令。

  第一步要做的就是把 000Ch 地址处的 Halt 指令替换为一个 Load 指令。但你仍然需要增加两条 Add 指令,一条 Store 指令,以及一条新的 Halt 指令。唯一的问题是,现在 0010h 地址已经保存了一些数据,因此需要把这些数据转移到较高的地址空间中,然后还需要修改那些指向这些地址空间的指令。

在当前的例子中,我们选择从 0020h 地址开始存放新的指令,并从 0030h 处开始存放新的操作数据。



注意,第一条 Load 指令所指向的地址为 0013h,这个位置保存着第一次运算的结果。
现在,两部分指令的位置分别起始于地址 0000h 和 0020h,而两部分操作数据的地址分别起始于 0010h 和 0030h。我们希望自动加法器从 0000h 开始执行所有指令完成计算任务。

在上述的地址存放序列中,我们需要移除 000Ch 处的 Halt(停止)指令,这里的移除指的是用其他代码替换它。但仅仅是替换是不够的,它可能导致一些问题:不论我们用什么来替换 Halt 指令,保存在地址 000Ch 的字节都会被当做指令代码。而且从这个位置开始,每隔 3 个字节的地址都会被当做指令代码进行处理。

  针对上述问题,我们选择使用一个新的指令 Jump(跳转)来替代 Halt 指令,如下指令表所示:

  通常情况下自动加法器是以顺序方式对 RAM 阵列寻址的。Jump 指令改变了机器的这种寻址方式,取而代之的是从某个指定的地址开始寻址。这种指令有时也被称作分支(branch)指令或者 Goto 指令,即 “转到另一个位置”。

  在上面的例子中,我们可以用一个 Jump 指令来替换 000Ch 地址处的 Halt 指令。

因此在上面的例子中,自动加法器仍然从 0000h 地址开始,依次执行一条 Load 指令,一条 Add 指令,一条 Subtract 指令和一条 Store 指令。之后执行一条 Jump 指令,跳转至地址 0020h 继续依次执行一条 Load 指令,两条 Add 指令,一条 Store 指令,最后执行一条 Halt 指令。

  Jump 指令通过作用于 16 位计数器实现其功能。无论何时,只要自动加法器遇到 Jump 指令,计数器就会被强制输出该 Jump 指令后的 16 位地址。这可以通过 16 位计数器的 D 型边沿触发器的预置(Pre)和清零(Clr)输入来实现。

在正常的操作下,Pre 和 Clr 端的输入都应该是 0。但是,当 Pre = 1,Q = 1;当 Clr = 1,则 Q = 0。

如果你希望向一个触发器加载一个新的值(用 A 表示,代表地址),可以像下图所示这样连接。


通常,置位信号为 0。此时,触发器的预置端输入为 0,在复位信号不为 1 的情况下,清零信号也为 0。在这种情况下,触发器就可以独立清零,而不受置位信号的影响。当置位信号为 1 时,如果 A 为 1,则清零(Clr)输入为 0,预置(Pre)输入为 1;如果 A 为 0,则预置(Pre)输入 0,清零(Clr)输入为 1。这就意味着 Q 端将被设置为与 A 端相同的值(需要注意,和前面一样,这里的置位和复位不能同时为 1)。将 Q 端置为与 A 端相同的值,能够使得我们的计数器从我们传入的数值位开始计数,因为计数器随着振荡器的交替信号逐渐递增。

  我们需要为 16 位计数器的每一位设置一个这样的触发器。一旦加载了某个特定的值,计数器就会从该值开始计数。

  这对我们之前设计的电路改动并不大,从 RAM 阵列锁存得到的 16 位地址既可以作为 2-1 选择器(它允许该地址作为 RAM 阵列的地址输入)的输入,也可以作为 16 位计数器置位信号的输入。

其实到这里我们会发现整个电路体系省略了较多的具体细节,比如我们知道某些输出可以作为某个结构的输入,但这些具体如何去实现呢?这里只能大致了解其工作原理,不必深究。

  显然,只有当指令代码为 30h(Jump) 并且其后的 16 位地址被锁存时,我们才必须确保置位信号为 1。这样才能保证跳转功能的实现,根据指令后接的地址传入计数器和存储器(RAM 阵列),而计数器作为存储器(RAM 阵列)的地址输入,可以实现从给定位置开始后续的指令的操作。

  Jump 指令的确很有用。但与之相比,一个在我们想要的情况下跳转的指令更加有用,这种指令称做条件跳转(Conditional Jump)。

  这里我们通过一个例子来引出相应的内容:怎样让自动加法器进行两个 8 位数的乘法运算?例如,我们如何利用自动加法器得到像 A7h(167)与 1Ch(28)相乘这种简单运算的结果呢?

乘法运算可以转换为加法,例如 4 × 3 = 4 + 4 + 4。除法也可以进行转换,例如 13 ÷ 4 即为 13 - 4 = 9 -》9 - 4 = 5 -》5 - 4 = 1,那么可知 13 ÷ 4 = 3···1

  我们首先确定要把乘数和乘积存放在什么地址。为了方便起见,把示例中涉及的 3 个数均表示为 16 位数。(1004h 和 1005h 分别存储高低字节的计算结果)

  同样我们需要将乘法转换为加法,因此上述计算即为把 28 个 A7h 累加,下图演示了如何把 A7h 累加:

这里需要注意的是:这里的计算过程是和前面一样分高低字节进行的。因此会有 1005h+1001h,1004h+1000h。

  当上述指令执行完成之后,存储器 1004h 和 1005h 地址保存的 16 位数与 A7h 乘以 1 的结果相同,按照我们之前的想法需要把这 6 条指令连续输入 27 次。对于此,我们可以选择把这些指令连续输入,也可以在 0012h 处保存一个 Halt 指令,然后将复位键连续按 28 次得到最终结果。

  上述介绍的两种方式都存在将枯燥的过程重复多次执行,如果使用我们的 Jump 指令,这个指令将使得计数器再次从 0000h 处开始计数。

  第一次执行完指令之后,位于存储器的 1004h 和 1005h 地址的 16 位数等于 A7h 乘 1,然后 Jump 指令使下一条指令从存储器顶部开始执行。第二次执行指令后,该 16 位数等于 A7h 乘 2,最后其结果可以等于 A7h 乘 1Ch。但是,这个过程不会停止下来,它会一直反复执行下去。

  我们需要的是这样一种 Jump 指令,它只让这个过程重复执行所需要的次数,这种指令就是条件跳转指令。要实现它,要做的第一步是增加一个与进位锁存器类似的 1 位锁存器。该锁存器被称为零锁存器(Zero latch),这是因为只有当 8 位加法器的输出全部为 0 时,它锁存的值才是 1。

这里的进位锁存器即为我们在介绍用 8 位累加器计算 16 位二进制数的计算时,将 16 位数分成高低字节分别计算,并利用一个锁存器将低字节的进位输出保存作为高字节的进位输入。

  使或非门的输出为 1 的唯一方法是其所有的输入全为 0。与进位锁存器的时钟输入一样,只有当 Add、Subtract、Add with Carry、Subtract with Borrow 这些指令执行时,零锁存器才锁存 1 个数,该数称做零标志位(Zero flag)。注意,它是以一种似乎是相反的方式工作的:当加法器的输出全为 0 时,零标志位等于 1;当加法器的输出不全为 0 时,零标志位等于 0。

  有了进位锁存器和零锁存器以后,我们可以为指令表新增 4 条指令。

  例如,非零转移指令(Jump If Not Zero)只有在零锁存器的输出为 0 时才会跳转到指定的地址。换言之,如果上一步的加法、减法、进位加法、或者借位减法等运算的结果为 0 时,将不会发生跳转。为了实现这个设计,只需要在常规跳转命令的控制信号之上再加一个控制信号:如果指令是 Jump If Not Zero,那么只有当零标志位是 0 时,16 位计数器才被触发。

  我们接着上面的乘法的实现,补充相应的指令。下图中 0012h 地址之后的指令即两个数相乘所用到的上表中的所有指令。

  第一次循环之后,位于地址 0004h 和 0005h 处的 16 位数等于 A7h 与 1 的乘积。在上图中,地址 1003h 处的字节通过 Load 指令载入到累加器,该字节是 1Ch。把这个数和 001Eh 地址的字节相加后。FFh 与 1Ch 相加的结果与从 1Ch 中减去 1 的结果相同,都是 1Bh,因为这个数不等于 0,所以零标志位是 0,1Bh 这个结果会存回到 1003h 地址。下一条要执行的指令是 Jump If Not Zero,零标志位没有置为 1,因此发生跳转。接下来要执行的一条指令位于 0000h 地址。

FFh 与 1Ch 相加的结果与从 1Ch 中减去 1 的结果相同:因为我们这里的计算存在进位,但我们会对计算结果的进位舍弃,保存的是进位后的数。以 53 + 99 为例:53 + 99 = 152,舍弃百分位得到结果 52,也即 53 + 99 - 100 = 52,相当于减去 1。而在 16 进制中,FFh 就相当于十进制中的 99,也即在减法实现部分我们介绍的补数,单个位置能取到的最大数。只要加数(53)大于等于 1,都能实现计算结果减 1 的效果。

  需要记住的是,Store 指令不会影响零标志位的值。只有 Add、Subtract、Addwith Carry、Subtract with Borrow 这些指令才能影响零标志位的值,因此它的值与最近执行上述某个指令时所设置的值相同。经过两次循环后,1004h 和 1005h 地址所保存的 16 位数等于 A7h 与 2 的乘积。1Bh 与 FFh 的和等于 1Ah,不为 0,因此仍然返回到顶部执行。

不难发现,这里的控制次数是通过每次相加后,1003h 位的被乘数减 1。

  当执行到第 28 次循环时,1004h 和 1005h 地址保存的 16 位数等于 A7h 和 1Ch 的乘积。1003h 地址保存的值是 1,它和 FFh 相加的结果是 0,因此零标志位被置位。Jump If Not Zero 指令不会再跳转,相反,下一条要执行的指令即 Halt 指令。这样,我们就完成了全部的工作。

  通过上面的一步步地优化我们得到了一个计算机!条件跳转指令将它与我们以往设计的加法器区别开来,能否控制重复操作或者循环是计算机和计算器的区别。当然,上述介绍了的仅仅为一个演示,我们可以对其进行进一步优化,使其能够胜任更加复杂的运算(例如除法、开平方根、取对数、三角函数等)。

  我们装配的计算机属于数字计算机(digital computer),因为它只处理离散数据(曾经还有一种模拟信号计算机(analog computer),但现在已经很少见了)。一台数字计算机主要由 4 部分构成:处理器(processor)、存储器(memory),至少一个输入(input)设备和一个输出(output)设备。我们装配的计算机中,存储器是 64 KB 的 RAM 阵列,输入和输出设备分别是 RAM 阵列控制面板上的开关和灯泡。这些开关和灯泡可以让我们向存储器中输入数据,并可以检查运算结果。

  在我们所设计的计算机中,8 位反相器和 8 位加法器一起构成了算术逻辑单元(Arithmetic Logic Unit),即 ALU。该 ALU 只能进行算术运算,最主要的是加法和减法运算。在更加复杂的计算机中(我们会在后面的章节看到),ALU 还可以进行逻辑运算。

  我们的计算机是由继电器、电线、开关,以及灯泡构造而成的,这些东西都叫做硬件(hardware)。与之对应,输入到存储器中的指令和数值被称做软件(software)。在计算机领域,“软件” 与 “计算机程序” 或 “程序” 等术语是同义的,我们确定用一些指令让计算机实现两个数相乘的过程就是在进行计算机程序设计。计算机程序设计有时也被称做编写代码(writing code),或编码(coding)。

  能够被处理器响应的操作码(比如 Load 指令和 Store 指令的代码 10h 和 11h),称做机器码(machine codes),或机器语言(machine language)。

回顾我们第一篇文章中说到的我们在找寻一种能够与计算机 “交流” 的方式,一种双方都能理解的方式。计算机能够理解和响应机器码,其原理和人类能够读写语言是类似的,是我们和计算机进行沟通的一种特殊的 “语言”。

  一直以来,我们都在使用很长的短语来引用机器所执行的指令,比如 Add withCarry 指令。通常而言,机器码都分配了对应的简短助记符,这些助记符都用大写字母表示,包括 2 个或 3 个字符。下面是一系列上述计算机大致能够识别的机器码的助记符。

  当这些助记符与另外一对短语结合使用时,其作用更加突出。例如,对于这样一条长语句 “把 1003h 地址处的字节加载到累加器”,我们可以用如下简洁的句子替代:

LOD  A,  [1003h]

  位于助记符右侧的 A 和 [1003h] 称为参数(argument),它们是这个 Load 指令的操作对象。参数由两部分组成,左边的操作数称为目标(destination)操作数(A 代表累加器),右边的操作数称为源(source)操作数。方括号 “[]” 表明要加载到累加器的不是 1003h 这个数值,而是位于存储器地址 1003h 的数值。

  类似的,指令 “把 001Eh 地址的字节加到累加器”:

ADD A,  [0010Eh]

“如果零标志位不是 1 则跳转到 0000h 地址处” 这个冗长的语句可以简明地表示为:

JNZ  0000h

  注意,这里没有使用方括号,这是因为跳转指令要转移到的地址是 0000h,而不是保存于 0000h 地址的值,即 0000h 地址就是跳转指令的操作数。

在编码时最好不要使用实际的数字地址,因为它们是可变的。例如,如果要把数值保存在存储器的 2000h~2005h 地址空间中,需要在程序中重复写这些语句。可以选择用标号(label)来指代存储器中的地址空间。

  有语言基础的朋友可能已经发现了,这里给出的是一种计算机程序设计语言,称为汇编语言(assembly language)。它是全数字的机器语言和指令的文字描述的一种结合体。同时它用标号表示存储器地址。这里就不针对这一门语言的内容进行展开,了解即可。


3 小结

  编码:自动加法器(二)篇针对前面设计自动加法器进行改进,实现了更加复杂的功能,并借此引出了指令和语言的相关概念。为了精简内容删减了部分较为详细的书写,仅作为整理总结。

相关文章

网友评论

      本文标题:编码:自动加法器(二)

      本文链接:https://www.haomeiwen.com/subject/esuealtx.html