PCI 总线体系结构是一种层次式的( Hierarchical )体系结构。在这种层次式体系结构中, PCI 桥设备占据着重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树型结构。树的顶端是系统的 CPU ,它通过一个较为特殊的 PCI 桥设备—— Host / PCI 桥设备与根 PCI 总线( root pci bus )连接起来。下图 1 是一个较为典型的 PCI 总线体系结构图。
从上图也可以看出,作为一种特殊的 PCI 设备, PCI 桥又包括以下几种:
( 1 ) Host/PCI 桥:用于连接 CPU 与 PCI 根总线(注意, the first root bus 总是编号为 0 )。由于像 Memory Controller 这样的设备通常也被集成到 Host/PCI 桥设备芯片中,因此, Host/PCI 桥通常也被称为“北桥芯片组( North Bridge Chipset )”。
( 2 ) PCI/ISA 桥:用于连接遗留的 ISA 总线。通常,像 i8359A 中断控制器这样的设备也会被集成到 PCI/ISA 桥设备中,因此, PCI/ISA 桥通常也被称为“南桥芯片组( South Bridge Chipset )”。
( 3 ) PCI-to-PCI 桥:用于连接 PCI 主总线( primary bus )与次总线( secondary bus )。 PCI 桥所处的 PCI 总线称为“主总线”(即次总线的父总线),桥设备所连接的 PCI 总线称为“次总线”(即主总线的子总线)。
更多类型的 PCI 桥分类可以参见《 PCI Local Bus Specification 》 Revision2.2 的附录 D 。
本文以下部分假设读者对 PCI 总线规范有一定的了解,因此本文是为那些想要深入了解 Linux 内核是如何实现 PCI 总线驱动程序的 Kernel Hacker 而写的。
从前面的图 1 可以看出,在 PCI 总线体系结构中,有两个核心的概念存在: PCI 总线和 PCI 设备(桥设备是一种特殊的 PCI 设备)。基于对这两个核心概念的抽象, Linux PCI 总线驱动程序定义了两个关键的数据结构:结构类型 pci_bus 和结构类型 pci_dev ,以分别描述 pci 总线和 pci 设备。在此基础上, Linux PCI 总线驱动程序又将系统中当前存在的所有 PCI 总线的 pci_bus 结构组织成一张层次式的链表图,显然该链表图的顶层链表是系统中所有根总线的 pci_bus 结构链表,因此用一个 list_head 结构类型的全局变量 pci_root_buses 来描述该链表图中的顶层链表表头,也即根总线链表的表头。系统中当前存在的所有 PCI 设备的 pci_dev 结构也被组织成一条链表,称为“全局 pci 设备链表”,用一个 list_head 结构类型的变量 pci_devices 来表示该链表的表头。通过这两条总链表, Linux 内核就可以得到一张当前系统的 PCI 总线体系结构视图。
所有种类的 PCI 设备都可以用结构类型 pci_dev 来描述。更为准确地说,应该是每一个 PCI 功能,即 PCI 逻辑设备都唯一地对应有一个 pci_dev 设备描述符。该数据结构的定义如下( include/linux/pci.h ):
/*
* The pci_dev structure is used to describe both PCI and ISAPnP devices.
*/
struct pci_dev {
struct list_head global_list; /* node in list of all PCI devices */
struct list_head bus_list; /* node in per-bus list */
struct pci_bus *bus; /* bus this device is on */
struct pci_bus *subordinate; /* bus this device bridges to */
void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */
unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
u8 hdr_type; /* PCI header type (`multi' flag masked out) */
u8 rom_base_reg; /* which config register controls the ROM */
struct pci_driver *driver; /* which driver has allocated this device */
void *driver_data; /* data private to the driver */
dma_addr_t dma_mask; /* Mask of the bits of bus address this
device implements. Normally this is
0xffffffff. You only need to change
this if your device has broken DMA
or supports 64-bit transfers. */
/* device is compatible with these IDs */
unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];
/*
* Instead of touching interrupt line and base address registers
* directly, use the values stored here. They might be different!
*/
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
struct resource dma_resource[DEVICE_COUNT_DMA];
struct resource irq_resource[DEVICE_COUNT_IRQ];
char name[80]; /* device name */
char slot_name[8]; /* slot name */
int active; /* ISAPnP: device is active */
int ro; /* ISAPnP: read only */
unsigned short regs; /* ISAPnP: supported registers */
int (*prepare)(struct pci_dev *dev); /* ISAPnP hooks */
int (*activate)(struct pci_dev *dev);
int (*deactivate)(struct pci_dev *dev);
};
各成员的含义如下:
( 1 )全局链表元素 global_list :每一个 pci_dev 结构都通过该成员连接到全局 pci 设备链表 pci_devices 中。
( 2 )总线设备链表元素 bus_list :每一个 pci_dev 结构除了链接到全局设备链表中外,还会通过这个成员连接到其所属 PCI 总线的设备链表中。每一条 PCI 总线都维护一条它自己的设备链表视图,以便描述所有连接在该 PCI 总线上的设备,其表头由 PCI 总线的 pci_bus 结构中的 devices 成员所描述。
( 3 )总线指针 bus :指向这个 PCI 设备所在的 PCI 总线的 pci_bus 结构。因此,对于桥设备而言, bus 指针将指向桥设备的主总线( primary bus ),也即指向桥设备所在的 PCI 总线。
( 4 )指针 subordinate :指向这个 PCI 设备所桥接的下级总线。这个指针成员仅对桥设备才有意义,而对于一般的非桥 PCI 设备而言,该指针成员总是为 NULL 。
( 5 )无类型指针 sysdata :指向一片特定于系统的扩展数据。
( 6 )指针 procent :指向该 PCI 设备在/ proc 文件系统中对应的目录项。
( 7 ) devfn :这个 PCI 设备的设备功能号,也成为 PCI 逻辑设备号( 0 - 255 )。其中 bit[7:3] 是物理设备号(取值范围 0 - 31 ), bit[2:0] 是功能号(取值范围 0 - 7 )。
( 8 ) vendor :这是一个 16 无符号整数,表示 PCI 设备的厂商 ID 。
( 9 ) device :这是一个 16 无符号整数,表示 PCI 设备的设备 ID 。
( 10 ) subsystem_vendor :这是一个 16 无符号整数,表示 PCI 设备的子系统厂商 ID 。
( 11 ) subsystem_device :这是一个 16 无符号整数,表示 PCI 设备的子系统设备 ID 。
( 12 ) class : 32 位的无符号整数,表示该 PCI 设备的类别,其中, bit [ 7 : 0 ]为编程接口, bit [ 15 : 8 ]为子类别代码, bit [ 23 : 16 ]为基类别代码, bit [ 31 : 24 ]无意义。显然, class 成员的低 3 字节刚好对应与 PCI 配置空间中的类代码。
( 13 ) hdr_type : 8 位符号整数,表示 PCI 配置空间头部的类型。其中, bit [ 7 ]= 1 表示这是一个多功能设备, bit [ 7 ]= 0 表示这是一个单功能设备。 Bit [ 6 : 0 ]则表示 PCI 配置空间头部的布局类型,值 00h 表示这是一个一般 PCI 设备的配置空间头部,值 01h 表示这是一个 PCI-to-PCI 桥的配置空间头部,值 02h 表示 CardBus 桥的配置空间头部。
( 14 ) rom_base_reg : 8 位无符号整数,表示 PCI 配置空间中的 ROM 基地址寄存器在 PCI 配置空间中的位置。 ROM 基地址寄存器在不同类型的 PCI 配置空间头部的位置是不一样的,对于 type 0 的配置空间布局, ROM 基地址寄存器的起始位置是 30h ,而对于 PCI-to-PCI 桥所用的 type 1 配置空间布局, ROM 基地址寄存器的起始位置是 38h 。
( 15 )指针 driver :指向这个 PCI 设备所对应的驱动程序定义的 pci_driver 结构。每一个 pci 设备驱动程序都必须定义它自己的 pci_driver 结构来描述它自己。这个数据结构将在后面分析。
( 16 )无类型指针 driver_data :指向驱动程序为这个 PCI 设备所分配的一块私有数据区。通常,设备驱动程序会为它所支持的每一种设备类型定义一个特定于设备类型的数据结构,以描述该类型设备的信息。而且, driver 会为它所找到的每一个设备实例分配一个特定于该设备类型的数据结构实例。指针 driver_data 一般就用来指向这个有驱动程序所分配的数据结构实例。
( 17 ) dma_mask :用于 DMA 的总线地址掩码,一般来说,这个成员的值是 0xffffffff 。数据类型 dma_addr_t 定义在 include/asm/types.h 中,在 x86 平台上, dma_addr_t 类型就是 u32 类型。
( 18 ) vendor_compatible [ DEVICE_COUNT_COMPATIBLE ]和 device_compatible [ DEVICE_COUNT_COMPATIBLE ]:定义这个 PCI 设备与哪些设备相兼容。
( 19 )无符号的整数 irq :表示这个 PCI 设备通过哪根 IRQ 输入线产生中断,一般为 0 - 15 之间的某个值。
( 20 )资源数组 resource [ DEVICE_COUNT_RESOURCE ]:表示该设备可能用到的资源,包括: I/O 断口区域、设备内存地址区域以及扩展 ROM 地址区域。宏 DEVICE_COUNT_DEVICE 在 pci.h 头文件中被定义为常值 12 。但是 PCI 设备通常仅使用这 12 各资源区域中的一部分。其中, resource [ 5 : 0 ]分别对应于配置空间中的 BAR0 - BAR5 (注意,桥设备只有 BAR0 和 BAR1 ), resource [ 6 ]对应于配置空间中的 ROM 基地址寄存器所描述的 ROM 区域,而 resource [ 10 : 7 ]分别对应于桥设备的地址过滤窗口。
( 21 )数组 dma_resource [ DEVICE_COUNT_DMA ]:用于 DMA 的资源, DEVICE_COUNT_DMA 为 2 。
( 22 )数组 irq_resource [ DEVICE_COUNT_IRQ ]:用于 IRQ 的资源, DEVICE_COUNT_IRQ 为 2 。
( 23 ) name [ 80 ]数组:定义这个 PCI 逻辑设备的名字字符串。
( 24 ) slot_name [ 8 ]数组:如果这个 PCI 逻辑设备是通过 PCI 插槽连接到 PCI 总线上的,则 slot_name 数组表示该插槽的名字字符串。
( 25 ) active :被 ISAPnP 模块用来表示设备是否被激活。
( 26 ) ro :被 ISAPnP 模块用来表示设备是否为只读设备。
( 27 ) regs :被 ISAPnP 模块用来表示设备设备所支持的寄存器个数。
( 28 )函数指针 prepare 、 activate 和 deactivate :都是仅被 ISAPnP 模块钩挂的函数指针。
该结构定义在 include/linux/pci.h 头文件中,如下所示:
struct pci_bus {
struct list_head node; /* node in list of buses */
struct pci_bus *parent; /* parent bus this bridge is on */
struct list_head children; /* list of child buses */
struct list_head devices; /* list of devices on this bus */
struct pci_dev *self; /* bridge device as seen by parent */
struct resource *resource[4]; /* address space routed to this bus */
struct pci_ops *ops; /* configuration access functions */
void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procdir; /* directory entry in /proc/bus/pci */
unsigned char number; /* bus number */
unsigned char primary; /* number of primary bridge */
unsigned char secondary; /* number of secondary bridge */
unsigned char subordinate; /* max number of subordinate buses */
char name[48];
unsigned short vendor;
unsigned short device;
unsigned int serial; /* serial number */
unsigned char pnpver; /* Plug & Play version */
unsigned char productver; /* product version */
unsigned char checksum; /* if zero - checksum passed */
unsigned char pad1;
};
各成员的含义如下:
( 1 )链表元素 node :对于 PCI 根总线而言,其 pci_bus 结构通过 node 成员链接到本节一开始所述的根总线链表中,根总线链表的表头由一个 list_head 类型的全局变量 pci_root_buses 所描述。而对于非根 pci 总线,其 pci_bus 结构通过 node 成员链接到其父总线的子总线链表 children 中(见下面)。
( 2 ) parent 指针:指向该 pci 总线的父总线,即 pci 桥所在的那条总线。
( 3 ) children 指针:描述了这条 PCI 总线的子总线链表的表头。这条 PCI 总线的所有子总线都通过上述的 node 链表元素链接成一条子总线链表,而该链表的表头就由父总线的 children 指针所描述。
( 4 ) devices 链表头:描述了这条 PCI 总线的逻辑设备链表的表头。除了链接在全局 PCI 设备链表中之外,每一个 PCI 逻辑设备也通过其 pci_dev 结构中的 bus_list 成员链入其所在 PCI 总线的局部设备链表中,而这个局部的总线设备链表的表头就由 pci_bus 结构中的 devices 成员所描述。
( 5 )指针 self :指向引出这条 PCI 总线的桥设备的 pci_dev 结构。
( 6 )资源指针数组 resource [ 4 ]:指向应路由到这条 pci 总线的地址空间资源,通常是指向对应桥设备的 pci_dev 结构中的资源数组 resource [ 10 : 7 ]。
( 7 )指针 ops :指向一个 pci_ops 结构,表示这条 pci 总线所使用的配置空间访问函数。下一节将详细讨论这个数据结构。
( 8 )无类型指针 sysdata :指向系统特定的扩展数据。
( 9 )指针 procdir :指向该 PCI 总线在/ proc 文件系统中对应的目录项。
( 10 ) number :这条 PCI 总线的总线编号( bus number ),取值范围 0 - 255 。
( 11 ) primary :表示引出这条 PCI 总线的“桥设备的主总线”(也即桥设备所在的 PCI 总线)编号,取值范围 0 - 255 。
( 12 ) secondary :表示引出这条 PCI 总线的桥设备的次总线号,因此 secondary 成员总是等于 number 成员的值。取值范围 0 - 255 。
( 13 ) subordinate :这条 PCI 总线的下属 PCI 总线( Subordinate pci bus )的总线编号最大值,它应该等于引出这条 PCI 总线的桥设备的 subordinate 值。
( 13 ) name [ 48 ]:这条 PCI 总线的名字字符串。
( 14 ) vendor 和 device :表示引出这条 PCI 总线的桥设备的厂商 ID 和设备 ID 。
( 15 ) serial :系列号。
( 16 ) pnpver : Plug & Play 版本号。
( 17 ) productver :产品版本号。
( 18 ) chechsum : pci_bus 结构的校验和,因改为 0 。
( 19 ) pad1 :为了使 pci_bus 结构的大小沿内存边界对齐而设立的成员,无实际意义。
Bus 0 |
PCI/PCI 桥 Primary = 0 Secondary=1 Subordinate=2 |
PCI/PCI 桥 Primary = 1 Secondary=2 Subordinate=2 |
Bus 2 |
Bus 1 |
图 2 PCI 总线编号 |
下面对上述结构中的总线编号进行一下说明。假定这样一个 PCI 总线体系结构:根总线 0 上有一个 PCI 桥,它引出子总线 bus 1 , bus 1 上又有一个 PCI 桥引出 bus 2 ,如下图 2 所示:
PCI 桥的配置空间头部中定义桥两侧的主、次总线编号,以及桥后面的下级总线编号的最大可能值。因此在上图中, bus 0 总线的 pci_bus 结构中的 number 、 primary 、 secondary 都应该为 0 ,因为它是通过 Host/PCI 桥引出的根总线;而 bus 1 总线的 pci_bus 结构中的 number = secondary = 1 ,而 bus 1 的 primary 应该为 0 ;而 bus 2 总线的 pci_bus 结构中的 number=secondary = 2 ,其 primary 则应该等于 1 。这三条总线的 subordinate 值都应该等于 2 。
前面已经讲过,所有的 PCI 设备都通过其 pci_dev 结构中的 global_list 成员链接一条“全局 pci 设备链表” pci_devices ,表头 pci_devices 定义在 drivers / pci / pci.c 文件中:
LIST_HEAD(pci_device) ;
另外,同属一条 PCI 总线上的所有 PCI 设备也通过其 pci_dev 结构中的 bus_list 成员链接成一条局部这条 PCI 总线的“总线设备链表”,表头则由该 PCI 总线的 pci_bus 结构中的 devices 成员所定义。
global_list |
bus_list |
…… |
pci_dev 结构 |
global_list |
bus_list |
…… |
pci_dev 结构 |
global_list |
bus_list |
…… |
pci_dev 结构 |
pci_devices |
pci_bus.devices |
图 3 全局 PCI 设备链表与局部总线链表 |
下图 3 描述可以很清楚地描述可以很清楚地描述上述这两重链表的关系
■ 遍历 PCI 设备链表的辅助宏
为了更为方便地遍历上述两类 PCI 设备链表(全局 PCI 设备链表和局部总线设备链表), Linux 在头文件中 pci.h 定义了几个辅助宏,从而使得遍历链表的代码更为简单、易懂。它们是:
(1 ) 宏 pci_for_each_dev(dev) :正向遍历全局设备链表 pce_devices 中的每一个 PCI 设备,参数 dev 是一个 pci_dev 结构类型的指针。 其定义如下:
# define pci_for_each_dev ( dev ) /
for(dev=pci_dev_g(pci_devices.next);dev!=pci_dev_g(&pci_devices);dev=pci_dev_g(dev->global_list.next))
上述定义中的宏 pci_dev_g() 用于将一个 list_head 类型的指针转换为一个 pci_dev 类型的指针(下面谈及)。对这个宏的典型使用方法如下所示:
pci_dev * dev ;
……
pci_for_each_dev(dev)
{
……/* 对 dev 所指向的每一个 pci 设备进行处理 */
}
( 2 )宏 pci_for_each_dev_reverse(dev) :逆向遍历全局设备链表 pci_devices 中的每一个 pci 设备。参数 dev 同样也是一个 pci_dev 结构类型的指针。其定义如下:
# define pci_for_each_dev_reverse ( dev ) /
for(dev=pci_dev_g(pci_devices.prev);dev!=pci_dev_g(&pci_devices);dev=pci_dev_g(dev->global_list.prev))
显然,上述宏将从全局设备链表中的最后一个元素(由 pci_devices.prev 所指向)开始逆向遍历整个链表。该宏的使用与 pci_for_each_dev() 宏相同,只是遍历方向相反而已。
( 3 )宏 pci_dev_g(n) 和 pci_dev_b(n) :宏 pci_dev_g() 用来将与全局设备链表对应的 list_head 类型指针 n 转换为 pci_dev 类型的指针,宏 pci_dev_b() 用来将与总线局部设备链表对应的 list_head 类型指针 n 转换为 pci_dev 类型的指针。实际上,它们都是通过 list_entry() 宏来实现实际的转换工作,如下所示:
#define pci_dev_g(n) list_entry(n, struct pci_dev, global_list)
#define pci_dev_b(n) list_entry(n, struct pci_dev, bus_list)
系统中当前存在的所有根总线都通过其 pci_bus 结构中的 node 成员链接成一条全局的根总线链表,其表头由 list 类型的全局变量 pci_root_buses 来描述。它的定义如下( drivers/pci/pci.c ):
LIST_HEAD(pci_root_buses);
CPU1 |
主存 |
Host/PCI 桥 |
root bus 0 |
PCI/PCI 桥 |
pci bus 1 |
PCI/PCI 桥 |
pci bus 3 |
PCI/PCI 桥 |
pci bus 2 |
CPU2 |
Host/PCI 桥 |
root bus 4 |
PCI/PCI 桥 |
pci bus 5 |
PCI/PCI 桥 |
pci bus 6 |
图 4 多根总线的 PCI 体系结构 |
而根总线下面的所有下级总线则都通过其 pci_bus 结构中的 node 成员链接到其父总线的 children 链表中。这样,通过这两种 PCI 总线链表, linux 内核就将所有的 pci_bus 结构以一种倒置树的方式组织起来。假定对于图 4 所示的 PCI 总线体系结构,它所对应的总线链表结构如图 5 所示。
类似地,宏 pci_bus_b(n) 则被用来将 list_head 类型的指针 n 转换为 pci_bus 类型的指针。该宏也是定义在 include/linux/pci.h 头文件中:
#define pci_bus_b(n) list_entry(n, struct pci_bus, node)