下面对配置空间中一些关键的字段进行简要说明,详细的字段含义,以及本书没有提到的字段含义,请参考PCI总线规范。
1.Vendor ID和Device ID
Vendor ID用来标识设备的制造厂商,而Device ID则标识特定的设备,Vendor ID是由PCI规范组织统一分配的,因此不会重复(类似以太网接口卡的MAC地址),Device ID则是由厂家自行分配的,一般情况下,Device ID和厂家ID结合起来,可以精确识别一种设备。在Hello China的当前实现中,定义了下列结构,来对总线上的物理设备(并不一定是PCI接口设备)进行标识。
其中,总线类型(dwBusType)字段确定该结构中union部分的具体含义。例如,假设总线类型为BUS_TYPE_PCI,则按PCI_Identifier结构解释该标识对象,于是在PCI_Identifier结构中,wVendor就是Vendor ID,wDevice则是Device ID字段。为了进一步区分设备,又把Class code字段和Header Type字段纳入(dwClass成员,实际上只有高24bit有效,最低的8bit是Revision ID),这样这些字段结合起来,可以作为PCI设备的唯一标识。有些情况下,可能需要一种“模糊”标识,例如,要标识一个特定厂家(比如Intel)的所有设备,这样就只需要Vendor ID字段就可以了,其他字段不需要给出。因此,ucMask字段指明了PCI_Identifier结构中哪个字段有效。例如,假设ucMask字段取值IDENTIFIER_MASK_PCI_VENDOR,则该结构中只有Vendor ID字段有效,如果wMask字段取值IDENTIFIER_MASK_PCI_VENDOR|IDENTIFIER_MASK_PCI_DEVICE,则wVendor和wDevice字段同时有效。
2.Class code
Class code是设备类代码,用来描述设备的功能。这个字段长24bit,进一步分成三部分。
(1)最高的一个字节(偏移在0x0B处),是一个基类编号,粗略地描述了一个PCI设备的功能。比如,如果该字节是0x01,则说明对应的PCI设备是一个大容量存储设备的控制器,如果是0x02,则对应的PCI设备是一个网络控制器,等等。
(2)中间一个字节(偏移在0x0A处),是一个子类编号,进一步描述了设备的功能。比如,如果基类编号是0x02,子类编号是0x00,则说明对应的设备是以太网接口控制器,如果子类编号是0x01,则说明对应的设备是令牌环接口控制器。
(3)最后一个字节(偏移在0x09处),是一个编程接口字段,该字段可以用来进一步对PCI设备的功能做出区分,一般情况下,这个字节可以保持为0,也可以由PCI设备制造商根据自己的定义指定。
在对PCI设备进行枚举的时候,就是根据Class code字段判断设备功能的。该字段也用来作为设备标识符的一部分。
3.Header type
Header type是头部类型。根据PCI规范的定义,对所有PCI设备保留了256B的配置空间,其中前64B是预定义的,后续192B则根据设备的具体情况具体实现。对于预先定义的64B首部,根据PCI规范(Resivion 2.2),目前有三种情形。
(1)普通的PCI设备,这类设备的头部组织在本书中已经给出。
(2)PCI-PCI桥接设备(PCI-PCI桥),这类设备的头部组织在本书中已经给出。
(3)PCI-CardBus桥接设备(PCI-CardBus桥),这类设备的组织本书中没有给出。
上述三种情形预定义头部开始的16B的组织结构都是一样的,从16B往后(17~64B),根据不同的头部类型有不同的组织。Header type字段就是用来标识不同的头部的。该字段位于PCI配置空间偏移0x0E处,其中,该字段的最高比特(第7比特)用来标明当前的PCI设备是单功能设备(该比特为0)还是多功能设备(该比特为1)。0~6bit则用来区分不同的PCI配置空间头部类型。如果这7比特为0,则说明配置空间的头部是0型头部(普通的PCI设备头部),如果这7比特的值为1,说明配置空间的头部类型是1,即PCI-PCI桥设备的配置头部,如果是2,则是PCI-CardBus头部。因此,如果当前的PCI设备是一个多功能的PCI-PCI桥接设备,则Header Type的数值应该为0x81,如果是一个普通的单功能PCI设备,则该字段的值是0x00。
4.Base Address Register
PCI接口的硬件设备有一个突出的特性就是动态配置能力,即用于操作设备的IO端口、内存映射位置等,可以不用事先固定,推迟到系统引导的时候由软件根据系统资源情况进行统一配置。而传统的基于ISA总线的设备在安装的时候必须静态地完成IO端口分配,然后通过硬件跳线的方式,把硬件设备连接到计算机总线上,这样十分不方便,而且很容易出现冲突。
PCI设备就是通过Base Address Register来实现动态配置的。配置软件把分配给PCI设备的IO端口范围或内存映射地址范围写入这些寄存器,这样对设备的后续操作就可以通过写入的端口范围或寄存器进行。开始的时候,这些寄存器保持缺省值,并且设备所需要的IO端口范围大小(或内存映射区域的大小)也包含在这些寄存器中。对于普通的PCI设备,共有六个Base Address Register,这样一个普通的PCI设备可以申请6个IO地址范围或内存映射空间。而对于PCI-PCI桥,则只有两个Base Address Register,而且一般情况下,不使用这两个寄存器(有的桥设备也需要IO端口或内存映射空间来进行操作,此时这两个寄存器就得到了应用)。
一般情况下,对Base Address Register的操作有两种。
(1)获得PCI所需要的IO端口范围大小,或内存映射区域的数量。
(2)向Base Address Register写入分配给设备的IO端口范围,或者内存区域。
在进行上述两种操作前,一个很重要的前提就是如何得知Base Address Register是寄存了IO端口范围还是内存映射空间。按照PCI的规范,每个Base Address Register必须符合如图9-6所示的结构。
图9-6 Base Address Register的结构
其中,每个寄存器的最低比特(比特0)确定了该寄存器是映射到IO空间还是内存空间。如果该比特为0,则说明对应的寄存器映射到内存空间,如果为1,则映射到IO端口空间。对于映射到内存空间的Base Address Register,其次低的三个比特(比特1、2、3)是控制比特,对所映射的内存区域特性进行描述。表9-1是这三个比特的取值,以及对应的含义。
表9-1 三个控制比特的含义
对于“可预取(Prefetch)”,在这里进行进一步说明。许多情况下,为了提高效率,CPU都实现了cache机制,即在CPU的本地设置一个本地缓存,大小可以从512KB到2MB不等(有的甚至更大),有的CPU可能设置了2级甚至3级cache,这样每当CPU要读取一个内存单元的数据时,首先检索本地cache,因为从cache中读取数据的速度比从内存中读取数据的速度快几个数量级。如果能够从cache中获得期望的内容(叫做cache命中),则就节省了从内存中读取相应内容花费的时间,提高了整体效率。当然,如果要读取的数据不在cache中(叫做cache不命中),则CPU会到物理内存中读取相应的数据,在读取的同时,还把与该数据单元相邻的一块连续内存(如4KB)读入CPU的本地cache,这样后续CPU如果再读取同样的内存单元,或者与该内存单元相邻的内存单元,就可以直接从cache中读取了。这种结构得以实现的核心基础就是所谓的“局部性原理”。
当然,这种加快读取或写入操作的方式,对于通常的内存来说是没有问题的,CPU硬件可以保持数据的一致性。但对于映射到内存空间中的设备寄存器,则有的情况下可能不适合这样的操作。比如,有的硬件设备的寄存器读取之后就进行复位,然后根据设备的状态进一步设置为其他的值。这样的寄存器读取的时机就十分关键了,在时刻T1的时候进行读取,与在时刻T2的时候进行读取得到的结果可能是不一样的。因此,如果有这类特征的硬件寄存器被映射到CPU的内存空间,那么就不适合预先读取(或写入)。但有的设备寄存器却没有这种限制。因此,为了兼顾这两种情况,在Base Address Register中专门设置了比特位来区分这两种情况。
在当前版本Hello China的实现中,对于可预取的设备映射内存采取与普通的物理内存一样的访问策略(可预读,写的时候直接写入,详细信息请参考第5章),而对于不可预读的设备映射内存,则采取另外的禁止缓冲的访问策略,所有对该区域的内存读取操作直接从内存中读取(而不经过cache),在IA32硬件平台上,这可以通过设置合适的页面标志来实现。
而对于映射到IO端口空间的Base Address Register,情况相对简单,最后一个比特为1,第二个比特为0,其他的比特则保存了映射到的IO端口范围。
根据PCI规范(Revision 2.2)的描述,可以通过下列方式来获取IO端口范围大小或内存映射区域大小。(www.xing528.com)
(1)保存原来Base Address Register的值。
(2)写入0xFFFFFFFF到对应的Base Address Register。
(3)再次读取对应的寄存器的值。
假设最后一次读取的内容为size,则IO端口范围大小或内存映射范围大小可以按照下列方法计算。
(1)清除保留的比特,对于映射到内存的寄存器,清除0~3比特,对于映射到IO端口的寄存器,清除0比特。
(2)对经过上述步骤处理的结果取反,然后加1。
(3)得到的32bit数值就是内存映射范围的大小,如果是映射到IO端口范围内的大小,则忽略最高的16bit(16~31bit)。
在当前版本的Hello China的实现中,实现上述计算的函数如下。
计算出Base Address Register的尺寸后,就可以根据系统资源的情况为这些寄存器分配资源了。分配好资源之后,再把分配的资源(IO端口范围或内存映射范围)写入对应的寄存器,这样对设备的后续访问就可以直接通过分配的IO端口或内存映射区域进行。
最后,PCI规范对普通的PCI设备定义了六个Base Address Register,这样从理论上说,一台PCI设备最多可以定义6个IO端口或内存映射空间用于对设备的操作。但实际上用不了这么多的空间资源,比如,一般的设备,可能仅仅使用两个Base Address Register,一个用来指定IO端口范围,另外一个用来指定内存映射范围,这样剩余的没有使用的寄存器就全部设置为1。为了便于移植,按照PCI规范的建议,对于PCI设备尽量采用内存映射区域,因此,在Hello China的设计中,对于设备驱动程序的编写建议尽量采用内存区域对设备进行控制。
5.Interrupt Line和Interrupt Pin
Interrupt Line和Interrupt Pin两个字段给出了PCI设备的中断连接信息。其中,第一个字段Interrupt Line描述了设备的中断输入与中断控制器的哪条引脚连接。比如,在PC上,中断控制器一般采用两块8259芯片向外提供15个中断输入,这样Interrupt Line字段就指明了当前的PCI设备具体连接到8259的哪条引脚上。一般情况下,这个字段由BIOS(或固件)填写,操作系统和设备驱动程序可以读取这个字段,从而获得设备的中断向量号。在PC上,这个字段的值可以是0~15的任何数字,16~254保留,如果是255,则说明该设备没有中断输入。
另外一个字段Interrupt Pin则说明了对应的PCI设备的中断输入连接到PCI总线的哪条中断输入上。PCI总线有四条中断连接线—INTA、INTB、INTC和INTD,PCI设备的中断输入可以与这四条连接线的任何一条连接(按照PCI规范定义,对于单功能设备,强烈建议连接到INTA,对于多功能设备,则可以连接到INTA~INTD中的任何一条或几条),这四条中断连接线又跟中断控制器(PC上的8259芯片)的四条中断输入引脚进行连接。Interrupt Pin字段就指出了对应的PCI设备的中断输入跟INTA-INTD的哪条连接。如果该字段的值为1,则是跟INTA连接,如果是2,则是跟INTB连接,如果设备没有中断输入,则该字段设置为0,5~255是保留的设置。
在Hello China的当前实现中,对于PCI设备的这个字段只进行读取操作,即采用BIOS分配的默认值,而不会另外分配新的数值。如果设备驱动程序要获取设备的中断向量号,则建议调用DeviceManager对象的特定接口(函数)来获取,不建议直接读取Interrupt Line字段来获取。
6.Primary、Secondary和Subordinate总线号
Primary、Secondary和Subordinate三个字段目前只会在01型头部(PCI-PCI桥接器设备)中出现。一般情况下,PCI-PCI桥设备连接了两条PCI总线,一条总线是主总线(Primary BUS),就是PCI-PCI桥所在的总线,另外一条总线就叫做二级总线(Secondary BUS),这样Primary字段和Secondary字段就是主总线号和二级总线号。
但是PCI-PCI桥设备的二级总线还可能又连接了另外一个桥接器,另外一个桥接器又连接了第三条总线……这样不断连接,会组成一个复杂的树形结构(按照PCI规范,最多可以有256条总线通过桥设备连接在一起),相对每个PCI-PCI桥来说,其二级总线所连接的所有总线都称为该PCI-PCI桥的下级总线。这样Subordinate字段就是一个PCI-PCI桥设备所连接的下级总线中总线号最大的那条总线的总线标识号。图9-7是一个由两个PCI-PCI桥设备组成的典型的硬件配置结构,总共有三条PCI总线。
图9-7 一个由三条PCI总线组成的总线结构
在这个硬件系统中,HOST-PCI桥设备连接了总线0(BUS 0),Bridge 1作为一个PCI设备出现在总线0上,因此Bridge 1桥接设备的主总线(Primary BUS)就是总线0。Bridge1连接了总线1,因此,其二级总线号(Secondary字段)就是1,同样,在BUS 1上Bridge2作为一个普通的PCI设备出现,因此,Bridge 2的主总线号就是1,Bridge 2所连接的总线2,就是其二级总线(Bridge 2的Secondary BUS是2)。由于Bridge 2的二级总线上没有连接其他的桥接设备,因此Bridge 2的Subordinate字段就是2,同样,Bridge 1的Subordinate字段的值应该是2,因为在Bridge 1的下级总线上最大的总线号是2。
之所以在PCI-PCI桥设备中记录Subordinate总线号,是为了访问的方便。一旦CPU发起一个配置空间的访问(该访问使用总线号、设备号和功能号来定位一个具体的PCI功能设备),PCI-PCI桥设备就把访问请求的目标总线号与Secondary总线号和Subordinate总线号进行对比,如果发现请求的目标设备所在的总线号在Secondary和Subordinate之间,则该PCI-PCI桥会转发该访问,如果不在上述两个数字之间,则说明CPU访问的目标设备不在该PCI-PCI桥以下的总线上,因此该PCI-PCI桥就不做任何处理。
这些字段都是在PCI总线初始化的时候填写的,在PC上,这个初始化操作由BIOS软件完成。为可靠起见,在Hello China的实现过程中,会重新对PCI总线进行初始化,不过在初始化的过程中,如果发现PCI设备已经配置,则接收BIOS的配置不再进行更改。但如果有的PCI设备(或者PCI-PCI桥)没有被BIOS配置(很可能发生这种情况),则Hello China会配置这些没有经过BIOS配置的PCI设备。详细信息请参考9.5节。
7.IO Base和IO Limit
IO Base和IO Limit两个字段出现在1型预定义头部中(PCI-PCI桥设备的头部),其含义与PCI-PCI桥设备的Secondary和Subordinate字段类似,用于过滤对连接在PCI桥设备的下级总线上的设备的读/写。一旦PCI总线上的主设备发起一个读/写请求(不是配置空间的读/写,而是通过IO端口直接访问PCI设备),PCI桥设备就会判断请求的目标地址是否在IO Base和IO Limit限定的范围内。如果在限定的范围内,则PCI桥会向下级设备“转接”这个读/写请求,否则,如果请求的地址不在IO Base和IO Limit范围内,则PCI桥设备不作任何动作。
其中,IO Base字段给出了IO端口的起始地址,而IO Limit字段则给出了IO端口的范围大小,这样两者结合起来,就可以确定一个端口范围。对PCI设备的访问,凡是目标地址落在这个范围之内的请求,都会被PCI桥设备“转接”。因此,IO Base和IO Limit所确定的端口范围应该是PCI桥接器所有下级PCI设备的IO端口范围的“并集”,即IO Base确定的端口地址应该是所有该PCI桥下面的PCI设备的端口范围下界的最小值,而IO Base+IOLimit则是所有该PCI桥下面的PCI设备的端口范围上界的最大值。
需要注意的是,IO Base和IO Limit都是以256B为边界的,即如果IO Base取值为0xAB,则确定的IO端口范围的下界是0x00。同样地,IO Limit也是以256B为边界的,如果IO Limit的取值为0x01,则确定的端口范围实际上是0x0100,即256B。这样如果IO Base取值为0xAB00,IO Limit取值为0x01,则实际确定的IO端口范围是[0xAB00,0xABFF]。
这样IO Base和IO Limit就可以确定16bit的IO端口范围,这在Intel的硬件平台上是足够了,但有些CPU的端口范围可能是32bit的,因此,1型头部中的IO Base Upper 16字段和IO Limit Upper 16字段就作为32bit IO端口范围的扩展,与IO Base和IO Limit结合起来共同确定一个32bit的IO端口范围。在Hello China目前版本的实现中,目标CPU是IA32,因此没有考虑这种32bit端口的情况,但Hello China的相关数据结构的设计却充分考虑了这种存在,将来如果要实现支持32bit端口范围的版本,会十分容易。
8.Memory Base和Memory Limit
Memory Base和Memory Limit两个字段也是出现在1型预定义头部中(PCI-PCI桥设备头部),其含义与IO Base和IO Limit相同,用于完成对当前桥设备下的PCI设备的内存映射区域的访问过滤。
9.Prefetch memory base和Prefetch memory limit
Prefetch memory base和Prefetch memory limit两个字段也是出现在1型预定义头部中(PCI-PCI桥设备头部),其含义与IO Base和IO Limit相同,用于完成对当前桥设备下的PCI设备的可预取内存映射区域的访问过滤。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。