
本文是我学习 RTKLIB 源码时整理的笔记主要围绕rnx2rtkp的main函数、时间转换函数和配置加载函数记录了自己的理解过程。1. main 函数学习main 函数在rnx2rtkp.c文件中代码如下int main(int argc, char **argv) { prcopt_t prcoptprcopt_default;//定位的配置参数结构体 solopt_t soloptsolopt_default;//结果的配置参数结构体 filopt_t filopt{};//文件的配置参数结构体 //下面全部是配置参数的处理 gtime_t ts{0},te{0}; double tint0.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.navsys0; prcopt.refpos1; prcopt.glomodear1; solopt.timef0; sprintf(solopt.prog ,%s ver.%s %s,PROGNAME,VER_RTKLIB,PATCH_LEVEL); sprintf(filopt.trace,%s.trace,PROGNAME); /* load options from configuration file */ for (i1;iargc;i) { if (!strcmp(argv[i],-k)i1argc) {//配置文件获取配置 resetsysopts(); if (!loadopts(argv[i],sysopts)) return -1; getsysopts(prcopt,solopt,filopt); } } for (i1,n0;iargc;i) { if (!strcmp(argv[i],-o)i1argc) outfileargv[i]; else if (!strcmp(argv[i],-ts)i2argc) { sscanf(argv[i],%lf/%lf/%lf,es,es1,es2); sscanf(argv[i],%lf:%lf:%lf,es3,es4,es5); tsepoch2time(es); } else if (!strcmp(argv[i],-te)i2argc) { sscanf(argv[i],%lf/%lf/%lf,ee,ee1,ee2); sscanf(argv[i],%lf:%lf:%lf,ee3,ee4,ee5); teepoch2time(ee); } else if (!strcmp(argv[i],-ti)i1argc) tintatof(argv[i]); else if (!strcmp(argv[i],-k)i1argc) {i; continue;} else if (!strcmp(argv[i],-p)i1argc) prcopt.modeatoi(argv[i]); else if (!strcmp(argv[i],-f)i1argc) prcopt.nfatoi(argv[i]); else if (!strcmp(argv[i],-sys)i1argc) { for (pargv[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 (!(pstrchr(p,,))) break; } } else if (!strcmp(argv[i],-m)i1argc) prcopt.elminatof(argv[i])*D2R; else if (!strcmp(argv[i],-v)i1argc) prcopt.thresar[0]atof(argv[i]); else if (!strcmp(argv[i],-s)i1argc) strcpy(solopt.sep,argv[i]); else if (!strcmp(argv[i],-d)i1argc) solopt.timeuatoi(argv[i]); else if (!strcmp(argv[i],-b)) prcopt.soltype1; else if (!strcmp(argv[i],-c)) prcopt.soltype2; else if (!strcmp(argv[i],-i)) prcopt.modear2; else if (!strcmp(argv[i],-h)) prcopt.modear3; else if (!strcmp(argv[i],-t)) solopt.timef1; else if (!strcmp(argv[i],-u)) solopt.timesTIMES_UTC; else if (!strcmp(argv[i],-e)) solopt.posfSOLF_XYZ; else if (!strcmp(argv[i],-a)) solopt.posfSOLF_ENU; else if (!strcmp(argv[i],-n)) solopt.posfSOLF_NMEA; else if (!strcmp(argv[i],-g)) solopt.degf1; else if (!strcmp(argv[i],-r)i3argc) { prcopt.refposprcopt.rovpos0; for (j0;j3;j) prcopt.rb[j]atof(argv[i]); matcpy(prcopt.ru,prcopt.rb,3,1); } else if (!strcmp(argv[i],-l)i3argc) { prcopt.refposprcopt.rovpos0; for (j0;j3;j) pos[j]atof(argv[i]); for (j0;j2;j) pos[j]*D2R; pos2ecef(pos,prcopt.rb); matcpy(prcopt.ru,prcopt.rb,3,1); } else if (!strcmp(argv[i],-y)i1argc) solopt.sstatatoi(argv[i]); else if (!strcmp(argv[i],-x)i1argc) solopt.traceatoi(argv[i]); else if (*argv[i]-) printhelp(); else if (nMAXFILE) infile[n]argv[i]; } if (!prcopt.navsys) { prcopt.navsysSYS_GPS|SYS_GLO; } if (n0) { showmsg(error : no input file); return -2; } //参数全部处理好后开始后处理定位。ts、te、tint分别是开始时间、结束时间、采样频率。n为输入文件个数 retpostpos(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 (year1970||2099year||mon1||12mon) return time; /* leap year if year%40 in 1901-2099 */ days(year-1970)*365(year-1969)/4doy[mon-1]day-2(year%40mon3?1:0); sec(int)floor(ep[5]); time.time(time_t)days*86400(int)ep[3]*3600(int)ep[4]*60sec; time.secep[5]-sec; return time; }这个函数epoch2time的核心思路是将日历时间年月日时分秒转换为从 GPS 时间原点1970-01-01 00:00:00 UTC开始的总秒数含小数秒。具体转换思路如下参数检查确保年份在 1970~2099 之间月份在 1~12否则返回全零的时间结构体。计算从 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总为 01 月 2 日为 1以此类推。如果当前年是闰年且月份 3需额外加 1 天补偿 2 月 29 日。最终days就是从 1970-01-01 到目标日期当天 00:00:00 所经过的天数。计算秒数将整数秒部分取出sec floor(ep[5])。总秒数 days*86400 hour*3600 minute*60 sec。小数秒部分单独存入time.sec。返回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%40 in 1901-2099 */ days(int)(t.time/86400); sec(int)(t.time-(time_t)days*86400); //处理四年周期1461 天1461 365×3 366即包含一个闰年的 4 年总天数这允许只在一个 4 年窗口内推算月日简化计算。 for (daydays%1461,mon0;mon48;mon) { if (daymday[mon]) day-mday[mon]; else break;//查表确定月、日 } ep[0]1970days/1461*4mon/12; ep[1]mon%121; ep[2]day1;//计算年、月、日、时、分、秒 ep[3]sec/3600; ep[4]sec%3600/60; ep[5]sec%60t.sec; }函数time2epoch的作用是将 RTKLIB 内部使用的gtime_t时间自 1970-01-01 00:00:00 UTC 起的秒数含小数转换为公历时间年、月、日、时、分、秒存入数组ep中。它与epoch2time互为逆运算。实现原理计算总天数与秒数days t.time / 86400整数天部分sec t.time % 86400当天已过去的整秒数处理四年周期1461 天day days % 14611461 365×3 366即包含一个闰年的 4 年总天数这允许只在一个 4 年窗口内推算月日简化计算。查表确定月、日预定义数组mday包含连续 48 个月的天数平年 12 个月 闰年 12 个月共 24 个月实际是 48 个元素覆盖两个完整的 4 年周期代码中循环mon48表示最多检查 48 个月即 4 年但正常只需 12 或 24 个月。实际上数组mday定义了 48 个元素先是平年的 12 个月接着闰年的 12 个月再重复一次共 48。这样通过day与mday[mon]逐月相减找到月份和日期。计算年ep[0] 1970 (days / 1461) * 4 (mon / 12)其中mon是累计的月份索引0~47mon/12给出当前四年周期内的第几个年份0~3。计算月、日、时、分、秒ep[1] mon % 12 1月份 1-12ep[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.3utc2gpst和time2gpst函数源码如下extern gtime_t utc2gpst(gtime_t t) { int i; for (i0;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 t0epoch2time(gpst0); time_t sect.time-t0.time; int w(int)(sec/(86400*7)); if (week) *weekw; 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。实现逻辑函数接受一个 UTC 时间tgtime_t类型。遍历一个全局闰秒表leaps通常每行包含一个闰秒生效的 UTC 时间点和对应的累积闰秒数leaps[i][6]。使用timediff(t, epoch2time(leaps[i])) 0.0判断当前 UTC 时间是否大于等于该闰秒条目的生效时间。当找到第一个满足条件的闰秒条目时用timeadd(t, -leaps[i][6])从 UTC 中减去累积闰秒数得到 GPST。如果没有任何闰秒条目匹配则直接返回原时间相当于闰秒为 0。一句总结utc2gpst根据内置闰秒表将 UTC 时间扣掉已累积的闰秒总数从而转换为连续的 GPS 时间。time2gpst函数的作用是将 RTKLIB 内部使用的gtime_t时间转换为 GPS 周数Week和周内秒Time of Week, TOW。具体逻辑定义 GPS 起始时刻gtime_t t0 epoch2time(gpst0);gpst0是一个全局数组通常定义为{1980, 1, 6, 0, 0, 0}即 GPS 时间的起点1980年1月6日 00:00:00 UTC注意此时与 UTC 的差值尚未累积闰秒但 GPST 从该时刻起连续计数。计算总秒差time_t sec t.time - t0.time;用t的整数秒部分减去t0的整数秒部分得到自 GPS 起始时刻以来的整数秒数不含小数秒。计算 GPS 周数int w (int)(sec / (86400 * 7));一周有 86400×7 604800 秒整数除法得到完整的周数。输出周数可选if (week) *week w;如果调用者提供了week指针则把周数存入。计算并返回周内秒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. 配置处理函数loadoptsmain函数中调用了loadopts。源码如下extern int loadopts(const char *file, opt_t *opts) { FILE *fp; opt_t *opt; char buff[2048],*p; int n0; trace(3,loadopts: file%s\n,file); if (!(fpfopen(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 (!(pstrstr(buff,))) { fprintf(stderr,invalid option %s (%s:%d)\n,buff,file,n); continue; } *p\0; chop(buff); if (!(optsearchopt(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数组中。执行流程打开文件以只读方式打开file指定的文件。若失败则打印错误并返回 0。逐行读取并处理使用fgets读取一行到buff行号n递增。调用chop(buff)去除行尾空白和#注释。跳过空行。解析键值对查找第一个字符。若不存在报错并跳过该行。将替换为\0使buff只保留选项名p指向选项值的起始位置。再次调用chop(buff)清理选项名末尾的空白。查找并设置选项值用searchopt(buff, opts)在选项表opts中查找匹配的选项名opts的每一元素包含选项名、类型、值指针等。若未找到跳过该行。调用str2opt(opt, p)将字符串p转换为选项所需的数据类型整数、实数、字符串等并存入opt对应的成员。关闭文件并返回处理完所有行后关闭文件返回 1成功。一句话总结loadopts是 RTKLIB 的通用配置文件加载器将形如keyvalue的文本设置批量应用到内部选项结构中。配置优先级在main函数中先处理-k加载配置文件再依次处理其他命令行选项。命令行参数的优先级高于配置文件即如果配置文件中配置了参数而命令行中也配置了参数最终命令行的参数解析会覆盖配置文件中对应参数的值。4. 总结通过分析rnx2rtkp的main函数我们理清了命令行参数与配置文件的解析流程时间转换函数epoch2time、time2epoch、utc2gpst、time2gpst的实现细节通用配置加载器loadopts的工作原理下一步将继续深入postpos函数研究观测文件和导航文件的读取以及核心的定位解算算法如estpos。RTKLIB 源码虽庞大但采用“由外向内”的方法每一层只关注当前抽象可以有效降低理解难度。学习建议结合单步调试观察关键变量的变化。用极简的单历元观测文件验证算法流程。勤做笔记画调用关系图和数据流图。希望本文对同样在学习 RTKLIB 的读者有所帮助。欢迎留言交流