# PCI Configuration Space 介紹 本篇文章介紹 PCI/PCIE Configuration Space 及其在 UEFI 領域的應用。PCI 是電腦以及伺服器中常見的周邊連接標準,理解 PCI 對於整個硬體架構以及程式實作方面會事半功倍。 ## PCI Read / Write 在 UEFI 中,對 PCI 配置空間與 I/O/MEM 存取通常透過 EFI_PCI_IO_PROTOCOL(每個 PCI device 的 protocol instance)或 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL(root-bridge 抽象)來完成。這些 protocol 提供 Pci.Read/Pci.Write、Mem.Read/Write、Io.Read/Write 等函式。 硬體層(x86 的 0xCF8/0xCFC, 或 PCIe ECAM/MCFG)會依平台不同而不同;UEFI 提供抽象層使 driver 能用同一種 protocol 來實現不同平台差異的操作。也因此 PCI device driver 不直接操控 I/O port 或直接做 MMIO,而是透過 EFI_PCI_IO / RootBridgeIO 來存取。 ## PCI DMA 當需要做大量資料搬移、或讓 PCI/PCIe 裝置自己直接存取主機記憶體以減少 CPU 負擔、或裝置本身以 bus-master 模式運作時,就會用到 DMA。在 UEFI(DXE / 驅動階段、某些 boot service)常見的使用情境包含:高速儲存(NVMe/AHCI)、網路傳輸 (PXE / NIC)、USB mass-storage、顯示/圖形驅動、以及裝置韌體下載等。 在 UEFI 環境中做 DMA(bus-master)時,常用 EFI_PCI_IO_PROTOCOL(或 root-bridge IO)提供的 Map / Unmap / AllocateBuffer / FreeBuffer / Flush / Attributes 等服務來確保 buffer 對 DMA 裝置可見且被正確 mapping/同步。此外,若平台有 IOMMU,UEFI 會與 IOMMU 協同為 DMA 做保護/映射。 ### DMA 的基本步驟 #### 1. 分配 Host buffer 可用 EFI_PCI_IO_PROTOCOL->AllocateBuffer()(或 Boot Services 的 AllocatePages / AllocatePool 視情況)來獲得適合的 buffer。 #### 2. Map(取得 Device Address) 呼叫 EFI_PCI_IO_PROTOCOL->Map(),傳入 operation(例如 BusMasterRead / BusMasterWrite / CommonBuffer),以及 HostAddress 與長度。 Map() 會回傳 DeviceAddress(device 可用來程式 DMA 的位址)與一個 opaque Mapping token。 程式必須使用回傳的 DeviceAddress 來設定裝置的 DMA descriptor / register。 #### 3. 啟動 DMA(程式 device) 將 DeviceAddress 寫入 device 的 DMA descriptor / register,並啟動 DMA。 #### 4. 等待 DMA 完成並同步 DMA 完成後,若為 write-to-memory 的情況,可能要呼叫 controller-specific read 或 Flush() 以確保 write 緩衝寫回系統記憶體(PCI Spec 要求)。 然後呼叫 PciIo->Unmap()。 對於 CPU 需要讀取 DMA 寫入結果的情境,可能也需做 cache invalidation(視平台而定)。 #### 5. 釋放 Buffer / Unmap 完成後呼叫 Unmap()(傳回 mapping token),並釋放先前分配的 buffer(FreeBuffer() / FreePool)。 ## PCI Configuration Space PCI Configuration Space 和一般記憶體空間是分離的,通常可以透過 I/O space 和 MMIO 的方法存取,但 PCI device 還沒分配到資源之前,只能透過 I/O space 的方式去存取。 傳統的 PCI 裝置的 Configuration Space 包含了 64 Bytes 的 header space 和 192 Bytes 的 capability space,總共 256 Bytes. CPU 可以透過 BDF 加上 register offset 的方式存取 I/O address space 讀取 PCI header. 接下來我們都將以 type 0 (非 Brdige)的設備為例。 下圖為 header space 的架構: ![image](https://hackmd.io/_uploads/HkHhYkeRR.png) ### Vendor ID / Device ID 這兩個欄位是區分不同設備的最重要部分,OS 和 UEFI 透過匹配他們找到不同的設備驅動。為了確保唯一性,Vendor ID 需要向 PCI SIG 組織申請取得。System Software(BIOS/UEFI) 會去掃描所有的PCI deivce,若 Vendor 和 Device ID 回傳 FFFFh /FFFFh,則 device 不存在。 ### Revision ID(08h) 裝置的版本,由廠商制定。 ### Header Type(0Eh) 長度為1 byte,Bit 7用來表示這個 device 是否為 multi-function device,如果為 1 表示這個 device 有多個function number.(ex. Bus 0/Dev 0/Func0, Bus 0/Dev 0/Func1, Bus 0/Dev 0/Func2.....等) Bit 0-6 則用來決 定Configuration Space 是什麼 Type。 ### Device Control(04h): 長度為2 bytes,Device command control regsiter 主要是用來產生或回應PCI cycles。 * [1:0]: 是用來當發生有PCI transactions到device的IO space 或 Memory Space時,這些cycles能不能正常被decode,如果為1表示enable,為0則是disable 。 * [2]: Bus master,bus master的用意是為了減輕CPU的負擔。當device想要發出DMA transactions,它會assert REQ# signal 來爭取bus的使用權, 當獲得GNT# signal的回應後,這個device就可以使用bus。 * [10]: Bit10為device能不能使用INTx來產生interrupt,為1表示disable INTx,為0表示enable,如果disable INTx,通常就會使用MSI/MSI-X來作為interrup來源 ## UEFI 存取 PCI 裝置 在 UEFI 中,我們常會透過 PCI read 去判 斷一個 PCI 裝置類型,例如 VGA controller,如下簡單範例: ```clike! EFI_HANDLE CheckVgaController{ ( IN EFI_DRIVER_BINDING_PROTOCOL *This, IN EFI_HANDLE Controller, IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath ) EFI_STATUS Status; UINTN Index; UINTN HandleIndex; EFI_HANDLE *HandleBuffer; UINTN HandleCount; PCI_TYPE00 Pci; EFI_PCI_IO_PROTOCOL *PciIo; EFI_HANDLE VideoController; VideoController = NULL; // // Get all handles which contain an instance of PCI_IO_PROTOCOL // Status = gBS->LocateHandleBuffer ( ByProtocol, &gEfiPciIoProtocolGuid, NULL, &HandleCount, &HandleBuffer ); if (EFI_ERROR (Status)) { return NULL; } // // For all PCI Devices // for (HandleIndex = 0; (HandleIndex < HandleCount) && (VideoController == NULL); HandleIndex++) { Status = gBS->HandleProtocol (HandleBuffer[HandleIndex], &gEfiPciIoProtocolGuid, (VOID **) &PciIo); if (!EFI_ERROR (Status)) { // // Check if this device is a VGA Controller // Status = PciIo->Pci.Read ( PciIo, EfiPciIoWidthUint32, 0, sizeof (Pci) / sizeof (UINT32), &Pci ); } if(!EFI_ERROR(Status)) { return Status; } if(Status == EFI_SUCCESS) { // // See if the device is an enabled VGA device. // Most systems can only have on VGA device on at a time. // if (((Pci.Hdr.Command & 0x03) == 0x03) && IS_PCI_VGA (&Pci)) { Status = EFI_SUCCESS; } } return Status; } } ```