浅谈Linux PCI设备驱动(一)
要弄清楚Linux PCI设备驱动,首先要明白,所谓的Linux PCI设备驱动实际包括Linux PCI设备驱动和设备本身驱动两部分。不知道读者理不理解这句话,本人觉得这句话很重要,对于PCI、USB这样的驱动来说,必须要理解这个概念,才能明白该如何看待Linux下的PCI和USB以及类似的总线型的驱动。理由也很简单,就是Linux PCI驱动是内核自带的,或者说内核帮你写好了!而我们需要完成的就是设备本身的驱动,比如网卡驱动等。当然,并不是说内核帮咱们写好了Linux PCI驱动我们什么就不用做了,至少你要明白内核大致都干了些什么,这样你才能明白你该干什么,如何完成设备本身的驱动。这跟我们学习操作系统时要学习很多系统调用接口一样的道理,不知道这些接口,怎么用操作系统或者说操作系统给你提供的功能呢? 所以这里我们就来研究下Linux PCI驱动到底都干了些什么,以便我们在此基础上完成我们设备本身的驱动。
在http://tldp.org/LDP/tlk/dd/pci.html这篇文章里(整本书叫做The Linux Kernel,中文翻译见http://oss.org.cn/ossdocs/linux/kernel/ 本文也参考了该中文翻译) 提到了:
Linux PCI 初始化代码逻辑上分为三个部分:
(1)PCI设备驱动程序
这个伪设备驱动程序从总线0开始查询PCI系统并且定位系统中所有的PCI设备和PCI桥。它建立一个可以用来描述这个PCI系统拓朴层次的数据结构链表。并且对所有的发现的PCI桥编号。
(2)PCI BIOS
这个软件层提供在bib-pci-bios归约中描述的服务。虽然Alpha AXP不提供BIOS服务,在其Linux版本中包含了相应的功能。
(3)PCI Fixup
与特定系统相关的PCI初始化修补代码
而这里主要就是探讨Linux PCI设备驱动,会在最后列出一段包含设备本身驱动的示例代码,仅供参考。
一、概述及简介
PCI(Periheral Component Interconnect)有三种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间。其中,PCI I/O空间和PCI内存地址空间由设备驱动程序使用,而PCI配置空间由Linux内核中的PCI初始化代码使用,这些代码用于配置PCI设备,比如中断号以及I/O或内存基地址。所以这里的PCI设备驱动就是要大致描述对于PCI设备驱动,Linux内核都帮我们做了什么(主),接着就是我们应该完成什么(次)。
(1)Linux内核做了什么
简单的说,Linux内核主要就做了对PCI设备的枚举和配置;这些工作都是在内核初始时完成的。
枚举:对于PCI总线,有一个叫做PCI桥的设备用来将父总线与子总线连接。作为一种特殊的PCI设备,PCI桥主要包括以下三种:
(1)Host/PCI桥: 用于连接CPU与PCI根总线,第1个根总线的编号为0。在PC中,内存控制器也通常被集成到Host/PCI桥设备芯片中,因此Host/PCI桥通常也被称为“北桥芯片组(North Bridge Chipset)”。
(2)PCI/ISA桥: 用于连接旧的ISA总线。通常,PCI中类似i8359A中断控制器这样的设备也会被集成到PCI/ISA桥设备中。因此,PCI/ISA桥通常也被称为“南桥芯片组(South Bridge Chipset)”
(3)PCI-to-PCI桥(以下称为PCI-PCI桥): 用于连接PCI主总线(Primary Bus)和次总线(Secondary Bus)。PCI-PCI桥所处的PCI总线称为主总线,即次总线的父总线;PCI-PCI桥所连接的PCI总线称为次总线,即主总线的子总线。
下图摘自PCI Local Bus Specification Revision 2.1,可以看到PCI-PCI桥的Class Code(见图3)就是0x060400。
CPU通过Host/PCI桥与一条PCI总线相连,处在这种位置上的PCI总线称为根总线。PC机中通常只有一个Host/PCI桥,在一条PCI总线的基础上,可以再通过PCI桥连接到其他次一层的总线,例如通过PCI-PCI桥可以连接到另一条PCI总线,通过PCI-ISA桥可以连接到一条ISA总线。事实上,现代PC机中的ISA总线正是通过PCI-ISA桥连接在PCI总线上的。这样,通过使用PCI-PCI桥,就构筑起了一个层次的、树状的PCI系统结构。对于上层的总线而言,连接在这条总线上的PCI桥也是一个设备。但是这是一种特殊的设备,它既是上层总线上的一个设备,实际上又是上层总线的延伸。 所谓枚举,就是从Host/PCI桥开始进行探测和扫描,逐个“枚举”连接在第一条PCI总线上的所有设备并记录在案。如果其中的某个设备是PCI-PCI桥,则又进一步再探测和扫描连在这个桥上的次级PCI总线。就这样递归下去,直到穷尽系统中的所有PCI设备。其结果,是在内存中建立起一棵代表着这些PCI总线和设备的PCI树。每个PCI设备(包括PCI桥设备)都由一个pci_dev结构体来表示,而每条PCI总线则由pci_bus结构来表示。你有通过PCI桥建立起的硬件设备树,我有内存中通过数据结构构建的软件树,多么和谐 呵呵。
图1 PCI系统示意图
配置:PCI设备中一般都带有一些RAM和ROM 空间,通常的控制/状态寄存器和数据寄存器也往往以RAM区间的形式出现,而这些区间的地址在设备内部一般都是从0开始编址的,那么当总线上挂接了多个设备时,对这些空间的访问就会产生冲突。所以,这些地址都要先映射到系统总线上,再进一步映射到内核的虚拟地址空间。而所谓的配置就是通过对PCI配置空间的寄存器进行操作从而完成地址的映射(只完成内部编址映射到总线地址的工作,而映射到内核的虚拟地址空间是由设备本身的驱动要做的工作)。
(2)Linux内核怎么做的
这里首先要说明的是,对于PCI的设备初始化(即上面提到的枚举和配置工作),PC机的BIOS和Linux内核都可以做。一般而言,只要是采用PCI总线的PC机,其BIOS就必须提供对PCI总线操作的支持,因而称为PCI BIOS。而且最早Linux内核也是通过这种BIOS调用的方式来获取系统中的PCI设备信息的,但是不是所有的平台都有BIOS(如某些嵌入式系统),并且在实践中也发现有些母板上的PCI BIOS存在这样那样的问题,所以后来就改由Linux内核自己动手了,自己动手 丰衣足食 呵呵。不过,Linux内核还是很体贴的在make menuconfig的选项里为我们提供了自己选择的权利,即PCI access mode,里面提供了四个选项分别是BIOS、MMconfig、Direct和Any。Direct方式就是抛开BIOS而由内核自己完成初始化工作的意思。
二、开始我们的枚举与配置之路
注:为了更清晰,简单的描述PCI设备的初始化过程(因为2.4.18中还没有引入设备驱动模型,这样可以让我们专心研究PCI设备驱动本身)。这里是对Linux-2.4.18的内核进行的分析,主要原因大家从参考资料中也应该能明白,这里很多就是参考了[1]中的资料来分析的。如果想学PCI设备驱动,那么应该好好看看[1]的第八章中的PCI总线一节。然后再能找到一个驱动的例子代码看看,就可以说算是对PCI设备驱动入门了,当然,前提是都看懂了 呵呵。
废话少说,下面进入正题。前面提到了PCI有三种地址空间,其中的PCI配置空间是给Linux内核中的PCI初始化代码用的,也就是我们这里的枚举与配置时用到的。那么这个PCI配置空间里放的是什么东西呢,显然应该是寄存器,称为配置寄存器组。当PCI设备上电时,硬件保持未激活状态。即该设备只会对配置事务做出响应。上电时,设备上不会有内存和I/O端口映射到计算机的地址空间;其他设备相关的功能,例如中断报告,也被禁止。
PCI标准规定每个设备的配置寄存器组最多可以有256字节的连续空间,其中开头的64字节的用途和格式是标准的,称为配置寄存器的头部。系统中提供一些与硬件有关的机制,使得PCI配置代码可以检测在一个给定的PCI总线上所有可能的PCI配置寄存器头部,从而知道哪个PCI插槽上目前有设备,哪个插槽上暂无设备。这是通过读PCI配置寄存器头部上的某个域完成的(一般是“Vendor ID" 域)。如果一个插槽上为空,上述操作会返回一些错误返回值,如0xFFFFFFFF。这种头部(指64字节头部)又有三种,其中“0型”(type 0)头部用于一般的PCI设备,“1型”头部用于各种PCI-PCI桥, “2型”头部是用于PCI-CardBus桥的,CardBus是笔记本电脑中使用的总线,我们不关心。而64字节头部中的16个字节中又包含着有关头部的类型、设备的种类、设备的一些性质、由谁制造等等信息。根据这16个字节中提供的信息,来确定应该怎样进一步解释和处理剩余头部中的48个字节。对于这16个字节的地址,include/linux/pci.h中定义了这样一些常数。
#define PCI_VENDOR_ID 0x00 /* 16 bits */
#define PCI_DEVICE_ID 0x02 /* 16 bits */
#define PCI_COMMAND 0x04 /* 16 bits */
#define PCI_STATUS 0x06 /* 16 bits */
#define PCI_CLASS_REVISION 0x08 /* High 24 bits are class, low 8 revision */
#define PCI_REVISION_ID 0x08 /* Revision ID */
#define PCI_CLASS_PROG 0x09 /* Reg. Level Programming Interface */
#define PCI_CLASS_DEVICE 0x0a /* Device class */
#define PCI_CACHE_LINE_SIZE 0x0c /* 8 bits */
#define PCI_LATENCY_TIMER 0x0d /* 8 bits */
#define PCI_HEADER_TYPE 0x0e /* 8 bits */
对应我们的图3(见下)中的前16字节。而且我们也看到了紧挨着PCI_HEADER_TYPE (即存放头部类型的寄存器)下面定义的就是上面提到的三种类型的头部:
#define PCI_HEADER_TYPE_NORMAL 0
#define PCI_HEADER_TYPE_BRIDGE 1
#define PCI_HEADER_TYPE_CARDBUS 2
在Linux系统上,可以通过cat /proc/pci 等命令查看系统中所有PCI设备的类别、型号以及厂商等等信息,那就是从这些寄存器来的。下面是在虚拟机中用lspci -x命令的信息截取(lspci命令也是使用/proc文件作为其信息来源):
00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
首先要说明的是PCI寄存器是小端字节序格式的。那么根据最下面的PCI配置寄存器组的结构(图 3),显然这个Host bridge的Vendor ID是0x8086,我不说你也能猜到这个Vendor就是Intel了。
这里有个问题要先说清楚,就是这些寄存器的地址问题,不然往后就进行不下去了。配置寄存器可以让我们来进行配置以便完成PCI设备上的存储空间的访问,但这些配置寄存器本身也位于PCI设备地址空间中,如何访问这部分空间也就成了我们整个初始化工作的一个入口点,就像每个可执行程序都要有入口点一样。PCI采用的办法是让所有设备的配置寄存器组都采用相同的地址,由所在总线的PCI桥在访问时附加上其他条件来区分。而CPU则通过一个统一的入口地址向“宿主--PCI桥”发出命令,由相应的PCI桥间接的完成具体的读写。对于i386结构的处理器,PCI总线的设计者在I/O地址空间保留了8个字节用于这个目的,那就是0xCF8~0xCFF。这8个字节构成了两个32位的寄存器,第一个是“地址寄存器”0xCF8,第二个是“数据寄存器”0xCFC。要访问某个设备中的某个配置寄存器时,CPU先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据。不过,写入地址寄存器的目标地址是一种总线号、设备号、功能号以及设备寄存器地址在内的综合地址。格式如图2:
图2 写入地址寄存器0xCF8的综合地址
这里的总线号、设备号和功能号是对配置寄存器地址的扩充,就是上面提到的附加的其他条件。首先每个PCI总线都有个总线号,主总线的总线号为0,其余的则由CPU在枚举阶段每当探测到一个PCI桥时便为其指定一个,依次递增。设备号一般代表着一块PCI接口卡(更确切的说是PCI总线接口芯片),通常取决于插槽的位置。每块PCI接口卡上可以有若干个功能模块,这些功能模块共用一个PCI总线接口芯片,包括其中用于地址映射的电子线路,以降低成本。从逻辑的角度说,每个“功能”实际上就是一个设备(看过USB设备驱动的人很眼熟吧 呵呵),所以设备号和功能号合在一起又可以称作“逻辑设备号”,而每块卡上最多可以容纳8个设备。显然,这些字段(指整个32bit)结合在一起就惟一确定了系统中的一项PCI逻辑设备。开始时,只有0号总线可以访问,在扫描0号总线时如果发现上面某个设备是PCI桥,就为之指定一个新的总线号,例如1,这样1号总线就可以访问了,这就是枚举阶段的任务之一。
现在请读者考虑一个问题:当我们拿到一块PCI网卡,我们把它插到PC的主板上,打算写个这个网卡的驱动。那么第一步该干什么呢?读者可以回顾前面的内容,既然我们说Linux内核帮我们做了设备的枚举和配置工作,那么我在写网卡驱动之前是不是可以先看看Linux内核对我们的这个PCI网卡设备完成的枚举工作的结果呢?或者直白些说,我把网卡插上了,现在Linux内核有没有识别出这块设备呢? 注意识别出设备跟能正常使用设备是不同的概念,这很好理解。安装过PC网卡驱动的人都知道,当设备的驱动没有安装时,我们在设备管理器中是可以看到这个设备的,不过上面是一个黄色的大问号。而在Linux系统中,我们可以通过lspci命令来查看。
下面是在LDD3的PCI驱动那一章截取的一段内容: lspci 的输出( pciutils 的一部分, 在大部分发布中都有)和在 /proc/pci 和 /porc/bus/pci 中的信息排布. PCI 设备的 sysfs 表示也显示了这种寻址方案, 还有 PCI 域信息. 当显示硬件地址时, 它可被显示为 2 个值( 一个 8-位总线号和一个 8-位 设备和功能号), 作为 3 个值( bus, device, 和 function), 或者作为 4 个值(domain, bus, device, 和 function); 所有的值常常用 16 进制显示.
例如, /proc/bus/pci/devices 使用一个单个16位字段(来便于分析和排序), 而 /proc/bus/busnumber 划分地址为3个字段. 下面内容显示了这些地址如何显示, 只显示了输出行的开始:
$ lspci | cut -d: -f1-3
0000:00:00.0 Host bridge
0000:00:00.1 RAM memory
0000:00:00.2 RAM memory
0000:00:02.0 USB Controller
0000:00:04.0 Multimedia audio controller
0000:00:06.0 Bridge
0000:00:07.0 ISA bridge
0000:00:09.0 USB Controller
0000:00:09.1 USB Controller
0000:00:09.2 USB Controller
0000:00:0c.0 CardBus bridge
0000:00:0f.0 IDE interface
0000:00:10.0 Ethernet controller
0000:00:12.0 Network controller
0000:00:13.0 FireWire (IEEE 1394)
0000:00:14.0 VGA compatible controller
$ cat /proc/bus/pci/devices | cut -f1
0000
0001
0002
0010
0020
0030
0038
0048
0049
004a
0060
0078
0080
0090
0098
00a0
$ tree /sys/bus/pci/devices/
/sys/bus/pci/devices/
|-- 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0
|-- 0000:00:00.1 -> ../../../devices/pci0000:00/0000:00:00.1
|-- 0000:00:00.2 -> ../../../devices/pci0000:00/0000:00:00.2
|-- 0000:00:02.0 -> ../../../devices/pci0000:00/0000:00:02.0
|-- 0000:00:04.0 -> ../../../devices/pci0000:00/0000:00:04.0
|-- 0000:00:06.0 -> ../../../devices/pci0000:00/0000:00:06.0
|-- 0000:00:07.0 -> ../../../devices/pci0000:00/0000:00:07.0
|-- 0000:00:09.0 -> ../../../devices/pci0000:00/0000:00:09.0
|-- 0000:00:09.1 -> ../../../devices/pci0000:00/0000:00:09.1
|-- 0000:00:09.2 -> ../../../devices/pci0000:00/0000:00:09.2
|-- 0000:00:0c.0 -> ../../../devices/pci0000:00/0000:00:0c.0
|-- 0000:00:0f.0 -> ../../../devices/pci0000:00/0000:00:0f.0
|-- 0000:00:10.0 -> ../../../devices/pci0000:00/0000:00:10.0
|-- 0000:00:12.0 -> ../../../devices/pci0000:00/0000:00:12.0
|-- 0000:00:13.0 -> ../../../devices/pci0000:00/0000:00:13.0
`-- 0000:00:14.0 -> ../../../devices/pci0000:00/0000:00:14.0
所有的 3 个设备列表都以相同顺序排列, 因为 lspci 使用 /proc 文件作为它的信息源. 拿 VGA 视频控制器作一个例子, 0x00a0 意思是 0000:00:14.0 当划分为域(16位), 总线(8位), 设备(5位)和功能(3位).为什么0x00a0对应的是0000:00:14.0呢,这就要看图2中的内容了,根据图2中的寄存器对应0x00a0就代表着总线(8位), 设备(5位)和功能(3位).0x00a0=0000000010100000,很容易看出高8位是总线号也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能号,那么剩下的10100就是设备号,补全成8位的值就是00010100即0x14.
图3 PCI配置寄存器组
浅谈Linux PCI设备驱动(二)
我们在 浅谈Linux PCI设备驱动(一)中(以下简称 浅谈(一) )介绍了PCI的配置寄存器组,而Linux PCI初始化就是使用了这些寄存器来进行的。后面我们会举个例子来说明Linux PCI设备驱动的主要工作内容(不是全部内容),这里只做文字性的介绍而不会涉及具体代码的分析,因为要分析代码的话,基本就是对 Linux内核源代码情景分析(下册)第八章的解读,读者若想分析代码,可以参考该书的内容,我们这里就不去深入分析这些代码了。
Linux PCI设备驱动代码必须扫描系统中所有的PCI总线,寻找系统中所有的PCI设备(包括PCI-PCI桥设备)。系统中的每条PCI总线都有个编号number,根PCI总线的编号为0。系统当前存在的所有根总线(因为可能存在不止一个Host/PCI桥,那么就可能存在多条根总线) 都通过其pci_bus结构体中的node成员链接成一个全局的根总线链表,其表头由struct list_head类型的全局变量pci_root_buses来描述,我们在/linux-2.4.18/linux/drivers/pci/pci.c的38行可以看到如下定义:
LIST_HEAD(pci_root_buses);
而根总线下面的所有下级总线则都通过其pci_bus结构体中的node成员链接到其父总线的children链表中。这样,通过这两种PCI总线链表,Linux内核就将所有的pci_bus结构体以一种倒置树的方式组织起来。
另外,每个PCI设备都由一个pci_dev结构体表示,每个pci_dev结构体都同时连入两个队列,一方面通过其成员global_list挂入一个总的pci_dev结构队列(队列头是pci_devices);同时又通过成员bus_list挂入其所在总线的pci_dev结构队列devices(队列头是pci_bus.devices,即该pci设备所在的pci总线的devices队列),并且使指针bus(指pci_dev结构体里的bus成员)指向代表着其所在总线的pci_bus结构。如果具体的设备是PCI-PCI桥,则还要使其指针subordinate指向代表着另一条PCI总线的pci_bus结构。同样我们在/linux-2.4.18/linux/drivers/pci/pci.c的39行可以看到如下定义:
LIST_HEAD(pci_devices);
对于PCI设备链表,我们可以通过图1来理解。
注:该图摘自Linux设备驱动开发详解 第21章 PCI设备驱动。
图1 Linux PCI设备链表
而对于我们在浅谈(一)中贴出的图1的PCI系统结构示意图,Linux内核中对应的数据结构如这里的图2所示。
图2 Linux内核PCI数据结构
Linux PCI初始化代码从PCI总线0开始扫描,它通过读取"Vendor ID"和"Device ID"来试图发现每一个插槽上的设备。如果发现了一个PCI-PCI桥,则创建一个pci_bus数据结构并且连入到由pci_root_buses指向的pci_bus和pci_dev数据结构组成的树中。PCI初始化代码通过设备类代码0x060400来判断一个PCI设备是否是PCI-PCI桥。然后,Linux核心开始构造这个桥设备另一端的PCI总线和其上的设备。如果还发现了桥设备,就以同样的步骤来进行构建。这个处理过程称之为深度优先算法。PCI-PCI桥横跨在两条总线之间,寄存器PCI_PRIMARY_BUS和PCI_SECONDARY_BUS的内容就说明了其上下两端的总线号,其中PCI_SECONDARY_BUS就是该PCI-PCI桥所连接和控制的总线,而PCI_SUBORDINATE_BUS则说明自此以下、在以此为根的子树中最大的总线号是什么。
我们可以在/linux-2.4.18/linux/include/linux/pci.h看到如下定义:
112: /* Header type 1 (PCI-to-PCI bridges) */
113: #define PCI_PRIMARY_BUS 0x18 /* Primary bus number */
114: #define PCI_SECONDARY_BUS 0x19 /* Secondary bus number */
115: #define PCI_SUBORDINATE_BUS 0x1a /* Highest bus number behind the bridge */
由于在枚举阶段做的是深度优先扫描,所以子树中的总线号总是连续递增的。当CPU往I/O寄存器0xCF8中写入一个综合地址以后,从0号总线开始,每个PCI-PCI桥会把综合地址中的总线号与自身的总线号相比,如果相符就用逻辑设备号在本总线上寻访目标设备;否则就进一步把这个总线号与PCI_SUBORDINATE_BUS中的内容相比,如果目标总线号落在当前子树范围中,就把综合地址传递给其下的各个次层PCI-PCI桥,要不然就不予理睬。这样,最终就会找到目标设备。当然,这个过程只是在PCI设备的配置阶段需要这样做,一旦配置完成,CPU就直接通过有关的总线地址访问目标设备了。
PCI-PCI桥要想正确传递对PCI I/O,PCI Memory或PCI Configuration地址空间的读和写请求,必须知道下列信息:
(1)Primary Bus Number(主总线号)
该PCI-PCI桥所处的PCI总线称为主总线。
(2)Secondary Bus Number(子总线号)
该PCI-PCI桥所连接的PCI总线称为子总线/次总线号。
(3)Subordinate Bus Number
PCI总线的下属PCI总线的总线编号最大值。有点绕,看后面的分析就明白了。
PCI I/O 和 PCI Memory 窗口
PCI桥的配置寄存器与一般的PCI设备不同。一般PCI设备可以有6个地址区间,外加一个ROM区间,代表着设备上实际存在的存储器或寄存器区间。而PCI桥,则本身并不一定有存储器或寄存器区间,但是却有三个用于地址过滤的区间。每个地址过滤区间决定了一个地址窗口,从CPU一侧发出的地址,如果落在PCI桥的某个窗口内,就可以穿过PCI桥而到达其所连接的总线上。此外,PCI桥的命令寄存器中还有”memory access enable”和”I/O access enable ”的两个控制位,当这两个控制位为0时,这些窗口就全都关上了。在未完成对PCI总线的初始化之前,还没有为PCI设备上的各个区间分配合适的总线地址时,正是因为这两个控制位为0,才不会对CPU一侧造成干扰。例如, 对于浅谈(一)的 PCI系统示意图 ,仅当读和写请求中的PCI I/O或PCI memory地址属于SCSI或Ethernet设备时,PCI-PCI桥才将这些总线上的请求从PCI总线0传递到PCI总线1。这种过滤机制可以避免地址在系统中没必要的繁衍。为了做到这点,每个PCI-PCI桥必须正确地被设置好它所负责的PCI I/O或PCI memory的起始地址和大小。当一个读或写请求地址落在其负责的范围之内,这个请求将被映射到次级的PCI总线上。系统中的PCI-PCI桥一旦设置完毕,如果Linux中的设备驱动程序存取的PCI I/O和PCI memory地址落在在这些窗口之内,那么这些PCI-PCI桥就是透明的。这是个很重要的特性,使得Linux PCI设备驱动程序开发者的工作容易些。
问题是配置一个PCI-PCI桥的时候,并不知道这个PCI-PCI桥的subordinate bus number。那么就不知道该PCI桥下面是否还有其他的PCI-PCI桥。即使你知道,也不清楚如何对它们赋值。解决方法是利用上述的深度扫描算法来扫描每个总线。每当发现PCI-PCI桥就对它进行赋值。当发现一个PCI-PCI桥时,可以确定它的secondary bus number。然后我们暂时先将其subordinate bus number赋值为0xFF。紧接着,开始扫描该PCI-PCI桥的downstream桥。这个过程看起来有点复杂,下面的例子将给出清晰的解释:
图3 配置PCI系统 第一步
PCI-PCI桥编号--第一步
以图3的拓扑结构为例,扫描时首先发现的桥是Bridge1。Bridge 1的downstream PCI总线号码被赋值1。自然该桥的secondary bus number也是1。其subordinate bus number暂时赋值为0xFF。上述赋值的含义是所有类型1的含有PCI总线1或更高(<255)的号码的PCI配置地址将被Bridge 1传递到PCI总线1上。如果PCI总线号是1,Bridge 1 还负责将配置地址的类型转换成类型0(对于这里说的类型0和类型1,请参考浅谈(一))。否则,就不做转换。上述动作就是开始扫描总线1时Linux PCI初始化代码所完成的对总线0的配置工作。
图4 配置PCI系统 第二步
PCI-PCI桥编号--第二步
由于Linux PCI设备驱动使用深度优先算法进行扫描,所以初始化代码开始扫描总线1。从而Bridge 2被发现。因为在Bridge 2下面发现不再有PCI-PCI桥,所以Bridge 2的subordinate bus number是2,等于它的secondary bus number。图4显示了在这个时刻总线和PCI-PCI桥的赋值情况。
图5 配置PCI系统 第三步
PCI-PCI桥编号--第三步
Linux PCI设备驱动代码从总线2的扫描中回来接着进行扫描总线1,发现Bridge 3。它的primary bus number被赋值为1,secondary bus number为3。因为总线3上还发现了PCI-PCI桥,所以Bridge 3的subordinate bus number暂时赋值0xFF。图5显示了这个时刻系统配置的状态。到目前为止,含有总线号1,2,3的类型1的PCI配置都可以正确地传送到相应的总线上。
图6 配置PCI系统 第四步
PCI-PCI桥编号--第四步
现在Linux开始扫描PCI总线3,Bridge 3的downstream。PCI总线3上有另外一个PCI-PCI桥,Bridge 4。因此Bridge 4的primary bus number的值为3,secondary bus number为4。由于Bridge 4下面没有别的桥设备,所以Bridge 4的subordinate bus number为4。然后回到PCI-PCI Bridge 3。这时就将Bridge 3的subordinate bus number从0xFF改为4,表示总线4是从Bridge 3往下走的最远的PCI-PCI桥。最后,Linux PCI设备驱动代码将4以同样的道理赋值给Bridge 1的subordinate bus number。图6反映了系统最后的状态。