浅谈 Linux 内核开发之 PCI 设备驱动![]() |
![]() |
![]() |
级别: 初级 赵 昊翔 ([email protected] ), 软件工程师, Cisco Systems 2009 年 6 月 11 日 本文介绍了 PCI 的基本概念,并从 Linux 内核的角度出发,介绍了 PCI 设备的初始化以及配置。 随着计算机应用的不断更新和发展(比如百兆网卡、视屏流等),计算机内数据传输的带宽要求越来越高,传统内部总线带宽已经远远不能满足这些应用的需要,因此人们推出了 PCI 总线标准 PCI 是 Peripheral Component Interconnect 的缩写,它因为高性能、低成本以及良好的扩展性而在计算机系统中被广泛使用。上至服务器,下至嵌入式设备都能找到它的身影。图 1 显示了一个标准 PCI 总线的组织结构图。 图 1. 标准 PCI 总线的组织结构图 ![]() 从图中我们可以看出 PCI 总线架构主要被分成三部分:
PCI 总线操作表示主设备向目标设备所发起的操作请求,最多有16种类型。主要类型有:IO 方式读/写,Memory 方式读/写,Configuration 方式读/写等。 对于软件开发者来说,该如何对 PCI 设备进行编程呢?PCI 总线标准中定义了一套配置空间寄存器用于读取或者设置 PCI 设备的信息。每个 PCI 设备/桥都有自己的配置空间寄存器。 配置空间共有256字节,设备类型不同,其配置空间的布局也不尽相同。设备类型的区分可以通过配置空间内的 Header Type 寄存器(0Eh)进行,该寄存器值为 00h 表示当前设备是一个 PCI 设备,01h 表示当前设备是一个 PCI 桥。 配置空间的前64字节是配置空间起始段,它对于每种类型的设备都是相同的。显示了 PCI 设备的配置空间起始段。 图 2. PCI 设备的配置空间起始段 ![]() 图 3 显示了 PCI 桥的配置空间起始段。 图 3. PCI 桥的配置空间起始段 ![]() 配置空间寄存器有些是只读的,有些是可写的,下面介绍几个在编程时会用到的寄存器。 Device ID 和 Vendor ID 寄存器 这两个寄存器分别存放了设备信息和厂商信息(值在 0x0000 和 0xFFFF 之间,但不能取 0xFFFF),因此软件开发者可以通过读取这两个寄存器的值,并与 0xFFFF 比较,从而判断当前设备是否有效。 Command 和 Status 寄存器 Command 寄存器存放了设备的配置信息,比如是否允许 Memory/IO 方式的总线操作、是否为主设备等。Status 寄存器存放了设备的状态信息,比如中断状态、错误状态等。 Header Type 寄存器 这个寄存器前面曾经提过,它定义了设备类型,比如 PCI 设备、PCI 桥等。 Base Address 寄存器 这个寄存器有三个作用。
PCI 设备最多有6个 Base Address 寄存器,而 PCI 桥最多有2个 Base Address 寄存器。 Subordinate Bus Number,Secondary Bus Number 和 Primary Bus Number 寄存器 这三个寄存器只在 PCI 桥配置空间中存在,因为 PCI 桥会连接两条 PCI 总线,上行的总线被称为 Primary Bus,下行的总线被称为 Secondary Bus,Primary Bus Number 和 Secondary Bus Number 寄存器分别存储了上行和下行总线的编号,而 Subordinate Bus Number 寄存器则是存储了当前桥所能直接或者间接访问到的总线的最大编号。 通常 PPC 会提供一个(或更多的)PCI 控制器来连接 PCI 总线,通过 PCI 控制器,CPU 可以发起 Configuration 读写操作来访问所连接的所有 PCI 设备/桥的配置空间。每个 PCI 设备/桥都会用(总线号,设备号,功能号)这一组合来进行编号,因此在 PCI 控制器中输入设备对应的(总线号,设备号,功能号)就能寻址到具体的 PCI 设备/桥。以 PPC8548 为例,它提供了两个寄存器来实现 Configuration 操作,分别是 CFG_ADDR 和 CFG_DATA 寄存器,如果想对某个设备发起读/写操作,则首先将该设备的(总线号,设备号,功能号)写入 CFG_ADDR 中,这代表寻址一个具体的 PCI 设备,同时在 CFG_ADDR 中写入需要操作的配置空间寄存器的编号,最后从 CFG_DATA 中读取/写入相应的数据即可。
Linux 内核(2.6 版本)在初始化之初就对所有 PCI 设备进行了扫描并且配制,具体操作分为下面几个步骤。 如果想要 Linux 内核支持 PCI,首先需要对其配制文件进行相应的修改,在 config 文件中需要配置下面的宏参数。
在编译内核之前,如果在 config 文件中提供了以上宏参数的设置,则编译出来的内核映像提供了对于 PCI 支持的功能。 Linux 提供了三类数据结构用以描述 PCI 控制器、PCI 设备以及 PCI 总线。 PCI 控制器 PCI 控制器用 pci_controller 结构来描述,它有以下几个主要的属性:
PCI 总线 PCI 总线用 pci_bus 结构来描述,它有以下几个主要的属性:
PCI 设备 PCI 设备用 pci_dev 结构来描述,它有以下几个主要的属性:
内核里的 PCI 数据结构图 当 Linux 内核在做 PCI 初始化工作时,它会根据图 4 建立一个由 pci_controller、pci_bus 和 pci_dev 三者组成的一个组织结构图。根据这个结构,软件开发者可以很方便的通过 PCI 控制器索引到每个 PCI 设备或者 PCI 总线。 图 4. 组织结构图 ![]() 当一个支持 PCI 的内核映像开始运行时,它会在系统初始化时对 PCI 进行配置。函数调用链如下所示(以 PPC85XX 为例)。 图 5. 函数调用链 ![]() 内核从 start_kernel() 函数处开始进行系统初始化,一直执行到 mpc85xx_setup_hose() 函数处便是配制 PCI 控制器以及连接在该控制器上所有设备的过程。 所有这些函数的定义处都加上了 __init 的符号类型,由 __init 修饰的函数表明在链接最终的内核映像时,这些函数将被放在一个特殊的初始化代码段中(.init.text,可以在链接文件 vmlinux.lds.S 中找到相关的段描述),这个初始化代码段会随着内核初始化完成而被释放。 在这一步骤中,内核会对它所支持的所有 PCI 控制器进行初始化工作,每个 PCI 控制器都对应一个 pci_controller 属性的变量,初始化工作会在这些变量中设置 Memory/IO 访问空间的起始地址以及结束地址、设置当前 PCI 控制器所连接的第一条和最后一条 PCI 总线编号等等。 系统如何知道当前连接了多少 PCI 设备?有多少根 PCI 总线?每个 PCI 设备的访问空间如何配置?等等。这些都得靠 PCI 自动扫描来完成。PCI 自动扫描主要做下面的工作:
这部分代码在 pciauto_bus_scan() 函数中(位于 arch/ppc/syslib/pci_auto.c 中)。 识别 PCI 总线上的设备 PCI 总线扫描的原理是从总线 0 扫描到总线 255,对于每条总线,系统都会扫描所有(总线号,设备号,功能号),通过 Configuration 方式读出每个设备的 Device ID 和 Vendor ID 寄存器,如果这两个寄存器的值是个有效值(非 0xFFFF),则说明当前设备是个有效的 PCI 设备/桥。进而再读取该设备的 Header Type 寄存器,如果该寄存器为 1,则表示当前设备是 PCI 桥,否则是 PCI 设备。 对所有 PCI 总线进行编号 PCI 桥如何知道它所连接的 PCI 总线情况呢?这就需要对 PCI 桥进行总线编号。前面介绍过 PCI 桥提供了 Primary Bus Number、Secondary Bus Number 和 Subordinate Bus Number 三个寄存器用于标志该桥所连接的 PCI 总线,下面通过一个示例来说明内核对于 PCI 总线是如何进行编号的。 图 6. 示例 ![]()
配置访问空间 当系统需要访问 PCI 设备时,它需要产生 Configuration、Memory 或者 IO 的读写操作,对于 Memory/IO 的访问方式来说,它们需要定义一个地址范围,落在这个地址范围的操作会被认为是相应的 Memory/IO 的读写操作。 通常 PCI 设备提供了最多6组 Base Address 寄存器,在 PCI 总线扫描时,每当扫描出一个可用的 PCI 设备后,会对该设备的 Base Address 寄存器进行 Memory/IO 访问空间的配置。 而对于 PCI 桥来说,它只提供了2组 Base Address 寄存器,当 PCI 总线扫描出一个 PCI 桥后,也会对该桥的 Base Address 寄存器进行 Memory/IO 访问空间的配置。 需要注意的是,在构建系统之初,需要明确当前系统的地址范围,划分出特定的物理地址作为 PCI Memory 或者 PCI IO 空间,在给 PCI 设备/桥进行访问空间配置时,就是取事先约定的地址空间中的某段地址进行配置,所有设备/桥的访问地址不能冲突。定义系统的 Memory/IO 访问空间是在 mpc85xx_setup_hose() 函数中提供的(位于 arch/ppc/syslib/ppc85xx_setup.c 中)。 这一操作在 pcibios_init() 函数中进行(位于 arch/ppc/kernel/pci.c 中)。它会在前面操作结束后,对 PCI 总线和 PCI 设备分别分配 pci_bus 和 pci_dev 类型的节点,并建立如所示的组织结构关系。 Linux 会将 PCI 的相关信息保存在一个文件中,从而方便用户的查阅。这一文件的创建就在 pci_proc_init() 函数中进行(位于 drivers/pci/proc.c 中)。首先它在 /proc/bus 目录下建立起一个名为 pci 的目录,然后在该目录中建立一个名为 devices 的文件,该文件中存放了当前内核所配置的所有 PCI 设备的信息。 我们通常会使用 lspci 命令来查看系统中的 PCI 设备,这条命令就是从 devices 文件中解析相应的字段来显示的。 |