1. 项目概述与背景
在十多年前的嵌入式开发黄金时代,Windows Embedded CE 6.0(我们习惯简称为WinCE 6.0)是许多工业控制、车载终端和便携式设备的主流操作系统。当时,为这些设备集成GPS功能,实现定位导航,是一个既基础又充满挑战的任务。我手头正好有一个基于飞思卡尔i.MX31 PDK开发板的旧项目,需要从GPS模块中稳定、高效地提取经纬度信息。这不仅仅是调用一个API那么简单,它涉及到从BSP(板级支持包)的驱动配置、系统中间件的理解,到最终应用层数据解析的全链路打通。
GPS,或者说全球定位系统,其核心原理是通过接收至少四颗卫星的信号,通过测量信号传播时间来计算接收器与各卫星之间的距离(伪距),再通过三边测量法解算出接收器的三维位置(经度、纬度、海拔)和时间。在嵌入式领域,GPS模块通常通过串口(COM Port)输出遵循NMEA-0183标准的ASCII码语句。这些语句就像一封封格式固定的电报,包含了位置、速度、时间、卫星状态等所有信息。我们的任务,就是教会WinCE系统如何“听懂”这些电报,并把我们需要的信息——尤其是经纬度——提取出来。
WinCE 6.0为此提供了一个非常关键的软件层:GPS Intermediate Driver,也就是GPSID。你可以把它理解为一个“翻译官”兼“调度员”。它向下屏蔽了不同品牌、不同型号GPS硬件(可能是串口、USB或CF卡接口)的差异,向上为应用程序提供了统一、简洁的编程接口。无论底层接的是SiRF、u-blox还是其他什么模块,应用程序都通过同样的方式与GPSID对话。这极大地提高了代码的复用性和系统的可维护性。本次实践,就是围绕如何配置这个GPSID,并利用其提供的两种数据接口(解析后的数据结构和原始NMEA语句)来获取经纬度。
2. GPSID架构与配置全解析
2.1 GPSID在WinCE系统中的角色定位
要玩转GPSID,首先得在脑子里建立起它的架构图。根据飞思卡尔那份应用笔记的描述和我们实际项目的验证,GPSID在WinCE中的位置非常清晰。它位于应用程序和硬件抽象层(HAL)的GPS驱动之间。应用程序不直接操作串口去读那一堆“$GPGGA,…”的原始数据,而是调用GPSID提供的API。GPSID则负责与底层的GPS硬件驱动通信,读取原始NMEA数据,并进行初步的解析和缓存。
它的核心价值在于两点:硬件抽象和数据复用。硬件抽象使得更换GPS模块时,通常只需调整底层驱动和少量注册表配置,应用层代码几乎不用动。数据复用则更为重要,GPSID内部有一个多路复用器(Multiplexer),可以同时为多个客户端应用程序提供GPS数据。想象一下,你的设备上可能同时运行着导航软件、轨迹记录器和天气应用,它们都需要位置信息。如果没有GPSID,每个应用都得自己去开串口、读数据、解析,会造成资源冲突和浪费。而GPSID让它们共享同一个硬件连接和数据流,效率高得多。
2.2 系统组件添加与BSP集成
在开始写代码之前,我们必须确保目标操作系统镜像里包含了必要的组件。这步通常在Platform Builder中进行。根据文档,需要添加两个关键组件:
- GPS Intermediate Driver:这是核心中间件。路径在
Catalog -> Core OS -> CEBASE -> Application and Services Development -> Location -> GPS Intermediate Driver。把它勾选上,它就相当于把“翻译官”请进了系统。 - 具体的GPS硬件驱动:这取决于你的硬件平台。在i.MX31 PDK的BSP中,路径是
Catalog -> Third Party -> BSP -> Freescale i.MX31 3DS: ARMV4I -> Device Drivers -> GPS。这个驱动是飞思卡尔针对其平台上的GPS模块(可能是通过特定UART或I2C连接)编写的HAL层驱动,负责最底层的硬件操作。
注意:很多新手会忽略第二步,导致系统启动后根本找不到GPS硬件设备。务必确认你使用的BSP提供了对应的GPS驱动,并且这个驱动与你硬件上GPS模块的连接方式(如UART几、波特率)匹配。如果BSP里没有,你可能需要自己移植或编写一个流接口驱动。
添加完组件,编译生成系统镜像(NK.bin)并烧录到设备中,我们的系统就具备了GPS服务的基础能力。但这还不够,我们还需要告诉GPSID:“硬件在哪里,怎么连接”。
2.3 注册表配置详解:打通硬件与驱动
WinCE系统的配置信息大量存储在注册表中,GPSID也不例外。这里的配置是项目成败的关键,也是最容易出错的地方。我们需要配置两处注册表项,它们通常在平台项目的.reg文件中设置,并随系统镜像一起固化。
2.3.1 配置输入源(Input Source)
这个配置的目的是告诉GPSID:“数据从哪里来”。通常,数据来自一个具体的串口。相关注册表路径在:HKEY_LOCAL_MACHINE\System\CurrentControlSet\GPS Intermediate Driver\Drivers
在这里,我们需要创建一个子键(SubKey)来命名我们的GPS硬件,例如MyGPSHardware。然后,在这个子键下设置几个关键值:
CurrentDriver(位于Drivers键下):这个字符串值(REG_SZ)应设置为你的硬件子键名称,例如MyGPSHardware。GPSID启动时会读取这个值,知道该去加载哪个配置。InterfaceType(位于MyGPSHardware键下):指定接口类型。对于最常用的串口,这里填COMM。FriendlyName(位于MyGPSHardware键下):一个友好名称,方便识别,例如"Primary GPS Receiver"。
2.3.2 配置硬件连接参数
这一步是具体告诉GPSID:“怎么和这个硬件对话”。配置位于你刚才创建的硬件子键下(例如...\Drivers\MyGPSHardware)。对于串口设备,必须设置以下值:
CommPort:串口端口号。这里有个大坑:在WinCE中,串口编号有时从1开始(COM1:),有时在驱动层有不同映射。你需要根据BSP文档和硬件原理图确认GPS模块实际连接到了哪个UART控制器,以及它在WinCE中暴露的COM号。例如,可能是1(代表COM1:)。Baud:波特率。常见的GPS模块波特率是4800或9600,但新一代模块可能支持115200。务必与你的硬件规格书一致。Parity:奇偶校验位。GPS NMEA数据通常是无校验,所以设为0。StopBits:停止位。通常是1。
一个完整的注册表示例片段如下:
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\GPS Intermediate Driver\Drivers] "CurrentDriver"="MyGPSHardware" [HKEY_LOCAL_MACHINE\System\CurrentControlSet\GPS Intermediate Driver\Drivers\MyGPSHardware] "InterfaceType"="COMM" "FriendlyName"="Primary GPS Receiver" "CommPort"=dword:1 ; COM1: "Baud"=dword:2580 ; 9600 波特率 (0x960 = 2400, 0x2580=9600) "Parity"=dword:0 "StopBits"=dword:0 ; 1位停止位 "DataBits"=dword:8 ; 8位数据位(有时需要显式指定)实操心得:波特率的设置是十六进制的。计算方法是
波特率 = 0x3F * 时钟频率 / 分频系数,但更简单的方法是查表或使用已知值。上面的0x2580对应十进制的9600。如果不确定,可以先用PC串口工具连接GPS模块,扫描确定其输出波特率。
2.3.3 配置多路复用器(Multiplexer)
为了让应用程序能够连接到GPSID,还需要配置多路复用器。路径在:HKEY_LOCAL_MACHINE\System\CurrentControlSet\GPS Intermediate Driver\Multiplexer
这里最重要的值是DriverInterface。它定义了一个虚拟的“设备名”,应用程序将通过这个名字(类似一个串口名)来打开GPS数据流。它的命名有规则:可以以“COM”开头,后跟数字;或者以“GPD”开头,后跟数字。例如,设置为COM9:或GPD1:。我通常喜欢用GPD1:,以避免与物理串口冲突。
MaxBufferSize:缓冲区大小。如果应用程序读取数据的速度跟不上GPS模块输出的速度,数据会堆积在这里。设置太小会丢数据,太大会浪费内存。对于1Hz输出频率的GPS,4096字节通常足够。
3. 使用解析后GPS数据接口提取经纬度
这是最常用、也是最推荐的方式。GPSID已经帮我们把繁琐的NMEA语句解析成了结构化的C语言数据,我们直接读取即可。
3.1 核心API工作流程
使用解析接口,通常遵循“打开-获取-关闭”的标准流程,涉及四个核心函数:
- GPSOpenDevice:打开到GPSID的连接。它需要两个事件句柄参数(
hNewLocationData和hDeviceStateChange),用于异步通知。但在很多简单应用里,我们可以采用轮询方式,将这两个参数设为NULL,然后通过GPSGetPosition主动获取。最后一个参数szDeviceName在CE 6.0下也必须为NULL。 - GPSGetPosition:这是获取经纬度的核心函数。它填充一个
GPS_POSITION结构体,里面包含了几乎所有的定位信息。 - GPSGetDeviceState:获取GPS硬件设备的状态(如开启、关闭、故障等)。
- GPSCloseDevice:关闭连接,释放资源。
3.2 GPS_POSITION结构体深度解读
GPS_POSITION结构体是信息的载体,定义在gpsapi.h头文件中。在调用GPSGetPosition之前,必须正确初始化这个结构体的两个字段:
dwVersion:必须设置为GPS_VERSION_1(或更高版本,取决于SDK)。dwSize:必须设置为sizeof(GPS_POSITION)。
这是WinCE API常见的做法,用于版本控制和安全检查。成功调用后,结构体里对我们最有用的字段包括:
dblLatitude:双精度浮点数,表示纬度。重要:正值表示北纬,负值表示南纬。dblLongitude:双精度浮点数,表示经度。正值表示东经,负值表示西经。dblAltitude:海拔高度(单位:米)。flSpeed:地面速度(单位:节)。flHeading:航向(度)。ftime:UTC时间戳。dwFlags:一个位掩码,指示哪些字段是有效的(例如GPS_VALID_LATITUDE,GPS_VALID_LONGITUDE)。在读取任何数据前,务必检查对应的标志位!因为GPS可能没有定位成功,无效的数据字段是未定义的。
3.3 完整代码示例与逐行分析
下面是一个控制台应用程序的完整示例,它同步地(轮询方式)获取一次经纬度信息。在实际项目中,你可能会将其封装成一个类或模块,并加入异步事件处理。
#include <windows.h> #include <gpsapi.h> // 关键头文件 #include <stdio.h> int main() { HANDLE hGPS = NULL; GPS_POSITION gpsPos = {0}; DWORD dwResult = 0; // 1. 初始化GPS_POSITION结构体 gpsPos.dwVersion = GPS_VERSION_1; gpsPos.dwSize = sizeof(GPS_POSITION); // 2. 打开GPS设备连接 // 使用NULL参数表示采用轮询模式,不依赖事件通知 hGPS = GPSOpenDevice(NULL, NULL, NULL, 0); if (hGPS == NULL || hGPS == INVALID_HANDLE_VALUE) { printf("ERROR: Failed to open GPS device. Check GPSID configuration and driver.\n"); return -1; } // 3. 获取位置信息 // 第三个参数1000表示我们愿意接受1秒内的缓存数据,避免频繁读取硬件 dwResult = GPSGetPosition(hGPS, &gpsPos, 1000, 0); if (dwResult != ERROR_SUCCESS) { printf("ERROR: GPSGetPosition failed with code: %d\n", dwResult); GPSCloseDevice(hGPS); return -1; } // 4. 检查并提取有效数据 if ((gpsPos.dwFlags & GPS_VALID_LATITUDE) && (gpsPos.dwFlags & GPS_VALID_LONGITUDE)) { printf("定位成功!\n"); printf("纬度: %.6f %s\n", fabs(gpsPos.dblLatitude), (gpsPos.dblLatitude >= 0) ? "N" : "S"); printf("经度: %.6f %s\n", fabs(gpsPos.dblLongitude), (gpsPos.dblLongitude >= 0) ? "E" : "W"); if (gpsPos.dwFlags & GPS_VALID_ALTITUDE) { printf("海拔: %.2f 米\n", gpsPos.dblAltitude); } } else { printf("警告: 获取的经纬度数据无效。GPS可能正在搜索卫星或信号不佳。\n"); printf("状态标志: 0x%08X\n", gpsPos.dwFlags); } // 5. 关闭设备 GPSCloseDevice(hGPS); printf("GPS设备连接已关闭。\n"); return 0; }注意事项:
- 链接库:在项目的链接器设置中,需要添加
gpsapi.lib。- 错误处理:
GPSOpenDevice失败通常意味着GPSID服务未启动或注册表配置错误。GPSGetPosition失败可能是硬件无响应或超时。- 数据有效性:永远不要假设
dblLatitude和dblLongitude有值。必须检查GPS_VALID_LATITUDE和GPS_VALID_LONGITUDE标志位。刚启动或处于室内的设备,这些标志位很可能是0。- 单位:经纬度是十进制度格式。如果需要度分秒格式,需要自己转换(1度=60分,1分=60秒)。
4. 访问原始NMEA数据及自定义解析
虽然解析接口很方便,但有些高级应用需要GPSID不直接提供的信息(比如具体的卫星信噪比列表、原始伪距数据等),或者你想完全掌控解析过程。这时就需要使用原始数据接口。
4.1 原始接口的工作机制
原始接口绕过了GPSID的解析层,应用程序直接与GPSID的多路复用器建立的虚拟串口(即注册表中设置的DriverInterface,如COM9:)通信。你就像操作一个普通串口一样,用CreateFile打开它,用ReadFile读取数据流,读到的就是原始的、未经处理的NMEA语句字符串。
4.2 操作步骤与代码框架
#include <windows.h> #include <stdio.h> #include <string.h> #define GPS_BUFFER_SIZE 1024 int main() { HANDLE hCom = INVALID_HANDLE_VALUE; DWORD bytesRead = 0; char buffer[GPS_BUFFER_SIZE] = {0}; BOOL bResult = FALSE; // 1. 打开GPSID提供的虚拟串口 // 这里的“COM9:”必须与注册表中Multiplexer下的DriverInterface值一致 hCom = CreateFile(TEXT("COM9:"), GENERIC_READ, 0, // 独占方式打开 NULL, OPEN_EXISTING, 0, NULL); if (hCom == INVALID_HANDLE_VALUE) { printf("ERROR: Cannot open COM9:. Make sure GPSID is running and DriverInterface is set correctly.\n"); return -1; } // 2. 循环读取原始NMEA数据 while (1) { memset(buffer, 0, GPS_BUFFER_SIZE); bResult = ReadFile(hCom, buffer, GPS_BUFFER_SIZE - 1, &bytesRead, NULL); if (!bResult || bytesRead == 0) { printf("ReadFile failed or timeout.\n"); Sleep(1000); // 等待一秒再试 continue; } buffer[bytesRead] = '\0'; // 确保字符串终止 printf("Raw Data: %s\n", buffer); // 打印原始数据 // 3. 在这里添加你的自定义解析逻辑 // 例如,查找以“$GPGGA”开头的行,并解析经纬度 parseNMEASentence(buffer); // 简单延时,避免CPU占用过高 Sleep(200); } // 4. 关闭句柄 (实际上上面的循环是无限的,这里仅作示范) CloseHandle(hCom); return 0; } void parseNMEASentence(const char* data) { // 这是一个简化的GPGGA解析示例 char sentence[256]; const char* p = strstr(data, "$GPGGA"); if (p) { // 提取一行(到回车换行符为止) sscanf(p, "%255[^\r\n]", sentence); char time[12], lat[12], ns, lon[12], ew; int fix, satNum; float hdop, alt; // 解析GPGGA语句的关键字段 if (sscanf(sentence, "$GPGGA,%[^,],%[^,],%c,%[^,],%c,%d,%d,%f,%f", time, lat, &ns, lon, &ew, &fix, &satNum, &hdop, &alt) >= 9) { if (fix > 0) { // fix > 0 表示有有效定位 printf("[自定义解析] 时间:%s, 纬度:%s %c, 经度:%s %c, 卫星数:%d, 海拔:%.1f\n", time, lat, ns, lon, ew, satNum, alt); // 可以将字符串格式的纬度(如“3150.1234”)转换为十进制度... } } } }4.3 核心NMEA语句解析要点
原始数据是一行行以$开头,以<CR><LF>(回车换行)结尾的文本。最常见的几条语句是:
$GPGGA:全球定位系统固定数据。这是获取经纬度和时间最基本、最重要的语句。它包含UTC时间、纬度、经度、定位质量指示、卫星数量、水平精度因子、海拔等。$GPRMC:推荐最小定位信息。包含时间、日期、位置、速度、航向和磁偏角。信息比GPGGA更综合,但海拔信息可能没有。$GPGSA:当前卫星信息。显示DOP(精度因子)值和用于解算的卫星PRN号,有助于判断定位精度。$GPGSV:可见卫星信息。详细列出天空中每颗卫星的编号、仰角、方位角和信噪比,对于分析信号强度非常有用。
解析关键:
- 字段分隔:NMEA语句以逗号分隔字段。字段可能为空。
- 校验和:每条语句以
*开头,后跟两个十六进制字符,是前面所有字符(介于$和*之间)的异或校验和。工业级应用必须验证校验和以确保数据完整性。 - 格式转换:经纬度通常是“度分”格式(DDMM.MMMMM)。例如
3150.1234表示31度50.1234分。需要转换为十进制度:31 + 50.1234 / 60 = 31.83539度。半球标识(N/S, E/W)决定正负。 - 定位状态:
$GPGGA的第6个字段(定位质量指示器),0=无效,1=GPS单点定位,2=差分GPS定位等。只有非0时才表示有有效定位。
5. 项目实战中的常见问题与深度排查
在实际部署中,你会遇到各种各样的问题。下面是我踩过坑后总结的排查清单。
5.1 GPSID服务无法启动或设备打开失败
- 症状:
GPSOpenDevice返回NULL或INVALID_HANDLE_VALUE。 - 排查步骤:
- 检查组件:确认系统镜像确实包含了“GPS Intermediate Driver”和正确的BSP GPS驱动。
- 检查注册表:这是最常见的问题源。使用远程注册表编辑器(如Platform Builder的Remote Registry Editor)连接设备,逐项核对
Drivers和Multiplexer下的所有键值。特别注意CommPort的数值是否正确映射到物理硬件。 - 检查驱动加载:在设备控制台或使用
Remote Process Viewer查看进程列表,确认device.exe是否加载了GPS相关的驱动(如GPSID.dll和你的GPS硬件驱动)。有时驱动加载失败会有错误日志输出到调试串口。 - 权限问题:确保你的应用程序有足够的权限访问GPS设备。在CE下,这通常不是大问题,但如果是高度定制的安全策略,可能需要修改进程权限。
5.2 能打开设备但获取不到有效数据(dwFlags无效)
- 症状:
GPSGetPosition调用成功,但返回的GPS_POSITION结构体中dwFlags没有设置GPS_VALID_LATITUDE等标志位。 - 排查步骤:
- 天线与信号:这是硬件层问题。检查GPS天线是否连接牢固,是否放置在能看见天空的地方(首次定位需要较长时间)。室内几乎无法定位。
- 原始数据监听:编写一个简单的原始数据读取程序(如第4节的例子),打开
DriverInterface指定的虚拟串口(如COM9:),看是否能持续收到$GPGGA、$GPRMC等语句。如果收不到,问题出在GPSID或底层驱动。 - 检查波特率:如果原始数据是乱码,很可能是波特率不匹配。用PC串口工具直接连接GPS模块的TX引脚,尝试不同的波特率(4800, 9600, 38400, 115200等),直到看到清晰的NMEA语句。然后回头修改注册表中的
Baud值。 - 冷启动与热启动:有些模块长时间断电后,星历数据丢失,需要重新搜索卫星(冷启动),这可能耗时数分钟。确保给GPS模块足够的定位时间。
5.3 数据跳动(漂移)或精度差
- 症状:经纬度数值不稳定,在几十米范围内跳动。
- 原因与对策:
- HDOP值:查看
$GPGSA语句中的HDOP(水平精度因子)。HDOP值越小,精度越高。通常<1表示很好,1-2好,2-5中等,>5较差。高楼间、峡谷中HDOP会变差。 - 可见卫星数:查看
$GPGGA中的卫星数。少于4颗无法定位,6颗以上定位效果较好。$GPGSV语句可以查看每颗卫星的信噪比(SNR),信噪比高的卫星贡献的定位数据质量更好。 - 软件滤波:对于导航等实时应用,可以在应用层加入简单的软件滤波算法,比如滑动平均滤波,来平滑跳动的数据点。
- 启用SBAS:如果模块支持(如WAAS, EGNOS),在注册表或通过AT命令启用卫星增强系统,可以提高精度。
- HDOP值:查看
5.4 多应用程序访问冲突
- 症状:多个程序同时访问GPS时,其中一个可能失败或数据异常。
- 解决:这正是GPSID多路复用器要解决的问题。确保所有应用程序都通过GPSID的API(
GPSOpenDevice)或通过同一个虚拟串口(DriverInterface)来访问。绝对不要绕过GPSID直接去打开硬件对应的物理串口(如COM1:),这会导致冲突。GPSID会妥善管理多个客户端的连接和数据分发。
5.5 性能与功耗考量
在电池供电的嵌入式设备上,GPS模块是耗电大户。
- 控制电源:通过GPIO控制GPS模块的电源开关或使能引脚。当不需要定位时,彻底关闭它。
- 间歇性更新:如果不是需要实时轨迹(如1Hz更新),可以设置为每5秒或10秒获取一次位置,以节省电量。
- 使用设备状态:通过
GPSGetDeviceState监控设备状态,在设备进入省电模式或发生错误时做出相应处理。
通过以上从系统配置、驱动集成、API使用到问题排查的完整流程,你应该能够在Windows Embedded CE 6.0平台上稳健地实现GPS数据的获取与解析。这套方案虽然基于一个较老的平台,但其设计思想——通过中间件抽象硬件、提供统一接口——在今天的嵌入式Linux或Android系统上依然能看到影子。理解了这个底层过程,再去接触更高层的定位框架,就会觉得豁然开朗。