VC6.0环境下GPS数据采集程序设计:串口通信与NMEA协议解析实战
2026/9/8 9:29:01 网站建设 项目流程

简介:一份基于VC6.0的GPS数据采集程序资料包,面向GIS、导航及嵌入式开发初学者,重点解决在Windows环境下通过串口实时获取并解析GPS数据的问题。压缩包约2.99MB,资料围绕VC6.0工程实现展开,涵盖串口通信参数配置、NMEA协议报文解析、MFC图形界面搭建、数据库存取以及调试优化等关键环节,便于读者跟随代码理解完整实现流程。目前已有124人学习浏览,适合正在学习串口编程或需要快速搭建GPS采集原型的学习者参考。通过该资料可掌握GPGGA、GPGLL等常见NMEA报文的解析方法,熟悉CreateFile、ReadFile、WriteFile等串口操作函数的使用,并了解如何在VC6.0中设计简洁的用户界面实时显示位置、速度与时间信息。对于后续开发车辆追踪、户外导航或科研数据采集系统,这份资料能提供可复用的工程思路与排错经验。 在VC6.0这个环境下做GPS数据采集程序,放在今天看多少有点“老古董”的味道,但当年这几乎是嵌入式、车载导航、移动测绘这类方向入门必踩的一关。串口收发、NMEA协议解析、坐标格式转换、多线程读缓冲区——这些东西的逻辑框架至今没变,只不过现在很多人直接用Python的pyserial几行搞定,反而把底层原理给跳过去了。我当年做这套程序的时候也是从零开始啃Windows API,既用过MSComm控件也用过纯API方式,踩了不少坑,这里把这套实现思路和完整细节整理出来,希望能帮到还在用VC6.0做课程设计、毕业设计或者老项目维护的朋友。

1. 项目背景与整体设计思路

1.1 这个程序要解决什么问题

GPS数据采集,说白了就是把GPS模块(比如常见的u-blox NEO-6M、中科微ATGM336H、老一点的SiRF Star III)通过串口输出的数据流,实时读进电脑,解析出经纬度、速度、时间、卫星数量这些关键信息,然后按需求做存储或者转发。

我当年做这个程序的场景是这样的:设备端有一块GPS模块,通过RS232或者USB转串口连到电脑上,模块上电之后会持续不断地往外吐NMEA 0183格式的语句,每秒大概输出1到5条不等。程序需要做的就是把这一串字符流稳定地接收下来,按“$”开头、“回车换行”结尾的帧格式切成一条一条完整语句,再逐条解析出我们需要的那几个字段。

这个程序最核心的价值在于解决两个问题:第一是串口数据的不间断接收,因为GPS模块不会等你的程序准备好了再发数据,字节流是源源不断往外涌的,如果接收不及时,后面的数据就会把前面的覆盖掉,导致丢包、掉帧;第二是NMEA字符串的解析,这涉及到字符串分割、校验和校验、坐标格式转换,属于典型的文本处理活,看着简单,但坑不少。

1.2 为什么选VC6.0,而不是用其他工具

如果纯粹从“实现功能”的角度讲,用LabVIEW、MATLAB,甚至Python,都要比VC6.0省事得多。LabVIEW有现成的GPS解析库,MATLAB有串口工具箱,Python写起来更是一马平川。但选VC6.0有几个现实原因:

第一是环境限制。很多高校的单片机、嵌入式课程还是在Windows XP或者老电脑上做实验,VC6.0是那个环境下最顺手的C/C++ IDE,体积小、启动快,写控制台程序或者MFC对话框程序都很方便。

第二是学习价值。用VC6.0写GPS解析,意味着你要自己处理串口API、自己写字符串解析函数、自己在多线程环境下小心翼翼地保护共享缓冲区——这些东西恰恰是嵌入式开发里真正值钱的基本功。你用Python一行pyserial.read()读回来的数据,在嵌入式的世界里可能需要你自己跟硬件寄存器打交道。

第三是兼容性。很多老的车载导航终端、工控机,系统环境还停留在很老的状态,只能用老编译器编译出来的程序,这个现实需求到现在还客观存在。

当然,如果你是纯粹想快速出结果、不关心底层原理,那直接用LabVIEW的VISA串口工具包会轻松很多,但那不在本文的讨论范围内。我这里讲的还是VC6.0环境下用纯Windows API方式实现串口采集的完整流程。

2. 串口通信原理与GPS协议基础

2.1 NMEA-0183协议到底长什么样

GPS模块输出的数据格式遵循NMEA-0183标准,这是航海电子设备常用的数据格式标准。每条语句以$开头,以\r\n(回车换行)结尾,中间用逗号分隔各个字段。最常用的语句有以下几种:

语句类型含义是否常用
$GPGGA全球定位系统固定数据(经纬度、质量、卫星数、海拔)最常用
$GPRMC推荐最小定位信息(经纬度、速度、日期、航向)最常用
$GPGSA卫星精度因子与有效卫星编号一般
$GPGSV可见卫星信息少用
$GPVTG地面速度向量少用

拿一条实际的$GPRMC语句举例:

$GPRMC,083559.00,A,3145.38429,N,11706.92020,E,1.00,99.83,130321,,,A*56

拆开来看:

  • $GPRMC:语句类型标识
  • 083559.00:UTC时间,08点35分59秒(注意这是UTC时间,不是北京时间)
  • A:定位状态,A(Active)表示定位有效,V(Void)表示定位无效
  • 3145.38429,N:纬度31度45.38429分,北纬
  • 11706.92020,E:经度117度06.92020分,东经
  • 1.00:地面速度,单位节(海里/小时)
  • 99.83:航迹方向,单位度
  • 130321:日期,2021年3月13日

最关键的判断标志就是那个A/V状态位,如果模块还没有定位成功(比如刚上电、在室内、搜星数量不足),输出的就是V,这种情况下后面解析出来的经纬度全是无效数据,程序里一定要对这一步做过滤。

2.2 串口参数与数据流特性

GPS模块和电脑通信的串口参数,绝大多数模块的默认值是:波特率9600、8位数据位、无校验、1位停止位(8N1)。也有部分模块默认4800,甚至有些高端模块支持115200,但9600是绝对主流。

但这里有一个很多人容易忽略的常识问题:9600波特率下,串口每秒最多传9600/10≈960个字节(每字节包含起始位和停止位)。一条完整的NMEA语句一般在70到90字节左右,GPS模块每秒输出1到5条,那么每秒的数据量大约在100到450字节之间。这个量级对于VC6.0的串口接收来说压力很小,但是如果你的程序在接收时会话阻塞(比如在UI线程里做解析、写文件、画界面),那么缓冲区就可能溢出,导致数据不完整。

GPS数据流的另一个特点是连续不断,没有明确的数据边界。你看到的是一条一条独立语句,但串口传输层面它就是一条字节流,中间没有特殊分隔符让你知道“这是第N条的开头”。所以程序必须自己去做“帧同步”——寻找$字符作为语句起始,然后等\r\n作为结束。这个思路一定要在代码里贯彻到底,否则就会解析出一堆乱码。

3. 核心实现:串口通信的完整代码

3.1 两种串口编程方式的选型对比

VC6.0下操作串口有两种主流方式:一种是Microsoft Communications Control(MSComm)控件,拖到MFC对话框上就能用,事件驱动接收数据,代码量少、上手快;另一种是直接用Windows API的CreateFileReadFileWriteFile系列函数,自己管理一切。

我实际项目中最终选了纯API方式。原因有几个:MSComm控件在Win7、Win10系统上经常遇到兼容性问题,注册困难,而且控件封装得太死,出了问题很难调试,串口参数修改也不够灵活。API方式虽然代码量大一些,但完全自主可控,出了问题可以用调试器直接查看每一步的返回值,而且编译出来的程序在各类Windows系统上都能跑,不依赖控件注册。

3.2 打开串口与参数配置

下面的代码展示了打开串口并进行参数配置的完整过程。这里用的是CreateFile这个通用API,它不仅能打开文件,也能打开串口设备,在Windows体系里串口被抽象成一种文件设备。

HANDLE hCom; hCom = CreateFile( "COM3", // 串口名,注意从COM10开始要写成"\\\\.\\COM10" GENERIC_READ | GENERIC_WRITE, // 读写访问 0, // 独占方式打开 NULL, OPEN_EXISTING, 0, // 不用重叠I/O,用同步方式即可 NULL); if (hCom == INVALID_HANDLE_VALUE) { AfxMessageBox("打开串口失败,请检查串口号或设备连接"); return FALSE; } // 配置串口参数 DCB dcb; GetCommState(hCom, &dcb); dcb.BaudRate = 9600; // 波特率 dcb.ByteSize = 8; // 数据位 dcb.Parity = NOPARITY; // 无校验 dcb.StopBits = ONESTOPBIT;// 1位停止位 SetCommState(hCom, &dcb); // 设置超时,防止ReadFile阻塞时间过长 COMMTIMEOUTS timeouts; timeouts.ReadIntervalTimeout = 100; // 两个字符间的最大间隔时间,单位毫秒 timeouts.ReadTotalTimeoutMultiplier = 10; // 每读取一个字节乘上的系数 timeouts.ReadTotalTimeoutConstant = 100; // 固定超时时间 SetCommTimeouts(hCom, &timeouts); // 清空缓冲区,丢弃残留数据 PurgeComm(hCom, PURGE_RXCLEAR | PURGE_TXCLEAR);

这里需要注意几个细节。第一,COM10及以上的串口号需要加\\.\前缀,这坑过不少人,如果你的设备恰好在COM10之后,用CreateFile("COM10", ...)会直接返回失败,必须写成"\\\\.\\COM10"。第二,SetCommState前最好先调用GetCommState取一次当前配置再修改,因为DCB结构体里有些保留字段,直接自己初始化一个全零对象去设置,可能会意外修改不该动的内容。第三,超时参数的设置很重要,如果全部设成0,ReadFile会一直阻塞在那里等数据,在多线程环境下会导致线程无法正常退出。

3.3 多线程接收数据与缓冲区设计

串口接收数据这件事,正规做法是开一个专门的接收线程,在循环里调用ReadFile读数据,然后把读到的字节追加到缓冲区里,主线程(UI线程)定时或者通过消息通知去缓冲区取数据做解析。绝对不能把ReadFile放在UI线程的消息循环里,因为串口数据的到达是异步的,UI线程一旦阻塞在读取上,整个界面就会卡死,点和没点按钮都没反应。

我当年用AfxBeginThread创建了一个接收线程:

UINT RecvThreadProc(LPVOID pParam) { CMyDialog *pDlg = (CMyDialog *)pParam; char buff[1024] = {0}; DWORD dwRead = 0; while (pDlg->m_bRunThread) // 用布尔变量控制线程退出 { BOOL bRet = ReadFile(pDlg->m_hCom, buff, sizeof(buff), &dwRead, NULL); if (bRet && dwRead > 0) { pDlg->m_strRecvBuffer.Append(buff, dwRead); // 追加到字符串缓冲区 pDlg->ParseGPSFrame(); // 尝试解析完整帧 } } return 0; }

这里有一个非常关键的工程问题:while循环里ReadFile的调用频率。如果ReadFile的缓冲区设得太大(比如1024字节),那么你第一次调用ReadFile往往只会读到部分数据,剩下的还留在系统缓冲区里,要等下一次ReadFile才能读到。所以,更合理的做法是设置一个较小的读取缓冲区(比如256字节),配合串口的超时设置,让ReadFile在有数据到达时尽快返回。

接收线程里往缓冲区追加字符串,主线程里读取这个缓冲区做解析,这里就涉及多线程共享数据的安全问题。我个人的做法比较朴素,用一个CCriticalSection临界区保护接收缓冲区,线程在追加数据和读取数据时都先Lock,操作完再Unlock。对于这个量级的数据传输,临界区的性能开销可以忽略不计。

4. NMEA数据解析与坐标换算

4.1 帧同步与校验和校验

从缓冲区里解析GPS数据,第一步就是帧同步。因为串口数据是连续的字节流,你不可能保证缓冲区里刚好是一整条从$开始的语句。所以我的解析逻辑是:先在缓冲区里查找$字符,找到之后接着往后找\r\n,如果找到了,就把这两个标记之间的内容取出来当成一条完整语句;如果没有找到\r\n,说明这条语句还没接收完整,先留着等下一批数据到了再说。

void ParseGPSFrame() { m_cs.Lock(); CString strBuf = m_strRecvBuffer; m_cs.Unlock(); int nDollar = strBuf.Find('$'); if (nDollar < 0) { // 没有$符号,直接清空缓冲区 m_cs.Lock(); m_strRecvBuffer.Empty(); m_cs.Unlock(); return; } if (nDollar > 0) { // $之前的垃圾数据直接丢弃 strBuf = strBuf.Mid(nDollar); } int nCR = strBuf.Find("\r\n"); if (nCR < 0) { // 数据不完整,等待更多数据 return; } CString strSentence = strBuf.Left(nCR); // 提取一条完整语句 // 从缓冲区中移除已取走的语句 m_cs.Lock(); m_strRecvBuffer = strBuf.Mid(nCR + 2); m_cs.Unlock(); // 解析这条语句 ParseSentence(strSentence.GetBuffer(0)); }

这里有一个小技巧:当缓冲区里找不到$的时候,说明进来的数据全是噪声或者干扰,直接清空就行;但如果开头是$结尾却没有\r\n,那就说明这条语句还没接收完,要保留缓冲区等待下一批数据。这也是为什么缓冲区不能用简单的“读完清空”逻辑,必须做增量处理。

校验和校验是NMEA协议里很多人容易忽略的部分。NMEA语句在*号后面有两个十六进制字符,表示语句中从$之后到*之前所有字符的异或校验值。例如$GPRMC,...,A*56,这个56就是前面所有字符(不含$*)逐字节异或的结果。在真实工程里,尤其是做车载导航设备时,校验和校验不能省,因为GPS信号在传输过程中受到干扰导致数据畸变的情况并不罕见,如果不做校验直接拿去算坐标,结果会非常离谱。

4.2 $GPRMC和$GPGGA的解析与坐标格式转换

我最常用的是解析$GPRMC,因为它包含了定位状态、时间、经纬度、速度、日期,信息最全。下面是实际验证过的解析代码,用的就是最原始的C字符串处理函数:

void ParseSentence(char *pSentence) { if (strncmp(pSentence, "$GPRMC", 6) == 0) { // 准备解析 char *pToken[14] = {0}; int nIndex = 0; char *p = strtok(pSentence, ","); while (p != NULL && nIndex < 14) { pToken[nIndex++] = p; p = strtok(NULL, ","); } // pToken[0] = $GPRMC // pToken[1] = UTC时间 // pToken[2] = 定位状态 A/V // pToken[3] = 纬度 ddmm.mmmm // pToken[4] = N/S // pToken[5] = 经度 dddmm.mmmm // pToken[6] = E/W // pToken[7] = 速度(节) // pToken[8] = 航向(度) // pToken[9] = 日期 if (nIndex < 7 || pToken[2][0] != 'A') { return; // 字段不够或者定位无效,直接丢弃 } double dLat = ConvertNMEAToDeg(pToken[3]); // 纬度 double dLon = ConvertNMEAToDeg(pToken[5]); // 经度 if (pToken[4][0] == 'S') dLat = -dLat; if (pToken[6][0] == 'W') dLon = -dLon; // 存到成员变量或写文件,这里省略 } }

坐标转换是GPS解析里最容易出错的一步。NMEA输出的纬度是ddmm.mmmm这种格式,意思是“度分”格式,比如3145.38429表示31度45.38429分。要把这个转成十进制度,公式是:十进制度 = 度 + 分/60,所以3145.38429转换后是31 + 45.38429/60 = 31.7564048度。

double ConvertNMEAToDeg(char *pNMEA) { // 输入形如 3145.38429 int nDeg = 0; double dMin = 0.0; char szTmp[32] = {0}; // 找小数点,小数点前至少4位(纬度)或5位(经度) char *pDot = strchr(pNMEA, '.'); if (pDot == NULL) return 0.0; int nIntLen = (int)(pDot - pNMEA); // 整数部分长度 int nDegLen = nIntLen - 2; // 前面是度,最后两位是分 strncpy(szTmp, pNMEA, nDegLen); szTmp[nDegLen] = 0; nDeg = atoi(szTmp); strcpy(szTmp, pNMEA + nDegLen); // 宽度和年数确保保留两位整分 dMin = atof(szTmp); return nDeg + dMin / 60.0; }

注意一个很容易踩的坑:纬度的整数部分一定是4位(两位度+两位分),经度的整数部分一定是5位(三位度+两位分)。如果你的转换函数写死了根据固定位数来切分,那么处理纬度3145.38429和处理经度11706.92020时,切分的位置是不同的。上面这段代码用了动态方式,根据小数点位置反推度的位数,通用性更强。

4.3 UTC时间与本地时间的换算

GPS模块输出的时间是UTC时间(协调世界时),而国内使用的是UTC+8的北京时间。直接用GPS给的时间做日志记录,会出现“时间对不上”的低级错误。转换方法很简单,把UTC小时数加8,如果超过24就减去24,同时日期也要对应加一天。

不过这里有一个很多人没考虑到的问题:因为串口数据有延迟,从GPS模块解算出时间到程序接收到这条数据,往往有几十到几百毫秒的延迟。如果你的采集软件对时间精度要求高(比如做时间同步、轨迹分析),就需要用GPS模块的PPS(Pulse Per Second,秒脉冲)引脚做硬件校时,单纯靠解析NMEA字符串里的时间字段是做不到毫秒级精度的。如果只是做普通的数据记录,软件层面对一下时钟到秒就够了。

5. 常见问题与排查技巧实录

这部分我把自己实际调试过程中遇到过的典型问题整理成了速查表,每个问题都是真实踩过坑才总结出来的经验。

现象常见原因排查思路
串口打不开,返回INVALID_HANDLE_VALUE串口号错误、被其他程序占用、COM10以上未加\.\前缀设备管理器里确认串口号,关闭占用程序
收到大量乱码字符波特率配置不对、串口线接触不良、模块供电不足先确认模块参数,再换一根串口线试
数据能收到但解析出来的全是零或空GPS模块未定位,状态位是V检查天线是否接好,到窗边或室外开阔处测试
经纬度数值完全不对度分转换逻辑写错了、N/S和E/W符号没处理用带GPS的手机和软件对照,验证转换公式
程序运行一段时间后卡死多线程竞争问题、缓冲区无限增长加临界区保护,给缓冲区设最大长度限制
数据断断续续,丢帧串口缓冲区太小、接收线程优先级被抢占调大接收缓冲区,提高线程优先级

5.1 数据乱码与串口线问题

我遇到过最离谱的一次乱码问题,最后查明原因竟然是串口延长线质量太差,屏蔽层脱落,导致高频干扰把信号打乱了。在短距离(1米以内)的USB转串口线一般没什么问题,但一旦超过3米,劣质线材的抗干扰能力就会急剧下降。GPS模块附近如果有电机、开关电源这类电磁干扰源,也容易出现这种随机乱码。

排查乱码问题有个经典办法:把GPS模块的输出直接用串口调试助手(比如友善串口助手、SSCOM)来接,如果调试助手里看到的也是乱码,那就基本能确定是硬件层面的问题,而不是软件解析的问题。反之,如果调试助手显示正常,那就得检查你自己的软件配置。

5.2 定位无效与测试数据模拟

GPS模块刚上电时,如果天线所处位置不好,可能需要30秒到几分钟才能完成首次定位。室内或者高楼密集的区域,定位时间会大幅延长,甚至完全无法定位。做程序开发调试时,不能总等到定位成功了才开始工作,我一般会用一个串口模拟工具往程序里灌GPS数据——就是用文本文件保存一段真实的NMEA语句记录,然后通过虚拟串口软件按波特率模拟发送。这样调试起来效率高很多,不受天气和位置影响。

网传的“partapack H2”这类硬件可以模拟GPS卫星信号来测试导航设备,我没有实际用过那套设备,但从原理上讲,它本质上就是把真实的卫星射频信号做二次重放,比软件模拟NMEA数据流更接近真实环境。如果你只是调试串口解析逻辑,纯软件模拟完全够用,没必要上射频级的信号模拟器。

5.3 缓冲区无限增长问题

这是一个容易被忽视的隐患。如果接收线程持续向缓冲区追加数据,而主线程解析速度跟不上(比如用户点了暂停按钮),缓冲区就会越积越大,最终耗尽内存。解决思路就是给缓冲区设一个上限,超出上限时丢弃旧数据或者清空重来。GPS这种实时性比较强的数据流,老数据本身也没有太大保留价值,丢掉反而是合理的。

我采用的做法是,在加入新数据之前先判断m_strRecvBuffer.GetLength()是否超过某个阈值(比如10KB),如果超过了就先清空再追加。这样即使主线程某个时间段处理不过来,缓冲区也不会爆掉。

6. 实操体验与后续扩展方向

前面把整个程序的框架和关键代码都过了一遍,这里说点我个人的实际体会。VC6.0写这种程序最让人抓狂的不是代码逻辑,而是调试体验——VC6.0的调试器非常古老,查看CString内部数据很不方便,而且Win7以上系统对老IDE的兼容性时好时坏。我后来干脆在关键解析函数里加了一个日志文件输出,把收到的原始语句和解析结果都写进去,调试效率反而比单步调试高很多。如果你也在用VC6.0做类似项目,强烈建议从第一天就养成写日志的习惯,直接在界面上显示原始NMEA数据和解析结果的对照,能少走很多弯路。

另外,这个项目后续可以扩展的方向很多。比如把解析出来的经纬度用GDI绘制成轨迹图,或者叠加到地图引擎上显示车辆实时位置;也可以把采集到的数据保存成GPX或CSV格式,方便导入到专业GIS软件做后期处理。在数据积累足够之后,还可以加入三边测量定位算法的实验——用多个模拟基站的信号强度推算终端位置,这种定位原理在室内场景下比纯GPS信号更实用,跟本章节讲的GPS定位在应用场景上刚好互补。

还有一点想提醒的是,GPS模块的天线摆放位置对整个采集质量影响极大,哪怕是软件再完美,天线放在金属机箱旁边也会导致搜星数量骤降。在实际部署的时候,天线最好放在室外可见天空的位置,至少也要放在窗口旁边。这种硬件层面的经验,往往比调试软件更能决定一个GPS项目的成败。

这个项目看起来只是串口编程和字符串解析的组合,但真正做完一遍,你对Windows API串口编程、多线程协作、数据帧协议处理的理解都会有明显提升。哪怕现在已经有更现代的编程方式,这套底层功底的含金量并不会贬值。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询