# 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 的架構:

### 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;
}
}
```