30. PCI 루트 브리지 찾은 후 시스템의 모든 PCI 기능 가져오기

해당 장에서는 시스템에서 사용할 수 있는 모든 PCI 장치를 보여주고자 합니다.

이 작업을 위해 UEFI 스펙에서 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL을 활용해야 한다. 해당 프로토콜은 시스템의 모든 PCI 루트 브리지에 설치된다. 이 루트 브리지 아래에서 PCI 장치에 엑세스하기 위한 다양한 기능을 제공한다. 예를 들어 도움을 통하여 모든 PCI 장치에 대한 PCI 장치 메모리, I/O 및 구성 공간을 읽을 수 있다.

아래의 프로토콜 구조를 보고 무엇을 할 수 있는지 예측할 수 있다.

typedef struct _EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL {
 EFI_HANDLE ParentHandle;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_POLL_IO_MEM PollMem;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_POLL_IO_MEM PollIo;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Mem;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Io;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Pci;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_COPY_MEM CopyMem;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_MAP Map;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_UNMAP Unmap;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ALLOCATE_BUFFER AllocateBuffer;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_FREE_BUFFER FreeBuffer;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_FLUSH Flush;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_GET_ATTRIBUTES GetAttributes;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_SET_ATTRIBUTES SetAttributes;
 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_CONFIGURATION Configuration;
 UINT32 SegmentNumber;
} EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL;

시스템에서 많은 PCI 루트 브리지가 있기 때문에 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 또한 많이 존재한다. 그래서 이를 LoacteHandleBuffer를 사용하여 해당 프로토콜이 있는 모든 핸들을 가져온 후 모든 핸들에서 OpenProtocol을 사용하여 반복해야 한다.

FI_BOOT_SERVICES.LocateHandleBuffer()

Summary:
Returns an array of handles that support the requested protocol in a buffer allocated from pool.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE_BUFFER) (
 IN EFI_LOCATE_SEARCH_TYPE SearchType,
 IN EFI_GUID *Protocol OPTIONAL,
 IN VOID *SearchKey OPTIONAL,
 OUT UINTN *NoHandles,
 OUT EFI_HANDLE **Buffer
 );

Parameters:
SearchType 	Specifies which handle(s) are to be returned.
Protocol 	Provides the protocol to search by. This parameter is only valid for a SearchType of ByProtocol.
SearchKey 	Supplies the search key depending on the SearchType.
NoHandles 	The number of handles returned in Buffer.
Buffer 		A pointer to the buffer to return the requested array of handles that support Protocol.
		This buffer is allocated with a call to the Boot Service EFI_BOOT_SERVICES.AllocatePool().
		It is the caller's responsibility to call the Boot Service EFI_BOOT_SERVICES.FreePool() when the caller no longer
		requires the contents of Buffer.

Description:
The LocateHandleBuffer() function returns one or more handles that match the SearchType request. Buffer is allocated from pool, and the number of entries in Buffer is returned in NoHandles. Each
SearchType is described below:

AllHandles 		Protocol and SearchKey are ignored and the function returns an array of every handle in the system.
ByRegisterNotify 	SearchKey supplies the Registration returned by EFI_BOOT_SERVICES.RegisterProtocolNotify(). 
			The function returns the next handle that is new for the Registration.
			Only one handle is returned at a time, and the caller must loop until 
			no more handles are returned. Protocol is ignored for this search type.
ByProtocol 		All handles that support Protocol are returned. SearchKey is ignored for this search type.
EFI_STATUS             Status;
UINTN                  HandleCount;
EFI_HANDLE             *HandleBuffer;
Status = gBS->LocateHandleBuffer(
                ByProtocol,
                &gEfiPciRootBridgeIoProtocolGuid,
                NULL,
                &HandleCount,
                &HandleBuffer
              );
if (EFI_ERROR (Status)) {
  Print(L"Can't locate EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL: %r\n", Status);
  return Status;
}

Print(L"Number of PCI root bridges in the system: %d\n", HandleCount);
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL* PciRootBridgeIo;
for (UINTN Index = 0; Index < HandleCount; Index++) {
  ...
}
FreePool(HandleBuffer);

EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 사용을 위해<Protocol/PciRootBridgeIo.h> 헤더를 추가하고 FreePool을 위해 <Library/MemoryAllocationLib.h>를 추가하겠다. 그리고 당연히 inf 파일에 프로토콜도 추가 해야한다.

[Protocols]
  gEfiPciRootBridgeIoProtocolGuid

특정 핸들에 대한 프로토콜을 얻기 위해서 OpenProtocol 함수를 사용하면 된다.

EFI_BOOT_SERVICES.OpenProtocol()

Summary:
Queries a handle to determine if it supports a specified protocol. If the protocol is supported by the
handle, it opens the protocol on behalf of the calling agent. This is an extended version of the EFI boot
service EFI_BOOT_SERVICES.HandleProtocol(). 

Prototype
typedef
EFI_STATUS
(EFIAPI *EFI_OPEN_PROTOCOL) (
 IN EFI_HANDLE Handle,
 IN EFI_GUID *Protocol,
 OUT VOID **Interface OPTIONAL,
 IN EFI_HANDLE AgentHandle,
 IN EFI_HANDLE ControllerHandle,
 IN UINT32 Attributes
 );

Parameters:
Handle 			The handle for the protocol interface that is being opened.
Protocol 		The published unique identifier of the protocol.
Interface 		Supplies the address where a pointer to the corresponding Protocol Interface is returned. NULL will be returned in *Interface if a
			structure is not associated with Protocol. This parameter is optional, and will be ignored if Attributes is EFI_OPEN_PROTOCOL_TEST_PROTOCOL.
AgentHandle 		The handle of the agent that is opening the protocol interface specified by Protocol and Interface. For agents that follow the UEFI
			Driver Model, this parameter is the handle that contains the EFI_DRIVER_BINDING_PROTOCOL instance that is produced by
			the UEFI driver that is opening the protocol interface. For UEFI applications, this is the image handle of the UEFI application that is
			opening the protocol interface. For applications that use HandleProtocol() to open a protocol interface, this parameter is
			the image handle of the EFI firmware.
ControllerHandle 	If the agent that is opening a protocol is a driver that follows the
			UEFI Driver Model, then this parameter is the controller handle that
			requires the protocol interface. If the agent does not follow the UEFI
			Driver Model, then this parameter is optional and may be NULL.
Attributes 		The open mode of the protocol interface specified by Handle and
			Protocol.

Description:
This function opens a protocol interface on the handle specified by Handle for the protocol specified by Protocol.
The first three parameters are the same as EFI_BOOT_SERVICES.HandleProtocol(). The only difference is that the agent that is opening a protocol interface is tracked in an EFI's internal handle
database

해당 함수의 마지막 파라미터인 Attributes에는 여러가지가 존재한다.

#define EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL 0x00000001
#define EFI_OPEN_PROTOCOL_GET_PROTOCOL 0x00000002
#define EFI_OPEN_PROTOCOL_TEST_PROTOCOL 0x00000004
#define EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER 0x00000008
#define EFI_OPEN_PROTOCOL_BY_DRIVER 0x00000010
#define EFI_OPEN_PROTOCOL_EXCLUSIVE 0x00000020

우리는 위의 속성 중 EFI_OPEN_PROTOCOL_GET_PROTOCOL을 사용할 것이며 나머지 속성들은 UEFI 스펙에서 자세히 알아볼 수 있다.

GET_PROTOCOL - 핸들에서 프로토콜 인터페이스를 가져오기 위해 드라이버에서 사용

반복문에서 OpenProtocol 호출을 사용하고 발견된 모든 프로토콜에 대해 직접 만든 EFI_STATUS PrintRootBridge( EFI_PCI_ROOT_BRIDGE_IOPROTOCOL* PciRootBridgeIo) 함수를 호출한다.

for (UINTN Index = 0; Index < HandleCount; Index++) {
  Status = gBS->OpenProtocol (
                  HandleBuffer[Index],
                  &gEfiPciRootBridgeIoProtocolGuid,
                  (VOID **)&PciRootBridgeIo,
                  ImageHandle,
                  NULL,
                  EFI_OPEN_PROTOCOL_GET_PROTOCOL
                );
  if (EFI_ERROR(Status)) {
    Print(L"Can't open protocol: %r\n", Status);
    return Status;
  }
  Print(L"\nPCI Root Bridge %d\n", Index);
  Status = PrintRootBridge(PciRootBridgeIo);
  if (EFI_ERROR(Status)) {
    Print(L"Error in PCI Root Bridge printing\n");
  }
}

이제 PrintRootBridge 함수에 대해서 작성하겠다. 먼저 PCI 루트 브리지에 사용 가능한 모든 bus를 가져와야 한다. 이를 위해 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL에서 Configuration() 함수를 사용할 수 있다.

EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.Configuration()

Summary:

Retrieves the current resource settings of this PCI root bridge in the form of a set of ACPI resource descriptors.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_CONFIGURATION) (
 IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL *This,
 OUT VOID **Resources
 );

Parameters:
This 		A pointer to the EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL. 
Resources 	A pointer to the resource descriptors that describe the current configuration of this PCI root bridge.
		The storage for the resource descriptors is allocated by this function. The caller must treat the return
		buffer as read-only data, and the buffer must not be freed by the caller.

Description:
The Configuration() function retrieves a set of resource descriptors that contains the current
configuration of this PCI root bridge.

또한 다음은 ACPI resource descriptors에 대한 중요한 정보이다. 즉, 해당 함수를 실행하여 얻을 수 있는 데이터이다.

ACPI 규격에는 PCI 루트 브리지에 할당된 현재 리소스를 설명하는데 사용할 수 있는 두가지 resource descriptor 유형만 있다.
이들은 QWORD 주소 Space Descriptor와 End Tag 이다.
QWORD Address Space Descriptor는 동적 또는 고정 리소스에 대한 메모리, I/O 그리고 bus 번호 범위를 설명할 수 있다.
PCI 루트 브리지의 구성은 하나 이상의 QWORD Address Space Descriptors와 End Tag로 설명된다.

따라서 두가지 유형의 ACPI resource descriptors에 대한 ACPI 스펙을 확인해야 한다.

  • QWORD Address Sapce Descriptor

  • End Tag Descriptor

QWROD address space descriptor는 아래의 ACPI 스펙에 정의되어 있다. https://uefi.org/specs/ACPI/6.4/06_Device_Configuration/Device_Configuration.html?#qword-address-space-descriptor 그리고 아래의 파일에 EDKII에서의 구조가 나와있다. https://github.com/tianocore/edk2/blob/master/MdePkg/Include/IndustryStandard/Acpi10.h

///
/// The common definition of QWORD, DWORD, and WORD
/// Address Space Descriptors.
///
typedef PACKED struct {
  UINT8   Desc;
  UINT16  Len;
  UINT8   ResType;
  UINT8   GenFlag;
  UINT8   SpecificFlag;
  UINT64  AddrSpaceGranularity;
  UINT64  AddrRangeMin;
  UINT64  AddrRangeMax;
  UINT64  AddrTranslationOffset;
  UINT64  AddrLen;
} EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR;

End Tag descriptor는 아래의 ACPI 스펙에 정의되어 있다. https://uefi.org/specs/ACPI/6.4/06_Device_Configuration/Device_Configuration.html#end-tag 그리고 아래의 파일에 EDKII에서의 구조가 나와있다. https://github.com/tianocore/edk2/blob/master/MdePkg/Include/IndustryStandard/Acpi10.h

#define ACPI_END_TAG_DESCRIPTOR                   0x79

따라서 PciRootBridgeIo->COnfiguration 호출에서 EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR의 배열을 얻은 후 디스크립터 ACPI_END_TAG_DESCRIPTOR를 만날 때까지 반복해야 한다.

QWORD address space descriptor는 여러 리소스 유형 중 하나를 가질 수 있다.

//
// Resource Type
//
#define ACPI_ADDRESS_SPACE_TYPE_MEM   0x00
#define ACPI_ADDRESS_SPACE_TYPE_IO    0x01
#define ACPI_ADDRESS_SPACE_TYPE_BUS   0x02

지금은 ACPI_ADDRESS_SPACE_TYPE_BUS 유형을 사용할 것이며 얼마나 많은 PCI bus가 PCI 루트 브리지를 가지고 있는지 알아야 한다. 이를 토대로 함수를 만들면 아래와 같다.

EFI_STATUS PrintRootBridge(EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL* PciRootBridgeIo)
{
  EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR* AddressDescriptor;
  EFI_STATUS Status = PciRootBridgeIo->Configuration(
                                         PciRootBridgeIo,
                                         (VOID**)&AddressDescriptor
                                       );
  if (EFI_ERROR(Status)) {
    Print(L"\tError! Can't get EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR: %r\n", Status);
    return Status;
  }
  while (AddressDescriptor->Desc != ACPI_END_TAG_DESCRIPTOR) {
    if (AddressDescriptor->ResType == ACPI_ADDRESS_SPACE_TYPE_BUS) {
      ...
    }
    AddressDescriptor++;
  }
}
return Status;

PCI 루트 브리지에 사용 가능한 모든 bus를 알고 있으면 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.Pci.Read() 함수의 사용으로 장치에 대한 PCI 구성 공간을 읽기 위한 시도를 할 수 있다.

EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.Pci.Read()
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.Pci.Write()

Summary:
Enables a PCI driver to access PCI controller registers in a PCI root bridge’s configuration space.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_IO_MEM) (
 IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL *This,
 IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_WIDTH Width,
 IN UINT64 Address,
 IN UINTN Count,
 IN OUT VOID *Buffer
 );

Parameters:
This		A pointer to the EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.
Width 		Signifies the width of the memory operations.
Address		The address within the PCI configuration space for the PCI controller.
Count 		The number of PCI configuration operations to perform. Bytes moved is Width size * Count, starting at Address.
Buffer 		For read operations, the destination buffer to store the results.
		For write operations, the source buffer to write data from.

Description:
The Pci.Read() and Pci.Write() functions enable a driver to access PCI configuration registers for a
PCI controller.
All the PCI transactions generated by this function are guaranteed to be completed before this function
returns.

Parametersaddress는 아래와 같이 구성된다.

그래서 우리는 Bus/Device/Function/Register 값에서 Address 변수를 생성하는 간단한 함수를 작성한다.

UINT64 PciConfigurationAddress(UINT8 Bus,
                               UINT8 Device,
                               UINT8 Function,
                               UINT32 Register)
{
  UINT64 Address = (((UINT64)Bus) << 24) + (((UINT64)Device) << 16) + (((UINT64)Function) << 8);
  if (Register & 0xFFFFFF00) {
    Address += (((UINT64)Register) << 32);
  } else {
    Address += (((UINT64)Register) << 0);
  }
  return Address;
}

가능한 모든 PCI 함수를 반복하여 모든 기능에 대해 PCI 구성 공간에서 헤더를 읽어보도록 하겠다.

PCI bus, device 및 function의 최대 값은 PCI 규격에 따라 결정된다. EDKII에는 아래와 같이 정의되어 있다.

#define PCI_MAX_BUS     255
#define PCI_MAX_DEVICE  31
#define PCI_MAX_FUNC    7

ACPI와 마찬가지로 최신 PCI 스펙에는 이전 사양이 포함된다.

Pci.h > PciExpress50.h > PciExpress40.h > PciExpress31.h > PciExpress30.h > PciExpress21.h > Pci30.h > Pci23.h > Pci22.h

가능한 모든 PCI 함수에 대해 공통 PCI 구성 공간 헤더를 읽으려고 한다.

///
/// Common header region in PCI Configuration Space
/// Section 6.1, PCI Local Bus Specification, 2.2
///
typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  Command;
  UINT16  Status;
  UINT8   RevisionID;
  UINT8   ClassCode[3];
  UINT8   CacheLineSize;
  UINT8   LatencyTimer;
  UINT8   HeaderType;
  UINT8   BIST;
} PCI_DEVICE_INDEPENDENT_REGION;

데이터를 가져온 후 VendorId 필드가 유효한지 확인한다. 0xffff와 같지 않으면 실제 PCI 기능이며 이 경우에는 정보를 출력한다.

아래는 Bus/Device/Func의 반복에 대한 코드이다.

for (UINT8 Bus = AddressDescriptor->AddrRangeMin; Bus <= AddressDescriptor->AddrRangeMax; Bus++) {
  for (UINT8 Device = 0; Device <= PCI_MAX_DEVICE; Device++) {
    for (UINT8 Func = 0; Func <= PCI_MAX_FUNC; Func++) {
      UINT64 Address = PciConfigurationAddress(Bus, Device, Func, 0);
      PCI_DEVICE_INDEPENDENT_REGION PCIConfHdr;
      Status = PciRootBridgeIo->Pci.Read(
        PciRootBridgeIo,
        EfiPciWidthUint8,
        Address,
        sizeof(PCI_DEVICE_INDEPENDENT_REGION),
        &PCIConfHdr
      );
      if (!EFI_ERROR(Status)) {
        if (PCIConfHdr.VendorId != 0xffff) {
          Print(L"\tBus: %02x, Dev: %02x, Func: %02x - Vendor:%04x, Device:%04x\n",
                                                                  Bus,
                                                                  Device,
                                                                  Func,
                                                                  PCIConfHdr.VendorId,
                                                                  PCIConfHdr.DeviceId);
        }
      } else {
        Print(L"\tError in PCI read: %r\n", Status);
      }
    }
  }
}

빌드 후 실행하면 아래와 같은 결과가 나온다.

FS0:\> ListPCI.efi
Number of PCI root bridges in the system: 1

PCI Root Bridge 0
        Bus: 00, Dev: 00, Func: 00 - Vendor:8086, Device:1237
        Bus: 00, Dev: 01, Func: 00 - Vendor:8086, Device:7000
        Bus: 00, Dev: 01, Func: 01 - Vendor:8086, Device:7010
        Bus: 00, Dev: 01, Func: 03 - Vendor:8086, Device:7113
        Bus: 00, Dev: 02, Func: 00 - Vendor:1234, Device:1111

결과에 대한 확인은 UEFI Shell의 pci 명령어를 통해서 확인이 가능하다.

FS0:\> pci
   Seg  Bus  Dev  Func
   ---  ---  ---  ----
    00   00   00    00 ==> Bridge Device - Host/PCI bridge
             Vendor 8086 Device 1237 Prog Interface 0
    00   00   01    00 ==> Bridge Device - PCI/ISA bridge
             Vendor 8086 Device 7000 Prog Interface 0
    00   00   01    01 ==> Mass Storage Controller - IDE controller
             Vendor 8086 Device 7010 Prog Interface 80
    00   00   01    03 ==> Bridge Device - Other bridge type
             Vendor 8086 Device 7113 Prog Interface 0
    00   00   02    00 ==> Display Controller - VGA/8514 controller
             Vendor 1234 Device 1111 Prog Interface 0

챕터를 끝내기 이전에 PciLib를 활용하여 PCI 구성 공간 레지스터에 엑세스도 가능하니 아래의 링크도 참고하자.

https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Library/PciLib.h

Last updated