9.1 Windows CE驱动程序结构概述
Windows CE的驱动程序可以从多种角度进行区分。
1.从加载以及接口方式来区分
可以分为本机设备驱动(Built-In Driver)、可加载驱动(Loadable Driver)以及混合型驱动。
(1)本机设备驱动
本机设备驱动即Native Device Drivers。这些驱动程序在系统启动时,在GWES的进程空间内被加载,因此它们不是以独立的DLL形式存在。这些驱动对应的设备通常在系统启动时就被要求加载,如果没有串口,也没有LCD的话,整个系统就不能和用户信息交流。另外,流驱动程序也能作为本机设备驱动而存在。
(2)可加载驱动
也被称为流驱动。
这些驱动可以在系统启动时或者和启动后的任何时候由设备管理器动态加载。通常它们以DLL动态链接库的形式存在,系统加载它们后,这些驱动程序也只是以用户态的角色运行。可加载驱动程序通过文件操作API来从设备管理器和应用程序获得命令。
在Windows CE中典型的可加载驱动有以下各类:
n PCMCIA driver(PCMCIA.dll)
n Serial driver(SERIAL.dll)
n ATAFLASH driver(ATA.dll)
n Ethernet driver(NE2000.dll,SMSC100FD.dll)
(3)混合型驱动
这类驱动综合了前两种驱动的特性。它同时使用了stream接口和custom-purpose接口。
混合型驱动主要是提供custom-purpose 接口,但是由于需要和系统中只允许使用stream接口的那些模块进行交互,因此也必须提供stream接口。例如,PC card socket驱动同时拥有两套接口。
2.从驱动层次上分
可以分为独立驱动和层次型驱动。图9-1是这两种驱动在系统中的位置。
图9-1 独立驱动和层次型驱动在系统中的位置
(1)独立驱动程序
可以将驱动程序编写成同时包含MDD和PDD层的独立驱动。独立驱动的代码应当包括中断服务例程和平台相关处理函数。使用独立驱动的好处在于可以省去MDD和PDD层驱动之间的信息传递,这一点在实时处理中非常重要。另外,如果设备的操作和MDD驱动层的接口描述相吻合,可以使用独立驱动程序提高处理性能。
(2)层次型驱动
层次型驱动分为两层,较上层的Model Device Driver(MDD)和比较下层的Platform Dependent Driver(PDD)。MDD实现的是和平台无关的功能,它描述了一个通用的驱动程序框架。而PDD是和硬件以及平台相关的代码组成。MDD调用PDD中特定的接口来获取硬件相关的信息。当使用层次型驱动的时候,一般只需要基于相近的样列驱动程序,针对特定的硬件修改PDD程序,MDD建立的框架可继续使用。由于层次间接口的层层调用以及消息的传递,使得处理速度相对独立驱动程序要慢,因此在时间要求苛刻的环境下,层次型驱动显得不是很适合。
一般MDD将完成以下任务。
n 连接PDD层,并且定义它要使用到的Device Driver Service Provider Interface(DDSI)函数集;
n 向设备管理器提供Device Driver Interface(DDI)接口集;
n 处理复杂的事件,如中断等等。
每一种MDD驱动都处理不同种类的设备。DDI是由MDD层驱动以及独立型驱动提供给设备管理器的一组接口集。DDSI是由PDD向MDD层提供的接口集。公司的设备可以用同样的DDI。
在开发过程中,MDD层驱动是不需要被修改的。微软公司不保证被修改的MDD能在系统中正确运行的。和MDD层驱动不同的是,PDD层驱动必须被修改成和特定硬件相匹配的代码。程序员可以自己开发一个PDD程序,多数情况下建议开发者在Platform Builder提供的样例驱动程序上进行修改。例如,Platform Builder提供了Wavedev驱动程序,它的代码位于%WINCEROOT%/public/common/oak/drivers/WAVEDEV下,这是一个容易理解的流接口层次型驱动程序。此样例audio驱动程序仅提供了播放及录音功能,只提供播放功能的结构框架,播放功能和音频设备的交互还需要PDD层来解决。
9.1.1 本机设备驱动程序
通常只有OEMs才会对本机设备驱动程序进行修改,其他*设备生产商由于只提供附加的硬件设备,对本机设备驱动程序不会有过多涉及。因此下面的本机设备驱动程序面向OEMs。
微软公司为每一种本机设备驱动程序设了一套custom接口。在此基础上,微软公司还为相同类型的设备驱动设计出了一组标准的接口。这样,Windows CE操作系统就能按照标准来操作同一类型的设备驱动,而不需要过多地去了解它们之间的硬件区别。如很多移动设备上都使用LCD来作为显示器,其中就有很多不同的LCD模块可以被使用到Windows CE的系统中。
Windows CE中提供了如下本机设备驱动程序实例。
n Display
n Battery
n Keyboard
n Touch screen
n Notification LED
如果在目标设备上,有以上列出之外的设备需要本机设备驱动程序,程序员就应当自己开发一套本机设备驱动程序。如果正在开发的目标系统上有上述的设备,开发人员应当考虑修改PlatformBuilder提供的样例驱动程序,尽可能不要重新编写驱动程序。样例驱动程序经过了Microsoft的测试,这比重新编写驱动程序省去一些验证上的麻烦。
9.1.2 流接口驱动程序的结构
n 流接口驱动有一套标准的接口,这和本机驱动是不一样的。
n 对于I/O设备来说是非常适合的。
n 操作接口和文件系统API十分类似,比如ReadFile,IOControl等。
n 应用程序可以和流接口驱动进行交互,并且可以把流驱动当成文件来操作。
流驱动与驱动接口、提供设备的种类无关,因为这组接口有统一的接口规范。对于需要数据流的设备来说,这种驱动是十分适合的,如串口就是个典型的例子。可以把使用流驱动的设备近似地看作是文件,这样可以通过文件系统API来操作设备,如ReadFile,IOControl。由于采用了文件系统的API,使得驱动程序能通过文件系统进行访问,这点和独立驱动程序是不同的。
这种将设备看成是文件的做法在很多操作系统上都比较常见。包括Windows桌面系统和类Unix的操作系统。例如,开发Windows驱动的程序员通常将打印机设备贯以LPTx:前缀,窗口被贯以COMx:前缀,这些都是特殊的文件名。
图9-2是流驱动程序在整个系统中的结构示意图。
图9-2 流驱动程序在系统中的结构示意图
图9-2显示了作为本机驱动而存在的流驱动程序,在启动时被设备管理器所加载。上节9.1.1介绍过,一般本机驱动是指custom接口的驱动程序,但是流驱动也可以成为本机驱动,例如串口。
如图9-2所示,流驱动通过文件系统API来和应用程序交互,同时又通过流接口接受设备管理器的管理。无论流驱动管理的是本机设备还是动态加载的设备,它们自身是在启动时被加载还是启动后由设备管理器动态加载,这和系统中其他模块的交互模型是一样的。
9.1.3 流驱动程序入口的实现
实现流驱动程序大致需要完成以下步骤。
(1)选择代表设备的文件名前缀;
(2)实现驱动的各个入口点;
(3)建立.DEF文件;
(4)在注册表中为驱动程序建立表项。
为了说明如何实现流驱动程序,我们将以platform builder提供的一个样例程序为参考来介绍。这个程序位于WINCE420/PLATFORM/SA11X0BD/DRIVERS/PWRBUTTON目录下,它是SA110XBD开发版上的power button驱动程序。
以下是创建流驱动的具体步骤。
(1)首先确定设备名的前缀。前缀非常重要,设备管理器在注册表中通过前缀来识别设备。同时,在流接口命名时,也将这个前缀作为入口点函数的前缀,如果设备前缀为XXX,那么流接口对应为XXX_Close,XXX_Init等。
(2)实现流接口的各个入口点。所谓入口点是指提供给设备管理器的标准文件I/O接口。
表9-1是对这些接口的介绍:
表9-1 接口的介绍
接 口 名 |
功能描述 |
XXX_Close |
关闭hOpenContext参数指定的设备上下文 |
XXX_Deinit |
通知设备管理器回收设备初始化时分配的资源 |
XXX_Init |
通知设备管理器为设备初始化时分配资源 |
XXX_Open |
打开设备,这个接口可以由应用程序直接调用createfile,然后通过文件系统映射为XXX_Open |
XXX_IOControl |
I/O控制指令 |
XXX_PowerUp |
设备加电时,此接口会被自动调用,可以在这里分配资源等 |
XXX_PowerDown |
如果设备能由软件控制断电,则在设备断电前,设备管理器会调用这个接口做些安全性检查 |
XX_Read |
从打开的设备文件中读取数据 |
XXX_Seek |
文件定位,如果设备支持的话 |
XXX_Write |
写数据到设备文件 |
在样例提供的驱动代码中,WINCE400/PLATFORM/SA11X0BD/DRIVERS/PWRBUTTON
/pwrbuttonpdd.c可以找到以上对应的入口点。
(3)建立*.DEF文件。在这个例子中,文件名为PWRBUTTON.DEF,DEF文件中定义了DLL要导出的接口集。流驱动大多是以DLL形式存在的,所以应将DLL和DEF的文件名统一起来。如PWRBUTTON.DEF,PWRBUTTON.DLL。
下面是PWRBUTTON.DEF文件的内容:
;Copyright (c) Microsoft Corporation. All rights reserved.
/* Copyright _ 1999 Intel Corp. */
LIBRARY PWRBUTTON
EXPORTS
PWR_Init
PWR_Deinit
PWR_Open
PWR_Close
PWR_Read
PWR_Write
PWR_Seek
PWR_IOControl
PWR_PowerDown
PWR_PowerUp
PWR_PowerHandler
PWR_DllEntry
如上代码所示,EXPORTS标签下,列出了一系列的接口,它们都是以PWR为前缀。
(4)在注册表中建立驱动程序入口点,这样设备管理器才能识别和管理这个驱动。
下面是power button驱动在注册表中的信息:
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/PWRBUTTON]
"Prefix"="PWR"
"Dll"="PwrButton.Dll"
"Order"=dword:2
"Ioctl"=dword:4
此外,注册表中还能存储额外的信息,这些信息可以在驱动运行之后被使用到。DLL项是设备管理器在加载驱动时需要的DLL名称;Prefix代表了设备前缀;ORDER是驱动程序被加载的顺序。
1.Streams入口:Open和Close
(1)XXX_Open入口
用于读/写打开一个设备文件。当应用程序调用CreateFile的时候,文件系统会自动调用本接口,打开一个已经存在的设备文件。当这个接口被调用的时候,设备驱动可以向设备管理器申请分配资源,并且为读/写文件做好准备。
下面是接口的原形:
DWORD XXX_Open( DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode );
参数解释:
hDeviceContext
指向XXX_Init返回的设备句柄(上下文)。
AccessCode
打开设备的权限描述符,这些权限包括读设备、写设备、读/写等。
所谓的设备上下文指的是代表图形设备接口graphics device interface(GDI)的数据结构。它包含了在特定区域内设备显示图象的信息。设备上下文包含了图形对象(例如pen、brush、字体等),不断地修改和调用它们,从而达到显示不同图象的效果。
(2)XXX_Close入口
在关闭设备的时候被操作系统调用。对应于CloseHandle接口。
BOOL XXX_Close( DWORD hOpenContext );
参数解释:
hOpenContext
指向XXX_Open返回的已经打开的设备句柄(上下文)。
应用程序可以调用CloseHandle来关闭正在使用的流驱动程序,然后设备管理器会相应地调用本接口来关闭设备,当设备关闭后hOpenContext描述的设备句柄将不再有效。
2.Streams入口:Init and Deinit
(1)XXX_Init入口
XXX_Init要完成以下任务。
n 在驱动被系统加载时,本接口被调用;
n 初始化需要的资源在本接口处理中被分配;
n 创建内存映射。
下面是接口的原型:
DWORD XXX_Init( DWORD dwContext );
参数解释:
n dwContext
指向一个字符串,它描述了注册表中的一个流设备接口。
当调用设备ActivateDeviceEx函数后,设备管理器自动调用这个函数。当用户激活一个新的设备时,如插入USB设备后,当总线自检时,设备就会被激活,这个接口就会被调用,这个接口是不允许应用程序直接调用的。
当这个接口的处理结果返回时,设备管理器就在注册表中寻找驱动的Ioctl子键。如果这个子键存在,设备管理器将调用XXX_IOControl接口将dwCode参数传入驱动入口点。
(2)XXX_Deinit入口
在驱动被系统卸载的时候,本接口将被调用,它将释放所有占用的阻援,并且停止IST。
下面是接口的原型:
BOOL XXX_Deinit( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向设备上下文的句柄。这个句柄应该是由XXX_Init返回的。
当程序调用DeactivateDevice时,设备管理器将自动调用本接口。流接口将释放全部它申请的资源,并且停止设备的运行。
3.Streams入口:Read,Write,Seek
(1)XXX_Read入口
当应用程序直接调用ReadFile函数时,设备管理器将调用这个接口。
下面是接口的原型:
DWORD XXX_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
pBuffer
指向缓冲区,这个缓冲区将用来存放从设备中读出的数据,以字节为单位。
Count
指定要从设备读取多少字节的数据存入pBuffer指向的缓冲区中。
这个从指定的设备中读取指定数量的字节数据,它对应的应用层API为ReadFile。ReadFile函数的参数hFile是指向设备的句柄,ReadFile函数的参数hFile将被填写到count参数中,ReadFile中的pSizeRead将存放实际读取数据的字节数。本接口的返回值是pSizeRead中填充的数值,若返回-1,代表发生了错误。
(2)XXX_Write入口
当应用程序调用WriteFile的时候,设备管理器将调用本接口。
下面是接口的原型:
DWORD XXX_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
pBuffer
指向缓冲区,这个缓冲区将用来存放要向设备中写入的数据,以字节为单位。
Count
指定要从pBuffer指向的缓冲区向设备读取写入多少字节的数据。
(3)XXX_Seek入口
在定位I/O指针的时候被调用。
下面是接口的原型:
DWORD XXX_Seek( DWORD hOpenContext, long Amount, WORD Type );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
Amount
指定指针要移动多少距离,以字节为单位。正值代表向文件尾端移动,负值则相反。
Type
描述了起始点的位置。当应用程序调用了SetFilePointer函数后,设备管理器就会调用本接口。
如果设备是可以重复打开的,本接口用到的指针只是针对hOpenContext的。
4.Streams入口:IOControl
XXX_IOControl入口
允许应用程序进行非文件的操作。
I/O控制字可以用来识别命令类别,普通的读写操作通常是不能完全满足程序要求的,I/O控制字是和设备相关的。
下面是接口的原型:
BOOL XXX_IOControl( DWORD hOpenContext, DWORD dwCode, PBYTE
pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD
pdwActualOut );
参数解释:
hOpenContext
指向XXX_Open接口建立并且返回的设备上下文。
dwCode
指定驱动程序要操作的I/O操作的标识码。这是由设备特定的,一般在头文件中有I/O操作的标识码的定义。
pBufIn
指向存放要向设备传输的数据的缓冲区。
dwLenIn
指定要从pBufIn指向的缓冲区向设备读取写入多少字节的数据。
pBufOut
指向缓冲区,这个缓冲区将用来存放从设备中读出的数据,以字节为单位。
dwLenOut
指定要从设备读取多少字节的数据存入pBuffer指向的缓冲区中。
pdwActualOut
DWORD型指针,指向的内容反映了从设备中读取的实际字节数。
这个接口主要是传递了包括读写在内的I/O控制命令给设备。和Windows桌面平台类似,应用程序可以通过直接调用DeviceIOControl函数使设备管理器激活本接口。dwCode参数指定了命令的类型,这些命令类型是由驱动程序指定的,并且通过头文件的形式提供给应用程序。
如果注册表中有HKEY_LOCAL_MACHINE/Drivers/BuiltIn/YourDevice/Ioctl键,设备管理器在加载驱动的时候就会调用本接口,并且使用注册表中相应项的值作为dwCode的值,把NULL填写入pBufIn和pBufOut中。驱动程序可以在这个时候加载其他模块以及其他不适合在XXX_Init出现的功能,在设备交互的过程中,以上各个接口基本遵循如下的调用顺序:XXX_Init,XXX_Open,XXX_IOControl,XXX_Close。XXX_Open接口是要获得设备句柄所必须的操作,XXX_Close是释放资源所必须的操作。
5.Streams入口:PowerUp和PowerDown
(1)XXX_PowerDown入口
这个接口是在停止对设备供应电源时被调用。下面是接口的原型:
void XXX_PowerDown( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向由XXX_Init返回的设备上下文。
这个函数应当执行停止设备供电的操作,此设备必须支持软关电的功能。在I/O control接口中,如果I/O命令字为IOCTL_POWER_XXX,那么就应该调用本接口。这个接口是对应与应用程序的,系统电源管理并不会用到这个接口。
设备管理器在将设备设置成节电模式之前将调用本接口,本接口将尽可能避免引起阻塞的操作,并尽快返回。
(2)XXX_PowerUp入口
恢复了设备的供电。
下面是接口的原型:
void XXX_PowerUp( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向XXX_Open接口建立并且返回的设备上下文。
这个接口在需要恢复设备电源供应时被调用。在I/O control接口中,如果I/O命令字为IOCTL_POWER_XXX,那么就应该调用本接口。这个接口是对应与应用程序的,系统电源管理并不会用到这个接口。本接口应尽可能避免引起阻塞的操作,将尽快返回,并设置全局变量来表明电源已经被恢复,可以进行后续操作。
9.1.4 加载设备驱动所需要的接口
设备管理器是如何加载驱动程序的呢?这里用到了一个重要的函数ActivateDeviceEx。
ActivateDeviceEx是指:
n 由设备管理器,也就是Device.exe来加载设备驱动;
n 注册表枚举时,用这个函数来读取启动时应当加载的驱动程序信息;
n ActivateDeviceEx将使用注册表中的Dll,Prefix,Index,Flags项。
ActivateDevice也能用来加载设备驱动,其实它内部也是调用了ActivateDevice
Ex。ActivateDeviceEx函数的主要功能就是用来加载驱动程序,它读取参数lpszDevKey中描述的注册表键来获取驱动程序的DLL名称、设备名前缀和其他相关信息(包括IOCTL等),这个函数可以用来替代ActivateDevice和RegisterDevice。
还以powerbutton为例,下面是注册表中它的相关内容。
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/PM]
"Prefix"="PWR"
"Dll"="pm.dll"
"Order"=dword:0
"Ioctl"=dword:4
"Index"=dword:0
"IClass"=multi_sz:"{A32942B7-920C-486b-B0E6-92A702A99B35}"
根据以上信息可以看出,在加载驱动程序时,调用ActivateDeviceEx函数的步骤为:
(1)ActivateDeviceEx(“//HKEY_LOCAL_MACHINE//Drivers//BuiltIn//PM”,…)
(2)ActivateDeviceEx函数将在//HKEY_LOCAL_MACHINE//Drivers//BuiltIn//PM键下寻找DLL的名称,在此也就是pm.dll,然后读取设备名前缀PWR以及index:0和其他信息。这里的index就是在前缀后面要添加上的序列号,如果是1,设备就是PWR1,它是用来妥善处理多重类似驱动设备在系统中被加载的情况。Index如果没有定义,设备管理器将为其指定一个空闲的索引号。
(3)ActivateDeviceEx将把设备添加到已激活设备列表中去。这时,如果没有定义index号,设备管理器必须指定一个空闲的索引号给设备,在添加完之后,设备管理器才将设备驱动加载到自己的进程上下文中。
下面是ActivateDeviceEx的函数原型:
HANDLE ActivateDeviceEx(
LPCWSTR lpszDevKey,
LPCVOID lpRegEnts,
DWORD cRegEnts,
LPVOID lpvParam
);
参数解释:
lpszDevKey
字符串指针,指向注册表中包含驱动信息的键,这个键应当包含驱动程序的DLL名、前缀、索引和入口点等信息。
lpRegEnts
指向REGINI结构体的数组,这个数组中定义了一些需要被添加到激活设备列表中的信息(ActivateDevice),这些信息填写后,驱动程序才被加载。如果是总线驱动的话,这里应该设置成NULL。
cRegEnts
lpRegEnts指向REGINI结构体的数组中元素的个数。
lpvParam
通过这个指针向已经加载的驱动程序传递参数,而不必将参数保留在注册表中,这个参数将以第2参数的角色被传递到XXX_Init(Device Manager)函数入口中。
返回值
如果返回具体设备的句柄,则代表操作成功,否则为失败。设备句柄可以在调DeactivateDevice函数的时候作为参数使用。
9.1.5 实现自己的流驱动程序
由于在Windows CE中没有生成驱动程序框架的wizard,所以一切必须手动添加,能由wizard所生成的,只是一个dll工程而已。
本节将先建立一个通用的流驱动程序框架,实现应用程序和驱动程序的交互。在后续章节中,就可以在本节的基础上再添加具体传输控制和完全实现软件狗的驱动程序了。
其步骤如下。
n 选择Emulator platform——Internet Appliance建立一个新的平台,其他都选默认项;
n 新建一个dll文件作为驱动程序,命名为MyDriver;
n 在MyDriver.dll的项目目录下新建一个MyDriver.def文件,用于表示dll导出函数;
n 在Platform Builder中添加MyDriver.def文件;
n 新建一个WCE应用程序,命名为helo.exe;
n 在MyDriver.cpp文件中添加如下函数(具体见MyDriver.cpp文件);
n MDV_Init MDV_Deinit MDV_Open MDV_Close MDV_IOControl;
n MDV_PowerUp MDV_PowerDown MDV_Read MDV_Write MDV_Seek;
n 在驱动中除了对设备的读写,还有必要对设备进行控制,这样就需要额外的例程;
n 在应用程序和驱动程序中添加IO Control Function代码(具体见MyDriver.cpp文件);
n 实现应用程序调用devicecontrol函数时,系统会自动映射到MDV_IOControl例程;
n 此外还需要更改def文件中的内容,标识DLL导出的函数;
n 在helo.cpp中添加启动设备和调用流接口的函数(见helo.cpp文件):
n 在project.reg注册表文件中添加注册表项,将虚拟设备的Prefix和Index值写入HKEY_LOCAL_MACHINE/Drivers/BuiltIn键下,这样系统在启动时会自检注册表,从而自动激活(具体见project.reg);
n 编译平台;
n 单击“Build”菜单项->“Open release directory”菜单,弹出命令行界面。转到MyDriver.dll所在目录,键入命令:Dumpbin /exports mydriver.dll;
n 显示出如下信息,这其中包括了DLL文件的导出函数信息(图9-3所示);
图9-3 DLL文件的导出函数信息
n 下载系统kernel,选择“Target”命令->“Run programs”->helo.exe;
n 在Platform Builder的build区内就可以看到类似以下的调试信息。
通过以上步骤,一个流驱动程序的框架就构造完成了。
9.1.6 设备文件名
如9.1.2节介绍,应用程序可以通过文件系统API来访问流驱动程序。在加载打开流设备驱动时,要用到设备标识,如COM1,PGR7等,文件系统ApI的调用被设备管理器重定向到流驱动入口点。
下面将介绍创建的文件格式、前缀和索引的内容。
1.设备文件的命名
如果要把设备当作文件来操作,那么有一点是必须要做的,就是文件名必须有特定的格式来标识文件是个设备,这种文件名应当由3个大写字母以及一位数字和冒号“:”组成,如串口可以被命名为“COM27:”、并口可以被命名“LPT1”。
2.设备文件的前缀
如果在注册表中Prefix子键没有被定义,那么流驱动程序的初始化和de-初始化入口点必须在注册表中定义,否则就应该在注册表中定义前缀。前缀由三个字符组成,它将出现在流驱动入口点的函数名称首部,这样就将设备文件名和特定的驱动入口点映射起来。
3.设备文件的索引
文件索引号是将同种设备的不同实例区分开来。索引是用跟在前缀之后的一位数字标示,默认情况下取值范围从0~9。如果需要有第10个设备实例,则用0作为索引值。
如果需要从非1的值开始记数设备的实例,那么应当在注册表中设置子键“Index”指定开始的索引值,这在某些情况下是有必要的,例如,如果系统Built-In驱动程序已经为某类设备占用了1-4的索引值,那么同种类的驱动再要被加载,索引值就应当从5开始。
9.1.7 注册表自举
什么是注册表自举?它在系统中的作用是什么?
n 注册表自举是由设备管理器(Device.exe)在系统启动时加载的。
n 注册表自举时,通过读取注册表来寻找系统中的新设备。设置新设备的注册表信息应当是在安装驱动程序时,即InstallDriver入口中设置到注册表中去的。
n 实现为REGENUM.DLL动态链接库。
n 实现将代码存放在WINCE400/public/common/oak/DRIVERS/REGENUM目录下。
注册表自举是设备驱动加载中的一部分。在系统启动时,初始化设备管理器完毕后,设备管理器将进入设备驱动加载阶段,在这个阶段的早期将进行注册表自举,这个过程在注册表中不断寻找新的设备驱动(未加载,但是需要被加载的设备驱动)加载。注册表自举是可以重入的,通过软件函数调用可以重新进行注册表自举。
下面以一个例子来分析注册表自举的具体过程:
[HKEY_LOCAL_MACHINE/Drivers]
"RootKey"="Drivers//BuiltIn"
"Dll"="RegEnum.dll“
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn]
"Dll"="RegEnum.dll"
[HKLM/Drivers/ BuiltIn /PCI]
"Dll"="PCIbus.dll"
"Order"=dword:4
"Flags"=dword:1
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]]
"Order"=dword:0
"Flags"=dword:0
"Dll"="RegEnum.dll"
[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual /NDIS]
"Dll"="NDIS.dll"
"Order"=dword:1
"Prefix"="NDS"
1.在Device.exe进入注册表自举阶段,检查HKLM/Drivers/RootKey指向的子键。在这个例子中,RootKey的值被设置成Drivers//BuiltIn,一般默认为Drivers。
2.在HKEY_LOCAL_MACHINE/Drivers/BuiltIn子键中检查DLL项的值,这里被设置成RegEnum.dll,也就是注册表自举模块,然后加载这个DLL。
3.将HKEY_LOCAL_MACHINE/Drivers/BuiltIn作为参数传递给RegEnum的Init函数。注意,Init函数不是一个流式接口,RegEnum只检查键下的一级子键中有无DLL需要加载,至于二级以下的子键,则必须递归调用RegEnum。
4.Init函数在HKLM/Drivers/RootKey键下基于“Order”值的顺序进行检查。逐个检查每个入口加载,为它们初始化驱动程序。在此,HKLM/Drivers/RootKey下有两个子键:[HKLM/Drivers/BuiltIn/PCI]和[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual],其中,[HKLM/Drivers/BuiltIn/PCI]的Order的值为4,而[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]]的Order值为0,因此RegEnum将优先检查[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]。
5.RegEnum进入[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]子键进行检查,发现此子键下的DLL项值为RegEnum.dll,则说明要递归使用RegEnum检查其下的子键。这里为[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual/NDIS],其Order项值为1,并且没有其他子键与其竞争加载顺序,所以它将被[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]下的RegEnum.dll加载。[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual /NDIS]下的DLL项值为NDIS.dll,因此NDIS驱动被加载。
6.完成[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Virtual]下一级子键的检查后,[HKEY_LOCAL_MACHINE/Drivers/BuiltIn]下的RegEnum.dll将检查[HKLM/Drivers/ BuiltIn /PCI]子键下的DLL,这里为PCIbus.dll,因此PCI驱动被加载。
通过以上步骤这个例子中的注册表自举完成。
在注册表自举的过程中,子键有表9-2中所示的各种形式:
表9-2 子键的形式
子 键 |
类 型 |
描 述 |
Order |
DWORD |
最小的Order值将优先被加载 |
Dll |
SZCHAR |
定义了要让注册表自举程序加载的驱动的DLL名,这个名称也将被写入ActiveDevice。只有DLL项是必须被设置的,如果Order项没有被设置,那么该驱动将在所有设置了Order项的驱动被加载后加载 |
Index |
DWORD |
同类设备驱动的实例被加载时可以通过不同的索引号来区分。如COM0:COM1:,索引的取值范围从0到9,如果没有指定索引值,那么系统将会自动指定一个 |
Flags |
DWORD |
描述注册表自举的默认操作。Flags的值是一系列位,他们将描述ActivateDevice的行为。如果不需要这个项的话,可以将其设置为0。位3~23为保留位,必须设置成0,注册表自举时可以使用位23~31来设置 |
表9-3是对Flag各个位功能的描述:
表9-3 Flag各个位功能
Flag |
值 |
功能描述 |
DEVFLAGS_NONE |
0x00000000 |
没有任何功能定义 |
DEVFLAGS_UNLOAD |
0x00000001 |
在调用驱动程序XXX_Init入口后,卸载驱动程序 |
DEVFLAGS_LOADLIBRARY |
0x00000002 |
使用LoadLibrary函数来加载驱动,而不是LoadDriver |
DEVFLAGS_NOLOAD |
0x00000004 |
不加载DLL |
9.1.8 服务与设备的比较
本节将讨论有关Windows CE服务模块、服务模块和普通驱动程序的区别以及如何注册、激活/控制/停止一个服务。
Services.exe是一个和Device.exe有同等地位的进程,它的用途是为了弥补Device.exe的不稳定。如果一个设备驱动由于某种原因处于异常状态或者不受设备管理器控制时,若让其继续存留在设备管理器中,则会影响其他正常运行的驱动程序,甚至使系统崩溃,而使用服务这种机制能尽可能地避免这种情况发生,当系统中有类似服务操作失败的信号产生时,系统不会因此而受到影响。和驱动程序一样,服务也提供了能控制其自身运行状态的函数接口集,如启动服务、暂停服务、其他I/O控制。
在Windows CE. NET中操作服务和操作设备驱动的方式十分类似。如果一个设备驱动实际上并不需要和硬件发生交互,也不需要进程间通信的时候,就应当将其实现为一个服务,以提高系统安全性。服务出错,不会引起系统的严重错误。
Services.exe同样可以被配置用来在特定的连接上等待信息。Socket端口接收到数据包时,Services.exe就会将数据分发到相应的服务中去。
有一点需要注意的是,Services.exe只是防止驱动程序因为服务失败而崩溃,但并不保证它不会引起其他服务的错误,因此服务应当认真调试。
在%WINCEROOT%/Public/Servers/Sdk/Samples/Telnetd目录下,Windows CE.NET提供了一个样例程序,里面包含了如何启动、加载和卸载一个服务,同时还包含了如何进行流接口服务的实现方式。
1.如何激活并且控制一个服务
所谓激活服务就是将服务加载到Service.exe的进程空间里来。可以使用以下方法来加载服务并且在加载后进行控制。
n 使用注册表中built-in键的功能,在系统启动时,通过注册表自举将服务加载;
n 使用ActivateService函数,通过程序来加载服务;
n 可以使用CreateFile函数来打开一个服务;
n 可以使用I/O control、ReadFile、WriteFile、SetFilePointer等函数来操作一个服务;
n 还可以使用GetServiceHandle来获得服务的句柄。
当用程序来加载一个服务时,代码加载的格式为:
ActivateService(L"TELNETD", 0);
和设备管理器能控制一个正在运行的设备驱动一样,Service.exe也能通过多种途径来控制一个正在运行的服务。
以下是实现控制服务的具体步骤。
(1)使用CreateFile函数,在参数中设置适当的前缀和索引值。这些值应当在注册服务的时候被写入注册表;
(2)向服务发送I/O控制字。利用DeviceIoControl函数,设备管理器重定向到该服务的XXX_IOControl入口。下面的代码段显示了如何获取一个服务的状态:
HANDLE hService = CreateFile(L"TEL0:",0,0,NULL,OPEN_EXISTING,0,NULL);
if(hService != INVALID_HANDLE_VALUE) {
DWORD dwState;
DeviceIoControl(hService, IOCTL_SERVICE_STATUS, NULL, 0,
&dwState, sizeof(DWORD), NULL, NULL);
CloseHandle(hService);
};
(3)如果服务还支持流式接口的话,那么可以通过额外的操作来控制一个服务。如ReadFile、WriteFile、SetFilePointer函数。
还可以使用GetServiceHandle 函数来控制一个服务的状态。需要注意的是,在调用流式接口之前,必须获取服务的有效句柄,否则这些操作不会成功。可以通过GetServiceHandle函数获取服务的有效句柄。
下面是使用GetServiceHandle的一个例子:
HANDLE hService = GetServiceHandle("TEL0:", NULL, NULL);
if(hService != INVALID_HANDLE_VALUE) {
DWORD dwState;
ServiceIoControl(hService, IOCTL_SERVICE_STATUS, NULL, 0,
&dwState, sizeof(DWORD), NULL, NULL);
}
2.Windows CE服务的目的
n 作为设备管理器的补充
n 处理不需要直接和系统有交互的请求
n 增强驱动的可靠性,降低系统崩溃的可能
n 提供超级服务
Services.exe是作为对Device.exe模块的补充,着重加强了其加载系统服务的可靠性。它将那些和系统内核交互不是很多或者不用直接和内核交互的请求与系统服务分割开来独立处理,这样就降低了内核的处理负担。
当有一个服务发生故障时,及时地将这个服务和系统服务分割开来是保障系统安全运行的重要手段之一。这个特性使得Windows CE中的驱动程序在系统服务发生故障的时候,仍然有较高的安全运行能力,降低系统崩溃风险。例如,很多嵌入式设备在运行之后可能无人监管,因此系统安全性要求比较高,不能象桌面Windows系统那样经常因为设备驱动的错误而崩溃。
所谓的超级服务是指一个所有端口都允许被Services.exe监控的标准服务,即一个服务能监视所有其他的服务。使用超级服务有一个好处就是,即便许多的服务在使用,只要它们是被超级服务监视的,那么这些服务的处理线程都只由超级服务的处理线程承担,即所有服务最终只有一个处理线程。如果不使用超级服务,那么每个服务都要创建一个接受线程。
Services.exe模块是通过注册表加载的。代码如下所示:
[HKEY_LOCAL_MACHINE/init]
"Launch60"="services.exe"
"Depend60"=hex:14,00
3.服务与设备的区别
n Device.exe加载驱动用来管理设备
n Sevice.exe加载驱动用来管理软件服务
n Services.exe象Device.exe一样可以加载多个服务
n 如果要同时使用Device.exe和Services.exe, Windows CE中32个进程槽就要被占用两个
Services.exe和Device.exe模块被设计用来互相补充。Device.exe即设备管理器用来加载驱动程序,以此来管理外围设备,Services.exe将加载那些不和设备打交道的服务。
Services.exe可以管理多个服务,正如设备管理器可以管理多个设备驱动那样。 Services.exe负责将和系统无关的服务的处理从设备管理器中分割出来。当然,设备管理器可以在Services.exe没有被加载的环境下运行,这样可以节省一个进程槽。但是,不加载Service.exe的后果就是驱动程序运行的安全性下降。
4.如何注册一个服务
n 使用RegisterService函数注册
n RegisterService函数功能和RegisterDevice函数功能类似,后者目前逐渐被ActivateDeviceEx所替代。
Services.exe 也可以通过编程来启动。代码如下所示:
HANDLE hService = RegisterService("TEL",0,"telnetd.dll",0);
这行代码启动了telnet server的服务例程。如果telnet server当前正在TEL0设备上运行,那么这个函数将返回false。如果调用GetlastError函数,将得到ERROR_DEVICE_IN_USE的错误信息。
RegisterService调用后,设备管理器将调用服务的xxx_Init入口,在调用完xxx_Init接口后,服务处于什么样的状态是依据服务各自不同dwContext设置而定的。在Services.h中,为这个参数预设置了以下值:
#define SERVICE_INIT_STARTED 0x00000000
#define SERVICE_INIT_STOPPED 0x00000001
如果dwContext参数中没有设置SERVICE_INIT_STOPPED值,那么服务在被注册后将建立一个线程,并且启动它作为服务的请求接收处理线程。
如果设置了SERVICE_INIT_STOPPED标志,那么代表本服务将接收超级服务的监督管理,自己将不拥有接收线程,而统一有超级服务进行处理。
下面是RegisterService函数的原型:
HANDLE RegisterService(
LPCWSTR lpszType,
DWORD dwIndex,
LPCWSTR lpszLib,
DWORD dwInfo
);
参数解释:
lpszType
指向一个字符串,这个字符串包含了服务的前缀名(由三个字母组成,和流驱动类似)
dwIndex
定义了0~9之间的服务的索引号。
lpszLib
指向一个字符串,该字符串包含了代表服务的DLL文件名。
dwInfo
这是代表在调用服务的xxx_Init 入口时,传入的参数。
返回值
如果返回值是被注册的服务的句柄,则代表操作成功。如果返回空值则代表操作失败。
RegisterService可以将一个未加载的服务加载到系统中,同样也可以为一个系统中已经存在的服务加载第2个实例。
RegisterService将先加载在参数lpszLib中定义的DLL链接库,然后检查该DLL链接库是否导出了xxx_Init、xxx_Deinit、xxx_IOControl接口。如果DLL链接库名出错,将导致系统寻找不到相应的文件,那么函数将返回ERROR_FILE_NOT_FOUND错误;如果DLL链接库中导出的接口集不正确的话,函数将返回ERROR_FILE_NOT_FOUND错误。
如果系统中已经有同类的服务在运行,即前缀名一致,那么就需要用索引值来区分实例。如果索引值也冲突的话,函数将返回ERROR_DEVICE_IN_USE错误。
在成功加载DLL文件后,Services.exe将调用xxx_Init入口,并且将dwInfo中的参数传递给它。如果init是成功的话,函数将xxx_Init的返回值保留,这将是后面xxx_IOControl、xxx_Deinit入口调用时的重要参数。
5.如何停止一个正在运行的服务
使用DeregisterService函数来停止并且卸载一个服务。DeregisterService将在系统中定位一个服务,并且将它标记为无效;然后DeregisterService将阻止任何企图打开该服务的函数。
下面的代码是实现关闭一个服务的例子:
HANDLE hServide = GetServiceHandle(L"TEL0: ",NULL,NULL);
if(hSerice != NULL)
DeregisterService(hService);
DeregisterService 需要使用从RegisterService返回的服务句柄,而不是使用CreateFile 返回的句柄。因此应用程序无法通过文件系统的流式API来获取服务的句柄,只能使用GetServiceHandle。
6.服务中所使用的API
Services.exe实现了如下的接口。
n XXX_Close
服务实现这个接口,并且由Services.exe 调用。当应用程序调用CloseHandle或者CloseService来关闭一个服务时,Services.exe会将操作重映射到本接口;
n XXX_Deinit
服务实现这个接口,并且由Services.exe 调用。当应用程序调用DeregisterService来取消注册一个服务时,Services.exe会将操作重映射到本接口;
n XXX_Init
初始化服务的接口;
n XXX_IOControl
向服务传递I/O控制字;
n XXX_Read
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似读文件的功能,只能被流式的服务所实现;
n XXX_Seek
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似文件定位的功能,只能被流式的服务所实现。
n XXX_Write
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似写文件的功能,只能被流式的服务所实现。
9.2 启动程序BootLoader的分析
9.2.1 什么是BootLoader
BootLoader就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境设置成一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。
BootLoader是严重地依赖于硬件而实现的,特别是在嵌入式系统。因此,在嵌入式系统里建立一个通用的BootLoader几乎是不可能的。尽管如此,我们仍然可以对BootLoader归纳出一些通用的概念来,以指导用户进行特定的BootLoader设计与实现。
嵌入式系统中,Bootloader的意义与作用与PC上的BIOS有点类似,它对开发板上的主要部件如CPU、SDRAM、FLASH、串口等进行了初始化,可以使用Bootloader下载文件到开发板,可以浏览目录,可以烧录flash,可以启动系统等,实际上,一个功能比较强大的Bootloader已经相当于一个微型的操作系统了。
初始化基础硬件——CPU速度、存储器定时、中断、检测ram大小。
引导装载程序通常是在任何硬件上执行的第一段代码。台式机这样的常规系统中,通常将引导装载程序装入主引导记录(Master Boot Record,MBR)中,在嵌入式设备中,通常将引导程序放置在不易丢失的存储器的开始地址或者是系统冷启动时PC寄存器的初始值。通常,在台式机或其他系统上,BIOS将控制移交给引导装载程序。而在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。引导程序完成自己的任务后,也将控制权移交给操作系统。
总体上Bootloader需要完成以下工作。
n 初始化CPU速度;
n 初始化内存,包括启用内存库,初始化内存配置寄存器等;
n 初始化中断控制器,在系统启动时,关闭中断,关闭看门狗;
n 初始化串行端口(如果在目标上有的话);
n 启用指令/数据高速缓存;
n 设置堆栈指针;
n 设置参数区域并构造参数结构和标记(这是重要的一步,因为内核在标识根设备、页面大小、内存大小以及更多内容时要使用引导参数);
n 执行POST(加电自检)来标识存在的设备并报告有何问题;
n 为电源管理提供挂起/恢复支持;
n 传输操作系统内核镜像文件到目标机。也可以将操作系统内核镜像文件事先存放在Flash中,这样就不需要BootLoader和主机传输操作系统内核镜像文件,这通常是在做成产品的情况下使用。而一般在开发过程中,为了调试内核的方便,不将操作系统内核镜像文件固化在Flash中,这就需要主机和目标机进行文件传输;
n 跳转到内核的开始,在此又分为ROM启动和RAM启动。所谓ROM启动就是用XIP技术直接在Flash中执行操作系统镜像文件;所谓RAM启动就是指把内核镜像从Flash复制到RAM中,然后再将PC指针跳转到RAM中的操作系统启动地址。
嵌入式系统的资源有限,程序通常都是固化在ROM中运行。ROM中程序执行前,需要对系统硬件和软件运行环境进行初始化,这些工作是用汇编语言编写的启动程序完成。
启动程序是嵌入式程序的开头部分,应与应用程序一起固化在ROM中,并首先在系统上运行,它应包含各模块中可能出现的所有段类,并合理安排它们的次序。
9.2.2 BootLoader和主机之间文件传输的通信协议
最常见的情况就是,目标机上的BootLoader通过串口与主机之间进行文件传输,传输协议通常是xmodem/ymodem/zmodem协议中的一种。但是,串口传输的速度是有限的,因此通过以太网连接并借助TFTP协议来下载文件是个更好的选择。
此外,在论及这个话题时,主机方所用的软件也要考虑。比如,在通过以太网连接和TFTP协议来下载文件时,主机方必须有一个软件用来的提供TFTP服务。
9.2.3 BootLoader选项
程序员可以在自己的BootLoader中实现不同的启动选项,在此将对一些常用的选项进行讨论。如:BootLoader通信方式,串口中的启动功能等。
图9-4是最常见的嵌入式系统目标机和主机通信的模型。开发人员可以使用超级终端通过串口向目标机发送命令,由于串口协议是最简单、可靠的,因此经常被使用在系统未启动前的阶段。
图9-4 嵌入式系统中目标机和主机通信模型
串口命令行选项
超级终端上使用串口协议,用文本形式来显示和目标机的通信过程。
用户可以在超级终端的命令行中指定目标机执行某个命令。这些命令包括。
n 从Ethernet下载操作系统镜像文件;
n 从Flash启动(这要求操作系统镜像文件已经被固化在Flash中);
n 启动RAM测试程序;
n 对目标机的I/O端口进行测试;
开发人员可以自己对这些命令进行修改或者扩充。
下面就是通过串口通信来控制目标机的菜单样例:
*************************
Generic BootLoader Version 1.0
*************************
Select option:
D – Download Image
M – Memory Test
B – Start Windows CE .NET
E – Download via Ethernet
P -- Download via Parallel
A – Dial-up Boot
=> (enter your selection here)
下面解释这些命令的功能。
n Memory test命令
这个命令包含了两种测试方式:其一,对设备寄存器的访问测试。由于ARM芯片内部的各个寄存器都是内存编址的,因此对设备寄存器的访问可以等同于对内存的读写;其二,对常规内存进行测试,方法大致是向一个内存地址写一个数据,再读一边,如果值相等,则说明数据总线和地址总线工作正常没有发生短路现象。
在嵌入式系统开发过程中,硬件的制作是第一步。当硬件成型之后,就需要软件来测试硬件平台是否符合设计要求,因此对硬件测试要从RAM、Flash、register等个方面进行。
下面列出的是x86平台下测试寄存器的样例代码:
UCHAR __inline READ_PORT_UCHAR(PUCHAR port)
{
return _inp((USHORT)port);
}
VOID __inline WRITE_PORT_UCHAR(PUCHAR port, UCHAR value)
{
_outp((USHORT)port, (value));
}
For other platforms other than x86, you use
UCHAR __inline READ_PORT_UCHAR(PUCHAR port)
{
return *(volatile unsigned char * const)port;
}
VOID __inline WRITE_PORT_UCHAR(PUCHAR port, UCHAR value)
{
*(volatile unsigned char * const)port = value;
}
下面的代码显示了如何读写普通内存:
ULONG __inline READ_REGISTER_ULONG(PULONG Register)
{
return *(volatile unsigned long * const)Register;
}
VOID __inline WRITE_REGISTER_ULONG(PULONG Register, ULONG
value)
{
*(volatile unsigned long * const)Register = value;
}
n Program FLASH命令
由于硬件结构的原因,Flash的写方式和RAM是不同的,因此对Flash是否能够正常读写一定要编程检测。读Flash的方式基本和读RAM一样,可以随机读写。
下面,我们以Intel StrataFlash Memory(J3)Flash为例,简单说明如何擦写Flash。
Intel StrataFlash Memory(J3)Flash是一款Nor型的Flash。Flash将其内部存储空间分割成一个个block,要对Flash写编程,就应当将block擦除,然后执行写操作。Flash有一个特性,即如果要将某一存储单元从值1改变成值0,只有通过擦除的方式,而不能直接把值0写入目标地址。
根据上述列出的编程介绍,我们用下列函数:
void IntelFlashErase(U16 *MY_FLASHADDR)
{
U16 temp;
*(U16*)MY_FLASHADDR = 0x60;
*(U16*)MY_FLASHADDR = 0xD0;
*(U16*)MY_FLASHADDR = 0x20;
*(U16*)MY_FLASHADDR = 0xD0;
}
void IntelFlashWrite(U16 *MY_FLASHADDR)
{
*(U16*)MY_FLASHADDR = 0x40;
*(U16*)MY_FLASHADDR = 0xAA55;//写入AA55值
}
通过以上这两个C函数,我们既可以用来测试Flash,又可以通过整片擦除的方式清空整片Flash,从而可以将操作系统的镜像文件写入Flash。
n Boot from FLASH命令
这也就是利用XIP技术在Flash中启动操作系统,而不是将它加载到RAM后运行。
9.2.4 实现一个 BootLoader
1.BootLoader的构成组件
BootLoader主要由以下两部分组成。
(1)OEM startup code
这部分代码是在BootLoader中最先被执行的。它的主要功能是初始化最小范围的硬件设备,比如设置CPU工作频率、关闭看门狗、设置cache、设置RAM的刷新率、填写内存控制寄存器(通知CPU有效的数据总线引脚数)等。由于系统刚刚启动,不适合使用复杂的高级语言,因此这部分代码主要由汇编程序完成。在汇编程序段设置完堆栈后,就跳转到C语言的Main函数入口(位于<PLATNAME>/eboot/main.c);
(2)Main code
这部分代码由C语言实现,是BLCOMMON代码的一部分,它可以用来执行比较复杂的操作。比如检测内存和Flash的有效性、检测外部设备接口、检测串口并且向已经连接的主机发送调试信息、通过串口等待命令、启动网络接口、建立内存映射等汇编无法完成的工作。
Main code包含以下代码段:
Image download代码段
Image的下载可以通过以下接口。
① A. Parallel port I/O接口。这是通过主机和目标机的并口之间的数据传输来完成下载工作。具体实现代码,读者可以参考/kernel/hal/mdppfs.c文件;
② Ethernet port I/O接口。这是通过主机和目标机的网络接口之间的数据传输来完成下载工作。具体实现代码,读者可以参考WINCE420/public/common/oak/DRIVE|RS/ETHDBG/EBOOT文件夹;
③ Debug serial I/O接口。这是通过主机和目标机的串口之间的数据传输来完成下载工作。利用串口来传输的缺点非常明显,那就是速度太慢;
通过事先Flash write代码将镜像文件固化入Flash。只要BootLoader被设计成能从Flash加载镜像文件,本选项就可以使用。
2.BootLoader控制流函数Control Flow Functions
图9-5为Windows CE.NET的BootLoader的整体架构。其中列出了各种控制流函数。
图9-5 Boot Loader的整体架构
下面,我们来看以下Windows CE.NET的BootLoader在系统启动时所执行的函数。
图9-5中的所有函数分为3个模块:BLCOMMON、Download Function、FLASH Function。其中BLCOMMON模块是由微软公司提供的,执行一些逻辑上的功能,因此建议开发人员不要对其进行修改。而Download Function、FLASH Function中的函数与硬件平台息息相关,因此对于每种硬件平台都要将函数的实现进行修改。
BLCOMMON模块的功能解释。
BLCOMMON是一个库,其实现代码位于%_WINCEROOT%/Public/Common/Oak/Drivers/
Ethdbg/Blcommon目录下。它实现了Windows CE.NET BootLoader的基本框架。这个库的工作为:将bootloader加载到RAM中执行、解压缩.bin文件、校验硬件平台的完整性、对加载的进度进行跟踪。在BLCOMMON阶段执行的过程中,主要使用OEM函数集。
BLCOMMON库的入口点为BootloaderMain函数,它有Startup汇编函数完成后跳转至该入口。BLCOMMON库将被BootLoader的程序链接在一起。
在系统启动时,CPU首先执行StartUp函数,这是个由汇编实现的函数。StartUp函数主要的功能为:设置CPU工作频率、关闭看门狗、设置cache、设置RAM的刷新率、填写内存控制寄存器(通知CPU有效的数据总线引脚数)等。在StartUp完成任务后,就跳转到BootLoaderMain函数中。这个是由C语言编程实现的函数入口点。
下面是SMDK2410中的BootLoader中的main函数实现代码:
void main(void)
{
//清空LED
OEMWriteDebugLED(0, 0xF);
//通用BootLoader (blcommon)主入口
BootloaderMain();
//注意,在此调用了BootloaderMain函数,并且没有返回值
SpinForever();
}
(1)BLCOMMON模块函数
下面列举出BLCOMMON中的控制函数并分析它们,这些函数在Blcommon.h中声明,代码实现在Blcommon.lib里:
n OEMDebugInit函数:
在运行BootloaderMain程序后,将首先调用OEMDebugInit函数,它用来初始化调试信息的I/O设备,最常见的是串口设备。由于RS232协议简单性,在系统没有启动前对串口初始化较适用。在OEMDebugInit里,又通常调用OEMInitDebugSerial函数来初始化串口。
n OEMPlatformInit函数
OEM层的初始化函数,它主要负责目标机上的硬件初始化。在汇编阶段只是初始化了很小一部分硬件,这是由于BootLoader要求处理时间短,因此在汇编阶段的硬件初始化是十分简单的。所以有必要用高级语言完成对目标机的硬件设置,这包括具体的时钟设置、驱动和传输设备接口的初始化。
下面是此函数代码实例:
BOOL OEMPlatformInit(void)
{
BYTE BootDelay;
BYTE KeySelect;
EBOOT_CFG EbootCfg;
DWORD dwStartTime, dwPrevTime, dwCurrTime;
PCI_REG_INFO NANDInfo;
EdbgOutputDebugString("Microsoft Windows CE Bootloader for the Samsung SMDK2410 Version %d.%d Built %s/r/n/r/n", EBOOT_VERSION_MAJOR, EBOOT_VERS
ION_MINOR, __DATE__);
//初始化LCD显示器
InitDisplay();
// 初始化驱动全局区域
memset(pDriverGlobals, 0, sizeof(DRIVER_GLOBALS));
pDriverGlobals->MajorVer = DRVGLB_MAJOR_VER;
pDriverGlobals->MinorVer = DRVGLB_MINOR_VER;
pDriverGlobals->eth.EbootMagicNum = EBOOT_MAGIC_NUM;
// 初始化Flash,SMDK2410上的FLASH为AMD AM29LV800型。
if (!AM29LV800_Init(AMD_FLASH_START))
{
RETAILMSG(1, (TEXT("ERROR: OEMPlatformInit: Flash 初始化
failed./r/n")));
return(FALSE);
}
........
// 让用户选择启动选项
while((dwCurrTime - dwStartTime) < EbootCfg.BootDelay)
{
KeySelect = OEMReadDebugByte();
......
}
switch(KeySelect)//判别用户命令
{
case 0x20: // 根菜单项
g_bDownloadImage = MainMenu(&EbootCfg);
break;
case 0x00: //无按键失败
case 0x0d: //用户取消了倒计时
default:
if (EbootCfg.ConfigFlags & CONFIG_FLAGS_AUTOBOOT)
{
EdbgOutputDebugString ( "/r/nLaunching flash image ... /r/n");
g_bDownloadImage = FALSE;
}
else
{
EdbgOutputDebugString ( "/r/nStarting auto-download ... /r/n");
g_bDownloadImage = TRUE;
}
break;
}
//如果用户指定了静态IP地址,那么就使用静态IP地址(不使用DHCP)
if (g_bDownloadImage && !(EbootCfg.ConfigFlags & CONFIG_FLAGS_DHCP))
{
pDriverGlobals->eth.TargetAddr.dwIP = EbootCfg.IPAddr;
pDriverGlobals->eth.SubnetMask = EbootCfg.SubnetMask;
}
//配制以太网控制器
if (!InitEthDevice(&EbootCfg))
{
DEBUGMSG(1, (TEXT("OEMPlatformInit: Failed to initialize Ethernet
controller./r/n")));
return(FALSE);
}
return(TRUE);
}
n OEMPreDownload函数
在下载操作系统前执行这个函数,它可以用来设置如何进行Image文件下载。例如,可以设置成从网络下载或者跳过下载直接加载Flash中的Image文件。
下面是此函数代码实例:
DWORD OEMPreDownload(void)
{
CHAR szDeviceName[EDBG_MAX_DEV_NAMELEN];
BOOL bGotJump = FALSE;
DWORD dwDHCPLeaseTime = 0;
PDWORD pdwDHCPLeaseTime = &dwDHCPLeaseTime;
DWORD dwBootFlags = 0;
//如果用户想进入已存在的映像,那么跳过下载
if (!g_bDownloadImage)
{
g_bWaitForConnect = FALSE; // 不等待宿主机连接
return(BL_JUMP);
}
//如果用户想用一个静态IP地址,那么就不要从DHCP服务器请求一个地址
//将DHCP租期时间变量设置为NULL
if (pDriverGlobals->eth.TargetAddr.dwIP &&
pDriverGlobals->eth.SubnetMask)
{
pdwDHCPLeaseTime = NULL;
RETAILMSG(1, (TEXT("INFO: Using static IP address %s./r/n"),
inet_ntoa(pDriverGlobals->eth.TargetAddr.dwIP)));
RETAILMSG(1, (TEXT("INFO: Using subnet mask %s./r/n"),
inet_ntoa(pDriverGlobals->eth.SubnetMask)));
}
//创建基于以太网地址的设备名称(也就是Platform Builder如何定义设备)
//
memset(szDeviceName, 0, EDBG_MAX_DEV_NAMELEN);
CreateDeviceName(&pDriverGlobals->eth.TargetAddr, szDeviceName,
PLATFORM_STRING);
EdbgOutputDebugString("INFO: Using device name: '%s'/n", szDeviceName);
//初始化TFTP传送
//
if (!EbootInitEtherTransport(&pDriverGlobals->eth.TargetAddr,
&pDriverGlobals->eth.SubnetMask,
&bGotJump,
pdwDHCPLeaseTime,
EBOOT_VERSION_MAJOR,
EBOOT_VERSION_MINOR,
PLATFORM_STRING,
szDeviceName,
EDBG_CPU_ARM720,
dwBootFlags))
{
return(BL_ERROR);
}
//保存DHCP租期时间(注意,本例中使用的是静态IP)
pDriverGlobals->eth.DHCPLeaseTime = dwDHCPLeaseTime;
return(bGotJump ? BL_JUMP : BL_DOWNLOAD);
}
n DownloadImage
这个函数将执行把操作系统Image文件下载到目标机的操作。
n OEMLaunch函数
这个函数将PC指针直接设置到Image文件的开始地址,它是启动操作系统前BootLoader的最后一个函数,没有返回值。在此之后,BootLoader就消失了。
下面分析SMDK2410的OEMLaunch的实现代码:
void OEMLaunch(DWORD dwImageStart, DWORD dwImageLength, DWORD dwLaunchAddr,
const ROMHDR *pRomHdr)
{
DWORD dwPhysLaunchAddr;
EDBG_OS_CONFIG_DATA *pCfgData;
EDBG_ADDR EshellHostAddr;
EBOOT_CFG EbootCfg;
//从flash得到eboot配制
ReadEbootConfig(&EbootCfg);
//下载并从服务器得到IP和端口设置后,等待Platform Builder连接
//连接也发送KITL标志,稍后将用于OS(KITL)
if (g_bWaitForConnect)
{
……
}
//如果该下载未提供一个地址,那么记住kernel的启动地址或者重新调用保存的地址、、
//(也就是不下载kernel区域)
if (dwLaunchAddr && (EbootCfg.LaunchAddress != dwLaunchAddr))
{
EbootCfg.LaunchAddress = dwLaunchAddr;
WriteEbootConfig(&EbootCfg);
}
else
{
dwLaunchAddr = EbootCfg.LaunchAddress;
}
//如果用户请求一个储存在flash中的RAM映像,可以马上去请求.对于多个RAM BIN文件
//需要将RAM地址映射到flash地址
// RAM中基于地址偏移的映像在flash中是连续的
if (g_bDownloadImage && (EbootCfg.ConfigFlags & CONFIG_FLAGS_SAVETOFLASH))
{
if (!WriteRegionsToSmartMedia(&EbootCfg))
{
EdbgOutputDebugString("WARNING: OEMLaunch: Failed to store image
to Smart Media./r/n");
}
}
//跳到下载的映像(物理地址,因为马上将关闭MMU)
dwPhysLaunchAddr = ToPhysicalAddr(dwLaunchAddr);
EdbgOutputDebugString("INFO: OEMLaunch: Jumping to Physical Address 0x%Xh
(Virtual Address 0x%Xh).../r/n/r/n/r/n", dwPhysLaunchAddr, dwLaunchAddr);
Launch(dwPhysLaunchAddr);
//应该从不返回
SpinForever();
}
(2)下载模块函数
下载函数是由DownloadImage函数调用的。
下面列出下载模块函数并解释它们。
n OEMReadData
BLCOMMON调用这个函数从文件的传输器中读取数据。读者可以参看Public/Common/
Oak/Ethdbg/Eboot/Ebsimp.c文件中网络传输的例子
n OEMShowProgress
BLCOMMON在下载操作系统镜像文件的时候调用这个函数。在这个函数中,可以实现通知用户下载状态的各种手段比如可以用LED灯交替闪烁或者向主机的串口发送进度信息等。
n OEMMapMemAddr
如果目标系统的需求是要能支持把操作系统的镜像文件下载到FLASH中去,就必须调用本函数。由于FLASH操作速度比RAM慢,在片擦除的时候甚至会使读写操作停滞,这样在每次下载操作系统镜像文件时,由于FLASH的擦写都会使下载停滞。而OEMMapMemAddr使用了RAM缓冲操作系统镜像文件的方式,使得用户在下载操作系统镜像文件时感觉不到停滞,这个函数将FLASH地址映射到RAM地址,这样向FLASH写的数据实际上先被缓冲到RAM中,然后再写到FLASH中。
(3)FLASH编程模块
FLASH函数用于对不同的FLASH存储器进行编程。开发人员需要实现微软公司提供的框架里的函数。
n OEMIsFlashAddr函数
判别地址是否为有效的FLASH地址。注意,这里的FLASH地址与平台相关的,如S3C2410芯片和PXA255芯片的FLASH地址是不一样的,即便是同一款CPU,由于硬件结构的不同(FLASH大小、位置等)FLASH地址也不尽相同。
n OEMStartEraseFlash函数
BLCOMMON在获取FLASH的实际大小和开始地址后,将立即调用这个函数。这个函数将进行FLASH的擦除工作。
n OEMContinueEraseFlash函数
BLCOMMON在下载操作系统镜像文件的过程中可以调用这个函数。当FLASH擦除发生错误的时候,可以用这个函数来重复擦除操作,并且进行校验。
n OEMFinishEraseFlash函数
FLASH擦除完成时,BLCOMMON调用这个函数。这个函数将校验所有的擦除工作是否完成。
n OEMWriteFlash函数
调用这个函数,将缓冲在FLASH_CACHE中的操作系统镜像文件写入FLASH中。
3.代码分析
(1)Startup代码分析
在/PLATFORM/XSC1BD/EBOOT/ARM/fwp2.s文件中定义了BootLoader启动时执行的汇编指令:
GBLL ETHBOOT
ETHBOOT SETL {TRUE}
INCLUDE ..//..//kernel//hal//arm//fwxsc1.s
END
这里将..//..//kernel//hal//arm//fwxsc1.s中的代码引入。
kernel//hal//arm//fwxsc1.s:
; --- Setup interrupt / exception vectors
STARTUPTEXT
LEAF_ENTRY StartUp
IMPORT main
B Reset_Handler
B Undefined_Handler
B SWI_Handler
B Prefetch_Handler
B Abort_Handler
NOP ;
B IRQ_Handler
B FIQ_Handler ;
SWI_Handler
IF PLAT_LUBBOCK = "1"
ldr r1, =0x01000000
ldr r2, =FPGA_REGS_BASE_PHYSICAL
str r1, [r2, #HEXLED_OFFSET]
ENDIF
Prefetch_Handler
IF PLAT_LUBBOCK = "1"
ldr r1, =0x00100000
ldr r2, =FPGA_REGS_BASE_PHYSICAL
str r1, [r2, #HEXLED_OFFSET]
ENDIF
Abort_Handler
; 在此处不保存状态
IF B_STEP_PXA2X0 = "1"
; 在步骤B中,只对PXA250或PXA210芯片做这个操作
mrc p15, 0, r3, c15, c1, 0 ;Get Reg15 of CP15 for Access to CP7
; **********************************
IRQ_Handler
IF PLAT_LUBBOCK = "1"
ldr r1, =0xABCDEFAB
ldr r2, =0x08000010
str r1, [r2]
ENDIF
b IRQ_Handler
; **********************************
FIQ_Handler
IF PLAT_LUBBOCK = "1"
ldr r1, =0x00000100
ldr r2, =FPGA_REGS_BASE_PHYSICAL
str r1, [r2, #HEXLED_OFFSET]
ENDIF
FIQ_STAY
b FIQ_STAY
; **********************************
ALIGN 32
Reset_Handler
bl INITGPIO
bl INITMEMC
bl INITINTC
bl INITCLKS
bl INITOST
bl INITRTC
bl INITPWRMAN
bl ENABLECLKS
bl INITPLATFORM
bl DISPLAY_FREQS
b INITMMU
(2)Main函数代码分析
BLCOMMON的实现代码在/WINCE420/PUBLIC/COMMON/OAK/DRIVERS/ETHDBG/
BLCOMMON目录下的blcommon.c文件中。
void BootloaderMain (void)
{
ROMHDR *pRomHdr = NULL; // 镜像文件的PTOC
DWORD dwAction, dwpToc;
DWORD dwImageStart = 0, dwImageLength = 0, dwLaunchAddr = 0;
BOOL bDownloaded = FALSE;
if (!KernelRelocate (pTOC)) {
HALT (BLERR_KERNELRELOCATE);
}
//初始化debug支持,然后就可以用OEMWriteDebugString了
if (!OEMDebugInit ()) {
HALT (BLERR_DBGINIT);
}
//输出版本
EdbgOutputDebugString (NKSignon, CURRENT_VERSION_MAJOR, CURRENT_
VERSION_MINOR);
//初始化平台(时钟、驱动等)
if (!OEMPlatformInit ()) {
HALT (BLERR_PLATINIT);
}
//准备下载
EdbgOutputDebugString ("System ready!/r/nPreparing for download.../r/n");
//调用OEM指定的pre-download函数
switch (dwAction = OEMPreDownload ()) {
case BL_DOWNLOAD:
//下载映像
if (!DownloadImage (&dwImageStart, &dwImageLength, &dwLaunchAddr))
{
//报告DownloadImage的错误
SPIN_FOREVER;
}
bDownloaded = TRUE;
//检查pToc信号("CECE")
if (*(LPDWORD) OEMMapMemAddr (dwImageStart, dwImageStart +
ROM_SIGNATURE_OFFSET) == ROM_SIGNATURE) {
EdbgOutputDebugString("Found pTOC signature./n");
}
else
{
EdbgOutputDebugString
("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!/r/n");
EdbgOutputDebugString
("!! ERROR: Unable to find a table of contents in the downloaded image. If !!/r/n");
EdbgOutputDebugString
("!! this is a multi-region image, move the chain file region to an address !!/r/n");
EdbgOutputDebugString
("!! following the kernel region. Aborting.!/r/n");
EdbgOutputDebugString ("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!/r/n");
//如果没有信号,将永远循环下去
HALT (BLERR_SIGNATURE);
}
dwpToc = *(LPDWORD) OEMMapMemAddr (dwImageStart, dwImageStart +
ROM_SIGNATURE_OFFSET + sizeof(ULONG));
// 为了修正指针,需要再次映射一次
dwpToc = (DWORD) OEMMapMemAddr (dwImageStart, dwpToc + g_dwROMOffset);
//注意:必须复制或删除CLEAN_BOOT标志
memcpy (pRomHdr = &romhdr, (LPVOID) dwpToc, sizeof(ROMHDR));
EdbgOutputDebugString ("ROMHDR at Address %Xh/r/n", dwImageStart +
ROM_SIGNATURE_OFFSET + sizeof (DWORD)); // right after signature
EdbgOutputDebugString ("RomHdr.ulRAMStart=%Xh RomHdr.physfirst=%Xh./r/n",
romhdr.ulRAMStart, romhdr.physfirst);
//失败
case BL_JUMP:
//跳到映像之前,可以选择检查映像信号
//注意:如果现在没有下载的映像,那么在OEMLaunch中将假定从本地存储装载
//是RAM中残留的以前的下载,这种情况下,.映像启动地址必须是0.
//也就是映像信号程序将需要在存储或者RAM中寻找映像进行验证.
//因此OEM的OEMLaunch函数始终需要这样做.
//
if (g_pOEMCheckSignature)
{
if (!g_pOEMCheckSignature(dwImageStart, g_dwROMOffset, dwLaunchAddr, bDownloaded))
HALT(BLERR_WHQL_SIGNATURE);
}
//最后启动镜像,从不返回
OEMLaunch (dwImageStart, dwImageLength, dwLaunchAddr, pRomHdr);
// 从不返回
default:
HALT (BLERR_INVALIDCMD);
}
}
(3)FLASH操作代码分析
由于FLASH操作函数比较多,在此我们分析其中的几个有代表性的函数。
BOOL OEMIsFlashAddr (DWORD dwAddr)
{
//特殊处理Eboot,Eboot是为RAM创建,但存在于flash中
if (fileType & BOOTLOADER)
{
return(TRUE);
}
if ((dwAddr & 0xF0000000) == 0x90000000 || (dwAddr & 0xF0000000) == 0xB0000000)
{
return(TRUE);
}
return(FALSE);
}
static UINT16 FlashErase(DWORD dwStartBlock, DWORD dwNumBlocks)
{
BlockLockInfo BlockLockInfo;
if (dwStartBlock >= g_FlashInfo.dwNumBlocks || (dwStartBlock + dwNumBlocks - 1) >= g_FlashInfo.dwNumBlocks)
{
EdbgOutputDebugString("FlashErase: block number outside valid range./r/n");
return(-1);
}
//Unlock全部需要删除的块
EdbgOutputDebugString("FlashErase: Unlocking flash block(s) [0x%x, 0x%x] (please wait): ", dwStartBlock, (dwStartBlock + dwNumBlocks - 1));
BlockLockInfo.StartBlock = dwStartBlock;
BlockLockInfo.NumBlocks = dwNumBlocks;
if (!FMD_OEMIoControl(IOCTL_FMD_UNLOCK_BLOCKS, (PBYTE)&BlockLockInfo, sizeof(BlockLockInfo), NULL, 0, NULL))
{
EdbgOutputDebugString("/r/nWARNING: Unable to unlock all flash blocks!/r/n");
}
else
{
EdbgOutputDebugString("Done./r/n");
}
EdbgOutputDebugString("Erasing flash block(s) [0x%x, 0x%x] (please wait): ", dwStartBlock, (dwStartBlock + dwNumBlocks - 1));
while (dwNumBlocks--)
{
if (!FMD_EraseBlock(dwStartBlock))
{
EdbgOutputDebugString("/r/nFlashErase: unable to erase block (0x%x)./r/n", dwStartBlock);
return(-1);
}
++dwStartBlock;
EdbgOutputDebugString(".");
}
EdbgOutputDebugString("Done./r/n");
return(0);
}
9.2.5 Windows CE标准BootLoader的需求
1.BSP必须在加载Windows CE BootLoader以及操作系统的时候有一个默认的模式。
(1)在系统启动时,即使用户不输入指令,也能自动地下载操作系统镜像文件。即便使用了启动菜单,也是有时间限制了,超过时间限制,BootLoader应当能自动地按照默认方式启动操作系统;
(2)BootLoader应当被固化在FLASH中;
(3)BootLoader应当使用BLCOMMON的架构;
(4)BootLoader应当提供FLASH擦写和BootLoader自我更新的功能。这就要求BootLoader要分段执行,在FLASH中执行时,主要把自身剩余的代码复制到RAM中,然后进入RAM中运行时就能更新在FLASH中的BootLoader镜像文件。如果一直在FLASH中执行,同时又更新FLASH中的数据,那样会引起程序的逻辑错误;
(5)BootLoader应当提供向线性地址的FLASH下载操作系统镜像文件,并且跳转到其镜像文件数据首地址的功能;
(6)BootLoader应当提供向线性地址的RAM下载操作系统镜像文件,并且跳转到其镜像文件数据首地址的功能;
(7)在Platform Builder中的功能控制流里出现的任何分支,BootLoader都应当提供支持;
(8)BootLoader应当能使用硬件提供的任何接口来下载操作系统镜像文件,如并口、串口、以太口以及硬盘。在Platform Builder 4.2自带的样例BootLoader中,都以以太口为默认的传输方式。
2.目标机硬件结构的设计应当兼顾软件性能。硬件结构设计是否合理,这关系到后续软件开发以及调试的效率。
(1)目标机的flash应当是可以被替换的;
(2)目标机应当提供足够的RAM和flash来支持debugging;
(3)目标机是否有LED灯之类操作简单的、可用于调试的设备。
3.BootLoader应当提供两个启动选项:从主机下载操作系统镜像文件以及从FLASH中固化的镜像文件启动。在下载操作系统镜像文件执行时,又分为两个步骤:先将镜像文件下载到RAM中,然后再固化到FLASH里。
以上便是符合Windows CE标准要求的BootLoader功能及其硬件支持。这些要求保证了BootLoader在各个平台间中逻辑结构上的一致性。
那么BootLoader是如何被写入裸板的呢?实现方法有以下几种方式。
(1)使用ADS软件和JTAG仿真器。先将BootLoader的镜像文件通过JTAG下载到目标机的RAM中,然后在ADS中运行FLASH的烧写软件,这样可以把RAM中的数据写入FLASH;
(2)使用专门的FLASH编程器,将BootLoader写如FLASH(注意,这时FLASH还没有插入目标机,不受CPU控制)然后将烧写完毕的FLASH插入目标机中。
(3)在BootLoader已经驻留在FLASH的情况下,可以通过BootLoader实现自我更新的功能。
9.2.6 编译BootLoader程序
BootLoader程序可以通过PB的集成编译环境编译链接,控制文件为.bib文件,下面是一个简单的BootLoader的.bib文件:
MEMORY
CLI 9fc00000 00050000 RAMIMAGE
RAM 80080000 00070000 RAM
CONFIG
COMPRESSION=ON
SRE=ON
ROMSTART=9fc00000
ROMSIZE=00020000
ROMWIDTH=32
ROMOFFET=000000
MODULES
Nk.exe $(_FLATRELEASEDIR).exe CLI
MEMORY部分,定义了生成的映像文件的目标地址,以及程序运行可以使用的内存空间。
CONFIG部分,COMPRESSION是否对目标代码进行压缩;SRE是否生成格式为sre的目标代码;ROMSTART与ROMSIZE、ROMWIDTH、ROMOFFSET共同定义了开发平台上存放BootLoader物理介质的起始地址、大小、宽度和偏移量;
MODULES部分,定义了BootLoader所包含的文件,一般就只有一个文件cli.exe。
编译过程中,首先用命令build-c编译生成文件cli.exe,然后用romimage cli.bib命令产生最后的映像文件cli.sre。
BootLoader文件的下载有很多种方法:可以通过仿真器下载;可以通过其他调试程序下载;还可以直接烧写到Flash中。需要说明的是,这些方法可能会要求不同的映像格式。在PB环境下,可以生成的有.sre格式、纯二进制格式(用于直接烧写Flash)以及和Windows CE映像一样的.bin格式。
编译步骤如下。
1.点击菜单中“Build”->“Open Build Release Directory”打开命令行;
2.在Platform命令行下,键入如下命令:Set WINCEREL=1;
3.进入BootLoader的目录cd %_TGTPLATROOT%/Eboot;
4.Build-cfs。