13.4 PCI卡的驱动程序设计
13.4.1 WDM驱动程序模型
设计完成的信号采集设备在插入计算机后,在对其进行控制之前,需要编写基于操作系统平台上的驱动程序。设备驱动程序是一个包含了许多操作系统可调用例程的容器,这些例程可以使硬件设备执行相应的动作,它是硬件与上层软件之间沟通的桥梁。
在本案例中,我们针对最常使用的操作系统Windows 98/2000/XP系统,使用了WDM(Windows Driver Model)驱动程序模型进行程序开发。
WDM模型是从WinNT 3.51和WinNT 4的内核模式设备驱动程序发展而来的。WDM主要的变化是增加了对即插即用、电源管理、Windows Management Interface(WMI)、设备接口的支持。WDM模型的主要目标是实现能够跨平台使用、更安全、更灵活、编制更简单的Windows设备驱动程序。
WDM采用了“基于对象”的技术,建立了一个分层的驱动程序结构。通过WDM模型的引入,可以减轻设备驱动程序的开发难度和周期,逐渐规范设备驱动程序的开发,应该说,WDM是当前基于Windows平台的设备驱动程序的主流。
WDM模型主要采用分层的方法,模仿面向对象的技术,先进行逻辑上的“分层”,然后将标准的实现和低层细节“封装”起来,形成“基类”,客户程序通过“继承”的方式来扩展“基类”的功能,完成所需要的实现。
13.4.2 设备和驱动程序的层次结构
在WDM模型中,每个硬件设备至少有两个驱动程序:一个功能驱动程序(function driver)和一个总线驱动程序(bus driver)。
如图13.14所示为WDM中设备对象和驱动程序的层次结构。
图13.14 WDM中设备对象和驱动程序的层次结构
1.过滤驱动程序
过滤驱动程序是一个可选项,当一个用户需要改变或新添一些功能到一个设备、一类设备或一种总线时,就可以编写一个过滤驱动程序。在设备栈里,过滤驱动程序安装在一个或几个设备驱动程序的上面或下面。
过滤驱动程序拦截对具体设备、类设备、总线的请求,做相应的处理,以改变设备的行为或添加新的功能。但过滤驱动程序只处理那些它所关心的I/O请求,对于其他的请求可以交给其他的驱动程序来处理,这样可以非常灵活地改变设备的行为。
2.功能驱动程序
功能驱动程序是物理设备的主要驱动程序,它实现设备的具体功能,一般由设备的生产商来编写。功能驱动程序的主要功能是:提供对设备的操作接口、操作对设备的读写、管理设备的电源策略等。
功能驱动程序由类驱动程序和微型驱动程序组成。类驱动程序实现了某一类设备的常用操作,驱动程序的开发者可以只编写非常小的微型驱动程序,去处理具体设备特殊的操作,而对于其他大量的常规操作,可以调用该类的类驱动程序,这也是WDM驱动程序的优点之一。
微软公司提供的类驱动程序处理常用的系统任务,比如,即插即用功能和电源管理。类驱动程序保证了操作系统在处理类似的任务时的一致性,从而提高了系统的稳定性。
设备生产商提供微型驱动程序,以实现自己设备的特殊功能,同时调用合适的类驱动程序完成其他的通用工作。将大量的标准操作的代码通过各种类驱动程序来实现,并集成在操作系统中,这样的方式可以有效地减少具体设备的微型驱动程序的大小,也就减小了程序出错的可能。
如果某一类设备存在着工业标准,微软公司就会提供一个该类设备的WDM类驱动程序。这个类驱动程序实现了该类设备所有必须的任务,但不实现任何具体设备所特有的东西。
3.总线驱动程序
总线驱动程序为实际的I/O总线服务。在WDM的定义中,总线是用来连接其他的物理的、逻辑的、虚拟的设备。总线包括传统的总线SCSI和PCI,也包括并口、串口以及i8042端口。微软公司已经为Windows操作系统提供了总线驱动程序。总线驱动程序已经包含在操作系统里了,用户不必安装。
一个总线驱动程序负责以下的工作:枚举总线上的设备,向操作系统报告总线上的动态事件,响应即插即用和电源管理的I/O请求,提供总线的多路存取,管理总线上的设备等。
13.4.3 PCI设备驱动程序例程
PCI设备的WDM驱动程序一般需要使用Windows DDK(Drivers Develop Kits)及C语言进行开发。下面介绍一些PCI设备最常见的例程,这些例程将告诉我们如何对PCI设备进行控制。
1.DriverEntry例程
每个WDM驱动程序,不管它的用途是什么,都要对外界显示一个名字为DriverEntry的例程。该例程初始化各种驱动程序数据结构,并为所有其他驱动程序组件准备好执行环境。主要的工作是在传递的DriverObject中存储一系列的回调例程指针。DRIVER_OBJECT结构有操作系统用于存储与驱动程序有关的任何信息。
在DriverEntry例程中通常要完成如下步骤。
· DriverEntry 找到它将要控制的硬件。那个硬件是经过分配的,即被标志为由该驱动程序控制。
· 通过声明另一个驱动程序入口点,初始化驱动程序对象。通过把函数指针直接保存到驱动程序对象中完成声明工作。
· 如果成功,DriverEntry应该把STATUS_SUCCESS返回给I/O管理程序。
DriverEntry的函数原型为:
NTSTATUS DriverEntry (
PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegistryPath
)
它接收一个指向它本身的驱动程序对象的指针,DriverEntry例程必须对它(指针)初始化。它还接收一个UNICODE_STRING,它包含注册表中驱动程序服务键的路径。内核模式驱动程序根据该字符串从系统注册表中提取任何驱动程序专有的参数。注册表字符串采用如下形式:
HKEY_LOCAL_MACHINESystemCurrentControlSetServicesDriverName
下面是驱动程序的DriverEntry例程的部分代码,里面定义了将要用到的回调函数。
NTSTATUS DriverEntry (
PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegistryPath
)
{ …
// 回调函数
pDriverObject->DriverUnload = DriverUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] =Dispatch_Create;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] =Dispatch_Close;
pDriverObject->MajorFunction[IRP_MJ_READ] =Dispatch_Read;
pDriverObject->MajorFunction[IRP_MJ_WRITE] =Dispatch_Write;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] =Dispatch_Cleanup;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]
=Dispatch_IoControl;
pDriverObject->MajorFunction[IRP_MJ_PNP] = Dispatch_Pnp;
pDriverObject->MajorFunction[IRP_MJ_POWER] =Dispatch_Power;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL]
=Dispatch_System Control;
pDriverObject->DriverExtension->AddDevice = AddDevice;
…
return STATUS_SUCCESS;
}
2.AddDevice例程
大多数的WDM PDO 都是在PnP管理器调用该程序入口点时被创建的。插入新设备后,系统启动时,总线枚举器会发现总线上的所有设备会自动寻找并安装设备的驱动程序,并由驱动程序中的处理PnP功能模块自动处理AddDevice例程及其他的PnP消息。
AddDevice例程使用IoCreateDevice函数创建设备对象,再使用IoCreateSymbolicLink函数将设备组成为一个特定的设备接口,供Win32使用。
其函数原型为:
NTSTATUS AddDevice (PDRIVER_OBJECT pDriverObject, PDEVICE_OBJECT pdo)
必须在DriverEntry入口函数中进行声明,下面是该函数的部分代码:
NTSTATUS AddDevice (
PDRIVER_OBJECT pDriverObject,
PDEVICE_OBJECT pdo
)
{…
// 建立设备名称并创建它
for (i=0; i < 20; i++)
{
//转成String格式
Swprintf (DeviceName, L"\Device\" PLX_DRIVER_NAME_UNICODE L"-%d", i);
//初始化DeviceName_Unicode
RtlInitUnicodeString (&DeviceName_Unicode, DeviceName);
// 创建设备
status = IoCreateDevice(
pDriverObject,
sizeof(DEVICE_EXTENSION),
&DeviceName_Unicode,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fdo
);
…
//为用户应用程序创建Win32关联名
//转成String格式
swprintf (DeviceLinkName, L"\DosDevices\" PLX_DRIVER_NAME_UNICODE L"-%d", i);
//初始化DeviceLinkName_Unicode
RtlInitUnicodeString (
&DeviceLinkName_Unicode,
DeviceLinkName
);
//建立设备关联符号
status = IoCreateSymbolicLink (
&DeviceLinkName_Unicode,
&DeviceName_Unicode
);
…
return STATUS_SUCCESS;
}
3.DispatchPnp例程
支持即插即用主要是指实现一个AddDevice例程和一个IRP_MJ_PNP处理程序。这个PnP IRP有8个主要次功能代码,大多数的WDM驱动程序需要支持这些次功能代码。
· IRP_MN_START_DEVICE。
· IRP_MN_QUERY_REMOVE_DEVICE。
· IRP_MN_REMOVE_DEVICE。
· IRP_MN_CANCLE_REMOVE_DEVICE。
· IRP_MN_STOP_DEVICE。
· IRP_MN_QUERY_STOP_DEVICE。
· IRP_MN_CANCLE_STOP_DEVICE。
· IRP_MN_QUERY_CAPABILITIES。
还有一些不太常用的IRP,这里就不再一一介绍,下面是这部分驱动的部分代码。
NTSTATUS Dispatch_Pnp(
PDEVICE_OBJECT fdo,
PIRP pIrp
)
{ …
// 检查次功能代码
switch (stack -> MinorFunction)
{
case IRP_MN_START_DEVICE: //配置并初始化设备
status = HandleStartDevice(fdo, pIrp);
break;
case IRP_MN_STOP_DEVICE: //关闭设备
status = HandleStopDevice(fdo, pIrp);
break;
case IRP_MN_REMOVE_DEVICE: //关闭并删除设备
Unlock = FALSE;
status = HandleRemoveDevice(fdo, pIrp);
break;
case IRP_MN_QUERY_REMOVE_DEVICE: //查询设备是否可被安全删除
status = DefaultPnpHandler(fdo, pIrp);
break;
case IRP_MN_CANCEL_REMOVE_DEVICE: //忽略以前的QUERY_REMOVE
status = DefaultPnpHandler(fdo, pIrp);
break;
case IRP_MN_QUERY_STOP_DEVICE: //查询设备是否可被安全关闭
status = DefaultPnpHandler(fdo, pIrp);
break;
case IRP_MN_CANCEL_STOP_DEVICE: //忽略以前的QUERY_STOP
status = DefaultPnpHandler(fdo, pIrp);
break;
case IRP_MN_QUERY_CAPABILITIES: //取设备能力
status = DefaultPnpHandler(fdo, pIrp);
break;
… }
return status;
}
这些功能代码函数都在DriverEntry()入口函数中进行了声明。
4.DispatchPower例程
WDM设备驱动程序支持电源管理,一个设备可以改变它的电源使用来响应系统电源状态变化,且在处于空闲状态时可以减少它自己的电源使用。一个休眠的设备可以唤醒系统,如当调制解调器收到到达的呼叫时。
驱动程序的电源管理例程围绕电源IRP_MJ_POWER IRP 进行处理,这些例程处理这个IRP,并在需要时产生这个IRP。这个IRP有4个电源管理次功能代码。
· IRP_MN_WAIT_WAKE。
· IRP_MN_POWER_SEQUENCE。
· IRP_MN_SET_POWER。
· IRP_MN_QUERY_POWER。
这部分的代码如下:
NTSTATUS Dispatch_Power(
PDEVICE_OBJECT fdo,
PIRP pIrp
)
{
NTSTATUS status;
PIO_STACK_LOCATION stack;
//获取指向被调用的Irp的栈位置的指针
stack = IoGetCurrentIrpStackLocation(pIrp);
switch (stack->MinorFunction)
{
case IRP_MN_WAIT_WAKE: //唤醒计算机,响应一个外部事件
status = DefaultPowerHandler(fdo, pIrp);
break;
case IRP_MN_POWER_SEQUENCE: //确定设备是否真正进入特定的电源状态
status = DefaultPowerHandler(fdo, pIrp);
break;
case IRP_MN_SET_POWER: //设置系统或设备电源状态
status = HandleSetPower(fdo, pIrp);
break;
case IRP_MN_QUERY_POWER: //查询系统或设备状态变化是否可行
status = HandleQueryPower(fdo, pIrp);
break;
default: //确定设备是否真正进入特定的电源状态
status = DefaultPowerHandler(fdo, pIrp);
break;
}
return status;
}
5.Unload例程
在默认情况下,驱动程序装入之后,在重新引导之前就一直保持在系统中。要使系统可卸载,必须有一个Unload例程。Uload例程在DriverEntry期间声明,然后,每当驱动程序被手动或自动卸载时,I/O管理程序就调用该例程。
该例程函数原型为:
VOID Unload ( PDRIVER_OBJECT pDriverObject )
通常一个Uload例程执行如下工作。
· 对于某些种类的硬件,设备状态应当保存在注册表中。那样,在DriverEntry下一次执行时,设备能够恢复到最近已知的状态。
· 如果启动了设备中断,Unload例程必须仅用它们,并断开它们与中断对象的连接。
· 必须释放属于驱动程式的硬件。
· 必须从Win32名字空间中删除符号链接名字,这可以用IoDeleteSymbolicLink完成。
· 必须用IoDeleteDevice删除设备对象自身。
· 释放驱动程序占据的任何池内存。
驱动中的部分代码如下:
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
// 释放公共缓冲区
if (Gbl_CommonBuffer.PciMem.PhysicalAddr != (U32)NULL)
{
// 释放内存描述列表(MDL)
if (Gbl_CommonBuffer.pMdl != NULL)
{
IoFreeMdl(Gbl_CommonBuffer.pMdl);
Gbl_CommonBuffer.pMdl = NULL;
}
// 释放公共缓冲区
if (Gbl_CommonBuffer.pKernelVa != NULL)
{
MmFreeContiguousMemory(Gbl_CommonBuffer.pKernelVa);
Gbl_CommonBuffer.pKernelVa = NULL;
}
}
//释放DMA使用的缓冲区
#if defined(DMA_SUPPORT)
DriverBufferTerminate();
#endif
}
6.Dispatch例程
驱动程序装载是第一步工作,但是最终驱动程序的作业是响应I/O请求——来自用户模式应用程序的请求或者来自系统其他地方的请求。Windows 2000驱动程序通过Dispatch例程来处理这些请求。I/O管理程序调用这些例程以响应请求。
要启用特定的I/O函数代码,驱动函数必须首先“声明”响应这样一个请求的Dispatch例程。声明机制是DriverEntry执行的工作,它把Dispatch例程函数地址保存在驱动程序对象的MajorFunction表的合适位置上。
I/O函数代码是用于表的索引。其中,每个I/O函数代码(表索引)由一个IRP_MJ_XXX形式的惟一符号标示,在NTDDK.h(或WDM.h)包含文件中定义。
所有驱动程序必须支持函数代码IRP_MJ_CREATE,因为编写此代码的目的是响应Win32 CreateFile调用。如果不支持该代码,Win32应用程序将无法获取设备的句柄。同样也必须支持IRP_MJ_CLOSE,以处理Win32 CloserHandle调用。
驱动程序应当支持的其他函数代码取决它控制的设备的本质。表13.3将用到的I/O函数代码与产生它们的Win32调用相关联。
表13.3 Dispatch例程表
Win32函数 |
IRP主功能代码 |
驱动程序例程的名称 |
说 明 |
CreateFile |
IRP_MJ_CREATE |
Dispatch_Create |
请求一个句柄 |
CloseHandle |
IRP_MJ_CLEANUP |
Dispatch_Cleanup |
关闭句柄 |
CloseHandle |
IRP_MJ_CLOSE |
Dispatch_Close |
取消挂起的IRP |
ReadFile |
IRP_MJ_READ |
Dispatch_Read |
从设备获取数据 |
WriteFile |
IRP_MJ_WRITE |
Dispatch_Write |
将数据发送到设备 |
DeviceIoControl |
IRP_MJ_DEVICE_CONTROL |
Dispatch_IoControl |
控制操作 |