GPS模块协议解析:NMEA 0183与UBX配置实战指南
1. 项目整体设计与协议选择思路——先分清NMEA和UBX到底差在哪GPS模块的上手难度并不在硬件而在协议。很多人第一次拿到ATGM336H、NEO-M8N这类模块时插上USB转TTL就开始往串口助手灌数据看到一堆$GNGGA开头的字符串就以为搞定了结果后面要做高精度定位、要降低功耗、要跟飞控/车载系统对接时才发现只懂NMEA根本不够。这个项目最初的动机很简单把手头几种常用GPS模块的通信协议彻底摸透尤其是NMEA 0183和UBX这两套完全不同的“语言”然后整理出一套可以复用的解析与配置方案。先说结论NMEA 0183是GPS模块出厂默认打开的文本协议几乎所有厂家产的模块都支持优点是通用、可读性好串口助手上直接就能看懂缺点是信息密度低、解析开销大、而且不同厂家的实现细节千差万别。UBX是u-blox自家的二进制协议只在u-blox系列模块NEO-M8N、NEO-M9N、F9P等上原生支持二进制帧结构紧凑、校验严格、配置能力强适合需要深度定制和低延迟的场景。如果你只是看个经纬度、测个速NMEA完全够用但如果你要改模块的刷新率、要拿到原始观测值做RTK、要在中断里快速解析定位结果那就绕不开UBX。1.1 为什么几乎所有GPS模块都默认说NMEA 0183你随便拿一个GPS模块上电之后第一句输出大概率是$GNGGA,....或者$GPRMC,....这就是NMEA 0183。这套协议是NMEANational Marine Electronics Association制定的最早是给船舶导航设备之间通信用的后来被GPS接收机广泛采纳变成了一种事实上的行业标准。它的好处很明显纯文本用逗号分隔字段每条语句以$开头以\r\n结尾只要你会字符串分割就能解析。更妙的是它不依赖于特定的硬件平台任何能输出ASCII字符的串口设备都能处理。所以在早期嵌入式系统里MCU资源紧张用NMEA解析定位数据是最省事的方案。但NMEA的问题同样突出。首先是信息冗余大一条RMC句子里面经纬度、速度、日期、时间、定位状态全都挤在一起但实际有效信息占比很低传同样的数据量NMEA比UBX要多占一倍的带宽。其次是语句种类多而不统一不同厂商对某些字段的处理方式不一样比如$GPGGA和$GNGGA的差别、速度单位是节还是km/h、海拔是椭球高还是海拔高这些细节非常容易踩坑。更麻烦的是NMEA对模块配置的支持几乎为零你想改一下输出频率、关掉某些语句NMEA本身完全做不到必须依赖厂商私有的命令或者UBX/其他二进制协议。1.2 UBX协议的价值与适用场景UBX是u-blox定义的私有二进制协议每条消息都是一个紧凑的数据包结构固定、字段明确、校验可靠。协议整体分三层传输层帧头、长度、校验、消息层类Class和ID、数据层具体载荷。它最明显的优势是效率高。拿位置信息来说UBX的NAV-PVT消息固定输出一条包含经纬度、高度、速度、航向、定位质量、水平/垂直精度估计等几十个字段的二进制数据总长度只有92字节左右。而同样这些信息用NMEA表达至少需要GGA、RMC、GSA、GSV四条语句加起来超过200字节。对于需要高频输出的场景比如无人机飞控、自动驾驶、测量测绘UBX能显著降低串口带宽占用和MCU解析负担。另一个优势是可配置性。u-blox模块几乎所有的运行参数都可以通过CFG类消息来设置比如串口波特率、输出语句、更新率、卫星系统启用/禁用、SBAS/RTK模式切换等。而且这些配置可以写入模块内部flash掉电不丢失。这个能力在批量生产时非常关键——你可以用一套脚本把所有模块配置成完全一致的状态而不是靠人工去敲u-center。再就是UBX的校验比NMEA更可靠。NMEA用的是简单的异或校验对$和*之间的字符逐字节异或只占2个十六进制字符出错概率不低UBX的校验由两个字节组成一个是CHECKSUM_A一个是CHECKSUM_B算法是简单的累加和但覆盖面更大、效率更高。实际使用中UBX在长距离传输、高干扰环境下表现更稳定。1.3 我的选型思路什么时候用NMEA什么时候用UBX在我做过的项目里这个选择不是非黑即白的。如果是快速原型验证或者只需要把定位数据送到一个现成的NMEA消费端比如某些飞控、AIS设备、海图机那就保留NMEA省去额外开发插上就能用。如果是自己做数据记录、姿态解算、或者需要高频定位输出我会优先切到UBX因为解析UBX的代码写起来其实比NMEA还要简单——不用做字符串分割只需要按固定偏移量读字节。有个折中方案也值得推荐让模块同时输出NMEA和UBX。u-blox的UBX协议支持在一条串口上同时输出两种协议只要在CFG-PRT里把协议使能位设置好。这样你可以用NMEA做调试和人工观察用UBX做正式的数据采集互不干扰。当然这会占用双倍带宽所以在线速不高的情况下要谨慎使用。2. 协议细节拆解帧结构、校验、字段含义2.1 NMEA 0183的帧格式与典型语句NMEA 0183的语句格式可以归纳成一句话以$开头地址域数据域校验和以\r\n结束。地址域通常是两个字符的讲者ID如GP表示GPS、GL表示GLONASS、GN表示多系统融合加三个字符的语句类型如GGA、RMC、GSA。数据域用逗号分隔每个字段都有固定含义。我最常用的是GGA和RMC两条。GGA提供的是定位信息包括UTC时间、纬度、经度、定位质量标识、卫星数、水平精度因子HDOP、海拔高度等。RMC提供的是推荐最小定位信息包含时间、日期、经纬度、地面速度以节为单位、航向角等更精简适合做记录和航迹回放。这里有一个很关键但容易被忽略的细节NMEA语句中的纬度经度格式是“度分”格式而不是我们日常用的“度”格式。举一个实际例子$GPRMC,083559.00,A,4717.11437,N,00833.91522,E,0.112,084.4,230394,003.1,W*6A其中4717.11437表示北纬47度17.11437分换算成十进制度数是47 17.11437/60 47.2852395度。我在项目里见过不少朋友直接把这个原始字符串丢给地图API结果定位点飞到了莫名其妙的地方就是因为没有做度分转换。再看定位质量标识GGA的第6个字段0表示未定位1表示单点定位2表示差分定位DGPS/SBAS4表示RTK固定解5表示RTK浮点解。这个字段非常重要在做测量或者自动驾驶的时候如果只看经纬度而忽略了它你很难判断当前数据到底能不能达到厘米级精度。2.2 NMEA校验和的计算方法含示例NMEA的校验和算法很简单把$和*之间的所有字符逐字节做异或结果用大写十六进制表示跟在*后面。比如计算$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47中的*47做法是def nmea_checksum(sentence): # 去掉$和*后面的部分 body sentence[1:sentence.index(*)] checksum 0 for ch in body: checksum ^ ord(ch) return f{checksum:02X}把GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,代入计算得到0x47。实际工作中我写解析器时不只是提取字段还会校验这个值因为串口线长、干扰大的时候数据很容易传错。如果一个句子在校验和都不通过的情况下被采用轻则显示位置漂移重则直接让导航系统跑飞。需要注意有的模块输出的NMEA语句中校验和前面是*但有些模块可能省略了校验和或者输出的是小写十六进制。这些都要在适配不同模块的时候做兼容处理。2.3 UBX协议的三层结构类、ID、长度与校验UBX协议的帧格式严格定义每个UBX包由以下几个部分组成前导字节0xB5 0x62两个字节固定不变类Class1个字节比如0x01表示NAV导航数据0x06表示CFG配置ID1个字节表示同类型下的具体消息比如NAV类下的0x07是PVT长度Length2个字节小端模式表示载荷部分的字节数载荷Payload长度由上面的Length决定校验和2个字节分别是CHECKSUM_A和CHECKSUM_B校验和的计算方式是def ubx_checksum(msg_class, msg_id, payload): ck_a 0 ck_b 0 for byte in bytes([msg_class, msg_id]) payload: ck_a (ck_a byte) 0xFF ck_b (ck_b ck_a) 0xFF return ck_a, ck_b也就是说CHECKSUM_A是所有参与校验的字节累加和CHECKSUM_B是累加过程中每一步的CHECKSUM_A的累加和。这个算法比NMEA异或要复杂一点但用代码实现非常直接。我这里尤其想强调长度字段是小端模式。很多初学者在解析UBX时读到了长度0x5C 0x00第一反应是“0x5C0023552”这完全错了正确值应该是0x005C92。UBX几乎所有的多字节数值都是小端包括经纬度、高度、速度这些字段。这一点在移植代码时特别容易踩坑。2.4 UBX核心消息NAV-PVT, NAV-STATUS, CFG-PRT等在UBX消息族里你最需要认识的首先是NAV-PVT它的类和ID分别是0x01 0x07。这条消息集成了位置、速度、时间于一体是u-blox模块里最常用的导航输出消息。它的载荷中从偏移量0开始是UTC时间信息iTOW毫秒格式的GPS周内秒、year、month、day、hour、min、sec之后是fixType定位类型numSV可见卫星数再之后就是经纬度单位是1e-7度需要除以10000000才能得到十进制度数。高度hMSL单位是毫米速度gSpeed单位是毫米/秒等等。另一条常用的是NAV-STATUS0x01 0x03它有一个关键字段gpsFix用于指示当前定位状态0x00无定位0x022D定位0x033D定位等。有时候你想快速判断模块到底有没有定位而不想解析一长串PVTSTATUS会更轻量。配置类的消息也同样重要。CFG-PRT0x06 0x00用来配置串口参数和协议使能。你可以用它修改波特率、数据位、停止位、校验位以及使能哪些协议NMEA、UBX、RTCM。CFG-MSG0x06 0x01用来控制某类消息在某个端口上的输出开关。CFG-CFG0x06 0x09则用来保存当前配置到flash防止掉电丢失。3. 实操过程从串口抓帧到解析出可靠定位数据3.1 硬件连接与串口参数设置先说硬件。最常见的GPS模块比如ATGM336H国产、NEO-M8Nu-blox都是UART TTL电平输出直接跟STM32、ESP32或者USB转TTL芯片相连就行。连接时注意模块的VCC一般支持3.3V或者5V但IO电平很多是3.3V如果接5V的单片机最好加电平转换否则长期运行容易烧模块。串口的默认参数一般是波特率9600部分模块是38400或者115200比如NEO-M9N默认1152008个数据位1个停止位无校验。新的模块上电后会以默认波特率持续输出NMEA句子。我建议先用USB转TTL接到电脑打开串口助手设置好波特率看到类似$GNGGA,的输出确认硬件通路没问题后再开始写代码。模块的供电和天线不能马虎。在室内窗口边测试时如果天线头朝下、或者被金属遮挡定位时间会非常长。我通常会把天线放在窗边、吸盘朝上确保上方有较大范围的天空视野。冷启动定位等待1-2分钟很正常这时候不要急着换模块先看看是不是天线问题。3.2 用Python脚本捕获原始数据并进行帧识别拿到串口数据流之后第一步不是急着解析而是先弄明白数据长什么样。用Python的pyserial库就能轻松实现原始抓取import serial ser serial.Serial(COM3, 9600, timeout1) with open(gps_raw.bin, wb) as f: while True: data ser.read(256) if data: f.write(data) print(data.hex())运行这段脚本采集几秒钟数据然后用十六进制查看一下。你会发现一个有趣的现象数据流里既有以0xB5 0x62开头的UBX帧也有以$开头的NMEA文本帧。这要看你模块当前启用的协议。如果最初只有NMEA那么你只会看到ASCII字符如果你在u-center里打开过UBX输出那么二进制帧就会混进来。帧识别是解析的第一步。对于NMEA我通常扫描$字符然后往后找*再找\r\n把完整的句子切割出来。对于UBX我会用一个状态机先找0xB5 0x62然后读类、ID、长度再读载荷和检验和。以下是一个简化的状态机示例class UBXParser: def __init__(self): self.state 0 self.buf bytearray() self.msg_class 0 self.msg_id 0 self.length 0 self.payload bytearray() def feed(self, byte): if self.state 0: if byte 0xB5: self.state 1 elif self.state 1: if byte 0x62: self.state 2 else: self.state 0 elif self.state 2: self.msg_class byte self.state 3 elif self.state 3: self.msg_id byte self.state 4 elif self.state 4: self.length byte self.state 5 elif self.state 5: self.length | (byte 8) self.payload bytearray() self.state 6 elif self.state 6: if len(self.payload) self.length: self.payload.append(byte) else: # 到这里接下来两个字节是校验和 ck_a self.check_a() ck_b self.check_b() if ck_a self.buf[0] and ck_b self.buf[1]: # 校验通过处理完整消息 self.handle_message() self.state 0这种状态机写法不需要依赖线程在单片机上也能直接移植。3.3 启用UBX输出并关闭NMEA配置流程要让u-blox模块输出UBX而不是NMEA可以有两种途径。一是用u-center软件界面配置后保存二是直接用串口命令下发配置。这里我以NEO-M8N为例介绍如何用Python发送配置命令。首先我们要把NAV-PVT消息输出到串口UART1。对应的消息是CFG-MSG类0x06ID 0x01。它的载荷前面需要指定消息类/ID以及各个端口上的输出率0表示关闭1表示每次更新都输出。比如要开启UART1上的NAV-PVT载荷是msg_class 0x01 msg_id 0x07 payload bytes([msg_class, msg_id, 0, 1, 0, 0, 0, 0, 0]) # 其中第3个字节是UART1的速率这里设为1第4个字节是UART2以此类推然后封装成UBX帧发送def build_ubx(msg_class, msg_id, payload): length len(payload).to_bytes(2, little) ck_a, ck_b ubx_checksum(msg_class, msg_id, payload) return b\xB5\x62 bytes([msg_class, msg_id]) length payload bytes([ck_a, ck_b])发送之后模块应该在串口上开始输出0xB5 0x62 0x01 0x07 ...的帧。如果同时又想关闭NMEA输出可以通过CFG-PRT把UART1的协议输出设置为只使能UBX。CFG-PRT载荷格式为端口ID0x01表示UART1、保留字节、波特率、配置掩码、协议使能掩码。协议使能掩码中bit0是UBXbit1是NMEAbit2是RTCM。想要只输出UBX就设置协议掩码为0x01。完整代码类似payload bytes([0x01, 0x00, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00]) # 其中0xC0 0x08 0x00 0x00 是波特率115200的小端表示0x0008C02240? 不对需要仔细这里我不建议手写每个字节最好用u-center里“Generate configuration”功能辅助或者参考u-blox手册对照计算。实际项目中我一般先用u-center把波特率、输出语句配置好点击保存然后用工具导出一份完整的配置脚本这样既不会遗漏字段也方便批量生产。3.4 设计一个可用的实时NMEA解析器虽然UBX效率更高但NMEA解析还是绕不开尤其当你需要兼容不同品牌模块的时候。我这里的建议是解析NMEA不要用简单的split(,)而要逐字节解析并同时校验这样做能避免字段错位和异常数据导致系统崩溃。一个典型的解析函数如下def parse_rmc(sentence): # sentence 形如 $GPRMC,081836.00,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62 if not sentence.startswith($) or * not in sentence: return None body sentence[1:sentence.index(*)] checksum int(sentence.split(*)[1], 16) calc 0 for ch in body: calc ^ ord(ch) if calc ! checksum: return None fields body.split(,) if len(fields) 10: return None # fields[0] GPRMC lat_raw float(fields[3]) lat_deg int(lat_raw / 100) (lat_raw % 100) / 60.0 if fields[4] S: lat_deg -lat_deg lon_raw float(fields[5]) lon_deg int(lon_raw / 100) (lon_raw % 100) / 60.0 if fields[6] W: lon_deg -lon_deg speed_knots float(fields[7]) speed_kmh speed_knots * 1.852 return { lat: lat_deg, lon: lon_deg, speed_kmh: speed_kmh, status: fields[2] }这段代码里最关键的转换就是度分格式换算。很多朋友算出来纬度对了、经度不对多半是没看象限字母N/S/E/W。另外还要留意一些极低成本的模块输出的RMC句子里速度、航向可能是空字段直接float()转换会抛异常所以解析时最好加上字段长度判断。3.5 验证数据对比经纬度、定位类型与PDOP模块解析出来之后不要急着说“通了”至少要做三项验证。第一项是定位状态用GGA的fix quality或UBX的fixType确认当前是否已有有效定位。如果你在室内状态一直是0或者1无定位或单点定位很正常需要到窗边或室外重测。第二项是坐标合理性。把你解析出的经纬度拿到地图上对照一下看是否在你所在地附近。注意WGS84坐标系和GCJ-02坐标系之间会有几百米的偏差如果你在调用国内地图API还需要做坐标转换这不是GPS模块的问题而是坐标系的问题。第三项是精度指标。GGA里有HDOPUBX里还有pDOP、hAcc、vAcc。正常情况下单点定位HDOP小于1.5就说明几何分布不错如果HDOP大于5即使显示“已定位”数据也不适合做精确导航。UBX的NAV-PVT里hAcc和vAcc以毫米为单位我一般会设置一个阈值比如水平精度超过5米就丢弃当前定位结果这样能有效过滤掉树荫、峡谷里的飘移点。4. 常见问题与排查经验4.1 串口收到乱码但模块灯常亮——波特率/电平问题这个现象出现频率极高。模块指示灯正常闪烁但串口助手打印出来的全是ÿ¥‰之类的乱码。原因通常有两种第一种是波特率设置错了。很多模块出厂默认是9600但也有一些版本默认是38400或115200尤其是二手模块或者被之前的人改过配置你根本不知道它现在是什么速率。解决方法是逐个试波特率或者用逻辑分析仪抓一下波形测量一个字符位的脉宽反推波特率。第二种是电平不匹配比如模块是3.3V TTL而你的USB转TTL是5V或者模块是RS232电平你却接到了TTL接口上。TTL电平的识别电压和RS232完全不同接错了自然全是乱码。解决办法就是确认电平标准必要时加转换板。4.2 只有时间没有定位——天线/星历/室内问题如果你看到GGA语句里经纬度都是0或者fix quality是0但时间字段是正常的说明模块已经收到了卫星时间但没有足够的卫星信号来计算位置。常见原因是天线位置不好、天线馈线断裂、或者在室内。排查顺序是先看可见卫星数GGA第8个字段或者UBX PVT的numSV如果为0说明模块根本收不到卫星信号检查天线如果大于4但不定位可能是星历老化这时候可以做一次冷启动。冷启动命令在不同模块上不一样u-blox用CFG-RST比如在一个新坐标系下清除星历payload bytes([0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 这是CFG-RST的部分载荷具体参考协议手册发送后模块会重启并重新搜星首次定位时间可能长达几十秒到几分钟耐心等。4.3 UBX配置重启后丢失——如何保存配置到Flash这是新手最容易踩的坑。用u-center或者串口命令把模块改成UBX输出当时好用一断电再上电又变回NMEA了。原因很简单你下发的是“运行时配置”并没有持久化到非易失存储。u-blox的CFG-CFG消息就是用来保存/加载/清空配置的。类0x06ID 0x09。载荷分成三组掩码每组4字节分别控制“要保存的配置子集”、“要加载的配置子集”和“要清空的配置子集”。如果你想把所有当前配置都保存到flash可以发送payload bytes([0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 第一组掩码为0x0000FFFF表示保存所有配置不同型号的u-blox模块对CFG-CFG掩码定义略有差异但基本思路一致。发送之后等几百毫秒再断电配置就保存好了。4.4 NMEA语句缺字段——不同厂家的差异NMEA 0183虽然名义上是标准但实际每个厂家的实现都留了自己的小心思。比如某些国产模块的GSV语句里SNR字段是空的某些模块的RMC语句在未定位时不输出后面几个字段还有些模块把GGA里的差分时间字段省略了导致字段序号整体前移。因此写NMEA解析器时不要假设每个字段都存在。我在代码里通常会先用fields body.split(,)然后检查关键字段的索引是否超长、字符串是否为空再考虑是否继续解析。对于空字段可以返回None或者直接跳过该条语句。千万不要让float()遇到空字符串时崩溃这样整个数据链就断了。4.5 工具推荐u-center、串口助手、逻辑分析仪调试GPS模块我常用的三件套u-centeru-blox官方上位机支持实时显示地图、查看UBX原始帧、生成配置、保存回放。几乎每个用u-blox模块的工程师都得装一个。串口助手类软件比如SSCOM、MobaXterm自带的串口终端适合快速看NMEA输出。要选支持十六进制显示和定时发送的因为配置UBX时经常要发hex字符串。逻辑分析仪当模块配置混乱、串口数据看不出规律时直接抓波形最靠谱。优先选支持协议解码的能直接把UART帧解成ASCII字符。除此之外如果你的项目量产建议用Python写个自动化测试脚本自动给模块下发固定配置、校验输出帧、记录通过/失败数量。这个脚本投入不大但能把产线上的“模块配置不一致”问题消灭在早期。实战下来我最想强调的一点是无论用NMEA还是UBX协议本身并不复杂复杂的是各种边缘情况——模块差异、信号抖动、数据异常。所以我在所有项目里都会在串口解析层加一道“看门狗”超过一定时间没收到有效定位就主动告警而不是让系统带着脏数据继续跑。GPS模块通信这件事做到“能用”不难做到“可靠”才真正考验细节。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →