简介:Wince下的Wifi实现是一份围绕Windows CE无线网络开发的示例工程,面向嵌入式开发者与物联网设备程序员,用于理解Wi-Fi驱动集成、WLAN API调用及上层应用编写流程。压缩包共21个文件,体积约68KB,以C++源文件、头文件及Visual Studio工程配置为主,同时包含无线WZC API导入库、界面资源与文本说明,覆盖从底层接口到界面展示的完整代码骨架。目前已有317人学习。借助WifiDemo工程,开发者可以查看WlanConnect、WlanScan等关键API的调用方式,了解网络扫描、连接建立与断开处理的具体写法,并参考其中的对话框实现快速搭建自己的Wi-Fi配置界面。库文件与API头文件一并提供,省去查找SDK匹配版本的麻烦。资源小巧,结构直接,适合希望快速在Wince平台上落地无线功能的初中级开发者。
1. WinCE 下的 WiFi 实现:为什么老系统还需要手动调网
很多开发者一听到 WinCE,第一反应是“这东西还能联网吗”。实际接手一台运行 Windows CE 的工业终端或老式手持机时,真正要做的是把三件事串起来:一个能识别 SDIO、USB 或 CF 总线上无线模块的 NDIS miniport 驱动,一段让系统在启动时正确加载驱动的注册表配置,以及一套让应用层知道“已连接、掉线、需要配置”的状态机制。WinCE 下的 WiFi 实现没有想象中那么难,但坑一点不比 Linux 少,绝大多数加载失败、扫不到信号、反复掉线的问题,都出在驱动加载顺序、供电 GPIO 时序和省电策略上。这篇文章适合 BSP 工程师、设备维护工程师,也适合刚接手 WinCE 项目的新手,照着注册表和命令在板子上试,基本能跑通。
2. WinCE 的 WiFi 实现为什么绕不开 NDIS:架构与接口选型
2.1 NDIS 驱动与流接口驱动:WiFi 必须走 NDIS 的三个理由
WinCE 下的设备驱动分为两大类:一类是流接口驱动,另一类是 NDIS 网络驱动。流接口驱动很好理解,它实现XXX_Init、XXX_IOControl、XXX_Read这一组函数,注册表里写一个Prefix,应用层通过CreateFile、DeviceIoControl去操作它。很多人做单片机习惯把 WiFi 模块当成一个 AT 命令设备来写流接口驱动,功能看起来能用,但网线永远插不到 TCP/IP 栈上。因为系统里的协议栈不认识流接口设备,浏览器、socket 程序不会去打开\XXX0,它只认 NDIS 适配器。
WiFi 必须走 NDIS 的理由可以归纳成三条。第一,WinCE 的 TCP/IP 栈通过 NDIS 绑定网卡适配器,只有注册为 NDIS 设备的驱动才能被协议栈枚举。第二,802.11 的配置机制是 OID,像OID_802_11_SSID、OID_802_11_DESIRED_BSSID、OID_802_11_BSSID_LIST_SCAN这一类都是通过 NDIS 接口下发的,流接口驱动没有这套机制。第三,微软的 WZC,也就是无线零配置服务,以及各种厂商自带的 WiFi 管理工具,都通过 NDIS 适配器的 GUID 去枚举无线网卡,不是 NDIS 设备就进不了这个体系。
当然,NDIS miniport 驱动比流接口驱动复杂得多。它需要实现初始化、打开、关闭、发包、收包、复位、电源管理、OID 查询设置这些回调。厂商提供的 WiFi 驱动包通常就是一个 NDIS 驱动 DLL,放在系统里由 DeviceManager 加载。我们做 BSP 的活,核心不是重写驱动,而是把这个 DLL 和一个正确的注册表键引入镜像,并处理好总线关系。有一点要特别提醒:即使是用厂商样例,也要检查驱动是否实现了OID_802_11_ADD_WEP、OID_802_11_REMOVE_WEP,以及查询OID_802_11_RSSI和OID_802_11_BSSID_LIST_SCAN的处理,否则上层 WiFi 管理器会看到信号强度永远为零,或者扫描列表永远是空的。
2.2 SDIO、CF、USB 三种 WiFi 硬件接口:如何把模块和驱动绑定
选 WiFi 硬件接口是 WinCE 项目里常见的一个分岔口,因为接口不仅决定模块形状,还决定你要不要额外做总线驱动。下面是一张我在项目里常用的对比表。
| 接口类型 | 常见场景 | 主要优势 | 主要坑点 |
|---|---|---|---|
| SDIO | 大多数 WinCE 6.0 主板、核心板 | 体积小、功耗低、可焊在板上 | 依赖 SDHC 总线驱动,中断线和时钟要求高 |
| CF | 老式 PPC、手持机 | 老驱动多,出问题好查 | CF 卡槽快淘汰,热插拔和供电都麻烦 |
| USB | 开发调试、临时验证 | 接线方便,容易搭环境 | WinCE 对 USB WiFi 芯片支持很零散,驱动难找 |
SDIO 现在是主流,因为大部分 SoC 都内置了 SD/MMC 控制器,WiFi 模块可以做到板载,不占用外部接口。但 SDIO WiFi 最容易翻车的地方在总线上:如果 BSP 里的 SD 卡驱动没加载好,WiFi 模块就只能在注册表里看得到,枚举不到物理设备。所以选 SDIO 模块时,我会优先确认两件事:一是芯片厂商的驱动包是否明确支持当前处理器平台,二是 BSP 里是否已经有稳定运行的 SDHC 驱动。这两点缺一个,后面调试都是玄学。
CF 接口在早期 WinCE 设备上很常见,因为模块就是一张卡,插进去系统就能枚举,很多老驱动是经过大量设备验证的。但 CF 卡槽现在越来越少见,而且 CF 卡的热插拔检测和 3.3V 供电切换如果处理不好,容易出现系统死机。USB WiFi 则是调试阶段的救急手段,但不要太指望它量产,因为 WinCE 的 USB host 驱动大多针对存储设备和串口设备做过适配,USB WiFi 的 NDIS 驱动很少是系统自带的,通常要芯片厂商专门出一个 CE 版本。某些芯片在 Linux 上有现成驱动,不代表 WinCE 上有,这是很多开发者被误导的地方。
最终选型我给一个很现实的原则:优先选择 BSP 供应商已经在参考板上验证过的 WiFi 模块,哪怕它贵一点。WinCE 生态不比 Linux,驱动源码很少随板子给你,能拿到一个能在同一处理器架构上跑起来的 DLL,比研究哪家芯片参数好看有用得多。记住 CPU 架构也要区分,ARMV4I 和 ARMV7 的驱动 DLL 不能混用,烧进去不会有任何明确报错,只会表现为加载失败。
2.3 动手前确认的三个前提:总线驱动、电源管脚和内核版本
真正开始对着驱动包操作之前,我会花半天时间把三个前提确认完,省得后面反复刷机。第一是总线。SDIO 接口必须确认系统里已经有 SD 卡驱动,并且能从目录HKEY_LOCAL_MACHINE\Drivers\BuiltIn\SDHC下找到对应注册表项。如果没有,先去调 SD 卡驱动,不要碰 WiFi。第二是电源管脚。WiFi 模块通常有使能脚和复位脚,板上如果把这几个脚接到了系统可关断的电源域上,系统空闲时掉电,WiFi 就会无故消失。最典型的现象是待机一段时间后唤醒,网络图标还在但 ping 不通,这种问题排查起来非常费时间。第三是内核版本。WinCE 5.0、Windows Embedded CE 6.0 和 Compact 2013 三者的 NDIS API 有差异,厂商驱动包上写明了支持哪个版本,不要硬装。
这几点确认完,再开始写注册表,成功率会高很多。如果有一个前提没满足,后面每一步看起来都是对的,但就是起不来,最后回过来查还是这里的问题。
3. 在 BSP 里把 WiFi 拉起来的步骤:注册表、加载日志与 WZC 配置
3.1 添加 NDIS 注册表项:最小配置清单和参数说明
拿到厂商的 WiFi 驱动 DLL 后,第一步是把驱动挂到系统的 Comm 目录下。WinCE 的 DeviceManager 会在系统启动时扫描HKEY_LOCAL_MACHINE\Comm下的子键,找到带有 NDIS 标志的项就尝试加载。下面是一个最小化的注册表片段,我按常见驱动包的样子做了简化,放在platform.reg里。
; platform.reg 中 WiFi NDIS 驱动的注册表配置 [HKEY_LOCAL_MACHINE\Comm\WI-FI1] "ImagePath" = "wifi_ndis.dll" ; NDIS miniport 驱动文件名 "DisplayName" = "WiFi Adapter 0" ; 在系统中显示的名称 "Class" = "ndis" ; 标记为 NDIS 设备 "Dll" = "NDIS.dll" ; 固定使用 NDIS 中央驱动 "Index" = dword:0 "BusName" = "SD" ; 总线类型:SD、PCI、USB 等 "BusNumber" = dword:0 ; 总线实例编号 [HKEY_LOCAL_MACHINE\Comm\WI-FI1\Parameters] "AdapterType" = dword:0 "NetworkAddress" = "" ; 为空则使用模块出厂 MAC这段注册表里最值得解释的是BusName和BusNumber。BusName告诉 DeviceManager 这个网卡挂在哪条总线上,不同平台的写法不完全一样,有的 BSP 用"BUS",有的用"BUSNAME",需要按驱动包里的说明改。Index表示这是第几块网卡,如果和系统中已有的网络设备冲突,TCP/IP 绑定会失败。NetworkAddress如果填写了内容,会覆盖模块的 MAC 地址,这个字段平时保持为空最安全。
写完注册表还要确认 DLL 被编译进镜像。BSP 的.bib文件里有 MODULES 或 FILES 段,需要加一行把wifi_ndis.dll打包到\Windows目录。这一步不能漏,很多新手只改了注册表,DLL 没进镜像,启动时注册表键在但文件不存在,DeviceManager 直接跳过。
3.2 把驱动 DLL 放进镜像并验证加载:启动日志与加载顺序
驱动 DLL 进入镜像的方式取决于你的开发环境。通常做法是把 DLL 放到 BSP 的Files目录或Release目录,然后在.bib文件里指定模块路径。如果是调试阶段不想重新烧写,可以把 DLL 放在 SD 卡上,系统启动后用命令拷贝到\Windows目录。
# 临时调试用:把驱动 DLL 从 SD 卡拷到系统 Windows 目录 copy "SD Card\Drivers\wifi_ndis.dll" "\Windows\" # 拷贝后复位目标机,让 DeviceManager 重新加载驱动设备复位后,观察串口调试输出。WinCE 的 NDIS 封装层在加载驱动时会打印一条类似下面的日志,看到它基本能确认驱动已经被调用。
NDIS: Loading Driver wifi_ndis.dll NDIS: Miniport Driver wifi_ndis registered如果串口没开启调试输出,就打开目标机上的“设备管理器”或使用远程工具查看 DLL 列表,里面出现wifi_ndis.dll说明加载成功。还有一个低技术但很有效的办法:在驱动 DLL 的DllMain或初始化函数入口临时加一个NKDbgPrintfW,重新编译后放到镜像里,日志直接打出来,一步到位判断是不是真的进去了。
加载顺序也是这里面的一个大坑。如果 WiFi 挂在 SDIO 总线上,SD 卡总线驱动必须先加载,WiFi 驱动后加载。WinCE 注册表里通常用Order字段控制加载顺序,数字小的先加载。我会在 SD 卡驱动节点写Order为 0,在 WiFi 节点写Order为 1。这样能避免 WiFi 先被 DeviceManager 调用,然后发现总线设备还不存在,最后加载失败的尴尬情况。
3.3 配置 SSID 和加密:用 WZC 接口做自动连网的最小代码
驱动加载成功后,下一步是配置无线网络。WinCE 系统里如果带有 WZC 服务,可以通过它的 API 来设置 SSID 和加密。WZC 的 C 语言接口在wzcsapi.h里,核心就是拿到网卡 GUID,然后调用WZCSetConfiguration。下面是一个最小示意。
#include <wzcsapi.h> // 通过 WZCEnumInterfaces 或厂商工具拿到无线网卡的 GUID GUID gAdapterGuid = {0}; // 要加入的无线配置 WZC_WLAN_CONFIG cfg = {0}; cfg.dwSize = sizeof(cfg); // 填入 SSID:长度必须和实际 SSID 字节数一致 cfg.Ssid.dwLength = 8; memcpy(cfg.Ssid.ucSSID, "TESTAP12", 8); // 加密方式和认证方式,以开发商头文件里的枚举为准 cfg.AuthMode = WZC_WLAN_AUTH_WPA2; cfg.Encryption = WZC_WLAN_ENC_AES; // 填入预共享密钥 // 注意:不同头文件里字段名可能是 wszKey 或 szKey wcsncpy(cfg.key.wszKey, L"12345678", 8); // 配置下发到网卡 int ret = WZCSetConfiguration(&gAdapterGuid, &cfg, FALSE, NULL); if (ret == 0) { // 配置成功,驱动开始扫描并关联到目标 AP }这段代码里最需要注意的是枚举值。不同版本的 WZC 头文件,WZC_WLAN_AUTH_WPA2和WZC_WLAN_ENC_AES的定义不完全一致,特别是老的 CE6 SDK 可能只有 WEP 和开放式认证。所以我通常会先打开头文件确认一次枚举值,再往工程里贴。另一个容易错的地方是 SSID 长度,Ssid.dwLength必须等于Ssid.ucSSID里有效字符的字节数,如果你写入 16 字节但实际网络名是 8 字节,驱动会去搜索一个不存在的网络名。
如果项目里没有现成的 WZC API 可用,也可以用厂商自带的配置工具先把网络加入,然后观察注册表里新增的配置节点,把对应键值导出到镜像中。这种方式对量产比较友好,设备第一次上电就能自动连接,不需要应用层每次都调接口。缺点是不同厂商的注册表结构差别很大,替换驱动时这套配置要重做。
4. WinCE WiFi 的避坑排查:加载失败、掉线和扫描为空
4.1 无线列表为空,驱动却已经加载:先查扫描触发和天线
现象:驱动加载日志正常,设备管理器里也能看到网卡,但 WiFi 管理器里一个 AP 都扫不到。这种情况最容易让人误判成模块坏了,实际多半是驱动没有真正执行扫描。
原因要分两层看。第一层,很多 NDIS 驱动只有在上层下发OID_802_11_BSSID_LIST_SCAN时才会做主动扫描,如果驱动把主动扫描放在了初始化流程里,又因为某种原因没执行完,应用层看到的就是空列表。第二层是硬件问题,模块的天线引脚是贴片天线时,天线没焊好、天线区域靠近金属外壳,都会有信号但扫描列表为空或者 RSSI 极低。
解决方法是先在驱动调试日志里确认OID_802_11_BSSID_LIST_SCAN有没有被调用。如果没被调用,优先在应用层用DeviceIoControl强制触发一次扫描,看看列表是否出现。如果调用了但没有结果,把扫描结果打印出来,检查驱动的信道码表是否为空。信道码表为空时,驱动在全世界任何一个国家码下都不会输出扫描结果。还有一个容易忽略的点:很多工业 AP 默认隐藏 SSID,扫描结果里不显示,需要手动输入网络名连接,不要误判成驱动坏了。
4.2 连接后周期性掉线,ping 一会就断:先关省电模式
现象:WiFi 能连接上,ping 通十几秒或几十秒,然后开始丢包,再过几秒彻底断,有时候会自动恢复,有时候不会。这个问题在 WinCE 设备上非常经典。
原因里占比最高的是 802.11 省电模式。嵌入式 WiFi 模块为了省电,默认开启了 power save,这时模块会周期性进入 sleep,AP 在缓存了数据包后需要发送 beacon 来唤醒模块。如果模块的唤醒实现有问题,或者 SDIO 总线的时钟在低功耗状态下被切掉了,数据包就会丢。另一个常见原因是 DHCP 租约,如果 AP 分配的租约时间很短,而 WinCE 设备没有正确续租,网络会周期性断开重连,表现和掉线很像。
解决思路是先把省电模式关掉再观察。对于大多数 NDIS 驱动,可以通过厂商工具或注册表关闭 802.11 power save,把模块固定在持续接收状态。调试时把OID_802_11_POWERSTATE设置为全功率模式,如果掉线消失,基本就是省电策略导致的。再把省电模式慢慢打开,进一步确认是哪一层触发的问题。如果关闭省电后还是掉线,把 AP 信道固定到 1、6、11 中任意一个,排除同频干扰。信道上如果存在大量邻居的 AP,即使 RSSI 很高也会频繁冲突。
4.3 休眠唤醒后 WiFi 状态丢失:总线和驱动的电源恢复顺序
现象:系统休眠后唤醒,WiFi 图标还在,网络连接显示已连接,但实际 ping 不通,必须手动禁用再启用网卡才能恢复。有些设备干脆连网卡都找不到了。
这个问题的根源几乎都在电源恢复顺序上。驱动不是没有处理OID_PNP_SET_POWER,而是处理得太早或太晚。常见的一个情况是:唤醒流程里 SDIO 总线的时钟还没有稳定,WiFi 驱动就开始恢复寄存器配置,结果寄存器读写失败,驱动错误地把网卡设置成了断开状态。更严重的做法是在系统休眠时把 WiFi 模块的供电直接断了,唤醒后又没有重新做完整的初始化和扫描流程,网卡还在,但和 AP 之间的关联信息已经丢失。
处理这个坑,首先不要图省事直接切断 WiFi 模块的电源域。如果硬件上必须由系统控制断电,驱动里就要实现完整的后复位流程:重新初始化芯片、重新读取 MAC、重新进行无线扫描、重新关联 AP。如果在 BSP 的 OAL 层里有办法保留 SDIO 控制器的电源和时钟,就让 WiFi 走软件睡眠而不是完全断电。同时在应用层增加一个网络状态监测,定时 ping 网关,连续丢包三次后调用一次重新关联接口,这是保底手段。
4.4 同一套驱动在某板上行、另一块不行:先查中断和引脚配置
现象:两块同样型号的主板,一块 WiFi 一切正常,另一块要么驱动加载失败,要么信号特别差,要么唤醒后必死。两块板子的 BSP 镜像一模一样,最后查出是硬件装配差异。
这种问题在量产阶段特别折腾,因为软件层看着没有任何区别。原因往往出在电路板的中断引脚和复位引脚上。简单的方案是把 WiFi 模块的中断脚接到一个 GPIO,这个 GPIO 的中断号在注册表里写死,如果同一批板子因为装配工艺导致 GPIO 虚焊、上拉电阻不稳,中断信号就会丢,驱动自然收不到网卡的事件。还有一种情况是两批板子改过版,WiFi 中断脚从 GPIO 改到了另一个引脚,但注册表没跟着改。
排查时先看中断有没有来。在驱动里打开中断调试打印,用手碰一下模块附近,如果中断风暴,说明引脚悬空;如果完全没有中断,先量电压再量复位时序。更换主板对比时,一定要确认 WiFi 模块自身的版本,模块上丝印差一个尾号,固件都可能不同,驱动表现完全不一样。这里我一般会做一个简单的量产自检程序,把OID_802_11_RSSI和 MAC 地址读出来,连上 AP 后持续 ping,连续跑一段时间,能过滤掉大部分硬件装配问题。
5. 给 WiFi 做一个连接状态机,顺便把产线验收做扎实
最后一个让项目省心的建议,是把 WiFi 的连接状态抽象成一个状态机,而不是让应用层每次直接去读网卡状态。我一般会在网卡驱动外面包一层管理模块,定时调用WZCQueryInterface获取关联状态和 RSSI,把结果映射到四个状态:未配置、正在连接、已连接、连接失败。每次状态变化都要记录时间和原因,比如扫描结果为空、DHCP 超时、关联被拒、信号丢失,这些日志是现场排查的第一手资料。
状态机里还要加上“防抖”和“自愈”两个机制。防抖是指连接断开后不要立刻重连,等待三到五秒,避免在 AP 重启或者信道切换时反复关联,反而把模块搞得不稳定。自愈是指连续一次重连失败后,做一次完整的驱动复位,而不是只重新扫描。这个复位动作可以放在应用层,也可以调用驱动暴露的私有 IOCTL,但最好和状态的 C 语言封装放在一起,不要散落在界面逻辑里。
产线验证时,固定用一台已知无干扰的 AP,把设备放到离 AP 三米以内的位置,做一个连续 ping 100 次、每次 1400 字节的测试,丢包率要低于 1%。还要同时记录信号强度,信号强度忽高忽低但丢包正常,可能是天线装配问题,这个问题在出厂时发现成本最低。另一个我吃过亏的地方是只测“能连上 AP”就放行,没有等 DHCP 地址分配和网关 ping 通,结果到客户现场才发现模块能关联但拿不到 IP。后来我坚持用“关联成功、拿到 IP、能 ping 通网关”三个条件同时满足才判定通过,连续跑 12 小时没有一次掉线,这批设备才敢放出去。
我做 WinCE 项目时最大的教训是:WiFi 这类模块不要等出了问题再查,最好在一开始就把状态机、日志和功耗控制做进框架里,因为量产之后每一秒现场调试都在烧成本。希望这些踩坑经验能帮到你,少走我走过的弯路。
本文还有配套的精品资源,点击获取