简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19o测试观测文件和一个txt说明文本,整体仅4.3MB;其中r_rinex302_o_data.m是读取观测文件的主程序,UTC_GPSsecond.m负责将UTC时间换算为GPS周秒,另有原始测试数据及运行结果便于校验。该代码由作者根据多方资料自行编制,测试过程无误,虽时间转换函数仍可优化,但足以完成历元数计算,适合需要读取Rinex3.02文件或二次开发定位程序的MATLAB使用者参考。目前已有5244人浏览学习,下载后结合说明文件即可快速上手。 做GNSS数据处理的同学,十有八九遇到过这种场景:手里拿到一台新接收机的Rinex 3.02格式观测文件(o文件),想用MATLAB做伪距单点定位或多系统融合实验,结果第一步就卡在读取上。MATLAB自带函数并没有直接支持Rinex 3.02,网上搜到的大多数老代码又基于2.11,一跑就报错。这篇文章就是把这层窗户纸捅破,从Rinex 3.02头文件格式到多系统观测值怎么解析,给你一套可以“抄作业”的MATLAB读取方案。适合正在做卫星导航数据处理、精密单点定位预处理,或者只是想快速把o文件变成可用矩阵的研究生和工程师。
1. 项目背景与适用场景
1.1 为什么MATLAB读Rinex 3.02会让人头疼
Rinex(Receiver Independent Exchange Format)是GNSS领域最通用的数据交换格式,几乎每台接收机、每个处理软件都会涉及。Rinex 3.02版本正式支持多系统混合观测,比如同一时刻能看到GPS的G、BDS的C、GLONASS的R、Galileo的E,卫星标识带系统前缀,观测类型也从2.11的C1、L1、P2这类短编码,变成了C1C、L1C、C2W、L5Q这种带频率和跟踪模式的组合编码。
MATLAB里常见的readmatrix、textscan直接怼上去是读不出来的,因为o文件混合了文本头部和数值数据行,头部每个类型列数还不一样。更麻烦的是,每个卫星系统的观测类型数量不同,导致数据记录中每行的观测值个数也各不相同。直接按列宽读,或者用%f全量扫描,很容易把数据错位。这也是很多人在论坛上问到一半就弃坑的原因。
1.2 适合谁读
这篇文章不是给纯GNSS理论研究者看的,而是给实际要动手处理数据的人写的。如果你在做RTK/PPP预处理、多系统组合定位、接收机性能评估,或者只是想把一个站点的o文件快速变成结构体/表格用于后续画图和计算,那么下面的内容可以直接套用。代码基于MATLAB的文本读取函数,不需要额外工具箱,基础版MATLAB就能运行。
2. RINEX 3.02 格式必须知道的关键点
2.1 头文件:版本、系统与观测类型
Rinex 3.02的头文件是纯文本行,比较重要的是下面几行:
RINEX VERSION / TYPE:版本和文件类型,一般在第1行。第1~9字符是版本号,第20字符是文件类型,O代表观测文件。SYS / # / OBS TYPES:系统观测类型列表,每个卫星系统一行,一个文件可能有多行。TIME OF FIRST OBS、TIME OF LAST OBS:起止时间,用于粗检查。
SYS / # / OBS TYPES这一行的格式很有规律,举个例子:
G 16 C1C L1C D1C S1C C2W L2W D2W S2W C5Q L5Q D5Q S5Q C2L L2L D2L第一个字符是系统标识,比如G、R、E、C;第7~9字符是当前系统的观测类型数量;从第10或11字符开始是具体的类型名称列表。有些软件导出的文件列位会有点偏差,所以我个人不太建议去死抠绝对列位置,而是用strsplit按空格拆之后再取第一个字符和后面的类型名,这样兼容性更高。
2.2 数据记录:历元与观测值行的对应关系
数据部分由若干个历元组成,每个历元的第一行是时间信息,称为“历元记录行”,格式类似:
2024 07 01 12 00 00.000000 0 8第1~6组数字是年月日时分秒,后面跟着历元标志(一般0代表正常)和卫星数量。紧接着会有多行卫星观测数据,每行以卫星号开头,例如:
G01 23012345.678 23012345.678 123456789.012 ... R02 21098765.432 ...卫星号占前3个字符,后面的观测值顺序必须严格对应该卫星系统在头文件里SYS / # / OBS TYPES那行给出的类型顺序。这是整个读取过程最容易出问题的地方。
2.3 和RINEX 2.11的几个重要区别
建议先明确差异,避免把旧版思路带过来:
| 对比项 | RINEX 2.11 | RINEX 3.02 |
|---|---|---|
| 卫星标识 | PRN只有数字,如01 | 带系统前缀,如G01、R02 |
| 观测类型 | C1、L1、P2 | C1C、L1C、C2W、L5Q |
| 系统支持 | 主要是GPS,少数扩展GLONASS | G/R/E/C/J多系统混合 |
| 观测类型列表 | 全文件统一一个列表 | 每个系统单独一个列表 |
| 数据行对齐 | 按观测值个数固定读 | 必须按系统各自类型数对应读 |
正是由于每个系统都有自己的观测类型列表,读数据时不能直接按“文件里所有类型的并集”去统一截取第几个字段,必须动态判断这个卫星属于哪个系统,再找该系统对应的类型顺序。
3. MATLAB读取函数设计与实现
3.1 设计思路
读取逻辑可以拆成两条线:
- 头部解析:逐行读到
END OF HEADER为止,把每个系统的观测类型存成结构体,比如obsTypes.G、obsTypes.R。 - 数据解析:继续逐行处理,遇到历元记录就开一个新历元,然后按照该历元下的卫星系统去解析后续卫星行。
可以用状态机模型理解:正常状态下,遇到“日期时间+卫星数”的行就进入“等待卫星数据”状态;遇到卫星号行就解析并填充当前历元;遇到下一个历元行再刷新状态。这样不用一次把所有文件读进内存,对动辄几十MB的o文件更友好。
我实践的结论是:用fgetl逐行读是最稳的。textscan虽然也能用,但一旦遇到某些接收机输出的非标准空格数,很容易把时间字段和观测字段粘在一起。
3.2 头部解析
核心代码片段如下,负责提取版本号和每个系统的观测类型:
fid = fopen(fname, 'rt'); obsTypes = struct(); version = []; while true line = fgetl(fid); if ~ischar(line), break; end if contains(line, 'END OF HEADER'), break; end if contains(line, 'RINEX VERSION / TYPE') version = str2double(strtrim(line(1:9))); end if contains(line, 'SYS / # / OBS TYPES') sysStr = strtrim(line(1)); n = str2double(strtrim(line(7:9))); typesTxt = strtrim(line(10:60)); typeList = regexp(typesTxt, '[A-Z][0-9A-Z]', 'match'); if isempty(typeList) typeList = strsplit(typesTxt); end if ~isnan(n) && n > 0 typeList = typeList(1:min(n, length(typeList))); end obsTypes.(sysStr) = typeList; end end这里有个小技巧:用regexp(typesTxt, '[A-Z][0-9A-Z]', 'match')提取类型名,比直接strsplit更能过滤掉多余的空格和换行符。如果某行只有一个类型,regexp也能正确匹配。
3.3 历元与观测数据解析
首先要解决“怎么判断当前行是新的历元”的问题。我用一个正则表达式匹配年份、月、日、时、分、秒的连续数字:
epochRegex = '^\s*\d{1,4}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}(\.\d+)?\s+';卫星观测行以字母开头,例如G01、R02,所以不会被这个正则误判。这个做法在绝大多数Rinex 3.02文件里都成立。
判断出是历元行之后,再用sscanf(line, '%f')把整行的数字都读出来,分别对应年月日时分秒、历元标志和卫星数。接下来循环读取后续卫星行,并按16字符宽度截取观测值字段:
obsStr = line(4:end); row = NaN(1, length(allTypes)); % allTypes是全部系统观测类型的并集 for k = 1:length(types) colIdx = find(strcmp(allTypes, types{k}), 1); if isempty(colIdx), continue; end startPos = (k-1)*16 + 1; endPos = startPos + 13; % 前14个字符是观测值 if startPos > length(obsStr), continue; end field = obsStr(startPos:min(endPos, length(obsStr))); val = str2double(strtrim(field)); if ~isnan(val) row(colIdx(1)) = val; end end重点说明一下为什么取前14个字符:Rinex 3.02观测值字段固定占16个字符,其中前14位是数值,后2位分别是失锁指示(LLI)和信噪比(SSI)。我们读取数值时会把后2位忽略,避免LLI或SSI混入数字导致str2double失败。如果某些文件是14字符对齐的紧凑格式,这段代码依然能工作,因为取不到的就直接保持NaN。
3.4 调用方法与输出结构
把上面的逻辑组合成一个函数read_rinex302_obs,保存为read_rinex302_obs.m后,调用方式很简单:
obs = read_rinex302_obs('test.obs');输出结构包含:
obs.version:Rinex版本号obs.obsTypes:每个系统的观测类型,如obs.obsTypes.Gobs.time:每个历元的UTC时间,datetime列向量obs.sat:每个历元的卫星号,cell数组obs.data:一个表格,列名是合并去重后的所有观测类型,每列是一个cell数组,内部再按历元保存数值向量
比如想要第1个历元、GPS卫星G01的C1C伪距,可以通过obs.data.C1C{1}(1)访问。如果后续要做位置解算,更常用的做法是把表格展开成“一行一颗卫星”的宽表,加一个for循环把cell数组拆开即可。
4. 实操验证与结果展示
4.1 测试文件读取
我自己用一个多系统静态站数据做了测试,文件头能看到这样的观测类型列表:
G 16 C1C L1C D1C S1C C2W L2W D2W S2W C5Q L5Q D5Q S5Q C2L L2L D2L R 8 C1C L1C D1C S1C C2P L2P D2P S2P E 15 C1X L1X D1X S1X C5X L5X D5X S5X C7X L7X D7X S7X C8X L8X D8X C 14 C2I L2I D2I S2I C7I L7I D7I S7I C6I L6I D6I S6I C1I L1I运行读取函数后,MATLAB工作区里能看到obs.time的区间覆盖了外业时段,obs.data.C1C{1}的长度和该历元GPS卫星数一致。
用一个小脚本验证读取结果和原始文件是否相符:
% 检查第一个历元的卫星数量和卫星号 disp(obs.time(1)); disp(obs.sat{1}); % 检查GPS L1伪距是否有异常 c1c_vals = obs.data.C1C{1}; fprintf('C1C非NaN个数: %d\n', sum(~isnan(c1c_vals)));如果C1C列中非NaN个数明显少于该历元GPS卫星数,说明观测值字段截取可能偏了,或者该文件里某些观测类型没被接收机记录。
4.2 数据快速检查
还有一个很实用的检查点是卫星号是否带频率槽位,尤其是GLONASS。标准Rinex 3.02中GLONASS卫星号同样写成R01、R02,但部分接收机在观测值行后面可能会附加频率信息。遇到这种情况时,解析卫星号用strtrim(line(1:3))仍然有效,但如果文件里出现R01和频率槽作为单独字段的扩展格式,就需要在读取后额外提取。多数情况下,我们只需要关注前3个字符的卫星号,其余扩展字段可以忽略。
5. 常见问题与排查技巧实录
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 版本始终为空 | 头部前9列被软件导出成空格或版本号不在第1行 | 用contains(line, 'RINEX VERSION / TYPE')定位,再strtrim(line(1:9)),不要假设绝对列位置 |
| 某个系统没读到观测类型 | SYS / # / OBS TYPES行第一个字符是空格 | 用strtrim(line(1))而不是line(1) |
| 观测值全是NaN | 字段宽度不对,或类型顺序和头文件不一致 | 检查行首是否对齐,尝试把字段宽改成14或16分别测试 |
| 卫星数偏少或历元缺失 | 正则表达式没匹配到某些非标准时间格式(如秒数无小数且后面连续空格) | 改用regexp(line, epochRegex, 'once'),并对行首字符是否为数字做二次判断 |
| GLONASS卫星号解析异常 | 某些文件里卫星号占了4位,如R01 | 统一用strtrim(line(1:min(3, length(line))))再补零 |
几个容易踩的坑,单独补充一下:
- 不要用
textscan(fid, '%f%f%f%f%f%f%f%f')直接读整个数据段,因为不同系统的观测值个数不一样,读到最后会错位。 - 不要简单地把所有观测类型并集后固定成统一列索引就完事,必须通过卫星系统的判断去映射。否则GPS的数据可能会被塞到BDS的列里。
- 对空值一定要保留NaN,而不是用0填充。后续做伪距定位时,0值会直接污染最小二乘过程。
genvarname在MATLAB新版里会有warning,可以换成matlab.lang.makeValidName,效果一样,还更安全。
最后再分享一个我自己的习惯:拿到任何新格式的o文件,先不要跑完整代码,先用一个几十KB的小文件,读完后打印第一个历元的前5行原始数据,再打印解析后的obs.data,人工对比几列。这一步看起来笨,但比任何调试log都管用,几分钟就能定位是列宽问题还是类型映射问题。多试几个不同品牌的接收机文件,自然就能总结出哪些字段是“弹性”的,哪些是必须严格按标准来的。
本文还有配套的精品资源,点击获取