一、什么是配置空间(Configuration Space)
PCI 设备有三个相互独立的物理空间地址:存储器地址空间、I/O地址空间、配置空间地址空间,而配置空间是一个 PCI 特有的物理空间,通常由硬件端提供。
系统上电时 BIOS 检测 PCI 总线,确定所有连接在 PCI 连接在 PCI 总线上的设备以及它们的配置要求,并进行系统配置。所以 PCI 设备必须实现配置空间,从而实现参数的自动配置。
PCI配置空间由 48 字节的头部、6字节的PCI Class Code、32字节的设备配置空间和可选的一些扩展配置空间组成。配置空间中存储设备的基本属性和状态信息,例如设备的供应商ID和设备ID、中断线路号、总线号、IO映射和内存映射等资源等。
PCI子系统通过访问PCI设备的配置空间,来获取设备的配置信息,并进行设备初始化和驱动程序加载。这些配置信息是由设备制造商提供的,它们可以被Linux内核和相应的驱动程序用来管理和控制PCI设备。
二、对配置空间的访问
访问方式有:I/O端口空间访问和内存空间访问。
x86 架构中 pci 配置空间的具体到代码中的访问有4种方式:pci_bios、pci_conf1、pci_conf2、pci_mmcfg。最优的方式是 mmcfg,这需要 bios 配置,把 pci 配置空间映射到 cpu mem 空间,也就是内存空间访问;
pci_conf1、pci_conf2 方式是通过I/O端口空间访问间接访问的;
pci_bios 方式应该是调用 bios 提供的服务进程进行访问。
使用 I/O 访问的方式只可以访问配置空间的前256字节,而使用 mmcfg 的方式则可以完全支持 PCIE 的扩展寄存器即4K字节的配置空间。在 linux 初始化的时候,需要给驱动程序选择一种最优的访问方式。
三、PCI 驱动中对配置空间的访问的实现
1. PCI驱动读写接口
系统在初始化PCI总线的时候,会设置好读取配置空间的方法,读取的方式就上述的两大类(I/O端口访问、MEM访问),提供给上层的可用接口函数是read函数和write函数,系统初始化完成后会将实现好的 read 方法和 write 方法绑定至结构体pci_ops,我们首先来看两段代码。
该段代码是对 read 方法和 write 方法的实现,raw_pci_read 和raw_pci_write 函数,它们分别对应PCI总线上的读和写操作。这两个函数接收相同的参数:domain(总线域)、bus(总线号)、devfn(设备和函数号)、reg(寄存器号)、val(要读取或写入的值)和len(值的长度)。在读写操作中,首先通过判断 domain 和 reg 的值来确定最终使用哪个函数指针;如果 raw_pci_ops 和 raw_pci_ext_ops 都不存在则返回错误码-EINVAL。
需要注意的是在实际使用哪怕是枚举过程我们对于配置空间的访问使用的函数是以下几种:
int pci_read_config_byte(const struct pci_dev *dev, int where, u8 *val) //8
int pci_read_config_word(const struct pci_dev *dev, int where, u16 *val) //16
int pci_read_config_dword(const struct pci_dev *dev, int where, u32 *val) //32
int pci_write_config_byte(const struct pci_dev *dev, int where, u8 val) //18
int pci_write_config_word(const struct pci_dev *dev, int where, u16 val) //16
int pci_write_config_dword(const struct pci_dev *dev, int where, u32 val) //32
这些是linux源码中提供的访问函数接口,这些函数本质上是调用 pci_ops 的 raw_pci_read 和raw_pci_write 函数的(可以认为是内核在pci_ops的一个封装)。我们看一个使用实例,如下:
这个里面的 PCI_XXX 宏均来自于 <linux/pci.h> 头文件中,其它的配置空间寄存器也可以在里面找到。 上面的实例参照 PCI Agent 设备的配置空间可以直观得看到,如下
2. PCI配置空间读写函数的实现过程
2.1 I/O 访问方式配置
pci_direct_probe 函数
位置:arch\x86\pci\direct.c
系统启动的 grub.cfg 可知,bootloder 在启动内核时没有传入相应的参数,所以 pci_probe 使用默认值,即:
函数解析:该函数的作用主要就是确定PCIE设备的 I/O 访问方式。
2.2 内存访问方式配置
pci_mmcfg_early_init 函数
位置:arch\x86\pci\mmconfig-shared.c
函数解析:该函数配置了 raw_pci_ext_ops 方式将其绑定为 pci_mmcfg 的方式并且同时也重新设置了 pci_probe 的值。确定PCIE设备的内存访问方式。
2.3 pci_direct_init 函数
位置:arch\x86\pci\direct.c
函数解析:该函数根据 pci_direct_probe 的返回值来对 raw_pci_ops 和 raw_pci_ext_ops 进行设置,由于 raw_pci_ext_ops 在 pci_mmcfg_early_init()这个函数中已经设置完毕,所以在此无需进行设置,因此该函数直接返回。
2.4 上述函数总结
以上三个函数都是位于 pci_arch_init()函数中,该函数的启动等级为3,此函数就是设置整个PCI总线设备配置空间的读写方法。
函数执行前:
pci_probe = 0xf(默认值)
raw_pci_ops = 空
raw_pci_ext_ops = 空
函数执行后:
pci_probe = 0x8
raw_pci_ops = pci_direct_conf1
raw_pci_ext_ops = pci_mmcfg
即在 0~256 字节大小配置空间内采用I/O访问,大于256字节则才有内存空间访问。
2.5 再次确认pci_mmcfg配置
pci_mmcfg_late_init 函数
位置:arch\x86\pci\mmconfig-shared.c
函数解析:此函数的执行等级较后,它是 pci_mmcfg 的第二次配置,即如果前面 pci_mmcfg 配置异常则再次配置,此时 pci_probe 的值为 0x8 而 raw_pci_ext_ops 绑 定 pci_mmcfg,所以表示前面的配置成果,所以如果 pci_mmcfg_early_init 函数完成了配置那么 pci_mmcfg_late_init 函数一般就直接返回。
四、总结
经过对上面四个函数的的分析,我们可以梳理出对于PCI设备配置空间的访问方法的实现,系统是如何完成的: