浅谈 Linux 内核开发之 PCI 设备驱动

时间:2022-03-18 11:07:26
本文介绍了 PCI 的基本概念,并从 Linux 内核的角度出发,介绍了 PCI 设备的初始化以及配置。

PCI 介绍

随着计算机应用的不断更新和发展(比如百兆网卡、视屏流等),计算机内数据传输的带宽要求越来越高,传统内部总线带宽已经远远不能满足这些应用的需要,因此人们推出了 PCI 总线标准

PCI 是 Peripheral Component Interconnect 的缩写,它因为高性能、低成本以及良好的扩展性而在计算机系统中被广泛使用。上至服务器,下至嵌入式设备都能找到它的身影。图 1 显示了一个标准 PCI 总线的组织结构图。


图 1. 标准 PCI 总线的组织结构图
浅谈 Linux 内核开发之 PCI 设备驱动  

从图中我们可以看出 PCI 总线架构主要被分成三部分:

  1. PCI 设备。符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
  2. PCI 总线。PCI 总线在系统中可以有多条,类似于树状结构进行扩展,每条 PCI 总线都可以连接多个 PCI 设备/桥。上图中有两条 PCI 总线。
  3. PCI 桥。当一条 PCI 总线的承载量不够时,可以用新的 PCI 总线进行扩展,而 PCI 桥则是连接 PCI 总线之间的纽带。图中的 PCI 桥有两个,一个桥用来连接处理器、内存以及 PCI 总线,而另外一条则用来连接另一条 PCI 总线。

PCI 总线操作

PCI 总线操作表示主设备向目标设备所发起的操作请求,最多有16种类型。主要类型有:IO 方式读/写,Memory 方式读/写,Configuration 方式读/写等。

PCI 配制空间

对于软件开发者来说,该如何对 PCI 设备进行编程呢?PCI 总线标准中定义了一套配置空间寄存器用于读取或者设置 PCI 设备的信息。每个 PCI 设备/桥都有自己的配置空间寄存器。

配置空间共有256字节,设备类型不同,其配置空间的布局也不尽相同。设备类型的区分可以通过配置空间内的 Header Type 寄存器(0Eh)进行,该寄存器值为 00h 表示当前设备是一个 PCI 设备,01h 表示当前设备是一个 PCI 桥。

配置空间的前64字节是配置空间起始段,它对于每种类型的设备都是相同的。显示了 PCI 设备的配置空间起始段。


图 2. PCI 设备的配置空间起始段
浅谈 Linux 内核开发之 PCI 设备驱动  

图 3 显示了 PCI 桥的配置空间起始段。


图 3. PCI 桥的配置空间起始段
浅谈 Linux 内核开发之 PCI 设备驱动  

配置空间寄存器有些是只读的,有些是可写的,下面介绍几个在编程时会用到的寄存器。

Device ID 和 Vendor ID 寄存器

这两个寄存器分别存放了设备信息和厂商信息(值在 0x0000 和 0xFFFF 之间,但不能取 0xFFFF),因此软件开发者可以通过读取这两个寄存器的值,并与 0xFFFF 比较,从而判断当前设备是否有效。

Command 和 Status 寄存器

Command 寄存器存放了设备的配置信息,比如是否允许 Memory/IO 方式的总线操作、是否为主设备等。Status 寄存器存放了设备的状态信息,比如中断状态、错误状态等。

Header Type 寄存器

这个寄存器前面曾经提过,它定义了设备类型,比如 PCI 设备、PCI 桥等。

Base Address 寄存器

这个寄存器有三个作用。

  1. 该寄存器存放了 Memory/IO 访问空间的起始地址。
  2. 该寄存器存放了 Memory/IO 访问空间的大小,这个数据可以通过下面的方式读出:
    1. 往寄存器里写 0xFFFFFFFF;
    2. 读出寄存器的值,并取反;
    3. 将上一步的值加上1后就是该空间的大小。
  3. 该寄存器定义了这段地址空间的访问类型(Memory 方式还是 IO 方式)。

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 的支持

通常 PPC 会提供一个(或更多的)PCI 控制器来连接 PCI 总线,通过 PCI 控制器,CPU 可以发起 Configuration 读写操作来访问所连接的所有 PCI 设备/桥的配置空间。每个 PCI 设备/桥都会用(总线号,设备号,功能号)这一组合来进行编号,因此在 PCI 控制器中输入设备对应的(总线号,设备号,功能号)就能寻址到具体的 PCI 设备/桥。以 PPC8548 为例,它提供了两个寄存器来实现 Configuration 操作,分别是 CFG_ADDR 和 CFG_DATA 寄存器,如果想对某个设备发起读/写操作,则首先将该设备的(总线号,设备号,功能号)写入 CFG_ADDR 中,这代表寻址一个具体的 PCI 设备,同时在 CFG_ADDR 中写入需要操作的配置空间寄存器的编号,最后从 CFG_DATA 中读取/写入相应的数据即可。


浅谈 Linux 内核开发之 PCI 设备驱动
浅谈 Linux 内核开发之 PCI 设备驱动
浅谈 Linux 内核开发之 PCI 设备驱动



Linux 内核对 PCI 的支持

Linux 内核(2.6 版本)在初始化之初就对所有 PCI 设备进行了扫描并且配制,具体操作分为下面几个步骤。

编译时的 PCI 配制

如果想要 Linux 内核支持 PCI,首先需要对其配制文件进行相应的修改,在 config 文件中需要配置下面的宏参数。

  1. Linux 提供了 PCI 配制全局控制宏参数 CONFIG_PCI,它控制着 PCI 控制器和设备是否能够被配制的流程,因此该宏值需要被设置成“Y”。
  2. 对于某些处理器来说,比如 PPC85XX、PPC83XX 等,它们提供了两个或者更多的 PCI 控制器,因此在 config 文件中专门提供了诸如 CONFIG_MPC85xx_PCI2、CONFIG_MPC83xx_PCI2 等宏参数,它们控制着第二个(或更多的)PCI 控制器的配置流程,因此如果需要使用第二个(或更多的)PCI 控制器时,需要将相应的宏值设置成“Y”。

在编译内核之前,如果在 config 文件中提供了以上宏参数的设置,则编译出来的内核映像提供了对于 PCI 支持的功能。

PCI 相关数据结构

Linux 提供了三类数据结构用以描述 PCI 控制器、PCI 设备以及 PCI 总线。

PCI 控制器

PCI 控制器用 pci_controller 结构来描述,它有以下几个主要的属性:

  • index:该属性标志 PCI 控制器的编号。
  • next:该属性指向下一个 PCI 控制器,通过 next 属性,PCI 控制器可以形成一个单向链表。
  • first_busno:该属性标志了连接在该控制器上第一条总线的编号。
  • last_busno:该属性标志了连接在该控制器上最后一条总线的编号。
  • ops:该属性标志了当前 PCI 控制器所对应的 PCI 配制空间读写操作函数。
  • io_space:该属性标志了当前 PCI 控制器所支持的 IO 地址空间。
  • mem_space:该属性标志了当前 PCI 控制器所支持的 Memory 地址区间。
  • cfg_addr:该属性标志了当前 PCI 控制器发起 Configuration 访问方式时所需要写入的地址空间。
  • cfg_data:该属性标志了当前 PCI 控制器发起 Configuration 访问方式时所需要读写的数据空间。
  • bus:该属性标志了当前 PCI 控制器所连接的 PCI 总线,它对应的数据结构是 pci_bus。

PCI 总线

PCI 总线用 pci_bus 结构来描述,它有以下几个主要的属性:

  • parent:可通过该属性索引到上层 PCI 总线。
  • self:该属性标志了连接的上行 PCI 桥(对应的数据结构是 pci_dev)。
  • children:该属性标志了总线连接的所有 PCI 子总线链表。
  • devices:该属性标志了总线连接的所有 PCI 设备链表。
  • ops:该属性标志了总线上所有 PCI 设备的配制空间读写操作函数。
  • number:该属性标志了当前 PCI 总线的编号。
  • primary:该属性标志了 PCI 上行总线编号。
  • secondary:该属性标志了 PCI 下行总线编号。
  • subordinate:该属性标志了能够访问到的最大总线编号。
  • resource:该属性标志了 Memory/IO 地址空间。

PCI 设备

PCI 设备用 pci_dev 结构来描述,它有以下几个主要的属性:

  • global_list:Linux 定义了一个全局列表来索引所有的 PCI 设备,该属性标志了这个全局列表的首指针。
  • bus:该属性标志了当前设备所在的 PCI 总线(对应的数据结构是 pci_bus)。
  • devfn:该属性标志了设备编号和功能编号。
  • vendor:该属性标志了供应商编号。
  • device:该属性标志了设备编号。
  • driver:该属性标志了设备对应的驱动代码(对应的数据结构是 pci_driver)。
  • irq:该属性标志了中断号。
  • resource:该属性标志了 Memory/IO 地址区间。

内核里的 PCI 数据结构图

当 Linux 内核在做 PCI 初始化工作时,它会根据图 4 建立一个由 pci_controller、pci_bus 和 pci_dev 三者组成的一个组织结构图。根据这个结构,软件开发者可以很方便的通过 PCI 控制器索引到每个 PCI 设备或者 PCI 总线。


图 4. 组织结构图
浅谈 Linux 内核开发之 PCI 设备驱动  

PCI 控制器初始化

当一个支持 PCI 的内核映像开始运行时,它会在系统初始化时对 PCI 进行配置。函数调用链如下所示(以 PPC85XX 为例)。


图 5. 函数调用链
浅谈 Linux 内核开发之 PCI 设备驱动  

内核从 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 自动扫描来完成。PCI 自动扫描主要做下面的工作:

  1. 扫描 PCI 总线,识别 PCI 总线上的所有设备。
  2. 对于连接在 PCI 总线上的所有 PCI 桥进行总线编号。
  3. 对于连接在 PCI 总线上的所有 PCI 设备和 PCI 桥进行 Memory/IO 访问空间的配置。

这部分代码在 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. 示例
浅谈 Linux 内核开发之 PCI 设备驱动  
  1. 系统运行初始,Bus A 为 0,通过上面的 PCI 总线扫描得到连接在 Bus A 上的 PCI 桥(即图中的 Bridge 1)。
  2. 下面开始设置 Bridge 1 的 Bus 寄存器。将 Primary Bus Number 寄存器设置成 Bus A 的编号,即 0。将 Secondary Bus Number 寄存器设置成 Bus B 的编号,它的值等于(Bus A + 1),也就是 1。由于暂时无法知道该桥所能访问的所有下行总线数目,Subordinate Bus Number 寄存器暂时设置成 0xFF。
  3. 当扫描完所有 Bus A 上所有(设备号,功能号)后,开始扫描 Bus B,Bus B 的编号在扫描完 Bus A 后已经得到,为 1。Bus B 的扫描方法同步骤(1),先扫描出 Bus B 上的 PCI 桥(即图中的 Bridge 2),然后配置 Primary Bus Number 寄存器为 1,Secondary Bus Number 寄存器为 2,而 Subordinate Bus Number 寄存器依然为 0xFF。
  4. Bus B 扫描完后得到 Bus C 的编号,为2。下面开始扫描 Bus C,因为 Bus C 上没有 PCI 桥,于是在扫描完其它(设备号,功能号)后,Bus C 的扫描结束。
  5. 由于 Bridge 2 所能访问到的最大 Bus 编号是 2,因此重新设置 Bridge 2 的 Subordinate Bus Number 寄存器为 2。
  6. 由于 Bridge 1 所能访问到的最大 Bus 编号也是 2,因此重新设置 Bridge 1 的 Subordinate Bus Number 寄存器为 2。
  7. 总线编号结束。

配置访问空间

当系统需要访问 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 中)。

PCI 设备和总线初始化

这一操作在 pcibios_init() 函数中进行(位于 arch/ppc/kernel/pci.c 中)。它会在前面操作结束后,对 PCI 总线和 PCI 设备分别分配 pci_bus 和 pci_dev 类型的节点,并建立如所示的组织结构关系。

Linux 的 PCI 设备初始化

Linux 会将 PCI 的相关信息保存在一个文件中,从而方便用户的查阅。这一文件的创建就在 pci_proc_init() 函数中进行(位于 drivers/pci/proc.c 中)。首先它在 /proc/bus 目录下建立起一个名为 pci 的目录,然后在该目录中建立一个名为 devices 的文件,该文件中存放了当前内核所配置的所有 PCI 设备的信息。

我们通常会使用 lspci 命令来查看系统中的 PCI 设备,这条命令就是从 devices 文件中解析相应的字段来显示的。


浅谈 Linux 内核开发之 PCI 设备驱动
浅谈 Linux 内核开发之 PCI 设备驱动

总结

PCI 标准更新很快,现在最新的标准被称为 PCI Express,从硬件角度来看与以前的 PCI 有了很大的区别。但是由于 PCI 标准的兼容性,其设备配置空间的布局仍然相同,从而也保证了软件的兼容性,对于 PCI Express 的内核配置与 PCI 基本相同。良好的兼容性——这大概也是 PCI 的一个魅力所在吧!