VC6.0环境下GPS数据采集程序设计:串口通信与NMEA协议解析实战
简介一份基于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.00UTC时间08点35分59秒注意这是UTC时间不是北京时间A定位状态AActive表示定位有效VVoid表示定位无效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 ControlMSComm控件拖到MFC对话框上就能用事件驱动接收数据代码量少、上手快另一种是直接用Windows API的CreateFile、ReadFile、WriteFile系列函数自己管理一切。我实际项目中最终选了纯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结构体里有些保留字段直接自己初始化一个全零对象去设置可能会意外修改不该动的内容。第三超时参数的设置很重要如果全部设成0ReadFile会一直阻塞在那里等数据在多线程环境下会导致线程无法正常退出。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时间协调世界时而国内使用的是UTC8的北京时间。直接用GPS给的时间做日志记录会出现“时间对不上”的低级错误。转换方法很简单把UTC小时数加8如果超过24就减去24同时日期也要对应加一天。不过这里有一个很多人没考虑到的问题因为串口数据有延迟从GPS模块解算出时间到程序接收到这条数据往往有几十到几百毫秒的延迟。如果你的采集软件对时间精度要求高比如做时间同步、轨迹分析就需要用GPS模块的PPSPulse 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串口编程、多线程协作、数据帧协议处理的理解都会有明显提升。哪怕现在已经有更现代的编程方式这套底层功底的含金量并不会贬值。本文还有配套的精品资源点击获取