902-输入输出接口的编址方式
# 902-输入输出接口的编址方式
CPU 的运算能力很强, 但它与外界沟通交流的手段却非常地单一, 它总是希望有这样“我给你一个地址,你就给我一个数据”非常直白的沟通方法。 所以它平时也只能和存储器这样胸怀宽广,但是同样头脑简单的家伙在一起玩了。
然而现在它需要面对外界那么多的朋友,各个都非常复杂,有时还经常捣些乱, 那它恐怕就应付不了了,所以它就找了 I/O 接口这样的帮手帮它打理外部的世界, 而它呢还是希望能够用一个简单的方式和 I/O 接口进行交互和沟通。 所以我们就来看一看 CPU 是怎么和 I/O 接口进行沟通的。
这是 I/O 接口在计算机系统当中的位置, 和访问存储器中的单元一样,我们想要 CPU 访问 I/O 接口当中的这些寄存器,也是需要通过编写指令来实现,那问题就是,I/O 接口里面的这些寄存器的地址究竟是什么?
我们先来看几个基本概念,在系统当中通常会有多个 I/O 接口, 每个 I/O 接口内部都有若干个寄存器,这些寄存器一般被称为 I/O 端口, 我们不要被这个词的字面意义所迷惑, 这个端口指的并不是我们计算机上的 USB 接口、网线接口这样实实在在的接口, 而是一个抽象的概念, 它实际上指的就是这些在 I/O 接口芯片内部的寄存器,它们就像在存储器当中的一个个存储单元一样,CPU 要访问它们,就得有特定的地址, 因此每个寄存器,也就是每个 I/O 端口都需要有自己的地址,这称为端口地址,也叫做端口号。那在计算机系统中, 如何去设定这些端口号就称为 I/O 端口的编址方式。
# 编址方式
常见的 I/O 端口编址方式有两种: 第一种是 I/O 端口和存储器分开编址, 又被称为 I/O 映像的方式,x86 体系结构就采用了这种方式。 另一种常见的方式是 I/O 端口和存储器统一编址, 又称为存储器映像的 I/O 方式。 ARM、MIPS、PowerPC 等体系结构都采用了这样的方式。
# 分开编址
那我们先来看分开编址的方式。我们假设这个体系结构地址的宽度为 3, 那它一共可以访问的地址单元就是 2 的三次方,总共 8 个, 如果每个单元是一个字节,那它的存储器最大就是 8 个字节
然后我们需要在这个计算机系统当中增加一些 I/O 端口, 那 I/O 端口的地址是重新编排的,和存储器地址无关。 一般情况下,我们需要的 I/O 端口的数量都比存储器单元要少得多, 比如在这个示例的系统当中,我们需要四个 I/O 端口, 那我们就给它分配四个端口号,0、1、2、3,这样的编址方式就称为分开编址。
在这种编址方式下,要访问 I/O 端口需要用特殊的指令, x86 提供了 IN 和 OUT 这两条指令, IN 指令用于把 I/O 端口的内容输入到 CPU 当中的寄存器, 而 OUT 指令则是把 CPU 寄存器当中的内容输出到 I/O 端口中。
那我们来看一看 IN 和 OUT 指令应该如何书写。 如果要访问的端口地址在 0 到 255 之间,那可以采用两种方式。 一是叫直接寻址,也就是写一个立即数指定端口地址, 例如这条指令,这个 80H 就是一个端口号,那这条指令说的是 从 80H 这个端口读出一个字节的内容,并存放到 AL 寄存器当中去,
而这条指令则是说,将 AX 当中的两个字节的内容,传送到 80H 所指定的 I/O 端口中,当然这个 I/O 端口对应的应该是一个两字节的寄存器。
而如果端口地址大于 255,则需要将这个地址先保存到 DX 寄存器当中, 然后再执行 IN 和 OUT 的操作。我们也来看一个例子, 假如我们要访问第 288 号端口, 这时候就需要先把这个端口号存到 DX 寄存器当中, 然后在 IN 和 OUT 指令当中,使用 DX 寄存器来指定端口号,这样就是对 288 号端口地址进行操作。
当然,对于端口地址在 0 到 255 之间的,也可以使用间接寻址的方式,那么 x86 为什么要设定这两种方式呢? 主要还是为了指令的长度, 我们看到在直接寻址的情况下,我们需要有一个字节的操作码, 还需要有一个字节保存这个端口号,那么一个字节所能表达的范围就是 0 到 255, 如果端口地址大于 255,那原本就需要再增加一个字节来记录端口号,但这样指令就太长了,为了缩短指令长度,宁可多增加一个指令,这也是 CISC 的特点。
所以对于间接寻址,有另外一个操作码, 这条指令只有一个字节,既没有立即数,也没有寄存器的编号, 所以它是默认地采用 DX 寄存器来保存端口地址, 这样在访问更大的端口地址时,指令的长度反而可以更短一些。 那这里就有一个问题,既然用这个方式能访问的地址范围更大,指令长度还更短, 那我们为什么不只用这一种方式呢?还需要去直接寻址这样的方式? 这个问题就留给你自己思考。
那我们再通过一个例子来看一看这样的指令的操作过程。 这条指令是 AL 寄存器当中的一个字节,传送到 21H 这个端口号, 那么当 CPU 从存储器当中取回了这条指令,通过译码发现是一条 OUT 指令, 那它就会将 AL 寄存器当中的内容取出来,放到数据总线上,并将 21H 放到地址总线上, 那么这时候系统总线应该怎么办呢?
我们不妨把系统总线看成城市中的一条街道,而把系统总线所连接的存储器 I/O 接口看成街道两旁的一些建筑,每个建筑里面还有很多个单位,每个单位都有一个门牌号, 那现在就好像有一个快递员接收了一个任务,要把一个包裹送到 21H 这个地址, 于是他就在这个街道上走,查看着每个大楼的门牌号
然后他发现,这两个存储器地址范围,一个是从 0 到 7FFF, 一个是从 80000 到 5 个 F。那么在存储器 1 当中,实际上是包含了 21H 这个地址的, 但他接着再看,这个 I/O 接口 1 当中的地址是 00 到 1F, 而 I/O 接口 2 当中的地址是 20 到 3F,I/O 接口 3 的地址是 40 到 5F,所以在 I/O 接口 2 当中,也包含了 21H 这个地址。 那这个包裹应该送到哪儿呢? 所以单凭这个地址,系统总线是无法判定要访问哪个设备的。
因此,CPU 发出的信号中, 除了地址,还应该有一个别的信号,这个信号指明了当天要访问的是存储器还是 I/O 接口。 在 x86 的 CPU 当中,这个信号叫做 M/IO, 当这个信号为 0 的时候,表明当前在访问 I/O 接口,而这个信号为 1 的时候,表明在访问存储器,这样系统总线就知道该怎么办了,它会在所有的 I/O 接口当中, 寻找这个地址所对应的端口,那么这就发现是在 I/O 接口 2 中, 所以系统总线会把这个传输传到 I/O 接口 2,I/O 接口 2 可能是一个独立的芯片, 当它在系统总线上采样到这个地址和一个数据之后,就会在内部找到对应的端口号。
我们要注意的一点是,这些 I/O 接口内部一般只有 少数几个端口,所以它只会采样地址的低几位, 然后用这个低位在内部进行索引。在这个例子当中,21H 的这个 2 是系统总线用来找到这个 I/O 接口的,而这个 I/O 接口只用接收到地址的低位这个 1,然后在内部找到对应的端口就可以了。 最后这个 I/O 接口将从数据总线上采样到数据,也就是 AL 寄存器当中的内容, 保存到它内部的数据输出寄存器中,这就完成了这条 OUT 指令所需的操作。至于这个数据输出寄存器它外面是连接到了几个小灯泡,还是一个数码管, 那就是这个 I/O 接口和外射的连接情况了。
# 统一编制
然后我们再来看一看 I/O 端口和存储器统一编址的情况。 还是假设地址宽度为 3,那我们在这个统一编址的体系结构当中,总共就只有 8 个单元, 然后根据需要,其中有一部分用来作为 I/O 端口的地址,其他部分用来作为存储单元的地址。
那我们之前介绍的模型机就采用了统一编址的方式, 它的地址总线宽度为 4 位,这样一共就有 16 个单元, 而存储器当中用了的地址是 0 到 7,一共 8 个单元。 而输入、输出设备则用了 15 和 16 这两个地址, 另外还有一些地址在这个系统当中没有使用, 那在之后扩展中,可以增加一些存储器,或者增加一些输入、输出端口, 但是总共只有这 16 个,是不可以重复的。
当然,因为它是统一编址的,所以给出任意一个地址,只有唯一的一个单元与之对应, 所以也就不需要刚才 x86 当中使用的 M/IO 这样的信号来指定当前的地址 到底是 I/O 地址还是存储器地址。
# 统一编址方式的优缺点
那我们来看一看统一编址方式的优缺点。
首先来看优点。因为在统一编址的情况下,是不区分存储器地址和 I/O 端口地址的, 所以我们就可以直接用访问存储器的指令来访问 I/O 端口,而访问存储器的指令功能通常比较丰富,比如数据可以有各种的宽度,地址也有多种的产生方式,可以是立即数,可以是寄存器,也可以是寄存器加立即数,甚至还可以放在存储器当中, 所以这样就比较方便对 I/O 端口进行处理。
另外,如果要涉及单独的 I/O 操作的指令,无论它做得怎么简单,也是需要额外的一套硬件逻辑, 而采用统一编址的方式,CPU 中只需要有一套对外部总线的控制逻辑就可以了, 内部结构简单,对外的引脚数目也会少一些。
但是统一编址也有它的缺点,由于 I/O 端口占用了 一部分地址空间,从而使用于存储器的地址空间变小了, 这个问题对于早期的处理器影响还是非常大的, 我们想一想,x86 的早期只有 16 位宽的地址,按说只能访问 64KB 的存储器, 它为了有更多的存储器空间,还设计了一个非常复杂的 段加偏移的方式,才能访问一兆的地址空间, 如果在这个时候还要为了 I/O 端口占用了一部分地址空间,那就很难接受了, 这也是 x86 选用了分开编址方式的一个重要原因。
而当 CPU 的字长到了 32 位之后, 在很长一段时间,地址空间都不是一个问题, 所以那个时候直接从 32 位起步的 MIPS 就采用了统一编址的方式, 当然现在到了 64 位之后,物理的存储器远远用不了这么大的地址空间, 所以地址空间被挤占这个因素现在已经不成问题了。另外,如果要用访问存储器的指令来进行 I/O 操作,那这些指令往往比单独设计的 I/O 指令要长,而且因为这些指令比较复杂,执行的时间可能也会长一些。这也是 risk 为什么普遍采用了统一编址方式的原因,因为 risk 的指令都是固定长度的, 所以即使设计单独的 I/O 指令,也不会比普通的访存指令更短一些,而 x86 这样的 cisc 采用了变长的指令, 所以就可以设计出更短的专门用于 I/O 的指令,从而提高指令的密度。
# 小结
那么了解了统一编址的特点、分开编址的特点,我们也就清楚了他们的优缺点刚好是相对的,在分开编址的情况下,I/O 端口不会挤占存储器的地址空间, 而且因为涉及了单独的 I/O 指令,它的指令编码可以做得很短,执行速度也比较快, 而 I/O 地址空间一般是远远小于存储器地址空间的, 所以独立的 I/O 指令可以使用较短的地址编码,从而让地址译码变得更为方便。 另外从软件编程的角度来看, 有了单独的 IN 和 OUT 指令,可以很清晰地看出哪些是 I/O 操作, 哪些是存储器操作,让程序的结构变得清晰易懂。 而分开编址的缺点,我们刚才也都已经说完了,就不再重复了。
现在 CPU 可以用它习惯的方式和 I/O 接口进行交互, 这确实让事情变得简单了很多,但是这并不意味着 CPU 就可以做甩手掌柜了。 其实 I/O 接口只能帮 CPU 解决一些沟通交互上的琐碎细节, 真正的沟通的核心内容还得 CPU 自己来做。
- 01
- 中国网络防火长城简史 转载10-12
- 03
- 公告:博客近期 RSS 相关问题10-02