1、作者:付林林我想即使读者看过微软的关于驱动开发的培训教材和 CE 帮助文档中的驱动部分,头脑中仍然一片茫然。要想真正了解驱动程序必须结合一些驱动程序源码,在此我以串口驱动程序(COM16550)中初始化过程为线索简单讲一讲驱动开发的基础知识。Windows CE 下的串口驱动程序能够处理所有 I/O 行为类似串口的设备,包括基于16450、 16550 UART(通用异步收发芯片)的设备和一些采用 DMA 的设备,常见的有 9针串口、红外 I/O 口、Modem 等。在%_WINCEROOT%PublicCommonOAKDriversSerial 目录下,COM_MDD2 子目录包含新的串口
2、驱动 MDD 层函数代码。COM16550 子目录包含串口驱动 PDD 层代码。SER16550 子目录包含的一系列函数专用于控制与 16550 兼容的 UART,这样 PDD 层的主要工作就是调用 SER16550 中的函数。还有一个 ISR16550 子目录包含的是串口驱动程序专用的可安装 ISR(中断服务例程),而很多硬件设备驱动程序采用 CE 默认的可安装ISR giisr.dll。一般串口设备相应的注册表设置例子及意义如下:HKEY_LOCAL_MACHINEDriversBuiltInSerial_1键 意义“SysIntr“=dword:13 串口 1 的中断 ID 为十进制 1
3、3“IoBase“=dword:02F8 串口 1 的 IO 空间首地址为十六进制 2F8“IoLen“=dword:8 串口 1 的 IO 空间长度为 8 个字节“DeviceArrayIndex“=dword:0 串口 1 的索引,是 1 的由来“Order“=dword:0 串口 1 驱动的加载顺序“DeviceType“=dword:0 串口 1 的设备类型“DevConfig“=hex: 10,00 串口 1 在与 Modem 设备通讯时的配置,如波特率、奇偶校检等“FriendlyName“=“COM1:“ 串口 1 在拨号程序中显示的名字“Tsp“=“Unimodem.dll“
4、串口 1 被用于与 Modem 设备通讯的时候要加载的 TSP(TAPI Service provider)DLL“Prefix“=“COM“ 串口 1 的流接口的前缀“Dll“=“com16550.Dll“ 串口 1 的驱动程序 DLLSysIntr 由 CE 在文件 Nkintr.h 中预定义,用于唯一标识中断设备。OEM 可以在文件Oalintr.h 中定义自己的 SysIntr。常见的预定义 SysIntr 有 SYSINTR_NOP(中断只由 ISR处理,IST 不再处理),SYSINTR_RESCHED(重新调度线程),SYSINTR_DEVICES(由 CE 预定义的设备中断 I
5、D 的基值),SYSINTR_PROFILE、SYSINTR_TIMING、SYSINTR_FIRMWARE 等都是基于SYSINTR_DEVICES 定义的。IoBase 是串口 1 的 IO 地址空间的首地址,IoLen 是 IO 空间的大小。IO 地址空间只存在于 x86 平台,如果在其它平台硬件寄存器必须映射到物理地址空间,那子键的名称为 MemBase 和 MemLen。在 x86 平台更多硬件的寄存器由于 IO空间的局限也映射到物理地址空间。DeviceArrayIndex 是设备的索引,用于区分同类型的设备。Prefix 是流驱动程序的前缀,当应用程序调用 CreateFile
6、函数传递 COM1:参数时,文件系统负责与串口驱动程序通信,串口驱动程序是在 CE 启动时由 device.exe 加载的。下面从 MDD 层函数 COM_Init 开始探索串口驱动的初始化过程。COM_Init 是在串口设备被检测后由设备管理器 device.exe 调用的,主要的作用是初始化设备,它的唯一参数Identifier 是由 device.exe 传递的,其类型是一个字符串指针,字符串的内容是HLMDriversActivexx,xx 是一个十进制数(device.exe 会跟踪系统中每个驱动程序,把加载的驱动程序记录在 Active 键下)。COM_Init 先分配一个 HW_
7、INDEP_INFO 结构体,这个结构体是独立于串口硬件的头信息(MDD 、PDD、SER16550 都包含自己独特的结构体,具体的结构体定义请参见串口驱动源码),分配之后再初始化结构体中每个成员,初始化结构体后调用 OpenDeviceKey(LPCTSTR)Identifier)打开 HLMDriversActivexxKey 包含的注册表路径,在这里路径一般为 HLMDriversBuiltInSerial,即串口的驱动程序信息在注册表中所处的位置。COM_Init 接着在 HLMDriversBuiltInSerial 下查询 DeviceArrayIndex、Priority256的
8、值,Priority256 指定了驱动程序的优先级,如果没有就用默认的优先级。接下来调用GetSerialObject(DeviceArrayIndex),这个函数由 PDD 层定义,返回 HWOBJ 结构体,这个结构体主要包含 PDD 层和 SER16550 定义的函数的指针。也就是说 MDD 通过调用这个函数才能调用底层实现的函数。接下来的大多数工作都是调用底层函数实现初始化。第一个调用的底层函数 SerInit 主要设置由用户设置的硬件配置,例如线路控制、波特率。它调用 Ser_GetRegistryData 函数得到保存在注册表中的硬件信息,Ser_GetRegistryData 在内
9、部调用系统提供的 DDKReg_GetIsrInfoDDK 和DDKReg_GetWindowInfo 函数得到在 HLMDriversBuiltInSerial 下保存的IRQ、SysIntr、IsrDll 、IsrHandler、IoBase 、IoLen 。IRQ 是逻辑中断号,IsrDll 表示当前驱动程序的可安装 ISR 所在的 DLL 名称,IsrHandler 表示可安装 ISR 的函数名称。在这里顺便提一下可安装 ISR,读者在我以前发表的关于 OAL 的文章中可以了解到OEM 在 OEMInit 函数中关联 IRQ 和 SysIntr,当硬件设备发生中断时,ISR 会禁止同级
10、和低级中断,然后根据 IRQ 返回关联的 SysIntr,内核根据 ISR 返回的 SysIntr 唤醒相应的IST(SysIntr 与 IST 创建的 Event 关联),IST 处理中断之后调用 InterruptDone 解除中断禁止。在 OEMInit 中关联的缺点是一旦编译了 CE 内核后就无法添加这种关联了,而一些硬件设备会随时插拔或者共享中断,要关联这样的硬件设备解决方法就是可安装 ISR,可安装 ISR 专用于处理指定的硬件设备发出的中断,所以如果硬件设备需要可安装 ISR 必须在注册表中添加 IsrDll、IsrHandler 。多数硬件设备采用 CE 默认的可安装 ISR
11、giisr.dll,格式如下:“IsrDll“=“giisr.dll“IsrHandler“=“ISRHandler“如果一个硬件驱动程序需要可安装 ISR 而开发者又不想自己写一个,那么可以利用giisr.dll 来实现。除了在注册表中添加如上所示外,还要在驱动程序中调用相关函数注册可安装 ISR。伪代码如下:g_IsrHandle = LoadIntChainHandler(IsrDll, IsrHandler, (BYTE)Irq);GIISR_INFO Info;PHYSICAL_ADDRESS PortAddress = PhysAddr, 0;TransBusAddrToStati
12、c(BusType, dwBusNumber, PortAddress, dwAddrLen, Info.CheckPort = TRUE;Info.PortIsIO = (dwIOSpace) ? TRUE : FALSE;Info.UseMaskReg = TRUE;Info.PortAddr = PhysAddr + 0x0C;Info.PortSize = sizeof(DWORD);Info.MaskAddr = PhysAddr + 0x10;KernelLibIoControl(g_IsrHandle, IOCTL_GIISR_INFO, LoadIntChainHandler
13、函数负责注册可安装 ISR,参数 1 为 DLL 名称,参数 2 为 ISR函数名称,参数 3 为 IRQ。TransBusAddrToStatic 函数在后面讲。如果要利用 giisr.dll 作为可安装 ISR,必须先填充 GIISR_INFO 结构体,CheckPort=TRUE 表示 giisr 要检测指定的寄存器来确定当前发出中断的是否是这个设备。PortIsIO 表示寄存器地址属于哪个地址空间,FALSE 表示是内定空间,TRUE 表示 IO 空间。UseMaskReg=TRUE 表示设备有一个掩码寄存器,专用于指定当前设备是否是中断源,也就是发出中断,而 MaskAddr 表示掩
14、码寄存器的地址。如果对 Info.Mask 赋值,那么 PortAddr 表示一个特殊的寄存器地址,这个寄存器的值与 Mask 的值 /1 表示是 IO 空间PHYSICAL_ADDRESS ioPhysicalBase = iobase, 0; /相当于 ioPhysicalBase.LowPart = iobase在地址转换后就要将转换后的地址映射到驱动程序(一般 IST 和应用程序一样运行在用户模式)能够访问的虚拟地址空间(0x80000000 以下)和 ISR 能够访问的静态虚拟地址空间中(0x80000000 以上)。例如:/如果地址属于物理地址空间ioPortBase = (PUC
15、HAR)MmMapIoSpace(ioPhysicalBase, Size, FALSE);TransBusAddrToStatic(Isa, 0, ioPhysicalBase, Size, MmMapIoSpace 函数负责将物理地址映射到驱动程序能够访问的虚拟地址空间中,通过源码分析 MmMapIoSpace 在内部分别调用:pVirtualAddress =VirtualAlloc(0, SourceSize, MEM_RESERVE, PAGE_NOACCESS);VirtualCopy(pVirtualAddress, (PVOID)(SourcePhys 8), SourceSi
16、ze, PAGE_PHYSICAL | PAGE_READWRITE | (CacheEnable ? 0 : PAGE_NOCACHE);VirtualAlloc 分配一块和 MemLen 一样大小的虚拟地址空间,因为参数 1 为 0,所以内核自动分配。一般 MemLen 小于 2MB,所以会在应用程序的地址空间中分配。VirtualCopy 负责将硬件设备寄存器的物理地址与 VirtualAlloc 分配的虚拟地址做一个映射关系,这样驱动程序访问 PvirtualAddress 实际上就是访问第一个寄存器。因为硬件设备寄存器的物理地址一定是在 512MB(CE 支持 RAM 的最大值)以上
17、,所以除了最后的参数要加 PAGE_PHYSICAL 外,第二个参数物理地址也要右移 8 位(或者除以 256)。映射硬件寄存器当然 PAGE_NOCACHE 是必须加的。TransBusAddrToStatic 函数负责将物理地址映射到 ISR 能够访问的静态虚拟地址空间中,当出现中断共享时,ISR 要负责访问硬件设备的某一个寄存器来判断中断源,所以将寄存器的物理地址映射到静态虚拟地址空间中是必要的(ISR 只能访问静态的虚拟地址空间)。所谓静态虚拟地址空间是指在 OEMAddressTable 中定义的虚拟地址空间(当然是 0x80000000 以上)。在 x86 平台一般这个表只定义 R
18、AM 的物理地址与虚拟地址对应关系,而硬件设备的寄存器地址并不在该表中定义,所以如果要创建一块静态的虚拟地址空间供 ISR 访问,必须在此之前调用CreateStaticMapping 函数在 0xC4000000 到 0xE0000000 虚拟地址空间中分配。TransBusAddrToStatic 函数在内部就是调用了 CreateStaticMapping 函数。注:硬件设备的寄存器地址也可以在 OEMAddressTable 中定义。/如果地址属于 IO 空间ioPortBase = (PUCHAR)ioPhysicalBase.LowPart;*ppStaticAddress=ioP
19、ortBase这种情况只属于 x86 平台,是 IO 空间就可以直接访问,即使是用户模式。SerInit 函数接着初始化 SER_INFO 结构体成员,之后调用 SL_Init 函数,这个函数在ser16550 中定义,负责初始化 SER16550_INFO 结构体,在这个结构体中保存串口 8 个寄存器的地址。SerInit 函数执行完毕后 COM_Init 函数创建接收缓冲区,然后调用StartDispatchThread 函数初始化中断并且创建 IST。StartDispatchThread 函数在内部调用 InterruptInitialize 函数关联 SysIntr 和 Event,然后调用 InterruptDone 函数告诉内核当前串口可以中断处理,接着调用 CreateThread 函数创建 IST 线程。