自动操作
我们要设计一个更加先进的机器,目标是使加减法的过程自动化。同时这个机器用途非常广泛,因为它可以解决所有用加减法处理的问题,而事实上现实中的很多问题都可以用加法与减法解决。
本章的末尾我们将会设计出一种机器,我们可以称它为计算机(Computer)。
电平触发与边沿触发
回忆我们曾在第14章讨论过的一个加法器。这个版本的加法器包括一个8位锁存器,用于对8个开关的输入数据进行迭代求和。下面是其结构图。
从图中可以看出,8位锁存器利用触发器来保存8位数据。使用这个设备时,首先需要按下清零开关使锁存器中的内容全部都变为0,然后通过开关输入第一个数。加法器只是简单地将这个数字和锁存器输出的0进行求和,因此相加的结果与原先输入的数字是一样的。按下相加开关可以把这个数保存在锁存器中,最后会点亮某些灯泡以显示它。现在通过开关输入第二个数,加法器把它与已经存放在锁存器中的第一个数相加。再次按下相加开关,九可以把相加的结果存入锁存器中,并通过灯泡显示这个结果。通过这种方式,可以把一串数相加并显示运行结果。显然,这种设计方案存在一个缺陷:8个灯泡无法显示大于255的数。
对于第14章所介绍的这种电路,目前为止只讲到了一种锁存器,它是电平触发(level triggered)的。在电平触发的锁存器中,为了保存数据必须将时钟输入端首先置1,然后回置为0.当时钟输入端为1时,锁存器的数据输入端可以改变,而这些变化将会影响数据输出。在第14章的后半部分还介绍了边沿触发(edge-triggered)的锁存器,这种锁存器在时钟输入从0跳变为1的瞬间保存数据。边沿触发器在很多方面更加易于使用。因此假定本站用到的所有触发器都是边沿触发的。
累加器与64x8 RAM组合
用来累加多个数的锁存器称为累加器(accumulator).
很显然,上面的加法器存在着一个很大的缺陷:假如要把100个二进制数加起来,你必须端坐于加法器前,并且耐心地输入所有的数并累加起来。如果在这段过程中有两个数输错了,你只能重复一遍所有的工作。
但是,也许并非如此。在前一章我们使用了大约500万个继电器构造了一个64KB的RAM阵列。除此之外,我们还把一个控制面板连接到电路帮助我们工作,闭合它的控制(Takeover)端开关后,就可以使用其他开关来控制RAM阵列的读写了。下面是64KB RAM阵列结构图。
现在我们改进累加器,让它不仅仅做简单的累加,还能充当锁存器的角色,保存一个数,并且和下一个数做加法或减法运算。
如果我们把这100个二进制数输入到RAM阵列中而不是直接输入到加法器中,一旦需要修改一些数据,我们的工作将会变得容易得多。
因此我们现在所面临的挑战是如何把RAM阵列和累加器连接起来。很显然,RAM阵列的输出信号可以代替加法器的开关。16位的计数器就可以控制RAM阵列的地址信号。这个电路图中,RAM的数据输入信号和写操作端信号省去。修改后的电路结构如下图所示。
当然,这并不是迄今发明的最易于使用的计算设备。要使用它,首先要闭合清零开关,这样做的目的是,清除锁存器中的内容并把16位计数器的输出置为0000h,然后闭合RAM控制面板的控制端开关。现在你就可以从地址0000h开始输入一组你想要相加的8位数。如果有100个数,那么它们将被存放在0000h~0063h的地址空间中(也应该把RAM阵列中未使用的单元设置为00h).然后闭合RAM控制面板的控制开关(这样控制面板就不再控制RAM阵列了),同时断开清零开关。做完了这些,我们可以静静地坐下来,观察灯泡显示运算结果。
让我们来看一下它是如何工作的。
当清零开关第一次断开时,RAM阵列的地址输入是0000h。RAM阵列的该地址中存放的8位数值是加法器的输入数据。加法器的另一个输入数据为00h,因为此时锁存器也已经清零了振荡器提供的时钟信号——一个可以在0,1之间快速切换的信号。清零开关断开后,时钟信号由0跳变为1时,将有两件事同时发生: 锁存器保存加法器的计算结果,同时16位计数器增1,指向下一个地址单元。当时钟发生第二次跳变时,锁存器保存之前两个数的求和结果,同时计数器增加为0002h;按这种方式往复操作。
要注意的是,这里首先做了一些假设。最主要的一点就是,振荡器要足够慢以使电路的其他部分可以工作。
这个电路存在的一个缺陷是:我们没有办法使它停下来。当RAM阵列的剩余部分存放的数都是00h时,灯泡停止发光,我们才可以读取二进制运算的结果。
这个加法器还存在一个问题:它只能做加法运算,并且只能做8位数的加法。尽管可以用2的补数表示负数,但是加法器能处理的数字范围被限制在-128到127之间。要处理更大的数,比如16位数,一个简单的方法是:把RAM阵列,加法器,锁存器的位宽全部加倍,同时增加8个灯泡。但这些投资在我们看来是不合算的。
可分别进行多对数相加和存储的累加器
当然,这里提到的这些问题最终我们要解决它。但首先我们来关注另一个问题:如果不是把100个数加在一起,而是把50对数分别相加,得到50个不同的结果呢?
我们需要设计一种万能机,它可以方便地对两个数,10个数甚至100个数求和,并且所有的计算结果都可以很方便地使用。
先前的自动加法器是将灯泡连接到锁存器中显示运算结果的,但是如果你想对50个数分别求和的话,这就不是一个好的方法了。我们需要把运算结果存回RAM中阵列中去,这样的话,就可以在适当的时候用RAM阵列的控制面板来检查运算结果。为了实现这个思路,在控制面板上专门设计了灯泡。
这意味着我们可以去掉与锁存器连接的灯泡,取而代之的是把锁存器的输出端连接到RAM阵列的数据输入端,这样可以把计算结果写回到RAM阵列中去。如下图所示。
上面略去了自动加法器的其他部分,其中包括振荡器和清零开关,这是因为我们不再需要特别标注计数器和锁存器的清零及时钟输入。此外,既然我们现在已经利用RAM的数据输入,因此需要一种用来控制RAM写入信号的方法。
现在我们不需要担心电路是否工作,而要把注意力集中到急需解决的问题上来。目前的当务之急是如何配置一个自动加法器,使它不仅仅可以对一组数字做累加运算,还能希望它能够自主地确定要累加多少个数字,而且还能记住在RAM中存放了多少个计算结果,这样就可以简化查询工作。
例如,假设我们先要对三个数进行求和,然后对两个数进行求和,最后再对三个数进行求和。想象一下,我们可以把这些数保存在RAM阵列中以0000h开始的一组空间中,这些数存储在RAM阵列中的具体形式如下图所示。
本书中将用这样的形式表示一小段存储器。方格表示的是存储器的内容。存储器的每一个字节写在一个方格里。地址标记在方格的左边,并且是线性排布。方格右边是关于该存储单元内容的注释,这些标记的单元就是我们想要自动加法器保存三个计算结果的位置(尽管这些单元画出来是空的,但是存储单元内并不是空的,他们总是保存着一些随机数).
我们希望这个版本的加法器更加强大,能做这四件事:进行加法操作,首先把一个字节从存储器中传送到累加器中,这个操作称为加载(Load).第二个操作把存储器中的一个字节(Add)到累加器的内容中去。第三个操作把累加器中的计算结果取出并放(Save)到存储器中。另外我们需要用一个方法令自动加法停(Halt)下来。
我们借助具体的例子详细介绍这一过程,以上文提到的自动加法器所做的运算为例说明。
- 把0000h地址处的内容加载到锁存器。
- 把0001h地址处的内容加到加法器中。
- 把0002h地址处的内容加到加法器中。
- 把锁存器中的内容存储到0003h地址处。
- 把0004h地址处的内容加载到锁存器。
- 把0005h地址处的内容加到加法器中。
- 把锁存器中的内容存储到0006h地址处。
- 把0007h地址处的内容加载到锁存器。
- 把0008h地址处的内容加到加法器中。
- 把0009h地址处的内容加到加法器中。
- 把锁存器中的内容存储到000Ah地址处。
- 令自动加法器停止工作。
这里要注意,同最初的自动加法器一样,存储器中的每一个字节的地址仍然是以0000h为起点线性排列的。最初的加法器只是简单地把存储器指定地址的内容和累加器中的内容相加。但是这个新的加法器可以把存储器中的某个值直接加载到累加器,或者把累加器中的值直接保存到存储器,而且还能方便地让自动加法器停下来,以便于查看RAM阵列中存放的值。
该如何来完成这个工作呢?能不能仅仅简单地向RAM阵列中输入一组数,然后期待自动加法器正确地完成所有工作呢?答案是否定的。对于RAM阵列中的每一个数,我们还需要用一些数字代码来标示加法器要做的每一个工作:加载,相加,保存和终止。
也许存放这些代码的最简单的方法是把它们存放在一个独立的RAM阵列中。这个RAM应该和第一个RAM同时被访问。但是这个RAM中存放的是不需要求和的数,而是一些数字代码,用来标记自动加法器对第一个RAM中指定地址要做的一种操作。这个RAM可以分别被标记为“数据”(第一个RAM阵列)和“代码”(第二个RAM阵列)。其结构如下图所示。
我们已经清楚地认识到新的自动加法器能够把数据求和的结果写入到第一个RAM阵列(标记为“数据”),而新的RAM阵列(标记为“代码”)则只能通过控制面板写入。
我们需要四个代码来标记新的自动加法器需要做的四个操作,这些代码可以任意指定。如下所示的是一种方案。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Halt | FFh |
为了使上面讨论的三组加法得以正常执行,你需要通过控制面板把如下值存入代码RAM阵列。
比较一下该RAM阵列与存放累加数据的RAM阵列中的内容,你会发现,代码RAM阵列中存放的每一个代码都对应着数据RAM中要被加载或者加到累加器中的数,或者对应需要存回到数据RAM中的某个数。以这种方式使用的数字代码常常被称为指令码(instruction code)或操作码(operation code, opcode).它们指示电路要执行的某种操作。
如前所述,最初的自动加法器的8位锁存器的输出要作为数据RAM阵列的输入,这就是Save指令的功能。还需要做另一个改变:以前8位加法器的输出是8位锁存器的输入,但现在为了执行Load指令,数据RAM阵列的输出有时也要作为8位锁存器的输入,这种新的变化需要一个2-1选择器来实现。改进后的自动加法器如下图所示。
图中略去了一些组件,但是仍然清晰地描述了各个组件之间的8位数据通路。16位的计数器为两个RAM阵列提供地址输入。通常,数据RAM阵列的输出传入到8位加法器执行加操作。8位锁存器的输入可以是数据RAM阵列的输出(当执行Load执行时),也可以是加法器的输出(当执行Add执行时),这种情况就需要2-1选择器。通常, 锁存器电路的输出又流回到加法器中,但是当执行Save指令时,它就成为了数据RAM阵列的输入数据。
上图缺少的是控制所有这些组件的信号,它们统称为控制信号,包括16位计数器的“时钟”输入和“清零”输入,8位锁存器的“时钟”输入和“清零”输入,数据RAM阵列的“写”(W)输入,2-1选择器的“选择”(S)输入。其中的一些信号很明显是基于代码RAM阵列的输出,例如,如果代码RAM阵列输出是Load指令,那么2-1选择器的“选择”输入必须是0(即选择数据RAM的输出)。只有当操作码是指令Store时,数据RAM阵列的“写”(W)输入必须是1.这些控制信号可以通过逻辑门的各种组合来实现。
添加减法功能
利用最少的附加硬件和一些新增的操作码,可以让这个电路从累加器中减去一个数。第一步是向操作码表增加一些代码。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Subtract | 21h |
Halt | FFh |
对于Add和Subtract的代码,其区别仅在于最低有效位,我们称该位为C0.如果操作码为21h,数据RAM阵列的数据传入加法器之前要取反,并且加法器进位输入置1.C0信号完成这两项任务。改进后的电路结构图如下。
添加进位加法功能
还有一个一直没有找到合适的解决办法的问题:加法器及连接到它的所有设备的宽度只有8位。以前提出过的一个解决办法是把两个8位加法器(其他大部分设备也用两个)连在一起,构成一个16位的设备。
但还有一个成本更低的解决办法,假如把两个16位的数相加,比如:
76ABh
+232Ch
------
这种16位的加法先单独处理最右边的字节(通常称之为低字节):
ABh
+2Ch
----
D7h
然后再计算最左边的字节,即高字节的和:
76h
+23h
----
99h
得到相同的结果99D7h。因此,如果我们把两个16位的数用这种方式保存在存储器中,就像下面这样:
运算结果D7h将被保存到地址0002h,而结果99h将被保存到地址0005h。
如果两个数的低字节求和产生了一个进位,产生的这个进位必须与两个数的高字节的和再相加。
现在我们改进自动加法器的电路,使它可以正确地进行16位数的加法操作。我们用一位锁存器——进位锁存器(Carry latch)保存低字节数运算的进位输出,作为下一步高字节数运算的进位输入。
为了使用进位锁存器,还需要另一个操作码,我们称之为“进位加法”(Add with Carry)。
当进行8位数加法时,使用的是常规的Add指令。加法器的进位输入是0,它的进位输出将会保存到进位锁存器中,尽管根本不会被用到。
当进行两个16位数加法运算,我们仍然使用常规的Add指令对两个低字节数进行加法运算。加法器的进位输入为0,而其进位输出被锁存到进位锁存器中。当把两个高字节数相加时,要使用新的Add with Carry指令。在这种情况下,两个数相加时要用进位锁存器的输出作为加法器的进位输入。因此,如果第一步低字节数的加法运算有进位,则该进位讲用于第二步高字节数的加法运算;如果没有进位,则进位锁存器的输出是0.
添加借位减法的功能
如果要进行16位数的减法运算,则还需要一个新的指令,称为“借位减法”(Subtract and Borrow)。通常,Subtract指令需要将减数取反并且把加法器的进位输入置1.进位输出通常不是1,因此应该被忽略。但16位数进行减法运算时,进位输出应该保存在进位锁存器中。在进行第二步的高字节减法运算时,锁存器保存的结果应该作为加法器的进位输入。
在加入了Add with Carry和Subtract and Borrow之后,目前我们已经有了7个操作码,如下表所示。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Subtract | 21h |
Add with Carry | 22h |
Subtract with Borrow | 23h |
Halt | FFh |
当进行减法运算,或者进位锁存器的数据输入为1且正在执行加法或者借位减法运算时,8位加法器的进位输入都是置1的。
对多位数进行加法
需要记住的是,只有当前一次的加法或者进位加法操作使加法器产生进位输出时,Add with Carry指令才会使8位加法器的进位输入置1.因此,只要进行多字节加法运算,不管实际是否需要,都应该使用Add with Carry指令。
为了保证编码的正确,使用前面提到16位加法正常进行,可用如下方法。
无论操作数是什么,该方法都可以正确执行。
增加了两个新的操作码之后,我们已经极大地扩展了加法器的功能,它不再局限于8位数的加法运算。通过执行进位加法操作,可以对16位,24位,32位,40位,甚至更多位的数进行加法运算。假如要进行两个32位数7A892BCDh和65A872FFh的加法运算,我们仅需要1条Add指令和3条Add with Carry指令,如下图所示。
扩展代码RAM进行地址管理的累加器
当然,把这些数一次输入存储器并不是最好的做法。因为你不但要使用开关来输入这些数,而且保存这些数的存储单元的地址也不是连续的。
除此之外,当前设计的自动加法器不允许在随后的计算中重复使用前面的计算结果。
产生上述情况的原因就在于我们构造的自动加法器具有如下的特性:它的代码存储器和数据存储器是同步的,顺序的,并且都是从0000h开始寻址。代码存储器中的每一条指令对应数据存储器中相同地址的存储单元。一旦执行了一条Store指令,相应的,就会有一个数被保存到数据存储器中,而这个数将不能重新加载到累加器。
要解决这个难题,需要对自动加法器的设计做一个根本性且程度极大的修改。这个想法实现起来似乎非常困难,但改进后的加法器具有更高的灵活性。
让我们立刻开始吧,目前已经有了7个操作码,如下所示。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Subtract | 21h |
Add with Carry | 22h |
Subtract with Borrow | 23h |
Halt | FFh |
每一个操作码在存储器中占1个字节。现在除了Halt操作码外,我希望每个指令在存储器中仅占据3个字节的空间,其中第一个字节为代码本身,另外的两个字节用来存放1个16位存储器单元地址。
对于Load指令来说,后两个字节保存的地址用来指明RAM阵列的一个单元,该单元存放的是需要被加载到锁存器中的字节。对于Add,Subtract,Add with Carry,Subtract with Borrow指令来说,该地址指明的存储单元所保存的是要从累加器中加上或减去的字节。对于Store指令来说,该地址指明的是累加器中的内容要保存到的存储单元地址。
例如,当前加法器所能进行的最简单的运算就是对两个数求和。为了执行这个操作,需要按下面的方式设置代码RAM阵列和数据RAM阵列。
在改进的自动加法器中,每条指令(除了Halt指令)需要3个字节。
每一条指令的代码(除了Halt指令)后跟两个字节,用来指明数据RAM阵列中16位的存储地址。这三个地址恰巧是0000h,00001h和00002h,但它们可以是任何其他可用的地址。
前面讲到了如何用Add或Add with Carry指令来对两个16位数——比如76ABh和232Ch求和。使用的思路是把两个数的低字节保存到存储器的0000h和0001h地址,把其高字节保存到0003h和0004h地址,运算的结果分别保存在0002h和0005h。
改进后的加法器,我们可以用一种更合理的方式来保存这两个操作数及其运算结果,可能会把它们保存到我们从未用到过的存储区域。
这6个存储单元不必像上图中这样全都连在一起,它们可以分散在整个64KB数据RAM阵列的任意位置。为了把这些地址中的数相加,代码RAM阵列中的指令必须用以下方式设置。
可以看到,保存在地址4001h和4003h处的两个低字节数先执行加法,其结果保存在4005h地址处。两个高字节数(分别保存在4000h和4002h处)通过Add with Carry指令相加,其结果保存在地址4004h处。如果去掉Halt指令并向代码RAM中加入更多指令,随后的计算可以通过引用地址很方便地使用原来的那些操作数及其结果。
实现该设计的关键是把代码RAM阵列的数据输出到3个8位锁存器中。每个锁存器保存该3个字节指令的一个字节。第一个锁存器保存指令代码本身,第二个锁存器保存地址的高字节,第三个锁存器保存地址的低字节。第二个和第三个锁存器的输出构成了数据RAM阵列的16位地址。
从存储器中取出指令的过程称为取指令(introduction fetch)。
机器响应指令码做一系列茶欧洲的过程称为执行(execute)指令。
在我们设计的加法器中,每一条指令的长度是3个字节。而存储器每次只取回一个字节,所以每条指令需要的时间为3个时钟周期。因此一个完整的指令周期需要4个时钟周期。如果使用同样的振荡器,它的运算速度只有本章提到的第一个加法器的1/4.这这验证了TANSTAAFL的工程准则。
合并指令RAM和数据RAM为一个RAM
前面介绍了两种RAM阵列,一个用来存放指令码,另一个用来存放操作数据——这种设计使得自动加法器的结构非常清晰和易于使用。但现在我们使用3个字节长的指令格式,第二个和第三个字节用来指明操作数的存储地址,因此就没有必要再使用连哥哥独立的RAM阵列。操作码和操作数可以放在同一个RAM阵列中。
为了实现这个设计,我们需要一个2-1选择器来确定如何对RAM阵列寻址。通常,和前面的方式相同,我们用一个16位计数器来计算地址。指令RAM阵列的输出仍然连接到3个锁存器,分别用来保存指令代码机器对应操作数的16位地址,其16位地址输出是2-1选择器的第二种输入。地址被锁存后,可以通过选择器将作为RAM阵列的地址输入。
我们已经对原电路做了不少改进,现在可以把操作指令和操作数据保存在同一个RAM阵列中。例如,下图演示了如何把两个8位数相加,然后从结果中再减去一个8位数。
通常,指令从0000h开始存放,这是因为当计数器复位后从该位置访问RAM阵列。最后的Halt指令存放再000Ch地址。我们可以把这3个操作数及它们的运算结果保存在RAM阵列的任何地址(当然这不包括最开始的14个字节,因为它们已经用来存放操作指令),所以我们选择在0010h地址开始保存操作数。
Jump指令处理
假设现在你发现需要在原来的结果中再加两个数,我们更倾向于在原指令的地址后增加一些新的指令。
在当前例子中,我们更愿意从0020h地址开始存放新的指令,并从0030h处开始存放新的操作数据。
注意,第一条Load指令所指向的地址为0013h,这个位置保存着第一次运算的结果。
现在,两部分指针的位置分别起始于地址0000h和0020h,而两部分操作数据的地址分别起始于0010h和0030h。我们希望自动加法器从0000h开始执行所有指令完成计算任务。
我们可以用一个称为Jump(跳转)的新指令来替换Halt指令。现在把它加入到指令表。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Subtract | 21h |
Add with Carry | 22h |
Subtract with Borrow | 23h |
Jump | 30h |
Halt | FFh |
通常情况下自动加法器是以顺序方式对RAM阵列寻址的。Jump指令改变了机器的这种寻址方式,取而代之的是从某个指定的地址开始寻址。这种指令有时也被称为分支(branch)指令或者Goto指令,即“转到另一个位置”。
在上面的例子中,我们可以用一个Jump指令来替换000Ch地址处的Halt指令。
30h即Jump指令的代码。其后的两个字节中存放的16位地址就是自动加法器要执行的下一条指令的地址。
因此在上面的例子中,自动加法器仍然从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,则Pre=1且Clr=0;若A=0,则Pre=0且Clr=1.这意味着Q端的值设置为A端的值。
我们需要16位计数器的每一位设置一个这样的触发器。一旦加载了某个特定的值,计数器就会从该值开始计数。
然而,这对电路的改动并不是很大。从RAM阵列锁存得到的16位地址既可以作为2-1选择器(它允许该地址作为RAM阵列的地址输入)的输入,也可以作为16位计数器置位信号的输入并由置位信号设置。
显然,只有当指令代码为30h并且其后的16位地址被锁存时,我们才必须确保置位信号为1.
条件跳转(Conditional Jump)处理乘法运算
毋庸置疑,Jump指令的确很有用,但与之相比,一个在我们想要的情况下跳转的指令更加有用,这种指令称作条件跳转(Conditional Jump)。也许说明该命令重要性的最好方法是这样一个问题:怎样让自动加法器进行两个8位数的乘法运算?例如,我们如何利用自动加法器得到像A7h与1Ch相乘的运算结果呢?
第一步确定要把乘数和乘积保存到什么位置。
A7h和1Ch相乘的结果(即十进制的28)和把28个A7h累加的结果相同。
下图演示了如何把A7h加到该地址。
当这6条指令执行完毕之后,存储器1004h和1005h地址保存的16位数与A7h乘以1的结果相同。因此,为了使存放于该地址的值等于A7h与1Ch相乘的结果,要把这6条指令再反复执行27次。为了达到这个目的,可以把0012h地址开始把这6条指令连续输入27次;也可以在0012h处保存一个Halt指令,然后将复位键连续按28次得到最终结果。
当然,这两种方式都不是很理想,它们都让我们重复做了这些繁琐的操作。
但如果在地址0012h处放置一条Jump指令会怎样呢?这个指令使得计数器再次从0000h处开始计数。
这的确是一个巧妙的方法。第一次执行完指令之后,位于存储器的1004h和1005h地址的16位数等于A7h乘以1,然后Jump指令使下一条指令从存储器顶部开始执行。第二次执行后,该16位数等于A7乘2,最后其结果可以等于A7乘1Ch。但是,因为是Jump指令,这个过程不会停止下来,会一直反复执行下去。
我们需要的是这样一种Jump指令,它只让这个过程重复执行所需要的次数,这种指令就是条件跳转指令。
要实现它,并不难。要做的第一步就是增加一个与进位锁存器类似的1位锁存器。该锁存器被称为零锁存器(Zero latch),我们用这个锁存器标志0.只有当8位加法器的输出全部为0时,它锁存的值才是1.
使或非门的输出为1的唯一方法是所有的输入全为0,零锁存器才锁存1个数,该数称为零标志位(Zero flag)。当加法器的输出全为0时,零标志位等于1;当加法器的输出不全为0时,零标志位等于0.
有了进位锁存器和零锁存器以后,我们可以为指令表新增4条指令。
操作码 | 代码 |
---|---|
Load | 10h |
Store | 11h |
Add | 20h |
Subtract | 21h |
Add with Carry | 22h |
Subtract with Borrow | 23h |
Jump | 30h |
Jump If Zero | 31h |
Jump If Carry | 32h |
Jump If Not Zero | 33h |
Jump if Not Carry | 34h |
Halt | FFh |
例如,非零转移指令(Jump If Not Zero)只有在零锁存器的输出为0时才会跳转到指定的地址。换言之,如果上一步的加法,减法,进位加法,或者借位减法等运算的结果为0时,将不会发生跳转。为了实现这个设计,只需要在常规跳转命令的控制信号之上再加一个控制信号:如果指令是Jump If Not Zero,那么只有当零标志位是0时,16位计数器才被触发。
下图中0012h地址之后的指令即两个数相乘所用到的上表中所有指令。
第一次循环之后,位于地址0004h和0005h处的16位数等于A7h与1的乘积,这和我们的设计相符。在上图中,地址1003h处的字节通过Load指令载入到累加器,该字节是1Ch。把这个数和001Eh地址的字节相加后,恰好遇到的是Halt指令,当然这是一个合法的数。FFh与1Ch相加的结果与从1Ch中减1的结果相同,都是1Bh,因为这个数不等于0,所以零标志位是0,1Bh这个结果会存回到1003h地址。下一条要执行的指令是Jump If Not Zero,零标志位没有置1,因此发生跳转。接下来要执行的一条指令位于0000h地址。
要记住的是,Store指令不会影响零标记位的值。只有Add,Subtract,Add with Carry,Subtract with Borrow这些指令才能影响零标志位的值,因此它的值与最近执行上述某个指令时所设置的值相同。
经过两次循环后,1004h和1005h地址保存的16位数等于A7h与2的乘积。1Bh与FFh的和等于1Ah,不为0,因此仍然返回到顶部执行。
当执行到28次循环时,1004h和1005h地址保存的16位数等于A7h和1Ch的乘积。1003h地址保存的值是1,它和FFh相加的结果是0,因此零标志位被置位!Jump If Not Zero指令不会在跳转到000h地址,并且下一条要执行的指令即Halt指令。这样,我们完成了全部的工作。
计算机
硬件
现在可以断言,我们一直不断完善的这组硬件构成的机器可以称为计算机(Computer)。当然,它还很原始,但毕竟是一台真正的计算机。条件跳转指令将它与我们以往设计的加法器区别开来,能否控制重复操作或者循环(looping)是计算机(computer)和计算器(calculator)的区别。
这里演示了该机器如何用条件跳转实现两个数的乘法运算,用类似的方法还可以进行两个数的除法运算。而且,这不仅仅局限于8位数,它可以对16位,24位,32位,甚至更高位的数进行加,减,乘,除运算。而且,既然它能完成这些运算,那么对于开平方根,取对数,三角函数等运算也完全可以胜任。
既然我们已经装配了一台计算机,因此可以使用计算机相关词汇了。
我们装配的计算机属于数字计算机(digital computer),因为它只处理离散数据。曾经还有一种模拟信号计算机(analog computer),但现在已经非常少见了。数字数据就是离散数据,即这些数据是一些确定的离散值;模拟数据是连续的,并且在整个取值区间变化。
一台数字计算机主要由4部分构成:处理器(processor),存储器(memory),至少一个输入(input)设备和一个输出(output)设备。我们装配的计算机,存储器是64KB的RAM阵列,输入和输出设备分别是RAM阵列控制面板上的开关和灯泡。这些开关和灯泡可以让我们向存储器中输入数据,并可以检查运算结果。
除了上述3种设备之外的其他设备都归类于处理器。处理器也被称为中央处理单元(central processing unit)或者CPU。更通俗的说法是将其称作计算机的大脑,但本文将避免使用这样的词,毕竟确实比不上大脑。
我们设计的处理器为8位处理器。累加器的位宽为8位,而且大部分数据通路都是8位的宽度。唯一的16位数据通路是RAM阵列的地址通路。
处理器包括若干组件。毫无疑问累加器就是其中一个,它只是一个简单锁存器,用来保存处理器内部的部分数据。我们所设计的计算机中,8位反相器和8位加法器一起构成了算数逻辑单元(Arithmetic Logic Unit),即ALU。该ALU只能进行算数运算,最主要的是加法和减法运算。在更加复杂的计算机中,ALU还可以进行逻辑运算,比如AND,OR,XOR等。16位的计数器被称作程序计算器(PC,Program Counter)。
我们计算机是由继电器,电线,开关,以及灯泡构造而成,这些东西都叫做硬件。与之对应,输入对存储器中的指令和数值被称为软件(software)。之所以叫做软件,是因为相对于硬件,指令和数据更容易修改。
软件
当我们在计算机领域进行讨论时,“软件”这个词几乎与“计算机程序”(computer program),或“程序”(program)等术语是同义的,编写软件也称为计算机程序设计(computer program)。我们确定用一些指令让计算机实现两个数相乘的过程就是计算机程序设计。
通常,在计算机程序中,我们能够把代码(即指令本身)和数据(即代码要处理的数)区分开。但有时它们之间的界限也不是很明显,比如Halt指令(FFh)就可以有两种功能,除了作为代码时表示停止执行之外,还能代表数值-1.
计算机程序设计有时也被称为编写代码(writing code),或编码(coding)。程序员更喜欢别人称呼为“软件工程师”(software engineers)。
汇编语言
能够被处理器响应的操作码(比如Load指令和Store指令的代码10h和11h),称作机器码(machine codes),或机器语言(machine language)。计算机能够理解和响应机器码,其原理和人类能够读写语言是类似的,因此这里使用了“语言”来描述它。
一直以来,我们都在使用很长的短语来引用机器所执行的指令,比如Add with Carry指令。通常而言,机器码都分配了对应的简短助记符,这些助记符都用大写字母表示,包括2个或3个字符。下面是一系列上述计算机大致能够识别的机器码的助记符。
操作码 | 代码 | 助记符 |
---|---|---|
Load | 10h | LOD |
Store | 11h | STO |
Add | 20h | ADD |
Subtract | 21h | SUB |
Add with Carry | 22h | ADC |
Subtract with Borrow | 23h | SBB |
Jump | 30h | JMP |
Jump If Zero | 31h | JZ |
Jump If Carry | 32h | JC |
Jump If Not Zero | 33h | NC |
Jump if Not Carry | 34h | JNC |
Halt | FFh | HLT |
当这些助记符与另外一对短语结合使用时,其作用更加突出。例如,对于这样一条长语句“把1003h地址处的字节加载到累加器”,我们可以用如下简洁的句子替代:
LOD A, [1003h]
位于助记符右侧的A和[1003h]称为参数(argument),它们是这个Load指令的操作对象。参数由两部分组成,左边的操作数称为目标(destination)操作数(A代表累加器),右边的操作数称为源(source)操作数。方括号”[]”表明要加载到累加器的不是1003h这个数值,而是位于存储器地址1003h的数值。
类似的,指令“把001Eh地址的字节加到累加器”,可以简写为:
ADD A, [001Eh]
指令“把累加器的内容保存到1003h地址”,可简写为:
STO [1003h], A
注意,上面的语句中,目标操作数(Store指令在存储器中的位置)仍然在左边,源操作数在右边。这就决定了累加器中的内容必须要保存到存储器的1003h地址。
“如果零标志位不是1则跳转到0000h地址处“这个冗长的语句可以简明地表示为:
JNZ 0000h
注意,这里没有使用方括号,这时因为跳转指令要转移到的地址是0000h,而不是保存于0000h地址的值,即0000h地址就是跳转指令的操作数。
用缩写的形式表示指令是很方便的,因为这种形式下指令以可读的方式顺序列出而不必画出存储器的空间分配情况。通过在一个十六进制地址后加一个冒号,可以表示某个指令超存在某个特定地址空间,例如:
0000h: LOD A, [1005h]
下面的语句表示了数据在特定地址空间的存储情况。
1000h: 00h, A7h
1002h: 00h, 1Ch
1004h: 00h, 00h
你可能已经注意到了,上面的两个字节都是以逗号分开的,它表示第一个字母保存在左侧的地址空间中,第二个字节保存在该地址后的下一个地址空间中。上面的三条语句等价于下面的这套语句:
1000h: 00h, A7h, 00h, 1Ch, 00h, 00h
因此,上面讨论的乘法程序可以用如下一系列语句来表示:
0000h: LOD A, [1005h]
ADD A, [1001h]
STO [1005h], A
LOD A, [1004h]
ADC A, [1000h]
STO [1004h], A
LOD A, [1003h]
ADD A, [001Eh]
STO [1003h], A
JNC 0000h
001Eh: HLT
1000h: 00h, A7h
1002h: 00h, 1Ch
1004h: 00h, 00h
使用空格和空行的目的仅仅是为了人们更方便地阅读程序。
在编码时最后不要使用实际的数字地址,因为它们是可变的。用标号(label)来指代存储器中的地址空间是个较好的办法。这些标号是一些简单的单词,或是类似单词的字符串。上面的代码可以改写为:
BEGIN: LOD A, [RESULT + 1]
ADD A, [NUM1 + 1]
STO [RESULT + 1], A
LOD A, [RESULT]
ADC A, [NUM1]
STO [RESULT], A
LOD A, [NUM2 + 1]
ADD A, [NEG1]
STO [NUM2 + 1], A
JNZ BEGIN
NEG1: HLT
NUM1: 00h, A7h
NUM2: 00h, 1Ch
RESULT: 00h, 00h
注意,NUM1, NUM2,RESULT这些标号都是指存储器中保存两个字节的地址单元。在这些与剧中,NUM1+1, NUM2+1和RESULT+1分别指标号NUM1,NUM2,RESUL后的第二个字节。注意,NEG1(negative one)用来标记HLT指令。
最后,如果你可能忘记这些语句说表示的意思,那么可以在该语句后面加注释(commnet),这些注释可以用我们人类的自然语言表述,然后通过分号于程序语句分隔开。
BEGIN: LOD A, [RESULT + 1]
ADD A, [NUM1 + 1] :低字节相加
STO [RESULT + 1], A
LOD A, [RESULT]
ADC A, [NUM1] :高字节相加j
STO [RESULT], A
LOD A, [NUM2 + 1]
ADD A, [NEG1] :第二个数减1
STO [NUM2 + 1], A
JNZ BEGIN
NEG1: HLT
NUM1: 00h, A7h
NUM2: 00h, 1Ch
RESULT: 00h, 00h
这里给出的是一种计算机程序设计语言,称为汇编语言(assembly language)。它是全数字的机器语言和指令的文字描述的一种结合体。每一条汇编语句都对应着机器语言中的某种特定字节。
错误(bug)
对于学习计算机程序设计的人来说,应该尽早了解“错误”(bug)这个术语。
一些错误可能导致意想不到的结果。假设你使用Jump指令跳转到一个地址,而该地址没有存放任何合法的指令,或者你偶然误用Store指令覆盖了其他指令,类似的情况有可能发生。
甚至上述乘法程序中就存在一个错误。如果你把程序执行两次,第二次得到的将会是A7h与256相乘的结果,并且程序会把这个结果与第一次运算的结果相加。这是因为程序执行一次之后,1003h地址保存的数值是0.当第二次执行时,FFh与这个0相加的结果不是0,因此程序会继续执行直到把它变为0.
回顾历史和展望未来
前面不止一次强调过,这些硬件部件早在100年前就发明出来了。但是本章所设计的计算机在当时并没有被制造出来。当时,人们仍然尝试在计算机使用十进制数而不是二进制数;而且计算机程序也不是存储在存储器中的,它们被保存在纸带上。
当回顾完计算器和计算机这一段历史,让我们展望一下未来。继电器被真空管和晶体管这类电子器件所替代。已经有人创造出一种全新的设备,它的处理器和存储器的能力与我们设计的不相上下,并且这种机器精致小巧,可以放在我们掌心。