0%

PCI 调研

PCI 设备驱动

PCI(Periheral Component Interconnect)有三种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间

  • PCI I/O空间和PCI内存地址空间由设备驱动程序(设备本身驱动)使用
  • PCI配置空间由Linux PCI初始化代码使用,这些代码用于配置PCI设备,比如中断号以及I/O或内存基地址

PCI设备驱动就是要大致描述对于PCI设备驱动

  • Linux内核都帮我们做了什么(主)
  • 我们应该完成什么(次)。

Linux内核做了什么

Linux内核主要就做了对PCI设备的枚举配置;这些工作都是在Linux内核初始化时完成的。

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总线称为次总线,即主总线的子总线。

image-20240416112936826

CPU通过Host/PCI桥与一条PCI总线相连,处在这种位置上的PCI总线称为根总线。PC机中通常只有一个Host/PCI桥,在一条PCI总线的基础上,可以再通过PCI桥连接到其他次一层的总线,例如通过PCI-PCI桥可以连接到另一条PCI总线,通过PCI-ISA桥可以连接到一条ISA总线。

通过使用PCI-PCI桥,就构筑起了一个层次的、树状的PCI系统结构。对于上层的总线而言,连接在这条总线上的PCI桥也是一个设备。但是这是一种特殊的设备,它既是上层总线上的一个设备,实际上又是上层总线的延伸。

枚举

就是从Host/PCI桥开始进行探测和扫描,逐个“枚举”连接在第一条PCI总线上的所有设备并记录在案。如果其中的某个设备是PCI-PCI桥,则又进一步再探测和扫描连在这个桥上的次级PCI总线。就这样递归下去,直到穷尽系统中的所有PCI设备。

其结果,是在内存中建立起一棵代表着这些PCI总线和设备的PCI树。每个PCI设备(包括PCI桥设备)都由一个pci_dev结构体来表示,而每条PCI总线则由pci_bus结构来表示。

image-20240416112940709

配置

PCI设备中一般都带有一些RAM和ROM 空间, 通常的控制/状态寄存器和数据寄存器也往往以RAM区间的形式出现,而这些区间的地址在设备内部一般都是从0开始编址的,那么当总线上挂接了多个设备时,对这些空间的访问就会产生冲突。所以,这些地址都要先映射到系统总线上,再进一步映射到内核的虚拟地址空间

配置就是通过对PCI配置空间的寄存器进行操作从而完成地址的映射

linux 内核只完成内部编址映射到总线地址的工作,而映射到内核的虚拟地址空间是由设备本身的驱动要做的工作

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和AnyDirect方式就是抛开BIOS而由内核自己完成初始化工作的意思。

当PCI设备上电时,硬件保持未激活状态。即该设备只会对配置事务做出响应。上电时,设备上不会有内存和I/O端口映射到计算机的地址空间;其他设备相关的功能,例如中断报告,也被禁止。

每个设备的配置寄存器组最多可以有256字节的连续空间,其中开头的64字节的用途和格式是标准的,称为配置寄存器的头部。

系统中提供一些与硬件有关的机制,使得PCI配置代码可以检测在一个给定的PCI总线上所有可能的PCI配置寄存器头部,从而知道哪个PCI插槽上目前有设备,哪个插槽上暂无设备。这是通过读PCI配置寄存器头部上的某个域完成的(一般是“Vendor ID” 域)。如果一个插槽上为空,上述操作会返回一些错误返回值,如0xFFFFFFFF。这种头部(指64字节头部)又有三种

1
2
3
#define PCI_HEADER_TYPE_NORMAL 0
#define PCI_HEADER_TYPE_BRIDGE 1
#define PCI_HEADER_TYPE_CARDBUS 2
  • “0型”(type 0)头部用于一般的PCI设备
  • “1型”头部用于各种PCI-PCI桥
  • “2型”头部是用于PCI-CardBus桥的,CardBus是笔记本电脑中使用的总线,

64字节头部中的16个字节中又包含着有关头部的类型、设备的种类、设备的一些性质、由谁制造等等信息。根据这16个字节中提供的信息,来确定应该怎样进一步解释和处理剩余头部中的48个字节

在Linux系统上,可以通过cat /proc/pci 等命令查看系统中所有PCI设备的类别、型号以及厂商等等信息,那就是从这些寄存器来的。下面是在虚拟机中用lspci -x命令的信息截取(lspci命令也是使用/proc文件作为其信息来源)

配置寄存器可以让我们来进行配置以便完成PCI设备上的存储空间的访问,但这些配置寄存器本身也位于PCI设备地址空间中,如何访问这部分空间也就成了我们整个初始化工作的一个入口点

PCI采用的办法是让所有设备的配置寄存器组都采用相同的地址,由所在总线的PCI桥在访问时附加上其他条件来区分。

对于i386结构的处理器,PCI总线的设计者在I/O地址空间保留了8个字节用于这个目的,那就是0xCF8~0xCFF。这8个字节构成了两个32位的寄存器,第一个是“地址寄存器”0xCF8,第二个是“数据寄存器”0xCFC。要访问某个设备中的某个配置寄存器时,CPU先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据。不过,写入地址寄存器的目标地址是一种总线号、设备号、功能号以及设备寄存器地址在内的综合地址。

image-20240416112944585

这里的总线号、设备号和功能号是对配置寄存器地址的扩充,就是上面提到的附加的其他条件

首先每个PCI总线都有个总线号,主总线的总线号为0,其余的则由CPU在枚举阶段每当探测到一个PCI桥时便为其指定一个,依次递增。设备号一般代表着一块PCI接口卡(更确切的说是PCI总线接口芯片),通常取决于插槽的位置。每块PCI接口卡上可以有若干个功能模块,这些功能模块共用一个PCI总线接口芯片.

所以设备号和功能号合在一起又可以称作“逻辑设备号”,而每块卡上最多可以容纳8个设备。显然,这些字段(指整个32bit)结合在一起就惟一确定了系统中的一项PCI逻辑设备。开始时,只有0号总线可以访问,在扫描0号总线时如果发现上面某个设备是PCI桥,就为之指定一个新的总线号,例如1,这样1号总线就可以访问了,这就是枚举阶段的任务之一。

当我们拿到一块PCI网卡,我们把它插到PC的主板上,打算写个这个网卡的驱动。那么第一步该干什么呢?

把网卡插上了,现在Linux内核有没有识别出这块设备呢? 注意识别出设备跟能正常使用设备是不同的概念,当设备的驱动没有安装时,我们在设备管理器中是可以看到这个设备的,不过上面是一个黄色的大问号。而在Linux系统中,我们可以通过lspci命令来查看。

lspci 的输出( pciutils 的一部分, 在大部分发布中都有)和在 /proc/pci 和 /porc/bus/pci 中的信息排布. PCI 设备的 sysfs 表示也显示了这种寻址方案, 还有 PCI 域信息. 当显示硬件地址时它可被显示为 所有的值常常用 16 进制显示.

  • 2 个值( 一个 8-位总线号和一个 8-位 设备和功能号),
  • 3 个值( bus, device, 和 function),
  • 4 个值(domain, bus, device, 和 function);

lspci 使用 /proc 文件作为它的信息源.

/sys/bus/pci/devices/

cat /proc/bus/pci/devices | cut -f1

拿 VGA 视频控制器作一个例子, 0x00a0 意思是 0000:00:14.0 当划分为域(16位), 总线(8位), 设备(5位)和功能(3位).为什么0x00a0对应的是0000:00:14.0呢

0000000010100000,很容易看出高8位是总线号也就是0, 设备号为0b10100 即0x14 , 功能号为0

pci设备 pci桥扫描与配置

Linux PCI 设备驱动代码必须扫描系统中所有的 PCI 总线,寻找系统中所有的 PCI 设备 (包括 PCI-PCI 桥设备)。系统中的每条 PCI 总线都有个编号 number,根 PCI 总线的编号为 0。系统当前存在的所有根总线 (因为可能存在不止一个 Host/PCI 桥,那么就可能存在多条根总线) 都通过其 pci_bus 结构体中的 node 成员链接成一个全局的根总线链表,而根总线下面的所有下级总线则都通过其 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 结构

image-20240416112948290

Linux PCI 初始化代码从 PCI 总线 0 开始扫描,它通过读取 Vendor IDDevice ID 来试图发现每一个插槽上的设备。如果发现了一个 PCI-PCI 桥,则创建一个 pci_bus 数据结构并且连入到由 pci_root_buses 指向的 pci_bus 和 pci_dev 数据结构组成的树中。PCI 初始化代码通过设备类代码 0x060400 来判断一个 PCI 设备是否是 PCI-PCI 桥。

在枚举阶段做的是深度优先扫描,所以子树中的总线号总是连续递增的。当 CPU 往 I/O 寄存器 0xCF8 中写入一个综合地址以后,从 0 号总线开始,每个 PCI-PCI 桥会把综合地址中的总线号与自身的总线号相比,如果相符就用逻辑设备号在本总线上寻访目标设备, 否则就进一步把这个总线号与PCI_SUBORDINATE_BUS 中的内容相比,如果目标总线号落在当前子树范围中,就把综合地址传递给其下的各个次层 PCI-PCI 桥,要不然就不予理睬。

PCI-PCI 桥要想正确传递对 PCI I/O,PCI Memory 或 PCI Configuration 地址空间的读和写请求,必须知道下列信息:

  1. Primary Bus Number(主总线号)

​ 该 PCI-PCI 桥所处的 PCI 总线称为主总线。

  1. Secondary Bus Number(子总线号)

    该 PCI-PCI 桥所连接的 PCI 总线称为子总线 / 次总线号。
    
  2. Subordinate Bus Number

​ PCI 总线的下属 PCI 总线的总线编号最大值。

PCI 桥的配置寄存器与一般的 PCI 设备不同。一般 PCI 设备可以有 6 个地址区间,外加一个 ROM 区间,代表着设备上实际存在的存储器或寄存器区间。而 PCI 桥,则本身并不一定有存储器或寄存器区间,但是却有三个用于地址过滤的区间。每个地址过滤区间决定了一个地址窗口,从 CPU 一侧发出的地址,如果落在 PCI 桥的某个窗口内,就可以穿过 PCI 桥而到达其所连接的总线上

每个 PCI-PCI 桥必须正确地被设置好它所负责的 PCI I/O 或 PCI memory 的起始地址和大小。当一个读或写请求地址落在其负责的范围之内,这个请求将被映射到次级的 PCI 总线上。系统中的 PCI-PCI 桥一旦设置完毕,如果 Linux 中的设备驱动程序存取的 PCI I/O 和 PCI memory 地址落在在这些窗口之内,那么这些 PCI-PCI 桥就是透明的。这是个很重要的特性,使得 Linux PCI 设备驱动程序开发者的工作容易些。

image-20240416112951715