1. PCI 簡介
PCI 匯流排標準是一種將系統外部設備連線起來的匯流排標準,是 PC 中最重要的匯流排,實際上是系統的各個部分如何互動的接口。傳輸速率可達到 133MB/s。在當前的 PC 體系結構中,幾乎所有的外部設備採用的各種各樣的接口匯流排,均是通過橋接電路掛接到 PCI 系統上。在這種 PCI 系統中, Host/PCI 橋稱為北橋,連線主處理器匯流排到基礎 PCI 局部匯流排。 PCI 與其他匯流排的接口稱為南橋,其中南橋還通常含有中斷控制器、IDE 控制器、USB 控制器和 DMA 控制器等。南橋和北橋組成主機板的晶片組。
2. PCI配置空間
每個 PCI 設備都有自己的配置空間,用於支持即插即用,使之滿足現行的系統配置結構。下面對PCI配置空間做一下簡要介紹。
配 置空間是一容量為256位元組並具有特定結構的地址空間。這個空間又分為頭標區和設備有關區兩部分。頭標區的長度是64位元組,每個設備都必須配置該區的暫存 器。該區中的各個欄位用來唯一地識別設備。其餘的192位元組因設備而異。配置空間的頭標區64個位元組的使用情況如圖1示。
為了實現即插即用,系統可根據硬體資源的使用情況,為PCI設備分配新的資源。因此編寫設備驅動程式重點是獲得基址暫存器(Base Address)和中斷幹線暫存器的內容。配置空間共有六個基址暫存器和一個中斷幹線暫存器,具體用法如下:
PCI Base Address 0 暫存器:系統利用此暫存器為PCI接口晶片的配置暫存器分配一段PCI地址空間,通過這段地址我們可以以記憶體映射的形式訪問PCI接口晶片的配置暫存器。
PCI Base Address 1暫存器:系統利用此暫存器為 PCI 接口晶片的配置暫存器分配一段PCI地址空間,通過這段地址我們可以以I/O的形式訪問PCI接口晶片的配置暫存器。
PCI Base Address 2、3、4、5暫存器:系統BIOS利用這些暫存器分配PCI地址空間以支持PCI接口晶片的局部配置暫存器0、1、2、3的訪問。
在所有基址暫存器中,第0位均為唯讀位,表示這段地址映射到存儲器空間還是I/O空間,如果是“1”表示映射到I/O空間,如果是“0”則表示映射到存儲器空間。
中斷幹線暫存器(Interrupt Line):用於說明中斷線的連線情況,這個暫存器的值與標準8259的IRQ編號(0~15)對應。
表1 PCI配置空間
3.設備初始化
PCI 設備驅動程式要完成識別 PCI 器件、尋找 PCI 硬體的資源和對 PCI 器件中斷的服務。在驅動程式初始化過程中,使用 HalGetBusData()函式完 成尋找 PCI 設備的工作。在初始化過程中,使用器件識別號(Device ID)和廠商識別號(Vendor ID),通過遍歷匯流排上的所有設備,尋找到指定的PCI設備,並獲取設備的匯流排號,器件號與功能號。通過這些配置信息,可以在系統中定址該設備的資源配置 列表。
在此之後,驅動程式需要從配置空間獲取硬體的參數。PCI設備的中斷號、連線埠地址的範圍(I/O)方式、存儲器的地址與映射 方式等,都可以從硬體資源列表數據結構中獲取。在Windows NT中,調用HalAssignSlotResources()函式來獲得指定設備的資源列表數據結構指針,然後通過遍歷該列表中的所有資源描述符,獲取 該設備的I/O連線埠基地址與長度,中斷的中斷級、中斷向量與模式,存儲器基地址與長度等硬體資源數據。
我們設計的DMA通信採用匯流排主控方式進行通信,在 設備初始化時需要對DMA適配器進行初始化,使用HalGetAdapter()獲得作業系統分配的適配器對象指針。
示例代碼如下:
// 遍歷匯流排,獲得指定設備的匯流排號,器件號與功能號
for ( busNumber = 0; busNumber < MAX_PCI_BUSES; busNumber++ ) {
for ( deviceNumber = 0;deviceNumber < PCI_MAX_DEVICES;deviceNumber++ ) {
slotNumber.u.bits.DeviceNumber = deviceNumber;
for ( functionNumber = 0; functionNumber < PCI_MAX_FUNCTION; functionNumber++ ) {
slotNumber.u.bits.FunctionNumber = functionNumber;
if (!HalGetBusData(PCIConfiguration, busNumber, slotNumber.u.AsULONG,
&pciData,sizeof(ULONG)) ) {
deviceNumber = PCI_MAX_DEVICES;
break;
}
if (pciData.VendorID == PCI_INVALID_VENDORID ) {
continue;
}
if ( ( VendorId != PCI_INVALID_VENDORID ) &&
( pciData.VendorID != VendorId || pciData.DeviceID != DeviceId )) {
continue;
}
pPciDeviceLocation->BusNumber = busNumber;
pPciDeviceLocation->SlotNumber = slotNumber;
pPciDeviceLocation = &PciDeviceList->List[++count];
status = STATUS_SUCCESS;
}
}
}
// 獲取設備的資源列表數據指針
status = HalAssignSlotResources(RegistryPath,
&pDevExt->ClassUnicodeString,
DriverObject,
DeviceObject,
pDevExt->InterfaceType,
pDevExt->BusNumber,
pDevExt->SlotNumber,
&pCmResourceList );
4. I/O連線埠訪問
在 PC機上,I/O定址方式與記憶體定址方式不同,所以處理方法也不同。I/O空間是一個64K位元組的定址空間,I/O定址沒有實模式與保護模式之分,在各種 模式下定址方式相同。在Windows NT下,系統不允許處於Ring3級的用戶程式和用戶模式驅動程式直接使用I/O指令,對I/O連線埠進行訪問,任何對I/O的操作都需要藉助核心模式驅動 來完成。在訪問I/O連線埠時,使用READ_PORT_XXX與WRITE_PORT_XXX函式來進行讀寫。I/O連線埠基地址使用從配置空間基址暫存器 PCI Base Address 1中返回的I/O連線埠基地址。
示例代碼如下:
RegValue = READ_PORT_ULONG(pBaseAddr+RegOffSet);
WRITE_PORT_ULONG(pBaseAddr+ RegOffset, RegValue);
5. 設備記憶體訪問
Winsows 工作在32位保護模式下,保護模式與實模式的根本區別在於CPU定址方式上的不同,這也是Windows驅動程式設計中需要著重解決的問題。 Windows採用了分段、分頁機制,使得一個程式可以很容易地在物理記憶體容量不一樣的、配置範圍差別很大的計算機上運行,編程人員使用虛擬存儲器可以寫 出比任何實際配置的物理存儲器都大得多的程式。每個虛擬地址由16位的段選擇字和32位段偏移量組成。通過分段機制,系統由虛擬地址產生線性地址。再通過 分頁機制,由線性地址產生物理地址。線性地址被分割成頁目錄(Page Directory)、頁表(Page Table)和頁偏移(Offset)三個部分。當建立一個新的Win32進程時,作業系統會為它分配一塊記憶體,並建立它自己的頁目錄、頁表,頁目錄的地 址也同時放入進程的現場信息中。當計算一個地址時,系統首先從CPU控制器CR3中讀出頁目錄所在的地址,然後根據頁目錄得到頁表所在的地址,再根據頁表 得到實際代碼/數據頁的頁幀,最後再根據頁偏移訪問特定的單元。硬體設備讀寫的是物理記憶體,但應用程式讀寫的是虛擬地址,所以存在著將物理記憶體地址映射到 用戶程式線性地址的問題。
從物理記憶體到線性地址的轉換是驅動程式需要完成的工作,可以在初始化驅動程式的進行。在已經獲得設備的存 儲器基地址後,首先調用HalTranslateBusAddress()函式將匯流排相關的記憶體地址轉換成系統的物理地址,然後調用 MmMapIoSpace()函式將系統的物理地址映射到線性地址空間。在需要訪問設備記憶體時,調用READ_REGISTER_XXX()與 WRITE_REGISTER_XXX ()函式來進行,基地址使用前面映射後的線性地址。在設備卸載時,調用MmUnmapIoSpace()斷開設備記憶體與線性地址空間的映射。
示例代碼如下:
HalTranslateBusAddress(InterfaceType,
BusNumber,
BaseAddress->RangeStart,
&addressSpace,
&cardAddress)
BaseAddress->MappedRangeStart = MmMapIoSpace(cardAddress,
BaseAddress->RangeLength,
MmCached );
……
RegValue = READ_REGISTER_ULONG(pRegister);
WRITE_REGISTER_ULONG(pRegister, pInBuf->RegValue);
……
MmUnmapIoSpace(pBaseAddress->MappedRangeStart, pBaseAddress->RangeLength );
6. 中斷處理
中 斷的設定、回響與調用在驅動程式中完成。設定中斷應該在設備創建時完成,使用從CmResourceTypeInterrupt描述符中提取的參數,先調 用HalGetInterruptVector()將與匯流排有關的中斷向量參數轉換為系統的中斷向量,然後調用IoConnectInterrupt() 指定中斷服務,註冊中斷服務函式ISR(Interrupt Service Routine)的函式指針。
當硬體設備產生中斷時,系統 會自動調用ISR函式來回響中斷。ISR函式運行的中斷請求級較高,主要完成對硬體設備中斷的清除,不適合執行過多的代碼。在傳輸大塊數據時,需要使用延 遲過程調用(Delay Process Call,DPC)機制。例如,使用PCI設備進行DMA通信時,在ISR函式中完成對指定設備中斷的判斷以及清除中斷,在退出ISR前,調用DPC函 數;在DPC函式中,完成DMA通信的過程,並將數據返回給用戶程式。
示例代碼如下:
DeviceExtension->InterruptLevel = partialData->u.Interrupt.Level;
DeviceExtension->InterruptVector = partialData->u.Interrupt.Vector;
DeviceExtension->InterruptAffinity = partialData->u.Interrupt.Affinity;
if (partialData->Flags & CM_RESOURCE_INTERRUPT_LATCHED)
{
DeviceExtension->InterruptMode = Latched;
} else {
DeviceExtension->InterruptMode = LevelSensitive;
}
……
vector = HalGetInterruptVector(pDevExt->InterfaceType,
pDevExt->BusNumber,
pDevExt->InterruptLevel,
pDevExt->InterruptVector,
&irql,
&affinity );
status = IoConnectInterrupt(&pDevExt->InterruptObjec