Bootstrap

UEFI——PCIe子系统(III) PCIe设备扫描之BusNumber分配

InitializePciHostBridge 函数完成了对HostBridge和RootBridge的初始化之后,接下来就可以对系统中所有的PCIe设备进行扫描并且为其分配资源。这部分内容包含了最开始提出的问题的答案:BDF分配是如何进行的?

Attention


此处从BIOS的层面对PCIe的BDF做一个说明:BIOS只对PCIe设备的Bus Nmuber进行分配,设备的Device Number, Function Nmuber都是设备自身硬件的连接方式决定的,BIOS不会进行更改。所以接下来我们讨论的仅仅是Bus号的分配过程。


PCIe设备的枚举、资源分配都集中在 PciEnumerator这个函数中。执行完这个函数,系统中PCIe设备的层级就全部构建完成了。总体来看这个函数实现的功能就是两个部分:
1. 对设备进行扫描进行信息收集并且分配bus number
2. 对设备进行资源分配

分别对应的就是下图中的两个函数
在这里插入图片描述

函数涉及的内容非常多,目前我也没有完全搞清楚每一步,所以只对当前理解有关系的地方进行详细的介绍。首先按照代码顺序介绍 设备的枚举与Bus Number的分配 PciHostBridgeEnumerator

PciHostBridgeEnumerator

提醒: 对比代码看接下来的分析效果更好!!!
这个函数很长,当前我们关心的就是与PCIe设备枚举、bus number分配相关的部分。因此当前我们只需要关注函数的前面一部分就可以了。相关的代码如下

EFI_STATUS
PciHostBridgeEnumerator (
  IN EFI_PCI_HOST_BRIDGE_RESOURCE_ALLOCATION_PROTOCOL  *PciResAlloc
  )
{
  EFI_HANDLE                         RootBridgeHandle;
  PCI_IO_DEVICE                      *RootBridgeDev;
  EFI_STATUS                         Status;
  EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL    *PciRootBridgeIo;
  UINT16                             MinBus;
  EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR  *Descriptors;
  EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR  *Configuration;
  UINT8                              StartBusNumber;
  LIST_ENTRY                         RootBridgeList;
  LIST_ENTRY                         *Link;

  if (FeaturePcdGet (PcdPciBusHotplugDeviceSupport)) {
    InitializeHotPlugSupport ();
  }

  InitializeListHead (&RootBridgeList);

  //
  // Notify the bus allocation phase is about to start
  //
  Status = NotifyPhase (PciResAlloc, EfiPciHostBridgeBeginBusAllocation);

  if (EFI_ERROR (Status)) {
    return Status;
  }

  DEBUG ((DEBUG_INFO, "PCI Bus First Scanning\n"));
  RootBridgeHandle = NULL;
  while (PciResAlloc->GetNextRootBridge (PciResAlloc, &RootBridgeHandle) == EFI_SUCCESS) {
    //
    // if a root bridge instance is found, create root bridge device for it
    //

    RootBridgeDev = CreateRootBridge (RootBridgeHandle);

    if (RootBridgeDev == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }

    //
    // Enumerate all the buses under this root bridge
    //
    Status = PciRootBridgeEnumerator (
               PciResAlloc,
               RootBridgeDev
               );

    if ((gPciHotPlugInit != NULL) && FeaturePcdGet (PcdPciBusHotplugDeviceSupport)) {
      InsertTailList (&RootBridgeList, &(RootBridgeDev->Link));
    } else {
      DestroyRootBridge (RootBridgeDev);
    }

    if (EFI_ERROR (Status)) {
      return Status;
    }
  }

当前这个函数我们关心的只有上述这个部分

开始介绍

  if (FeaturePcdGet (PcdPciBusHotplugDeviceSupport)) {
    InitializeHotPlugSupport ();
  }

函数首先对支持热插拔的系统进行了一些初始化操作,当前不是操作的重点,跳过

 InitializeListHead (&RootBridgeList);

创建了一个双向的链表,这个链表用来保存根据当前的RootBridge Handle创建的PCI_IO_DEVICE *RootBridgeDev 结构体。

接下来函数进入一个while循环,是对当前能够找到的RootBridge都进行了相同的操作。我们重点介绍这个循环中的内容,因为Bus的分配、需要资源的获取都在这一步。

RootBridgeDev = CreateRootBridge (RootBridgeHandle);

根据系统中已经存在的RootBridgeHandle创建对应的PCI_IO_DEVICE *RootBridgeDev 结构体。
PCI_IO_DEVICE结构定义如下
在这里插入图片描述
这是一个很长的结构体可以看出包含了当前设备的很多属性


简单 看一下初始化函数
在这里插入图片描述

这个函数首先为申请了PCI_IO_DEVICE *RootBridgeDev 申请了地址空间,然后对其中基本的成员进行了初始化:

  1. 结构体的Handle等于传入的Handle
  2. 复制传入Handle上的DevicePath
  3. PciRootBridgeIo等于传入Handle的protocol
  4. 初始化若干成员:PciIo DriverOverride PciLoadFile2
  5. 初始化双向链表 ReservedResourceList OptionRomDriverList
    对以上这些简单的信息进行初始化之后,就正式开始枚举当前这个RootBridge下面的所有的设备

接下来函数进入while循环中,对每一个RootBridge都调用了 PciRootBridgeEnumerator,这个函数是枚举过程的核心函数,函数将当前RootBridge下的所有设备以及相关信息进行收集并将收集的结构存储在结构体链表中。详细过程下一节 PciRootBridgeEnumerator 介绍
最后函数将前面生成的Pcie device的链表(保存在RootBridgeDev->Link)插入到 RootBridgeList中。最终得到的数据结构的状态可以在文章最后查看

PciRootBridgeEnumerator

接下来介绍 PciRootBridgeEnumerator这个函数
在这里插入图片描述
函数开始获取了当前RootBridge的Handle,并且对DevicePath进行相应的处理(暂时不关心这个位置 DevicePath也需要单独整理一下)。接下来调用了HostBridge的函数StartBusEnumeration获取相关的信息

StartBusEnumeration

在这里插入图片描述

在这个函数中,首先在当前HostBridge下找到目标RootBridge,然后根据当前已知的RootBridge信息(CreateRootBridge的时候就已经有了这些信息 详细可以参考上一篇笔记 UEFI——PCIe子系统(II) HostBridge&RootBridge初始化创建RootBridge Instance结构体 一节)设置Configuration,其中关键的就是Descriptor->AddrRangeMin&Descriptor->AddrLen 两个成员,这两个成员分别表示了当前RootBridge的起始Bus Nmuber能够分配的Bus号的跨度

继续回到PciRootBridgeEnumerator函数
在这里插入图片描述
获取到当前RootBridge相应的Configuration之后,等于获取了当前RootBridge的Bus Number范围
在这里插入图片描述
接下来这一段是一个选择排序的具体实现,目的就是为了将当前Configuration 链表中的成员按照AddrRangeMin由小到大形成一个升序的排列
当前这个Configuration的具体含义还不清楚,故不做详细的描述

注 :这是一种ACPI信息相关的结构体 这个部分当前还没有深入了解 详细待补充


补充知识 Subordinate/Secondary/Primary Bus Number

在继续向下看代码之前,需要先介绍一下 Subordinate/Secondary/Primary Bus 的含义

在这里插入图片描述

在Type 1类型Header的配置空间中,Offset 18开始有三个寄存器分别存储了不同含义的BusNumber,Subordinate/Secondary/Primary Bus Number 。在枚举的过程中,这三个bus number也会被分配,最后填写到相应的位置。三个Bus Nmuber 代表的意义如下(详细解释可以参考PCI Express® Base Specification Revision 6.0 7.5.1.2-7.5.1.4):
在这里插入图片描述
总结一下:

Subordinate Bus Number : 当前桥设备下游连接的最大的 Bus Number
Secondary Bus Number : 当前桥设备下游连接的Bus Number
Primary Bus Number :当前桥所在的 Bus Number

为了生动说明这三个Bus number代表的含义可以参考下面的图示
在这里插入图片描述
上面图片中的Bus号已经给出,根据给出的Bus Nmuber,填写紫色框中的Bridge的以上三个参数的情况如图所示。


接下来都以上图的PCIe设备层级作为参考描述枚举的详细过程

在这里插入图片描述

初始化StartBusNumber和SubBusNumber ,可以看到StartBusNumber的值是和RootBridge的设置相关,也就是这个StartBusNumber的值是前面就已经设置好的,在枚举的过程中,需要以此为基础来进行BusNumber的分配
在这里插入图片描述
这个函数不展开说明,比较简单,其作用就是在开始枚举之前,将当前RootBridge下的所有设备的的Subordinate/Secondary/Primary Bus Number 都置0。Spec中也强调过,这三个寄存器的默认置必须为0.
在这里插入图片描述

接下来这个函数就是Bus号分配的核心函数,也是重点函数。
Bus Number分配的实质简单来说就是PciScanBus函数的递归

☆☆☆☆☆ PciScanBus

首先看一下这个函数的传入参数

Bridge : 传入的PCI_IO_DEVICE *RootBridgeDev
StartBusNumber : 枚举的起始Bus Nmuber
SubBusNumber : 最终得到的当前这个RootBridge下最大的Bus Number 这是需要输出的结果
PaddedBusRange : ???? 暂时不清楚 留个问号这

在这里插入图片描述
函数开始进行的都是简单的定义与初始化操作 ,无难点
在这里插入图片描述

接下来是两个for循环,直到函数结束剩下的所有操作都是在这两个for循环中进行的。for循环的参数就是device number 和 function number,通过这个位置的参数我们就能够看出,在UEFI中,只对Bus号进行分配,device/function number 是设备自身的决定的,而对Bus Number 进行分配的方法就是最简单最朴素的“深度遍历”
在这里插入图片描述
进入for循环,首先判断当前的StartBus Device Function组合下是否存在设备,此时如果得到的结果是当前这个BDF组合不存在,那么就存在两种情况分别对应两种不同的处理

  1. 如果设备不存在并且此时设备的Function Nmuber 为0 ,执行 break ,当前device不存在,直接对下一个device number进行查找。 因为PCIe Spec规定设备必须如果设备存在,那么function 0 是必须被定义的,如果是multiple function的设备,同样function 0 是必须被定义的,只不过接下来的function定义则没有顺序的要求。
    在这里插入图片描述
  2. 如果设备不存在并且此时设备的Function Nmuber 不为0 ,那此时可能为multiple function的设备,执行continue,继续查找下一个function。

PciDevicePresent

PciDevicePresent函数除了能够判断当前的设备是否存在,还能够存在的设备的配置空间进行读取。函数的实现比较简单,如下
在这里插入图片描述

这个函数核心就是 Pci.Read,可以简单看一下Pci.Read 就知道这个位置在进行什么操作了。函数原型
在这里插入图片描述

函数依旧是调用的函数,但是不继续向下说了,麻烦而且没必要,一般都是直接用Pci.Read。如果感兴趣可以自己看一下,RootBridgeIoPciAccess能够实现获取PCI配置空间的远离其实就是以IO方式循环读取依次获取配置空间中的信息然后返回buffer
函数的参数介绍

This : this指针 指向自己
Width :以Width宽度进行内存操作
Address : 希望读取的设备的地址,此参数由BDF转换得到,转换的依据0000 0110.UEFI–PCIe系统(I)的介绍
Count : 按照上述的数据宽度想要访问的数据量
Buffer : 返回的访问信息的保存位置

了解了Read的操作之后回头看PciDevicePresent,主要就是实现了两个操作

  1. 读取PCIe 设备配置空间的前4 Byte ,如果返回的VendorID为0xffff,表示当前BDF的位置不存在PCIe设备
  2. 如果当前返回的VendorID不是0xffff,那么读取当前设备的Configuration Space Header并保存读取的结果

☆☆☆☆☆ PciSearchDevice

如果前一步确定当前的BDF位置上确实存在设备,就需要为当前的设备创建相应的PCI_IO_DEVICE *PciDevice结构体。上述操作由函数PciSearchDevice实现,如下所示
在这里插入图片描述

PciSearchDevice 包含了我们关心的绝大部分内容,详细看一下这个函数。
首先是函数的形参

Bridge : 输入参数, 表示当前读取的是哪一个桥下面的设备
Pci : 输入参数,上一个函数PciDevicePresent 中获取到的当前设备的Configuration Space Header
StartBusNumber : 输入参数,当前设备的Bus Nmuber
Device : 输入参数,当前设备的Device Nmuber
Func : 输入参数,当前设备的 Funcrtion Number
PciDevice : 输出参数 ,为当前设备创建的 PCI_IO_DEVICE 结构体

函数 PciSearchDevice首先对当前设备的类型进行了判断,判断的依据就是之前提过的Configuration Space Header中的Header Type的值 (可以参考 UEFI——PCIe子系统(I) PCIe基础知识
之前就介绍了两种类型00 01。其实很久之前的定义中 还有02 这个类型的,现在已经被淘汰了,所以这个地方我们也忽略02类型的情况,只看00 01类型的情况
在这里插入图片描述
当Header Type为00表示当前设备是一个PCIe device,执行 GatherDeviceInfo 为当前设备创建相应的结构体 ,并且将当前设备需要的memory / IO 大小都已经读取保存下来。 详细分析在 UEFI—— 读取Bar寄存器的实现(函数GatherDeviceInfo 解析)
同理当Header Type为01表示当前设备是一个PCIe bridge GatherPpbInfo 为当前设备创建相应的结构体 函数GatherPpbInfo解析
这样无论当前设备是PCIe设备或者PCIe bridge,都能够创建好相应的结构体PCI_IO_DEVICE *PciDevice
在这里插入图片描述
将对应的设备的结构体创建完成之后,将其插入结构体的链表中,这个链表用来维护系统中的所有的Pcie设备的基本信息
对当前的设备进行完基本的信息收集并保存之后,就要开始正式进行设备的枚举与Bus Number的分配了
首先将当前BDF的构成的地址保存下来

PciAddress = EFI_PCI_ADDRESS (StartBusNumber, Device, Func, 0);

在这里插入图片描述

如果当前的设备不是PCI Bridge 可能处理器需要在枚举前对设备进行一定的操作,这个位置对当前不重要跳过
在这里插入图片描述
为了简单化当前的问题,暂时不考虑热插拔的情况,因此上面部分代码同样跳过
接下来的部分是核心部分,这部分对当前设备是Bridge & Device两种情况进行了分别的处理

当前为Bridge

在这里插入图片描述
如果当前为桥设备,首先要做的就是对当前当前设备的下游的最大值 SubBusNumber 加1 。因为 桥设备一定会延展出一条Bus
SecondBus表示桥下游的Bus Nmuber ,此时就应该等于当前延展出的Bus的Number。
综上此时

StartBusNumber 不变
SubBusNumber = SubBusNumber +1
SecondBusNumber = SubBusNumber

在这里插入图片描述
按照前述分析更新当前设备的 Secondary/Primary Bus Number寄存器,(注意 这里没有写入 Subordinate Bus Number Register 因为此时还没有获取到最终的值)
当前设备的
Primary Bus Number Register 应该写入 StartBusNumber
Secondary Bus Number Register 应该写入 SecondBusNumber
故有 Status = PciRootBridgeIo->Pci.write()这一操作
在这里插入图片描述
接下来暂时的将当前Bridge的Subordinate Bus Number Register设置为当前桥能够支持的最大的Bus号
PreprocessController函数不是重点跳过不分析
在这里插入图片描述
接下来是一个递归操作,用最新获得的SecondBus SubBus PaddedBusRange对下一个设备进行PciScanBus的操作
Ps : 起始这个递归并不复杂 可以自己找一个图从头写一下每个变量的变化,就能清楚的看到这是一个深度优先搜索的具体应用
在这里插入图片描述

当递归函数返回,便可以将最终值写入 Subordinate Bus Number Register了,此时这个值就应该等于返回值中的 SubBusNumber
到此为止,若当前设备是Bridge的情况就分析完了。总结来看,这部分的主要流程就是

  1. 发现当前为Bridge设备,更新当前 SubBusNumber+=1 , SecondBus= SubBusNumber
  2. 然后以新的SecondBus为StartBus,与 SubBusNumber一起作为入参对下一个设备再次进行 PciScanBus
当前为Device

在这里插入图片描述
如果当前的设备是Deivce,若当前的设备不支持 PCI IOV(将一个设备虚拟成多个) ,那么不需要进行任何的操作,继续向下执行即可。如果当前的设备支持PCI IOV ,则需要为这个设备预留出一定的Bus Number供其虚拟扩展使用。故调用

PciAllocateBusNumber (PciDevice, *SubBusNumber, (UINT8)(PciDevice->ReservedBusNum - TempReservedBusNum), SubBusNumber);

对需要预留的Bus范围进行保留 (不太了解PCI IOV的具体 先不进行详细的介绍 不影响)
同时对每一个Pcie设备能够预留的BusNumber进行更新 防止溢出出现意外

从这个部分就能够看出来,在PCIe Device的 配置空间头中,并没有一个类似于Bridge 配置空间头中的寄存器用来保留当前的Bus 号。 个人觉得原因主要是Pcie Device是不能够延伸出Bus的,因此只要知道与之相连的Bridge的 Second Bus Number 就是当前设备的Bus Number,在查找时候据此判断即可,故无需多增加一个用以存储此信息的寄存器

跳过剩余Function Number

在这里插入图片描述
如果当前的设备的Fun = 0 并且当前的设备不是一个multiple function 那么可以直接跳过对后面的function的判断

总结

PciScanBus就是按照深度扫描顺序依次扫描当前所有的PCI设备并分配Bus Number,详细的过程在代码中已经解释过了,按照前面的描述可以将前面例中的设备BusNumber 进行分配了 分配的Bus号如图所示
在这里插入图片描述
根据上图,当前RootBridge1下创建维护的设备链表如图所示
在这里插入图片描述
可以发现,PCIe设备链表的层级结构是和实际的设备层级结构相同的

PciRootBridgeEnumerator(II)

PciScanBus执行完成后,我们返回看函数 PciRootBridgeEnumerator 。
在执行完BusNumber的分配(也就是将对应的Bus Number写入Bridge的三个和Bus相关的寄存器中)接下来就是对当前RootBridge的一些信息进行更新
在这里插入图片描述
当前系统资源分配的具体情况我还不了解 先跳过 此时枚举就全部完成了

PciHostBridgeEnumerator (II)

完成当前RootBridge上的枚举之后,接下来就返回最初的函PciHostBridgeEnumerator
在这里插入图片描述
将前面生成的Pcie device的链表插入到 RootBridgeList中,这个部分最开始的时候就已经大致说过了。
前面一系列的函数执行完成之后,现在已经完成了对当前系统中的设备的扫描。将前面所有操作生成的数据结构进行整合,就能得到这一系列操作最终能够得到的数据结构链表状态
在这里插入图片描述

;