在WinCE中,最简单的一个驱动程序莫过于一个内置(Built-in)设备的流接口驱动。对于一个不支持热拔插的设备,最快捷的方法就是为其实现一个内置的流接口的驱动。
对于这样一类驱动程序,我们只需要按一种特定的规则实现一个动态库,其中实现对所有的硬件功能的调用,再将这个动态库加入系统中,然后设置相关的注册表项,使得在系统启动时设备管理器能识别并且加载这个设备即可。
1.流接口设备驱动的架构
流接口驱动借助文件系统调用从设备管理器和应用程序接收命令,驱动封装了所有的信息,这些信息对于这些命令转换为被控制的设备适当动作是必要的。所有的流接口驱动,否认它们管理内建设备还是管理可安装设备,或者在引导时被加载还是被动动态地加载,与其他系统组件都有相同的交互。
2.流接口驱动的实现
(1)流接口驱动的入口点
流接口驱动的入口点主要包括XXX_Init、XXX_Deinit、XXX_Open、XXX_Close、XXX_Read、XXX_Write、XXX_Seek、XXX_PowerUp、XXX_PowerDown和XXX_IOControl,其中XXX是在驱动程序注册表里定义的设备前缀。
(2)单访问和多访问
生成一个流接口驱动程序的时候,由于外围设备是以特殊文件形式供应用程序使用的,因此很容易出现多个应用程序同时访问同一个设备的情况。这时要考虑用户驱动程序对它的服务设备是否有多次打开文件处理的功能。
在WinCE中一个流接口驱动程序可以通过使用hOpenContext参数来实现单访问和多访问。如果是实现单访问,则第一次对XXX_Open的调用返回一个合法值,保持该值的合法性,后来的调用都返回空值;若是实现多访问,对于XXX_Open的调用每次都返回不同的值。
3.流接口函数
流接口函数也称作流接口驱动的入口点,下表对流接口函数进行了总结。
流接口函数 | 描述 |
XXX_Init | 当设备管理器初始化一个具体设备时调用这个函数。通过ActiveDeviceEx、ActiveDevice或RegisterDevice加载一个驱动时必需。 |
XXX_Deinit | 当设备管理器卸载一个驱动程序时调用这个函数。通过DeactiveDevice或DeregisterDevice卸载一个驱动时所必需。 |
XXX_PreDeinit | 将一个设备实例标记为无效并唤醒正在休眠的线程。如果一个驱动实现了XXX_PreClose函数,那么这个函数是必需的。//??LG1010 |
XXX_Open | 当应用程序调用CreateFile来获得一个设备句柄时会间接调用这个函数。通过CreateFile的函数函数访问设备时所必需。 |
XXX_Close | 关闭由hOpenContext识别的设备句柄。通过CreateFile的函数函数访问设备时所必需。与XXX_Open成对使用 |
XXX_PreClose | 将一个关闭的句柄标记为无效并唤醒任何正在休眠的线程。//??LG1010 |
XXX_Read | 从由hOpenContext识别的设备读取数据。不是必需,取决于驱动程序所导出的设备功能。该函数需要实现XXX_Open和XXX_Close函数。 |
XXX_Write | 写数据到设备。不是必需,取决于驱动程序所导出的设备功能。该函数也需要实现XXX_Open和XXX_Close函数。 |
XXX_Seek | 移动设备的数据指针。不是必需,取决于驱动程序所导出的设备功能。该函数也需要实现XXX_Open和XXX_Close函数。 |
XXX_PowerUp | 恢复对设备进行供电,它是可选的。 |
XXX_PowerDown | 结束对设备的供电,字是可选的。只有对在软件的控制下可以关闭的设备才是有用的。 |
XXX_IOControl | 发送一个命令到设备。不是必需的,取决于驱动程序所导出的设备功能。该函数需要实现XXX_Open和XXX_Close函数。 |
由于流接口驱动使用得非常广泛,下面对流接口函数进行较详细的论述。
1) DllEntry(HINSTANCE DllInstance, INT Reason, LPVOID Reserved )
这个函数是动态链接库的入口,每个动态链接库都需要输出这个函数,它只在动态库被加载和卸载时被调用,也就是设备管理器调用LoadLibrary而引起它被装入内存和调用UnloadLibrary将其从内存释放时被调用,因而它是每个动态链接库最早被调用的函数,一般用它做一些全局变量的初始化。
DllInstance:DLL的句柄,与一个EXE文件的句柄功能类似,一般可以通过它在得到DLL中的一些资源,例如对话框,除此之外一般没什么用处。
Reason:一般我们只关心两个值:DLL_PROCESS_ATTACH与DLL_PROCESS_DETACH,Reason等于前者是动态库被加载,等于后者是动态库被释放。
所以,我们可以在Reason等于前者是初始化一些资源,等于后者时将其释放。
2) DWORD XXX_Init(LPCTSTR pContext, LPCVOID lpvBusContext);
pContext:指向包含有流接口驱动的Active注册表路径的字符串。
lpvBusContext:一般不用。一个指向驱动程序特定的数据结构,开发者可以使用这个参数为总线驱动程序传递总线特定的信息。
该函数是驱动加载后第一个被执行的。主要负责完成对设备的初始化操作和驱动的安全性检查。由ActiveDeviceEx通过设备管理器调用。其返回值一般是一个数据结构指针,作为函数参数传递给其他流接口函数。
实际上,很多程序中将这个函数写成了DWORD XXX_Init( DWORD pContext ),我们只需要将pContext转化成LPCTSTR即可。
3) BOOL XXX_Deinit(DWORD hDeviceContext);
hDeviceContext:指向设备上下文的句柄,XXX_Init函数创建和返回这个句柄。
整个驱动中最后执行。用来停止和卸载设备。由DeactivateDevice触发设备管理器调用。成功返回TRUE。
如果驱动程序中有阻塞的线程,与这个句柄和设备实例相关的资源可能不被释放,为了避免这样,需要实现XXX_PreClose和XXX_PreDeinit入口点。 //??LG1010
4) DWORD XXX_Open(DWORD hDeviceContext, DWORD AccessCode , DWORD ShareMode);
hDeviceContext:指向设备上下文的句柄。
AccessCode:定义设备的访问请求代码,它是读和写的组合。
ShareMode:定义被请求文件的共享模式,它是一个文件读和写共享的组合。
打开设备,为后面的操作初始化数据结构,准备相应的资源。应用程序通过CreateFile函数间接调用之。返回一个结构指针,用于区分哪个应用程序调用了驱动,这个值还作为参数传递给其他接口函数XXX_Read、XXX_Write、XXX_Seek、XXX_IOControl。
5) BOOL XXX_Close(DWORD hOpenContext);
hOpenContext:由XXX_Open返回的句柄,用于识别打开的设备上下文。
关闭设备,释放资源。由CloseHandle函数间接调用。
如果驱动程序中有阻塞的线程,与这个句柄和设备实例相关的资源可能不被释放,为了避免这样,需要实现XXX_PreClose和XXX_PreDeinit入口点。 //??LG1010
6) DWORD XXX_Read(DWORD hOpenContext, LPVOID pBuffer, DWORD Count);
hOpenContext:由XXX_Open返回的句柄,用于识别打开的设备上下文。
pBuffer:指向存储从设备读取的数据缓冲的指针,这个缓冲至少应该Count字节长。
Count:从设备读到pBuffer的字节数。
由ReadFile函数间接调用,用来读取设备上的数据。返回读取的实际数据字节数。
7) DWORD XXX_Write(DWORD hOpenContext, LPCVOID pBuffer, DWORD Count);
hOpenContext:由XXX_Open返回的句柄,用于识别打开的设备上下文。
pBuffer:指向要写的数据缓冲的指针。
Count:从pButton写到设备的字节数。
由WriteFile函数间接调用,把数据写到设备上,返回实际写入的数据数。
8) DWORD IOC_Seek(DWORD hOpenContext, long Amount, WORD Type)
hOpenContext:由XXX_Open返回的句柄,用于识别打开的设备上下文。
Amount:定义要移动的设备数据指针的字节数,正值向文件后端移动指针,负值向文件开始方向移动指针。
Type:定义数据结构指针的起始点,它可以是值FILE_BEGIN、FILE_CURRENT或FILE_END。
将设备的数据指针指向特定的位置,应用程序通过SetFilePointer函数间接调用。不是所有设备的属性上都支持这项功能。如果一个设备可以被打开多次,这个函数只能修改由hOpenContext定义的设备实例的数据指针。
9) BOOL XXX_IOControl(
DWORD hOpenContext,
DWORD dwCode,
PBYTE pBufIn,
DWORD dwLenIn,
PBYTE pBufOut,
DWORD dwLenOut,
PDWORD pdwActualOut
);
hDeviceContext:由XXX_Open返回的句柄,用于识别打开的设备上下文;
dwCode:定义要完成的I/O控制操作,如果是CE已经支持的设备类,就用它已经定义好码值,否则就可以自已定义;
pBufIn:指向包含被传送到设备的数据缓冲的指针;
dwLenIn:由pBufIn定义的缓冲中数据的字节数;
pBufOut:指向从设备输出数据的缓冲的指针;
dwLenOut:由pBufOut定义的缓冲的最大字节数;
pdwActualOut:指向一个DWORD缓冲,这个函数使用它返回从设备收到的实际字节数。
其中,前两个参数是必须的,其它的任何一个都有可能是NULL或0。所以,当给pdwActualOut赋值时应该先判断它是否为一个有效的地址。
几乎可以说一个驱动程序的所有功能都可以在这个函数中实现。对于一类CE自身已经支持的设备,它们已经被定义了一套IO操作类,我们只需按照各类设备已经定义的内容去实现所有的IO操作。但当我们实现一个自定义的设备时,我们就可以随心所欲定义我们自已的IO操作。
用于向设备发送命令,应用程序通过DeviceIoControl调用来实现该功能。
10) void XXX_PowerUp(DWORD hDeviceContext);
hDeviceContext:设备的上下文句柄,由XXX_Init函数返回。
XXX_PowerUp和XXX_PowerDown电源处理函数都在内核模式执行,且不能被抢先。这种模式不允许绝大多数的函数调用,所以一般来说,电源处理函数不允许进行系统调用。 这个函数不应该调用可能引起它阻塞的任何函数,并应该尽快返回。
11) void XXX_PowerDown(DWORD hDeviceContext);
hDeviceContext:设备的上下文句柄,由XXX_Init函数返回。
操作系统调用这个函数挂起设备的电源,当操作系统进入省电模式时调用这个函数。这个函数不应该调用可能引起它阻塞的任何函数,并应该尽快返回。尽快返回的一个策略是使这个函数设置一个全局变量来指出出现了电源损失,并将任何复杂的处理推迟到以后XXX_PowerUp函数被调用时再进行处理。
12) XXX_PreClose和XXX_PreDeinit
XXX_PreClose函数将一个关闭的句柄标记为无效并唤醒任何正在休眠的线程,而XXX_PreDeinit函数将一个设备实例标记为无效并唤醒正在休眠的线程。两者被使用在当一个设置驱动中的线程被阻塞,而这些线程不能释放与设备句柄或设备实例相关联的资源的情况下。
4.流接口驱动中注册表设置
系统启动时启动设备管理程序,设备管理程序读取HKEY_LOCAL_MACHINE/Drivers/BuiltIn键的内容并加载已列出的流接口驱动程序。因此注册表对于驱动的加载有着关键作用。下面我们以设备名是“STR”来举一个例子:
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/STR]
"Prefix"="STR"
"Dll"="MyDev.Dll"
"Order"=dword:1
其中 "Prefix"="STR" 表示流接口的前缀,我们以后可以通过Creatfile 函数来实现对这个流接口的操作,实现如下:HANDLE hStr = CreateFile(TEXT("str1:"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0) 。其中“str1:”中的表示设备好“1”是必须的,不管你的设备当中是不是刚好只有一个,不然的话会出现打开不了这个设备的情况。
"Dll"="MyDev.Dll" 指的是“STR”设备调用的驱动程序 DLL。
HKEY_LOCAL_MACHINE/Drivers/BuiltIn/SampleDev 表示该注册表存在于CE系统注册表的 HKEY_LOCAL_MACHINE/Drivers/BuiltIn/SampleDev 目录下。
5.创建Makefile和Sources和.def文件
这三个文件主要是来控制编译的,Makefile 和 .def 文件实现比较简单,Sources 文件相对比较复杂。
Makefile:只需要这样一行!INCLUDE $(_MAKEENVROOT)/makefile.def ,具体的实现什么任务还不知道。
.def:文件定义需要输出的函数,这些函数能够被其它代码用动态加载的方法调用。
Sources:这个文件很重要,内容也多,最基本的一个文件该有如下内容。
TARGETNAME=MyDev(指定要生成的动态库的名称)
TARGETTYPE=DYNLINK(指定要生成的是一个动态库)
TARGETLIBS=$(_COMMONSDKROOT)/lib/$(_CPUINDPATH)/coredll.lib /
$(_COMMONOAKROOT)/lib/$(_CPUINDPATH)/ceddk.lib
(下面两项指定需要与哪些动态库链接,一般要第一项就足够了)
DLLENTRY=DllEntry(指定动态库的入口函数)
SOURCES= (请在这写上你所有源文件的名字,它们将会被编译)