PCI设备的WDM驱动程序设计
柳泉 罗耀华 柳华伟
摘要:本文详细地讨论了利用DDK开发PCI设备的WDM驱动程序的设计原理、方法及在设计中注意事项,实现了以芯片PCI9052开发的PCI卡的具有内存和I/O读写及中断处理的WDM驱动程序。
关键字:PCI,WDM,驱动程序,DDK
在Windows操作系统中,为了保证系统的安全性和可移植性,对应用程序对硬件的操作进行了限制,尤其Windows 2000和Windows XP,不支持直接对系统的硬件资源的操作。因而在设计开发PCI设备时,需要开发相应的驱动程序来实现对PCI设备的操作,用户应用程序通过驱动程序来访问PCI设备。
由于计算机硬件设备都存在不同的特点,因此各种设备的驱动程序也都有自己的特点,比如PCI设备、USB设备等等。尽管在整体框架中基本相同,但设备功能上不同,因此本文以PCI桥芯片PCI9052开发的PCI卡为硬件设备,来探讨PCI设备的驱动程序的开发。
1. 驱动程序类型和开发工具的选择
在WINDOWS操作系统下,支持PCI总线及其设备的驱动程序类型有支持Windows 98/95的VxD、支持Windows NT的NT式驱动程序和支持Windows 2000、Windows XP和Windows 98的WDM(Windows Driver Model)。前两种驱动程序类型由于其支持的操作系统的逐渐淘汰而淘汰。现在主流的操作系统是Windows 2000和Windows XP,因此开发PCI设备的驱动程序最好的
是WDM驱动程序。在一个系统中开发出WDM驱动程序,稍加修改即可在其他系统中编译运行。
WDM是在Windows NT驱动程序体系的基础上发展而来的,修改或增加了即插即用、电源管理等功能,使之适应硬件和用户的要求。
开发WDM驱动程序的主要工具是微软为各操作系统提供的开发软件包 Device Driver Kits(DDK) ,该软件包为驱动程序开发者提供了用于驱动程序开发的资源文件、编译连接程序、开发技术文档等。还有第三方提供的开发工具:NuMega公司的DriverStudio和Jungo公司的WinDriver,这些工具是在DDK的基础上为方便开发用户而进行开发的工具。在使用中,虽然利用DDK开发驱动程序难度较大,但是代码非常简洁,结构清晰,效率也高。利用第三方开发工具使用简单,开发速度较快,但对于驱动程序的理解和深入开发不如DDK。因此选择DDK开发PCI设备驱动程序,虽然开始会觉得非常复杂,但从执行效率和功能上会更有利。
2. PCI设备驱动程序的特点
在开发驱动程序之前对PCI总线和硬件设备进行了解是十分必要的,而且还要详细地掌握PCI设备的特性以及PCI设备驱动程序在设备程序栈的关系等,以便进行WDM驱动程序的设计。
PCI总线是一种高性能、与CPU无关的32/64位地址数据复用的总线,它支持突发传输、即插即用、电源管理等功能,不但能满足现在的应用需要,而且能够适应未来的需求。PCI总线支持硬件资源动态自动配置,以支持即插即用。在PCI设备插入PCI插槽或上电后,PCI总线配置机构自动根据PCI设备的要求实现配置。PCI总线支持内存读写、I/O端口读写、中断机制和DMA功能。由于这些硬件特点使PCI设备的WDM驱动程序的设计变得很复杂。在开发WDM驱动程序之前,还有必须掌握PCI设备的需要分配的资源等配置信息以及PCI设备的功能和操作方法。
在WDM中,采用了分层的驱动程序体系结构,总线驱动程序或类驱动程序在最底层直接与设备打交道,设备功能驱动程序在上层通过与低层驱动程序打交道,实现设备的功能,中间还可以有类过滤驱动程序或设备过滤驱动程序用于数据的过滤或转换。在PCI总线的驱动程序层中,其层次图如图4:
PCI总线驱动程序
图1 通用PCI总线的WDM驱动程序栈
在实际开发中,一般无需分很多层次,只需要开发一个设备驱动程序即可。设备驱动程序直接与PCI总线驱动程序打交道,进行硬件操作,以实现PCI设备的功能。
3. WDM驱动程序的设计
在PCI设备的WDM驱动程序中,一般是编写功能驱动程序。PCI总线驱动程序由操作系统实现,过滤驱动程序一般在特殊的情况下需要编写。因此本文只讨论PCI设备功能驱动程序的设计。在PCI设备功能驱动程序中,需要处理PCI设备的内存、端口的读写、中断处理和DMA数据传输,实现PCI设备的功能,因此,PCI设备功能驱动程序是很标准的WDM设备驱动程序。
PCI设备驱动程序在框架上与其他类型的设备驱动程序基本相同,包括初始化、创建设备、卸载和删除设备、即插即用处理、分发例程处理、电源管理、WMI等部分,限于篇幅,在此只讨论PCI设备的特别之处。
(1)PCI设备资源的获得
PCI设备的硬件资源是由PCI配置机构动态分配的,由PCI设备实现PCI配置寄存器,提出需要分配的硬件资源,由PCI配置机构分配资源。驱动程序需要取得这些资源,才能操作硬件。因此,PCI设备的硬件资源分配与管理是驱动程序中很重要的部分。硬件资源主要包括映射内存空间、I/O空间、中断。在WDM体系中,取得这些资源有四种方法:读写PCI配置寄存器、调用硬件抽象层(HAL)
数、向PCI总线驱动程序发送读写配置IRP和向PCI总线驱动程序传递开启设备IRP。第一种方法通过读写PCI总线配置I/O寄存器,来取得PCI设备的配置信息,其中包括资源的分配。这种方法需要将几乎所有的PCI设备枚举一遍,考虑到这种方法是对公共寄存器的读写,不利于系统的安全性,最好不使用这种方法,但是在调试PCI设备硬件时是个很好的方法。第二种方法通过调用函数HalGetBusData和HalGetBusDataByOffset来实现的,但是这种方法是为了能够与Windows NT的驱动程序兼容,而保留下来的方法,不推荐使用,其功能被第三种方法取代。在WDM体系中,总线驱动程序必须实现总线上设备的管理功能。PCI总线驱动程序实现了对PCI设备资源的枚举,设备驱动程序通过向PCI总线驱动程序传递设备配置IRP_MJ_PNP,经总线驱动程序的处理后,设备驱动程序得到PCI设备的资源信息。第四种方法是推荐的方法,当系统的PNP管理器在取得设备的资源后会自动向驱动程序发出IRP_MN_START_DEVICE的IRP,在该IRP栈中包含了设备的资源信息。好的驱动程序都应该使用这种方法,在此主要讨论该方法。
每个支持PNP功能的驱动程序,都应实现IRP_MN_START_DEVICE处理。在该IRP处理中应先交给低层驱动程序处理后,再根据IRP栈内内容进行资源分配。如下:
NTSTATUS PnpStartDevice(IN PDEVICE_OBJECT fdo, IN PIRP pIrp )
{
NTSTATUS status;
PIO_STACK_LOCATION stack;
pIrp->IoStatus.Status = STATUS_SUCCESS;
//先由低层驱动程序处理,并等待
KeInitializeEvent(&event,NotificationEvent,FALSE);
IoCopyCurrentIrpStackLocationToNext(pIrp);
IoSetCompletionRoutine(pIrp,(PIO_COMPLETION_ROUTINE) OnRequestComplete,
(PVOID) &event,TRUE,TRUE,TRUE);
status=IoCallDriver(((DEVICE_EXTENSION *)fdo->DeviceExtension) -> pLowerDeviceObject ,pIrp);
if (status == STATUS_PENDING){
KeWaitForSingleObject((PVOID)&event,Executive,KernelMode,FALSE,NULL);
}
if (!NT_SUCCESS(status)){
return CompleteRequest(pIrp, status);
}
stack = IoGetCurrentIrpStackLocation(pIrp);
ResourceRaw = stack->Parameters.StartDevice.AllocatedResources ->List[0].PartialResourceList->PartialDescriptors;
Resource = stack->Parameters.StartDevice.AllocatedResourcesTranslated ->List[0].PartialResourceList->PartialDescriptors;
for (i = 0; i < ResourceListRaw->Count; ++i, ++Resource, ++ResourceRaw){
switch (ResourceRaw->Type){
case CmResourceTypeInterrupt: //中断资源
IrqL = (KIRQL) Resource->u.Interrupt.Level;//中断IRQL
vector = Resource->u.Interrupt.Vector;//中断向量
affinity = Resource->u.Interrupt.Affinity;//中断分发的处理器集
//判断中断触发的类型
if (ResourceRaw->Flags == CM_RESOURCE_INTERRUPT_LATCHED)
mode = Latched; //低电平触发
else
mode = LevelSensitive; //下降沿出发
//是否共享,PCI中断都是共享的
irqshare = Resource->ShareDisposition == CmResourceShareShared;
//连接中断
status = IoConnectInterrupt(&pdx->pInterruptObject, (PKSERVICE_ROUTINE)OnInterrupt,(PVOID)pdx,NULL,vector,IrqL,IrqL,
mode, irqshare,affinity,FALSE);
case CmResourceTypePort: //端口资源
pdx->PhysicalIOBase = ResourceRaw->u.Port.Start;//开始物理地址
pdx->IOCount = ResourceRaw->u.Port.Length;//地址数量
pdx->IOBase = (ULONG *)MmMapIoSpace(pdx->PhysicalIOBase,
pdx->IOCount,MmNonCached);//映射端口
break;
case CmResourceTypeMemory: //内存资源
pdx->PhysicalMemBase = ResourceRaw->u.Memory.Start;//开始地址
pdx->MemCount = ResourceRaw->u.Memory.Length;//地址数量
pdx->MemBase = (ULONG *)MmMapIoSpace(pdx->PhysicalMemBase,
pdx->MemCount,MmNonCached);//映射内存
if (pdx->MemBase == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
//其他资源一般没有,可默认处理
default:
break;
}
}
return STATUS_SUCCESS;
}
在以上的代码中,限于篇幅,没有增加错误处理代码,在实际中应用一定需要进行在调用系统函数之后,进行相应的处理,如果不符合要求,立即退出,否则在其他例程中会发生错误,使系统崩溃。同时,在退出之前,一定要释放已分配的资源。
(2)内存读写
Windows工作在保护模式下,与实模式的区别在于CPU寻址方式不同,可以实现虚拟内存。在Windows系统中对内存又分为分页和非分页内存。分页内存一般用于应用程序,系统提供分页和分段使用户应用程序使用的内存可以在程序空闲的时候由系统将其从物理内存调配到硬盘中,以节省物理内存资源,当程序重新运行的时候,再由系统将其调配到物理内存,这样,系统可以得到比物理内存非常大的内存量,允许更多得应用程序保持运行。而非分页内存为系统常驻内存,不可以从物理内存调配到硬盘上,因此内存无需分页。在WDM驱动程序中,对于硬件的内存映射一般需要用非分页内存,因为在一些运行在DISPATCH_LEVEL或更高得中断级例程中,禁止使用分页内存,比如在中断处理程序中就不可以使用分页内存。再者,使用非分页内存无需太多的转换,非常安全,效率也高。如果使用分页内存,系统就有可能将其调配到硬盘上,容易产出错误。但是,不能过多地使用非分页内存。
在PCI设备的驱动程序中,获得的设备内存是一段映射物理内存,这是无法使用的,需要将其映射成系统可以访问的非分页内存。函数MmMapIoSpace完成该功能。该函数的原型为:
PVOID MmMapIoSpace(
IN PHYSICAL_ADDRESS PhysicalAddress,
IN ULONG NumberOfBytes,
IN MEMORY_CACHING_TYPE CacheEnable);
参数PhysicalAddress为物理地址;NumberOfBytes为地址的数量;CacheEnable为内存是否可以隐藏,取值可为MmNonCached,MmCached,MmWriteCombined,这里必须取为MmNonCached。其应用实例见以上代码中的“内存资源”处理部分。
当访问设备内存时,使用函数
UCHAR READ_REGISTER_UCHAR(IN PUCHAR Register);
ULONG READ_REGISTER_ULONG(IN PULONG Register);
USHORT READ_REGISTER_USHORT(IN PUSHORT Register);
VOID READ_REGISTER_BUFFER_UCHAR(IN PUCHAR Register,IN PUCHAR Buffer,IN ULONG Count);
VOID READ_REGISTER_BUFFER_UCHAR(IN PULONG Register,IN PULONG Buffer,IN ULONG Count);
VOID READ_REGISTER_BUFFER_UCHAR(IN PUSHORT Register,IN PUSHORT Buffer,IN ULONG Count);
VOID WRITE_REGISTER_UCHAR(IN PUCHAR Register,IN UCHAR Value);
VOID WRITE_REGISTER_ULONG(IN PULONG Register,IN ULONG Value);
VOID WRITE_REGISTER_USHORT(IN PUSHORT Register,IN USHORT Value);
VOID WRITE_REGISTER_BUFFER_UCHAR(IN PUCHAR Register,IN PUCHAR Buffer,IN ULONG Count);
VOID WRITE_REGISTER_BUFFER_UCHAR(IN PULONG Register,IN PULONG Buffer,IN ULONG Count);
VOID WRITE_REGISTER_BUFFER_UCHAR(IN PUSHORT Register,IN PUSHORT Buffer,IN ULONG Count);
以上函数对应的分别是对PCI设备内存的读写函数,参数Register为映射后的内存地址,在使用时,应进行相应的数据类型转换。其他参数为数据参数。XXX_REGISTER_XXX读写单个地址的内容;XXX_REGISTER_BUFFER_XXX读写一段内存的内容,这在PCI设备支持突发读写(Burst Transmission)时应用。例如读写单个内存的地址:
WRITE_REGISTER_UCHAR((PUCHAR)pdx->MmBase,0x03C);
(3)I/O读写
在PC上,I/O空间是一个64K字节的寻址空间。I/O端口的寻址方式与内存是不一样的。但是在WDM驱动程序中,对其处理与内存是一样的,把其看作寄存器,映射为设备内存。其映射方法和访问函数的用法与内存资源一样,只不过函数XXX_REGISTER_XXX改为XXX_PORT_XXX。
(4)中断的处理
在PCI总线中,很多设备共享一个中断,这就需要在中断处理函数要格外小心,处理不当,就会导致系统崩溃。驱动程序首先要在IRP_MN_START_DEVICE中获得中断资源,然后需要连接到中断处理函数中,使其当有中断请求时,进入中断服务例程。连接中断的函数为IoConnectInterrupt,具体用法见上段程序中的“中断资源”部分。十分需要注意的是在连接中断之前,一定要确定PCI设备不会产生中断请求,最好在PCI设备上电后,中断为屏蔽状态。在连接中断后,调用开启中断请求的函数需要同步处理,以防在函数的执行中,出现运行时间上的错误,而且在开启中断时,一定要在所有的硬件资源分配以后,否则如果有中断产生,系统就会立即调用中断处理例程,如果例程中使用了还没有分配的资源,就会出现意想不到的结果。同步处理使用函数:
BOOLEAN KeSynchronizeExecution(IN PKINTERRUPT Interrupt,IN PKSYNCHRONIZE_ROUTINE SynchronizeRoutine,IN PVOID SynchronizeContext);
参数Interrupt为IoConnectInterrupt返回的变量,SynchronizeRoutine为函数名称,SynchronizeContext为函数的输入参数。调用方式如下:
KeSynchronizeExecution(pdx->pInterruptObject,(PKSYNCHRONIZE_ROUTINE)EnablePciInterrupt,pdx);
在中断服务例程中,首先必须根据硬件信息来判断该中断是否是自己的设备发出的。这是因为PCI总线共享中断,系统在接收到中断后,顺序调用各个注册该中断资源的驱动程序的中断处理例程,如果有返回TRUE的例程,就代表该中断已处理,就不再调用其他例程,如果是返回FALSE的例程,则说明该中断没有处理,则继续调用其他的例程。如果返回错误,就会扰乱系统,造成系统崩溃。其框图如图3。
否
图2 中断服务例程框图
在中断服务例程中,相应的处理最好简洁快速,因为中断例程运行的级别很高,当有中断请求时,不但会打断应用程序的执行,而且会打断在硬件中断级以下的所有运行程序。在WDM中,提供了DPC(Deferred Procedure Call)例程,将在中断例程中耗时的但不需要立即处理的任务延时处理。比如,驱动程序接受应用程序的写PCI设备的数据,当写完后,硬件产生中断标志执行完毕,这时需要结束该IRP,就可以将结束IRP这个耗时的任务交给DPC完成。典型的用法示例如图4:
驱动程序
图3 中断处理过程示例
在该实例中,由应用程序调用函数WriteFile,将数据传递给驱动程序,驱动程序的DispatchWrite例程负责处理该IRP,在该例程中,由于需要中断的配合(假定),无法立即执行完毕,必须将IRP串行化,StartIo例程如果没有其他任务,就开始处理该IRP,处理完毕后立即返回,但不能结束IRP,当PCI设备完成操作后,就会产生中断,在中断服务例程中把IRP交给DPC,在DPC中处理完后结束该IRP。
4. 驱动程序的安装
PCI设备驱动程序安装与其他类型的驱动程序一样,通过INF文件进行安装。不同的是设备标识符,其完整形式如下:
PCI\VEN_vvvv&DEV_dddd&SUBSYS_ssssssss&REV_rr
vvvv是厂商标识符,由PCI组织赋予每个厂商,dddd是设备标识符,由厂商赋予每个设备,ssssssss是设备
的子系统id(一般为0),rr是修订号。一般情况只用到厂商标识符和设备标识符即可。
5. 驱动程序的调试和调用
由于PCI设备的硬件资源是动态配置的,只有当PCI设备上电后,驱动程序才能获得资源信息,再加上驱动程序主要是对硬件的操作,调试驱动程序应很小心,最好采取循序渐进的方法,一步一步实现功能,否则,经常死机,错误不容易查找。
调试工具可以选择DDK自带的WinDbg或NuMega公司的SoftIce。难度主要集中在IRP串行处理上。
WDM驱动程序的设备访问可以符号链接,也可以使用设备接口标识。两者的应用对于PCI设备驱动程序一样。
参考文献:
1. Windows WDM设备驱动程序开发指南 (美)Chris Cant 机械工业出版社 2000
2. Programming the Windows Model Driver (美)Walter Oney Microsoft Press 1999
3. Windows 2000 DDK Documents, Microsoft, 1999
注明: 该驱动程序实例以芯片PCI9052开发的PCI设备开发卡为硬件设备的,没有该卡,驱动程序PCI9052无法运行,但可以用DDK编译连接。可以用VC浏览工程。Sys目录下为WDM驱动程序代码,Exe目录下为调用PCI9052的演示程序。