PCI有三個相互獨立的物理地址空間:設備存儲器地址空間、I/O地址空間和配置空間。配置空間是PCI所特有的一個物理空間。由於PCI支持設備即插即用,所以PCI設備不占用固定的內存地址空間或I/O地址空間,而是可以由操作系統決定映射的基址。
系統加電時,BIOS檢測PCI總線,確定所有連接在PCI總線上的設備以及它們配置要求,並進行系統配置。所以,所有PCI設備必須實現配置空間,從而能實現參數自動配置,實現真正的即插即用。
前面簡單介紹了一下PCI設備的特性。現在來介紹一種方位PCI設備配置空間的常用方式:通過即插即用IRP獲得PCI配置空間。
在WDM驅動中,總線驅動會為每個設備提供一個PDO設備,當開發者縮寫的功能驅動掛載在PDO之上的時候。就可以將IRP_MN_START_DEVICE傳遞給底層的PDO去處理。PCI總線的PDO就會得到PCI配置空間,並從中得到有用信息,如中斷號、設備物理內存及IO端口信息等。
在處理完IRP_MN_START_DEVICE后,驅動程序會將處理結果存儲在IRP的設備堆棧中,從I/O堆棧可以取出CM_FULL_RESOURCE_DESCRIPTOR數據結構,從CM_FULL_RESOURCE_DESCRIPTOR中取出CM_PARTIAL_RESOURCE_LIST數據結構,而在CM_PARTIAL_RESOURCE_LIST中又可以取出CM_PARTIAL_RESOURCE_DESCRIPTOR數據結構。
CM_PARTIAL_RESOURCE_DESCRIPTOR數據結構就是PDO幫主程序員從256字節的PCI配置空間中獲取的有用信息。
下面來具體討論一下這個過程。首先是當開發者將自己寫的FDO掛載到PDO上去之后,就可以將Pnp的IRP_MN_START_DEVICE傳遞給底層的PDO去處理。
在Driver_Entry()函數中將我們自己的Pnp處理函數指針填充到對應的MajorFunction中去:
pDriverObject->MajorFunction[IRP_MJ_PNP] = xxxPnp;
然后是定義該函數:
NTSTATUS xxxPnp(IN PDEVICE_OBJECT fdo, IN PIRP Irp)//注意函數的參數
{
PAGED_CODE();
// KdPrint(("Enter HBAPnp\n"));
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
static NTSTATUS (*fcntab[])(PDEVICE_EXTENSION pdx, PIRP Irp) =
{
HandleStartDevice, // IRP_MN_START_DEVICE
// DefaultPnpHandler,
DefaultPnpHandler, // IRP_MN_QUERY_REMOVE_DEVICE
HandleRemoveDevice, // IRP_MN_REMOVE_DEVICE
DefaultPnpHandler, // IRP_MN_CANCEL_REMOVE_DEVICE
DefaultPnpHandler, // IRP_MN_STOP_DEVICE
DefaultPnpHandler, // IRP_MN_QUERY_STOP_DEVICE
DefaultPnpHandler, // IRP_MN_CANCEL_STOP_DEVICE
DefaultPnpHandler, // IRP_MN_QUERY_DEVICE_RELATIONS
DefaultPnpHandler, // IRP_MN_QUERY_INTERFACE
DefaultPnpHandler, // IRP_MN_QUERY_CAPABILITIES
DefaultPnpHandler, // IRP_MN_QUERY_RESOURCES
DefaultPnpHandler, // IRP_MN_QUERY_RESOURCE_REQUIREMENTS
DefaultPnpHandler, // IRP_MN_QUERY_DEVICE_TEXT
DefaultPnpHandler, // IRP_MN_FILTER_RESOURCE_REQUIREMENTS
DefaultPnpHandler, //
DefaultPnpHandler, // IRP_MN_READ_CONFIG
DefaultPnpHandler, // IRP_MN_WRITE_CONFIG
DefaultPnpHandler, // IRP_MN_EJECT
DefaultPnpHandler, // IRP_MN_SET_LOCK
DefaultPnpHandler, // IRP_MN_QUERY_ID
DefaultPnpHandler, // IRP_MN_QUERY_PNP_DEVICE_STATE
DefaultPnpHandler, // IRP_MN_QUERY_BUS_INFORMATION
DefaultPnpHandler, // IRP_MN_DEVICE_USAGE_NOTIFICATION
DefaultPnpHandler, // IRP_MN_SURPRISE_REMOVAL
};
ULONG fcn = stack->MinorFunction;
if (fcn >= arraysize(fcntab))
{ // 未知的子功能代碼
status = DefaultPnpHandler(pdx, Irp); // some function we don't know about
return status;
}
status = (*fcntab[fcn])(pdx, Irp);
KdPrint(("Leave HBAPnp\n"));
return status;
}
在該函數中,首先通過函數IoGetCurrentIrpStackLocation()獲得當前堆棧的指針,接着定義了一個函數指針數組static NTSTATUS (*fcntab[])(PDEVICE_EXTENSION pdx, PIRP Irp) 用於處理不同的Pnp IRP。這里主要針對IRP_MN_START_DEVICE進行說明,其他的說明可以參見上面代碼中的簡單注釋。從上述代碼可以看出,如果傳到該層驅動的IRP包類型為IRP_MN_START_DEVICE則會調用HandleStartDevice()進行處理。通過HandleStartDevice()函數我們將獲得PCI的配置空間信息。接着看一下HandleStartDevice()函數該如何定義:
NTSTATUS HandleStartDevice(PDEVICE_EXTENSION pdx, PIRP Irp)
{
PAGED_CODE();
// KdPrint(("Enter HandleStartDevice\n"));
//轉發IRP並等待返回
NTSTATUS status = ForwardAndWait(pdx,Irp);
if (!NT_SUCCESS(status))
{
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
//得到當前堆棧
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
//從當前堆棧得到翻譯信息
PCM_PARTIAL_RESOURCE_LIST translated;
if (stack->Parameters.StartDevice.AllocatedResourcesTranslated)
translate= &stack->Parameters.StartDevice.AllocatedResourcesTranslated->List[0].PartialResourceList;
else
translated = NULL;
// KdPrint(("Init the PCI card!\n"));
status=InitDevice(pdx,translated);
if(!NT_SUCCESS(status))
{
KdPrint(("Initialize device failed!%\n"));
IoSetDeviceInterfaceState(&pdx->interfaceName, FALSE);
RtlFreeUnicodeString(&pdx->interfaceName);
//調用IoDetachDevice()把fdo從設備棧中脫開:
if (pdx->NextStackDevice)
IoDetachDevice(pdx->NextStackDevice);
//刪除fdo:
IoDeleteDevice(pdx->fdo);
RtlFreeUnicodeString(&pdx->devName);
}
//完成IRP
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// KdPrint(("Leave HandleStartDevice\n"));
return status;
}
一進入該函數,首先調用了另一個函數:ForwardAndWait(pdx, Irp);
注意這個函數的參數中包含Irp,結合前面講解,此時我們是要將即插即用的IRP_MN_START_DEVICE類型的IRP包發送給PDO來處理。這里就涉及到一個處理的同步還是異步問題。
為了得到底層PDO處理IRP的結果,需要調用PDO后,能夠查詢IRP的結果。這就面臨兩個問題。
- 不知道PDO是基於同步完成還是異步完成。如果同步完成,即IoCallDrive()
函數返回就標志着PDO處理IRP完成。如果異步完成,IoCallDriver()的返回並不能代表PDO處理IRP完成。
- IRP一旦處理完成(即IoCompleteRequest(Irp, IO_NO_INCREMENT)),就不能再對IRP進行操作。但我們需要獲得底層設備PDO對IRP的設置情況。
解決上述兩個問題,需要采用完成例程,並歸結為以下幾個步驟:
① 插即用IRP進入WDM派遣函數
② 派遣函數初始化一個事件,這個事件作為與PDO同步之用
③ 設置完成例程,並將事件作為參數傳遞給完成例程
④ 調用底層驅動,即PDO,並緊接着等待這個同步事件
⑤ 底層驅動完成IRP時,觸發完成例程
⑥ 在完成例程中,將事件設為有效。
⑦ 事件有效后,等待停止。
ForwardAndWait(pdx, Irp)函數就是將上面的幾個步驟封裝而得。由於這里設置了完成例程(通過IoSetCompletionRoutine()),所以需要使用IoCopyCurrentIrpStackLocaction()函數。具體原因見之前文章的討論。
ForWardAndWait()函數具體定義為如下:
NTSTATUS ForwardAndWait(PDEVICE_EXTENSION pdx, PIRP Irp)
{ // ForwardAndWait
PAGED_CODE();
// KdPrint(("Entry ForwardAndWait!\n"));
KEVENT event;
//初始化事件
KeInitializeEvent(&event, NotificationEvent, FALSE);
//將本層堆棧拷貝到下一層堆棧
IoCopyCurrentIrpStackLocationToNext(Irp);
//設置完成例程
IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) OnRequestComplete,
(PVOID) &event, TRUE, TRUE, TRUE);
//調用底層驅動,即PDO
IoCallDriver(pdx->NextStackDevice, Irp);
//等待PDO完成
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
// KdPrint(("Leave ForwardAndWait!\n"));
return Irp->IoStatus.Status;
}
正如前面所說的:在處理完IRP_MN_START_DEVICE后,驅動程序會將處理結果存儲在IRP的設備堆棧中,從I/O堆棧可以取出CM_FULL_RESOURCE_DESCRIPTOR數據結構,從CM_FULL_RESOURCE_DESCRIPTOR中取出CM_PARTIAL_RESOURCE_LIST數據結構,而在CM_PARTIAL_RESOURCE_LIST中又可以取出CM_PARTIAL_RESOURCE_DESCRIPTOR數據結構。從HandleStartDevice()函數中的:
translate= &stack->Parameters.StartDevice.AllocatedResourcesTranslated->List[0].PartialResourceList;
可以看出,通過translate變量獲得了配置空間的基本信息,這是一個CM_PARTIAL_RESOURCE_LIST類型,CM_PARTIAL_RESOURCE_LIST定義為:
typedef struct _CM_PARTIAL_RESOURCE_LIST {
USHORT Version;
USHORT Revision;
ULONG Count;
CM_PARTIAL_RESOURCE_DESCRIPTOR PartialDescriptors[1];
} CM_PARTIAL_RESOURCE_LIST, *PCM_PARTIAL_RESOURCE_LIST;
但是其實具體的信息存儲來PartialDescriptors中,該結構在內核中的定義為:
typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR {
UCHAR Type;
UCHAR ShareDisposition;
USHORT Flags;
union {
struct {
PHYSICAL_ADDRESS Start;
ULONG Length;
} Generic;
struct {
PHYSICAL_ADDRESS Start;
ULONG Length;
} Port;
struct {
#if defined(NT_PROCESSOR_GROUPS)
USHORT Level;
USHORT Group;
#else
ULONG Level;
#endif
ULONG Vector;
KAFFINITY Affinity;
} Interrupt;
// This member exists only on Windows Vista and later
struct {
union {
struct {
#if defined(NT_PROCESSOR_GROUPS)
USHORT Group;
#else
USHORT Reserved;
#endif
USHORT MessageCount;
ULONG Vector;
KAFFINITY Affinity;
} Raw;
struct {
#if defined(NT_PROCESSOR_GROUPS)
USHORT Level;
USHORT Group;
#else
ULONG Level;
#endif
ULONG Vector;
KAFFINITY Affinity;
} Translated;
};
} MessageInterrupt;
struct {
PHYSICAL_ADDRESS Start;
ULONG Length;
} Memory;
struct {
ULONG Channel;
ULONG Port;
ULONG Reserved1;
} Dma;
struct {
ULONG Channel;
ULONG RequestLine;
UCHAR TransferWidth;
UCHAR Reserved1;
UCHAR Reserved2;
UCHAR Reserved3;
} DmaV3;
struct {
ULONG Data[3];
} DevicePrivate;
struct {
ULONG Start;
ULONG Length;
ULONG Reserved;
} BusNumber;
struct {
ULONG DataSize;
ULONG Reserved1;
ULONG Reserved2;
} DeviceSpecificData;
// The following structures provide support for memory-mapped
// IO resources greater than MAXULONG
struct {
PHYSICAL_ADDRESS Start;
ULONG Length40;
} Memory40;
struct {
PHYSICAL_ADDRESS Start;
ULONG Length48;
} Memory48;
struct {
PHYSICAL_ADDRESS Start;
ULONG Length64;
} Memory64;
struct {
UCHAR Class;
UCHAR Type;
UCHAR Reserved1;
UCHAR Reserved2;
ULONG IdLowPart;
ULONG IdHighPart;
} Connection;
} u;
} CM_PARTIAL_RESOURCE_DESCRIPTOR, *PCM_PARTIAL_RESOURCE_DESCRIPTOR;
CM_PARTIAL_RESOURCE_DESCRIPTOR 的第一個參數 Type的實際意義:
Type value |
u member substructure |
CmResourceTypePort |
u.Port |
CmResourceTypeInterrupt |
u.Interrupt or u.MessageInterrupt. If the CM_RESOURCE_INTERRUPT_MESSAGE flag of Flags is set, use u.MessageInterrupt; otherwise, use u.Interrupt. |
CmResourceTypeMemory |
u.Memory |
CmResourceTypeMemoryLarge |
One of u.Memory40, u.Memory48, or u.Memory64. The CM_RESOURCE_MEMORY_LARGE_XXX flags set in the Flags member determines which structure is used. |
CmResourceTypeDma |
u.Dma (if CM_RESOURCE_DMA_V3 is not set) or u.DmaV3 (if CM_RESOURCE_DMA_V3 flag is set) |
CmResourceTypeDevicePrivate |
u.DevicePrivate |
CmResourceTypeBusNumber |
u.BusNumber |
CmResourceTypeDeviceSpecific |
u.DeviceSpecificData (Not used within IO_RESOURCE_DESCRIPTOR.) |
CmResourceTypePcCardConfig |
u.DevicePrivate |
CmResourceTypeMfCardConfig |
u.DevicePrivate |
CmResourceTypeConnection |
u.Connection |
CmResourceTypeConfigData |
Reserved for system use. |
CmResourceTypeNonArbitrated |
Not used. |
當獲取了PCI設備空間之后,開發者就可以對PCI設備進行具體的操作。這里通過InitDevice()函數來實現,InitDevice()函數定義如下:
NTSTATUS InitDevice(IN PDEVICE_EXTENSION pdx, IN PCM_PARTIAL_RESOURCE_LIST list)
{
PAGED_CODE();
KdPrint(("Enter InitDevice!\n"));
PDEVICE_OBJECT fdo = pdx->fdo;
… //各種初始化
PCM_PARTIAL_RESOURCE_DESCRIPTOR resource = &list->PartialDescriptors[0];
ULONG nres = list->Count;
//獲取PCI資源
for (ULONG i = 0; i < nres; ++i, ++resource)
{
switch (resource->Type)
{
//設備物理內存資源
case CmResourceTypeMemory:
pdx->MemBar0 = (PUCHAR)MmMapIoSpace(resource->u.Memory.Start,
resource->u.Memory.Length,
MmNonCached); //將獲得的物理地址映射為系統空間賦給內存基地址0
pdx->nMem0 = resource->u.Memory.Length; //基地址BAR0占用字節數
pdx->RegsPhyBase=resource->u.Memory.Start; //寄存器物理地址首地址
//KdPrint(("pdx->RegsPhyBase = 0x%x\n",pdx->RegsPhyBase));
pdx->RegsBase = pdx->MemBar0; //寄存器虛擬地址首地址
pdx->pHBARegs=(PHBA_REGS)pdx->RegsBase; //寄存器地址==寄存器虛擬首址
continue;
//中斷資源
case CmResourceTypeInterrupt:
irql = (KIRQL) resource->u.Interrupt.Level; //中斷級別
vector = resource->u.Interrupt.Vector; //
affinity = resource->u.Interrupt.Affinity;
mode = (resource->Flags == CM_RESOURCE_INTERRUPT_LATCHED)
? Latched : LevelSensitive;
irqshare = resource->ShareDisposition == CmResourceShareShared;
gotinterrupt = TRUE;
KdPrint(("i=%u,irqvector= %u\n",i,vector));
continue;
default:
continue;
} //switch on resource type
} //for each resource
if (!(gotinterrupt))
{
KdPrint((" Didn't get expected I/O interrupt resources\n"));
return STATUS_UNSUCCESSFUL;
}
//注冊中斷
status = IoConnectInterrupt(&pdx->InterruptObject, (PKSERVICE_ROUTINE) ISRInterrupt,
(PVOID) pdx, NULL, vector, irql, irql, LevelSensitive, irqshare, affinity, FALSE);
if (!NT_SUCCESS(status))
{
KdPrint(("IoConnectInterrupt failed - %X\n", status));
if (pdx->MemBar0)
MmUnmapIoSpace(pdx->MemBar0,pdx->nMem0);
return status;
}
//初始化DPC例程
KeInitializeDpc(&pdx->fdo->Dpc,DPCForISR,NULL);
KeInitializeSpinLock(&pdx->spinLock);
PDEVICE_DESCRIPTION DeviceDescription=(PDEVICE_DESCRIPTION)ExAllocatePool(PagedPool, sizeof(DEVICE_DESCRIPTION));
//這里需要設置DeviceDescription,代碼略
ULONG NumberOfMapRegisters=100;
//創建一個DMA適配器
pdx->DmaAdapter=IoGetDmaAdapter(pdx->NextStackDevice,DeviceDescription,&NumberOfMapRegisters);
if(!pdx->DmaAdapter)
{
KdPrint(("Create DmaAdapter failed!\n"));
ExFreePool(DeviceDescription);
if (pdx->MemBar0)
MmUnmapIoSpace(pdx->MemBar0,pdx->nMem0);
IoDisconnectInterrupt(pdx->InterruptObject);
status=STATUS_UNSUCCESSFUL;
return status;
}
// KdPrint(("DMANumberOfMapRegisters=%u,DMAChannel=%u,DMAPort=%u\n",NumberOfMapRegisters,DeviceDescription->DmaChannel,DeviceDescription->DmaPort));
pdx->allocateCommonBuffer=*pdx->DmaAdapter->DmaOperations->AllocateCommonBuffer; //分配連續的物理內存DMA函數
pdx->freeCommonBuffer = *pdx->DmaAdapter->DmaOperations->FreeCommonBuffer;
//釋放連續的物理內存DMA函數
pdx->putDmaAdapter=*pdx->DmaAdapter->DmaOperations->PutDmaAdapter;
//釋放DMA Adapter對象
pdx->descAddress=pdx->allocateCommonBuffer(pdx->DmaAdapter,(ULONG)DESC_ADDRESS*PORT_NUM,&pdx->DescLogicalAddress,FALSE); //分配寄存器范圍的基本虛擬地址
if(!pdx->descAddress) KdPrint(("descAddress failed"));
for(int i=0;i<PORT_NUM;i++)
{
pdx->frameAddress[i]=pdx->allocateCommonBuffer(pdx->DmaAdapter,(ULONG)FRAME_ADDRESS,&pdx->FrameLogicalAddress[i],FALSE);//分配寄存器范圍的基本虛擬地址
if(!pdx->frameAddress[i]) {ret=0;KdPrint(("frameAddress[%d] failed\n",i));}
}
//代碼省略一部分,調用allocateCommonBuffer()繼續進行分配
ExFreePool(DeviceDescription);
//KdPrint(("Allocate first address is 0x%0x",memAddress));
//初始化DMA內存緩沖區
RtlZeroMemory(pdx->descAddress,DESC_ADDRESS*PORT_NUM);
//復位,代碼略
//獲得物理地址與虛擬地址的方法 , 都是用類似的方法來獲取
pdx->RxDescVirBase=(PCHAR)pdx->descAddress;
pdx->RxDescPhyBase=(ULONG)(pdx->DescLogicalAddress.LowPart);
pdx->InfBufferVirBase=(PCHAR)pdx->debugAddress;
pdx->InfBufferPhyBase=(ULONG)(pdx->DebugLogicalAddress.LowPart);
InitRecvAddr(pdx);
//注:寫寄存器都是用物理地址
WRITE_REGISTER_ULONG((PULONG) &pdx->pHBARegs->RxAddr_des_0,pdx->rx_fc_desc_buf_phy[0]+16);
WRITE_REGISTER_ULONG((PULONG) &pdx->pHBARegs->RxAddr_des_addr0_ptr,pdx->rx_fc_desc_buf_phy[0]+4);
…
//初始化寄存器
InitReg(pdx);
KdPrint(("Leave InitHBA!\n"));
return status;
}
注:這一部分主要參考了張帆大哥的“Windows 驅動開發技術詳解”,自己將一些分開的內容做了簡單的總結,並結合自己已經在工作的PCI驅動代碼進行說明。