首页 理论教育PCIExpress:Linux下PCI总线号初始化办法

PCIExpress:Linux下PCI总线号初始化办法

【摘要】:PCI总线树的枚举由pci_scan_child_bus函数完成,该函数的主要作用是分配PCI总线树的PCI总线号,而并不初始化PCI设备使用的BAR空间。subordinate总线号记载当前PCI总线树中最后一个PCI总线号,因此只有完成了对PCI总线树的枚举后,才能获得该参数。值得注意的是,在Linux系统中,许多PCIe设备并没有提供该结构。PCI总线规范规定了获取BAR空间的标准实现方法。

PCI总线树的枚举由pci_scan_child_bus函数完成,该函数的主要作用是分配PCI总线树的PCI总线号,而并不初始化PCI设备使用的BAR空间。

pci_scan_child_bus函数在第一次执行时[17],首先遍历当前HOST主桥之下所有的PCI设备,如果在HOST主桥下含有PCI桥,将再次遍历这个PCI桥下的PCI设备。并以此递归,直到将当前PCI总线树遍历完毕,并返回当前HOST主桥的subordinate总线号。subordinate总线号记载当前PCI总线树中最后一个PCI总线号,因此只有完成了对PCI总线树的枚举后,才能获得该参数。pci_scan_child_bus函数如源代码14-25和源代码14-29所示。

源代码14-25 pci_scan_child_bus函数片段1

该函数首先调用pci_scan_slot函数,扫描当前PCI总线的所有设备,并将其加入到对应总线的设备队列中。在pci_scan_bus_parented函数调用pci_scan_child_bus函数时,其输入参数为HOST主桥的pci_bus结构,此时pci_scan_slot函数首先初始化与HOST主桥直接相连的PCI设备,即Bus号为0的PCI设备。

1.pci_scan_slot函数

一条PCI总线上最多有32个设备,每个设备最多有8个Function。pci_scan_child_bus函数需要枚举每一个可能存在的Function。因此对于一条PCI总线,pci_scan_child_bus函数需要调用0x100次pci_scan_slot函数。而pci_scan_slot函数调用pci_scan_single_device函数配置对当前PCI总线上的所有PCI设备。

pci_scan_single_device函数进一步调用了pci_scan_device和pci_device_add函数。其中pci_scan_device函数主要对PCI设备的配置寄存器进行读写操作,侧重于PCI设备进行硬件层面的初始化操作,而pci_device_add函数侧重于软件层面的初始化。pci_scan_device函数如源代码14-26所示。

源代码14-26 pci_scan_device函数

pci_scan_device函数首先读取PCI设备的Vendor ID和Header Type寄存器,并根据这两个寄存器的内容对PCI设备进行完整性检查,之后创建pci_dev结构,并对该结构进行基本的初始化。

set_pcie_port_type函数的主要作用是处理PCI Express Extended Capabilities结构,并将其保存在pci_dev→pcie_type参数中,该结构的详细描述见第4.3.2节。值得注意的是,在Linux系统中,许多PCIe设备并没有提供该结构。在这段源代码的最后将调用pci_setup_de-vice函数,其实现如源代码14-27所示。

源代码14-27 pci_setup_device函数

pci_setup_device函数首先根据Header Type寄存器,判断当前PCI设备是PCI Agent设备、PCI桥还是Card Bus。PCI Agent设备使用的配置空间与PCI桥所使用的配置空间并不相同,因此Linux PCI需要区别处理这两种配置空间。本节忽略Card Bus的处理过程。

pci_setup_device函数需要调用pci_read_irq和pci_read_bases函数访问PCI设备的配置空间,并进一步初始化pci_dev结构的其他参数。

pci_read_irq函数的主要作用是读取PCI设备配置空间的Interrupt Pin和Interrupt Line寄存器,并将结构赋值到pci_dev→pin和irq参数中。其中pin参数记录当前PCI设备使用的中断引脚,而irq参数存放系统软件使用的irq号。

值得注意的是,在pci_setup_devic函数中初始化的pci_dev→irq参数并不一定是PCI设备驱动程序在request_irq函数中使用的irq入口参数。如果当前Linux x86系统使用了I/O A-PIC控制器时,Linux设备驱动程序调用pci_enable_device函数将会改变pci_dev→irq参数,详见第15.1.1节。

而如果PCIe设备使能了MSI/MSI-X中断处理机制,pci_dev→irq参数在设备驱动程序调用pci_enable_msi/pci_enable_msix函数后也将会发生变化,详见第15.2节。只有x86处理器使用8259A中断控制器处理PCI设备的中断请求时,pci_dev→irq参数才与Interrupt Line寄存器中的值一致。

pci_read_bases函数访问PCI设备的BAR空间和ROM空间,并初始化pci_dev→resource参数。在第12.3.2节Capric卡的初始化中使用的pci_resource_start和pci_resource_len函数就是从pci_dev→resource参数中获得BAR空间使用的基地址长度

这里有一个细节需要提醒读者注意,在pci_dev→resource参数中存放的BAR空间的基地址属于存储器域,而在PCI设备的BAR寄存器中存放的基地址属于PCI总线域。在x86处理器中,这两个值虽然相同,但是所代表的含义不同。

pci_read_bases函数调用__pci_read_base函数对pci_dev→resource参数进行初始化,__pci_read_base函数的实现方式如源代码14-28所示。

源代码14-28 __pci_read_base函数

__pci_read_base函数的实现较为简单,本节仅介绍该函数获取BAR空间长度的方法。PCI总线规范规定了获取BAR空间的标准实现方法。其步骤是首先向BAR寄存器写全1,之后再读取BAR寄存器的内容,即可获得BAR空间的大小。

我们以Capric卡为例说明该过程,由上文所示Capric卡的BAR0空间为不可预读的存储器空间,大小为0x10000字节。这个设备在被初始化之前,其BAR0寄存器的值由硬件预置,其值为0xFFFF-0000,其中BAR0寄存器的第15~0位只读,其15~4字段为0表示所申请的空间大小为64KB;第3位为0表示不可预读;第2~1字段为0x00表示BAR0空间必须映射到PCI总线域的32位地址空间中;第0位为0表示为存储器空间。

当系统初始化完毕后,将BAR0寄存器重新进行赋值,其值为PCI总线域的地址,如0x9030-0000。当软件对这个寄存器写入“~0x0”之后,该寄存器的值将变为0xFFFF-0000,因为最后16位只读。采用此方法可以获得Capric卡BAR0空间的大小。在Linux系统中,可以使用pci_size函数将0xFFFF-0000转换为BAR0空间使用的实际大小,即64KB。这段程序在获得BAR空间的基地址和长度后,继续判断当前BAR空间为64位PCI总线地址空间,还是32位PCI总线地址空间。为简化程序,本节仅列出处理“32位PCI总线地址这种情况”的源代码。

如果是当前PCI设备使用32位地址空间,则这段程序将初始化pci_dev→resource的start和end参数;如果是64位地址空间,该函数也需要初始化pci_dev→resource的start和end参数,只是过程稍微复杂。这段代码留给读者分析。

细心的读者在分析__pci_read_base函数后,会对“pci_read_config_dword(dev,pos,&l)”语句产生疑问。因为从Linux PCI的初始化过程,我们并没有发现处理器何时将PCI设备的BAR寄存器初始化,此时读到变量l的究竟是什么数值?

在x86处理器系统中,虽然Linux PCI并没有对PCI设备的BAR空间进行初始化操作,但是BIOS已经完成了对PCI总线树的枚举过程,因此变量l将保存有效的BAR空间基地址。对于其他处理器体系,负责初始化引导的Firmware可能并没有实现PCI总线树的枚举[18],此时变量l将保存PCI设备的硬件复位值。(www.chuimin.cn)

无论对于哪种处理器系统,执行__pci_read_base函数总能获得正确BAR空间的大小。但是如果有些处理器系统的Firmware没有对PCI总线树进行枚举时,PCI设备的BAR空间中仅为上电复位值。在这些处理器系统中,__pci_read_base函数执行完毕后,在pci_dev→resource中保存的start和end参数仅是PCI设备从E2PROM中获得的初始值。

2.pci_scan_bridge函数

再次回到pci_scan_child_bus函数,分析剩余的程序,如源代码14-29所示。

源代码14-29 pci_scan_child_bus函数片段2

pci_scan_child_bus函数执行完毕pci_scan_slot函数后,将首先调用pcibios_fixup_bus函数。pcibios_fixup_bus函数的主要目的是为一些PCI设备中的errata提供work-around,但是在该函数中还含有一个非常重要的函数,即pci_read_bridge_bases函数。

因为历史原因pci_read_bridge_bases函数一直存在于pcibios_fixup_bus函数中,但是这个函数更应该直接放入到pci_scan_child_bus函数中。pci_read_bridge_b ases函数将读取当前PCI桥[19]的I/O Limit、I/O Base、Memory Limit、Memory Base、Prefetchable Memory Limit和Pre-fetchable Memory Base寄存器,并根据这些寄存器的值,初始化pci_bus→resource参数,该参数存放当前PCI桥所能管理的地址空间。

之后pci_scan_child_bus函数将调用pci_scan_bridge函数处理当前PCI总线上所挂接的PCI桥,并初始化在这个桥片Secondary PCI总线上的PCI设备。值得注意的是pci_scan_bridge函数被调用了两次,一次pass参数等于0,另外一次pass参数等于1。

在一个处理器系统中,有些负责初始化引导的Firmware可能已经完成对PCI总线树的枚举操作,而有些Firmware没有做这样的操作。当pass参数等于0时,pci_scan_bridge函数处理“已完成枚举”的PCI桥;当pass参数等于1时,pci_scan_bridge函数处理“尚未完成枚举”的PCI桥。对于x86处理器系统而言,BIOS将预先对PCI总线树进行枚举;而对于其他处理器系统,如PowerPC处理器系统,U-Boot并没有进行这个枚举操作;当然还存在一种可能,就是Firmware完成了部分枚举。无论是哪种情况,通过两次调用pci_scan_bridge函数,都将完成对处理器系统中所有PCI桥的处理。

在Linux PCI中有许多函数都是通用函数,即各类处理器系统都需要使用的函数,这些通用函数给Linux PCI的设计带来了不小的麻烦。为不同的处理器平台开发通用架构,是对任何资深系统程序员的巨大考验。在Linux PCI中有许多这样的程序。pci_scan_bridge函数是其中之一,该函数的主体实现如源代码14-30所示。

源代码14-30 pci_scan_bridge函数

pci_scan_bridge函数首先读取当前PCI/HOST主桥配置空间的第21~18字节,这段数据的描述如第2.3节所示。在这段数据中,依次存放PCI桥配置寄存器的Secondary Latency Timer、Subordinate Bus Number、Secondary Bus Number和Primary Bus Number寄存器。

这段程序通过判断PCI桥的Subordinate和Secondary总线号是否为0,判断当前PCI桥是否已经被初始化。如果Subordinate或者Secondary总线号不为0,则表示该PCI桥已经被Firmware遍历;如果为0,表示没有被Firmware遍历。

如果当前PCI桥已经被Firmware遍历,即((bus & 0xffff00)...)的计算结果为True时,这段程序将继续判断pass参数,如果为1则跳出;否则这段程序将直接调用pci_add_new_bus函数为这个PCI桥创建pci_bus结构,然后递归调用pci_scan_child_bus函数初始化该PCI桥管理的PCI子树。当pci_scan_child_bus函数递归执行完毕后,这段程序将重新修正pci_bus→subordinate参数。

如果当前PCI桥没有被Firmware遍历,即((bus & 0xffff00)...)的计算结果为False时,这段程序将执行“else”分支,并首先判断pass参数是否为0,如果为0则跳出;否则这段程序将调用pci_add_new_bus函数为这个PCI桥创建并初始化pci_bus结构,同时还需要初始化PCI桥的Subordinate Bus Number、Secondary Bus Number和Prima ry Bus Number寄存器,之后这段程序也递归调用pci_scan_child_bus函数。当pci_scan_child_bus函数递归完毕后,重新修正pci_bus→subordinate参数。

3.acpi_pci_root_add函数的剩余操作

当pci_scan_bridge函数执行完毕后,我们再次回到acpi_pci_root_add函数,如源代码14-31所示。

源代码14-31 acpi_pci_root_add函数片段3

这段代码首先调用acpi_pci_bind_root函数绑定acpi_device与pci_bus结构。该函数还将acpi_device→ops.bind和ops.unbind参数分别赋值为acpi_pci_bind和acpi_pci_unbind。然后这段代码调用acpi_pci_irq_add_prt和acpi_pci_bridge_scan函数分析当前处理器系统的中断路由表,这部分内容将在第15.1.2节介绍。

这段代码在pcie_aspm_enabled、pci_msi_enabled函数成功返回后将HOST主桥的_OSC参数的“MSI supported位”和“Active State Power Management supported”位设置1。

4.acpi_pci_root_start函数

acpi_pci_root_add函数执行完毕后,Linux x86将调用acpi_pci_root_start函数。该函数首先扫描acpi_pci_roots链表,并调用pci_bus_add_devices函数处理这个链表中的每一个HOST主桥。pci_bus_add_devices函数在./driver/pci/bus.c文件中,其实现如源代码14-32所示。

源代码14-32 pci_bus_add_devices函数

这段代码首先调用pci_bus_add_device函数,将当前PCI总线(pci_bus结构)上的所有PCI设备的相关信息(pci_dev结构)加入到proc和sysfs文件系统中。

之后这段代码递归调用pci_bus_add_devices函数遍历当前PCI总线上所有PCI子桥。这段代码最后调用pci_bus_add_child函数初始化PCI子桥pci_bus结构的dev.parent参数,并将一些相关信息加入到sysfs文件系统中。

当acpi_pci_root_start函数返回后,acpi_pci_root_init函数将执行完毕。Linux系统将继续调用acpi_pci_link_init函数进一步初始化PCI总线,该函数与PCI总线的中断路由相关,在第15.1.3节将详细介绍该函数的实现。