RTKLIB 源码学习笔记四:单点定位之从 main 函数出发深入理解 rnx2rtkp
本文是我学习 RTKLIB 源码时整理的笔记,主要围绕
rnx2rtkp的main函数、时间转换函数和配置加载函数,记录了自己的理解过程。
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)开始的总秒数(含小数秒)。具体转换思路如下:
参数检查:确保年份在 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月] = 1,day-2 = -1,总为 0;1 月 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%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互为逆运算。
实现原理
计算总天数与秒数
days = t.time / 86400(整数天部分)sec = t.time % 86400(当天已过去的整秒数)处理四年周期(1461 天)
day = days % 1461(1461 = 365×3 + 366,即包含一个闰年的 4 年总天数)
这允许只在一个 4 年窗口内推算月日,简化计算。查表确定月、日
预定义数组mday包含连续 48 个月的天数(平年 12 个月 + 闰年 12 个月,共 24 个月?实际是 48 个元素,覆盖两个完整的 4 年周期?代码中循环mon<48,表示最多检查 48 个月(即 4 年),但正常只需 12 或 24 个月。实际上数组mday定义了 48 个元素:先是平年的 12 个月,接着闰年的 12 个月,再重复一次(共 48)。这样通过day与mday[mon]逐月相减,找到月份和日期。计算年
ep[0] = 1970 + (days / 1461) * 4 + (mon / 12)
其中mon是累计的月份索引(0~47),mon/12给出当前四年周期内的第几个年份(0~3)。计算月、日、时、分、秒
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.3utc2gpst和time2gpst函数
源码如下:
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。
实现逻辑
函数接受一个 UTC 时间
t(gtime_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. 配置处理函数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数组)中。
执行流程
打开文件
以只读方式打开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 的通用配置文件加载器,将形如key=value的文本设置批量应用到内部选项结构中。
配置优先级
在main函数中,先处理-k加载配置文件,再依次处理其他命令行选项。命令行参数的优先级高于配置文件,即如果配置文件中配置了参数,而命令行中也配置了参数,最终命令行的参数解析会覆盖配置文件中对应参数的值。
4. 总结
通过分析rnx2rtkp的main函数,我们理清了:
命令行参数与配置文件的解析流程
时间转换函数(
epoch2time、time2epoch、utc2gpst、time2gpst)的实现细节通用配置加载器
loadopts的工作原理
下一步将继续深入postpos函数,研究观测文件和导航文件的读取,以及核心的定位解算算法(如estpos)。RTKLIB 源码虽庞大,但采用“由外向内”的方法,每一层只关注当前抽象,可以有效降低理解难度。
学习建议
结合单步调试,观察关键变量的变化。
用极简的单历元观测文件验证算法流程。
勤做笔记,画调用关系图和数据流图。
希望本文对同样在学习 RTKLIB 的读者有所帮助。欢迎留言交流!
