一說到資源,大家馬上想到“利用”兩個字。是的,沒有利用價值的資源不是真正的資源。大到整個社會,小到個人,都在利用資源實現自己的想法。PCI設備也不例外,想讓PCI設備工作,PCI設備驅動一定要有資源可以利用,但是這個資源從何而來?下面就來介紹一下EFI下最爲重要的一個驅動:PCIBUS驅動。
在此註明一下,IRQ也是PCI資源重要的一種。但是其並不是PCIBUS驅動所設,之前我也有寫過一篇INTERRUPT的文章講述這一點,所以此篇文章將此忽略。
PCIBUS驅動要爲PCI設備分配的資源有哪些呢?換言之,一個PCI設備插到系統中,它需要什麼東西纔可以正常工作?IO,Memory,IRQ,OpROM。IRQ是CPU查找設備請求訪問的觸發機制,IO和MEMORY是CPU訪問設備所用到的映射機制,OpROM是PCI設備廠商方便用戶使用其設備所提供的一個便利機制。想要搞懂如何分配這些資源,首先PCIBUS要找到相應的設備,我們來看一下PCIBUS找設備的過程。
1 PCI掃描
在PLTRST#過後,所有PCI設備均處於初始狀態。包括PCI BUS,也只有Host Bridge掌控的BUS0處於正常狀態,其餘的PCI橋都處於“雜亂無章”的狀態。如果想讓PCI BUS能夠正常工作,能夠讓CPU發出的PCI訪問信息正確路由至PCI設備處,我們必須掃描PCI BUS,爲各個PCI 橋分配上正確的Primary Bus,Secondary Bus和SubOrdinate Bus。
EFI BIOS採用深度優先的算法來掃描PCI Bus,具體可查看UDK2014 PCIBUS驅動的PciScanBus函數,這是一個遞規的函數。從BUS0開始,見到有掃描的PCI設備爲PCI橋的時候,便爲該PCI橋分配Secondary Bus,然後立即轉入該Secondary Bus的掃描。除了設置PCI Bus Number,PCI BUS驅動還會根據PCI橋下掛的設備資源來設置PCI橋的2個BASE ADRESSRegister以及MEMORY BASE,Limit和IO Base,Limit。
在掃描的過程中,PCI BUS驅動會檢查PCI Agent(我也不知道Agent怎麼翻譯)。如果其支持內存,IO和OpROM,則PCI BUS會計算其大小,然後爲其分配基址。下面來看一下,PCI BUS如何獲取PCI設備是否支持MEMORY,IO,OpROM的,以及如何獲取其大小的。
2PCIBar
一個PCI Agent,有6個PCIBar,在其配置空間的0ffset 0x10至0x24處,每個Bar佔4個字節。PCI Agent需不需要Memory和IO,需要多少,是由PCI Agent本身決定的,所以我們要通過訪問這些BAR來確定這些資源。
2.1 是否需要MEMORY和IO
在PCIBUS Gather PCI Agent信息的時候,會調用BarExisted()函數來確定某個BAR是否支持MEMORY和IO。我們來看一個函數體:
EFI_STATUS
BarExisted(
IN PCI_IO_DEVICE *PciIoDevice, //指向該PCI Agent結構體
IN UINTN Offset, //這個是BAR的offset,0x10到0x24
OUT UINT32 *BarLengthValue, //用來接收BAR需要的資源長度,這個最重要
OUT UINT32 *OriginalBarValue //PCI AGENT原始值
)
{
EFI_PCI_IO_PROTOCOL *PciIo;
UINT32 OriginalValue;
UINT32 Value;
EFI_TPL OldTpl;
PciIo = &PciIoDevice->PciIo;
// 保存原始的BAR值
PciIo->Pci.Read (PciIo,EfiPciIoWidthUint32, (UINT8) Offset, 1, &OriginalValue);
// 提升本任務優先級別,TPL_HIGH_LEVEL爲最高級別
OldTpl = gBS->RaiseTPL (TPL_HIGH_LEVEL);
//寫全1,再讀回值,對於有關長度的BIT,寫1無效。
PciIo->Pci.Write (PciIo,EfiPciIoWidthUint32, (UINT8) Offset, 1, &gAllOne);
PciIo->Pci.Read (PciIo, EfiPciIoWidthUint32,(UINT8) Offset, 1, &Value);
PciIo->Pci.Write (PciIo,EfiPciIoWidthUint32, (UINT8) Offset, 1, &OriginalValue);
// 恢復任務優先級
gBS->RestoreTPL (OldTpl);
if (BarLengthValue != NULL) {
*BarLengthValue = Value;
}
if (OriginalBarValue != NULL) {
*OriginalBarValue = OriginalValue;
}
//返回值爲0,說明此BAR不支持MEMORY或IO,否則是支持的
if (Value == 0) {
return EFI_NOT_FOUND;
} else {
return EFI_SUCCESS;
}
}
如果此BAR根本不支持MEMORY或IO,再去取補計算這個BAR的LENGTH是沒意義的。
2.2 計算BAR長度
計算BAR長度,就是對2.1中讀到的BarLengthValue參數求補操作。
看一下下面的程序段,Value就是BarLengthValue。
該BAR請求IO的情況:
Mask = 0xfffffffc;
if ((Value & 0xFFFF0000) != 0) { //操,難道還有爲0的情況?
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeIo32;
PciIoDevice->PciBar[BarIndex].Length = ((~(Value & Mask)) + 1);//該BAR真正的長度
PciIoDevice->PciBar[BarIndex].Alignment =PciIoDevice->PciBar[BarIndex].Length - 1;
} else {
//It is a IO16 bar
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeIo16;
PciIoDevice->PciBar[BarIndex].Length = 0x0000FFFF & ((~(Value & Mask)) +1);
PciIoDevice->PciBar[BarIndex].Alignment = PciIoDevice->PciBar[BarIndex].Length- 1;
如果BIT0=0,那麼該BAR是請求MEMORY的BAR,如下圖所示,可以看出其需求:
Mask =0xfffffff0;
PciIoDevice->PciBar[BarIndex].BaseAddress= OriginalValue & Mask;
switch (Value & 0x07) {
case 0x00: //BIT2,1=00B,需求32位
if ((Value & 0x08) != 0) {
PciIoDevice->PciBar[BarIndex].BarType= PciBarTypePMem32;
} else {
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeMem32;
}
PciIoDevice->PciBar[BarIndex].Length = (~(Value & Mask)) + 1; //和IO沒啥區別
if (PciIoDevice->PciBar[BarIndex].Length< (SIZE_4KB)) {
PciIoDevice->PciBar[BarIndex].Alignment = (SIZE_4KB - 1);
} else {
PciIoDevice->PciBar[BarIndex].Alignment =PciIoDevice->PciBar[BarIndex].Length - 1;
}
break;
case 0x04: //BIT2,1=10B,需求64位,需要兩個BAR
if ((Value & 0x08) != 0) {
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypePMem64;
} else {
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeMem64;
}
PciIoDevice->PciBar[BarIndex].Length = Value & Mask;
PciIoDevice->PciBar[BarIndex].Alignment =PciIoDevice->PciBar[BarIndex].Length - 1;
Offset += 4;
Status = BarExisted (
PciIoDevice,
Offset,
&Value,
&OriginalValue
);
if (EFI_ERROR (Status)) {
if(PciIoDevice->PciBar[BarIndex].Length == 0) {
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeUnknown;
}
return Offset + 4;
}
Value |= ((UINT32)(-1) <<HighBitSet32 (Value));
PciIoDevice->PciBar[BarIndex].BaseAddress |= LShiftU64 ((UINT64)OriginalValue, 32);
PciIoDevice->PciBar[BarIndex].Length = PciIoDevice->PciBar[BarIndex].Length |LShiftU64 ((UINT64) Value, 32);
PciIoDevice->PciBar[BarIndex].Length =(~(PciIoDevice->PciBar[BarIndex].Length)) + 1;//64位MEMORY,長度和32位算法一樣
if(PciIoDevice->PciBar[BarIndex].Length < (SIZE_4KB)) {
PciIoDevice->PciBar[BarIndex].Alignment = (SIZE_4KB - 1);
} else {
PciIoDevice->PciBar[BarIndex].Alignment =PciIoDevice->PciBar[BarIndex].Length - 1;
}
break;
//其它情況,PCI還未定義
default:
PciIoDevice->PciBar[BarIndex].BarType = PciBarTypeUnknown;
PciIoDevice->PciBar[BarIndex].Length = (~(Value & Mask)) + 1;
if(PciIoDevice->PciBar[BarIndex].Length < (SIZE_4KB)) {
//
// Force minimum 4KByte alignment forVirtualization technology for Directed I/O
//
PciIoDevice->PciBar[BarIndex].Alignment= (SIZE_4KB - 1);
} else {
PciIoDevice->PciBar[BarIndex].Alignment =PciIoDevice->PciBar[BarIndex].Length - 1;
}
break;
}
這些BAR和長度算出後,緊接着PCIBUS就會提交給HOST Bridge該設備的資源請求,HOST Bridge進行分配。
3 OpRom
3.1 判斷是否支持OpRom
如果設備上有一顆I2C的ROM,那麼該PCI Agent是支持OpROM的。那如何確定是否支持呢?我們來看一下UDK2014的一段代碼:
EFI_STATUS
GetOpRomInfo(
IN OUT PCI_IO_DEVICE *PciIoDevice
)
{
UINT8 RomBarIndex;
UINT32 AllOnes;
UINT64 Address;
EFI_STATUS Status;
UINT8 Bus;
UINT8 Device;
UINT8 Function;
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL*PciRootBridgeIo;
Bus = PciIoDevice->BusNumber;
Device = PciIoDevice->DeviceNumber;
Function = PciIoDevice->FunctionNumber;
PciRootBridgeIo =PciIoDevice->PciRootBridgeIo;
//PCI Agent的ROMBar在配置空間的0x30處,而PciBridge的RomBar在0x38處,我們先假設其爲PCI Agent
RomBarIndex = PCI_EXPANSION_ROM_BASE;
if (IS_PCI_BRIDGE (&PciIoDevice->Pci)){ //若PCI設備爲橋設備,則將Index改爲0x38
RomBarIndex= PCI_BRIDGE_ROMBAR;
}
//和PCIBar沒什麼區別,也要往寄存器裏寫全1
AllOnes = 0xfffffffe;
Address = EFI_PCI_ADDRESS (Bus, Device,Function, RomBarIndex);
Status = PciRootBridgeIo->Pci.Write (
PciRootBridgeIo,
EfiPciWidthUint32,
Address,
1,
&AllOnes
);
if (EFI_ERROR (Status)) {
return EFI_NOT_FOUND;
}
Status = PciRootBridgeIo->Pci.Read(
PciRootBridgeIo,
EfiPciWidthUint32,
Address,
1,
&AllOnes
);
if (EFI_ERROR (Status)) {
return EFI_NOT_FOUND;
}
//BIT0指示是否允許OpROM的讀取,BIT1到BIT10指示其大小
AllOnes &= 0xFFFFF800;
if ((AllOnes == 0) || (AllOnes ==0xFFFFF800)) {
return EFI_NOT_FOUND;
}
PciIoDevice->RomSize = (UINT64)((~AllOnes) + 1); //也相當於取補,和PCIBAR一樣
return EFI_SUCCESS;
}
當該RomSize確定後,會在LoadOpRomImage()中找尋OpROM,並將OpRom下載至相應內存處。該種方法在讀之前,需要進行RomDecode,就是將Expantion ROM base address的BIT0置1,那麼PCI Agent就會映射EEPROM的東西至該BASE Address處,我們就可以通過讀內存的方式讀取ROMSize個字節。
3.2 另一種找尋ROM的方法
OpROM並不總是放在EEPROM中,現在BIOS的作法是將其直接配在BIOS中,這樣可以節省一顆ROM芯片,並且省去了RomDecode的步驟。我使用的是INSYDE代碼,不敢貼代碼。現在就把實現流程說一下吧。
Step1,將OpROM編譯成一個RAW型SECTION,並生成一個FILE,放入任一FV中。
Step2,將OpROM的VID,DID,以及FILE GUID放入一個固定的數組中。
Step3,安裝一個事件,並使用RegisterProtocolNotify(),在gEfiBdsArchProtocolGuid安裝時,偷樑換柱,重定向BDS入口函數至自己的一個函數XXX上。
Step4,BDS進入時,先執行XXX,重定向gEfiPciPlatformProtocolGuid的實例。
Step5,PCI BUS掃描時,在RegisterPciDevice()函數中,優先CHECK STEP2的OpROM,如果VID和DID相同,則將STEP1中的OPROM解析至內存中。
(唉,每次寫點東西,查閱INSYDE代碼的時候,老是覺得自己是偷竊。希望早日把國產平臺的BIOS做出來。這纔是我自己真正的東西。)