当前位置: 首页 > news >正文

RTKLIB 源码学习笔记四:单点定位之从 main 函数出发深入理解 rnx2rtkp

本文是我学习 RTKLIB 源码时整理的笔记,主要围绕rnx2rtkpmain函数、时间转换函数和配置加载函数,记录了自己的理解过程。

1. main 函数学习

main 函数在rnx2rtkp.c文件中,代码如下:

int main(int argc, char **argv) { prcopt_t prcopt=prcopt_default;//定位的配置参数结构体 solopt_t solopt=solopt_default;//结果的配置参数结构体 filopt_t filopt={""};//文件的配置参数结构体 //下面全部是配置参数的处理 gtime_t ts={0},te={0}; double tint=0.0,es[]={2000,1,1,0,0,0},ee[]={2000,12,31,23,59,59},pos[3]; int i,j,n,ret; char *infile[MAXFILE],*outfile="",*p; prcopt.mode =PMODE_KINEMA; prcopt.navsys=0; prcopt.refpos=1; prcopt.glomodear=1; solopt.timef=0; sprintf(solopt.prog ,"%s ver.%s %s",PROGNAME,VER_RTKLIB,PATCH_LEVEL); sprintf(filopt.trace,"%s.trace",PROGNAME); /* load options from configuration file */ for (i=1;i<argc;i++) { if (!strcmp(argv[i],"-k")&&i+1<argc) {//配置文件获取配置 resetsysopts(); if (!loadopts(argv[++i],sysopts)) return -1; getsysopts(&prcopt,&solopt,&filopt); } } for (i=1,n=0;i<argc;i++) { if (!strcmp(argv[i],"-o")&&i+1<argc) outfile=argv[++i]; else if (!strcmp(argv[i],"-ts")&&i+2<argc) { sscanf(argv[++i],"%lf/%lf/%lf",es,es+1,es+2); sscanf(argv[++i],"%lf:%lf:%lf",es+3,es+4,es+5); ts=epoch2time(es); } else if (!strcmp(argv[i],"-te")&&i+2<argc) { sscanf(argv[++i],"%lf/%lf/%lf",ee,ee+1,ee+2); sscanf(argv[++i],"%lf:%lf:%lf",ee+3,ee+4,ee+5); te=epoch2time(ee); } else if (!strcmp(argv[i],"-ti")&&i+1<argc) tint=atof(argv[++i]); else if (!strcmp(argv[i],"-k")&&i+1<argc) {++i; continue;} else if (!strcmp(argv[i],"-p")&&i+1<argc) prcopt.mode=atoi(argv[++i]); else if (!strcmp(argv[i],"-f")&&i+1<argc) prcopt.nf=atoi(argv[++i]); else if (!strcmp(argv[i],"-sys")&&i+1<argc) { for (p=argv[++i];*p;p++) { switch (*p) { case 'G': prcopt.navsys|=SYS_GPS; case 'R': prcopt.navsys|=SYS_GLO; case 'E': prcopt.navsys|=SYS_GAL; case 'J': prcopt.navsys|=SYS_QZS; case 'C': prcopt.navsys|=SYS_CMP; case 'I': prcopt.navsys|=SYS_IRN; } if (!(p=strchr(p,','))) break; } } else if (!strcmp(argv[i],"-m")&&i+1<argc) prcopt.elmin=atof(argv[++i])*D2R; else if (!strcmp(argv[i],"-v")&&i+1<argc) prcopt.thresar[0]=atof(argv[++i]); else if (!strcmp(argv[i],"-s")&&i+1<argc) strcpy(solopt.sep,argv[++i]); else if (!strcmp(argv[i],"-d")&&i+1<argc) solopt.timeu=atoi(argv[++i]); else if (!strcmp(argv[i],"-b")) prcopt.soltype=1; else if (!strcmp(argv[i],"-c")) prcopt.soltype=2; else if (!strcmp(argv[i],"-i")) prcopt.modear=2; else if (!strcmp(argv[i],"-h")) prcopt.modear=3; else if (!strcmp(argv[i],"-t")) solopt.timef=1; else if (!strcmp(argv[i],"-u")) solopt.times=TIMES_UTC; else if (!strcmp(argv[i],"-e")) solopt.posf=SOLF_XYZ; else if (!strcmp(argv[i],"-a")) solopt.posf=SOLF_ENU; else if (!strcmp(argv[i],"-n")) solopt.posf=SOLF_NMEA; else if (!strcmp(argv[i],"-g")) solopt.degf=1; else if (!strcmp(argv[i],"-r")&&i+3<argc) { prcopt.refpos=prcopt.rovpos=0; for (j=0;j<3;j++) prcopt.rb[j]=atof(argv[++i]); matcpy(prcopt.ru,prcopt.rb,3,1); } else if (!strcmp(argv[i],"-l")&&i+3<argc) { prcopt.refpos=prcopt.rovpos=0; for (j=0;j<3;j++) pos[j]=atof(argv[++i]); for (j=0;j<2;j++) pos[j]*=D2R; pos2ecef(pos,prcopt.rb); matcpy(prcopt.ru,prcopt.rb,3,1); } else if (!strcmp(argv[i],"-y")&&i+1<argc) solopt.sstat=atoi(argv[++i]); else if (!strcmp(argv[i],"-x")&&i+1<argc) solopt.trace=atoi(argv[++i]); else if (*argv[i]=='-') printhelp(); else if (n<MAXFILE) infile[n++]=argv[i]; } if (!prcopt.navsys) { prcopt.navsys=SYS_GPS|SYS_GLO; } if (n<=0) { showmsg("error : no input file"); return -2; } //参数全部处理好后,开始后处理定位。ts、te、tint分别是开始时间、结束时间、采样频率。n为输入文件个数 ret=postpos(ts,te,tint,0.0,&prcopt,&solopt,&filopt,infile,n,outfile,"",""); if (!ret) fprintf(stderr,"%40s\r",""); return ret; }

该函数主要是做配置参数处理即准备,然后调用postpos函数开始进行后处理定位。

2. 时间转换函数理解

2.1epoch2time函数

源码如下:

extern gtime_t epoch2time(const double *ep) { const int doy[]={1,32,60,91,121,152,182,213,244,274,305,335}; gtime_t time={0}; int days,sec,year=(int)ep[0],mon=(int)ep[1],day=(int)ep[2]; if (year<1970||2099<year||mon<1||12<mon) return time; /* leap year if year%4==0 in 1901-2099 */ days=(year-1970)*365+(year-1969)/4+doy[mon-1]+day-2+(year%4==0&&mon>=3?1:0); sec=(int)floor(ep[5]); time.time=(time_t)days*86400+(int)ep[3]*3600+(int)ep[4]*60+sec; time.sec=ep[5]-sec; return time; }

这个函数epoch2time的核心思路是:将日历时间(年月日时分秒)转换为从 GPS 时间原点(1970-01-01 00:00:00 UTC)开始的总秒数(含小数秒)。具体转换思路如下:

  1. 参数检查:确保年份在 1970~2099 之间,月份在 1~12,否则返回全零的时间结构体。

  2. 计算从 1970 年到目标日期(0 时)的天数:

    • 先计算整年的天数:(year-1970)*365

    • 加上闰年数:(year-1969)/4
      这是因为 1970~2099 年间闰年规则简化为“能被 4 整除”,而(year-1969)/4能正确统计从 1970 年到year-1年中包含的闰年个数。

    • 加上当前年内到指定日期的天数(从 1 月 1 日起算):

      • doy[mon-1]给出当月第一天在年内的累计天数(例如 3 月 1 日为 60)。

      • 加上day - 2:因为 1 月 1 日应为第 0 天,所以doy[1月] = 1day-2 = -1,总为 0;1 月 2 日为 1,以此类推。

    • 如果当前年是闰年且月份 >= 3,需额外加 1 天(补偿 2 月 29 日)。

    • 最终days就是从 1970-01-01 到目标日期当天 00:00:00 所经过的天数。

  3. 计算秒数:

    • 将整数秒部分取出:sec = floor(ep[5])

    • 总秒数 =days*86400 + hour*3600 + minute*60 + sec

    • 小数秒部分单独存入time.sec

  4. 返回gtime_t结构体,其中time字段为整数秒(time_t类型),sec字段为小数秒(double)。

这种思路利用了 1970~2099 年闰年规律简单的特点,避免了复杂的公历计算,转换效率高且能覆盖 GPS 系统的常用时间范围。

2.2time2epoch函数

源码如下:

extern void time2epoch(gtime_t t, double *ep) { const int mday[]={ /* # of days in a month */ 31,28,31,30,31,30,31,31,30,31,30,31,31,28,31,30,31,30,31,31,30,31,30,31, 31,29,31,30,31,30,31,31,30,31,30,31,31,28,31,30,31,30,31,31,30,31,30,31 }; int days,sec,mon,day; /* leap year if year%4==0 in 1901-2099 */ days=(int)(t.time/86400); sec=(int)(t.time-(time_t)days*86400); //处理四年周期(1461 天)1461 = 365×3 + 366,即包含一个闰年的 4 年总天数)这允许只在一个 4 年窗口内推算月日,简化计算。 for (day=days%1461,mon=0;mon<48;mon++) { if (day>=mday[mon]) day-=mday[mon]; else break;//查表确定月、日 } ep[0]=1970+days/1461*4+mon/12; ep[1]=mon%12+1; ep[2]=day+1;//计算年、月、日、时、分、秒 ep[3]=sec/3600; ep[4]=sec%3600/60; ep[5]=sec%60+t.sec; }

函数time2epoch的作用是:将 RTKLIB 内部使用的gtime_t时间(自 1970-01-01 00:00:00 UTC 起的秒数,含小数)转换为公历时间(年、月、日、时、分、秒),存入数组ep。它与epoch2time互为逆运算。

实现原理

  1. 计算总天数与秒数
    days = t.time / 86400(整数天部分)
    sec = t.time % 86400(当天已过去的整秒数)

  2. 处理四年周期(1461 天)
    day = days % 1461(1461 = 365×3 + 366,即包含一个闰年的 4 年总天数)
    这允许只在一个 4 年窗口内推算月日,简化计算。

  3. 查表确定月、日
    预定义数组mday包含连续 48 个月的天数(平年 12 个月 + 闰年 12 个月,共 24 个月?实际是 48 个元素,覆盖两个完整的 4 年周期?代码中循环mon<48,表示最多检查 48 个月(即 4 年),但正常只需 12 或 24 个月。实际上数组mday定义了 48 个元素:先是平年的 12 个月,接着闰年的 12 个月,再重复一次(共 48)。这样通过daymday[mon]逐月相减,找到月份和日期。

  4. 计算年
    ep[0] = 1970 + (days / 1461) * 4 + (mon / 12)
    其中mon是累计的月份索引(0~47),mon/12给出当前四年周期内的第几个年份(0~3)。

  5. 计算月、日、时、分、秒
    ep[1] = mon % 12 + 1(月份 1-12)
    ep[2] = day + 1(日期,从 1 开始)
    ep[3] = sec / 3600(小时)
    ep[4] = (sec % 3600) / 60(分钟)
    ep[5] = (sec % 60) + t.sec(秒,含小数)

注意事项
该函数仅适用于1970–2099 年的日期范围(因为闰年规则简化为“能被 4 整除”,符合该区间的实际闰年规律)。
小数秒被保留在ep[5]中。

一句话总结
time2epoch将内部秒计数还原为人类可读的年月日时分秒(含小数),是 RTKLIB 中重要的时间格式化函数。

2.3utc2gpsttime2gpst函数

源码如下:

extern gtime_t utc2gpst(gtime_t t) { int i; for (i=0;leaps[i][0]>0;i++) { if (timediff(t,epoch2time(leaps[i]))>=0.0) return timeadd(t,-leaps[i][6]); } return t; } extern double time2gpst(gtime_t t, int *week) { gtime_t t0=epoch2time(gpst0); time_t sec=t.time-t0.time; int w=(int)(sec/(86400*7)); if (week) *week=w; return (double)(sec-(double)w*86400*7)+t.sec; }

函数utc2gpst的作用是:将 UTC 时间转换为 GPS 时间(GPST),并考虑闰秒的影响

原理
UTC 与 GPST 的关系:GPST 从 1980‑01‑06 00:00:00 开始连续累积秒数(无闰秒),而 UTC 为匹配地球自转会不定期插入闰秒,因此 GPST = UTC + 当前已累积的闰秒总数。
例如,若 UTC 时间为 2023‑01‑01 00:00:00,此时已累积 18 闰秒,则 GPST 为 2023‑01‑01 00:00:18。

实现逻辑

  1. 函数接受一个 UTC 时间tgtime_t类型)。

  2. 遍历一个全局闰秒表leaps(通常每行包含一个闰秒生效的 UTC 时间点和对应的累积闰秒数leaps[i][6])。

  3. 使用timediff(t, epoch2time(leaps[i])) >= 0.0判断当前 UTC 时间是否大于等于该闰秒条目的生效时间。

  4. 当找到第一个满足条件的闰秒条目时,用timeadd(t, -leaps[i][6])从 UTC 中减去累积闰秒数,得到 GPST。

  5. 如果没有任何闰秒条目匹配,则直接返回原时间(相当于闰秒为 0)。

一句总结
utc2gpst根据内置闰秒表,将 UTC 时间扣掉已累积的闰秒总数,从而转换为连续的 GPS 时间。

time2gpst函数的作用是:将 RTKLIB 内部使用的gtime_t时间转换为 GPS 周数(Week)和周内秒(Time of Week, TOW)

具体逻辑

  1. 定义 GPS 起始时刻
    gtime_t t0 = epoch2time(gpst0);
    gpst0是一个全局数组,通常定义为{1980, 1, 6, 0, 0, 0},即 GPS 时间的起点:1980年1月6日 00:00:00 UTC(注意此时与 UTC 的差值尚未累积闰秒,但 GPST 从该时刻起连续计数)。

  2. 计算总秒差
    time_t sec = t.time - t0.time;
    t的整数秒部分减去t0的整数秒部分,得到自 GPS 起始时刻以来的整数秒数(不含小数秒)。

  3. 计算 GPS 周数
    int w = (int)(sec / (86400 * 7));
    一周有 86400×7 = 604800 秒,整数除法得到完整的周数。

  4. 输出周数(可选)
    if (week) *week = w;
    如果调用者提供了week指针,则把周数存入。

  5. 计算并返回周内秒
    return (double)(sec - (double)w * 86400 * 7) + t.sec;

    • sec - w * 604800:当前周内已经过去的整数秒。

    • + t.sec:加上原始gtime_t中存储的小数秒部分(t.sec是 double,表示秒的小数部分)。
      最终结果是一个double,表示本周期内从周日零时开始的秒数(含小数)。

示例
若输入t对应 2023-01-01 00:00:00 UTC(此时 GPS 周约为 2242,周内秒约为 0 附近),函数将计算出正确的周数和周内秒。

一句话总结
time2gpst把 RTKLIB 的绝对时间(gtime_t)拆解成 GPS 周和周内秒,便于后续处理或输出。

3. 配置处理函数loadopts

main函数中调用了loadopts。源码如下:

extern int loadopts(const char *file, opt_t *opts) { FILE *fp; opt_t *opt; char buff[2048],*p; int n=0; trace(3,"loadopts: file=%s\n",file); if (!(fp=fopen(file,"r"))) { trace(1,"loadopts: options file open error (%s)\n",file); return 0; } while (fgets(buff,sizeof(buff),fp)) { n++; chop(buff); if (buff[0]=='\0') continue; if (!(p=strstr(buff,"="))) { fprintf(stderr,"invalid option %s (%s:%d)\n",buff,file,n); continue; } *p++='\0'; chop(buff); if (!(opt=searchopt(buff,opts))) continue; if (!str2opt(opt,p)) { fprintf(stderr,"invalid option value %s (%s:%d)\n",buff,file,n); continue; } } fclose(fp); return 1; }

函数loadopts的作用是:从指定的文本文件中读取配置选项,并将其解析后填充到预定义的选项表(opt_t数组)中

执行流程

  1. 打开文件
    以只读方式打开file指定的文件。若失败则打印错误并返回 0。

  2. 逐行读取并处理

    • 使用fgets读取一行到buff,行号n递增。

    • 调用chop(buff)去除行尾空白和#注释。

    • 跳过空行。

  3. 解析键值对

    • 查找第一个=字符。若不存在,报错并跳过该行。

    • =替换为\0,使buff只保留选项名,p指向选项值的起始位置。

    • 再次调用chop(buff)清理选项名末尾的空白。

  4. 查找并设置选项值

    • searchopt(buff, opts)在选项表opts中查找匹配的选项名(opts的每一元素包含选项名、类型、值指针等)。若未找到,跳过该行。

    • 调用str2opt(opt, p)将字符串p转换为选项所需的数据类型(整数、实数、字符串等),并存入opt对应的成员。

  5. 关闭文件并返回
    处理完所有行后关闭文件,返回 1(成功)。

一句话总结
loadopts是 RTKLIB 的通用配置文件加载器,将形如key=value的文本设置批量应用到内部选项结构中。

配置优先级
main函数中,先处理-k加载配置文件,再依次处理其他命令行选项。命令行参数的优先级高于配置文件,即如果配置文件中配置了参数,而命令行中也配置了参数,最终命令行的参数解析会覆盖配置文件中对应参数的值。

4. 总结

通过分析rnx2rtkpmain函数,我们理清了:

  • 命令行参数与配置文件的解析流程

  • 时间转换函数(epoch2timetime2epochutc2gpsttime2gpst)的实现细节

  • 通用配置加载器loadopts的工作原理

下一步将继续深入postpos函数,研究观测文件和导航文件的读取,以及核心的定位解算算法(如estpos)。RTKLIB 源码虽庞大,但采用“由外向内”的方法,每一层只关注当前抽象,可以有效降低理解难度。

学习建议

  • 结合单步调试,观察关键变量的变化。

  • 用极简的单历元观测文件验证算法流程。

  • 勤做笔记,画调用关系图和数据流图。

希望本文对同样在学习 RTKLIB 的读者有所帮助。欢迎留言交流!

http://www.jsqmd.com/news/1199389/

相关文章:

  • 2026北京爱马仕回收行业核验报告:5家正规商户专业度评级 - 生活时报
  • kylin-user-guide高级配置教程:终端个性化设置与快捷键详解
  • SAP ABAP 从一个程序调用另外一个程序的perform
  • Linux内核Lockup机制解析与实战排查
  • Lua调用C++函数实战:从虚拟栈原理到高性能插件开发
  • UE4 Pak文件分析利器:UnrealPakViewer图形化工具实战指南
  • GraphRAG:从向量检索到知识图谱增强的进化之路
  • 微软Modern OS核心技术解析与架构革新
  • AI系统中Multiple Tools架构设计与工程实践
  • 家用艾灸养生器具怎么提高转化?小程序商城运营思路参考,含零代码SAAS、源码定制交付
  • Linus Torvalds与AI编程:技术大牛的态度转变与实践
  • 深度学习迁移学习与微调技术详解
  • 2026广州黄金回收红黑榜,拒绝无资质流动回收 - 禹竞奢收行
  • 2026万博科技职业学院报考三件套:招生咨询电话、线上填报步骤、官方报考注意事项 - 最新资讯
  • Contrastive Knowledge Transfer and Robust Optimization for Secure Alignment of Large Language Models
  • LSTM遗忘门原理详解与Python实现:掌握序列模型核心机制
  • Windows10下YOLOv8 TensorRT CUDA加速部署实战
  • 0欧电阻在PCB设计中的关键应用与选型指南
  • 国产玻璃隔热膜怎么选:双星卡地亚建筑膜全产业链方案解析 - 资讯焦点
  • 1Panel安装全解析:Linux服务器Docker容器管理实战指南
  • jiuwen-deepsearch未来路线图:AI搜索技术的发展趋势与展望
  • AI Agent工程化落地:从Demo到生产级交付的完整路径
  • 2026 最新行情:万国葡七缺保卡亏上万,合扬 7 月回收附件加价规则曝光 - 小禾收名品
  • openEuler服务能力参考:版本定制服务的实施方法与案例
  • 北京劳力士绿水鬼 / 黑水鬼 / 鬼王回收,合扬实体店比价高出 30% - 全国奢侈品回收
  • ROS rqt 启动段错误(Segmentation Fault)的深度诊断与修复指南
  • Ubuntu系统安装与配置全指南
  • Windows装机必备软件清单:提升效率的精选工具
  • Windows API窗口创建与CreateWindow函数详解
  • 2026青岛黄金回收市场深度调研:闲置黄金如何安全高价变现——行业标杆易奢福全解析 - ys韩