最近开始尝试写WinCE6.0的驱动,当然从最简单的流驱动开始,选择了GPIO的驱动进行实验。本文参考了网上有很多流驱动的开发资料,但在开发的过程中也发现了一些细节问题,网络上并没有给出详细的解答,所以在这里记录下来,并对流驱动开发中的一些问题做了总结。
流驱动的开发有两种方法:添加驱动到
BSP和借助驱动调试助手。
第一种,添加驱动到
BSP。修改
BSP,将驱动加入到
BSP当中,再选择该
BSP当做
OS下载到目标板上。下面以
6410下实现
GPIO为例,说明详细的步骤:
首先,在
\WINCE600\PLATFORM\SMDK6410\SRC\DRIVES目录下面创建文件夹,命名为
GPIO,并在
GPIO文件夹下面创建源代码文件,命名为
gpio.c。同时在
DRIVES目录下修改
dirs文件,在
dirs文件最后添加新建的目录名
GPIO。在
gpio.c文件中实现的流式接口函数如下(这些接口函数的参数介绍见博文最后):
BOOL WINAPI DllEntry(HANDLE hInstDll, DWORD dwReason, LPVOID lpvReserved)
DWORD GPI_Init(LPCTSTR PContext, LPCVOID lpvBuscontext)
BOOL GPI_Deinit(DWORD hDeviceContext)
DWORD GPI_Open(DWORD hDeviceContext, DWORD AccessCode, DWORD shareMode)
BOOL GPI_Close(DWORD hOpenContext)
DWORD GPI_Read(DWORD hOpenContext, LPVOID pBuff, DWORD Count)
DWORD GPI_Write(DWORD hOpenContext, LPVOID pBuff, DWORD Count)
DWORD GPI_Seek(DWORD hOpenContext, long Amount, WORD Type)
void GPI_PowerUp(DWORD hDeviceContext)
void GPI_PowerDown(DWORD hDeviceContext)
BOOL GPI_IOControl(
DWORD hOpenContext,
DWORD dwCode,
PBYTE pBufIn,
DWORD dwLenIn,
PBYTE pBufOut,
DWORD dwLenOut,
PDWORD pdwactualOut
)
其次,在
GPIO文件夹下面创建
gpio.def文件,定义需要输出的函数,这些函数能够被其它代码用动态加载的方法调用。具体内容如下,有的介绍里面导出的函数中还有
EntryDll,其实没有必要,它是一个入口函数而已,主要导出那些会被调用的就可以了。
LIBRARY GPIO
EXPORTS GPI_Init
GPI_Deinit
GPI_Open
GPI_Close
GPI_Read
GPI_Write
GPI_Seek
GPI_PowerDown
GPI_PowerUp
GPI_IOControl
再次,在GPIO文件夹下面创建makefile文件,具体内容如下,这一步对于不同的流驱动基本不变。
!INCLUDE $(_MAKEENVROOT)\makefile.def
接下来,在
GPIO文件夹下面创建
sources文件,具体内容如下,各个宏的意义不做详细介绍,也可以添加其他的一些宏,根据具体情况定。
WINCEOEM=1
TARGETNAME=GPIO
TARGETTYPE=DYNLINK
RELEASETYPE=PLATFORM
DEFFILE=gpio.def
DLLENTRY=DllEntry
TARGETLIBS= \
$(_SYSGENSDKROOT)\lib\$(_CPUINDPATH)\coredll.lib
SOURCES=gpio.c
第五步,修改注册表信息了,打开WINCE600\PLATFORM\SMDK6410\FILES\Platform.reg 文件,添加以下内容:
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\GPIO]
"Prefix"="GPI"
"Dll"="GPIO.Dll"
"Index"=dword:1
"Order"=dword:1
这里要注意添加的时候不要添加到带有条件编译的语句当中去,否则还需要设置编译条件,比较麻烦。
最后,修改Platform.bib 文件,将GPIO驱动加载到NK当中。打开WINCE600\PLATFORM\SMDK6410\FILES\Platform.bib 文件,添加以下内容,
GPIO.dll $(_FLATRELEASEDIR)\GPIO.dll NK SH
到这里基本上准备工作就已经完成了,接下来编译,下载到目标板上。之后可以用调试工具查看驱动是否已经成功加入到注册表中,还有驱动是否被OS成功加载了。上述正确后,就可以编写上层测试代码进行验证了。
上述的步骤基本和网络的介绍一致,在调试过程中,遇到了以下两个问题:
1、驱动信息已经加入到注册表中,但是NK总是没有加载成功GPIO.dll驱动?
解决:出现这种情况,有可能是一下原因造成的:(1)没有实现上面提到的全部的流驱动接口函数,只是实现了其中的一部分,只要补齐所有的驱动接口函数就可以了;(2)在修改Platform.bib文件时,在文件末尾添加的内容。应该在宏FILES前面添加内容,否则就会无法加载驱动了。也有可能是其他原因,本人暂时没有遇到。
2、驱动加载后立马又被卸载?
解决:本人在调试过程中,通过串口输出信息,发现驱动GPIO被加载后立马又被卸载了,并且调用了GPI_Init函数,后来发现时由GPI_Init的返回值引起的。GPI_Init函数是驱动成功加载后调用的第一个函数,设备管理器通过调用GPI_Init初始化硬件,分配自己的内存空间,并将此内存块的地址以一个DWORD值返回给上层。如果返回0,说明初始化失败,之前分配的系统资源将全部释放,也就是驱动会被卸载。在简单的输出信息的流驱动中,最好将所有的流接口都设置为返回成功。
快速编译技巧:上面的方法要求每次都重新编译内核,很耗费时间,这里收集了网络上提供的修改驱动后快速编译的方法,下面的步骤建立在已经进行过一次上面的操作了,否则必须先修改注册表和.bib文件:
(1)在VS2005下Build菜单选择“Open Release Directory in Build Window”,进入到命令行模式,在命令行中进入到你的驱动目录,执行build命令就OK了。其实也可以直接在右侧“Solution Explorer”中找到你的驱动目录,右键选择Build就可以了。
(2)在VS2005下Build菜单选择“Make Run-Time Image”就可以产生NK文件了。
第二种,使用串口助手调试,这里给出原创的连接we-hjb的BLOG。
该方法将会比第一种方法效率高,不需要每次都编译NK,然后下载镜像到目标板,但存在适用范围的问题,即该方法并不是适用于任何的驱动。驱动调试助手,是用来动态管理流驱动。本地驱动和USB驱动不再它的控制范围之内,各位在使用时注意这一点。
补充流接口函数介绍(转自网络):
DllEntry(HINSTANCE DllInstance, INT Reason, LPVOID Reserved )
这个函数是动态链接库的入口,每个动态链接库都需要输出这个函数,它只在动态库被加载和卸载时被调用,也就是设备管理器调用LoadLibrary而引起它被装入内存和调用UnloadLibrary将其从内存释放时被调用,因而它是每个动态链接库最早被调用的函数,一般用它做一些全局变量的初始化。
参数:
DllInstance:DLL的句柄,与一个EXE文件的句柄功能类似,一般可以通过它在得到DLL中的一些资源,例如对话框,除此之外一般没什么用处。
Reason:
一般我们只关心两个值:DLL_PROCESS_ATTACH与DLL_PROCESS_DETACH,Reason等于前者是动态库被加载,等于后者是动态库被释放。
所以,我们可以在Reason等于前者是初始化一些资源,等于后者时将其释放。
DWORD XXX_Init( LPCTSTR pContext, LPCVOID lpvBusContext);它是驱动程序的动态库被成功装载以后第一个被调用的函数。其调用时间仅次与DllEntry,而且,当一个库用来生成多于一个的驱动程序实例时仅调用一次DllEntry,而xxx_Init会被调用多次。驱动程序应当在这个函数中初始化硬件,如果初始化成功,就分配一个自已的内存空间(通常用结构体表示),将自已的状态保存起来,并且将此内存块的地址做为一个DWORD值返回给上层。设备管理器就会用在调用XXX_Open时将此句柄传回,我们就能访问自已的状态。如果初始化失败,则返回0以通知这个驱动程序没有成功加载,先前所分配的系统资源应该全部释放,此程序的生命即告终至。当这个函数成功返回,设备管理器对这个程序就不做进一步处理,除非它设置了更多的特性。至此一个各为XXX的设备就已经加载成功,当用户程序调用CreateFile来打开这个设备时,设备管理器就会调XXX_Open函数。参数:pContext:系统传入的注册表键,通过它可以讲到我们在注册表中设置的配置信息。lpvBusContext:一般不用。实际上,很多程序中将这个函数写成了DWORD XXX_Init( DWORD pContext ),我们只需要将pContext转化成LPCTSTR即可。
DWORD XXX_Open(DWORD hDeviceContext,DWORD dwAccess, DWORD dwShareMode);当用户程序调用CreateFile打开这个设备时,设备管理器就会调用此驱动程序的XXX_Open函数。参数:hDeviceContext XXX_Init 返回给上层的值,也就是我们在XXX_Init中分配的用来记录驱动程序信息的那个结构体的指针,我们可以在这个函数中直接将其转化成所定义的结构,从而获取驱动程序的信息。dwAccess 上层所要求的访问方式,可以是读或者写,或者是0,即不读也不写。dwShareMode 上层程序所请求的共享模式,可以是共享读、共享写这两个值的逻辑或,或者是0,即独占式访问。系统层对设备文件的存取权限及共享方法已经做了处理,所以在驱动程序中对这两个参数一般可以不用理会。这个函数一般不用做太多处理,可以直接返回hDeviceContext表示成功,对于一个不支持多个用户的硬件,在设备已经打开后,应该总是返回0以至失败,则CreateFile调用不成功。
DWORD XXX_Close( DWORD hDeviceContext ); 当用户程序调用CloseHandle关闭这个设备句柄时,这个函数就会被设备管理器调用。参数:hDeviceContext 为XXX_Open返回给上层的那个值。这个函数应该做与XXX_Open相反的事情,具体包括:释放XXX_Open分配的内存,将驱动程序被打开的记数减少等。
DWORD XXX_Deinit( DWORD hDeviceContext );这个函数在设备被卸载时被调用,它应该实现与XXX_Init相反的操作,主要为释放前者占用的所有系统资源。参数:hDeviceContext XXX_Init函数返回给上层的那个句柄u void XXX_PowerUp( DWORD hDeviceContext );
void XXX_PowerDown(DWORD hDeviceContext );正如其名称中体现的那样,这两个函数在系统PowerUp与PowerDown时被调用,这两个函数中不能使用任何可能引起线程切换的函数,否则会引起系统死机。所以,在这两个函数中,实际上几乎是什么做不了,一般在PowerDown时做一个标志,让驱动程序知道自已曾经被Power Down过。在Power Down/On的过程中硬件可能会掉电,所以,尽管Power On以后,原来的IO操作仍然会从接着执行,但可能会失败。这时,当我们发现一次IO操作失败是因为程序曾经进入过Power Down状态,就重新初始化一次硬件,再做同样的IO操作。
BOOL XXX_IOControl(DWORD hDeviceContext,DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut );几乎可以说一个驱动程序的所有功能都可以在这个函数中实现。对于一类CE自身已经支持的设备,它们已经被定义了一套IO操作定,我们只需按照各类设备已经定义的内容去实现所有的IO操作。但当我们实现一个自定义的设备时,我们就可以随心所欲定义我们自已的IO操作。参数:hDeviceContext XXX_Open返回给上层的那个句柄,即我们自已定义的,用来存放程序所有信息的一个结构。dwCode IO操作码,如果是CE已经支持的设备类,就用它已经定义好码值,否则就可以自已定义。pBufIn 传入的Buffer,每个IO操作码都会定义自已的Buffer结构dwLenIn pBufIn以字节记的大小 pBufOut,dwLenOut分别为传出的Buffer,及其以字节记的大小pdwActualOut 驱动程序实际在pBufOut中填入的数据以字节记的大小其中,前两个参数是必须的,其它的任何一个都有可能是NULL或0。所以,当给pdwActualOut赋值时应该先判断它是否为一个有效的地址。
本文出自 “飞雪待剑” 博客,请务必保留此出处http://jazka.blog.51cto.com/809003/583199