PCI裝置驅動開發
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 )) {
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->InterruptLevel,
pDevExt->InterruptVector,
&irql,
&affinity );
status = IoConnectInterrupt(&pDevExt->InterruptObject,
(PKSERVICE_ROUTINE)PciDmaISR,
NULL,
vector,
irql,
pDevExt->InterruptMode,
TRUE,
affinity,
FALSE );
7. DMA通信過程
DMA通信在驅動程式中實作,需要多個例程才能完成一次DMA通信。
1) DriverEntry例程
構造DEVICE_DESCRIPTION結構,并調用HalGetAdapter,找到與裝置關聯的Adapter對象,并将傳回的Adapter對象的位址和映射寄存器的數目儲存在裝置擴充的資料結構中。
示例代碼:
// 申請DMA的擴充卡對象
deviceDescription.Version = DEVICE_DESCRIPTION_VERSION;
deviceDescription.Master = TRUE;
deviceDescription.ScatterGather = pDevExt->ScatterGather;
deviceDescription.DemandMode = FALSE;
deviceDescription.AutoInitialize = FALSE;
deviceDescription.Dma32BitAddresses = TRUE;
deviceDescription.BusNumber = pDevExt->BusNumber;
deviceDescription.InterfaceType = pDevExt->InterfaceType;
deviceDescription.MaximumLength = pDevExt->MaxTransferLength;
pDevExt->AdapterObject = HalGetAdapter(&deviceDescription,
&numberOfMapRegisters
);
2)Start I/O例程
該例程請求Adapter對象的擁有權,然後把其餘的工作留給AdapterControl回調例程。
a) 調用KeFlushIoBuffers從CPU的Cache把資料清到實體記憶體,然後計算映射寄存器的數目和使用者緩沖區的大小,及在第一次裝置操作中傳輸的位元組數。
b) 調用MmGetMdlVirtualAddress,從MDL中恢複使用者緩沖區的虛位址,并存入裝置擴充資料結構中。
c) 調用IoAllocateAdapterChannel請求Adapter對象的擁有權。如果調用成功,其餘的設定工作由AdapterControl例程去做;如果失敗了,則完成本次IRP包處理,開始處理下一個IRP。
3) AdapterControl例程
該例程完成初始化DMA控制器,并啟動裝置的工作。
a) 調用IoMapTransfer,裝入Adapter對象的映射寄存器。
b) 向裝置發送合适的指令開始傳輸操作。
c) 傳回值KeepObject保留Adapter對象的擁有權。
4)中斷服務(ISR)例程
在裝置中斷時,由系統調用。
a) 向硬體裝置發出中斷響應的指令。
b) 調用IoRequestDpc在驅動程式的DpcForIsr中繼續處理該請求。
c) 傳回TRUE,表示已經服務了本次中斷。
5)DpcForIsr例程
由ISR在每個部分資料傳輸操作的結束時觸發,完成目前IRP請求。
a) 調用IoFlushAdapterBuffers,清除Adapter對象的Cache中的任何剩餘資料。
b) 調用IoFreeMapRegisters,釋放所使用的映射寄存器。
c) 檢查有未傳完的剩餘資料,如果有,則計算下次裝置操作中需要傳輸的位元組數,調用IoMapTransfer重設映射寄存器,并啟動裝置;如果沒有剩餘資料,則完成目前IRP請求,并開始下一個請求。