
做GNSS接收机或者RTK解算的朋友谁没跟RTCM协议打过几回交道呢尤其是近几年多频多系统普及之后老一套的单系统消息格式越来越不够用RTCM SC-104委员会在3.2 Amendment 1 / 3.3版本里推的MSMMultiple Signal Message多信号观测消息已经成为事实上的标准。我最早接触MSM是被一堆“1074、1124”的消息号绕晕的后来为了解析MSM语句把协议啃了一遍才慢慢把卫星掩码、信号掩码、单元掩码这套逻辑整理清楚。这篇文章就以MSM语句为中心把多信号GNSS观测数据消息格式的整体框架、关键字段、掩码机制和实际解析要点拆开讲一遍。如果你正在做协议解析、PPP/RTK终端的固件开发或者只是被手里的接收机日志逼到想弄明白MSM到底在传什么那这篇文章应该能帮你省下不少翻标准文档的时间。我尽量把常用的参数、单位、位域含义和踩过的坑都写出来带着例子讲方便你直接照着处理数据。1. 先捋清楚整体框架1.1 MSM是什么为什么会出现MSM是RTCM 3.x协议里一组专门承载GNSS原始观测数据的消息类型。2016年RTCM 3.3标准正式把完整的MSM定义纳入规范实际产品里更早的RTCM 3.2 Amendment 1时代就已经在用了。在没有MSM之前RTCM 3.x用的是老一批分系统消息比如GPS的1001/1002/1004GLONASS的1009/1010/1012。这类消息的问题很明显GPS、GLONASS、Galileo、北斗、SBAS、QZSS每个系统都要单独定义一套消息号而且老消息只覆盖单频或双频一旦要做三频、四频的观测值传输就得不停打补丁兼容性搞得一塌糊涂。MSM解决这个问题的思路非常直接设计一套统一的消息骨架再用“系统编号 消息编号”来区分不同系统与不同观测类型。所有系统共享同一套字段布局卫星掩码和信号掩码用来动态表示“这条消息里有哪些卫星、哪些频点信号”数据量也能根据实际内容自适应压缩。简而言之MSM的出现让“一套协议传遍所有星座、所有频段”成为可能这也是今天RTK和PPP设备几乎都认MSM的原因。1.2 MSM1~MSM7从精简到完整的选择题MSM不是一个单独的消息而是一整族编号从MSM1一直到MSM7。它们的结构骨架完全一样区别在于每条消息里装载的观测数据类型不同。MSM1只包含伪距观测值MSM2伪距 载波相位MSM3伪距 载波相位 多普勒频移MSM4伪距 载波相位 多普勒 信噪比CNRMSM5伪距 载波相位 多普勒 信噪比 半周模糊度指示MSM6在MSM5基础上增加FDMA信号的质量指示字段MSM7最完整的版本包含所有观测值类型以及详细的质量指示信息你可能注意到了MSM5在实际RTK/NTRIP服务里非常常见大部分基准站默认输出的就是MSM4或MSM5级别。MSM7最齐全但数据量也最大对链路带宽要求更高。所以选哪个类型本质上是在“信息完整度”和“传输效率”之间做权衡。用消息号来区分的话每个系统都有一个MSM1~7对应序列常见的是系统MSM4 消息号MSM5 消息号MSM6 消息号MSM7 消息号GPS1074107510761077GLONASS1084108510861087Galileo1094109510961097SBAS1104110511061107QZSS1114111511161117BDS1124112511261127这里有个细节值得留意同样一条“MSM4”消息前缀数字不同对应的星座就不同。1074是GPS、1084是GLONASS、1094是Galileo、1124是北斗。协议解析的第一步就是先看消息号确定星座类型再决定怎么翻译后面的卫星编号和信号编号。1.3 MSM消息号与实际产品的对应你在NTRIP Caster上看到的挂载点比如“RTCM3.2-MSMS”通常会在数据流里混着1074/1075/1084/1085/1094/1095/1124/1125等多条消息。每一颗卫星在同一历元的观测值会按星座分别放进不同的MSM消息中。所以一次完整的多系统观测数据可能是GPS一条MSM5、GLONASS一条MSM5、Galileo一条MSM5、北斗一条MSM5同时出现。它们共享同一条数据流各自独立成帧。解析器要维护好“当前正在组包的星座上下文”否则很容易出现时间戳错位。2. MSM消息结构逐层拆开看2.1 公共头所有MSM共用的开头部分一条RTCM 3.x消息的外层结构由三部分组成前导码0xD3、12位保留字段和12位消息长度、消息体最后是24位CRC校验。MSM消息体内部又分为公共头、卫星段、信号段和观测值段四层。MSM公共头包含的字段包括Message Number12bit例如1074Reference Station ID12bit基准站编号GNSS Epoch Time30bit以毫秒为单位的历元时间Multiple Message1bit指出同一历元是否还有后续消息IODS3bit数据站播发标识Reserved1bit预留位GNSS Divergence-free Smoothing Indicator1bitGNSS Clock Steering Indicator2bitGNSS External Clock Indicator2bitGNSS Smoothing Indicator1bitGNSS Smoothing Interval3bitGNSS Epoch Time这个字段值得专门说一下。GPS、Galileo、QZSS、北斗的时间基准以毫秒计数而GLONASS使用的是它自己的时间系统与UTC之间存在整3小时的偏差。解析GLONASS MSM时Epoch Time换算不能直接套GPS的周内秒逻辑否则历元时间会整整偏掉3个小时。很多新手在这上面栽过跟头。Multiple Message位也很关键。如果该位为1说明当前历元的数据比较长被拆成了多条MSM消息需要等待同历元的后续消息全部到齐后才能当作完整的一帧观测数据处理。我在实际调试中就遇到过只处理单条消息导致卫星数量“随机跳动”的问题排查半天才发现是忽略了Multiple Message标志。2.2 卫星段与信号段掩码是怎么工作的公共头之后就是MSM最核心、也最容易把人绕晕的部分卫星掩码Satellite Mask、信号掩码Signal Mask和单元掩码Cell Mask。MSM的设计思想是不直接挨个列出每个观测值而是先用掩码描述“有哪些卫星、哪些信号”再按掩码定义好的顺序依次排列观测值。这样接收机只需要根据掩码中置为1的位去对应取数数据紧凑且无冗余。卫星掩码的每个bit代表该星座一颗预定义的卫星信号掩码的每个bit代表该星座一个预定义的信号频率。协议标准里为每个星座定义了参考卫星编号表和参考信号编号表解析时必须按这些表翻译。单元掩码则是在卫星掩码和信号掩码确定之后生成一个N颗卫星 × M种信号大小的矩阵逐位表示“这颗卫星是否有这个信号的观测值”。由于这个矩阵是按位压缩的解析时候的位偏移计算是最考验细心的环节。2.3 位域排布高位在前按顺序铺满RTCM 3.x协议统一采用大端位序也就是说所有多bit字段都是最高位在前。这个约定贯穿整个MSM消息不管读卫星掩码、信号掩码还是后续的细观测值都要遵循同样的位序逻辑。很多人在实现解析器时喜欢直接把整个消息转成字节数组然后逐bit取。这个思路没问题但要特别注意MSM里有些字段跨字节边界比如32bit的信号掩码可能横跨第4字节、第5字节、第6字节和第7字节。任何一次按字节位移的“想当然”都可能导致数据整体错位。我在工程里习惯先把整条消息的位流提取成bool数组再做后续取段虽然稍微浪费一点内存但可读性和排错效率都会高很多。如果不用位流数组那就得牢记公式bit_offset byte_index * 8 bit_index每次读取字段前先计算好起点和长度再按需拼接。这类逐位拼接的代码一旦出问题表现往往是“某颗卫星的伪距突然变成天文数字”非常烦人。3. 卫星段核心字段掩码如何映射到实际卫星3.1 卫星掩码的具体排布卫星掩码字段按照GNSS系统不同长度也不一样通常是64bit。GPS的64bit中低32位对应PRN 1到32高32位通常预留用于SBAS卫星或其他补充编号Galileo的64bit对应G1到G36剩余的位预留北斗的64bit对应C1到C37等。掩码置位的顺序很重要。解析器必须先从低位开始扫描找到所有置为1的bit按从低到高的顺序给这些卫星编号0、1、2……N-1。这个顺序就是后续所有卫星数据字段的分组顺序。假如卫星掩码中第1位、第5位、第9位为1那么卫星编号顺序就是卫星1、卫星5、卫星9绝不会是卫星9排在前面。如果把卫星掩码解析顺序搞反后面所有观测值都会对应到错误的卫星上。RTK解算出错时检查卫星掩码的扫描顺序是第一步这是我从一次真实事故里总结出来的教训当时我们发现某颗卫星的伪距残差总是异常大最后定位到竟是卫星掩码高低位扫描方向写反了固件里的注释还写着“LBS first”改完瞬间恢复正常。3.2 卫星数据段伪距粗值与细值卫星掩码确定后消息会进入卫星数据段里面包含与每颗卫星相关的粗略观测信息这些信息用于辅助恢复完整观测值。MSM里伪距观测值被拆成两个部分一个是“粗值”Rough Range一个是“细值”Fine Pseudorange。“细值”是分辨率很高的部分单位通常为0.001米但它的取值范围很小不足以覆盖整段星地距离。所以消息里还需要“粗值”来提供大尺度的距离段信息。粗值的单位通常对应较大的间隔可以简单理解成“整毫秒量级”的距离信息。载波相位也类似分为“粗相位”和“细相位”。“细相位”分辨率为0.0001周“粗相位”用来确定整周部分的数值。这两个部分配合在一起接收机才能重建出完整的伪距和载波相位观测值。很多刚接触MSM的人会问“为什么不直接存一个完整浮点数”答案很简单压缩。RTCM传输走的是串口、网络链路带宽有限粗/细值分离编码可以在不牺牲精度的前提下显著减少字节数。GNSS差分数据要求的是“传输前后一致”不是“最省事地传输”所以复杂一点也值得。3.3 伪距、载波相位、多普勒的常用单位MSM消息里观测值数据的单位与常规RTCM老消息不同也更细。这里把我常用的参考值列出来观测值类型分辨率常见约定说明伪距0.001米毫米级精度载波相位0.0001周万分之一个载波周期多普勒0.0001Hz足够精细的速度观测信息信噪比 CNR0.001dB-Hz信号质量指示处理的时候一定要把这些分辨率乘回实际数值。我曾经调试某条MSM5消息发现伪距输出一直比真实值小约300米怀疑是天线问题后来一查原来是解析代码里把伪距细值默认当成“已经乘过0.001米”的浮点数实际上协议里存的是原始整数必须自己乘回去。这类“单位是否已经换算”的问题是MSM解析里最常见也最隐蔽的坑。4. 信号段核心字段从掩码到观测量的完整链路4.1 信号掩码你看到的不是频率而是编号信号掩码紧随卫星掩码之后。它的作用与卫星掩码类似不过表示的是“这条消息里出现了该星座的哪些信号”。每个星座在RTCM标准里都维护着一张信号定义表比如GPS的L1 C/A、L1P、L2P、L2C、L5Galileo的E1、E5a、E5b、E6北斗的B1I、B1C、B2a、B2b、B3I。信号掩码的每一个bit依次对应这些信号编号。协议本身并没有直接把频率数值写进消息解析器必须查表才能知道bit 3代表的是L1还是L5。这也是MSM格式学习曲线较陡的原因之一。信号掩码的长度取决于该星座支持的信号总数常见实现是32bit或64bit。比如只支持8种信号的系统用32bit足够而现代全频段系统可能把扩展的64bit都利用上。解析时遇到信号掩码为64bit的消息一定不能用旧的32bit逻辑硬套否则后面全部字段错位。4.2 单元掩码观测值存在的“开关矩阵”卫星掩码和信号掩码确定后接下来是单元掩码Cell Mask。这个字段本质上是一个N×M的按位矩阵行是卫星列是信号。比如卫星掩码里有5颗卫星信号掩码里有4个信号单元掩码就可能有20个bit逐一表示“该卫星在该频率上是否有观测值”。为什么需要单元掩码呢因为不是每颗卫星、每个频点都有可用的观测值比如某颗卫星的L5信号可能暂时没有锁定。如果直接把所有N×M个观测值全量发送会浪费很多带宽。单元掩码用0/1把无效组合过滤掉解码端就知道哪些“格子”有数据、哪些没有。解析单元掩码时要特别注意它的读取顺序先按卫星顺序再按信号顺序。也就是说第一颗卫星的所有信号位排在最前面然后是第二颗卫星的信号位。如果顺序搞反数据不会报错但会把L1的数据当成L5事后排查非常麻烦。4.3 信号数据的“两遍读取”问题信号掩码和卫星掩码都解析完成后消息就到了观测值数据段也就是伪距、载波相位、多普勒、信噪比等物理量的实际数值。MSM的处理逻辑是先按照卫星段中“置位卫星”的排列顺序逐颗卫星读取每颗卫星的伪距粗值然后按照同样的顺序逐颗卫星读取每颗卫星的伪距细值再读载波相位的粗值和细值再读多普勒、信噪比。每一类观测值都是独立地、按相同卫星顺序循环一遍。有些解析器为了减少内存拷贝试图“读一个格子的所有值再读下一个格子”这在MSM里是行不通的。消息的排列方式是“所有卫星的所有伪距再所有卫星的所有载波相位”不是“每颗卫星的所有观测值再下一颗卫星”。我把它叫作“两遍读取逻辑”写代码时要把这个结构刻在脑子里。第一次接触MSM时我就在这里实现错了输出结果看起来很有规律但对不上卫星编号后来对照标准里的字段顺序图才修正过来。信号数据还会分带外频率FDMA跳频场景例如GLONASS的L1/L2信号单元掩码里每个信号对应的实际载波频率并不完全一致。解析MSM时如果要做高精度定位光把观测值解出来还不够还得根据卫星编号和信号编号去星历里查对应的实际频率。这部分属于MSM与导航电文的联动单看消息本身是解不出频率值的。5. 实操自己动手解析一条MSM消息5.1 准备工作字节序、CRC和对齐解析RTCM 3.x消息第一步是找帧头0xD3。找到之后读取第2、3字节中的12bit消息长度然后按长度取出消息体再校验消息末尾的24bit CRC。RTCM 3.x的CRC算法是CRC-24Q多项式为0x1864CFB初始值为0。这一环节跟MSM本身没太大关系但几乎所有解析器问题都藏在CRC实现的差异里。CRC校验通过后就可以开始按MSM的逻辑取位。我建议先把整条消息不包括CRC展开成一个bit数组然后从公共头开始一个字段一个字段地切。bit数组的index从0开始定位公式很直观消息号bit 0 ~ bit 11基准站IDbit 12 ~ bit 23GNSS历元时间bit 24 ~ bit 53后续字段依次偏移只要公共头的字段长度和顺序记清楚后面的偏移量就能顺势推下来。我建立一个结构体把所有字段名、位宽、偏移量都放进去解析时按表驱动方式读取这样后续扩展新消息号也方便。5.2 解析伪代码的核心逻辑以MSM4 GPS消息1074为例解析流程可以简化成下面几步读取12位消息号确认是1074读取公共头剩余字段得到基准站ID和历元时间读取64位卫星掩码扫描置1位得到卫星列表根据星座类型读取信号掩码得到信号列表计算卫星数与信号数的乘积读取单元掩码得到“哪些格子有有效观测值”按卫星顺序、信号顺序依次读取伪距粗值、伪距细值、载波相位粗值、载波相位细值、多普勒、信噪比将原始整数与分辨率相乘恢复成物理量。这段逻辑用伪码写出来大概是这样# 已提取出整个消息的bit数组 bits长度为 total_bits msg_number read_bits(bits, 0, 12) station_id read_bits(bits, 12, 12) epoch_time read_bits(bits, 24, 30) sat_mask read_bits(bits, sat_mask_start, 64) sat_list [i for i in range(64) if (sat_mask i) 1] sig_mask read_bits(bits, sig_mask_start, sig_mask_len) sig_list [j for j in range(sig_mask_len) if (sig_mask j) 1] cell_bits read_bits(bits, cell_mask_start, len(sat_list) * len(sig_list)) # 按行优先顺序读取cell masksat_idx为外层循环sig_idx为内层循环这段代码片段虽然只是示意但已经能反映MSM解析的核心先掩码、后单元、再数据。实际工程里还要处理Multiple Message标志、跨消息的历元合并等问题但骨架就是这样的。5.3 与RTKLIB等工具的结合经验如果你不想从零写解析器完全可以借助RTKLIB、GNSS-SDR、pyrtcm这类开源工具库。RTKLIB的lib/rcv/rtcm3.c里对MSM消息的解析写得很完整可以直接学习它的数据结构定义与位读取方式。pyrtcm库在Python环境下调试数据流也很方便。我用RTKLIB调试经验是先用它把MSM消息解成RINEX观测文件再对照RINEX里的卫星/信号顺序去检查自己解析器的输出是否一致。RINEX是标准中间格式比直接看二进制容易理解得多。只要两边在相同历元、相同卫星、相同信号上的观测值一致基本就能判定自己的解析逻辑没大问题。如果只是偶尔分析一段日志用RTKLIB自带的rtkrcv或者convbin转一下格式就够了。但如果是做嵌入式设备想把MSM解析集成到自己的固件里那就必须认真把位解析逻辑吃透开源代码可以当参考但不能直接照搬因为嵌入式环境往往对内存占用和数据拷贝更敏感。6. 常见问题与排查技巧实录6.1 典型问题速查表问题现象可能原因解决思路同一颗卫星的伪距偶尔跳变单元掩码读取顺序错位检查Cell Mask的遍历顺序确认外层是卫星历元时间差3小时GLONASS时间基准换算错误按GLONASS时间系统单独处理解算时卫星数永远偏少忽略了Multiple Message标志将同历元多条消息合并后再处理信号掩码长度不一致导致整体错位使用了旧的32bit固定长度读取前先按系统类型和标准确认掩码长度伪距数值特别大或特别小忘记乘分辨率确认原始整数与分辨率换算逻辑这张表是我多年调试经验的浓缩。遇到类似现象优先怀疑解析顺序和单位换算而不是怀疑信号源本身。GNSS数据链路是出了名的“数据漂亮但含义全错”的地方表面上看每个字段都有值实际上字段位置早就错位了。6.2 定位错位问题的独家技巧如果怀疑解析器字段错位有一个很实用的自查方法选取一颗已知位置的基准站利用卫星星历估算某颗卫星的伪距大致范围再对比自己解析出来的伪距是否在同一量级。如果解析结果偏离几千公里基本可以确定是掩码或粗值处理错误如果只差几米那可能是天线相位中心或者钟差修正的问题跟格式无关。另一个技巧是打印卫星掩码和信号掩码的原始bit图形。我会在调试时把64位卫星掩码打印成“10010001...”这样一串字符串人工核对卫星PRN顺序。掩码一旦错了后续所有步骤都没有继续排查的意义先确认这个是最省时间的。6.3 建议的调试顺序我第一次调MSM解析时脑子很乱后来摸索出一套顺序现在每次都按这个来先只解析公共头和卫星掩码打印消息号、历元时间、卫星列表确认星历验证后再解析信号掩码和单元掩码前两步稳定后再解析伪距并且只输出伪距跟RTKLIB对比伪距无误后再逐步加入载波相位、多普勒、信噪比等其余观测值。每一步都验证通过再进下一步。跳步调试是MSM开发的大忌因为位偏移是一环扣一环的一旦中间某步错了后面所有字段都会跟着错排查范围会被无限放大。严格按照这个节奏来一般半天就能完成一个可用的MSM解析模块。我自己在实际项目里最深的体会是MSM的协议本身并不算特别复杂难的是在所有边界条件下都不出错。多系统、多信号、可变掩码、多条消息拆分、跨历元拼接这些情况叠加在一起才是真正的考验。所以在代码里多留断言、多打日志把每一步位偏移和字段值都记录下来能帮你省下无数排查时间。最后再分享一个小技巧解析MSM的时候不要急着把所有观测值马上换算成浮点数尽量保留原始整数和分辨率在真正需要计算的时候再乘回去。这样不但能避免单位换算引入的二次误差而且在对比不同版本协议时能直接比对原始整数排查更高效。MSM这套格式其实已经相当稳定了后面你再接触新系统、新信号时会发现只要把掩码表更新一下解析框架完全不用动。这大概就是MSM最大的设计价值吧。