
做了这么多年GNSS数据解析有一个场景我一直印象很深明明手里是一套支持全频点全星座的高端接收机拿到的却是基站发来的一堆RTCM 1002、1004老消息。解析完一看GPS只有L1/L2GLONASS也只能给到两个频点Galileo、北斗、QZSS的消息干脆没有。这不是接收机的错是协议本身撑不住了。后来RTCM标准里加入了MSM也就是Multiple Signal Messages多信号GNSS观测数据消息格式这事才算彻底解决。如果你正在做RTK、网络RTK、PPP或者任何涉及多频多星座观测数据解析的工作这篇文章值得你从头看完。本篇是MSM系列的第一篇聚焦在“格式”本身为什么需要MSM、消息编号怎么认、报文结构长什么样、伪距和相位是怎么压缩的以及我实际解码时踩过的坑。后面我还会单独聊RTKLIB里MSM4/MSM5/MSM7的差异、典型解码实现和精度验证方法。1. 老RTCM消息为什么撑不住多频多星座先看痛点1.1 传统1002/1004消息的设计边界RTCM 3.x早期消息是为“单系统双频”时代设计的。GPS最常看到的是1001/1002仅L1、1003/1004L1L2GLONASS对应1009/1010、1011/1012。这类消息的工作方式很“死板”消息编号本身决定了解析器要按哪套结构去读而每一套结构里卫星系统、信号频点、数据字段顺序都是写死的。举个例子1004消息里每个卫星的L1、L2观测数据是固定顺序排列的接收机想通过这条消息告诉外界“我这颗卫星L5也收了信噪比多少”协议层面没有地方放。Galileo E1/E5a/E5b、北斗B1I/B1C/B2a这种后出现的信号更不可能在旧消息里给预留位置。这不是软件能补的必须改协议结构。另一个痛点是压缩方式不够精细。1002/1004里伪距、载波相位虽然也做了差分编码但针对不同信号、不同精度需求的灵活性很差。随着多系统RTK成为标配带宽消耗越来越大旧格式越来越捉襟见肘。1.2 MSM引入了哪些结构性的改变MSM的设计思路和老消息完全不一样它不再为“某个系统某几个频点”单独设计消息结构而是把“卫星列表”“信号列表”“卫星-信号有效组合”这三件事拆开用掩码Mask方式动态表达。这样一来一条MSM消息可以表达该系统下任意卫星、任意信号组合的观测值新增一个信号类型不需要重新设计消息号只需要在信号掩码表里追加定义。MSM从RTCM 3.2 Amendment 2开始引入RTCM 3.3又做了补充和完善。现在市面上的主流GNSS接收机、RTK解算软件、CORS系统基本都已经切到MSM了。我自己在项目里如果拿到全是1002/1004的流第一反应是“这基站是不是没升级”而拿到1074、1075、1087这类消息号才会觉得这数据流是“这个时代的”。2. MSM消息家族速览从1071到1127的编号逻辑与消息分工2.1 消息编号如何一眼定位系统和消息类型RTCM MSM的消息编号有极强的规律。不同GNSS系统分别占用一段连续编号每段内部再按MSM1到MSM7详细划分。下面是完整的对应关系系统MSM1MSM2MSM3MSM4MSM5MSM6MSM7GPS1071107210731074107510761077GLONASS1081108210831084108510861087Galileo1091109210931094109510961097SBAS1101110211031104110511061107QZSS1111111211131114111511161117BDS1121112211231124112511261127这套编号在解析程序里特别好用读消息头前12位判断落在哪个区间再对7取模就知道是哪一类MSM。比如收到1075先判断是GPS段1071~1077再算差值4就知道是GPS MSM5。很多开源库就是这么分流的。需要注意的是不同标准版本对部分系统支持的信号数量有差异但编号规律是稳定的。2.2 MSM1~MSM7各自装了什么、怎么取舍MSM1到MSM7不是七个完全不同的协议更像是一套“可裁剪”的观测数据结构。核心观测量无非四类伪距、载波相位、多普勒、信噪比CN0。区别在于哪些字段采用差分压缩、哪些字段直接放完整值、以及是否包含半周期模糊指示等附加信息。消息类型伪距载波相位多普勒/CN0半周期模糊指示典型用途MSM1完整值完整值整周小数有无数据结构完整但带宽大少见MSM2差分值完整值整周小数有无过渡较少用MSM3差分值整周部分差分小数无无早期RTK、低带宽场景MSM4差分值整周部分差分小数无有实时动态定位常用MSM5差分值整周部分差分小数有有目前RTK/网络RTK最常用MSM6差分值高精度相位差分有有高精度后处理MSM7差分值高精度相位差分有有更高精度、PPP-RTK等实际工程中RTK差分通常直接用MSM4或MSM5。MSM4不携带多普勒和信噪比做RTK解算足够带宽更省MSM5多了多普勒和CN0对流层、电离层估计和快速模糊度固定有帮助也是我默认推荐的。PPK、精密单点定位这类场景往往对相位分辨率更敏感可以考虑MSM7。3. 报文结构从头读到尾参考站号、历元时间、三个掩码3.1 消息头中的关键字段历元时间、IODS、平滑与时钟标志拿到一条MSM消息解码顺序是从消息头开始的。消息号之后紧跟的是参考站ID、历元时间、多重信号消息指示、IODS、保留位、时钟校正指示、外部时钟校正指示、平滑指示、平滑间隔、卫星数量和信号数量。历元时间这个字段特别值得注意。它不是所有系统都通用的。GPS、Galileo、QZSS、BDS使用的是各自系统时间基准下的“周内秒×1000”单位是毫秒GLONASS用的是以天内秒为基准的毫秒数且时间基准是UTC(SU)。如果你把GLONASS的历元直接当成GPST的周内毫秒去算时间会偏出几秒甚至更多后面载波相位、伪距在时间对齐上全乱。IODSIssue of Data Station是数据集标识。它存在的意义是让接收机知道“当前这条消息用的参考历元或观测会话是否变化了”。当IODS变化时需要用不同IODS的观测值做差分对齐就要格外小心。平滑指示与平滑间隔影响伪距质量评估0表示未平滑1~7对应从0.1秒到10秒的不同平滑时间常数。这些字段虽然不直接参与位置解算但会直接影响数据筛选逻辑。3.2 卫星掩码、信号掩码、单元掩码三张表怎么拼出一颗星MSM最精华的部分是三个掩码。卫星掩码Satellite Mask固定64位每一位代表该系统内的一颗卫星。GPS用前32位对应PRN 1~32GLONASS用前24位对应频率号Galileo用前36位对应SVID 1~36BDS用前37位对应PRN 1~37QZSS用前10位对应PRN 193~202。哪些位为1就说明这个历元里该卫星有观测数据。信号掩码Signal Mask固定32位每一位对应系统支持的一种信号类型。以GPS为例信号位1通常是L1 C/A位2是L1P位4是L2 C/A位5是L2P位7是L5 I/Q位8是L1C D/P。北斗的信号位排布则是B1I、B1C、B2I、B2a、B3I等依次往后排。不同系统的信号掩码映射表都是标准单独定义的做解析器的时候不能凭经验猜。单元掩码Cell Mask更“立体”它是卫星掩码和信号掩码的笛卡尔积总位数等于卫星数乘以信号数矩阵里每一个交叉点表示“该卫星在该信号上有观测值”。读取时先按卫星从第1颗到第N颗再对每颗卫星遍历所有信号位。只有单元掩码里值为1的“卫星-信号”组合才会在后续观测值数据区有对应的数据。理解三个掩码的层级关系是解析MSM的核心。我打个比方卫星掩码是“今天哪些员工上班了”信号掩码是“公司有哪些工种”单元掩码则是“具体哪个员工在哪个工种的岗位上”。三张表对不上后面观测值一个都对不准。4. 观测值是怎么压缩与还原的差分伪距、相位拼接、半周期模糊4.1 差分伪距的“最小参考值法”MSM消息数据区里的伪距大多数情况下不是原始伪距完整值而是差分值。设计思路是对同一个信号类型内的所有卫星伪距找出一个最小的、或某一选定的参考值其余卫星的伪距都减去这个参考值再换算成0.0005米的整数倍数发送。因为有符号整数的取值范围远小于原始伪距字节数可以压得很低。接收端解码时需要把同一信号类型下所有卫星的差分伪距收集齐选定参考卫星把“差值参考值”恢复成真实伪距。关键点在于参考值的选取规则必须在编码器、解码器两端一致否则恢复出来的伪距会整体偏移直接导致定位结果异常。RTKLIB等开源实现里对参考伪距的选择和最终解算结果有完整逻辑但如果你是自己手写解析器一定要对着标准文档把参考值选取规则抠清楚。4.2 载波相位整周与小数分离存储载波相位在MSM里通常被拆成两部分整周计数PhaseRange integer和小数部分PhaseRange fractional。整周部分可以继续做差分压缩小数部分则直接按固定精度存储通常以0.0005周为单位。恢复时把整周乘以单位周、再加上小数修正就得到完整的载波相位观测量。这里有一个工程细节因为整周计数是“变化快、范围大”的量它的差分压缩通常相对于同一信号类型的参考整周计数来做。解码时如果用错参考、或者参考星的相位发生周跳所有差分相位都会跟着错。所以实际处理MSM相位数据时周跳检测要放在“恢复完整相位之后”做而不是在差分域做。4.3 多普勒、CN0和半周期模糊指示MSM5及更晚的消息里包含多普勒观测值单位是0.0001Hz的有符号整数。它来自接收机的载波环跟踪结果主要用途有两类一类是接收机运动状态估计另一类是对伪距做时间平滑、辅助周跳检测。CN0即载噪比单位按0.25dBHz换算正常信号范围一般在20~55dBHz之间。如果你看到解析结果里CN0超过64大概率是单位换算或偏移量处理错了。半周期模糊指示是MSM4之后才明确设计的字段。某些信号比如B1C、E1 OS在跟踪时存在180度相位模糊这个字段用于告诉解算器当前观测值是否需要翻转半周。处理不当的话载波相位会被整周期加0.5周解算时模糊度固定会非常困难。做PPP或RTK时遇到某颗卫星某频率一直固定不了整数模糊度要记得回查这个标志位。5. 实际解码一帧MSM5的心路历程顺序、映射与雷区5.1 从字节流转到结构化观测数据的解码步骤一条完整的MSM5解码在我实际项目里通常是按下面这几步走的。这里以GPS MSM51075为例但思路适用于所有系统。读取12位消息号确认是1075。如果是1085就是GLO MSM5判断方式就是看落在哪个系统段。读取参考站ID、历元时间等头部字段。存放历元时间时一定要带上系统标识不能裸存一个整型就完事。读取64位卫星掩码遍历位得到本历元可见的卫星PRN列表。读取32位信号掩码遍历位得到本历元出现的信号ID列表。计算单元掩码总位数 卫星数量 × 信号数量逐位读取得到所有“卫星-信号”有效组合。根据有效组合数量依次读取伪距差分值、载波相位整周差分值、载波相位小数、多普勒、CN0半周期模糊指示。对每个有效组合把差分值还原为真实观测值并存入按PRN和信号索引组织的二维数组。这里要特别强调观测值数据区的排列顺序是以“信号”为主、以“卫星”为辅的也就是说先把所有卫星在信号1上的伪距读完再读所有卫星在信号2上的伪距。很多人第一次写MSM解析器会想当然地按“卫星为主”顺序读结果所有观测量全部错位还很难排查。5.2 我踩过的坑消息号、位序、历元基准与信号组踩坑一消息号判断太“宽松”。新手容易写成“1071~1077就是GPS”这在标准里没错但要注意RTCM数据流里可能混着1019、1020这种星历消息以及1005/1006站点坐标消息。如果解析器主循环只按“前缀”匹配消息号很容易把非MSM消息也拿进MSM分类器里。我的做法是先判断消息号是否落在1071~1127区间再按具体系统段细分。踩坑二位序问题。MSM报文的位读取顺序是高位在前MSB first这和很多嵌入式平台默认的位域处理方式不同。如果你直接把报文按字节拷贝进结构体然后读取位域在大小端不一致的平台上会出大问题。稳妥的做法是用bit stream逐位读取不要依赖结构体位域。踩坑三历元时间基准。我最早联调GLO数据时把1085的历元时间当成了GPST周内毫秒结果差出十几秒导致双系统融合定位一直没法收敛。后来才意识到GLONASS用的是天内毫秒基准还是UTC(SU)。这个必须作为解析器的“系统相关字段”单独处理。踩坑四信号掩码各系统不一样。GPS的信号位1是L1 C/A北斗的信号位1是B1I data。如果你把一套GPS的信号映射表套到北斗上解析出来的信号类型会完全错乱。我的做法是在程序里为每个系统单独建立信号掩码映射表而且版本控制要跟着RTCM标准走。RTCM 3.3 Amendment之后的映射表比3.2多了几个信号老表直接拿过来用会漏信号。最后分享一个我自己在用的调试技巧拿到一段原始RTCM字节流后不要急着去判断每个字段的物理意义。先用支持MSM解析的成熟软件生成一份“标准答案”然后让自己写的解析器逐字段对比。对比时不要只看最终的伪距、载波相位数值更要对比卫星掩码、信号掩码、单元掩码这三个中间结果。只要这三个掩码和“标准答案”一致观测值部分基本不会错如果掩码都对不上先把位序和长度搞清楚再往下走。