两种典型的微处理器
微处理器——正是它,将计算机中央处理器的所有构成部件整合在一起,集成在一个硅芯片上——诞生于1971年。它的诞生有着很好的开端:第一个微处理器,即Intel 4004系列,包括了2300个晶体管。到现在,大约三十年过去了,家用计算机的微处理器中的晶体管数量也逐步逼近10 000 000个。
微处理器实际的作用基本上保持不变。现在的芯片上附加的上百万个晶体管可以做许多有趣的事情,但在微处理器最初的探索过程中,这些事情更多的是分散我们的注意力而不是给我们以启迪。为了对微处理器的工作情况获得更清晰的认识,让我们先看一下最初的微处理器。
Intel 8080微处理器
我们要讨论的微处理器出现于1974年。在该年度,Intel公司在4月推出了8080,Motorola(摩托罗拉)—从20世纪50年代开始生产半导体和晶体管产品的公司—在8月份推出了6800。它们并非是那年仅有的微处理器。同样是在1974年,德克萨斯仪器公司推出了4位的TMS1000,用在许多计算器、玩具和设备上;NationalSemiconductor(国家半导体公司)推出了PACE,它是第一个16位的微处理器。然而,回想起来,8080和6800是两个最具有重大历史意义的芯片。
Intel设定8080最初价格为$360,这是对IBMSystem/360的一个讽刺。IBMSystem/360是一个大型机系统,由许多大公司使用,要花费几百万美元。(今天,你只花$1.95就可以买到一个8080芯片。)这并不是说8080可以与System/360相提并论,但不用几年,IBM公司也将会正视这些很小的计算机。
8080是一个8位的微处理器,有6000个晶体管,时钟频率为2MHz,可寻址64KB的存储空间。6800(今天也只卖$1.95)有大约4000个晶体管,也可寻址64KB的存储空间。第1代6800的时钟频率为1MHz,但到1977年Motorola公司发布新款的6800时,其时钟频率已为1.5MHz和2MHz。
这些芯片被称为“单芯片微处理器”(single-chip microprocessors),不太准确的说法是“单芯片的计算机”。处理器只是计算机的一部分。除了处理器之外,计算机还需要其他一些设备,至少要包括一些随机访问的存储器(RAM)一些方便用户把信息输入计算机的设备(输入设备),一些使用户能够把信息从计算机中读取出来的设备(输出设备),以及其他一些能把所有构件连接在一起的芯片。本书会在第21章详细介绍这些构件。
现在,让我们来仔细研究一下微处理器本身。当描述微处理器的时候,我们总是习惯用一些框图来阐明其内部的构件及其连接情况。然而,在第17章我们已经使用了数不清的框图来描述它,现在我们将观察微处理器和外部设备的交互过程,以此来认识其内部的结构和工作原理。换句话说,为了弄清微处理器的工作原理,我们把它视作一个不需要详细研究其内部操作的黑盒。取而代之的方法是通过贯彻芯片的输入,输出型号,特别是芯片的指令集来理解微处理器的工作原理。
8080和6800都是40个管脚的集成电路。这些芯片最常见的IC封装大约为2英寸长,1/2英寸宽,1/8英寸厚。
当然,你看到的只是外包装。位于其内部的硅晶片非常小,就拿早期的8位微处理器来说,其硅晶片小于1/4平方英寸。外包装保护硅晶片并通过管脚提供对芯片的输入和输出点的访问。下图显示了8080的40个管脚的功能:
本书中我们所创建的所有电气或电子设备都需要某种电源供电。8080的一个特殊地方就是它需要三种电源电压:管教20必须接到5V的电压;管教11需要接到-5V的电压;管教28需要接到12V的电压;管脚2接地。(1976年,Intel发布了8085芯片,简化了这些电源需求)。
其他的管脚都标有箭头。从芯片引出的箭头表明这是一个输出(output)信号,这种信号由微处理器控制,计算机的其他芯片对该信号响应。指向芯片的箭头表明该信号是一个输入(input)信号,该信号由其他芯片发出,并由8080芯片对其响应。还一些管脚既是输入又是输出。
第17章所设计的处理器需要一个振荡器来使其工作。8080需要两个不同的同步时钟输入,它们的频率都是2MHz,分别标记为ø1和ø2,位于管脚22和15上。这些信号可以很方便地由Intel公司生产的8224时钟信号发生器提供。给这个芯片连上一个18MHz的石英晶体,剩下的工作它基本上可以完成。
一个微处理器通常有多个用来寻址存储器的输出信号。用于寻址的输出信号的数目与微处理器的可寻址空间大小直接相关。8080有16个用于寻址的输出信号,标记为A0~A15,因此它的可寻址空间大小为2的16次方,即65 536字节。
8080是一个8位的微处理器,可以一次从存储器读取或者向存储器写入8位数据。该芯片还包括标记为D0~D7的8个信号,这些信号是芯片仅有的几个既可以用做输入又可以用做输出的信号。当微处理器从存储器中读取一个字节时,这些管脚的功能是输入;当微处理器向存储器写入一个字节时,其功能又变成了输出。
芯片的其余10个管脚时控制信号(control signals)。例如,RESET(复位)输入用于控制微处理器的复位。输出信号1-WR的功能是指明微处理器需要向RAM中写入数据(1-WR信号对应于RAM阵列的写输入)。另外,当芯片读取指令时,其他控制信号会在某个时候出现在D0~D7管脚。由8080构成的计算机系统通常使用8228系统控制芯片来锁存这些附加的控制信号。后面将会讲述一些控制信号。由于8080的控制信号非常复杂,因此,除非你想基于8080芯片来实际设计计算机,否则最好不要用这些控制信号来折磨自己。
8080芯片复位后,它把锁存在存储器0000h地址处的字节读入微处理器,通过在地址信号端A0~A15输出16个0实现该过程。它读取的字节必须是8080指令,读取该字节的过程被称为取指令(instruction fetch)。
在第17章构造的计算机里,所有指令(除了停止指令HLT)都是3个字节长,包括一个操作码和两个字节的地址。在8080中,指令长度可以是1个字节、2个字节或3个字节。有些指令可使8080从存储器的某一位置处读出一个字节送到微处理器中;有些指令可使8080从微处理器中把数据写入存储器的某一位置处;其他指令可使8080不使用RAM而在内部执行。第一条指令执行完后,8080访问存储器中的第二条指令,依此类推。这些指令组合在一起构成一个计算机程序,用来完成一些自己感兴趣的事情。
当8080运行在最高速度即2MHz时,每个时钟周期为500纳秒(1除以2000000周等于0.000000500秒)。第17章中的每条指令都需要4个时钟周期,8080的每条指令则需要4~18个时钟周期,这意味着每条指令的执行时间为2~9微秒(即百万分之一秒)。
指令集
也许了解某个特定微处理器的功能最好的办法就是全面地测试其完整的指令集。
第17章最后完成的计算机包括12个指令。一个8位处理器的指令数很容易达到256,每一条指令的操作码就是一个特定的8位数(如果某些指令包含2字节的操作码,其指令集会更大)。8080虽没有那么多,但它也有244条操作码。这看起来似乎很多,但总的来说,却又不比第17章中的计算机功能多多少。例如,如果想用8080做乘法或除法,仍然需要写一段小程序来实现。
在第17章曾经讲到过,为了方便地引用指令,我们为处理器的每一条指令的操作码都指派了一个特殊的助记符,而且其中的一些助记符是可以带有参数的。这种助记符只是在我们使用操作码提供方便,它对于处理器是没有帮助的,处理器只能读取字节,对于助记符组成的文本的含义一无所知(为了讲解清楚,本书选用了Intel 8080说明文档中用到的部分助记符为例说明)。
基本指令集
第17章中的计算机有两条很重要的指令,称作装载(Load)和保存(Store)指令。这些指令都占用三个字节的存储空间。装载指令的第一个字节是操作码,操作码后的两个字节表示16位地址。处理器把在此地址中的字节送到累加器。同样,保存指令把累加器中的内容存储到指令指定的地址处。
下面,我们用助记符来简写这两个操作:
LOD A, [aaaa]
STO [aaaa], A
在此,A表示累加器(装载指令的目的操作数,保存指令的源操作数),aaaa表示一个16位的存储器地址,通常用4位十六进制数来表示。
同第17章的累加器一样,8080的8位累加器也记做A。正如第17章中的计算机一样,8080也有两条与装载和保存指令功能一样的指令。8080中这两条指令的操作码为32h和3Ah,每个操作码后有一个16位地址。8080的助记符为STA(代表存储累加器的内容)和LDA(代表装载到累加器):
操作码 | 指 令 |
---|---|
32 | STA [aaaa],A |
3A | LDA A,[aaaa] |
8080芯片的微处理器的内部除累加器外还设置了6个寄存器(register),每个寄存器可以存放一个8位的数。这些寄存器和累加器非常相似,实际上累加器被视为一种特殊的寄存器。这6个寄存器和累加器一样,本质上都是锁存器。处理器既可以把数据从存储器读入寄存器,也可以把数据从寄存器存回存储器。当然,其他的寄存器没有累加器所具有的丰富的功能,例如,当把两个8位数相加时,其结果总是保存到累加器而不会保存到其他寄存器。
在8080中用B,C,D,E,H和L来表示新增的6个寄存器。为什么用H和L呢?因为使用H和L来命名寄存器是因为它们具有特殊的含义,H可以代表高(High)而L可以代表低(Low)。通常把两个8位寄存器H和L合起来构成一个16位存储器对(register pair),称作HL,H用来保存高字节而L用来保存低字节。这个16位的值通常用来对存储器寻址,我们将在下面看到它是怎样以简单的方式工作的。
寄存器是计算机必不可少的部件吗?为什么第17章搭建的计算机中并没有寄存器的踪迹?从理论上讲,这些寄存器不是必需的,在第17章也没有用到它们,但是实际应用中使用它们将带来很大的方便。很多计算机程序都同时用到多个数据,将这些数据存放在寄存器比存放在存储器更便于访问,因为程序访问内存的次数越少其执行速度就越快。
在8080中有一个指令至少用到了63个操作码,这条指令就是MOV,即Move的缩写,其实该指令是一条单字节指令,它主要用来把一个寄存器中的内容转移到另一个寄存器(也可能就是原来的寄存器)。因为8080微处理器设计了7个寄存器(包括累加器在内),因此应用中使用大量的MOV指令是很正常的。
下面列出了前32条MOV指令。再一次提醒你,两个参数中左侧的是目标操作数,右侧的是源操作数。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
40 | MOV B,B | 50 | MOV D,B |
41 | MOV B,C | 51 | MOV D,C |
42 | MOV B,D | 52 | MOV D,D |
43 | MOV B,E | 53 | MOV D,E |
44 | MOV B,H | 54 | MOV D,H |
45 | MOV B,L | 55 | MOV D,L |
46 | MOV B,[HL] | 56 | MOV D,[HL] |
47 | MOV B,A | 57 | MOV D,A |
48 | MOV C,B | 58 | MOV E,B |
49 | MOV C,C | 59 | MOV E,C |
4A | MOV C,D | 5A | MOV E,D |
4B | MOV C,E | 5B | MOV E,E |
4C | MOV C,H | 5C | MOV E,H |
4D | MOV C,L | 5D | MOV E,L |
4E | MOV C,[HL] | 5E | MOV E,[HL] |
4F | MOV C,A | 5F | MOV E,A |
这些指令使用起来非常方便。利用上面的指令可以方便地把一个寄存器存放的数据转移到另一个寄存器。下面我们研究一下以HL寄存器作为操作数的4条指令。
MOV B, [HL]
前面讲过LDA指令,它可以把单字节的操作数从存储器转移到累加器;LDA操作码后面直接跟着该操作数的16位地址。在上面列出的指令中,MOV指令把直接从存储器转移到V寄存器,但该直接的16位地址却放在HL寄存器对中。HL是怎么得到16位存储器地址的呢?这并不难解决,有很多方法可以做到,比如通过某种计算实现。
LDA A, [aaaa]
MOV B, [HL]
它们的功能都是把一个字节从内存读入微处理器,但它们寻址存储器的方式并不相同。第一个方式称作直接寻址(direct addressing);第二个方式称作间接寻址(indexed addressing)。
下面列出了其余32条MOV指令,我们看到HL保存的16位存储器地址也可以作为目标操作数。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
60 | MOV H,B | 70 | MOV [HL],B |
61 | MOV H,C | 71 | MOV [HL],C |
62 | MOV H,D | 72 | MOV [HL],D |
63 | MOV H,E | 73 | MOV [HL],E |
64 | MOV H,H | 74 | MOV [HL],H |
65 | MOV H,L | 75 | MOV [HL],L |
66 | MOV H,[HL] | 76 | HLT |
67 | MOV H,A | 77 | MOV [HL],A |
68 | MOV L,B | 78 | MOV A,B |
69 | MOV L,C | 79 | MOV A,C |
6A | MOV L,D | 7A | MOV A,D |
6B | MOV L,E | 7B | MOV A,E |
6C | MOV L,H | 7C | MOV A,H |
6D | MOV L,L | 7D | MOV A,L |
6E | MOV L,[HL] | 7E | MOV A,[HL] |
6F | MOV L,A | 7F | MOV A,A |
汽油的一些指令如:
MOV A, A
并不会有执行的操作。
而指令
MOV [HL], [HL]
是不存在的,实际上,与之对应的指令是HLT(Halt)即停止指令,也就是说该指令的意义是停止。
研究MOV操作码的位模式能更好地了解它,MOV操作码由8位组成:
01dddsss
其中ddd这3位是目标操作数的代码,sss这3位是源操作数的代码。它们所表示的意义如下:
000 = 寄存器B
001 = 寄存器C
010 = 寄存器D
011 = 寄存器E
100 = 寄存器H
101 = 寄存器L
110 = 寄存器HL保存的存储器地址中的内容
111 = 累加器A
例如,指令
MOV L, E
对应的操作码为:
01101011
用十六进制数可表示为6Bh。这与前面列出的表格是一致的。
可以设想一下,在8080的内部可能是这样的:标记为sss的3位用于8-1数据选择器,标记为ddd的3位用来控制3-8译码器以此确定哪一个寄存器锁存了值。
寄存器B和C也可以组合成16位的寄存器对BC,同样我们还可以用D和E组成寄存器对DE。如果这些寄存器对也包含要读取或保存的字节的存储器地址,则可以用下面的指令实现:
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
02 | STAX [BC], A | 0A | LDAX A, [BC] |
12 | STAX [DE], A | 1A | LDAX A, [DE] |
另一种类型的传送(Move)指令称作传送立即数(Move Immediate),它的助记符写作MVI。传送立即数指令是一个双字节指令,第一个字节为操作码,第二个是数据。这个单字节数据从存储器转移到某个寄存器,或者转移到存储器中的某个存储单元,该存储单元由HL寄存器对寻址。
操作码 | 指令 |
---|---|
06 | MVI B,xx |
0E | MVI C,xx |
16 | MVI D,xx |
1E | MVI E,xx |
26 | MVI H,xx |
2E | MVI L,xx |
36 | MVI [HL],xx |
3E | MVI A,xx |
例如,当指令:
MVI E,37h
执行后,寄存器E存放的字节是37h。这就是我们要介绍的第三种寻址方式——立即数寻址(immediate addressing)。
基本运算操作码
下面列出一个操作码集,包括32个操作码,它们能完成4种基本的算术运算,这些运算在第17章设计处理器时我们已经熟悉了,它们是加法(ADD),进位加法(ADC),减法(SUB)和借位减法(SBB)。可以看到,在所有的例子中,累加器始终用于存放其中的一个操作数,同时用来保存计算结果。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
80 | ADD A,B | 90 | SUB A,B |
81 | ADD A,C | 91 | SUB A,C |
82 | ADD A,D | 92 | SUB A,D |
83 | ADD A,E | 93 | SUB A,E |
84 | ADD A,H | 94 | SUB A,H |
85 | ADD A,L | 95 | SUB A,L |
86 | ADD A,[HL] | 96 | SUB A,[HL] |
87 | ADD A,A | 97 | SUB A,A |
88 | ADC A,B | 98 | SBB A,B |
89 | ADC A,C | 99 | SBB A,C |
8A | ADC A,D | 9A | SBB A,D |
8B | ADC A,E | 9B | SBB A,E |
8C | ADC A,H | 9C | SBB A,H |
8D | ADC A,L | 9D | SBB A,L |
8E | ADC A,[HL] | 9E | SBB A,[HL] |
8F | ADC A,A | 9F | SBB A,A |
假如累加器A存放的字节是35h,累加器B存放的字节是22h,经过减法运算:
SUB A, B
累加器中的值变为22h,即两个字节的差。
如果累加器A中的值为35h,寄存器H和L中的值分别是10h和7Ch,而存储器地址107Ch处的字节为4Ah,指令:
ADD A, [HL]
把累加器中的值(35h)与寄存器对HL寻址(107Ch)存储器得到的数值(4Ah)相加,并把计算结果(7Fh)保存到累加器。
在8080中,使用ADC指令和SBB指令可以对16位数,24位数,32位数甚至更高位的数进行加法,减法运算。例如,假设现在寄存器对BC和DE各自保存了一个16位的数,我们要把这两个数相加,并且把结果保存在寄存器对BC中。具体做法如下:
MOV A, C ;低字节操作
ADD A, E
MOV C, A
MOV A, B ;高字节操作
ADC A, D
MOV B, A
在上面的计算中,用ADD指令对低字节相加,用ADC指令对高字节相加。低字节相加产生的进位会进入高字节的运算中。在这段简短的代码中,我们用到了4个MOV指令,这是因为在8080中只能利用累加器进行加法运算,操作数在累加器和寄存器之间来回地传送,因此在8080的代码中大量使用MOV指令。
标志位
现在我们来讨论8080的标志位(flag)。第17章设计的处理器已经有了CF(进位标志位)和ZF(零标志位)两个标志位,在8080中又新增了3个标志位,包括符号标志位SF,奇偶标志位PF和辅助进位标志位AF。在8080中,用一个专门的8位寄存器来存放所有标志位,改寄存器称为程序状态字(Program Status World,PSW)。不同的指令对标志位有不同的影响,LDA,STA或MOV指令始终都不会影响标志位,而ADD,SUB,ADC以及SBB指令会影响标志位的状态,具体情况如下。
- 如果运算结果的最高位是1,那么符号标志位SF标志位置1,表示该计算结果是负数。
- 如果运算结果为0,则零标志位ZF置0.
- 如果运算结果中“1”的位数是偶数,即具有偶数性(even parity),则奇偶标志位PF置1;反之,如果“1”的位数是奇数,即运算结果具有奇数性(odd parity),则PF置0.由于PF的这个特点,有时会被用来进行简单的错误检查。PF在8080程序中并不常见。
- 进位标志位CF的情况和第17章描述的稍有不同,当ADD和ADC运算产生进位或者SUB和SBB运算不发生借位时,CF都置1.
- 辅助进位标志位AF只有在运算结果的低4位向高4位有进位时才置1.它只用于DAA(Decimal Adjust Accumulator,十进制调整累加器)指令中。
下面的两条会直接影响进位标志位CF。
操作码 | 指 令 | 含 义 |
---|---|---|
37 | STC | 令CF置1 |
3F | CMC | 令CF取反 |
逻辑运算
第17章设计的计算机可以执行ADD,ADC,SUB和SBB指令(虽然缺乏灵活性),而8080功能更为强大,它还可以执行AND(与),OR(或),XOR(异或)等逻辑运算。不论是算术运算还是逻辑运算,都是由8080处理器的算术逻辑单元(ALU)来完成的。
以下是8080的算术运算和逻辑运算指令。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
A0 | AND A,B | B0 | OR A,B |
A1 | AND A,C | B1 | OR A,C |
A2 | AND A,D | B2 | OR A,D |
A3 | AND A,E | B3 | OR A,E |
A4 | AND A,H | B4 | OR A,H |
A5 | AND A,L | B5 | OR A,L |
A6 | AND A,[HL] | B6 | OR A,[HL] |
A7 | AND A,A | B7 | OR A,A |
A8 | XOR A,B | B8 | CMP A,B |
A9 | XOR A,C | B9 | CMP A,C |
AA | XOR A,D | BA | CMP A,D |
AB | XOR A,E | BB | CMP A,E |
AC | XOR A,H | BC | CMP A,H |
AD | XOR A,L | BD | CMP A,L |
AE | XOR A,[HL] | BE | CMP A,[HL] |
AF | XOR A,A | BF | CMP A,A |
AND, XOR和OR都是按位运算(bitwise operation)指令,也就是说对于这些逻辑运算指令,其操作数的每一个对应位都是独立运算的,例如:
MVI A, 0Fh
MVI B, 55h
AND A, B
保存到累加器的结果将会是05h。假如我们把3条指令换作OR,则最终的结果将会是5Fh;如果换作XOR,则结果会变成了5Ah。
CMP指令
CMP(Compare,比较)指令同SUB指令类似,也是把两个数相减,不同之处在于它并不在累加器中保存计算结果,计算的目的是为了设置标志位。这个标志位的值可以告诉我们两个操作数之间的大小关系。例如,我们考虑下面的指令:
MVI B, 25h
CMP A, B
指令执行后,累加器A中的值并没有变化。改变的是标志位的值,如果A中的值等于25h,则零标志位ZF置1;如果A中的值小于25h,则进位标志位CF置1.
立即数操作
同样的,也可以对立即数进行这8种算术逻辑操作。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
C6 | ADI A,xx | E6 | ANI A,xx |
CE | ACI A,xx | EE | XRI A,xx |
D6 | SUI A,xx | F6 | ORI A,xx |
DE | SBI A,xx | FE | CPI A,xx |
例如,可以用下面的这条指令来代替上面列出的两条指令:
CPI A, 25h
CMA和DAA
下面是两种特别的8080指令。
操作码 | 指令 |
---|---|
27 | DAA |
2F | CMA |
CMA是Complement Accumulator的缩写。它对累加器中的按数位取反,即把0变成1,1变成0.例如,累加器中的数如果是01100101,使用CMA命令后,累加器中的数按位取反,得到10011010。我们还可以使用如下指令对累加器中的数取反:
XRI A, FFH
前面提到过,DAA是Decimal Adjust Accumulator的缩写,即十进制调整累加器,它可能是8080中最复杂的一条指令。在8080微处理器中专门设计了一个完整的小部件用来执行该指令。
DAA指令提供了一种用二进制码表示十进制数的方法,称为BCD码(binary-coded decimal),程序员可以在该指令的帮助下实现十进制的算术运算。BCD码采用的表示方式为,每4位为一段,每段所能表示数的范围是:0000~1001,对应十进制的0~9.因为1字节有8位故可分割为2个段,因此在BCD码格式下,一个字节可以表示两位十进制数。
假设累加器A存放的是BCD码表示的27h,显然它就对应十进制数的27。同时假设寄存器B存放着BCD码表示的94h。假设执行如下指令:
MVI A, 27h
MVI B, 94h
ADD A, B
累加器存放的最终结果是BBh,当然,这肯定不是BCD码。因为BCD码中每4位组成的段所能表示的十进制数不会超过9.然而,当我们执行命令:
DAA
那么累加器最后所保存的值是21h,而且进位标志位CF置1.因为十进制的27与94相加的结果为121.由此可以看到,使用BCD码进行十进制的算术运算是很方便的。
增量和减量操作
在8080程序中,经常会对一个数进行加1或减1操作。在第17章的乘法程序中,为了实现对一个数减1,我们把该数与FFh相加,它是-1的补码。8080提供了专门的指令用来对寄存器或存储器中的数进行加1(称作增量)或减1(称作减量)操作。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
04 | INR B | 05 | DCR B |
0C | INR C | 0D | DCR C |
14 | INR D | 15 | DCR D |
1C | INR E | 1D | DCR E |
24 | INR H | 25 | DCR H |
2C | INR L | 2D | DCR L |
34 | INR [HL] | 35 | DCR [HL] |
3C | INR A | 3D | DCR A |
INR和DCR都是单字节指令,它们可以影响除CF(Carry Flag)之外的所有标志位。
循环移位指令
8080还包括4个循环移位(Rotate)指令,这些指令可以把累加器中的内容向左或向右移动1位,它们的具体功能如下。
操作码 | 指令 | 含义 |
---|---|---|
07 | RLC | 累加器循环左移 |
0F | RRC | 累加器循环右移 |
17 | RAL | 累加器带进位循环左移 |
1F | RAR | 累加器带进位循环右移 |
这些指令只对进位标志位CF有影响。
假设累加器中存放的数是A7h,即二进制的10100111.RLC指令使其每一位都向左移一位。最终的结果是,最低位(左端为低位,右端为高位)移出顶端移至尾部成为最高位,这条指令也会影响CF的状态。在这个例子中CF置1,最后的结果为01001111.RRC指令以同样的方式进行右移位操作。如果移位之前的数是10100111,执行RRC之后将变为11010011,同时CF置1.
较之RLC和RRC,RAL和RAR指令的工作方式稍有不同。执行RAL指令时,累加器中的数仍然按位左移,把CF中原来的值移至累加器中数值的最后一位,同时把累加器中数据的原最高位移至CF。例如,假设累加器中移位之前的数是10100111且CF为0,执行RAL指令后,累加器中的数变为01001110而CF变为1.类似的,如果执行的是RAR指令,累加器中的数变为01010011,而CF变为1.
当我们在程序中需要对某个数进行乘2(左移)或除2(右移)运算时,使用移位操作会使运算变得非常简单。
堆栈
我们通常把微处理器可以寻址访问的存储器称为随即访问存储器(random access memory,RAM),主要的原因是:只要提供了存储器地址,微处理器可以用非常简单的方式访问存储器的任意存储单元。RAM就像一本书,我们可以翻到它的任意一页,这种方式非常方便。它并不像做在微缩胶片上的一个星期的报纸,要找到周六版,需扫过大半周。同样,它也不同于磁带,要播放磁带上的最后一首歌需快进整个一面。微缩胶片和磁带的存储不是随机访问的,而是顺序访问(sequential access)的。
然而,在某些情况下使用不同的寻址方式访问存储器也是有好处的。下面就是一种既非随机又非顺序访问的存储方式:假定你在一个办公室里,人们到你桌前给你分配工作,每个工作都需要某种文件夹。通常你会发现你在继续某项工作之前,必须使用另外一个文件夹先做一些相关的工作。因此你把第一个文件夹放在桌子上,又拿出第二个文件夹放在它上面进行工作。现在又有一个人来让你做一个优先权高于前面工作的工作,你拿来一个新文件夹放在那两个上面继续工作。而此项工作又需要另外一个文件夹,这样在你的桌子上很快就摆了一堆文件夹了。
你可能已经注意到了,事实上,这些堆叠的文件夹很有序地保存了你干活的顺序轨迹。最上面的文件夹总是代表优先级最高的共,完成该工作之后就可以做接下来的工作了,以此类推。最后当你处理完办公桌上最后一个文件夹(即接受的第一个任务)后,就可以回家了。
这种形式的存储器称作堆栈(stack)。使用堆栈时,我们以从底部到顶部的顺序把数据存入堆栈,并以相反的顺序把数据从堆栈中取出,因此该技术也称作后进先出存储器(last-in-first-out,LIFO).堆栈的特点是,最先保存到堆栈中的数据最后被取出,而最后保存的数据则最先被取出。
同样,在计算机中也可以使用堆栈,当然计算机中的堆栈保存的是数据而不是工作。大量的实践证明,在计算机中使用堆栈技术是非常方便的。通常把数据存储堆栈的过程称作压入(push),把从堆栈取出数据的过程称作弹出(pop)。
假设你正在编写一个汇编语言程序,需要用到寄存器A,B,C来存储数据。在编写程序的过程中,你注意到程序需要做一个小的计算,并且该计算也需要用到寄存器A,B,C。你希望在完成该计算之后仍然回到原来的地方,并且仍然使用寄存器A,B,C中原先存放的数据。
为了保存寄存器A,B,C原先存放的数据,可以简单地把这些数据保存到存储器中不同的地址中,需要进行的计算完成之后,再把这些数据从存储器转移到寄存器。但这种方式需要记录数据存放的地址。有了堆栈的概念之后,我们可以用一种更清晰的方式来处理这个问题,即把这些寄存器中的数据依次存放到堆栈中。
PUSH A
PUSH B
PUSH C
稍后将具体解释这些指令实际的意义。现在需要知道的是,这些指令以某种方式把寄存器中的内存保存到先进后出存储器。这些指令执行之后,寄存器中原有的数据将妥善地保存下来,你就可以放心地使用这些寄存器进行别的工作了。为了取回原来的数据,可以使用POP指令把它们从堆栈中弹出,当然弹出的顺序和原来压入的顺序是相反的。相应的POP指令如下所示。
POP C
POP B
POP A
再次谨记:后进先出。如果POP指令的顺序弄错了,将会引起严重的错误。
我们可以在程序中多次用到堆栈而不会引起混乱,这是堆栈机制的一个特殊优势。例如,在我们要编写的程序中已经把寄存器A,B,C中的数保存到堆栈,在程序的另一端又需要把寄存器C,D,E中的数保存到堆栈,可以使用下面的指令:
PUSH C
PUSH D
PUSH E
当然,在该程序中还需要使用一些指令将保存到堆栈中的数据取回至寄存器,这些指令是:
POP E
POP D
POP C
显然,由于先进后出的原则,这些数据在先前存放的C,B,A中的数据之前弹出堆栈。
堆栈的功能是怎样实现呢?
首先,堆栈其实就是一段普通的RAM存储空间,只是这段空间相对独立不另作他用。8080微处理器设置了一个专门的16位寄存器对这段存储空间寻址,这个特殊的寄存器称为堆栈指针(SP,Stack Pointer)。
在我们所举的例子中,对于8080来说使用PUSH和POP对寄存器操作实际上是不准确的。在8080中,执行PUSH指令实际上是16位的数据保存到堆栈,执行POP指令是把这些数据从堆栈中取回至寄存器。因此,对于上面的如PUSH C
,POP C
等指令,我们对其进行如下修改。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
C5 | PUSH BC | C1 | POP BC |
D5 | PUSH DE | D1 | POP DE |
E5 | PUSH HL | E1 | POP HL |
F5 | PUSH PSW | F1 | POP PSW |
PUSH BC
指令将寄存器B和C中的数据保存到堆栈,而POP BC
则将这些数据从堆栈取回到寄存器B和C中,并且保持原来的顺序。最后一行指令中的PSW代程序状态字,如前所述,这是一个8位的寄存器,用于保持标志位。最后一行的PUSH和POP指令的操作对象实际上是累加器和PSW,即压入和弹出堆栈的数据由累加器和PSW中的内容组成。如果你想把所有寄存器中的数据及全部标志位都保存到堆栈,可以使用下面的指令:
PUSH PSW
PUSH BC
PUSH DE
PUSH HL
堆栈是怎么工作的呢?我们假设堆栈指针是8000h,当执行PUSH BC
指令时将会引发一下操作。
- 堆栈指针减1,变为7FFFh
- 寄存器B中内容被保存到堆栈指针指向的地址,即存储器地址7FFFh处。
- 堆栈指针减1,变为7FFEh。
- 寄存器C中的内容被保存到堆栈指针指向的地址,即存储器地址7FFEh处。
类似的,在堆栈指针仍为7FFEh的情况下,执行POP BC
指针时会将上面的步骤反过来执行一遍:
- 堆栈指针指向的地址(7FFEh)的内容加载到累加器C。
- 堆栈指针加1,变为7FFFh。
- 堆栈指针指向的地址(7FFFh)的内容加载到累加器B。
- 堆栈指针加1,变为8000h。
每执行一条PUSH指令,堆栈都会增加两个字节,这可呢个会导致程序出现一些小错误——堆栈可能会不断增大,最终覆盖掉存储器中保存的程序所必需的代码或数据。这种错误被称作堆栈上溢(stack overflow)。类似的,如果在程序中过多地使用了POP指令,则会过早地取完堆栈中的数据从而导致类似的错误,这种情况称为堆栈下溢(stack underflow)。
如果8080连接的是一个64KB的存储器,你可能会把堆栈指针初始化为0000h。当执行第一条PUSH指令时,堆栈指针会减为1变为FFFFh,即存储器的最后一个存储单元。这时,堆栈的初始位置将会是存储器的最高地址,因为程序的代码通常从0000h开始存放的,因此两者将保持非常远的距离。
8080使用LXI指令为堆栈寄存器赋值,LXI是Load Extended Immediate的缩写,即加载扩展的立即数。下面的这些指令将把操作码后的两个字节保存到16位寄存器对中。
操作码 | 指令 |
---|---|
01 | LXI BC,xxxx |
11 | LXI DE,xxxx |
21 | LXI HL,xxxx |
31 | LXI SP,xxxx |
指令:
LXI BC, 527Ah
和下面两条指令等价:
MVI B, 25h
MVI C, 7Ah
LXI指令保存一个字节。而且上表中最后一条LXI指令为堆栈指针赋了一个特殊的值。通常下面的指令不作为微处理器复位之后首先执行的指令之一。
0000h: LXI SP, 0000h
类似的,还可以对寄存器对和堆栈指针进行加1或减1操作,即把它们看作16位寄存器。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
03 | INX BC | 0B | DCX BC |
13 | INX DE | 1B | DCX DE |
23 | INX HL | 2B | DCX HL |
33 | INX SP | 3B | DCX SP |
对于要讨论的16位指令,我们可以再看一些例子。下面的指令把由任意2个寄存器组成的16位寄存器对的内容加到寄存器对HL中。
操作码 | 指令 |
---|---|
09 | DAD HL,BC |
19 | DAD HL,DE |
29 | DAD HL,HL |
39 | DAD HL,SP |
这些指令可以减少操作的字节数。例如,上面的第一条指令一般情况下需要6个字节。
MOV A, L
ADD A, C
MOV L, A
MOV A, H
ADD A, B
MOV H, A
DAD指令一般用来计算存储器地址,只对进位标志位CF有影响。
各种各样的指令
接下来我么来认识一下各种各样的指令。下面两条指令的特点是操作码后面都跟着2字节的地址,第一条指令把HL寄存器对的内容保存到该地址,第二条指令把该地址的内容加载到HL寄存器对。
操作码 | 指令 | 意义 |
---|---|---|
22h | SHLD [aaaa], HL | 直接保存HL中饿数据 |
2Ah | LHLD HL, [aaaa] | 直接加载数据到HL |
寄存器L的数据保存在地址aaaa,而寄存器H的数据保存在地址 aaaa + 1.
下面的两条指令把寄存器对HL保存的数据加载到程序计数器和堆栈指针。
操作码 | 指令 | 意义 |
---|---|---|
E9h | PCHL PC, HL | 将HL保存的数据加载到程序计数器 |
F9h | SPHL SP, HL | 将HL保存的数据加载到堆栈指针 |
PCHL指令本质上是一种Jump指令,它把HL保存的内存地址加载到程序计数器,而8080处理器要执行的下一条指令就是程序计数器所指明的存储器地址中存放的指令。SPHL指令可以作为另一种为堆栈指针赋值的指令。
下面的两条指令中,第一条将HL保存的数据与堆栈顶部的两个字节进行交换;第二条指令将HL保存的数据和寄存器对DE保存的数据进行交换。
操作码 | 指令 | 意义 |
---|---|---|
E3h | XTHL HL, [SP] | 把HL中的内容和堆栈顶部2个字节进行交换 |
EBh | XCHG HL, DE | 把DE中的内容和HL中的内容进行交换 |
跳转指令
除了刚刚讲过的PCHL指令外,目前为止还没有介绍过8080的跳转指令。如第17章所述处理器中专门设置了一个称为程序计数器PC的寄存器,它用来保存处理器将要取出并执行的指令的存储地址。通常,处理器在PC指引下顺序执行存储器中存放的指令,但有一些指令,如Jump(跳转),Branch(分支)或Goto(无条件跳转)——使处理器脱离原来的执行顺序。这些指令使PC重新加载另外的值,处理器要执行的下一条指令存放于存储器的其他位置,而不在按原来的顺序寻址。
当然,原始的普通跳转指令的确有一定作用,但从第17章得来的经验可以知道,条件跳转(conditional jump)指令的作用更大。条件跳转指令使处理器根据某些标志位的值转移到特定的地址,这些标志位可以是进位标志位CF,零标志位ZF等。正是由于条件跳转指令的引入,第17章所设计的自动加法器才成为一般意义上的数字计算机。
8080有5个标志位,其中有4个可用于条件跳转指令。8080支持9种不同的跳转指令,包括了非条件跳转指令,还包括根据ZF,CF,PF以及SF是否为1而跳转的条件跳转指令。
在介绍这些指令之前,首先来介绍与Jump指令相关的另外两种指令。第一种是Call(调用)指令,它和Jump指令类似,但是有所不同的是:执行Call指令后,程序计数器(Program Counter)加载一个新的地址,而处理器会把原来的地址保存起来,保存到何处呢?最后的选择自然是堆栈了。
这种策略使Call指令有效地记录了“从何处跳转”(where it jump from),即保存了跳转之前的相关信息。堆栈中保存的地址可以使处理器最后返回到转移之前的位置。用于返回的指令称为Return(返回)。Return指令从堆栈中弹出两个字节,并把它们加载到PC中,这样就完成了返回到跳转点的工作。
对于任何处理器来说,Call和Return指令都非常重要。在它们的帮助下,程序员可以在程序中使用子程序(subroutine),子程序是一段频繁使用的完成特定功能的代码。对于汇编语言来说,子程序是其基本的组成部分。
来我们来看一个使用子程序的例子。假设你在编写汇编语言程序,在程序的某个位置你需要把两个字节相乘,因此你编写了一段用于两个数相乘的代码,然后继续向下写,在程序的另一个位置你发现需要再一次对两个字节相乘。因为你已经写过两个字节相乘的代码了,所以一个好的方法是跳转到先前写的那段乘法代码所在的位置。因此用到Call指令和Return指令来帮助实现这个功能,确保执行乘法之后能准确地返回程序的当前位置。
在第17章的乘法程序中,被乘数(还有乘积)被保存在存储器的特定位置;而在8080的子程序中,乘数和被乘数分别存放在寄存器B和寄存器C中,乘积保存到16位寄存器对HL中。8080的乘法子程序如下:
Multiply: PUSH PSW ; 将要修改的寄存器原有内容保存至堆栈
PUSH BC
SUB H, H ; 将 HL(即乘积)置为0000h
SUB L, L
MOV A, B ; 乘数送至累加器
CPI A, 00h ; 如果累加器中的值是0,则结束
JZ AllDone
MVI B, 00h ; 将BC的高字节置为0
MultLoop: DAD HL, BC ; 将BC的内容加到HL
DEC A ; 乘数减1
JNZ MultLoop; 如果不为0,则跳转
AllDone: POP BC ; 将堆栈中保存的数据恢复至寄存器
POP PSW
RET ; 返回
注意,上述子程序的第一行有一个标志Multiply。当然,实际上这个标志对应着子程序在存储器中的起始地址。该子程序在开始处使用了两个PUSH指令,这是因为通常在子程序的起始处要保存程序用到的寄存器。
保存寄存器后,子程序下面要做的是把寄存器H和L置0.尽管可以使用MVI(转移立即数)指令代替SUB指令来实现该操作,但这样会用到4个字节而不是2个字节的指令。子程序执行成功后,运算结果会保存到寄存器对HL中。
接下来子程序把寄存器B中的数(即乘数)转移到累加器A,并判断该数是否为0.如果为0,则乘法子程序结束,因为乘数为0.由于寄存器中H和L已经为0,所以子程序可以使用JZ(Jump If Zero)指令跳转到程序最后的两条POP指令。
如果乘数不是0,子程序会把寄存器B置为0.现在寄存器对BC存放的是16位的被乘数,而累加器中存放的是乘数。加下来DAD指令会把BC(被乘数)加到HL(运算结果)中。A中的乘数减1,如果结果不为0,则执行JNZ(非零跳转)指令,该指令会使BC再次加到HL。这个循环继续执行,知道循环的次数等于乘数为止
(当然也可以利用8080的移位指令编写一个更有效率的乘法子程序)。
在程序中使用如下指令来调用这个乘法子程序,例如,把25h和12h相乘:
MVI B, 25h
MVI C, 12h
CALL Multiply
CALL指令把PC的值保存到堆栈中,被保存的这个值是CALL指令的下一个指令的地址,然后CALL指令将使程序跳转到标志为Multiply的指令,即子程序的起始处。但子程序得到计算结果后,执行RET(返回)指令,该指令使保存在堆栈的PC的值弹出,并重新设置到PC,之后程序将继续执行CALL指令后面的指令。
8080指令集包括条件CALL指令和条件Return指令,但它们使用的频率比条件跳转指令小的多。下面的表格完整地列出了这些指令。
条件 | 操作码 | 指令 | 操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|---|---|---|
None | C9 | RET | C3 | JMP aaaa | CD | CALL aaaa |
Z not Set | C0 | RNZ | C2 | JNZ aaa | C4 | CNZ aaaa |
Z set | C8 | RZ | CA | JZ aaaa | CC | CZ aaaa |
C not set | D0 | RNC | D2 | JNC aaaa | D4 | CNC aaaa |
C set | D8 | RC | DA | JC aaaa | DC | CC aaaa |
Odd parity | E0 | RPO | E2 | JPO aaaa | E4 | CPO aaaa |
Even parity | E8 | RPE | EA | JPE aaaa | EC | CPE aaaa |
S not set | F0 | RP | F2 | JP aaaa | F4 | CP aaaa |
S set | F8 | RM | FA | JM aaaa | FC | CM aaaa |
微处理器与外围设备
正如你大概所了解的,存储器并不是连接在微处理器上的唯一设备。一个完整的计算机系统通常需要输入/输出设备(I/O)以实现人机交互。输入/输出设备通常包括键盘和显示器。
微处理器是如何与外围设备(peripheral,除存储器外,与微处理器连接的所有设备称为外围设备)互相通信的呢?外围设备配备了与存储器类似的接口,微处理器通过与某种外围设备对应的特定地址(即接口)对其进行读写操作。在某些微处理器中,外围设备实际上占用了一些通常用来寻址存储器的地址,这种结构称作内存映像I/O(memory-mapped I/O)。但在8080中,除了常规的65536个地址外,另外增加了256个地址专门用来访问输入/输出设备,它们被称作I/O端口(I/O ports)。I/O地址信号标记为A0~A7,但I/O的访问方式与存储器的访问方式不同,两者的区分由8228系统控制芯片的锁存信号来标识。
OUT(输出)指令把累加器中的内容写入到紧跟该指令后的字节所寻址的端口(port)。IN(输入)指令把一个字节从端口读入到累加器。它们的格式如下所示。
操作码 | 指令 |
---|---|
D3 | OUT PP |
DB | IN PP |
外围设备有时候需要获得处理器的注意。例如,当你按下键盘的某个键时,处理器应该马上注意到这个事件。这个过程由一个称为中断(interrupt)的机制实现,这是一个由外围设备产生的信号,连接至8080的INT输入端。
但是,当8080复位后,就不再响应中断。程序必须执行EI(Enable Interrupt)指令来允许中断,然后执行DI(Disable Interrupts)禁止中断。这两条指令如下所示。
操作码 | 指令 |
---|---|
F3 | DI |
FB | EI |
8080的INTE输出信号用来指明何时允许中断。当外围设备需要中断微处理器的当前工作时,它需要把8080的INT输入信号置为1.8080通过从存储器中取出指令来响应该中断,同时控制信号指明有中断发生。外围设备通常提供下列指令来响应8080微处理器。
操作码 | 指令 | 操作码 | 指令 |
---|---|---|---|
C7 | RST 0 | E7 | RST 4 |
CF | RST 1 | EF | RST 5 |
C7 | RST 2 | E7 | RST 6 |
DF | RST 3 | FF | RST 7 |
上面列出的这些指令都称作Restart(重新启动)指令,在其执行的过程中也会把当前PC中的数据保存到堆栈,这一点与CALL指令相似。但Restart指令在保存PC数据之后会立刻跳转到特定的地址,而且根据参数的不同将跳转到不同的地址:比如 RST 0
将跳转到地址0000h处, RST 1
将跳转到地址0008h处,依次类推,最后的RST 7
将跳转到地址 0038h处。这些地址存放的代码都是用来处理中断的。例如,由键盘引起的中断将执行 RST 4
指令,程序将跳转到该地址 0020h 处,该地址存放的代码将负责从键盘读入数据(完整的过程将在第 21 章讲述)。
NOP
目前为止,我们已经介绍了243个操作码。在前255个数中,有12个没有作为操作码使用,它们是:08h, 10h, 18h, 20h, 28h, 30h, 38h, CBh, D9h, DDh, EDh和FDh。
下面还需要讲到一个操作码。
操作码 | 指令 |
---|---|
00 | NOP |
NOP代表(即声明)no op(no operation,无操作)。NOP指令使处理器什么操作也不执行。这样做有什么好处呢?填空,即保持处理器的运行状态而不做任何事情。8080可以执行一批NOP指令而不会引起任何错误事件的发生。
Motorola 6800
本章不准备详细介绍Motorola 6800微处理器,因为在构造和功能方面它与8080非常相似。下面是6800的40个管脚的功能描述图。
在上图中,Vss标示接地,Vcc代表5V的电源。同8080一样,6800也有16个地址输出信号端和8个数据信号端,其中数据信号端既可以用于输入信号也可以用于输出信号。它还有一个RESET信号端和一个R/(1-W) (read/write, 读/写) 信号。1-1RQ信号代表中断请求。与8080相比,6800的时钟信号较为简单,6800没有设计独立的I/O端口,所有的输入/输出设备的地址都是存储器地址空间的一部分。
6800有一个16位程序计数器PC,一个16位的堆栈指针SP,一个8位的状态寄存器(用来保存标志位),以及两个8位的累加器A,B。A和B都可以用做累加器(而不是把B作为普通的寄存器),因为A和B功能完全相同,任何用A做的工作都可以用B实现。与8080不同,6800没有设置其他的8位寄存器。
6800设置了一个16位的索引寄存器(index register),它可以用来保存16位的地址,其功能与8080的HL寄存器对相似。对于6800的大部分指令来说,它们的地址都可以由索引寄存器与紧跟在操作码后的字节相加得到。
尽管6800实现的操作与8080大致相同——加载,保存,加法,减法,移位,跳转,调用等,但对应的操作码和助记符是完全不同的。例如,下面列出了6800的转移(Branch)指令集。
操作码 | 指令 | 含义 |
---|---|---|
20h | BRA | 转移 |
22h | BHI | 大于则转移 |
23h | BLS | 小于或相同则转移 |
24h | BCC | 进位为0则转移 |
25h | BCS | 进位置1则转移 |
26h | BNE | 不等则转移 |
27h | BEQ | 相等则转移 |
28h | BVC | 溢出为0则转移 |
29h | BVS | 溢出置1则转移 |
2Ah | BPL | 为正则转移 |
2Bh | BMI | 为负则转移 |
2Ch | BGE | 大于或等于0则转移 |
2Dh | BLT | 小于0则转移 |
2Eh | BGT | 大于0则转移 |
2Fh | BLE | 小于或等于0则转移 |
与8080不同,6800没有设置奇偶标志位,而是设置一个溢出标志位(Overflow flag)。上面的转移指令中有一些依赖于标志位的组合(combinations of flags)。
当然,8080和6800的指令集是不同的,虽然这两款芯片于同一年发布,但它们是由属于不同公司的两组不同的工程师设计的。这就造成了它们之间的不兼容,因此它们不能执行对方的机器码,为一种芯片编写的汇编语言程序也不能在另一种芯片上执行。如何编写能在不同类型处理器上执行的计算机程序是第24章的主题。
8080和6800的另一个有趣的区别是:在两个处理器中,LDA指令都从存储器的特定地址将数据加载到累加器。例如,在8080中,下面的字节序列:
将把存储器地址347Bh处的字节加载到累加器。下载对比一下6800的LDA指令,它使用6800扩展寻址模式(6800 extended addressing mode):
上组的这组字节序列将把存储器7B34h地址处的字节加载到累加器。
两者的区别是很微妙的。两种微处理器对紧跟在操作码后的地址的处理方式是不同的,8080假设低字节在前,高字节在后;而6800假设高字节在前,低字节在后。
这种Intel和Motorola微处理器保存多字节数时的根本不同从没有得到解决。直到现在,Intel微处理器在保存多字节数时,仍是最低有效字节在前(即在最低存储地址处);而Motorola微处理器在保存多字节数时,仍是最高有效字节在前。
这两种不同的方式分别称为little-endian(Intel 方式) 和 big-endian(Motorola方式).这两种差别确实造成了附加的兼容性问题,这种问题通常在采用little-endian和big-endian系统的及其共享信息时出现。
两种芯片的发展
这两种微处理器后来怎样了呢?8080用在一些人所谓的第一台个人计算机上,不过可能更准确的说法是第一台家用计算机上。下图是Altair8800,出现在1975年1月份的《PopularElectronics》杂志的封面上。
当你看到Altair8800时,前面面板上的灯泡和开关看起来似乎很熟悉。这和第16章为64KBRAM阵列建议的初始“控制面板”的界面是同一类型的。
8080之后出现了Intel8085,更具意义的是出现了Zilog制造的Z-80芯片。Zilog是Intel公司的竞争对手,是由Intel公司的前雇员,也曾在4004芯片上做出重要贡献的FedericoFaggin建立的。Z-80与8080完全兼容,且增加了许多很有用的指令。1977年,Z-80用于RadioShackTRS-80Model1上。
也是在1977年,由Steven Jobs和Stephen Wozniak建立的苹果计算机公司推出了AppleII。AppleII既不用8080也不用6800,而是使用了采用MOS技术的更便宜的6502芯片,它是对6800的改进加强版本。
1978年6月,Intel公司推出了8086,一个16位微处理器,它可访问的存储空间达到1MB。8086的操作码与8080不兼容,但它包含乘法和除法指令。一年后,Intel公司又推出了8088,其内部结构与8086相同,但其外部按字节访问存储器,因此该微处理器可使用较流行的为8080设计的8位外围芯片(8-bit support chips)。IBM在其5150个人计算机—通常叫作IBM PC—上使用了8088芯片,这种个人计算机在1981年秋季推出。
IBM进军PC市场产生了巨大影响,许多公司都发布了与PC兼容的机器(兼容的含义在随后各章里将要详细讨论)。多年来,“IBM PC兼容机”也暗指“Intel inside”,特指所谓x86家族的Intel微处理器。Intel x86家族继续发展,1985年出现了32位的386芯片,1989年出现了486芯片。1993年初,出现了Intel Pentium微处理器,普遍地用在PC兼容机上。虽然这些Intel微处理器都不断增加了指令的指令集,但它们仍然支持从8086开始的所有以前处理器的操作码。
苹果公司的Macintosh首次发布于1984年,它使用了Motorola 68000——一个16位的微处理器,也即6800的下一代处理器。68000和它的后代(常称为68K系列)是制造出的最受欢迎的一类微处理器。
从1994年开始,Macintosh计算机开始使用PowerPC,一种由Motorola、IBM和Apple公司联合开发的微处理器。PowerPC是由一种称作RISC(Reduced Instruction Set Computing,精简指令集计算机)的微处理器体系结构来设计的,它试图通过简化某些方面以提高处理器的速度。在RISC计算机中,每条指令通常长度相同,(在PowerPC中为32位),存储器访问只限于装载和保存指令,且指令做简单操作而不是复杂操作。RISC处理器通常有大量的寄存器以避免频繁访问存储器。
因为PowerPC具有完全不同的指令集,所以它不能执行68K的代码。但现在用于Apple Macintosh的PowerPC微处理器可仿真(emulate)68K。运行于PowerPC上的仿真程序逐个检验68K程序的每一个操作码,并执行适当的操作。仿真程序不如PowerPC自身代码那样快,但可以工作。
按照摩尔定律(Moore’s Law),微处理器中的晶体管数量应该每18个月翻一番。人们不禁要问:增加的这些大量的晶体管用来做什么呢?
一些晶体管用于增加处理器的数据宽度,从4位到8位到16位再到32位;另外一些增加的晶体管用于处理新的指令。例如,现在大多数微处理器都支持用于浮点算术运算的指令(这将在第23章解释);还有一些新增加的指令用来进行一些重复计算,以便在计算机屏幕上显示图片和电影。
现代处理器使用多种技术来提供其运行速度。其中一种就是流水线技术(pipelining),即处理器在执行一条指令的同时读取下一条指令,尽管Jump指令在一定程度上会改变这种流程。现代处理器还包括一个Cache(高速缓冲存储器),它是一个设置在处理器内部,访问速度非常快的RAM阵列,用来存放处理器最近要执行的指令。由于计算机程序经常执行一些小的指令循环,使用Cache可以避免反复加载这些指令。上面提到的这些运行速度的策略都需要在处理器内部增加更多的逻辑组件和晶体管。
正如前面所提到的,微处理器只是整个计算机系统的一部分(尽管是最重要的一部分)。我们会在21章构造这样一个系统,但首先要学习如何处理存储器中的数据,包括操作码和数字,我们要对这些数据进行编码。让我们从心态回归小学一年级,像孩子们学习读写一样学习如何编码吧。