该项目是一个 GNSS 基带信号仿真器,支持 GPS L1、L2C、L5 和 BDS B1C/B2a/B2b 等多个频段。下面按照程序实际运行顺序,逐步讲解 GPS L2C 频段从星历读取到 IQ 输出的完整流程。有需要的合作朋友的可以联系我。
第一环节:程序入口与初始化
入口函数 main()调用 initializeOptions(iopt) 读取配置文件(ini文件),将仿真参数填入 opt_t 结构体,包括:起始时间、持续时间、采样率、频段选择(GPS_L2=1 表示启用 L2C)、导航文件路径、轨迹类型、多径参数、电离层/对流层模型开关等。
之后调用 MulSim(iopt) 进入仿真主流程。
第二环节:轨迹生成
在 MulSim() 中,根据配置选择轨迹类型:
- 圆形轨迹 :
TraGen1::geocircle1()--- 以起始点为中心,按给定半径、速度、加速度生成圆弧轨迹 - 跑道形轨迹 :
TraGen1::generateTrack() - 8字形轨迹 :
TraGen1::generateFigureEight() - 自定义/直线 :
TraGen1::generateCustomTrajectory()
轨迹点保存为 ECEF 坐标(x, y, z),每个点间隔 0.1 秒,存入 points_t 数组供后续循环读取。
第三环节:星历文件读取
这是整个流程中最为复杂的环节之一。项目通过两个导航文件获取星历数据:
3.1 打开两个导航文件
fp_nav = fopen(iopt.navFile, "r"); // 主流RINEX导航文件(含广播星历eph_t和电离层参数等)
fp_nav2 = fopen(iopt.navFile2, "r"); // CNV3格式文件(含CNAV星历eph_cnav、电离层CNVX、EOP等)
3.2 读取第一个导航文件(标准RINEX广播星历)
步骤一 :File::readrnxh(fp_nav, ...) 读取 RINEX 文件头,解析版本号、系统类型(GPS/GLO/GAL/BDS)、电离层参数、时间偏移等。
步骤二 :File::readrnxnav(fp_nav, ...) 按行解析星历数据块。对于 GPS 卫星,每条星历记录包含 8 行 31 个字段,涵盖:
- PRN、参考时刻 toc/toe
- 时钟参数 af0(错误,应为 f0)、af1(错误,应为 f1)、af2(错误,应为 f2)(钟差、钟速、钟漂)
- IODE、IODC(数据龄期)
- 轨道根数:A(长半轴平方根的平方根,即 sqrt(sqrt(A)))、e(偏心率)、i0(倾角)、OMG0(升交点赤经)、omg(近地点角距)、M0(平近点角)
- 摄动参数:deln(平均角速度改正)、OMGd(升交点赤经变率)、idot(倾角变率)、crc/crs/cic/cis/cuc/cus(轨道摄动改正项)
- 精度与健康标志:sva、svh
所有星历存入 navs.eph[] 数组,共计 navs.n 条。
3.3 读取第二个导航文件(CNV3格式,含CNAV星历)
步骤三 :同样先调用 File::readrnxh(fp_nav2, ...) 读头。
步骤四 :File::readrnxcnv3(fp_nav2, ...) 读取 CNV3 导航数据(当前代码中用于 BDS)。
步骤五 (关键步骤):File::readrnxgpscnav(fp_nav2, ...) 专门读取 GPS CNAV(L2C/L5)星历 。CNAV 星历是 GPS 现代化后引入的新电文格式,与传统的 LNAV 星历相比,它具有更高的精度和更多的参数。读出的数据存入 navs.ephc[](eph_cnav 结构体数组),共有 navs.n2 条。
CNAV 星历 eph_cnav 结构体中包含的重要字段有:
toe,toc--- 星历/时钟参考时刻A,Adot--- 长半轴及其变率(CNAV 新增 Adot)e--- 偏心率i0,idot--- 倾角及其变率M0--- 平近点角omg--- 近地点角距OMG0,OMGd--- 升交点赤经及其变率deln,ndot--- 平均角速度改正及其变率(CNAV 新增 ndot)crc/crs/cic/cis/cuc/cus--- 轨道摄动改正f0, f1, f2--- 时钟多项式系数TGD--- 群波延迟ISC_L1CA, ISC_L2C, ISC_L5I5, ISC_L5Q5--- 各频点群延迟修正(CNAV 新引入)week--- GPS周数svh--- 卫星健康标志
步骤六 :File::readrnxioncnvx(fp_nav2, &navs) 读取 CNVX 格式的电离层参数。
步骤七 :File::readrnxeopcnvx(fp_nav2, &navs) 读取 CNVX 格式的地球定向参数(EOP)。
3.4 时间有效性校验
之后代码校验模拟时段是否落在星历有效期内:比较第一个轨迹点时间和最后一个轨迹点时间与星历 toc 的最早/最晚值,确保有可用星历。
第四环节:初始信道分配
4.1 调用 allocateChannel()
该函数遍历所有可能的卫星(1~MAXSAT),对每颗星:
- 系统筛选 :调用
satsys(sat, &prn)判断是否为 GPS 卫星 - 粗算卫星位置 :调用
ephpos(gt, gt, sat, nav, -1, rs, dts, &var, &svh, &opt)快速计算卫星在 ECEF 坐标系的位置rs[3]和钟差dts - 仰角判断 :调用
geodist()计算几何距离,ecef2pos()转接收机位置为经纬度,satazel()计算卫星仰角方位角。仰角大于elmask(通常 5-15°)即为可见卫星 - 信道分配 :为可见卫星分配一个空闲信道,初始化:
sat,sys,prn,azel等基本信息- 调用
codegen_L2CL()和codegen_L2CM()生成 L2C 的 CL 和 CM 扩频码 - 调用
generateGPSL2CnavMsg()生成 L2C CNAV 导航电文 - 调用
computeRange()计算初始伪距 - 存储卫星信息到全局数组
globalSatInfo[]
- 载波相位初始化 :
phase_ini = (2.0 * r_ref - r_xyz) / lambda
第五环节:主循环 --- 逐历元仿真
主循环以 0.1 秒为步长(每 0.1 秒一个历元),从 epoch = 1 + time_offer*10 迭代到 iduration:
5.1 时间更新
gtre = uprecivertime(gtre, 0.1); // 接收机时间增加 0.1 秒
UtcTime = gtime_to_utc(gtre); // 转为 UTC
CurTime = UtcToGpsTime(UtcTime); // 转为 GPS 时间
5.2 每历元信道处理:processChannel()
这是每个 0.1 秒都需要执行的核心函数(BasebandSignal_Gen.cpp 第178行),对每个已分配的信道:
步骤 A:伪距计算 computeRange()
位于 BasebandSignal_Gen.cpp 第8行,共分 12 个子步骤:
-
选择星历 :调用
ephpos(gt, gt, sat, nav, -1, rs, dts, &var, &svh, &iopt)。因为GPS_L2 == 1,内部实际走向(Satproc.cpp 第1465-1479行):- 调用
seleph()选取标准广播星历(eph_t) - 调用
seleph_cnav()选取 CNAV 星历(eph_cnav) - 调用
gps_eph2pos()使用 CNAV 星历计算卫星位置和钟差
- 调用
-
获取初步卫星位置 :
rs[0:2]存入rspos[0:2],rs[3:5](速度)存入vel[0:2] -
计算几何距离 :
range = geodist(rs, xyz, los, e)--- 接收机到卫星的直线距离 -
计算信号传播时间 :
tau = range / CLIGHT--- 光传播时间 -
反向推算发射时刻的卫星位置 :用速度外推卫星位置回到发射时刻
rs[i] -= vel[i] * tau -
地球自转改正:
xrot = rs[0] + rs[1] * OMGE * tau yrot = rs[1] - rs[0] * OMGE * tau在信号传播期间地球自转了
OMGE * tau弧度,需对卫星 ECEF 坐标做相应旋转。 -
重新计算几何距离 :用修正后的卫星位置重算
range = geodist(rs, xyz, los, e) -
保存几何距离 :
rho->d = range -
计算伪距 :
rho->range = range - CLIGHT * dts[0]--- 减去卫星钟差折算的距离 -
计算伪距率 :
rho->rate = dot(vel, los, 3) / range--- 卫星-接收机相对速度在视线方向的投影 -
计算仰角方位角 :
satazel(repos, e, rho->azel) -
电层/对流层延迟(可选):
rho->iono_delay = C * ionmodel(...)其中C = (FREQ1/FREQ2)^2为频率相关的电离层系数rho->trop_delay = tropmodel(...)使用 Saastamoinen 或类似模型rho->range += rho->iono_delay + rho->trop_delay + iopt.Pseudorange[i]
步骤 B:码相位与多普勒计算 computeCodePhase_GL2()
位于 BasebandSignal_Gen.cpp 第81行,共分 8 个子步骤:
-
计算伪距变化率 :
rhorate = (rho1.range - chan->rho0.range) / dt--- 相邻两时刻伪距差 / 时间间隔 -
计算载波多普勒频率 :
chan->f_carr = -rhorate / LAMBDA_L2--- 载波波长 LAMBDA_L2 = c / 1227.60 MHz -
计算码多普勒频率 :
chan->f_code = CODE_FREQ_L2C + chan->f_carr * CARR_TO_CODE_L2--- 载波多普勒折算到码片率 -
计算信号发射时间(GPS时间):
g0sec = time2gpst(chan->rho0.gt, &g0week); ms = (g0sec - chan->rho0.range / CLIGHT) * 1000.0;接收时刻 GPS 秒 - 伪距/光速 = 发射时刻,乘以 1000 转为毫秒。
-
计算 CM 码相位(第一步):
chan->code_phase_cm = fmod(ms, 20.0) / 20.0 * CM_SEQ_LEN;CM码周期 20ms(= 10230 chips),先对 20 取模得到 20ms 内的偏移,再归一化到码片数。
-
计算 CL 码相位:
chan->code_phase_cl = fmod(ms, 1500.0) / 1500.0 * CL_SEQ_LEN;CL码周期 1500ms(= 767250 chips),对 1500 取模得到 1500ms 内的偏移。
-
计算数据位索引:
chan->ibit = ((int)floor(ms / 20.0)) % 600;每个 CNAV 数据位持续 20ms,用发射时刻的毫秒数除以 20 得到当前 bit 索引。600 bits 对应一个完整的 CNAV 消息帧(12 秒 = 600 × 20ms)。
-
取当前数据位:
chan->dataBit = (chan->cnavData[chan->ibit] == 0) ? 1 : -1;从预先生成的 600-bit CNAV 电文中取出当前数据位,0→+1,1→-1(BPSK 调制)。
步骤 C:路径损耗、天线增益、信号增益计算
path_loss = 20200000.0 / rho.d; // 自由空间路径损耗
int ibs = (int)((90.0 - rho.azel[1]*R2D) / 5.0); // 仰角→舷角索引
double ant_gain = ant_pat[ibs]; // 接收机天线增益
gain[i] = (int)(path_loss * ant_gain * 128.0 * relative_power_factor);
第六环节:GPS L2C 扩频码生成
L2C 使用两套独立的扩频码:CM码 (Civil Moderate,中码)和 CL码(Civil Long,长码),二者时分复用。
6.1 CM码生成 codegen_L2CM()
CM 码本质是截断的 Gold 码:
- 寄存器 :27 级 LFSR(Linear Feedback Shift Register),多项式为
L2C_POLY(0x5A7B06,对应1 + x^3 + x^5 + ... + x^27) - 初相 :从
L2CM_PRN_INIT[prn-1](27-bit 表)加载初始状态 - 生成 10230 bit :调用
gen_gold_code(),每步 LFSR 前进一次,输出为 G1⊕G2。CM码在生成 10230 bit 后将 G2 复位(resetPos=10230) - CL比CM长很多(767250 vs 10230 chip),所以 CM 每 20ms 循环一次而 CL 每 1500ms 才循环一次
6.2 CL码生成 codegen_L2CL()
类似方法生成 CL 码:
- 初相 :从
L2CL_PRN_INIT[prn-1]加载 - 生成 767250 bit :一次生成完整序列,无需复位点(
resetPos=0)
6.3 LFSR 核心算法 gen_gold_code()
每个时钟周期:
A = (A << 1) | popcount(A & polyA) // G1寄存器
B = (B << 1) | popcount(B & polyB) // G2寄存器
out[i] = (A的最高位) ^ (B的最高位) // Gold码输出
popcount(汉明重量)用于计算反馈位。
第七环节:CNAV 导航电文生成 generateGPSL2CnavMsg()
这是 L2C 电文组织的核心,每 12 秒调用一次。整个编码流程从 276-bit 原始电文到最终 600-bit 发送符号。
7.1 电文调度
L2C CNAV 的超帧结构:4 条消息 × 12 秒 = 48 秒一个完整周期。消息类型顺序为:
const int WordAllocationGPSL2[4] = { 11, 30, 31, 10 };
每条消息长度 12 秒,编码后对应 600 个 CNAV 符号(每符号 20ms = 1 bit → 600 × 20ms = 12s)。
7.2 原始电文生成 generate_gps_l2_cnav()
每条 CNAV 消息 = 8-bit 前导码 + 268-bit 数据 = 276 bit
前导码(Preamble)
固定为 10001011(8 bit),放在电文最前面,用于接收机帧同步。
消息类型 10 --- 星历 1(轨道参数)
| 字段 | 位宽 | 位置 | 内容 |
|---|---|---|---|
| Preamble | 8 | 1-8 | 10001011 |
| PRN | 6 | 9-14 | 卫星 PRN 号 |
| Msg Type | 6 | 15-20 | 10 |
| TOW count | 17 | 21-37 | 下一条消息的周内秒/6 |
| Alert | 1 | 38 | 告警标志 |
| WN | 13 | 39-51 | GPS 周数 |
| L1 Health | 1 | 52 | L1 健康 |
| L2 Health | 1 | 53 | L2 健康 |
| L5 Health | 1 | 54 | L5 健康 |
| Top | 11 | 55-65 | 星历预测时刻 / 300 |
| URA ED | 5 | 66-70 | 精度指标 |
| toe | 11 | 71-81 | 星历参考时刻 / 300 |
| ΔA | 26 | 82-107 | 长半轴偏差(相对 A_REF=26559710m) |
| Ȧ | 25 | 108-132 | 长半轴变率 |
| Δn₀ | 17 | 133-149 | 平均角速度偏差/π |
| Δṅ₀ | 23 | 150-172 | 平均角速度变率/π |
| M₀ | 33 | 173-205 | 平近点角/π |
| e | 33 | 206-238 | 偏心率 |
| ω | 33 | 239-271 | 近地点角距/π |
| (padding) | 5 | 272-276 | 填0到 276 bit |
消息类型 11 --- 星历 2(轨道参数续)
包含:PRN、类型、TOW、toe、Ω₀(升交点赤经)、i₀(倾角)、Ω̇(升交点赤经变率)、i̇(倾角变率)、Cis、Cic、Crs、Crc、Cus、Cuc 等摄动参数。
消息类型 30 --- 时钟/电离层/群延迟
包含:PRN、类型、TOW、toc(时钟参考时刻)、af₀、af₁、af₂(时钟多项式系数)、TGD、ISC_L1CA、ISC_L2C、ISC_L5I5、ISC_L5Q5(各频点群延迟修正)、以及电离层 Klobuchar 模型的 α₀~α₃ 和 β₀~β₃ 参数。
消息类型 31-37 --- 时钟简化版
仅包含时钟核心参数(af₀、af₁、af₂)和一个 11-bit 的 toc 字段,其余填零。
7.3 数值缩放编码
所有浮点参数经过量化转化为整数比特:
IntValue = UnscaleInt(double_value, scale_factor);
例如 UnscaleInt(ephc->A - A_REF, -9) 将 ΔA 乘以 2^9 后取整为 26-bit 二进制补码。这是按照 IS-GPS-800(CNAV 接口规范)的量化和比例因子进行的。
7.4 FEC 卷积编码
编码参数:
- 约束长度 K = 7
- 码率 R = 1/2
- 生成多项式:
G1 = (171)₈ = 1111001₂,G2 = (133)₈ = 1011011₂
276 bit 输入 → 尾部补 6 个零 bit(使编码器归零)→ 输出 (276+6)×2 = 564 bit。
但实际代码中使用的基于 查表法 的 ConvolutionEncodePair()(第684行)效果更高效:每次输入 2 bit,查 ConvEncodeTable[256] 输出 4 bit。
7.5 CRC-24Q 校验
有两种实现:
Crc24qEncode():标准按 bit 计算,用于 L5 的generateFrame()流程Crc24qEncode1():按 32-bit word 查表,用于 CNAV
CRC-24Q 多项式:0x1864CFB(对应 x^24 + x^23 + x^18 + x^17 + x^14 + x^11 + x^10 + x^7 + x^6 + x^5 + x^4 + x^3 + x + 1)
计算过程 (Crc24qEncode1 第637行):
- 输入:
EncodeData[9](9 个 32-bit word,其中前 12 bit 为零填充,后 276 bit 为 CNAV 原始数据) - 遍历每一字节,
crc = (crc << 8) ^ Crc24q1[(data_byte) ^ (crc >> 16)] - 输出 24-bit CRC,附加到 276-bit 数据后面得到 300 bit
7.6 最终 FEC + CRC 编码流程(在 generateGPSL2CnavMsg() 内)
- 生成 276-bit 原始 CNAV → 存入
EncodeData[9](前面加 12 bit 零填充 = 288 bit) - 计算 CRC-24Q → 得到 24 bit CRC
- 对 276 bit 数据做卷积编码 → 得到 552 个符号
- 对 24 bit CRC 也做卷积编码 → 得到 48 个符号
- 总计:552 + 48 = 600 个编码符号
- 存入
chan->cnavData[600],供后续调制使用
最终数据链 :276 bit 原始电文 → +24 bit CRC → 300 bit → ×2 (FEC R=1/2) → 600 个编码符号
第八环节:L2C IQ 基带信号生成 generateGPSL2IQSamples()
这是生成最终基带 IQ 采样点的环节,在 0.1 秒(一个历元)内,产生 iq_buff_size(= samp_freq / 10,即 0.1 秒内的采样数)对 I/Q 数据。
8.1 采样级循环
对每个采样点 isamp:
8.2 CM 码周期管理
if (chan[i].code_phase_cm >= CM_SEQ_LEN) { // CM_SEQ_LEN = 10230
chan[i].code_phase_cm -= CM_SEQ_LEN;
chan[i].ibit++;
if (chan[i].ibit >= 600) {
chan[i].ibit = 0;
generateGPSL2CnavMsg(gt, &chan[i], chan[i].sat, nav); // 重新生成电文
}
chan[i].dataBit = (chan[i].cnavData[chan[i].ibit] == 0) ? 1 : -1;
}
每 20ms(10230 个 CM 码片 = 1 个 CNAV bit),CM 码周期结束→递增 bit 索引→取新的数据位。每 600 bit(12 秒 = 一个完整的 CNAV 消息帧),重新生成导航电文。
8.3 CL 码周期管理
if (chan[i].code_phase_cl >= CL_SEQ_LEN) // CL_SEQ_LEN = 767250
chan[i].code_phase_cl -= CL_SEQ_LEN;
CL 每 1500ms 循环一次,不携带数据(导频通道)。
8.4 TDM 时分复用 --- half-chip 槽位判决
int half_slot = ((int)(chan[i].code_phase_cm * 2.0)) & 1;
L2C 的独特设计:在每个码片周期内,偶数半片是 CM + 数据 ,奇数半片是 CL 导频。
半个码片(half-chip)时序:
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ CM+D │ │ CL │ │ CM+D │ │ CL │ ...
└──────┘ └──────┘ └──────┘ └──────┘
half=0 half=1 half=0 half=1
8.5 取码片值
int cm_index = (int)chan[i].code_phase_cm;
int cl_index = (int)chan[i].code_phase_cl;
int CM_chip = (chan[i].cm_GPS_L2[cm_index] == 0) ? 1 : -1; // 0→+1, 1→-1
int CL_chip = (chan[i].cl_GPS_L2[cl_index] == 0) ? 1 : -1;
8.6 调制信号生成
if (half_slot == 0) {
sig = CM_chip * chan[i].dataBit; // CM: 扩频码 × 数据(BPSK)
} else {
sig = CL_chip; // CL: 纯导频,不调制数据
}
8.7 载波调制(数字上变频)
int iTable = ((int)chan[i].carr_phase >> 16) & 0x1FF; // 9-bit 表索引
ip = (int)(AMP * sig * cosTable512[iTable] * gain[i]); // I 路 = sig × cos
qp = (int)(AMP * sig * sinTable512[iTable] * gain[i]); // Q 路 = sig × sin
载波相位累加器 carr_phase 是 32-bit 定点数,高 9 bit 用于查 512 点的 sin/cos 查找表。由于 L2C 是 BPSK 调制,I 和 Q 路信号相同(都是 sig×cos 和 sig×sin),即没有 QPSK 的正交复用。
幅度因子 AMP = 1/√2 = 0.7071,增益因子 gain[i] 包含了路径损耗、天线增益、功率比例。
8.8 载波相位和码相位推进
chan[i].carr_phase += chan[i].carr_phasestep; // 载波相位步进
chan[i].code_phase_cm += chan[i].f_code * delt; // CM 码相位推进
chan[i].code_phase_cl += chan[i].f_code * delt; // CL 码相位推进
8.9 多径效应叠加(可选)
if (iopt.Multipath == TRUE)
applyMultipathEffect(multipath[i], ip, qp, iq_buff_size, isamp, i_acc, q_acc, iopt.samp_freq);
多径模型存储了每个信道的延迟(秒)和衰减(幅度比例),对每个多径分量:延迟转采样点数 → 取对应位置的信号样本 → 乘以衰减系数 → 叠加到主信号。
8.10 定点和缩放输出
i_acc = (i_acc + 64) >> 7; // 除以 128 并四舍五入(恢复 gain 中的 ×128)
q_acc = (q_acc + 64) >> 7;
iq_buff[isamp * 2] = (short)i_acc;
iq_buff[isamp * 2 + 1] = (short)q_acc;
第九环节:IQ 数据写入
writeIQData()(WriteIQData.cpp)将生成的 IQ 数据按配置格式写入输出文件:
- SC08 格式:SIGNED CHAR 8-bit,每采样 I/Q 各 1 字节
- SC01 格式:压缩 1-bit 量化,每字节存 4 组 I/Q 对
- 乒乓缓冲:每 60 秒自动切换输出文件,实现无缝记录
第十环节:每 30 秒更新
在主循环中(第643-698行),每 300 个历元(= 30 秒):
- 打印可见卫星信息:PRN、位置、伪距、多普勒频率、钟差
- 重新生成导航电文 :对每个 GPS 信道调用
generateGPSL2CnavMsg(),确保播发的电文始终对应最新时间 - 重新分配信道 :调用
allocateChannel(),因为 30 秒后卫星可见性可能变化(卫星升落)
总结:L2C 核心信号特征
| 特性 | 参数 |
|---|---|
| 载波频率 | 1227.60 MHz(L2 频段) |
| 码片率 | 1.023 Mcps |
| CM 码长度 | 10230 chips(= 20 ms) |
| CL 码长度 | 767250 chips(= 1.5 s) |
| 调制方式 | TDM-BPSK(CM+数据 / CL导频 时分复用) |
| 数据速率 | 50 sps(CNAV 编码后符号率),等效于 25 bps(原始信息速率) |
| 电文帧结构 | 4 条消息 × 12 秒 = 48 秒超帧 |
| 每条消息 | 276 bit 原始 → +24 CRC → 300 bit → FEC R=1/2 → 600 编码符号 |
| FEC | 卷积码,K=7,R=1/2,G1=171₈,G2=133₈ |
| CRC | CRC-24Q(多项式 0x1864CFB) |
| 电文类型 | 10(星历1)、11(星历2)、30(时钟+电离层+群延迟)、31-37(时钟) |
| 星历来源 | CNAV 星历(eph_cnav),含 Adot、ndot、ISC 等新参数 |
与传统的 L1 C/A 码相比,L2C 的主要优势在于:CM/CL 时分复用的设计使接收机可以在 CL 导频上进行纯锁相环跟踪(不受数据跳变影响),提高了弱信号下的跟踪灵敏度;CNAV 电文使用卷积 FEC 编码提升了数据解调的抗干扰能力;CNAV 星历精度更高(包含长半轴变率 Adot 等新参数)。