嵌入式车载系统软件主要包括系统内核、驱动程序、应用程序三部分。设计的过程当中,我们采用瀑布模型进行设计,首先制定Windows CE5.0系统内核,再次编写相关设备驱动,最后编写或移植应用程序。
制定内核时,我们采用SunSaung2440 BSP(板级支持包)进行制定,同时提交组建保证系统支持网络通讯、文件系统、CAB包安装、汉语支持等功能。不仅如此,为方便系统应用程序开发,内核制定之后,我们发布相应的SDK。
图 5.1 软件设计流程
Windows CE5.0系统内核制定,主要采用Platfrom Builder5.0编译工具,其实Windows CE5.0系统内核制定并不是很难,制定一个简单的内核,只需要了解Platfrom Builder5.0编译器,按照一定的步骤制定即可。但是如果要制定一个完全新的内核却很困难,其中最困难的当属BSP开发,关于BSP的编写不是本论文要将的内容,在此我们从基础入手,介绍Windows CE内核制定的一般流程。虽然Windows CE5.0系统内核制定相对较简单,但却是整个系统中最重要的部分[2]。
Windows CE5.0内核制定的过程,是针对特定的硬件平台和应用,选择和配置各种组件,开发自定义组件,修改系统的配置文件,甚至对某些源码进行修改的过程。所以我们需要知道各种文件的存放位置,以便于迅速准确的定位,缩短开发时间,同时需要熟悉各种组件的作用和功能,以便将它们添加到系统中完成相应的功能。接下来我们来展示Windows CE5.0目录组织、内核组件等方面的内容。
各个软件安装完成之后,安装盘的根目录下会产生一个WINCE500目录,这里包括Windows CE5.0部分源代码、BSP和一些编译Windows CE时需要用到的工具和库文件。另外还产生一个%_WINCEROOT%/Program Files/Microsoft Platfrom Builder/5.00的目录,这里主要包括,Platform Builder5.0的开发环境工具,帮助文档和其他一些开发时用到的工具。
表 2是安装软件后的WINCE500目录组织;
表 2 WIN500目录结构
目录 |
内容说明 |
CRC |
存放Platfrom Builder5.0安装时候用到的校验文件CRC.INI |
PBWorkspaces |
_PLATFORMROOT环境变量标识Platform目录存放所有的BSP,一般来说BSP的名字与开发板的名字一致。 |
OTHERS |
包含WINCE中一些模块和二进制库文件和代码。如果在OS Design中选择某些组件,那么这些二进制代码就会被包含到最终的操作系统映像中。 |
PLATFORM |
存放了和硬件平台相关的BSP和驱动代码和文件 |
PRIVATE |
_PRIVATEROOT环境变量标识Private目录存放WINCE操作系统私有源代码。WINCE核心模块代码都放在此目录下。 |
PUBLIC |
包含硬件无关的Windows CE相关文件目录 |
SDK |
存放构建系统用到的编译器与其他一些辅助工具,在构建系统用_SDKROOT环境变量标识SDK目录。在/SDK/BIN/I386下存放构建系统可能用到的工具。而4个子目录ARM,MIPS,SH和X86分别针对WINCE所支持的4个平台的C/C++语言与汇编语言的编译器。 |
关于更详细的介绍,可以参考Platform Builder5.0的帮助文档。接下来详细介绍相关重要目录。
PLATFORM目录存放的是与平台相关的文件,除了COMMON子目录外,每一个子目录代表一个相应的平台。PLATFORM目录包含如表 3里面的子目录,具体目录取决于安装的Platform Builder所选的BSP,我们安装第三方提供的BSP包所生成的目录也将出现在PLATFORM目录里面。
表 3 PLATFORM目录
目录 |
内容说明 |
CEPC |
在PC机上运行Windows CE所需的BSP |
GEODE |
x86结构的AMD GEODE CPU开发板的BSP |
MAINSTONEII |
Intel MainstoneII硬件平台开发板的BSP |
SMDK2410 |
三星2410 ARM结构CPU的开发板的BSP |
COMMON |
多个BSP用到的公用代码 |
EMULATOR |
运行Windows CE模拟器所需的BSP |
MC9328MX1 |
Motorola开发板的BSP |
PRIVATE目录包含操作系统的源代码,在安装Windows CE5.0是需要选择Shared Source特征选项才会有这个目录。在构建系统中,Private目录由环境变量_PRIVATEROOT标识。但是这个目录中的代码没有完全开放,因此仅仅列出了一少部分。
表 4 PRIVATE目录
目录 |
内容说明 |
SERVERS |
Windows CE服务的源代码 |
SHELL |
Windows CE Shell组件 |
TEST |
一些Windows CE的测试代码 |
WCESHELLFE |
空目录 |
WINCEOS |
Windows CE操作系统的部分代码 |
Public目录包含操作系统中与硬件无关的组件和文件,包括各种工具,驱动和所有平台公共的组件,如表 5所示;
表 5 Public目录
目录 |
内容说明 |
CEBASE |
微软提供的一些设计模版,头文件,批处理文件,用来构建内核映像的时候使用 |
Common |
微软提供的与平台无关的通用模块,包括驱动程序,构建用的批处理与一些组件 |
DATASYNC |
在桌面Windows和Windows CE之间同步用的组件 |
DIRECTX |
DirectX相关的组件 |
GDIEX |
图像处理相关的组件,例如GIF,JPG等 |
IE |
IE浏览器相关的组件,有部分源代码。 |
NETCF |
.NET Compact Framework相关的组件 |
RDP |
远程桌面连接RDP相关的组件 |
SCRIPT |
Jscript和VBScript脚本引擎相关的组件 |
SERVERS |
网络相关的服务 |
SHELL |
Windows CE的Shell组件 |
SHELLSDK |
支持Pocket PC界面AygShell的库 |
SPEECH |
语音识别和朗读的SAPI组件 |
SQLCE |
SQL Server CE 2.0的二进制文件 |
VIEWERS |
微软的文件浏览器组件,包括PDF,Word,Excel等等的二进制文件,没有源代码 |
VOIP |
基于SIP标准的VoIP模块 |
WCEAPPSFE |
Windows CE的应用程序模块,包括WordPad,收件箱等 |
WCESHELLFE |
Windows CE的Shell应用模块:包括Dr Watson,任务管理器等 |
Public目录下的Common目录中含有非常多的内容。大致包含一些与平台无关的代码和工具,Common目录下有几个子目录值得我们关注[15]。
%_PUBLICROOT%/Common/OAK/Catalog目录
这个目录存放了与Platform Builder CEC相关的内容。Platform Builder中的CEC文件基本都存放在该目录下。
%_PUBLICROOT%/Common/OAK/Drivers目录
这个目录是所有的微软提供的外设驱动程序代码。代码是按照外设的种类存放的。在编写驱动程序的时候,这个目录的代码非常有参考价值。
%_PUBLICROOT%/Common/OAK/MISC目录
这个目录存放了在构建的时候用到的一系列批处理文件和其他工具。
%_PUBLICROOT%/Common/OAK/CSP目录
这个目录是CPU Support Package的存放目录,与某个CPU相关的通用代码都存放在该目录下。
Others目录包含Windows CE中一些模块的二进制库文件和代码。如果在PBWorkspaces中选择了某些组件,那么这些二进制代码就会被包含到最终的操作系统映像中,详细内容如表 6所示。
表 6 Others目录
目录 |
内容说明 |
WCETK |
Windows CE Test Kit的客户端(运行在CE上)程序 |
ATL |
ATL的头文件,库文件和源代码 |
MFC |
MFC的头文件,库文件和源代码 |
PLATMAN |
Platform Manager的客户端(运行在CE上)程序 |
SAMPLES |
MFC和ATL的示例代码 |
SQLCE20 |
Windows CE上支持SQLCE的库文件 |
EDB |
Windows CE上支持EDB的库文件 |
Windows CE5.0是一个组件化的操作系统,我们可以根据不同的应用需求来定制一些有针对性的平台,而一个功能往往需要多个组件才能够实现。Catalog就是实现某个功能的组件集合,每个Catalog Item会包含一个或多个组件。当我们的平台需要这个功能时,就将对应的Catalog item加入到平台中即可。Platform Builder5.0中Catalog窗口下的Core OS中可以清楚的看出Windows CE5.0的各个组件,如表 7详细说明;
表 7 内核组件
组件 |
内容说明 |
Applications-End User |
微软提供给用户的应用程序,如游戏、Word、Execl、PDF文件查看工具等 |
Applications and Services Development |
Windows CE5.0中可用来开发应用程序和服务的库和系统功能,如.NET Compact Framework2.0、C语言运行库、DCOM库 |
Communictions Services and NetWorking |
网络相关的特性,包括WAN、LAN、PAN上的一些协议的实现 |
Core OS Services |
操作系统的核心特性,包括电池驱动、电源管理、调试工具、消息队列、USB口支持、串口支持等 |
Devices Management |
设备管理特性,包括SNMP和设备管理客户端 |
File System and Data Store |
支持文件特性和数据存储选项,包括注册表、存储管理器等 |
Fonts |
各种可选的字体 |
Graphics and Multimedia technologies |
图形和多媒体支持,包括各种音频、视频组件 |
International |
国际化支持,包括各种语言输入法和MUI图形界面 |
Internet Client Services |
访问因特网组件,包括IE6、JS5.5和VBS5.5脚本等 |
Security |
安全性支持,各种用来认证、授权和加密的组件 |
Shell and User interface |
图形界面组件,例如各种风格的菜单和XP风格的皮肤 |
Voices over IP Phone Services |
VOIP相关组件,包括RTC协议的实现等 |
Windows CE Error Reporting |
Windows CE的错误报告组件 |
例如我们要添加Word软件,方便于查看Word文档,在Catalog窗体中“Applications-End User”目录下选择“Microsoft Word Viewer”点击右键选择“Add OS Design”即可,如图 5.2所示;
图 5.2 添加Word软件
介绍完Windows CE5.0目录和内核组件相关知识后,我们接下来介绍本系统中内核制定的一般流程,Windows CE5.0内核制定比较简单,熟悉Platform Builder5.0工具之后,按照一定流程制定即可,如下详细介绍。
(1) 打开Platform Builder5.0软件,新建相关项目,例如我们将项目命名为GPS2440A,如下图解。
图 5.3 新建项目
图 5.4 进入Step1
点击“Next>”,进入项目名字录入界面;
图 5.5 进入Step2
图 5.6 选择BSP包
图 5.7 进入项目模板选择
在这一步骤中,大家可以根据需要选择相应的项目模板,随后一直按照默认设计即可,最后完成创建步骤,进入Platform Builder5.0工作环境界面,如图 5.8所示。
图 5.8 工作环境
(2) 项目配置:新建项目完成之后,在编译之前我们需要对项目进行配置,如下图解。
图 5.9 进入项目设置页面
在弹出的“Platform Setting”页面中更改工程的属性。首先修改配置属性,将属性配置为All confirgurations,如图 5.10所示。
图 5.10 配置属性页
在“Platform Setting”页面中,选择Locales选项,更改工程的本地化属性,如所示。首先点击“Locales 列表框”右“Clear ALL”按钮清楚所有语言,然后选择“Locales 列表框”中的“中文(中国)”,并将Default locales中设置“中文(中国)”,在单击“应用”按钮,如图 5.11所示。
图 5.11 设置本地化属性
随后进入Build Options 页面,设置编译参数,去掉“Enable CE Target Control Support”、“Enable Full Kernel Mode”、“Enable Kernel Debugger”和“Enable KITL”四个选项,如图 5.12所示。
图 5.12 设置编译参数
(3) 添加组件:根据需要选择需要添加的组件,下面以添加Cab 安装包支持组件为例子显示,如图 5.13所示。
图 5.13 添加CAB组件
在制定中文显示的系统内核过程当中,有两个组件必须要添加的,如果不,则屏幕将出现乱码,这两个要添加的组件就是GB18030 Data Converter和SimSun & NSimSun (Subset 2_90),添加过程如图 5.14、图 5.15所示。
图 5.14 添加SimSun & NSimSun (Subset 2_90)
图 5.15 添加GB18030 Data Converter
(4) 编译内核:按照图 5.16所示,选择相关设置之后,点击“Build OS”——>“Build and Sysgen”即可,如果编译成功之后,调试窗口会显示“0 Error(s) X warning(s)”。
图 5.16 编译
经过这四个步骤,在“/PBWorkspaces/GPS2440A/RelDir/TQ2440_ARMV4I_
Release”目录下可以找到NK.BIN、EBOOT.nb0和STEPLDR.nb1文件。如下将介绍如下将系统下在到系统板子当中。
获取到NK.BIN、EBOOT.nb0和STEPLDR.nb1三个文件,接下来我们需要将这些内核文件下载到板子上。这一个过程,我们采用DNW软件和sscom32.exe配合进行,保证通过USB将内核文件下载到板子上,详细如下步骤;
(1) 安装相应USB驱动和DNW软件,安装USB驱动之后,运行DNW软件,软件界面如图 5.17所示,接下来设置COM参数,选择COM1口,设置波特率为115200,下载地址为0xc000000,如图 5.18所示。
图 5.17 DNW软件界面
图 5.18 DNW设置界面
(2) 将F_SEL设置为下载模式,启动终端,在sscom32.exe软件,选择“1”,开始通过DNW软件下载STEPLDR.nb1内核文件,待STEPLDR.nb1文件下载完毕时候,按照相同步骤下载EBOOT.nb0文件即可。
(3) 将F_SEL设置为启动模型,启动终端,在sscom32.exe软件,始终按住“SPACE”键,出现如图 5.19所示,在选择U,通过DNW软件下载NK.BIN文件。
图 5.19 下载NK.BIN文件
在基于ARM内核的嵌入式处理器的板级支持包中,BootLoader是系统在上电过程中要首先执行的第一段代码,虽然BootLoader不是系统在启动过程中所必需的,但是它的存在可以对嵌入式产品的开发和调试带来很多的方便,例如:每次对操作系统镜像进行修改以后,可以以太网,串口的硬件端口将镜像下载到目标嵌入式设备中,比起每次修改以后就要重新烧写Flash要简便得多[9]。
S3C2440A BootLoader的软件框架主要可以分为如下5个部分
(1) BLCOMMON:BootLoader通用代码库,主要完成boot过程中与硬件无关的操作。这一部分不需要用户自己开发,它主要是导出与硬件相关的硬件函数接口供OEM代码实现,BLOCMMON代码主要完成解析操作系统镜像文件,对下载的镜像数据执行数据校验,跟踪下载进度等
(2) EBoot 支持代码:主要负责实现与以太网相关的函数,同BLCOMMON代码一样,也是实现与硬件无关的操作,负责实现dhcp,tftp,udp等网络协议的,但仅适用于Eboot的情况,并且要导出与硬件相关的函数供用户实现。
(3) Ethdbg 驱动程序:严格来说,Ethdbg驱动程序并不是严格的驱动程序,它只用来实现基本的网络端口操作Ethdbg驱动程序不仅要为BootLoader服务,还要为OAL(OEM逻辑代码层)模块的KITL(内核无关传输层)服务。
(4) OEM代码:这是需要用户开发的代码核心,负责实现BLCOMMON导出的与硬件相关的函数。主要完成BootLoader的拷贝,硬件平台初始化(MMU的初始化,虚拟内存页表的设置,填充向OAL层传递共享信息的数据结构等)
(5) BootPart支持库:向BootLoader提供存储分区,底层读写,擦除,格式化等存储设备操作。
系统上电第一步就开始执行Startup函数,Eboot sources文件中有一项配置“EXEENTRY=StartUp”正是用于配置启动过程中运行入口函数StartUp。系统上电后第一步就是运行Startup函数的代码,这是一个汇编语言函数,主要其最主要功能是执行芯片级初始化:禁止中断,配置系统时钟频率,复制BootLoader镜像到内存,设置存储器的读写周期,构造内存映射表,启用MMU,并启用虚拟内存等操作。
Startup函数首先有两条重要的地址定义,定义ram空间的物理基地址和页表的基地址,这是startup函数主要操作的物理地址空间,如程序清单 5‑1所示。
程序清单 5‑1 定义RAM空间的物理基地址和页表的基地址
PHYBASE EQU 0x30000000 ; physical start
PTs EQU 0x30010000 ; 1st level page table address (PHYBASE + 0x10000)
; save room for interrupt vectors.
在进入入口函数以后首先通过对协处理器的操作来清空TLB、指令Cache和数据Cache如程序清单 5‑2所示。
程序清单 5‑2 清空TLB、指令Cache和数据Cache
mov r0, #0
mcr p15, 0, r0, c8, c7, 0 ; flush both TLB
mcr p15, 0, r0, c7, c5, 0 ; invalidate instruction cache
mcr p15, 0, r0, c7, c6, 0 ; invalidate data cache
然后向I/O端口F控制寄存器写入0x5555,使GPIO0到GPIO8全部设置为输出口,如程序清单 5‑3所示。
程序清单 5‑3 设置GPIO口输出
ldr r0, = GPBCON
ldr r1, = 0x055555
str r1, [r0]
在startup中将设置禁止中断,使系统进入轮询模式,这主要是为了保证BootLoader顺序执行,简化了开发和理解难度,虽然损失了效率,但是在BootLoader运行过程中仅仅是引导操作系统加载,不会用到很多的外设,很明显,这是利大于弊的,通过向INTMSK寄存器写入相应的位来禁止中断,除此之外还有INTSUBMSK(中断子屏蔽寄存器)及看门狗中断将所有的中断设置为外部中断(此时已经被屏蔽),如程序清单 5‑4所示。
程序清单 5‑4 关闭相关中断
ldr r0, = WTCON ; disable watch dog
ldr r1, = 0x0
str r1, [r0]
ldr r0, = INTMSK
ldr r1, = 0xffffffff ; disable all interrupts
str r1, [r0]
ldr r0, = INTSUBMSK
ldr r1, = 0x7fff ; disable all sub interrupt
str r1, [r0]
ldr r0, = INTMOD
mov r1, #0x0 ; set all interrupt as IRQ
str r1, [r0]
禁止所有中断后开始配置系统时钟,其中CLNDIVN(时钟分频寄存器)负责设置UCLK( USB总线时钟),FCLK与HCLK(AHB总线时钟)的比例,HCLK与PCLK(APB总线时钟的比例),如程序清单 5‑5所示;
程序清单 5‑5 设置时钟
ldr r0, = CLKDIVN
ldr r1, = 0x7 ; 0x0 = 1:1:1, 0x1 = 1:1:2, 0x2 = 1:2:2, 0x3 = 1:2:4,
; ldr r1, = 0x5 ; 0x0 = 1:1:1, 0x1 = 1:1:2, 0x2 = 1:2:2, 0x3 = 1:2:4,
; 0x7 = 1:3:6, 0x8 = 1:4:4
ldr r0, = MPLLCON ; Configure MPLL
; Fin=12MHz, Fout=400MHz
ldr r1, = PLLVAL
str r1, [r0]
ldr r0, = UPLLCON ; Fin=12MHz, Fout=48MHz
; ldr r1, = ((0x3c << 12) + (0x4 << 4) + 0x2) ; 16Mhz
ldr r1, = ((0x38 << 12) + (0x2 << 4) + 0x2) ; 12Mhz
str r1, [r0]
mov r0, #0x2000
20
subs r0, r0, #1
bne %B20
时钟设置之后,开始初始化内存控制器,为后面一系列的内存操作作准备:内存控制器的初始化其实就是设置访问的周期,访问字节宽度,访问中地址有效时间,等待时间,片选时长等,把要初始化的值建立成一张有序的内存控制表(MEMCTRLTAB),然后再存入相应的寄存器中会让汇编代码简洁,有较好的可读性,程序一目了然,如程序清单 5‑6所示;
程序清单 5‑6 初始化控制器
add r0, pc, #MEMCTRLTAB - (. + 8)
ldr r1, = BWSCON ; BWSCON Address
add r2, r0, #52 ; End address of MEMCTRLTAB
40 ldr r3, [r0], #4
str r3, [r1], #4
cmp r2, r0
bne %B40
ldr r0, = GPFDAT
mov r1, #0x60
str r1, [r0]
其中BSWCON就是存储器寄存器的开始地址,地址为0x48000000,后面的寄存器地址都是连续的4字节地址。这样内存空间就可以正常的访问了,前面的所有工作都是为了在内存空间内运行起来BootLoader作准备的,之后就是从Flash中拷贝BootLoader到Ram内存,然后使之运行起来了,当然,如果你的Flash足够大,能够让BootLoader流畅作XIP(execute in place)运行,也不一定要拷贝这个过程,在拷贝之前,还要检查当前是不是在Flash中运行,避免重复拷贝,检查代码如程序清单 5‑7所示;
程序清单 5‑7 检测代码
Ands r9 ,pc #0xff000000
Beq %F20
上述步骤完成之后,接下就是将BootLoader拷贝Ram中的过程,拷贝过程很简单,只需确定拷贝的源地址,目的地址,源地址可以确定就是从0地址,而目的地址却不能随意确定,需要有据可循,其依据就是全局内存映射表g_oalAddressTable和boot.bib文件中的定义,如程序清单 5‑8所示,拷贝代码如程序清单 5‑9 拷贝代码所示;
程序清单 5‑8 地址设置
EBOOT 8c038000 00040000 RAMIMAGE//指定的Eboot在Ram中的虚拟地址,
DCD 0x8C000000, 0x30000000, 32 ; 32 MB DRAM BANK 6
程序清单 5‑9 拷贝代码
ldr r0, = 0x38000 ; offset into the RAM
add r0, r0, #PHYBASE ; add physical base
mov r1, r0 ; (r1) copy destination
ldr r2, =0x0 ; (r2) flash started at physical address 0
ldr r3, =0x10000 ; counter (0x40000/4)
10 ldr r4, [r2], #4
str r4, [r1], #4
subs r3, r3, #1
bne %b10
完成拷贝之后就要开始运行BootLoader了,方法很简单,即将拷贝的物理地址赋值给pc。
Mov pc,r0
这意味BootLoader又要重新开始运行了,但这次启动不会重复复制BootLoader镜像,而是完成最重要的工作:构造页表并启动MMU,微软Windows CE的BootLoader所实现的功能与特性之一就是BootLoader的硬件初始化代码与OAL代码共享,而OAL作为Windows CE的开始是需要启用虚拟内存并为MMU设置正确的页表,还有一点,指导Windows CE的编译系统产生二进制文件的.bib文件使用的内存地址都是使用的虚拟地址,这些虚拟地址是编译系统对二进制代码进行地址重定位的重要依据,定义内存映射表的依据就是全局内存映射表(oemaddrtab_cfg.inc)。
INCLUDE oemaddrtab_cfg.inc
接下重新运行BootLoader,首先获取内存映射表的地址和页表要存储的地址,如程序清单 5‑10所示;
程序清单 5‑10 获得内存映射表的地址和页表要存储的地址
20 add r11, pc, #g_oalAddressTable - (. + 8)
ldr r10, =PTs ; (r10) = 1st level page table
构造“cachable”映射表,构造完成以后会以页表中的虚拟地址+20000000(也就是A0000000-BFFFFFFF)重新构造“uncachable”页表,但是所对应的物理地址是相同的,Cachable与uncachable页表的区别就是在页表所存储内容的低位有一个cachable位标志。
; (r10) = 1st level page table
; (r11) = ptr to MemoryMap array
BootLoader所使用的虚拟内存映射表是以1MB为单位构建,构建的虚拟地址为(0x80000000-0x9FFFFFFF)共512MB的地址,每个页表项占用4字节的空间,共2KB的地址空间,存储页表的地址为0x30012000-0x300127FF。
程序清单 5‑11 计算页表地址
add r10, r10, #0x2000 ; (r10) = ptr to 1st PTE for "unmapped space"
mov r0, #0x0E ; (r0) = PTE for 0: 1MB cachable bufferable
设置cachable位,每一个页表内容的高12(因为页表是以1MB为单位构建的)位是虚拟内存对应的物理内存段的起始地址,低20位用来存放相应虚拟及物理内存段的读写权限及可缓冲信息等属性信息。
程序清单 5‑12 设置cachable位
orr r0, r0, #0x400 ; set kernel r/w permission
25 mov r1, r11 ; (r1) = ptr to MemoryMap array
获取内存映射表的内容,如程序清单 5‑13所示。
程序清单 5‑13 获取内存映射表值
30 ldr r2, [r1], #4 ; (r2) = virtual address to map Bank at
ldr r3, [r1], #4 ; (r3) = physical address to map from
ldr r4, [r1], #4 ; (r4) = num MB to map
cmp r4, #0 ; End of table?
beq %f40
对虚拟地址进行512MB对齐,因为要映射到的虚拟地址对应的空间为0x80000000-0x9FFFFFFF,其大小不能超过512MB,r2的高14位用于指定虚拟地址对页表首地址的偏移量,同时还需对物理地址进行4GB对齐,具体操作如程序清单 5‑14所示。
程序清单 5‑14 地址对齐
ldr r5, =0x1FF00000
and r2, r2, r5 ; VA needs 512MB, 1MB aligned.
ldr r5, =0xFFF00000
and r3, r3, r5 ; PA needs 4GB, 1MB aligned.
接下来,开始构造uncachable页表,对应的虚拟地址均是cachable虚拟地址+20000000,物理地址不变,如程序清单 5‑15所示。
程序清单 5‑15 构造uncachable页表
40 tst r0, #8
bic r0, r0, #0x0C ; clear cachable & bufferable bits in PTE
add r10, r10, #0x0800 ; (r10) = ptr to 1st PTE for "unmapped uncached space"
bne %b25 ; go setup PTEs for uncached space
sub r10, r10, #0x3000 ; (r10) = restore address of 1st level page table
; Setup mmu to map (VA == 0) to (PA == 0x30000000).
ldr r0, =PTs ; PTE entry for VA = 0
ldr r1, =0x3000040E ; uncache/unbuffer/rw, PA base == 0x30000000
str r1, [r0]
; uncached area.
add r0, r0, #0x0800 ; PTE entry for VA = 0x0200.0000 , uncached
ldr r1, =0x30000402 ; uncache/unbuffer/rw, base == 0x30000000
str r1, [r0]
; Comment:
; The following loop is to direct map RAM VA == PA. i.e.
; VA == 0x30XXXXXX => PA == 0x30XXXXXX for S3C2400
; Fill in 8 entries to have a direct mapping for DRAM
接下来开始,建立虚拟地址到相等的物理地址的映射,如程序清单 5‑16所示。
程序清单 5‑16 建立虚拟地址到物理地址之间映射
ldr r10, =PTs ; restore address of 1st level page table
ldr r0, =PHYBASE
add r10, r10, #(0x3000 / 4) ; (r10) = ptr to 1st PTE for 0x30000000
add r0, r0, #0x1E ; 1MB cachable bufferable
orr r0, r0, #0x400 ; set kernel r/w permission
mov r1, #0
mov r3, #64
45 mov r2, r1 ; (r2) = virtual address to map Bank at
cmp r2, #0x20000000:SHR:BANK_SHIFT
add r2, r10, r2, LSL #BANK_SHIFT-18
strlo r0, [r2]
add r0, r0, #0x00100000 ; (r0) = PTE for next physical page
subs r3, r3, #1
add r1, r1, #1
bgt %b45
ldr r10, =PTs ; (r10) = restore address of 1st level page table
; The page tables and exception vectors are setup.
; Initialize the MMU and turn it on.
mov r1, #1
mcr p15, 0, r1, c3, c0, 0 ; setup access to domain 0
mcr p15, 0, r10, c2, c0, 0
mcr p15, 0, r0, c8, c7, 0 ; flush I+D TLBs
mov r1, #0x0071 ; Enable: MMU
orr r1, r1, #0x0004 ; Enable the cache
ldr r0, =VirtualStart
cmp r0, #0 ; make sure no stall on "mov pc,r0" below
mcr p15, 0, r1, c1, c0, 0
mov pc, r0 ; & jump to new virtual address
nop
; MMU & caches now enabled.
; (r10) = physcial address of 1st level page table
;
VirtualStart
mov sp, #0x80000000
add sp, sp, #0x30000 ; arbitrary initial super-page stack pointer
;;modified by huxiaohua
mrs r0,cpsr
bic r0,r0,#MODEMASK
orr r1,r0,#UNDEFMODE|NOINT
msr cpsr_cxsf,r1 ; UndefMode
ldr sp,=virtualUndefStack
orr r1,r0,#ABORTMODE|NOINT
msr cpsr_cxsf,r1 ; AbortMode
ldr sp,=VirtualAbortStack
orr r1,r0,#IRQMODE|NOINT
msr cpsr_cxsf,r1 ; IRQMode
ldr sp,=VirtualIRQStack
orr r1,r0,#FIQMODE|NOINT
msr cpsr_cxsf,r1 ; FIQMode
ldr sp,=VirtualFIQStack
mrs r0, cpsr
bic r0, r0, #MODEMASK|NOINT
orr r1, r0, #SVCMODE
msr cpsr_cxsf, r1 ; SVCMode.
ldr sp, =VirtualSVCStack
跳转到BootLoader的main函数,并结束StartUp结束,如程序清单 5‑17所示。
程序清单 5‑17 结束Startup
b main
ENTRY_END
LTORG
不管是制定Windows CE5.0内核,还是编写或修改Windows CE BSP,了解Windows CE5.0都是非常重要,Windows CE5.0的启动过程如图 5.20所示,接下来详细介绍Windows CE5.0这几个启动过程。
图 5.20 Windows CE5.0启动过程
(1) startup.s
kernel通过“EXEENTRY=StartUp”进入执行startup.s中汇编代码,startup.S的任务比较简单,只是将oemaddrtab_cfg.inc里面的g_oalAddressTable数组地址作为参数,传递给KernelStart,这个数组用来描述和实现物理地址到虚拟地址的映射,如程序清单 5‑18所示。
程序清单 5‑18 导入kernelStart
SleepState_WakeAddr EQU (SleepState_Data_Start )
SleepState_MMUCTL EQU (SleepState_WakeAddr + WORD_SIZE )
SleepState_MMUTTB EQU (SleepState_MMUCTL + WORD_SIZE )
SleepState_MMUDOMAIN EQU (SleepState_MMUTTB + WORD_SIZE )
SleepState_SVC_SP EQU (SleepState_MMUDOMAIN + WORD_SIZE )
SleepState_SVC_SPSR EQU (SleepState_SVC_SP + WORD_SIZE )
SleepState_FIQ_SPSR EQU (SleepState_SVC_SPSR + WORD_SIZE )
SleepState_FIQ_R8 EQU (SleepState_FIQ_SPSR + WORD_SIZE )
SleepState_FIQ_R9 EQU (SleepState_FIQ_R8 + WORD_SIZE )
SleepState_FIQ_R10 EQU (SleepState_FIQ_R9 + WORD_SIZE )
SleepState_FIQ_R11 EQU (SleepState_FIQ_R10 + WORD_SIZE )
SleepState_FIQ_R12 EQU (SleepState_FIQ_R11 + WORD_SIZE )
SleepState_FIQ_SP EQU (SleepState_FIQ_R12 + WORD_SIZE )
SleepState_FIQ_LR EQU (SleepState_FIQ_SP + WORD_SIZE )
SleepState_ABT_SPSR EQU (SleepState_FIQ_LR + WORD_SIZE )
SleepState_ABT_SP EQU (SleepState_ABT_SPSR + WORD_SIZE )
SleepState_ABT_LR EQU (SleepState_ABT_SP + WORD_SIZE )
SleepState_IRQ_SPSR EQU (SleepState_ABT_LR + WORD_SIZE )
SleepState_IRQ_SP EQU (SleepState_IRQ_SPSR + WORD_SIZE )
SleepState_IRQ_LR EQU (SleepState_IRQ_SP + WORD_SIZE )
SleepState_UND_SPSR EQU (SleepState_IRQ_LR + WORD_SIZE )
SleepState_UND_SP EQU (SleepState_UND_SPSR + WORD_SIZE )
SleepState_UND_LR EQU (SleepState_UND_SP + WORD_SIZE )
SleepState_SYS_SP EQU (SleepState_UND_LR + WORD_SIZE )
SleepState_SYS_LR EQU (SleepState_SYS_SP + WORD_SIZE )
SleepState_Data_End EQU (SleepState_SYS_LR + WORD_SIZE )
SLEEPDATA_SIZE EQU ((SleepState_Data_End - SleepState_Data_Start) / 4)
IMPORT KernelStart
(2) KernelStart函数
该函数主要完成以下工作,详细代码可以参考$(_PRIVATEROOT)WINCEOS/COREOS/NK/LDR/ARM/armstart.s所示。
l 完成OEMAddressTable表中的物理地址到虚拟地址和虚拟地址到物理地址之间的映射。
l 对存储器页表和内核参数区存储空间(RAM或DRAM)进行清零处理。
l 读出CPU的ID号,内核需要根据该ID决定ARM的MMU处理,因为ARMV6和ARMV6之前的ARM处理器的MMU处理过程有所区别。
l 设置并开启MMU和Cache,因为在Startup函数关闭MMU和Cache。
l 设置ARM处理器工作模式的SP指针,ARM处理器共用7种不同的工作模式(USER、FIQ、IRQ、Supervisor、Abort、 Undefined、System),除用户模式(USER)和系统模式(System)之外,其他5种工作模式都有具有特定的SP指针寄存器(ARM处理器称其为影子寄存器)。
l 读取内核启动所需要的KDataStruct结构体。
l 调用ARMInit函数重新定位Windows CE内核参数pTOC和初始化OEMInitGlobals全局变量。
l 利用mov pc, r12指令跳转到kernel.dll的入口位置,即NKStartup函数中。
(3) ARMInit函数
在ARMInit之前,系统仍无法使用全局变量,因为系统的全局还在ROM区域,对于操作系统而言,出于安全考虑,只有XIP程序才有读取ROM区域数据的权利,对于大部分Windows CE 操作系统,只有将数据拷贝到RAM区域后才能进行读写,ARMInit函数中通过调用KernelRelocate函数对pTOC全局变量重新定位,定位之后,对内核启动所需要的KDataStruct结构体进行初始化,其中OEMInitGlobals便是交换oal.exe和kernel.dll之间的全局指针,ARMInit函数返回kernel.dll的入口位置。并在KernelStart函数最后利用mov pc, r12指令跳转到kernel.dll的入口位置,即NKStartup函数中。
(4) OALIntrInit
调用OALIntrMapInit()初始化2个数组g_oalSysIntr2Irq,g_oalIrq2SysIntr,这2个数组表征irq和逻辑中断SysIntr的映射关系。然后初始化中断寄存器,最后、留一个接口给oem。
(5) KernelInit()
该函数主要完成在启动第一个线程前对内核进行初始化,主要包括API函数集初始化、堆的初始化、初始化内存池、进程初始化、线程初始化和文件映射初始化等操作,如程序清单 5‑19所示。
程序清单 5‑19 KernelInit函数
void KernelInit (void)
{
#ifdef DEBUG
g_pNKGlobal->pfnWriteDebugString (TEXT("Windows CE KernelInit/r/n"));
#endif
APICallInit ();// setup API set
HeapInit ();// setup kernel heap
InitMemoryPool ();// setup physical memory
PROCInit ();// initialize process
VMInit (g_pprcNK);// setup VM for kernel
THRDInit ();// initialize threadsv
MapfileInit ();
#ifdef DEBUG
g_pNKGlobal->pfnWriteDebugString (TEXT("Scheduling the first thread./r/n"));
#endif
}
(6) FirstSchedule
FirstSchedule函数为Windows CE操作系统启动过程中最后无条件跳转的一个函数,windows CE进行第一个调度,实际为一个空闲线程,因为windows CE系统还没有完成启动,只有当windows CE完全启动并进入稳定状态,然后启动文件系统filesys.dll,设备管理device.dll,窗体图像子系统gews.dll和shell程序 Explore.exe。
驱动程序是操作系统和外设设备进行交互的桥梁,正是由于驱动程序的存在,操作系统和应用程序的移植性得到了很大地增强,系统的灵活性也得到了很大地提高。接下来讲述本系统中的相关设备驱动开发,并在本章的第5节中将介绍一个简单的流接口驱动程序帮助理解本系统的流接口驱动程序开发方法。
对于有Windows 桌面PC驱动编程经验的开发人员来说,WDM(Windows Driver Model)驱动程序模式在熟悉不过了。在WDM驱动程序模型中,每一个硬件设备至少有两个驱动程序[2]。其中一个是驱动程序即我们称之为功能驱动,通常它就是你认为的那个硬件设备驱动程序。它了解使硬件工作的所有细节,负责初始化I/O操作。另一个驱动程序即我们称之为总线驱动,负责管理硬件和计算机的连接。WDM驱动程序模型为PC驱动程序的开发提供了便利,减轻了驱动程序人员的负担。
同样,在Windows CE5.0也提供了两种驱动程序模型,分别是本机设备驱动程序模型和流接口驱动程序模型。在编写Windows CE5.0下的驱动程序时,只要按照微软提供的驱动程序模型来开发驱动程序,那么开发人员所做的工作就会大大的减少,所开发的驱动程序也更容易移植。各种常见的设备驱动对应的驱动程序模型如图 5.21所示。
图 5.21 常见驱动模型
在图 5.21中,NDIS驱动程序模型和USB驱动程序模型来自于其它操作系统。本机设备驱动程序是平台建立时必须提供的驱动程序。通常这类驱动由GEWS统一管理和加载。因为本机设备驱动程序与操作系统有紧密的关系,所以微软采用定制接口的方式来支持内部设备驱动程序,大多数的开发人员不需要编写本机设备驱动程序。
另外一种是流接口驱动程序模型。顾名思义,这是一类与流接口相关的驱动程序。在Windows CE中,流接口是一组操作系统定义的函数,是对具有“数据流”属性设备的一种抽象。通常,流接口驱动程序会实现这些接口供设备管理器调用。应用程序如果要访问流接口驱动程序,一般需要通过文件系统进行访问。
在Windows CE中,无论是哪种驱动程序模型,驱动程序的物理表现均为一个DLL文件。DLL文件是一种动态链接库(Dynamic Link Library)文件。从微软公司推出第一个版本的Windows 操作系统以来,动态链接库就一直是Windows操作系统的基础。Windows CE也比例外,它的核心部分就是三个DLL文件:Kernel.dll、Filesys.dll和Device.dll。
在Windows CE5.0驱动程序中,包含两种类型:分层驱动程序和单体驱动程序。有时候又把分层驱动程序的扩展驱动程序称之为另一种类型——混合驱动程序。
驱动程序模型和驱动体系结构有什么关系呢?从根本上说,这只是从不同的角度对驱动程序分类的方法。驱动程序模型是从驱动程序和其他模块的接口形式上做的分类,比如由GWES调用的模块为本机设备驱动程序,由设备管理器调用的模块为流接口驱动程序。而驱动程序体系接口是从代码层面上做的分类,也就是说不管驱动模型如何,但代码层面上都可以按照体系结构进行划分。下面详细介绍这三种体系结构。
分层驱动程序是比较常见的驱动程序结构,通常被称作MDD/PDD结构。这个结构中,驱动程序被分成两层,MDD(Model Device Driver)层和PDD(Platform Dependence Driver)层。虽然在结构上我们把驱动程序分成两部分,但最终生成的可执行文件上和普通驱动程序没有什么区别,都是DLL文件。分层驱动程序的结构如图 5.22所示。
图 5.22 分层驱动程序
MDD层包含平台无关的代码。它通常实现一些操作系统预先定义的接口,来完成某一类设备的通用功能。在操作系统与MDD层之间通过DDI(设备驱动接口)进行交互。同样在MDD层也定义一些与PDD层进行交互的接口函数,这些接口函数成为DDSI(设备驱动服务接口)。
MDD层通常有如下的特性;
l 包含某一类驱动程序所有通用的代码。
l 调用PDD层函数访问硬件设备。
l 与PDD代码链接,定义PDD层必须实现的DDSI函数,并在代码中调用这些函数。
l 实现DDI函数,供操作系统调用。
l 进行中断处理。
l 代码具有可重用性。
l 可以与多个PDD层连接。
l 通常无须改动。
l 中断服务线程IST通常位于这一层。
PDD层包含与硬件相关的代码,通常具有如下特性;
l 当硬件平台改变时,通常需要更改代码。
l 只能与某一类MDD协同工作。
l 实现MDD层所需要的DDSI函数。
分层的驱动程序简化了驱动程序编写者的工作。开发人员只需要编写PDD层,并与MDD层代码进行链接即可完成驱动程序的编写,当需要移植驱动程序时,MDD层的代码也不需要更改,只需更改PDD层代码即可,非常有利于驱动程序的移植。
单体驱动程序是指没有代码上的明显层次划分的驱动程序。这类驱动程序的所有代码集中在一个代码片内,包括中断处理、I/O操作及硬件控制等。与分层驱动程序不同的是单体驱动程序通常只实现DDI接口,并实现DDSI接口。单体驱动程序的结构如图 5.23所示。
图 5.23 单体驱动程序
因为单体驱动程序在代码上是一个整体,结构相对紧凑,对与一些效率较高的场合,选用单体驱动程序有可能提高驱动程序的性能。在以下情况下可以悬着单体驱动程序结构。
(1) Windows CE没有提供分层驱动程序的实现,这种情况下开发者可以自己按照分层驱动程序的特性来开发,以方便驱动程序移植。
(2) 硬件设备上已经完成部分MDD功能,为了充分利用硬件的性能,可以采用单体驱动程序结构。
(3) 使用分层驱动程序结构不能达到性能上的要求。
相对分层驱动程序,单体驱动程序开发起来较为复杂,可重用性大大降低,所以为软件开发人员在开发驱动程序时尽量选用分层驱动程序,只有在性能特别重要或者分层的驱动不能发挥硬件的性能时才会选用单体驱动程序。
在单体驱动程序和分层驱动程序之间进行选择是驱动程序开发过程中平衡效率和可移植性的例子。虽然微软建议开发者要更多考虑可移植性,但在实际开发过程当中很多人会基于效率或者其他方面的考虑而选择单体驱动程序。但建议即使采用单体驱动程序结构进行驱动开发,也要遵循“高内聚,低耦合”软件开发方法,尽量将硬件操作的代码进行分离,提高程序的可移植性。
混合驱动程序第三种驱动程序结构,它往往并不在微软的帮助文档中出现,一个最要的原因是微软不推荐这样去做。这里进行简单的介绍主要是因为这种驱动程序结构在一些特殊场合确实有用武之地。
分层驱动程序尽管有很多好处,但分层驱动也不能满足所有的需求。但现在的分层驱动程序结构不能满足需求时,例如设备具体扩展功能,而开发者又不想从头开始编写驱动程序,就可以在现有MDD/PDD结构上进行扩展,称这一类驱动程序为混合驱动程序,如图 5.24所示。
图 5.24 混合驱动模型
混合驱动程序结构是在扩展现有分层驱动程序的一个方法,但这种扩展是在牺牲驱动程序可移植性基础上获得,除非很有必要,一般不推荐使用此结构。
流接口驱动程序是驱动开发模型中的一种,通过前面的介绍,相信大家对其已经有一定的了解。先介绍本系统其他关键驱动,如USB驱动、串口驱动之前,有必要介绍一下流驱动程序的开发方法。
在Windows CE5.0中流接口驱动程序是工作在用户模式下,应用程序可以通过ActivateDeviceEx和DeactivateDevice对其进行加载和卸载。当驱动程序加载完成之后,用户程序可以像操作文件一样操作它,同样采用CreateFile、ReadFile、WriteFile、SetFilePointer、DeviceIoControl和CloseFile函数即可,流接口驱动程序的体系结构如图 5.25所示。
图 5.25 流接口驱动程序的体系结构
另外在流接口驱动程序中如果涉及到中断操作,那么流接口驱动程序还需要实现中断服务线程。在中断产生时,终端服务线程得到执行,并对中断进行处理,中断完成之后调用InterruptDone函数来通知系统中断完成。
正如上述,流接口驱动程序是从系统导出函数的,系统规定流接口驱动程序必须实现表 8所示函数[4]。
表 8 流接口函数
流接口函数 |
描述 |
XXX_Init |
当设备管理器初始化一个具体的设备时调用这个函数 |
XXX_Deinit |
当设备管理器卸载一个驱动程序时调用这个函数 |
XXX_PreDeinit |
释放设备,在设备被卸载时调用 |
XXX_Open |
打开设备驱动程序 |
XXX_Close |
关闭设备驱动程序 |
XXX_PreClose |
通知设备管理器把它打开的句柄设置为无效,并唤醒任何正在休眠的线程 |
XXX_Read |
从设备中读取数据 |
XXX_Write |
向设备中写入数据 |
XXX_Seek |
该函数移动设备的数据指针 |
XXX_PowerUp |
该函数恢复对设备供电 |
XXX_PowerDown |
该函数停止对设备供电 |
XXX_IOControl |
该函数发出命令给客户 |
在表中XXX代表设备名称的前缀,通常是三个大写的字母,例如STR_Init。如果驱动程序在注册表中Flags键指定为0x08,那么驱动程序的实现就不需要XXX前缀,设备管理去会自动寻找没有前缀的接口函数,比如Init、Open等。如下对这些流接口函数进行详细的介绍。
(1) DWORD XXX_Init(LPCTSTR pContext,LPCVOID lpvBusContext);
它是驱动程序的动态库被成功装载以后第一个被调用的函数。其调用时间仅次与DllEntry,而且,当一个库用来生成多于一个的驱动程序实例时仅调用一次DllEntry,而xxx_Init会被调用多次。驱动程序应当在这个函数中初始化硬件,如果初始化成功,就分配一个自已的内存空间(通常用结构体表示),将自已的状态保存起来,并且将此内存块的地址做为一个DWORD值返回给上层。设备管理器就会用在调用XXX_Open时将此句柄传回,我们就能访问自已的状态。如果初始化失败,则返回0以通知这个驱动程序没有成功加载,先前所分配的系统资源应该全部释放,此程序的生命即告终至。
当这个函数成功返回,设备管理器对这个程序就不做进一步处理,除非它设置了更多的特性。至此一个各为XXX的设备就已经加载成功,当用户程序调用CreateFile来打开这个设备时,设备管理器就会调XXX_Open函数,XXX_Init函数参数如下;
l pContext:系统传入的注册表键,通过它可以讲到我们在注册表中设置的配置信息
l lpvBusContext:一般不用,在这先不做讲解
(2) DWORD XXX_Open(DWORD hDeviceContext,DWORD dwAccess, DWORD dwShareMode);
当用户程序调用CreateFile打开这个设备时,设备管理器就会调用此驱动程序的XXX_Open函数,该函数参数如下;
l hDeviceContext:XXX_Init 返回给上层的值,也就是我们在XXX_Init中分配的用来记录驱动程序信息的那个结构体的指针,我们可以在这个函数中直接将其转化成所定义的结构,从而获取驱动程序的信息。
l dwAccess:上层所要求的访问方式,可以是读或者写,或者是0,即不读也不写
l dwShareMode:上层程序所请求的共享模式,可以是共享读、共享写这两个值的逻辑或,或者是0,即独占式访问。
系统层对设备文件的存取权限及共享方法已经做了处理,所以在驱动程序中对这两个参数一般可以不用理会。这个函数一般不用做太多处理,可以直接返回hDeviceContext表示成功,对于一个不支持多个用户的硬件,在设备已经打开后,应该总是返回0以至失败,则CreateFile调用不成功。
(3) DWORD XXX_Close(DWORD hDeviceContext);
当用户程序调用CloseHandle关闭这个设备句柄时,这个函数就会被设备管理器调用,该函数参数如下;
l hDeviceContext:为XXX_Open返回给上层的那个值。
这个函数应该做与XXX_Open相反的事情,具体包括:释放XXX_Open分配的内存,将驱动程序被打开的记数减少等。
(4) DWORD XXX_Deinit(DWORD hDeviceContext);
这个函数在设备被卸载时被调用,它应该实现与XXX_Init相反的操作,主要为释放前者占用的所有系统资源,该参数如下;
l hDeviceContext XXX_Init函数返回给上层的那个句柄
(5) void XXX_PowerUp( DWORD hDeviceContext );
(6) void XXX_PowerDown(DWORD hDeviceContext );
正如其名称中体现的那样,这两个函数在系统PowerUp与PowerDown时被调用,这两个函数中不能使用任何可能引起线程切换的函数,否则会引起系统死机。所以,在这两个函数中,实际上几乎是什么做不了,一般在PowerDown时做一个标志,让驱动程序知道自已曾经被Power Down过。在Power Down/On的过程中硬件可能会掉电,所以,尽管Power On以后,原来的IO操作仍然会从接着执行,但可能会失败。这时,当我们发现一次IO操作失败是因为程序曾经进入过PowerDown状态,就重新初始化一次硬件,再做同样的IO操作。
(7) BOOL XXX_IOControl(DWORD hDeviceContext,
DWORD dwCode,
PBYTE pBufIn,
DWORD dwLenIn,
PBYTE pBufOut,
DWORD dwLenOut,
PDWORD pdwActualOut);
几乎可以说一个驱动程序的所有功能都可以在这个函数中实现。对于一类CE自身已经支持的设备,它们已经被定义了一套IO操作定,我们只需按照各类设备已经定义的内容去实现所有的IO操作。但当我们实现一个自定义的设备时,我们就可以随心所欲定义我们自已的IO操作,该函数参数如下;
l hDeviceContext:XXX_Open返回给上层的那个句柄,即我们自已定义的,用来存放程序所有信息的一个结构。
l dwCode: IO操作码,如果是CE已经支持的设备类,就用它已经定义好码值,否则就可以自已定义。
l pBufIn:传入的Buffer,每个IO操作码都会定义自已的Buffer结构
l dwLenIn: pBufIn以字节记的大小
l pBufOut,dwLenOut分别为传出的Buffer,及其以字节记的大小。
l pdwActualOut:驱动程序实际在pBufOut中填入的数据以字节记的大小。
其中,前两个参数是必须的,其它的任何一个都有可能是NULL或0。所以,当给pdwActualOut赋值时应该先判断它是否为一个有效的地址
MMU启动之后,CPU就不能够直接访问物理地址了。这时需要一个物理地址到虚拟地址的映射。虚拟地址的映射包括两种:静态映射和动态映射。
在OAL中有一个OEMAddressTable表,来完成虚拟地址的静态映射。也可以在系统启动之后,通过CreateStaticMapping和NKCreateStaticMapping函数扩展,但由这两个函数所完成的映射只能由内核访问。
OEMAddressTable表在BSP中可以搜索到。
_OEMAddressTable:
; OEMAddressTable defines the mapping between Physical and Virtual Address
; o MUST be in a READONLY Section
; o First Entry MUST be RAM, mapping from 0x80000000 -> 0x00000000
; o each entry is of the format ( VA, PA, cbSize )
; o cbSize must be multiple of 4M
; o last entry must be (0, 0, 0)
; o must have at least one non-zero entry
; RAM 0x80000000 -> 0x00000000, size 64M
dd 80000000h, 0, 04000000h
; FLASH and other memory, if any
; dd FlashVA, FlashPA, FlashSize
; Last entry, all zeros
dd 0, 0, 0
“dd 80000000h, 0, 04000000h”,将64MB(04000000h)的物理地址空间映射到从0x80000000开始的虚拟地址空间。物理地址从0开始。
通常OEMAddressTable完成的映射是基于Cached的。除此之外,操作系统还要完成一个Uncached的映射。访问物理寄存器的时候,虚拟地址空间一定要用基于Uncached的。一般的虚拟地址空间是0x8000 0000 – 0xA000 0000是基于Cached的,0xA000 0000 – 0xC000 0000是基于Uncached的。
在Windows CE5.0驱动中,常常需要访问内存地址、物理地址,对于这些操作不得不引入动态虚拟地址映射,动态映射方便于根据特定需求动态分配内存空间,并实现虚拟地址到物理地址的实际访问。动态虚拟地址映射操作函数主要包括VirtualAlloc、VirtualCopy、VirtualFree三个函数。其中VirtualAlloc作用是在用户进程空间中申请一块虚拟地址空间,用来映射物理地址,VirtualAlloc原型如程序清单 5‑20所示;
程序清单 5‑20 VirtualAlloc函数
LPVOID VirtualAlloc(
LPVOID lpAddress,
DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect
);
VirtualCopy函数作用是将物理地址绑定到由VirtualAlloc申请的虚拟地址空间,其原型如程序清单 5‑21所示。
程序清单 5‑21 VirtualCopy函数
BOOL VirtualCopy(
LPVOID lpvDest,
LPVOID lpvSrc,
DWORD cbSize,
DWORD fdwProtect
);
VirtualFree作用是取消虚拟地址与物理地址的映射,同时释放VirtualAlloc所申请的虚拟地址空间,其原型如程序清单 5‑22所示。
程序清单 5‑22 VirtualFree函数
BOOL VirtualFree(
LPVOID lpAddress,
DWORD dwSize,
DWORD dwFreeType
);
在Windows CE 5.0中,流驱动的加载有三种方式,其中两种涉及设备管理器,一种涉及应用程序。相应的流驱动的卸载有就存在两种方式,分别针对设备管理器和应用程序。
在Windows CE 5.0中第一种加载流驱动程序的方式是在启动的时候进行加载。也就是说,当Windows CE 5.0启动的时候,设备管理器随即启动,设备管理器会读取注册表中的HKEY_LOCAL_MACHINE/Drivers/BuiltIn键下的内容并加载已经列出的流驱动程序。比如在很多Windows CE 5.0系统中,设备管理器就是通过这个机制进行内部串口驱动程序的加载。
在Windows CE 5.0中,第二种加载流驱动的方式就是在设备管理器自动检测外围设备与系统连接的时候进行的。比如PC卡的自动检测等。
第三种加载流驱动程序的方式是应用程序在需要的时候调用ActivateDevice(EX)函数加载流驱动。ActivateDevice(EX)函数会根据指定的注册表中的键值,加载流驱动程序。该注册表键应该包括如下四个子键:
l DLL:包含流驱动程序的名字。
l Prefix:流驱动程序前缀。
l Order:当存在多个驱动时,该驱动的加载顺序
l Index:该驱动的索引号
在Windows CE 5.0中,我们可以根据设备管理器和驱动程序两种加载方式来决定流驱动程序的卸载方式。
l 设备管理器加载:当设备管理器加载了流驱动程序以后,设备管理器就会检测该驱动程序是否已经和Windows CE 5.0平台断开连接。如果断开,设备管理器就会从HKEY_LOCAL_MACHINE/Drivers/Active下删除驱动程序项,将该驱动从文件系统中删除,并将驱动的DLL从设备管理器的进程空间中释放。在这个过程中设备管理器需要调用DeactivateDevice函数。
l 应用程序加载:当流驱动程序是应用程序加载时,应用程序就必须通过调用DeactiveDevice函数来卸载该驱动。
为了提高系统的硬件可控制性和可扩展性,在硬件设计的时候,增加了GPIO模块。光有硬件,应用程序如何操作它则需要借助GPIO驱动了,GPIO驱动实现其实很简单,就是一个简单的流接口驱动程序,GPIO驱动结构及处理流程如图 5.26所示,相关实现过程随后继续介绍。
图 5.26 GPIO驱动结构及处理流程
S3C2440A_IOPORT_REG结构体客观的定义了S3C2440A芯片GPIO结构,只需要对其赋予初始地址即可访问GPIO地址中的相关数据,并控制GPIO,该结构的原型如程序清单 5‑23所示。
程序清单 5‑23 S3C2440A_IOPORT_REG结构体
typedef struct {
UINT32 GPACON; // Port A - offset 0
UINT32 GPADAT; // Data
UINT32 PAD1[2];
UINT32 GPBCON; // Port B - offset 0x10
UINT32 GPBDAT; // Data
UINT32 GPBUP; // Pull-up disable
UINT32 PAD2;
......
} S3C2440A_IOPORT_REG, *PS3C2440A_IOPORT_REG;
GIO_Init函数用于初始化“volatile S3C2440A_IOPORT_REG *v_pIOPregs ;”中定义的v_pIOPregs结构体对象,并进行相关设置,代码如程序清单 5‑24所示。
程序清单 5‑24 GIO_Init函数
DWORD GIO_Init(DWORD dwContext)
{
RETAILMSG(1,(TEXT("GPIO Initialize ...")));
if (!InitializeAddresses())
return (FALSE);
v_pIOPregs->GPBCON = (v_pIOPregs->GPBCON &~(3 << 10)) | (1<< 10); // GPB5 == OUTPUT.
v_pIOPregs->GPBCON = (v_pIOPregs->GPBCON &~(3 << 12)) | (1<< 12); // GPB6 == OUTPUT.
v_pIOPregs->GPBCON = (v_pIOPregs->GPBCON &~(3 << 14)) | (1<< 14); // GPB7 == OUTPUT.
v_pIOPregs->GPBCON = (v_pIOPregs->GPBCON &~(3 << 16)) | (1<< 16); // GPB8 == OUTPUT.
mInitialized = TRUE;
RETAILMSG(1,(TEXT("OK !!!/n")));
return TRUE;
}
这个函数中,真实实现地址初始化的是InitializeAddresses,InitializeAddresses函数调用的目的是通过VirtualAlloc、VirtualCopy、VirtualFree三个函数实现地址的动态映射,代码如程序清单 5‑25所示;
程序清单 5‑25 InitializeAddress函数
bool InitializeAddresses(VOID)
{
........
v_pIOPregs = (volatile S3C2440A_IOPORT_REG *)VirtualAlloc(0, sizeof(S3C2440A_IOPORT_REG), MEM_RESERVE, PAGE_NOACCESS);
......
if (!VirtualCopy((PVOID)v_pIOPregs, (PVOID)(S3C2440A_BASE_REG_PA_IOPORT >> 8), sizeof(S3C2440A_IOPORT_REG), PAGE_PHYSICAL | PAGE_READWRITE | PAGE_NOCACHE))
......
......
VirtualFree((PVOID) v_pIOPregs, 0, MEM_RELEASE);
......
}
真正对GPIO进行实质性的操作就在GIO_IOControl这个函数里面发生,操作命令如程序清单 5‑26所示,具体实现代码如程序清单 5‑27所示。
程序清单 5‑26 控制命令
#define IO_CTL_GPIO_1_ON 0x01 // 将B对GPIO1口设置为输入
#define IO_CTL_GPIO_2_ON 0x02 // 将B对GPIO2口设置为输入
#define IO_CTL_GPIO_3_ON 0x03 // 将B对GPIO3口设置为输入
#define IO_CTL_GPIO_4_ON 0x04 // 将B对GPIO4口设置为输入
#define IO_CTL_GPIO_ALL_ON 0x05 // 将B对所有GPIO口设置为1
#define IO_CTL_GPIO_1_OFF 0x06 // 将B对GPIO1口设置0
#define IO_CTL_GPIO_2_OFF 0x07 // 将B对GPIO2口设置0
#define IO_CTL_GPIO_3_OFF 0x08 // 将B对GPIO3口设置0
#define IO_CTL_GPIO_4_OFF 0x09 // 将B对GPIO4口设置0
#define IO_CTL_GPIO_ALL_OFF 0x0a // 将B对所有GPIO口设置0
程序清单 5‑27 GIO_IOControl函数
BOOL GIO_IOControl(DWORD hOpenContext,
DWORD dwCode,
PBYTE pBufIn,
DWORD dwLenIn,
PBYTE pBufOut,
DWORD dwLenOut,
PDWORD pdwActualOut)
{
switch(dwCode)
{
case IO_CTL_GPIO_1_ON:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT&~(0x1<<5);
break;
case IO_CTL_GPIO_2_ON:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT&~(0x1<<6);
break;
case IO_CTL_GPIO_3_ON:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT&~(0x1<<7);
break;
........
case IO_CTL_GPIO_ALL_ON:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT&~(0xF<<5);
break;
case IO_CTL_GPIO_1_OFF:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT|(0x1<<5);
break;
case IO_CTL_GPIO_2_OFF:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT|(0x1<<6);
break;
........
case IO_CTL_GPIO_ALL_OFF:
v_pIOPregs->GPBDAT=v_pIOPregs->GPBDAT|(0xF<<5);
break;
default:
break;
}
RETAILMSG(1,(TEXT("GPIO_Control:Ioctl code = 0x%x/r/n"), dwCode));
return TRUE;
}
相比GPIO驱动,USB驱动程序的设计过程就比较复杂了,不管要了解USB协议还需要连接USB驱动程序框架,接下我简要介绍下本系统中USB驱动的设计方法和经验总结。
在嵌入式设备中,广泛应用的操作系统主要有嵌入式Linux、Windows CE、VxWroks,Symbian等。Windows CE以良好的实时性、强大的通信能力、高度模块化、操作的简洁性等特性,拥有很高的市场占有率。
USB总线从诞生起便引发了一场产业革命。它以灵活、方便、应用范围广、通信稳定、成本低廉等优点,使得PC的接口纷纷从串行口和并行口转到USB总线上来。并且这种趋势正在向嵌入式设备上发展,如今很多嵌入式设备都携带了miniUSB总线接口。与此同时,Windows CE对USB 2.0协议的支持,更是为USB的普及推澜助兴。
要使设备能够通过USB通信,驱动程序是必不可少的。在Windows CE中已经集成了USB驱动和多种USB主机控制器驱动程序,所以要在Windows CE设备上实现USB通信,需要做的就是编写USB Host Client驱动程序,由于本系统中的GPS和3G设备都是USB接口的,编写USB Host Client驱动程序本系统中的关键,接下来分别介绍USB相关技术,包括协议、描述等方面的知识,然后再介绍Windows CE下USB驱动的编写方法。
USB 是一种支持热插拔的高速串行传输总线,它使用差分信号来传输数据,最高速度可达480Mb/S。USB支持“总线供电”和“自供电”两种供电模式。在总线供电模式下,设备最多可以获得500mA 的电流。USB2.0 被设计成为向下兼容的模式,当有全速(USB 1.1)或者低速(USB 1.0)设备连接到高速(USB 2.0)主机时,主机可以通过分离传输来支持它们。一条USB 总线上,可达到的最高传输速度等级由该总线上最慢的“设备”决定,该设备包括主机、HUB 以及USB 功能设备。
USB 体系包括“主机”、“设备”以及“物理连接”三个部分。其中主机是一个提供USB接口及接口管理能力的硬件、软件及固件的复合体,可以是PC,也可以是OTG 设备。一个USB系统中仅有一个USB主机;设备包括USB 功能设备和USB HUB,最多支持127 个设备;物理连接即指的是USB的传输线。在USB 2.0 系统中,要求使用屏蔽的双绞线。
从普通用户的观点来看,USB 的主要特征是每个设备都有同样的四芯电缆及标准化插头,都可以插入到PC 后部的标准化插座或与PC 相连的某个HUB上。另外,你还可以随意拔插USB设备而不管应用程序是否打开以及是否会造成电气损害。然而对于一个驱动软件开发人员来说,USB 并不像想象的那么简单,了解USB 某些电子和机械概念仍然很重要。
对于每个PC来说,都有一个或者多个称为Host 控制器的设备,该Host 控制器和一个根Hub(Root Hub)作为一个整体[5]。ROOT HUB 是一个特殊的USB HUB,它集成在主机控制器里,不占用地址。
l USB Host 控制器:每一个PC 的主板上都会有多个Host控制器,这个Host 控制器与其它I/O设备一样直接连接到系统总线上,其实它就是一个PCI 设备,挂载在PCI 总线上。操作系统与主控制器通信使用I/O口或内存寄存器,通过普通的中断信号,系统可以接受主控制器的事件通知。主控制器连接一棵USB 设备树(如图 5.27)。一种称为hub 的设备作为其它设备的连接点。多个hub 能以菊链方式连接,可以连接到USB规范中定义的最大深度。其它设备,如照相机、麦克风、键盘等等,直接连到hub上。
l USB Hub:每一个USB 控制器都会自带一个USB Hub,被称为根(Root)Hub。这个根Hub可以接子(Sub)Hub,每一个Hub 挂载USB 设备。在普通的PC 机当中,一般有8个USB 口,通过外界USB Hub,可以插更多的USB 设备。
l USB 设备:USB 设备就是插在USB 总线上工作的设备,广义地讲USB Hub 也算一个USB设备。每个USB Hub下可以直接或者间接地连接127各设备,并且彼此之间不会干扰。对于用回来说,可以看成是USB 设备和USB 控制器直接相连,之间通讯需要满足USB 的通讯协议。
图 5.27 USB设备树
USB设备树图说明:以HOST-ROOT HUB为起点, 最多支持7 层(Tier),也就是说任何一个USB系统中最多可以允许5个USB HUB级联。一个复合设备(Compound Device)将同时占据两层或更多的层。
USB 采用轮询的广播机制传输数据,所有的传输都由主机发起,任何时刻整个USB 体内仅允许一个数据包的传输,即不同物理传输线上看到的数据包都是同一被广播的数据包。USB 采用“令牌包”-“数据包”-“握手包”的传输机制,在令牌包中指定数据包去向或者来源的设备地址和端点(Endpoint),从而保证了只有一个设备对被广播的数据包/令牌包作出响应。握手包表示了传输的成功与否。
管道(Pipe)是主机和设备端点之间数据传输的模型,共有两种类型的管道:无格式的流管道(Stream Pipe)和有格式的信息管道(Message Pipe)。任何USB 设备一旦上电就存在一个信息管道,即默认的控制管道,USB主机通过该管道来获取设备的描述、配置、状态,并对设备进行配置。
USB 设备连接到HOST 时,HOST 必须通过默认的控制管道对其进行枚举,完成获得其设备描述、进行地址分配、获得其配置描述、进行配置等操作方可正常使用。USB 设备的即插即用特性即依赖于此。
USB 体系在实现时采用分层的结构,如下图 5.28所示:在HSOT 端,应用软件(ClientSW)不能直接访问USB总线,而必须通过USB系统软件和USB主机控制器来访问USB 总线,在USB 总线上和USB 设备进行通讯。从逻辑上可以分为功能层、设备层和总线接口层三个层次。其中功能层完成功能级的描述、定义和行为;设备级则完成从功能级到传输级的转换,把一次功能级的行为转换为一次一次的基本传输;USB 总线接口层则处理总线上的Bit 流,完成数据传输的物理层实现和总线管理。途中黑色箭头代表真实的数据流,灰色箭头代表逻辑上的通讯。
图 5.28 USB数据流传输结构
物理上,USB 设备通过分层的星型总线连接到HOST,但在逻辑上HUB 是透明的,各USB设备和HOST 直接连接,和HOST上的应用软件形成一对一的关系。各应用软件-功能设备对之间的通讯相互独立,应用软件通过USB 设备驱动程序(USBD)发起IRQ请求,请求数据传输。主机控制器驱动程序(HCD)接收IRQ请求,并解析成为USB传输和传输事务(Transaction),并对USB 系统中的所有传输事务进行任务排定(因为可能同时有多个应用软件发起IRQ 请求)。主机控制器(Host Controller)执行排定的传输任务,在同一条共享的USB 总线上进行数据包的传输。
USB 系统中数据的传输,宏观的看来是在HOST 和USB 功能设备之间进行;微观的看是在应用软件的Buffer 和USB 功能设备的端点之间进行。一般来说端点都有Buffer,可以认为USB通讯就是应用软件Buffer 和设备端点Buffer 之间的数据交换,交换的通道称为管道。应用软件通过和设备之间的数据交换来完成设备的控制和数据传输。通常需要多个管道来完成数据交换,因为同一管道只支持一种类型的数据传输。用在一起来对设备进行控制的若干管道称为设备的接口,这就是端点、管道和接口的关系。
一个USB 设备可以包括若干个端点,不同的端点以端点编号和方向区分。不同端点可以支持不同的传输类型、访问间隔以及最大数据包大小。除端点0外,所有的端点只支持一个方向的数据传输。端点0是一个特殊的端点,它支持双向的控制传输。管道和端点关联,和关联的端点有相同的属性,如支持的传输类型、最大包长度、传输方向等[5]。
USB 体系定义了四种类型的传输,它们是:
l 控制传输:主要用于在设备连接时对设备进行枚举以及其他因设备而已的特定操作。
l 中断传输:用于对延迟要求严格、小量数据的可靠传输,如键盘、游戏手柄等。
l 批量传输:用于对延迟要求宽松,大量数据的可靠传输,如U 盘等。
l 同步传输:用于对可靠性要求不高的实时数据传输,如摄像头、USB音响等。
在主机控制器和USB HUB 之间还有另外一种传输——分离传输(Split Transaction),它仅在主机控制器和HUB 之间执行,通过分离传输,可以允许全速/低速设备连接到高速主机。分离传输对于USB 设备来说是透明的、不可见的。
所有的USB设备都会在默认控制管道对主机发送过来的请求作出响应。这些请求利用控制传输产生。请求及其参数是通过SETUP包发送到设备。主机负责设置如下信息。
表 9 SETUP包格式
偏移量 |
场 |
大小 |
值 |
描述 |
0 |
bmRequestType |
1 |
位映像 |
请求的类型: D7 数据传输方向 0:主机到设备 1:设备到主机 D6~5 类型 0:标准 1:类型 2:厂商 3:保留 D4~0 接收器 0:设备 1:接口 2:端点 3:其他 4~31:保留 |
1 |
bRequest |
1 |
值 |
专用请求 |
2 |
wValue |
2 |
值 |
根据请求变化、长度大小为一个字 |
4 |
wIndex |
2 |
偏移量或索引 |
根据请求变化。典型的应用是用于传递索引或偏移量 |
6 |
wLength |
2 |
计数 |
如果有数据阶段,在数据阶段传输的字节数 |
所有USB设备都支持如表10下所示的标准设备请求;
表10 标准的设备请求
bmRequestType |
bRequest |
wValue |
wIndex |
wLength |
数据 |
|
00000000B |
CLEAR_FEATURE(1) |
特性选择符 |
接口端点0 |
0 |
无 |
|
00000001B |
||||||
00000010B |
||||||
10000000B |
GET_CONFIGURATION(8) |
0 |
0 |
1 |
配置值 |
|
10000000B |
GET_DESCRIPTOR(7) |
描述符类型及其索引 |
0或者语言ID |
描述符长度 |
描述符 |
|
10000001B |
GET_INTERFACE(11) |
0 |
接口 |
1 |
备用设置 |
|
10000000B |
GET_STATUS(0) |
0 |
接口端点0 |
2 |
设备、接口或端点状态 |
|
10000001B |
||||||
10000010B |
||||||
00000000B |
SET_ADDRESS(5) |
设备地址 |
0 |
0 |
无 |
|
00000000B |
SET_CONFIGURATION(9) |
配置值 |
0 |
0 |
无 |
|
00000000B |
SET_DESCRIPTOR(6) |
描述符类型及其索引 |
0或者语言ID |
描述符长度 |
描述符 |
|
00000000B |
SET_FEATURE(3) |
特性选择符 |
接口端点0 |
测试选择符 |
测试选择符 |
0 |
00000001B |
SET_INTERFACE(11) |
备用设置 |
接口 |
0 |
无 |
|
00000010B |
SYNCH_FRAME(12) |
0 |
端点 |
2 |
帧号码 |
|
其中描述符的类型值如表 11所示;
表 11 描述符类型
描述符类型 |
值 |
DEVICE |
1 |
CONFIGURATION |
2 |
STRING |
3 |
INTERFACE |
4 |
ENDPOINT |
5 |
DEVICE_QUALIFIER |
6 |
OTHER_SPEED_COFIGURATION |
7 |
INTERFACE_POWER |
8 |
OTG |
9 |
DEBUG |
10 |
INTERFACE_ASSOCIATION |
11 |
主机按照控制传输的约定,将SETUP包及其参数和数据发送到设备,并等待设备的握手信息。
(1) 设备描述符
设备描述符说明了USB设备的通用信息。它包括应用到全部设备和所有设备配置的信息。USB设备只有一个设备描述符。其格式如表 12所示。
表 12 设备描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
按字节算的描述符大小 |
1 |
bDescriptorType |
1 |
常数 |
设备描述符类型 |
2 |
bcdUSB |
2 |
BCD码 |
以BCD编码的USB规范版本号 210H即2.10 |
4 |
bDeviceClass |
1 |
类型 |
类型码(由USB-IF分配) 如果值为0,配置中的每个接口指定了自己的类型信息,而且接口独立操作 如果值在1~FFH之间设备支持不同接口有不同的类型规范,而且接口不独立操作 如果值为FFH,设备类型是厂商提供的 |
5 |
bDeviceSubClass |
1 |
子类型 |
子类型码(由USB-IF分配),这些代码受bDeviceClass场的限制。 |
6 |
bDeviceProtocol |
1 |
协议 |
协议码(USB-IF分配) |
7 |
bMaxPacketSize0 |
1 |
数字 |
端点0的最大包大小(只能是8,16,32或64) |
8 |
idVendor |
2 |
ID |
厂商ID(USB-IF分配) |
10 |
idProduct |
2 |
ID |
商品ID(由生产商分配) |
12 |
bcdDevice |
2 |
BCD码 |
BCD编码表式的设备发布号 |
14 |
iManufacturer |
1 |
索引 |
描述厂商的字符串描述符的索引 |
15 |
iProduct |
1 |
索引 |
描述产品的字符串描述符的索引 |
16 |
iSerialNumber |
1 |
索引 |
描述设备序列号的字符串描述符的索引 |
17 |
bNumConfiguration |
1 |
数字 |
可能的配置数 |
能进行高速操作的设备与全速和低速设备的配置信息不一样,因此还要有设备限制符。
表 13 设备限定描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
描述符的大小 |
1 |
bDescriptorType |
1 |
常数 |
设备限定符的类型 |
2 |
bcdUSB |
2 |
BCD |
USB规范的发布号 |
4 |
bDeviceClass |
1 |
类型 |
类型码 |
5 |
bDeviceSubClass |
1 |
子类型 |
子类型码 |
6 |
bDeviceProtocol |
1 |
协议 |
协议码 |
7 |
bMaxPacketSize0 |
1 |
数字 |
其他速度的最大包大小 |
8 |
bNumConfigurations |
1 |
数字 |
其他速度配置的数据 |
9 |
bReserved |
1 |
0 |
保留到以后使用,必须为0 |
(2) 配置描述符
配置描述符说明了一个特定配置的相关信息。
表 14 标准配置描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
按字节计算的描述符大小 |
1 |
bDescriptorType |
1 |
常数 |
配置描述符类型 |
2 |
wTotalLength |
2 |
数字 |
该配置返回的数据总长度,包括该配置返回的所有描述符(配置、接口、端点号和专用类型描述符) |
4 |
bNumInterfaces |
1 |
数字 |
这个配置支持的接口数 |
5 |
bConfigurationValues |
1 |
数字 |
用于SetConfiguration() |
6 |
iConfiguration |
1 |
索引 |
描述这个配置的字符串描述符索引 |
7 |
bmAttributes |
1 |
位映像 |
配置特性: D7 保留(设为1) D6 自供电 D5 远程唤醒 D4~0 保留(设为0) |
8 |
bMaxPower |
1 |
mA |
当设备运行时特定配置的USB设备从总线获得的最大功耗。 |
(3) 其他速度描述符
其他速度描述符说明了高速设备在其他速度模式下的操作配置。
表 15 其他速度配置描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
描述符的大小 |
1 |
bDescriptorType |
1 |
常数 |
描述符的类型 |
2 |
wTotalLength |
2 |
数字 |
返回的数据总长度 |
4 |
bInterfaces |
1 |
数字 |
支持的接口数 |
5 |
bConfigurationValue |
1 |
数字 |
用于选择配置的值 |
6 |
iConfiguration |
1 |
索引 |
字符串描述符的索引 |
7 |
bmAttributes |
1 |
位映像 |
与配置描述符符相同 |
8 |
bMaxPower |
1 |
mA |
与配置描述符相同 |
(4) 接口描述符
接口描述符描述了特定配置中的一个接口。
表 16 标准接口描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
按字节的描述符大小 |
1 |
bDescriptorType |
1 |
常数 |
描述符类型 |
2 |
bInterfaceNumber |
1 |
数字 |
接口的序列号 |
3 |
bAlternateSetting |
1 |
数字 |
选者备用配置时用的值 |
4 |
bNumEndpoints |
1 |
数字 |
接口的端点数(不包括端点0) |
5 |
bInterfaceClass |
1 |
类型 |
类型码(USB-IF分配) |
6 |
bInterfaceSubClass |
1 |
子类型 |
子类型码(USB-IF分配) |
7 |
bInterfaceProtocoal |
1 |
协议 |
协议码(USB-IF分配) |
8 |
iInterface |
1 |
索引 |
描述这个接口的字符串的索引值 |
(5) 端点描述符
表 17 标准端点描述符
偏移 |
场 |
大小 |
值 |
描述 |
0 |
bLength |
1 |
数字 |
按字节的描述符大小 |
1 |
bDescriptorType |
1 |
常数 |
描述符类型 |
2 |
bEndpointAddress |
1 |
端点 |
USB设备端点地址。 [3:0]端点号 [6:4]保留,设为0 [7]方向 |
3 |
bmAttributes |
1 |
位映像 |
[1:0] 传输类型 00 = 控制 01 = 同步 10 = 批量 11 = 中断 [3:2] 同步类型 00 = 非同步 01 = 异步 10 = 自适应 11 = 同步 [5:4] 使用类型 00 = 数据端点 01 = 反馈端点 10 = 隐式反馈数据端点 11 = 保留 |
4 |
wMaxPacketSize |
2 |
数字 |
端点能接受或发送的最大的包大小 |
6 |
bInterval |
1 |
数量 |
查询端点进行数据传输的间隔 |
USB主机的初始化包括根集线器和主机控制器的初始化。初始化的任务是将主机控制器和根集线器设置为特定的状态,初始化内存。下面从主机控制器上电开始对初始化过程简单介绍。
除了与图形界面相关的驱动,Windows CE的驱动都是由设备管理器(Device.exe)加载的。主机控制器驱动(在这里为OHCD)就是由设备管理器加载的。而设备管理器加载驱动的依据是/HKEY_LOCAL_MACHINE/Drivers下注册表键。其中OHCI的键的主要内容如程序清单 5‑28所示。从中可以看出OHCI的驱动程序为ohci2.dll。
程序清单 5‑28 OHCI注册键
[HKEY_LOCAL_MACHINE/Drivers/BuitIn]
"Prefix" = "HCD"
"Dll" = "ohci2.dll"
"Order" = dword:2
"Class" = dword:0c
"SubClass" = dword:03
"ProgIF" = dword:10
设备管理器加载ohci2.dll,并且调用ohci2.dll中的HCD_Init函数。该函数完成对主机控制器的所有初始化。HCD_Init函数的实现如程序清单 5‑29所示。
程序清单 5‑29 HCD_Init代码
extern "C" DWORD HCD_Init (DWORD dwContext )
{
HKEY ActiveKey; /* 指向打开的注册表键 */
WCHAR RegKeyPath[DEVKEY_LEN]; /* 保存设备在BuiltIn下的键值 */
DWORD status; /* 注册表操作函数的返回值 */
DWORD ValType; /* 注册表键值的类型 */
DWORD ValLen; /* 注册表键值的长度 */
status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, (LPCWSTR)dwContext,
0, 0, &ActiveKey); /* 打开设备注册表键值 */
if (status != ERROR_SUCCESS) { /* 打开失败打印提示信息,返NULL */
DEBUGMSG(ZONE_INIT|ZONE_ERROR,
(TEXT("EHCD!HCD_Init RegOpenKeyEx(%s) returned %d./r/n"),
(LPCWSTR)dwContext, status));
return NULL;
}
/* 得到Key的键值,这个值指向设
备在BuiltIn下的键 */
ValLen = sizeof(RegKeyPath);
status = RegQueryValueEx(ActiveKey, DEVLOAD_DEVKEY_VALNAME,
NULL, &ValType, (PUCHAR)RegKeyPath,&ValLen);
if (status != ERROR_SUCCESS) { /* 查找失败,关闭注册表,返回NULL */
DEBUGMSG(ZONE_INIT|ZONE_ERROR,
(TEXT("EHCD!HCD_Init RegQueryValueEx(%s//%s) returned %d/r/n"),
(LPCWSTR)dwContext, DEVLOAD_DEVKEY_VALNAME, status));
RegCloseKey(ActiveKey);
return NULL;
}
RegCloseKey(ActiveKey);
g_IstThreadPriority = GetInterruptThreadPriority(
(LPTSTR)dwContext); /* 得到中断优先级 */
RegKeyPath[DEVKEY_LEN-1] = 0 ;
EnterCriticalSection( &g_CSection );
g_dwContext = dwContext;
DWORD dwReturn =HcdPdd_Init((DWORD)RegKeyPath); /* 调用PDD层初始化函数 */
g_dwContext = 0;
LeaveCriticalSection( &g_CSection );
return dwReturn;
}
HCD_Init完成两件事:
l 得到设备中断处理程序(IST)的优先级,默认为101。
l 调用HcdPdd_Init函数,继续对设备进行初始化。
HCD_Init基本上没对设备做什么配置。大部分的工作都留给了PDD层的初始化函数HcdPdd_Init。HcdPdd_Init函数负责创建并实例化一个SOhcdPdd的结构体。该结构体有两个指针,分别指向设备内存对象(CPhysMem)和COhcd类的一个实例。设备内存对象负责对设备内存的管理,包括动态分配和释放内存。所有对设备内存的操作都是由设备内存对象完成的。而COhcd,如上一节所述,是主机控制器驱动程序,负责操作主机控制器。对这两个指针的实例化是由InitializeOHCI函数完成的。该函数还注册中断号,将中断与设备关联起来,其最主要的代码如程序清单 5‑30所示。
程序清单 5‑30 InitializeOHCI重要代码
dwSysIntr = MapIrq2SysIntr(dwIRQ); /* 映射中断 */
pobMem = HcdMdd_CreateMemoryObject(gcTotalAvailablePhysicalMemory,
gcHighPriorityPhysicalMemory, pvUsbHcca,
(LPVOID)(dwIoPortBase+HD64465_EMBEDED_SDRAM_OFFSET));
/* 创建内存对象 */
if(pobMem) { /* 创建内存对象成功,则继续
创建Cohcd */
pobOhcd = HcdMdd_CreateHcdObject(pPddObject, pobMem,
szDriverRegKey, pvUsbRegister, dwSysIntr);
fResult = pobOhcd ? TRUE : FALSE;
} else
fResult = FALSE; /* 创建失败,返回FALSE */
HcdMdd_CreateMemoryObject函数创建内存对象,HcdMdd_CreateHcdObject函数创建COhcd,其关键代码如程序清单 5‑31所示。
程序清单 5‑31 HcdMdd_CreateObject的关键代码
COhcd * pobOhcd = new COhcd(lpvOhcdPddObject, pobMem, szRegKey,
ioPortBase, dwSysIntr); /* 创建COhcd类 */
if (pobOhcd && (pobOhcd->DeviceInitialize() == FALSE)) {
/* 创建成功,但是
COhcd::Initialize失败,删除Cohcd */
delete pobOhcd;
pobOhcd = NULL;
}
最核心的初始化工作是由COhcd::DeviceInitialize函数完成的。 COhcd::DeviceInitialize分别调用CdeviceGlobal::Initialize()、CHW::Initialize()和CHCCAera::Initialize()函数。其中CdeviceGlobal类的Initialize()函数是对COhcd的进一步初始化并且建立和USBD的联系;CHW类的Initialize函数是对主机控制器的初始化,包括主机控制器寄存器的配置,为CHCC分配内存空间和设置中断线程优先级等;CHCCAera类的Initialize函数是对HCCA(Host Controller Communication Aeras)的初始化。
如图 5.29所示是主机初始化整个流程的示意图。
图 5.29 主机初始化示意图
在上面已经讲过,根集线器是由CRootHub类管理的。CRootHub类继承了CHub类。CHub::HubStatusChangeThread函数负责检测根集线器端口状态变化。该函数首先会激活所有的集线器端口,然后调用CHW::WaitForPortStatusChange函数等待端口变化。当端口有变化时,CHW::WaitForPortStatusChange函数会返回变化的端口号和变化后端口的状态。此时,CHub::HubStatusChangeThread函数会检查哪个端点发生变化,端口0表示集线器状态变化。然后判断设备是否是从挂起恢复,如果是,则调用恢复通知;若不是,则表示有新设备插入,调用CHub::AttachDevice函数,该函数会对设备作初始化配置。
CHub::AttachDevice函数的初始化包括:
l 建立控制管道,如果是高速设备,还要为其分配处理转化器(TT)。
l 复位设备,根据USB协议,在所有操作前必须先复位设备。
l 获取设备的设备描述符;
l 为设备分配地址,通过控制管道设置设备地址。
l 获取设备的配置描述符;
l 根据具体情况选择一个合适的配置描述符,并用这个配置描述符配置设备。
l 创建管理设备的类,若新插入的设备是集线器,则创建CExternHub类;若是功能设备,则创建CFunction类。
l 向新插入的设备发出进入“操作状态”。对于集线器,所要做的事是:分配并初始化与集线器相关数据结构,创建中断管道,并且创建中断处理例程用于检测本身状态变化。对于功能设备来说,所要做的事为:分配并初始化与功能设备相关的数据结构,通过调用HcdDeviceAttach函数来为设备加载驱动。
(6) 为功能设备加载驱动
HcdDeviceAttach函数的关键代码如程序清单 5‑32所示。
程序清单 5‑32 HcdDeviceAttach关键代码
while (fRet && !fLoaded) {
fRet = LoadDeviceDrivers(pDev, &fLoaded); /* 根据注册表加载驱动 */
if (fRet && !fLoaded) { /* 加载驱动失败,提示用户输入驱动 */
if (!GetClientDriverName(szDllName, USB_MAX_LOAD_STRING))
break;
else if (!InstallClientDriver(szDllName)) /* 调用驱动中USBInstallDriver函数
设置注册表,失败则继续提示用户
输入驱动名 */
CallNetMsgBox(NULL,NMB_FL_OK|NMB_FL_TITLEUSB,
NETUI_GETNETSTR_USB_INSTALL_FAILURE,szDllName);
}
}
加载驱动的思路是:首先根据注册表信息,加载合适的驱动。如果注册表表中没有合适的,则提示用户输入驱动的名字,然后调用驱动的USBInstallDriver函数设置注册表。再一次根据注册表信息加载合适驱动,如果还是没有合适的驱动,继续让用户输入驱动名。这样一直循环,直到找到合适的驱动。
具体怎么根据注册表来查找合适的驱动,即驱动搜索算法,会在后面详细讲述。现在先了解一下USBInstallDriver函数。
USBInstallDriver函数的作用就是为驱动设置注册表信息。所有USB驱动的信息都在注册表中HKEY_LOCAL_MACHINE/Drivers/USB下,驱动的加载信息在该键的LoadClients子键下。系统会根据USBInstallDriver所提供的USB_DRIVER_SETTINGS结构体信息,在LoadClients子键下设置驱动的加载信息。USB_DRIVER_SETTINGS结构体如程序清单 5‑33所示。
程序清单 5‑33 USB_DRIVER_SETTINGS结构体
typedef struct _USB_DRIVER_SETTINGS
{
DWORD dwCount; /* 结构体大小 */
/* 设备生产厂商信息 */
DWORD dwVendorId; /* 设备描述符中的厂商ID */
DWORD dwProductId; /* 设备描述符中的产品ID */
DWORD dwReleaseNumber; /* 设备描述符中的产品发布号 */
DWORD dwDeviceClass; /* 设备描述符中的设备类型 */
DWORD dwDeviceSubClass; /* 设备描述符中的设备子类型 */
DWORD dwDeviceProtocol; /* 设备描述符中的设备协议类型 */
DWORD dwInterfaceClass; /* 设备描述符中的接口类型 */
DWORD dwInterfaceSubClass; /* 设备描述符中的接口子类型 */
DWORD dwInterfaceProtocol; /* 设备描述符中的接口协议类型 */
} USB_DRIVER_SETTINGS, * PUSB_DRIVER_SETTINGS, * LPUSB_DRIVER_SETTINGS;
设备厂商信息,VendorId、ProductId、ReleaseNumber是一个组,称为Group1;设备类型信息,DeviceClass、DeviceSubClass、DeviceProtocol作为一个组,称为Group2;设备接口信息,InterfaceClass、InterfaceSubClass、InterfaceProtocol作为一个组,称为Group3。这三个组按照顺序(Group1/Group2/Group3)组织在一起就构成了驱动在注册表中的键值。当不知道或不想设置结构体中的某个成员时,可以将其设置为USB_NO_INFO(0xFFFFFFFF)。
USBInstallDriver函数所要做的就是设置USB_DRIVER_SETTINGS结构体成员,并调用RegisterClientSettings函数,将信息添加到注册表中。驱动加载信息在注册表中的最终表现为:HKEY_LOCAL_MACHINE/Drivers/USB/LoadClients/Group1/Group2/Group3。每一个Group都是X_Y_Z的形式,其中X、Y、Z是每个Group的第一个、第二个、第三个成员。例如:HID类的驱动的USB_DRIVER_SETTINGS结构体信息如程序清单 5‑34所示。
程序清单 5‑34 HID USB_DRIVER_SETTINGS示例
DriverSettings->dwVendorId = USB_NO_INFO;
DriverSettings->dwProductId = USB_NO_INFO;
DriverSettings->dwReleaseNumber = USB_NO_INFO;
DriverSettings->dwDeviceClass = USB_NO_INFO;
DriverSettings->dwDeviceSubClass = USB_NO_INFO;
DriverSettings->dwDeviceProtocol = USB_NO_INFO;
DriverSettings->dwInterfaceClass = USB_DEVICE_CLASS_HUMAN_INTERFACE;
/* 等于3 */
DriverSettings->dwInterfaceSubClass = USB_NO_INFO;
DriverSettings->dwInterfaceProtocol = USB_NO_INFO;
对应的注册表键为:
HKEY_LOCAL_MACHINE/Drivers/USB/LoadClient/Default/Default/3/Client_Driver_ID其中Client_Driver_ID是驱动通过RegisterClientDriverID注册的驱动标识符,驱动开发者应该保证该标识符的唯一性。
除了要设置注册表,驱动还需要设置一些键值:DLL,Index,Prefix,Order。其中DLL键值保存的是驱动的名字;Prefix是驱动的前缀;Index是驱动序号,和Prefix结合在一起表明驱动程序在系统中的表示;Order是驱动加载顺序,表示当有多个Prefix相同的驱动时,本驱动的加载顺序。
驱动程序还可以利用OpenClientRegisterKey打开HEKY_LOCAL_MACHINE/Drivers/ USB/ClientDrivers/Client_Driver_ID键,设置驱动程序需要的额外的信息。
综上所述,以下是USBInstallDriver函数需要完成的操作:
l 通过RegisterClientID函数,注册一个系统唯一的驱动表示字符串。
l 通过RegisterClientSettings函数,正确设置LoadClient下的子键。
l 通过OpenClientRegisterKey函数,设置驱动所需要的额外的信息。这一步不是必须的。
做完这些操作之后,系统将会再一次按照一定的顺序搜索驱动。找到合适的驱动后,调用驱动的USBDeviceAttach函数。USBDeviceAttach函数可以看作应用层的设备插入报告处理函数,每次有设备插入时,系统都会调用驱动的USBDeviceAttach函数。该函数可以做一些用户所需要的初始化。
当设备驱动加载成功后,主机和设备就可以相互通信。客户驱动程序可以调用USBD层的接口完成各种通信任务。如表 18所示为USBD层提供的部分接口。
表 18 USBD层提供的部分接口
接口名 |
描述 |
GetTransferError |
返回与指定参数相关的错误信息 |
lpAbortPipeTransfer |
取消管道上的所有传输 |
lpAbortTransfer |
取消特定的传输 |
lpClearFeature |
向USB设备发送CLEAE_FEATURE请求 |
lpClosePipe |
关闭特定的管道 |
lpCloseTransfer |
关闭特定的传输 |
lpDeviceNotifyRoutine |
处理设备通知 |
lpDisableDevice |
禁能设备 |
lpFindInterface |
寻找特定的接口 |
lpGetDescriptor |
向设备发送GET_DESCRIPTOR请求 |
lpGetDeviceInfo |
获取设备所有的描述符 |
lpGetFrameLength |
获得帧长度 |
lpGetFrameNumber |
获得当前帧号码 |
lpGetIsochResults |
获取同步传输结果 |
lpGetStatus |
向设备发送GET_STATUS请求 |
lpGetTransferStatus |
获取特定传输状态 |
lpGetUSBDVersion |
获得设备USB版本号 |
lpIsDefaultPipeHalted |
检测默认管道是否挂起 |
lpIsPipeHalted |
检测特定管道是否挂起 |
lpIsTransferComplete |
检测特定传输是否完成 |
lpIssueBulkTransfer |
发送批量传输 |
lpIssueInterruptTransfer |
发送中断传输 |
lpIssueIsochTransfer |
发送同步传输 |
lpIssueVendorTransfer |
向设备发送厂商特定的传输 |
lpLoadGenericInterfaceDriver |
加载设备接口驱动 |
lpOpenClientRegisterKey |
打开设备驱动注册表键 |
lpOpenPipe |
打开管道 |
lpRegisterClientDriverID |
注册设备ID |
lpRegisterDriverSettings |
注册设备驱动 |
lpRegisterNotifyRoutine |
注册处理设备通知的函数 |
lpReleaseFrameLengthControl |
释放对帧长度的控制权 |
lpResetDefaultPipe |
恢复默认管道 |
lpResetPipe |
恢复特定管道 |
USBD层的接口可以分为三类:针对管道的接口、针对传输的接口、针对设备的接口。针对管道的接口主要包括:管道的打开与关闭、管道状态检测、恢复管道和取消管道的所有传输等。针对传输的接口主要包括:发布传输、取消传输、传输状态检测、关闭传输。针对设备的接口主要包括:禁能设备、挂起设备、恢复设备、获取设备的各种描述符等。
从结构分析我们可知,所有的USB设备驱动程序必须在它们的DLL库设置一定的入口点与USBD模块进行适当的交互。设置入口点函数有两个作用:一是使得 USBD 模块能与外部设备交互;二是使得驱动程序能创建和管理任何可能需要的注册键,下面简要介绍相关函数的作用。
USBDeviceAttach是当 USB 设备连接到主计算机时运行,USBD模块会调用这个函数初始化USB设备,取得USB设备信息和配置USB设备,并且申请必需的资源。 USBInstallDrive是在第一次加载USB设备驱动程序时首先被调用,它使得驱动程序能创建需要的注册键,用于将一个驱动程序所需的注册表信息写入到HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers目录下,例如设备名称等。需要注意的是,USB设备驱动程序不使用标准的注册表函数,而是使用RegisterClientDriverID、RegisterClientSettings函数来注册相应的设备信息。
USBUninstallDriver是在用户删除USB设备驱动程序时调用,负责删除注册键并释放其它相关资源。它通过调用 UnRegisterClientSettings和UnRegisterClientDriverID函数来删除由驱动程序的 USBInstallDriver函数创建的所有注册键。因此、我们在驱动程序中就需要严格按照这三个函数的原型来实现,否则就不能为设备管理器所识别。
我们可以清晰的看到主机和外设之间的实现方式。在主机端,通过USBD模块和HCD模块使用默认的PIPE访问一个通用的逻辑设备,实际上就是说USBD和HCD是一组访问所有USB设备的逻辑接口,它们负责管理所有USB设备的连接、加载、移除、数据传输和通用配置。其中HCD是主机控制驱动,是为USBD提供底层的功能访问服务,USBD是USB总线驱动,位于HCD的上层,利用HCD的服务提供较高层次的功能。因此,实现USB加载流驱动程序大致需要完成以下步骤。
l 选择代表设备的文件名前缀,前缀非常重要,设备管理器在注册表中通过前缀来识别设备。同时、在流接口命名时也将这个前缀作为入口点函数的前缀,如果设备前缀为XXX,那么流接口对应为XXX_Close、XXX_Init等。
l 设置驱动的各个入口点函数,所谓入口点是指提供给设备管理器的标准文件I/O接口。在生成一个DLL后,就用设备文件名前缀替换名字中的XXX。因此,每个加载式流接口驱动程序必须实现XXX_Init、XXX_IOControl以及XXX_PowerUp等一组标准的函数,用来完成标准的文件I/O函数和电源管理等。
l 建立.DEF文件。当设备管理器初始化USB设备编译出来的流接口函数后,还必须建立一个.def文件。DEF文件定义了DLL要导出的接口集,而且加载式流驱动大多是以DLL形式存在的,所以应将DLL和DEF的文件名统一起来。DEF文件告诉链接程序需要输出什么样的函数,最后将驱动程序编译到内核中去,这样这个USB设备流接口驱动程序就可以被应用程序调用。
l 在注册表中为驱动程序建立表项。在注册表中建立驱动程序入口点,这样设备管理器才能识别和管理这个驱动。此外,注册表中还能存储额外的信息,这些信息可以在驱动运行之后被使用到。
驱动所有的通信操作都在TST_IOControl函数中完成。该函数导出了13个接口,每个接口都是一个IOCTL码,这样上层应用就可以通过DeviceIoControl函数来操作设备完成通信。这13个接口如下:
l IOCTL_INTERRUPT_IN 发送中断输入传输。
l IOCTL_INTERRUPT_OUT 发送中断输出传输。
l IOCTL_BULK_IN 发送批量输入传输。
l IOCLT_BULK_OUT 发送批量输出传输。
l IOCTL_OPEN_INTER_IN_PIPE 打开中断输入管道。
l IOCTL_CLOSE_INTER_IN_PIPE 关闭中断输入管道。
l IOCTL_OPEN_INTER_OUT_PIPE 打开中断输出管道。
l IOCTL_CLOSE_INTER_OUT_PIPE 关闭中断输出管道。
l IOCTL_OPEN_BULK_IN_PIPE 打开批量输入管道。
l IOCTL_CLOSE_BULK_IN_PIPE 关闭批量输入管道。
l IOCTL_OPEN_BULK_OUT_PIPE 打开批量输出管道。
l IOCTL_CLOSE_BULK_OUT_PIPE 关闭批量输出管道。
l IOCLT_GET_DESCRIPTOR 获取设备描述符的字符串表示。
所有的这些操作(除了IOCLT_GET_DESCRIPTOR)都是调用USBD层提供的接口。例如,IOCTL_INTERRUPT_IN是调用USBD层的IssueInterruptTransfer接口。下面以中断输入为例具体介绍驱动如何与设备通信的。
程序清单 5‑35 中断输入代码
case IOCTL_INTERRUPT_IN:
RETAILMSG(1, (TEXT("interrupt-in transfer/n"))); /* 打印调试信息 */
if (!pBufOut ||! pdwActualOut) { /* 检查输入参数的有效性 */
RETAILMSG(1, (TEXT("Interrupt In Transfer Parameter ERROR/n")));
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
if (!pUSBD12->InterruptIn.bOpen) { /* 检查输入管道是否打开 */
RETAILMSG(1, (TEXT("Please Open Pipe First/n")));
return FALSE;
}
EnterCriticalSection(&pUSBD12->InterruptIn.csPipeLock); /* 进入临界区,开始通信 */
/*
* 调用USBD层IssueInterruptTransfer接口,USB_IN_TRANSFER表明是输入传输。pBufOut是缓冲
* 区,dwLenOut是缓冲区的长度。操作是同步的。
*/
usbTransfer = (*pUSBD12->usbFuncs->lpIssueInterruptTransfer)(pUSBD12->InterruptIn.Pipe,
NULL, NULL,
USB_IN_TRANSFER |
USB_SHORT_TRANSFER_OK,
dwLenOut, pBufOut, 0);
if (NULL == usbTransfer) { /* 检查传输是否成功建立 */
RETAILMSG(1, (TEXT("Issue Interrupt transfer ERROR : %d/n") , GetLastError()));
LeaveCriticalSection(&pUSBD12->InterruptIn.csPipeLock);
return FALSE;
}
(*pUSBD12->usbFuncs->lpGetTransferStatus)(usbTransfer, pdwActualOut, dwErr);
/* 获得传输的状态 */
if (ERROR_SUCCESS != dwErr) { /* 检查是否正确无误的完成传输 */
RETAILMSG(1, (TEXT("Transfer Issue Status NOT ERROR_SUCCESS/n")));
(*pUSBD12->usbFuncs->lpCloseTransfer)(usbTransfer);
LeaveCriticalSection(&pUSBD12->InterruptIn.csPipeLock);
return FALSE;
}
(*pUSBD12->usbFuncs->lpCloseTransfer)(usbTransfer); /* 关闭传输 */
LeaveCriticalSection(&pUSBD12->InterruptIn.csPipeLock); /* 离开临界区 */
RETAILMSG(1, (TEXT("Interrupt In Transfer OK/n")));
return TRUE;
如程序清单 5‑35所示为中断输入传输所作的操作。首先检查参数的有效性和相应的管道是否打开。如果参数无效或管道没有打开,则提示相关信息并退出函数。下一步就是要发布传输了,在对管道发布传输前,先进入临界区,防止对线程的干扰。发布传输是调用USBD层的接口IssueInterruptTransfer,通过对参数的制定可以控制传输的特性。在这里驱动是使用同步方式发送数据的,也就是说,上层应用一直等待数据传输的完成,而不是由USBD在传输完成后通过回调函数通知上层应用的。发送传输完成后,要对传输的有效性进行检查,以确定传输的存在。然后调用USBD层的GetTransferStatus接口来查询传输的状态,只有当传输的状态为ERROR_SUCCESS时,才表明传输成功完成。最后是关闭管道,离开临界区。在上面的任何一个操作中出现错误,必须离开临界区并退出函数。中断输出传输和批量传输与上面介绍的步骤相同。不再另作介绍。
前面已经介绍过USBInstallDriver和USBUninstallDriver函数的作用,下面简单分析一下驱动中这两个函数的实现,其代码如程序清单 5‑35所示。
程序清单 5‑36 USBInstallDriver和USBUninstallDriver
BOOL USBInstallDriver (LPCWSTR szDriverLibFile)
{
BOOL bRet = FALSE; /* 函数返回值 */
USB_DRIVER_SETTINGS usbDriverSettings = {USBD12_DRIVER_SETTING};
/* USB_DRIVER_SETTINGS结构体 */
RETAILMSG(1, (TEXT("USBInstallDriver/n"))); /* 打印调试信息 */
bRet = RegisterClientDriverID(USBD12_DRIVERID); /* 向系统注册设备ID */
if (!bRet) { /* 注册失败,返回FALSE */
RETAILMSG(1, (TEXT("RegisterClientDriverID error:%d/n"), GetLastError()));
return FALSE;
}
bRet = RegisterClientSettings(szDriverLibFile, USBD12_DRIVERID,
0, &usbDriverSettings); /* 在注册表中注册驱动 */
if (!bRet) { /* 失败,返回FALSE */
RETAILMSG(1, (TEXT(" RegisterClientSettings Error : %d/n") , GetLastError()));
return FALSE;
}
return TRUE;
}
BOOL USBUnInstallDriver( void )
{
USB_DRIVER_SETTINGS usbDriverSettings = { USBD12_DRIVER_SETTING };
BOOL bRet = FALSE;
RETAILMSG(1, (TEXT("USBUNINSTALLDRIVER /n")));
bRet = UnRegisterClientDriverID(USBD12_DRIVERID); /* 删除设备ID */
if(!bRet) { /* 删除失败返回FALSE */
RETAILMSG(1, (TEXT("UnReigsterClientID ERROR : %d/n "), GetLastError()));
return FALSE;
}
bRet = UnRegisterClientSettings(USBD12_DRIVERID, NULL, &usbDriverSettings);
/* 删除驱动在注册表中的信息 */
if(!bRet) { /* 删除失败,返回FALSE */
RETAILMSG( 1 , (TEXT( " UnRegisterClientSettings ERROR : %d/n ") , GetLastError()) );
return FALSE;
}
return TRUE;
}
USBInstallDriver函数是在系统在注册表中找不到驱动时,调用该函数在注册表中注册设备的驱动,所以该函数最主要的目的就是注册驱动。根据USB_DRIVER_SETTINGS结构体的内容注册驱动,并且在系统中为驱动注册一个ID号[12]。
USBUninstallDriver函数的所完成的操作与USBInstallDriver函数完成的正好相反。删除驱动的ID,并且删除驱动在注册表中的信息。所有的这些操作都是调用USBD层的接口函数完成的。
在设备插入时,系统会调用驱动的USBDeviceAttach函数,该函数实现对设备插入的一些处理。这里主要是为设备分配索引号,注册该设备,创建设备相关的数据结构等。
在驱动内部维护着一个称为设备Tag的数据结构,该结构是一个长度为128位的数据。这个数据的每一位表示相应的一个设备的状态,1表示设备激活,0则反之。所谓的设备索引号就是设备在这128位中的顺序,bit0表示设备的索引号为0。索引号的分配是尽量小的索引号。在设备插入时,驱动会扫描这128位,确定一个最小的尚未使用的索引号。如程序清单 5‑37所示是该结构的定义;
程序清单 5‑37 设备Tag
struct __usb_tag {
ULONGLONG LowPart; /* 低64位 */
ULONGLONG HighPart; /* 高64位 */
};
typedef struct __usb_ta __usb_TAG;
typedef struct __usb_tag *PUSB_TAG;
为设备分配好索引号之后,就是要为设备创建相关的结构体(__USB_DEVICE)了。在驱动的本地堆中为__USB_DEVICE结构体分配空间,并作进一步的初始化,包括设置hUSBDevice、lpDeviceInfo、usbFuncs。然后就要对管道进行初始化,将管道的打开标志设置为否,初始化管道的临界区变量,设置管道端点的序号,并将Pipe设置为NULL。
下一步,要为设备创建注册表。驱动会在KEY_LOCAL_MACHINE/Drivers
/USB/ ClientDrivers/XXX键下以设备的索引号为名创建一个子键,这个子键包括设备驱动的DLL名称,驱动的前缀Prefix,驱动的顺序Order和驱动的索引Index。
现在的任务就是要在激活设备了。调用ActivateDevice函数,该函数有两个参数,第一个是设备的注册表键值,第二个可选的参数是与设备相关的句柄。ActivateDevice函数会在注册表HKEY_LOCAL_MACHINE/Drivers/Active键下为设备创建一个子键,并且将第一个参数所指定的注册表键下的键值复制到该子键下,同时将第二个参数设置在该子键的ClientInfo下。ActivateDevice函数返回一个句柄,在卸载设备时要用到,我们将这个句柄保存在设备__USBD12_DEVICE的hStreamDevice中。
最后为设备注册一个设备拔出回调函数。该回调函数会在设备拔出时由系统调用。到这里基本上的工作都完成了。
在Windows CE中串口的驱动实现是有固定模型的,Windows CE中的串口模型遵循ISO/OSI网络通讯模型(7层),就是说串口属于Windows CE网络模块的一个部分。其中rs232界面(或其它的物理介质)实现网络的物理层,而驱动和SerialAPI共同组成数据链路层,其它部分都没有做定义。
在典型的应用中,SerialAPI与间接通过TAPI或直接与ActiveSync交互,组成Windows CE网络的一部分。而红外本身的协议就相对复杂的多,它有专门的一套模型来描述其使用规则,对红外设备本身了解不多也就不能深入下去。在串口的这一侧,整个驱动模型也是相当的复杂的,但所幸的是驱动仅仅使用到SerialAPI这一层,在这个层次上串口的行为还是相对简单的。
在Windows CE提供的驱动例程中串口驱动采用分层结构设计,MDD提供框架性的实现,负责提供OS所需的基本实现,并将代码设计与具体的硬件设计无关。而PDD提供了对硬件操作相应的代码。这些代码通过结构HWOBJ来相互联系。对于MDD和PDD的整体驱动来看,串口驱动模型是作为Stream来实现的。
MDD和PDD两者合一以达到实现驱动的目的。DDSI就是指这两个部分之间的接口,这个接口并非受到强制的物理/逻辑关系来约束,而是人为的规定的。在涉及到一种特定硬件我们进行针对实现的时候往往需要的是了解硬件的物理特性和控制逻辑,然后根据DDSI的约束就来进行实现。对于这里描述的驱动模型而言结合关键在于结构指针HWOBJ的使用和具体实现。在实际的驱动应用中仅仅需要实现HWOBJ相关的一系列函数,而无需从驱动顶层完全开发。串口驱动模型作为一种常用驱动模型在Windows CE中常常用于串口USB Client的具体实现。该驱动模型中对全功能的串口进行了定义,除了常用的TX和RX引线定义以外,针对DTR、RTS等功能引脚都进行了支持,使得用该模型设计的串口驱动支持流控制、具备驱动Modem等设备的能力。
事实上,如果需要的话完全可以将该驱动一体化设计(抛开PDD和MDD的划分,也就无须DDSI)。也就是不使用现有的驱动架构来进行实现。考虑到串口驱动的使用频率和执行效率要求都不是很苛刻的情况下抛弃驱动架构另外实现的就没有多大必要了。
对于本系统而言,需要实现两个虚拟串口用来接收GPS接收器的数据和驱动3G Modern上网。
串口驱动本身分为MDD层和PDD层。MDD层对上层的Device Manager提供了标准的流接口设备驱动接口(COM_xxx),PDD层实现了HWOBJ结构及结构中若干针对于串口硬件操作的函数指针,这些函数指针将指向PDD层中的串口操作函数。DDSI是指MDD层与PDD层的接口,在串口驱动中实际上就是指HWOBJ,PDD层会传给MDD层一个HWOBJ结构的指针,这样MDD层就可以调用PDD层的函数来操作串口。在Windows CE中,串口驱动实际上就是一个流接口设备驱动,具体架构如图 51所示。
图 5.30 串口驱动框架
HWOBJ是相应的硬件设备操作的抽象集合。其中BandFlags指定IST的启动时间,可选为在初始化过程启动或是在打开设备的时候起动ISR,而第二个参数则是指定拦截的具体的系统中断号。最后一个参数是一个结构,该结构定义了硬件操作的各式行为函数的指针,MDD正是通过这些函数来访问具体的PDD操作,HWOBJ结构体如程序清单 5‑38所示;
程序清单 5‑38 HWOBJ结构体
typedef struct __HWOBJ {
ULONG BindFlags; // Flags controlling MDD behaviour. Se above.
DWORD dwIntID; // Interrupt Identifier used if THREAD_AT_INIT or THREAD_AT_OPEN
PHW_VTBL pFuncTbl;
} HWOBJ, *PHWOBJ;
HW_VTBL则是代表具体硬件操作函数指针的集合,该结构所指向的函数包括了初始化、打开、关闭、接收、发送、设置Baudrate等一系列操作。结构存在就像纽带一样联系着PDD中的具体实现和MDD中的抽象操作。PDD的实现必须遵循HW_VTBL中所描述的函数形式,并构造出相应的HW_VTBL实例。驱动的编写就是针对这些函数来一一进行实现,HW_VTBL结构体原型如程序清单 5‑39所示;
程序清单 5‑39 HW_VTBL结构体
typedef struct __HW_VTBL {
PVOID (*HWInit)(ULONG Identifier, PVOID pMDDContext, PHWOBJ pHWObj);
BOOL (*HWPostInit)(PVOID pHead);
ULONG (*HWDeinit)(PVOID pHead);
BOOL (*HWOpen)(PVOID pHead);
ULONG (*HWClose)(PVOID pHead);
INTERRUPT_TYPE (*HWGetIntrType)(PVOID pHead);
ULONG (*HWRxIntrHandler)(PVOID pHead, PUCHAR pTarget, PULONG pBytes); copyright sql163.com
VOID (*HWTxIntrHandler)(PVOID pHead, PUCHAR pSrc, PULONG pBytes);
VOID (*HWModemIntrHandler)(PVOID pHead);
VOID (*HWLineIntrHandler)(PVOID pHead);
ULONG (*HWGetRxBufferSize)(PVOID pHead);
BOOL (*HWPowerOff)(PVOID pHead);
BOOL (*HWPowerOn)(PVOID pHead);
VOID (*HWClearDTR)(PVOID pHead);
VOID (*HWSetDTR)(PVOID pHead);
VOID (*HWClearRTS)(PVOID pHead); copyright sql163.com
VOID (*HWSetRTS)(PVOID pHead);
BOOL (*HWEnableIR)(PVOID pHead, ULONG BaudRate);
BOOL (*HWDisableIR)(PVOID pHead);
VOID (*HWClearBreak)(PVOID pHead);
VOID (*HWSetBreak)(PVOID pHead);
BOOL (*HWXmitComChar)(PVOID pHead, UCHAR ComChar);
ULONG (*HWGetStatus)(PVOID pHead, LPCOMSTAT lpStat);
VOID (*HWReset)(PVOID pHead);
VOID (*HWGetModemStatus)(PVOID pHead, PULONG pModemStatus);
VOID (*HWGetCommProperties)(PVOID pHead, LPCOMMPROP pCommProp);
VOID (*HWPurgeComm)(PVOID pHead, DWORD fdwAction);
BOOL (*HWSetDCB)(PVOID pHead, LPDCB pDCB);
BOOL (*HWSetCommTimeouts)(PVOID pHead, LPCOMMTIMEOUTS lpCommTO);
BOOL (*HWIoctl)(PVOID pHead, DWORD dwCode,PBYTE pBufIn,DWORD dwLenIn,
PBYTE pBufOut,DWORD dwLenOut,PDWORD pdwActualOut);
} HW_VTBL, *PHW_VTBL;
为保障对系统架构的清晰认识,我们将MDD的代码和PDD的代码分开进行分析。
由于串口驱动由Device.exe直接调用,所以MDD部分是以完整的Stream接口给出的. 也就具备基于Stream接口的驱动程序所需的函数实现,包括COM_Init、COM_Deinit 、COM_Open、COM_Close、COM_Read、COM_Write、COM_Seek、 COM_PowerUp, COM_PowerDown、COM_IOControl几个基本实现。由于串口发送/接收的信息并不能定位,而仅仅是简单的传送,所以COM_Seek仅仅是形式上实现了一下。
COM_Init是该驱动的初始化函数,在设备管理器加载该驱动后首先调用,用于初始化所需的变量,硬件设备等资源。该过程分配代表设备硬件实例的数据结构,并通过硬件抽象接口HWInit初始化硬件。同时该函数会调用InterruptInitialize为接收内核中的逻辑中断创建相应事件并初始化临界区。该函数还需要得到硬件缓冲器的物理地址和获知该缓冲器的大小(该冲器最小为2K)。最后它将建立相应的缓冲作为接收的中介。下面我们来看这个函数的实现过程。
在函数中定义了两个重要的变量。pSerialHead和pHWHead,前者用于描述相应的串口的状态,后者则是对应硬件的数据抽象。首先为pSerialHead分配空间和初始化链表和临界区等数据并同时为接收和发送中断创建事件。然后再从注册表中获得当前的注册项值(由于device.exe是根据注册表键值来调用驱动的,当前键注册表项指的就是与上述键值在同一根下的注册项)。得到DeviceArrayIndex、Priority256键下的具体值后就可以调用GetSerialObject(在PDD中实现)来获得具体的HWObj对象,并通过该对象来调用硬件初始化函数了。由于在这里已经对硬件进行了初始化,之后的返回都需要调用COM_Deinit来完成。由于硬件初始化(实际的驱动初始化代码)已经得到执行这个时候就只有分配和初始化缓冲的工作需要做了。所以调用HWGetRxBufferSize(PDD代码)来获取PDD中设定的缓冲大小,并根据返回的具体值分配缓冲。最后如果BindFlags被设定为THREAD_AT_INIT就再调用StartDispatchThread启动分发线程(实际的IST),这样就完成了系统初始化的操作。
当驱动被称被卸下的时候该事件启动,用作与COM_Init相反的操作。这个过程大致会释放驱动中所使用的资源,停止期间创建的线程等操作。具体说来,大致为停止在MDD中的所有IST,和释放内存资源和临界区等系统资源。同时还需调用HWDeinit来释放PDD中所使用到的系统资源。
COM_Oepn在CreateFile后被调用,用于以读/写模式打开设备,并初始化所需要的空间/资源等,创建相应的实例,为后面的操作做好准备。这里的代码相对比较容易,下面就简单讲一下。既然是初始化,肯定就免不了对参数的检查。首先检查通过COM_Init返回的pHead结构是否有效,这里虽然没有显式的在这两个函数之间传递参数,而是在设备管理器的内部传递这个参数的。
之后是检查文件系统传递过来的Open句柄中的Open模式是否有效,这个参数由应用程序产生,通过文件系统直接传递到驱动。之后就开始初始化的操作,在这里将会建立相应的HW_OPEN_INFO实体,该结构的定义如程序清单 5‑40所示。
程序清单 5‑40 HW_OPEN_INFO结构体
typedef struct __HW_OPEN_INFO {
PHW_INDEP_INFO pSerialHead; // @field Pointer back to our HW_INDEP_INFO
DWORD AccessCode; // @field What permissions was this opened with sql163.com
DWORD ShareMode; // @field What Share Mode was this opened with
DWORD StructUsers; // @field Count of threads currently using struct.
COMM_EVENTS CommEvents; // @field Contains all in…. handling
LIST_ENTRY llist; // @field Linked list of OPEN_INFOs
} HW_OPEN_INFO, *P HW_OPEN_INFO;
结构中的第一个参数指向我们前面提到的HW_INDEP_INFO结构,第二个参数为操作权限码,也就是READ/WRITE这类的权限。第三个参数为共享模式,以确定是否支持独占。这两个参数都是与文件系统的内容对应的。而CommEvent则对应于本实例的事件。由于驱动架构支持多个OPEN操作实例的存在,所以这里维护了一个链表来联系这些结构。在这里由于IST的启动可以在COM_Init和COM_Open中进行,还有处理器启动IST的内容。准备好HW_OPEN_INFO结构后就可以调用HWOpen(PDD)来进行PDD所需的Open操作了。Open操作完成后调用HWPurgeComm(PDD)来处理(取消或等待)当前仍在通讯状态的任务。然后重置软件FIFO就基本完成了COM_Open的动作了。
事实上这里主要是对所需的数据结构进行处理,对于硬件的具体操作都留给PDD去做了,MDD所维护的仅仅是一个架构性的代码。Open操作完成后,驱动就进入了工作状态这个时候。
COM_Close为与COM_Open相对应的操作。这期间的目的是释放COM_Open所使用的系统资源,除此以外如果在COM_Open期间创建了相应的IST还需要停止该线程,在最后将该HW_OPEN_INFO脱链。这样一来驱动状态就得以恢复。当然这期间还做了一写避免线程竞争的处理,使得代码看起来不是那么简单。
这两个函数都不是Stream所需要的标准接口,但却是中断服务程序所需的IST启动和关闭的手段,所以在这里顺便说一下。StartDispatchThread函数用于启动IST,主要的工作为调用InterruptInitialize将系统中断与相应的事件联系起来。并启动SerialDispatchThread作为IST。其中调用了叫做 InterruptDone的函数,该函数会调用OAL中的OEMInterruptDone来完成中断的响应。
StopDispatchThread用于与StartDispatchThread相反的操作。停止的过程相对要复杂一些,该函数首先设定当前线程的优先级与分发线程相同,以便于在停止该线程的动作不会比释放内存的动作快以避免出错。停止的动作是让线程主动完成的,具体的方法是提交表示位KillRxThread然后通过Sleep请求调度,待到IST自己停止。这个时候由于IST已经停止所以在程序的最后调用InterruptDisable来屏蔽中断
SerialDispatchThread/ SerialEventHandler就是串口驱动的中断分发程序(也就是IST的所在)。整个IST被分开写成两个部分——循环主体和事件处理程序。循环主体SerialDispatchThread内容相对比较简单,反复等待串口事件并调用SerialEventHandler对具体的中断进行处理,直到pSerialHead->KillRxThread被设置后退出。SerialEventHandler为中断处理的具体实现,程序在获得串口事件后运行,目的在于对中断进行进一步的判断并执行相应的处理。如程序清单 5‑41两个结构体RX_BUFFER_INFO和TX_BUFFER_INFO就是用来完成接受和发送中断服务。
程序清单 5‑41 接收和发送数据结构体
typedef struct __RX_BUFFER_INFO {
ULONG Read; /* @field Current Read index. */
ULONG Write; /* @field Current Write index. */
ULONG Length; /* @field Length of buffer */
BOOL DataAvail; /* @field BOOL reflecting existence of data. */
PUCHAR RxCharBuffer; /* @field Start of buffer */
CRITICAL_SECTION CS; /* @field Critical section */
} RX_BUFFER_INFO, *PRX_BUFFER_INFO;
typedef struct __TX_BUFFER_INFO {
DWORD Permissions; /* @field Current permissions */
ULONG Read; /* @field Current Read index. */
ULONG Length; /* @field Length of buffer */
PUCHAR TxCharBuffer; /* @field Start of buffer */
CRITICAL_SECTION CS; /* @field Critical section */
} TX_BUFFER_INFO, *PTX_BUFFER_INFO;
COM_Read是获取串口所接收到数据的操作,在前面的IST中没有看到对RX buffer进行修改Read标记的操作,也就是这儿来完成的。该函数有三个参数,第一个参数是从上面的COM_OPEN通过设备管理器交换来的,后两个参数与文件系统的使用方法完全一样,一个是接受缓冲指针,另一个是长度。代码的开始照样是例行公事的参数检查,包括对存取权限,OpenCnt等。之后计算超时时间,如果设定了超时读取动作会在超时后返回,不管是否读到了足够长度的数据。随后就是简单对软件缓冲进行读取的操作了,读取的操作是在RX_CS中完成的。下面要处理器的主要就是几种异常的情形,读取过程中设备被关闭/取消读取和超时。最后在读取的过程中需要处理的就只是流控制的成本了。首先是软件流的情形,如果缓冲的状态由高于分位点至分位点以下就发出XON标记,启动发送端的发送。而硬件流的情形无论是RTS还是DTR与软件流的相类似,同样由一个分为点(50%)来决定发出启动发送端的信号,仅仅是这里使用的具体措施的不同。这些硬件信号的发出都是由PDD来完成的,其中包括HWSetRTS和HWSetDTR(2选一),至此Read的流程就结束了。
COM_Write是与COM_Read相对应的操作。所传递的参数的形式也是很相似的,仅仅是数据流向的不同。在程序的开始,同样也是参数检查,内容与COM_Read一致。在数据检查完成之后进入临界区(保障多线程下的独占)将送入的目标地址和长度设置为TX buffer,待到数据发送完成事件后调用DoTxData来启动发送。这里启动发送的目的在于获得硬件中断维持发送流程。在这里DoTxData是作为两种状态来执行的,在通过COM_Write的执行的过程中是在device.exe所创建的线程空间内执行的,但由系统中断事件主动启动的过程中属于IST本身的的进程空间,这样在COM_Write中调用DoTxData之前设置的权限代码(由GetCurrentPermissions获得)就可以由TxBufferInfo传递到IST中去使得中断过程也具备了访问缓冲的权限(结合前面说明IST的流程)。当提交中断处理发送后待到pSerialHead->hTransmitEvent被设置或是异常或超时后就结束了发送流程,在这部分的最后。与COM_Read类似需要处理一些异常情况,当然如果使用了硬件流控制还需要在这里清除掉发送请求信号,当这些状态处理完成以后发送EV_TXEMPTY事件通告所有open的句柄发送结束就完成了该部分的流程。
这两个函数的调用都由CE的电源事件来引发,MDD并没有对这两个函数进行处理,仅仅是将其传递给PDD。
该函数用于实现向设备发送命令的功能。由于代码本身没有什么流程或逻辑性可言,全都是单独的实现,下面就用列表部分命令字及其说明。
l IOCTL_SERIAL_SET_BREAK_ON:中断(暂停)serial当前的发送或是接收,具体实现在PDD中。
l IOCTL_SERIAL_SET_BREAK_OFF:从中断(暂停)状态恢复,具体实现在PDD中。
l IOCTL_SERIAL_SET_DTR:将DTR引线拉高,直接调用PDD实现。
l IOCTL_SERIAL_CLR_DTR:将DTR引线拉低,直接调用PDD实现。
l IOCTL_SERIAL_SET_RTS:将RTS引线拉高,直接调用PDD实现。
l IOCTL_SERIAL_CLR_RTS:将RTS引线拉低,直接调用PDD实现。
l IOCTL_SERIAL_SET_XOFF:软件流模式下中止数据发送(Xflow控制)。
l IOCTL_SERIAL_SET_XON:软件流模式下启动数据发送(XFlow控制)。
l IOCTL_SERIAL_GET_WAIT_MASK:获取当前的事件对象。
l IOCTL_SERIAL_SET_WAIT_MASK:等待与提供的事件相同的事件发生。
如前面所述,MDD正是通过HWOBJ结构体来访问具体的PDD操作,虽然在实现串口驱动过程当中,PDD部分并不需要做太多修改,甚至有时候不需要修改,但是了解其PDD层驱动构造将对编写MDD层驱动有很大的帮助,如下详细介绍。
MDD正是通过HWOBJ结构体来访问具体的PDD操作,也正是因为HWOBJ结构体使得MDD和PDD层联系起来,但在HWOBJ最重要的成员是pFuncTbl,它是HW_VTBL结构体指针,也是MDD和PDD联系中最重要的桥梁,HW_VTBL结构体如程序清单 5‑39所示。
在HW_VTBL结构体中,共有32个函数指针,分别用来指向串口驱动PDD层的物理设备的32个函数。由此可见编写串口PPD层驱动程序实际上就是需要实现HW_VTBL结构体中的32个函数。
如何构建MDD层这样的HW_VTBL对象了,如程序清单 5‑42代码所示,其中SerInit、SerPostInit、SerDeinit等变量都是函数已经实现的函数指针。
程序清单 5‑42 构造HW_VTBL结构体
const
HW_VTBL IoVTbl = {
SerInit,
SerPostInit,
SerDeinit,
SerOpen,
SerClose,
SerGetInterruptType,
SerRxIntr,
SerTxIntrEx,
SerModemIntr,
SerLineIntr,
SerGetRxBufferSize,
SerPowerOff,
SerPowerOn,
SerClearDTR,
SerSetDTR,
SerClearRTS,
SerSetRTS,
SerEnableIR,
SerDisableIR,
SerClearBreak,
SerSetBreak,
SerXmitComChar,
SerGetStatus,
SerReset,
SerGetModemStatus,
SerGetCommProperties,
SerPurgeComm,
SerSetDCB,
SerSetCommTimeouts,
SerIoctl
};
获得HW_VTBL结构体只是HWOBJ的一部分,接下需要构建HWOBJ整体,如程序清单 5‑43代码所示;
程序清单 5‑43 构造HWOBJ对象
extern "C" PHWOBJ
GetSerialObject(
DWORD DeviceArrayIndex
)
{
PHWOBJ pSerObj;
pSerObj=(PHWOBJ)LocalAlloc( LPTR ,sizeof(HWOBJ) );
if ( !pSerObj )
return (NULL);
pSerObj->BindFlags = THREAD_IN_PDD; // PDD create thread when device is first attached.
pSerObj->dwIntID = DeviceArrayIndex; // Only it is useful when set set THREAD_AT_MDD. We use this to transfer DeviceArrayIndex
pSerObj->pFuncTbl = (HW_VTBL *) &IoVTbl; // Return pointer to appropriate function
return (pSerObj);
}
COM_Init()函数在获得串口PDD层的对象后,接下来通过调用PDD层函数表中的SerInit()函数来初始化串口硬件,并将COM_Init()函数读到的串口注册表中Device ArrayIndex值传递给SerInit()函数的Identifier变量,SerInit()函数代码如程序清单 5‑44所示;
程序清单 5‑44 SerInit()函数
PVOID
SerInit(
ULONG Identifier, // @parm Device identifier.
PVOID pMddHead, // @parm First argument to mdd callbacks.
PHWOBJ pHWObj // @parm Pointer to our own HW OBJ for this device
)
{
DEBUGMSG (ZONE_CLOSE,(TEXT("+SerInit, 0x%X/r/n"), Identifier));
CSerialPDD * pSerialPDD = NULL;
if (pHWObj) {
DWORD dwIndex= pHWObj->dwIntID;
pHWObj->dwIntID = 0;
pSerialPDD = CreateSerialObject((LPTSTR)Identifier,pMddHead, pHWObj,dwIndex);
}
if (pSerialPDD==NULL) {
ASSERT(FALSE);
LocalFree(pHWObj);
}
DEBUGMSG (ZONE_CLOSE,(TEXT("-SerInit, 0x%X/r/n"), pSerialPDD));
return pSerialPDD;
}
SerInit()函数调用PDD层的CreateSerialObject函数获取串口PDD层的一个串口类,然后返回指向该串口对象的指针,其中Identifier指明了要获得的串口PDD层对象的类型,这样MDD层就真正获得了PDD层的对象,MDD层就和PDD层联系起来了。这样通过MDD层就可以通过调用PDD层的函数来操作串口硬件。
接下来看看MDD层调用SerOpen()函数时,最终怎样调用PDD层类中的Open方法,如程序清单 5‑45所示;
程序清单 5‑45 SerOpen函数
BOOL
SerOpen(
PVOID pHead /*@parm PVOID returned by Serinit. */
)
{
DEBUGMSG (ZONE_OPEN, (TEXT("SerOpen (%X)/r/n"),pHead));
CSerialPDD * pSerialPDD = (CSerialPDD *)pHead;
BOOL bReturn = FALSE;
if ( pSerialPDD) {
bReturn = pSerialPDD->Open();
}
DEBUGMSG (ZONE_OPEN, (TEXT("-SerOpen(%X) return %d /r/n"),pSerialPDD,bReturn));
return bReturn;
}
该类实现标准16550串口类的所有方法,即MDD层用到的32个函数,它继承自Cserial类和CminiThread类。串口PDD类之所以要继承CminiThread类,是因为它要处理串口终端,用到了一个终端服务线程,因此它继承了CminiThread类。程序清单 5‑46是串口PDD类的原型。
程序清单 5‑46 串口PDD类原型
class CPdd16550 : public CSerialPDD, public CMiniThread {
public:
CPdd16550 (LPTSTR lpActivePath, PVOID pMdd, PHWOBJ pHwObj);
virtual ~CPdd16550();
virtual BOOL Init();
virtual void PostInit();
virtual BOOL MapHardware();
virtual BOOL CreateHardwareAccess();
// Power Manager Required Function.
virtual void SerialRegisterBackup() { m_pReg16550->Backup(); };
virtual void SerialRegisterRestore() { m_pReg16550->Restore(); };
// Implement CPddSerial Function.
// Interrupt
virtual BOOL InitialEnableInterrupt(BOOL bEnable ) ; // Enable All the interrupt may include Xmit Interrupt.
private:
virtual DWORD ThreadRun(); // IST
// Tx Function.
public:
virtual BOOL InitXmit(BOOL bInit);
virtual void XmitInterruptHandler(PUCHAR pTxBuffer, ULONG *pBuffLen);
virtual void XmitComChar(UCHAR ComChar);
virtual BOOL EnableXmitInterrupt(BOOL bEnable);
virtual BOOL CancelXmit();
virtual DWORD GetWriteableSize();
protected:
BOOL m_XmitFifoEnable;
HANDLE m_XmitFlushDone;
// Rx Function.
public:
virtual BOOL InitReceive(BOOL bInit);
virtual ULONG ReceiveInterruptHandler(PUCHAR pRxBuffer,ULONG *pBufflen);
virtual ULONG CancelReceive();
virtual DWORD GetWaterMark();
virtual BYTE GetWaterMarkBit();
protected:
BOOL m_bReceivedCanceled;
DWORD m_dwWaterMark;
// Modem
public:
virtual BOOL InitModem(BOOL bInit);
virtual void ModemInterruptHandler() { GetModemStatus();};
virtual ULONG GetModemStatus();
virtual void SetDTR(BOOL bSet);
virtual void SetRTS(BOOL bSet);
// Line Function.
virtual BOOL InitLine(BOOL bInit) ;
virtual void LineInterruptHandler() { GetLineStatus();};
virtual void SetBreak(BOOL bSet) ;
virtual BOOL SetBaudRate(ULONG BaudRate,BOOL bIrModule) ;
virtual BOOL SetByteSize(ULONG ByteSize);
virtual BOOL SetParity(ULONG Parity);
virtual BOOL SetStopBits(ULONG StopBits);
// Line Internal Function
BYTE GetLineStatus();
protected:
CReg16550 * m_pReg16550;
PVOID m_pRegVirtualAddr;
BOOL m_bIsIo;
DWORD m_dwRegStride;
CRegistryEdit m_ActiveReg;
// Interrupt Handler
DWORD m_dwSysIntr;
HANDLE m_hISTEvent;
// Optional Parameter
DWORD m_dwDevIndex;
};
S3C2440A芯片最多可以支持4个COM口,正如上述,S3C2440A串口驱动程序先定义一个CReg2440Uart ,用于保存相关数据和寄存器操作方法,随后定义一个CPdd2440Uart类使其从CSerialPDD和CminiThread中继承,紧接着实现相关函数,关于CReg2440Uart类和CPdd2440Uart类的定义如程序清单 5‑47所示;
程序清单 5‑47 CReg2440Uart类和CPdd2440Uart定义
class CReg2440Uart {
public:
CReg2440Uart(PULONG pRegAddr);
virtual ~CReg2440Uart() { ; };
virtual BOOL Init() ;
// We do not virtual Read & Write data because of Performance Concern.
void Write_ULCON(ULONG uData) { WRITE_REGISTER_ULONG( m_pReg, (uData)); };
ULONG Read_ULCON() { return (READ_REGISTER_ULONG(m_pReg)); } ;
......
void Write_UBRDIV(ULONG uData) { WRITE_REGISTER_ULONG( m_pReg + 10, uData );};
ULONG Read_UBRDIV() { return READ_REGISTER_ULONG(m_pReg + 10); };
virtual BOOL Write_BaudRate(ULONG uData);
PULONG GetRegisterVirtualAddr() { return m_pReg; };
virtual void Backup();
virtual void Restore();
#ifdef DEBUG
virtual void DumpRegister();
#endif
protected:
volatile PULONG const m_pReg;
BOOL m_fIsBackedUp;
private:
......
ULONG m_BaudRate;
ULONG m_s3c2440_pclk;
};
class CPdd2440Uart: public CSerialPDD, public CMiniThread {
public:
CPdd2440Uart (LPTSTR lpActivePath, PVOID pMdd, PHWOBJ pHwObj);
virtual ~CPdd2440Uart();
virtual BOOL Init();
virtual void PostInit();
virtual BOOL MapHardware();
virtual BOOL CreateHardwareAccess();
// Power Manager Required Function.
virtual void SerialRegisterBackup() { m_pReg2440Uart->Backup(); };
virtual void SerialRegisterRestore() { m_pReg2440Uart->Restore(); };
// Implement CPddSerial Function.
// Interrupt
virtual BOOL InitialEnableInterrupt(BOOL bEnable ) ; // Enable All the interrupt may include Xmit Interrupt.
private:
virtual DWORD ThreadRun(); // IST
// Tx Function.
public:
virtual BOOL InitXmit(BOOL bInit);
.......
virtual DWORD GetWriteableSize();
protected:
BOOL m_XmitFifoEnable;
HANDLE m_XmitFlushDone;
//
// Rx Function.
public:
virtual BOOL InitReceive(BOOL bInit);
.......
protected:
BOOL m_bReceivedCanceled;
DWORD m_dwWaterMark;
//
// Modem
public:
......
virtual void SetDTR(BOOL bSet) {;};
virtual void SetRTS(BOOL bSet);
//
// Line Function.
virtual BOOL InitLine(BOOL bInit) ;
virtual void SetBreak(BOOL bSet) ;
virtual BOOL SetBaudRate(ULONG BaudRate,BOOL bIrModule) ;
......
protected:
CReg2440Uart * m_pReg2440Uart;
PVOID m_pRegVirtualAddr;
volatile S3C2440A_INTR_REG * m_pINTregs;
DWORD m_dwIntShift;
public:
.....
void ClearInterrupt(DWORD dwInt) {
m_pINTregs->SUBSRCPND = (dwInt<
}
.......
};
由于我们直接购买的一个USB接口的GPS接收器,其中集成了PL2303串口芯片,所有的数据由COM获取,从USB接口输出到平台终端。由于众多导航软件都是采用COM数据,在市场上USB接口的GPS接收器Windows CE驱动很少,为了操作方便并合导航软件兼容,在本系统中自主实现一个USB转串口的驱动,该驱动程序其实很简单,在Windows CE6.0目录下可以找到一些相关的Demo代码,可供参考编写。由于GPS主要是一个USB转串口的驱动程序,完整的介绍GPS的所有代码未免与前面5.5、5.6节重复,在此只介绍GPS驱动的整体框架,通过这个整体框架,综合前面两节的内容就可以编写出GPS驱动程序。
GPS驱动其实很简单,主要是一个USB转串口驱动,由于USB是即插即用的设备。因此、驱动的初始化和结束由USB设备插拔决定。USB转串口驱动其实就是在USB驱动的上虚拟一个串口,再通过串口串口的方式来操作USB设备。整个驱动框架非常明显,GPS驱动由两部分组成一是USB驱动部分,二是虚拟串口驱动部分。其中串口驱动部分又要分三部分数据接收部分、数据发送部分、串口状态部分。当USB设备初始化的时候,相应的也创建虚拟串口中的接收、发送、状态相关的对象,代码如程序清单 5‑48所示;
程序清单 5‑48 GPS驱动初始化
if (m_lpUsbClientDevice && CSerialPDD::Init()) {
LPCUSB_INTERFACE lpTargetInterface = m_lpUsbClientDevice->GetTargetInterface();
while (dwEndpoints && lpEndpoint!=NULL) {
......
if (USB_ENDPOINT_DIRECTION_IN(lpEndpoint->Descriptor.bEndpointAddress)) {
CreateBulkIn(lpEndpoint);}
else {
CreateBulkOut(lpEndpoint); }
......
if ((lpEndpoint->Descriptor.bmAttributes & USB_ENDPOINT_TYPE_MASK) == USB_ENDPOINT_TYPE_INTERRUPT &&
USB_ENDPOINT_DIRECTION_IN(lpEndpoint->Descriptor.bEndpointAddress)) {
CreateStatusIn(lpEndpoint);
......
return(m_lpSerialDataIn!=NULL && m_lpSerialDataOut!=NULL);
USB初始化完成之后,创建了相应的虚拟串口,在本系统中该串口采用“COM1“命名,下图 5.31是整个GPS驱动的工作流程,也是GPS驱动的实现过程。
图 5.31 GPS驱动流程图
编译获得USBSER.DLL驱动文件之后,加载到系统中即可使用,如下介绍具体的加载过程。
第一、添加注册表到platform.reg文件当中,该注册表信息如程序清单 5‑48所示;
程序清单 5‑49 GPS驱动加载注册表
[HKEY_LOCAL_MACHINE/Drivers/USB/LoadClients/Default/0_0_0/255_0_0/PL_USBSER_Driver]
"Dll"="USBSER"
[HKEY_LOCAL_MACHINE/Drivers/Active/33]
"Hnd"=dword:00809F00
"Name"="COM1:"
"Key"="Drivers//USBSER"
"ClientInfo"=dword:00000000
[HKEY_LOCAL_MACHINE/Drivers/USBSER]
"FriendlyName"="GPS"
"Order"=dword:00000005
"Dll"="USBSER.DLL"
"Prefix"="COM"
"DeviceArrayIndex"=dword:00000001
第二、将USBSER.DLL文件拷贝到BSP目录下Files文件夹中,并将如程序清单 5‑49代码加入到platform.bib文件当中。
程序清单 5‑50 bib文件相关设置
USBSER.dll $(_FLATRELEASEDIR)/USBSER.dll NK SH
第三、编译,完成之后下载到内核当中即可。
第四、测试是否接收到正确数据,实物界面如图 5.31所示。
图 5.32 GPS实物图