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驱动代码进行说明。