天天看點

PCI裝置Windows NT平台驅動程式設計

引言 在設計和使用PCI裝置時,經常要在PC機的軟體中通路和控制硬體裝置,但Windows作業系統 (Windows NT、Windows2000、WindowsXP)為了保證系統的安全性、穩定性和可移植性,對應用程式通路硬體資源加以限制,這就要求設計裝置驅動程 序以實作PC機的軟體對PCI裝置的通路。 核心(Kernel)模式的驅動程式可以應用于WINDOWS NT和WINDOWS 2000的作業系統中。它差別于WDM(Win32 Driver Model)模型,主要不支援即插即用,但對于程式設計的思想二者基本上一緻,對于本文所述的DMA程式設計的方法在WDM模式的驅動程式中一樣适合。 本 文通過現有最常見的AMCC公司生産的AMCC S5933 PCI 控制晶片為例說明在WINDOWS NT平台下如何編寫裝置驅動程式以實作DMA傳輸方式。本文對基本的驅動程式設計技術不作詳細的說明,重點介紹PCI裝置驅動程式開發的相關技術與實作方 法,以及使用者接口程式的設計與實作技術。 1 NT 平台驅動程式模式及開發工具 裝置驅動程式是指管理某個外圍裝置的一段代碼,驅動程式不會獨立地存在,而是作業系統的一部分。通過裝置驅動程式,多個程序可以同時使用這些資源,進而可以實作多程序并行運作。在本文中,将調用裝置驅動程式的PC機程式稱為使用者程式。 Intel 80386以上的微處理器有4個優先級别:0級、1級、2級和3級。Windows NT使用了一個簡化的模型描述硬體特權級,然後這個模型映射到指定CPU上可用的特權檢查機制,即核心模式和使用者模式。核心模式對應于Intel系統的0 級,可以執行特權級指令,對任何I/O裝置有全部的通路權,還能夠通路任何虛位址和控制虛拟記憶體硬體。使用者模式對應于Intel系統的3級環,使用者程式在 該模式下運作,對硬體的通路操作受到系統的限制。 開發裝置驅動采用的工具包括微軟為驅動開發提供的裝置驅動開發包(Device Driver Kit,DDK)。DDK開發包提供了裝置開發的幫助文檔,編譯需要的頭檔案和庫檔案,調試工具和範例程式。基于NT DDK使用C語言編寫,是以我們使用VC++6.0作為編輯與編譯工具。核心調試工具選用了Numega公司的産品SoftICE。 2 PCI 裝置驅動開發的特點 2.1PCI 配置空間 每個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)對應。

2.2 裝置初始化 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                                     );    2.3 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); 2.4 裝置記憶體通路 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 ); 2.5 中斷處理 中 斷的設定、響應與調用在驅動程式中完成。設定中斷應該在裝置建立時完成,使用從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->InterruptObject,                       (PKSERVICE_ROUTINE)PciDmaISR,                       DeviceObject,                        NULL,                       vector,                       irql,                       irql,                       pDevExt->InterruptMode,                       TRUE,                       affinity,                       FALSE                        ); 2.6DMA 通信過程 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請求,并開始下一個請求。

原文出處: http://tb.donews.net/TrackBack.aspx?PostId=544174

繼續閱讀