文章目录
本章像是根据前几章的电路组件,搭建一台比较完整的可以自动化操作的计算机。
先来回忆第14章中提到的,不考虑进位,以电平触发的锁存器为基础的加法计算器。
和带控制面板的RAM阵列:
1. 基础自动化加法器
假设振荡器的速度足够慢,使电流可以流过加法器输出之前电路中涉及到的所有继电器。
通过振荡器和计数器的组合作为RAM的地址输入,完全由控制面板对RAM数据输入端:
使用这台机器时,首先闭合并断开清零开关,用于清楚锁存器内容和计数器归零;然后闭合控制面板的takeover开关。此时就可以通过控制面板需要用于计算的数字输入RAM,输入完成后断开takeover开关。
该计算器仅仅是解决了自动化的问题。目前还存在的问题是:
- 没有办法中途停下,会一气之下求和所有加数;
- 不能进行8bit加法之外的计算;
2. 可写回RAM的自动加法器
如果期望中途停下,也就是期望可以自由的求和几个加数,那就需要将求和先保存起来(既然有了RAM,就可以用RAM保存加和)。由于控制面板上也有灯泡用于读取RAM,则可以取消电路锁存器后的灯泡:
这次直接略去了振荡器,计数器和清零开关等用于发出控制信号的附件,可以默认已经存在了。
可以先将加和保存在所有加数的下一个地址,如下。
但将计算结果存放在RAM中完全不同于我们手算之后将结果输入到RAM中。可以想到的一个方法是有另一个的RAM专门用于保存指令(本质也是二进制,不同二进制组合表示不同的人类语言指令),该指令RAM和存放待计算的数据的RAM有所存放的东西一一对应,即RAMA的0000h地址存放一个加数,则RAMB的0000h地址存放对这个加数的需要做的操作。
当前需要的操作可以抽象为四个:
- 将首个操作数加载(Load)到锁存器中;
- 将后续操作数累加(Add)到加法器中;
- 将计算结果保存(Store)到RAM中;
- 停止(Halt)自动加法器。
当保存数据的RAM和保存操作指令的RAM共同作用于自动加法器时,应该出现下面这个电路:
操作指令对应的二进制数字可以暂时表示成这样:
这些数字代码常被称为指令码(instruction code)或操作码(operation code,opcode)。因此,对应的指令码RAM当前应该是这样保存内容的:
由于锁存器的输入可能来自于RAM(Load操作),也可以来自于加法器(Add操作),因此需要布置一个2-1选择器电路:
显然,有些控制信号取决于代码RAM的输出。例如,当代码RAM为Load指令时,2-1选择器的S输入为0(即选择数据RAM的输出);只有当代码RAM为Store时,数据RAM的W输入才为1。所以可以认为代码RAM的输出会关联一些控制信号的输入。
3. 可进行减法计算的计算器
期望进行减法运算,那么至少在指令RAM中需要有一个减法指令。其他的,除了数据RAM传入加法器之前需要先取反并且加法器进位置1以外,电路所做的操作与执行Add指令所做的操作相同。
图中的C0开关可以控制反向器和在进位端输入1:
4. 可进位和借位的计算器
之前提到的,如果期望计算16位的加法,最简单直接粗暴的方法就是两个8位加法器组合在一起成为16位计算器。其实还可以先计算低字节加法再计算高字节加法。
假设两个16位数字76ABh和232Ch相加,则:
如果低字节需要进位,那刚才提到的就会变得麻烦。可以尝试新增一个专门用于保存进位的锁存器,称为进位锁存器(Carry latch),该锁存器仅需保存1位数据。此外,还需要随之更新一个新的指令,称为“进位加法(Add with carry)”。
新指令用于计算高字节的加法。因为低字节使用普通加法(Add)指令,相加的进位(无论是0还是1)作为进位锁存器的输入,计算高字节时需要考虑进位锁存器的内容。
如果需要计算16位数的减法,则还需要另外一个指令,“借位减法(Subtract and Borrow)”。
只有前一次的加法(或进位加法)产生进位输出时,Add with Carry指令才会使8位加法器的进位输入置1。因此,只要进行的是多字节加法,都应该在高字节使用Add with Carry指令。例如:
需要总结一下进位输入为1的情况:
- 减法运算;
- 进位锁存器为1,正在进行进位加法;
- 进位锁存器为1,正在进行借位减法。
由于指令和数据在不同的RAM是同步前进的,存在这样一个缺陷:计算A+B-C时,完成之后就很难再访问A+B的结果了,如果再想计算A+B-D的结果,将比较困难。
5. 指令和数据非同步前进的计算器
这是一个大的修改。对指令进行扩充,需要控制操作数的指令(除Halt外)都从1字节扩充至3字节,首字节仍旧保存原始的指令,另外两个字节用于存储对应操作数的16位RAM的地址。
- Load指令,跟的是需要加载到累加器的数据的地址(源RAM地址);
- Add/Subtract/Add with Carry/Subtract with Borrrow指令,跟的是将要从累加器中加上或减去的数据的地址(源RAM地址);
- Store指令,跟的是累加器中将要保存到RAM的数据地址(目的RAM地址)。
例如,对于下面的这个计算
可以将操作指令扩展为:
如果操作数是16位的,那就如前面所说,将一个操作数按高低字节分开为两个,分别放置在两个不同的地址中。例如76ABh和232Ch。
那么指令就需要扩展为:
如此看来,好像数据的地址并不必要连在一起,也能进行正确的计算。因为在扩展的指令里自然包含了操作数地址,只要指令里描述的地址正确,计算结果就正确。
实现这种设计的关键是把指令RAM的内容先输出到3个8位锁存器中。
- 第一个锁存器保存指令本身;
- 第二个锁存器保存数据地址高字节;
- 第三个锁存器保存数据地址低字节。
从存储器中取出指令的过程叫做取指令(instruction fetch)。现在几乎每条指令都是三字节,每次从存储器中取出一个字节,所以取出每条指令就需要3个时钟周期。
机器相应指令码做一系列操作的过程叫做执行(execute)。
如果说一条指令中已经包含了数据的地址,也就是这个指令知道该从哪里去找数据了,那么看起来把数据和指令都存贮在一个RAM中,好像也可以完成正确的计算。
仍然使用16位计数器来计算地址;RAM输出仍然连接三个8位锁存器;16位地址输出作为2-1选择器的一种输入,促使RAM输入出对应地址中的操作数。
如果期望计算例如"45h + A9h - 8Eh"这样的算式,在RAM中,指令和数据就可能是这样的:
此时如果有的新的需求,期望在上一个计算结果的基础上再加上两个数字。我们肯定不想重新通过控制面板输入新的指令,而是通过在原有基础上做些更改。
新增的两个操作数(假设是43h和2Fh)可以按照之前的方法,将指令存储在0020h开始的地址处。在执行新增加法需求之前需要先把上一次的计算结果(地址为0013h)Load到加法器中。
现在在RAM中两部分指令分别起始于地址0000h和0020h,两部分数据分别起始于0010h和0030h。我们希望从0000h开始执行指令,并得到最终的计算结果“45h + A9h - 8Eh + 43h + 2Fh = ?”
难点在于000Ch地址处的Halt指令应该如何处理。可以使用一个Jump新指令替换Halt。Jump指令会改变最初顺序寻址的方式,变成了一种跳转。
根设计好的一样,每个指令需要扩展为3个字节,因此Jump指令后仍旧需要跟一个地址,作为跳转的位置:
但是在电路上应该如何修改呢?
自动化的开端是计数器,当前使用的计数器是递增+1的对地址累加,如今新需求期望在地址上可以跳转,因此需要针对计数器进行修改。
计数器本质是由一个振荡器与一个D型边沿触发的锁存器组成(参考14th “反馈与触发器” )。
在触发器上添加一个Pre(预设)和Clr(清零)端口。在正常操作下Pre和Clr都是0,当Pre为1时,Q为1;Clr为1时,Q为0。
如果期望向锁存器输入一个预设的值(假设为A),参考如下电路:
通常情况下置位信号为0,此时Pre端就必然为0;在复位信号也为0的情况下,Clr也为0,此时就是一个普通的触发器。
当置位信号为1时,如果A为1,则Pre为1;如果A为0,则Clr为1。则可以认为当置位信号为1时,Q端输出与A相同。
针对16位的计数器,需要使用16个这样的改造锁存器。使其一旦Pre预设了某个值,计数器就会从该值开始计数。
对于整体电路来说,改动并不大。只需要将原本从两个地址锁存器的输出分出一个支路引入到计数器即可。当指令代码为30h(Jump)并且它后边跟的16位地址被锁存时,才需要确保置位信号为1。如果复位输入为1,则计数器归零,
6. 可以计算乘法的计算器
如何计算两个8bits数(例如:A7h和1Ch)的乘法呢?
一个简单的方法就是共有1Ch(十进制为28)个A7h相加。假设两个乘法操作数保存在这样的RAM地址中:
那么整个计算所进行28次累加中的每一次都需要做如下这样一遍操作。可以选择从0012h地址开始再重复输入27次这个指令,也可以将复位键连续按动28次(相当于从0000h处执行命令28次)得到最终结果:
显然提到的两种方法都不是很理想。如果再0012h处存放了一条Jump到0000h地址的指令,看起来是个不错的设计,但是机器将陷入死循环。
因此可以添加条件Jump(Conditional Jump)指令,根据某个条件去停止或启动Jump。
在电路中,我们需要新增一个零锁存器(Zero latch)。可以认为与进位锁存器是同类型的电路,但逻辑上感觉是相反的。只有当8bit加法器的输出全部为0时,锁存器输出1。零锁存器的输出刚好与计数器的置位端相连,控制计数器的地址变化。
零锁存器的“零”指的是在计算后的结果为0,计算包括:Add/Subtract/Add with Carry/Subtract with Borrrow;但不包括store。这也是说“零锁存器与进位锁存器同类型”的原因,两者都是根据前一次的计算进行数据锁存的。
有了进位锁存器和零锁存器,就可以新增4个指令:
这四个指令中的零和进位指的是,使用该指令前最近的一条计算指令的输出是否为0和是否进位。例如,Jump If Not Zero(非零转移)指令,如果该指令前最近的一条加、减、进位加、借位减指令输出不为0时就需要跳转。
计算乘法是就可以用到“Jump If Not Zero”指令,同样是将乘法认为是一个数(其中一个乘数)的重复多次(另一个乘数)相加,使用非零跳转指令,可以再没跳转一次后将累加的次数(另一个乘数)递减1直到减为为0,则停止跳转。
在0012h地址处写下如下指令:
1003h地址处存放的是需要累加的次数1Ch(28次),对1Ch加上001Eh地址处的FFh,相当于减去1。由于在001Bh地址上使用的是33h(即“Jump If Not Zero”)指令,因此当1Ch减到0时不再跳转到0000h地址,此时A7h便被累加了28次。不再跳转0000h地址,也就代表可以走到001Eh地址的Halt指令,即完成乘法运算。
当该电路可以计算乘法时,它也将可以计算除法,开平方,取对数,三角函数等数学计算。
能否控制重复操作或者循环(looping)是计算机(computer)与计算器(calculator)的关键区别。
因此,就可以称这台机器为计算机(computer)了。准确来说这台计算机叫做数字计算机(digital computer),因为它只能处理一些离散的准确的数据。(曾经有一种模拟信号计算机(analog computer),不过现在已经很少提了。)
7. 计算机初涉
一台数字计算机主要由4部分组成:处理器(processor),存储器(memory),输入设备(input)和输出设备(output)。
我们刚才搭建的计算器中,存储器是64KB RAM,输入设备和输出设备分别是控制面板上的开关和灯泡。除了这三种设备以外的其他所有设备都属于处理器。
处理器也被称作中央处理单元(central processing unit)或者CPU。可以认为是计算机的大脑。我们所设计的处理器为8位处理器,累加器宽度为8位,大部分数据通路也是8位,只有RAM是16位地址通路。
本文的处理器包含很多组件。其中,反向器和累加器构成了算数逻辑单元(Arithmetic Logic Unit,简称ALU),当前仅能计算加减法运算,在更复杂的计算机中,ALU还可以进行逻辑运算,例如OR,AND,XOR等。
16位的计数器被称作程序计数器(Program Counter,简称PC)。
我们的计算机是由继电器,电线,开关,灯泡组成的,这些东西都叫作硬件(Hardware),输入到存储器RAM中的指令和数据叫做软件(Software)。软件相较于硬件,软在更易于修改。
软件几乎等同于“计算机程序(computer program)”或“程序(program)”,编写软件也称为计算机程序设计(computer programming)。使用一些确定的指令让计算机进行两个数相乘的过程就是在进行计算机程序设计。
在计算机程序中一般把代码(即一组指令)与数据(即代码要处理的数字)区分开。但有时界限也没有那么明显,正如上一节计算乘法运算时使用重复累加的次数加上Halt(FFh)停止指令,本质上进行了-1的操作。
8. 汇编语言初涉
能够被处理器响应的操作码(例如Load和Store指令的代码分别为10h和11h)叫做机器码(machine codes),或机器语言(machine language)。当前我们一直使用短语来表示机器指令,例如Add with Carry,通常机器码都分配了对应的简短助记符,使用大写字母表示。
助记符和参数(argument)共同组成了一个指令。例如,LOD A, [1003h]
表示“把1003h地址处的字节加载到累加器”。其中A和[1003h]就是参数,也是LOD指令的操作对象。
参数包含两部分,左边的操作数称为目标(destination)操作数(A代表累加器),右边的操作数称为源(source)操作数。
方括号“[]”表示要加载到累加器的不是1003h这个数值,而是存储器中位于1003h地址的数值。
类似的,ADD A, [001Eh]
表示把001Eh地址的字节加到累加器;STO [1003h], A
表示把累加器中的内容保存到1003h地址。
Jump if Not Zero的助记符为“JNZ”,所以如果需要表示“如果不为零则跳转到0000h地址”应该写作“JNZ 0000h”。由于这里的目的并不是取0000h地址的值,所以不对0000h加方括号。
为了不再使用一些小方格表示指令和数据,通常将指令书写为:
这表示LOD A, [1005h]
这条指令存放在0000h地址位置。一般还可以看到纯数据的写法:
右边的两个字节被逗号隔开,表示逗号左边的字节存放在前一个地址,逗号右边的字节存放在下一个地址。(在linux系统中可以使用hexdump filename
命令来查看filename文件的数据排列。)本质上也可以写成:
因此,上面讨论的乘法程序可以书写为:
使用空格和空行的目的仅仅是为了提高可读性,不会影响程序的执行。
在实际编码时,尝尝使用一些label表示地址,因为实际地址是可变的。例如,上面的指令文字可以写成:
图中,NUM1、NUM2和RESULT都是表示RAM中保存两个字节单元的地址,且NUM1+1、NUM2+1和RESULT+1分别表示NUM1、NUM2和RESULT地址的后一个地址。NEG1(negative one)用来标记Halt指令。最后,也可以使用“;”在行尾添加人类自然语言的注释(comment),以防止忘记指令行的意义。
其实这就是汇编语言(assembly language)。它是全数字的机器语言和指令的文字描述的一种结合体。同时使用label表示存储地址。
机器语言和汇编语言,是同一种事物的不同表现形式,每一条汇编语言都对应着机器语言中某些特定的字节。
书中本章的最后还有一些有关bug的举例,我觉得也值得一看。