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