GNSS信号仿真全流程详解(以GPS L2C为例)

该项目是一个 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),对每颗星:

  1. 系统筛选 :调用 satsys(sat, &prn) 判断是否为 GPS 卫星
  2. 粗算卫星位置 :调用 ephpos(gt, gt, sat, nav, -1, rs, dts, &var, &svh, &opt) 快速计算卫星在 ECEF 坐标系的位置 rs[3] 和钟差 dts
  3. 仰角判断 :调用 geodist() 计算几何距离,ecef2pos() 转接收机位置为经纬度,satazel() 计算卫星仰角方位角。仰角大于 elmask(通常 5-15°)即为可见卫星
  4. 信道分配 :为可见卫星分配一个空闲信道,初始化:
    • sat, sys, prn, azel 等基本信息
    • 调用 codegen_L2CL()codegen_L2CM() 生成 L2C 的 CL 和 CM 扩频码
    • 调用 generateGPSL2CnavMsg() 生成 L2C CNAV 导航电文
    • 调用 computeRange() 计算初始伪距
    • 存储卫星信息到全局数组 globalSatInfo[]
  5. 载波相位初始化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 个子步骤:

  1. 选择星历 :调用 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 星历计算卫星位置和钟差
  2. 获取初步卫星位置rs[0:2] 存入 rspos[0:2]rs[3:5](速度)存入 vel[0:2]

  3. 计算几何距离range = geodist(rs, xyz, los, e) --- 接收机到卫星的直线距离

  4. 计算信号传播时间tau = range / CLIGHT --- 光传播时间

  5. 反向推算发射时刻的卫星位置 :用速度外推卫星位置回到发射时刻 rs[i] -= vel[i] * tau

  6. 地球自转改正

    复制代码
    xrot = rs[0] + rs[1] * OMGE * tau
    yrot = rs[1] - rs[0] * OMGE * tau

    在信号传播期间地球自转了 OMGE * tau 弧度,需对卫星 ECEF 坐标做相应旋转。

  7. 重新计算几何距离 :用修正后的卫星位置重算 range = geodist(rs, xyz, los, e)

  8. 保存几何距离rho->d = range

  9. 计算伪距rho->range = range - CLIGHT * dts[0] --- 减去卫星钟差折算的距离

  10. 计算伪距率rho->rate = dot(vel, los, 3) / range --- 卫星-接收机相对速度在视线方向的投影

  11. 计算仰角方位角satazel(repos, e, rho->azel)

  12. 电层/对流层延迟(可选):

    • 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 个子步骤:

  1. 计算伪距变化率rhorate = (rho1.range - chan->rho0.range) / dt --- 相邻两时刻伪距差 / 时间间隔

  2. 计算载波多普勒频率chan->f_carr = -rhorate / LAMBDA_L2 --- 载波波长 LAMBDA_L2 = c / 1227.60 MHz

  3. 计算码多普勒频率chan->f_code = CODE_FREQ_L2C + chan->f_carr * CARR_TO_CODE_L2 --- 载波多普勒折算到码片率

  4. 计算信号发射时间(GPS时间)

    复制代码
    g0sec = time2gpst(chan->rho0.gt, &g0week);
    ms = (g0sec - chan->rho0.range / CLIGHT) * 1000.0;

    接收时刻 GPS 秒 - 伪距/光速 = 发射时刻,乘以 1000 转为毫秒。

  5. 计算 CM 码相位(第一步):

    复制代码
    chan->code_phase_cm = fmod(ms, 20.0) / 20.0 * CM_SEQ_LEN;

    CM码周期 20ms(= 10230 chips),先对 20 取模得到 20ms 内的偏移,再归一化到码片数。

  6. 计算 CL 码相位

    复制代码
    chan->code_phase_cl = fmod(ms, 1500.0) / 1500.0 * CL_SEQ_LEN;

    CL码周期 1500ms(= 767250 chips),对 1500 取模得到 1500ms 内的偏移。

  7. 计算数据位索引

    复制代码
    chan->ibit = ((int)floor(ms / 20.0)) % 600;

    每个 CNAV 数据位持续 20ms,用发射时刻的毫秒数除以 20 得到当前 bit 索引。600 bits 对应一个完整的 CNAV 消息帧(12 秒 = 600 × 20ms)。

  8. 取当前数据位

    复制代码
    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 码:

  1. 寄存器 :27 级 LFSR(Linear Feedback Shift Register),多项式为 L2C_POLY0x5A7B06,对应 1 + x^3 + x^5 + ... + x^27
  2. 初相 :从 L2CM_PRN_INIT[prn-1](27-bit 表)加载初始状态
  3. 生成 10230 bit :调用 gen_gold_code(),每步 LFSR 前进一次,输出为 G1⊕G2。CM码在生成 10230 bit 后将 G2 复位(resetPos=10230
  4. CL比CM长很多(767250 vs 10230 chip),所以 CM 每 20ms 循环一次而 CL 每 1500ms 才循环一次

6.2 CL码生成 codegen_L2CL()

类似方法生成 CL 码:

  1. 初相 :从 L2CL_PRN_INIT[prn-1] 加载
  2. 生成 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 校验

有两种实现

  1. Crc24qEncode():标准按 bit 计算,用于 L5 的 generateFrame() 流程
  2. 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() 内)

  1. 生成 276-bit 原始 CNAV → 存入 EncodeData[9](前面加 12 bit 零填充 = 288 bit)
  2. 计算 CRC-24Q → 得到 24 bit CRC
  3. 对 276 bit 数据做卷积编码 → 得到 552 个符号
  4. 对 24 bit CRC 也做卷积编码 → 得到 48 个符号
  5. 总计:552 + 48 = 600 个编码符号
  6. 存入 chan->cnavData[600],供后续调制使用

最终数据链276 bit 原始电文+24 bit CRC300 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 秒):

  1. 打印可见卫星信息:PRN、位置、伪距、多普勒频率、钟差
  2. 重新生成导航电文 :对每个 GPS 信道调用 generateGPSL2CnavMsg(),确保播发的电文始终对应最新时间
  3. 重新分配信道 :调用 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 等新参数)。