Windows CE下驱动程序开发基础

 
我想即使读者看过微软的关于驱动开发的培训教材和 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 。一般串口设备相应的注册表设置例子及意义如下:
[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 线程。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值