GPS L2C 软件接收机从捕获、DLL/PLL跟踪到PVT解算定位全流程详解

本文基于研发的GPS L2C 软件接收机进行讲解。在开始之前先理解GPS L2C信号的结构,因为后续每个步骤的设计都与此直接相关。GPS L2频段(1227.6 MHz)上的L2C是现代化民用信号,它的核心设计思想是:用时分复用的方式在一个载波上同时传输数据分量和导频分量

L2C信号由CM码(Civil Moderate,中等长度码)和CL码(Civil Long,长码)交替拼接而成。码速率各为511.5 kcps,合并后等效1.023 Mcps。CM码长度10230码片,周期恰好20ms,每一个CM码片上调制了导航电文数据位(CNAV);CL码长度767250码片(75个CM周期),周期1.5s,上面没有数据调制,是一个纯净的导频信号。两种码都是归零码,码片取值为+1、0、-1,中间回到零点的设计使得波形在频域上主瓣更窄。

为什么要设计成数据+导频的双通道结构?纯数据通道(CM)传输星历,纯导频通道(CL)上没有符号翻转,适合做长相干积分来提升弱信号跟踪灵敏度------这是L2C相对于传统L1 C/A的关键改进之一。

总体处理流水线

postProcessing.m 展示了接收机的六个大步骤:

  1. 中频信号读取 --- 从文件中读入前端采集的IF样本
  2. 信号捕获 --- 搜索可见卫星的多普勒频率和码相位
  3. 通道初始化 --- 按信号强度排序分配给跟踪通道
  4. 信号跟踪 --- DLL+PLL闭环持续更新码相位和载波频率
  5. 导航电文解码 --- CNAV译码(Viterbi译码 + CRC校验)
  6. PVT解算 --- 伪距计算 + 最小二乘定位

接下来逐步详细讲解。

第一步:中频数据读取(init.m + postProcessing.m 开头部分)

目的:从射频前端采集的数字IF信号文件中读取原始样本,为后续处理准备数据流。

initSettings.m 中默认配置了 GPSL2.bin 文件,数据类型为 schar(8位有符号整数),文件格式为 type=2(I/Q交替采样)。IF频率设为0(零中频架构,即前端已直接下变频到基带),采样率4 MHz。

代码中的关键逻辑:

复制代码
fid = fopen(settings.fileName, 'rb');
fseek(fid, dataAdaptCoeff * settings.skipNumberOfBytes, 'bof');

dataAdaptCoeff 在I/Q交替模式下为2(每个复样本占2字节),skipNumberOfBytes 允许从记录的任意位置开始处理。文件指针随后在不同的处理阶段分别用于读取捕获数据段和跟踪数据流。

IF频率设为0且采样率4 MHz是零中频直接采样的典型配置------这简化了数字下变频,因为信号在采样时已经在基带附近(多普勒频移在±5 kHz以内)。

第二步:信号捕获(acquisition.m

目的:从接收到的宽带IF信号中检测哪些卫星PRN可见,并粗略估计每个可见信号的码相位延迟和载波多普勒频移,为后续跟踪环路提供初始值。

这是软件接收机最关键的第一步------没有捕获结果,后续所有处理都无从谈起。

2.1 捕获的基本原理

GPS信号的测距码(PRN码)具有良好的自相关特性:仅当本地生成的码与接收信号的码精确对齐时,相关器输出才会出现尖锐峰值。而卫星和接收机之间的相对运动会引入多普勒频移,对于地面静止接收机,L2频段的多普勒范围约为±5 kHz。

因此,"捕获"本质上是二维搜索问题:在多普勒频率和码相位的二维平面上寻找峰值。数学上这是一个最大似然检测问题------在所有可能的 (f_d, τ) 组合中,看哪个产生的相关功率最大。

代码实现了两次搜索:

粗捕获 :在频率维度以 acqStep = 1000/acqCohT/2 = 25 Hz(默认20ms相干积分时)为步长,覆盖±5 kHz的搜索带;在码相位维度用FFT做循环相关,一次计算覆盖全部10230个码相位。

精捕获:在粗捕获找到的最佳频率附近,以20 Hz步长做更高分辨率搜索。

2.2 粗捕获------基于FFT的并行码相位搜索

这是捕获代码的核心。传统串行搜索需要在每个码相位上逐一计算相关值,极其耗时。而FFT并行码相位搜索利用了时域循环相关等价于频域乘积的性质:

Rm=IFFT{FFT{x}⋅FFT{c}∗}

其中 x 是接收信号,c是本地码。一次FFT-IFFT对直接给出所有码相位的相关值。

代码实现:

复制代码
cmCodeFreqDom = conj(fft(localCode));          % 本地码的频域共轭(只算一次)
%
for frqBinIndex = 1:numberOfFrqBins
    sigCarr = exp(1i * frqBins(frqBinIndex) * phasePoints);  % 本地载波
    IQfreqDom1 = fft(sigCarr .* sig20PlusXms);                % 剥载波+FFT
    convCodeIQ1 = IQfreqDom1 .* cmCodeFreqDom;               % 频域相乘
    results(frqBinIndex, :) = abs(ifft(convCodeIQ1));        % IFFT取模
end

几个设计细节值得注意:

相干积分时间:默认设置为20 ms------恰好是一个完整的CM码周期。这利用了CM码周期性的特点,保证在20 ms内有一个完整的码周期可供相关。同时20 ms也是CNAV符号速率(50 sps)对应的一个符号宽度,避免了符号翻转导致的功率损失。

零填充相关 :使用了20+X ms的输入信号(sig20PlusXms),但本地码只有X ms长,其余补零。这样做的目的是捕获到跨越码周期边界的信号------额外20 ms确保至少包含一个完整的CM码周期。

检测判决:用GLRT(广义似然比检验)统计量:

复制代码
acqResults.peakMetric(PRN) = peakSize / sigPower;

即相关峰值功率与输入信号功率之比。当该比值超过阈值(默认6.0),判定信号存在。使用功率归一化使得阈值设置在不同信号强度下有较好的通用性。

2.3 精频率搜索

粗捕获的频率分辨率受相关积分时间限制(25 Hz,即1/2T的倒数)。为给PLL提供更精确的初始频率估计,做了二次精搜索:

复制代码
signa20DC = longSignal(codePhase:(codePhase + samplesPerCode-1));  % 按已找到的码相位截取
xCarrier = signa20DC .* cmCodesTable;   % 剥去CM码,留下纯载波
%
for FineBinIndex = 1 : NumOfFineBins
    sigCarr20cm = exp(1i * FineFrqBins(FineBinIndex) * finePhasePoints);
    FineResult(FineBinIndex) = abs(sum(xCarrier .* sigCarr20cm));  % 非相干累积
end

这里先剥去CM码调制(因为已经知道码相位了),然后在20 ms的载波上以20 Hz步长搜索,20 Hz正好是20ms积分时间的频率分辨率极限,直接找到使DFT幅度最大的那个精细频率点。

2.4 CL码相位搜索

由于跟踪支持利用导频通道(pilotTRKflag = 1),捕获阶段还需要确定CL码相对于CM码的相位偏移(即当前CM码周期对应CL码75个周期中的第几个)。

复制代码
for ind = 1:75
    CLCodeSample = CLCode(codeValueIndex + settings.codeLength * 2 * (ind-1));
    powerArray(1,ind) = abs(sum(signa20DC .* CLCodeSample .* sigCarr));
end
[~, acqResults.CLCodePhase(PRN)] = max(powerArray);

这里遍历CL码的75种可能的相位对齐,使用已经捕获到的码相位截取的20ms信号和已经捕获到的载波频率,看哪种CL码相位产生的相关功率最大。

2.5 可选优化------降采样

当原始采样率很高(>6 MHz)时,支持降采样加速捕获。核心思想是根据带通采样定理找到一个更低的等效采样率。降采样后IF频率也会变化(取模运算),从而减少每次FFT的计算量。

第三步:通道初始化(preRun.m

目的:将捕获到的卫星信号按检测峰值强度排序后,分配给有限数量的跟踪通道。

复制代码
[~, PRNindexes] = sort(acqResults.peakMetric, 2, 'descend');
for ii = 1:min([settings.numberOfChannels, sum(acqResults.carrFreq ~= 0)])
    channel(ii).PRN          = PRNindexes(ii);
    channel(ii).acquiredFreq = acqResults.carrFreq(PRNindexes(ii));
    channel(ii).codePhase    = acqResults.codePhase(PRNindexes(ii));
    channel(ii).CLCodePhase  = acqResults.CLCodePhase(PRNindexes(ii));
    channel(ii).status       = 'T';
end

按信号强度排序的原因是:更强的信号对应的捕获参数更可靠,优先分配硬件通道资源。默认通道数是12个,多于大多数时间可见的GPS卫星数,一般情况下所有捕获到的卫星都能被分配。

第四步:信号跟踪(tracking.m

目的:对每个通道持续闭环跟踪码相位延迟和载波频率/相位的变化,输出剥离了扩频码和载波后的基带I/Q相关值,为后续导航电文解码和伪距测量提供数据。

跟踪是整个接收机中持续运行最久、计算量最大的模块。

4.1 GPS L2C跟踪的特殊设计------数据和导频联合跟踪

L2C的CM和CL码在时间上交替传输:

复制代码
时间 →  CM码片 | CL码片 | CM码片 | CL码片 | ...

因此并不存在独立的"CM信号"和"CL信号"------它们是交替在同一个载波上的。但码同步之后,可以分别用CM码(有数据调制)和CL码(无数据调制)与接收信号做相关,产生数据分量和导频分量的I/Q积分值。对每个跟踪间隔(20ms)计算了六个相关值:CM的Early/Prompt/Late I和Q,以及CL的Early/Prompt/Late I和Q。

4.2 码环(DLL------Delay Lock Loop)

目的:使本地生成的扩频码与接收信号的码相位精确对齐。码相位误差直接影响伪距测量精度,因此DLL是决定定位精度的关键环节。

DLL使用超前-滞后延迟锁定环(Early-Late DLL),相关器间距为0.25码片:

复制代码
% 超前、即时、滞后三路相关
I_E = sum(earlyCode  .* iBasebandSignal);   % 超前
I_P = sum(promptCode .* iBasebandSignal);   % 即时
I_L = sum(lateCode   .* iBasebandSignal);   % 滞后

鉴相器使用的是归一化非相干超前减滞后

复制代码
codeError = (sqrt(I_E^2 + Q_E^2) - sqrt(I_L^2 + Q_L^2)) / ...
            (sqrt(I_E^2 + Q_E^2) + sqrt(I_L^2 + Q_L^2));

为什么要用非相干型(包含Q分量)而非相干型(只用I分量)?因为在这种E-L鉴相器中,载波环可能尚未锁定、载波相位误差会使得I分量的增益变化,但信号的包络功率 \\sqrt{I\^2+Q\^2} 与载波相位无关,所以非相干型对载波环的锁相状态不敏感,DLL可以在载波环锁定之前就开始工作。

归一化除法将鉴相器输出归一化到 ±0.5 码片范围,使得环路增益不随信号幅度变化,便于环路滤波器的设计。

对于数据和导频联合跟踪,做了简单平均:

复制代码
codeErrorCL = (sqrt(I_ECL^2+Q_ECL^2) - sqrt(I_LCL^2+Q_LCL^2)) / ...
              (sqrt(I_ECL^2+Q_ECL^2) + sqrt(I_LCL^2+Q_LCL^2));
codeError = (codeError + codeErrorCL) / 2;

这等效于在鉴相器层面实现了数据/导频联合,将两个分量各自独立的鉴相结果平均以降低噪声。

环路滤波器使用二阶数字环路滤波器

复制代码
codeNco = oldCodeNco + (tau2code/tau1code) * (codeError - oldCodeError) + ...
          codeError * (PDIcode/tau1code);

其中 tau1codetau2code 由设定的噪声带宽(2 Hz)和阻尼比(0.7)计算得出。二阶环路可以跟踪频率斜坡(即恒定的相对加速度),对于地面低动态场景足够了。2 Hz的窄带宽意味着DLL对噪声有很强的抑制能力,代价是动态响应较慢。

4.3 载波环(PLL------Phase Lock Loop)

目的:使本地载波NCO的相位与接收信号载波相位锁定,从而将信号完全下变频到基带。载波环锁定后,I支路能量最大(信号全在I上)、Q支路最小(只有噪声),这是后续比特判决的基础。

PLL使用Costas锁相环(对180°相位翻转不敏感型),鉴相器是反正切型:

复制代码
carrError = atan(Q_P / I_P) / (2.0 * pi);         % CM数据通道
carrErrorCL = atan(Q_PCL / I_PCL) / (2.0 * pi);    % CL导频通道
carrError = (carrError + carrErrorCL) / 2;          % 联合

选择反正切型的原因是它在线性范围内(通常±π/2)具有完美的线性特性------鉴相输出与输入的相位差严格成正比,且与信号幅度无关。对180°相位翻转不敏感是因为atan函数的天性:当数据比特翻转(I和Q同时变号),atan(Q/I)的结果不变。

数据和导频联合的巧妙之处:CM通道的数据符号翻转在鉴相器中已经被atan函数自然免疫(所以CM通道也可以直接用Costas环),但CL通道的信号没有数据翻转,它的鉴相结果更"干净"------没有180°的相位模糊残留噪声。两者平均后可以获得更平滑的相位误差估计。

载波环使用三阶环路滤波器

复制代码
d2CarrError = d2CarrError + carrError * pf3;
dCarrError  = d2CarrError + carrError * pf2 + dCarrError;
carrNco     = dCarrError + carrError * pf1;

三阶环可以跟踪频率加速度(加加速度),为什么用三阶?因为载波相位的变化比码相位更敏感------L2频率为1227.6 MHz,1 mm的位置变化就对应约4°的载波相位变化。三阶环提供了更好的动态跟踪能力,确保在地面接收机可能经历的加速度(如车辆颠簸、行人晃动)下不丢失锁相。18 Hz的PLL带宽(默认设置)远大于DLL的2 Hz,这是必要的------载波环需要更快响应来维持相位锁定。

4.4 CL码相位的递进

由于CL码周期是CM的75倍,跟踪每20 ms将CL码相位递进1:

复制代码
CLCodePhase = CLCodePhase + 1;
if (CLCodePhase >= 76)
    CLCodePhase = 1;
end

这利用了CL和CM码的确定关系------每过20 ms(一个CM周期),CL码恰好前进了自己的一个周期(在75个周期中到达下一个),因此在正确跟踪时CL码相位应该是线性递增的。

4.5 跟踪输出

每一次跟踪更新(20 ms一次),代码记录:

  • I_P, Q_P(即时支路I/Q值------导航电文比特的解调源)
  • 码频率、载波频率的估计值
  • 码相位和载波相位的残余值
  • 鉴相器和环路滤波器输出
  • C/N0(载噪比)估计

这些数据在后续的导航电文解码和伪距测量中全部用到。

第五步:导航电文解码(CNAVdecoding.m

目的:从跟踪环输出的I_P值中提取CNAV导航电文,获得卫星星历参数、时钟校正参数和电离层参数等。

L2C使用CNAV(Civil Navigation)电文格式,相比传统NAV电文(L1 C/A上使用的)有显著改进:帧结构更灵活(12秒300位的消息帧,而非6秒的子帧),采用1/2码率的卷积编码和CRC-24Q检错。

5.1 比特判决

跟踪输出I_P是模拟量(20 ms积分值),需要转换为0/1比特序列:

复制代码
dataBits = (I_P_InputBits < 0);   % I<0判为0, I>0判为1

转换为二进制后,比特速率是50 bps(20 ms一位)。

5.2 Viterbi译码

CNAV使用了(2,1,7)卷积编码(约束长度7,生成多项式171和133(八进制)),码率为1/2,意味着每个信息位产生2个编码位。因此经过卷积译码后,信息速率变为25 bps。

使用MATLAB的通信工具箱:

复制代码
trellis = poly2trellis(7, [171 133]);
for G1orG2 = 1:2
    decodedBits = vitdec(dataBits(G1orG2:end-(G1orG2-1)), ...
                          trellis, tblen, 'trunc', 'hard');

对两个可能的起始位置(G1和G2)分别尝试译码,因为接收比特流的起始位可能与编码器的G1或G2输出对齐(这两个输出是交替的),不确定起始位置,就要两种都试。

5.3 同步字检测

译码后的比特流中搜索8位同步字(10001011):

复制代码
preamble_bits = [-1 1 1 1 -1 1 -1 -1];
tlmXcorrResult = xcorr(antipodalBits, preamble_bits);

使用互相关而非直接比特比较,因为互相关对少量的位错误具有一定的容忍度。找到同步字后还可以用它来校正比特极性(如果同步字反相,说明整帧比特极性反转)。

5.4 CRC-24Q检错

每个300位的消息帧末尾有24位CRC校验码。CRC校验用于确定译码结果的可靠性:

复制代码
crcDet = comm.CRCDetector([24 23 18 17 14 11 10 7 6 5 4 3 1 0]);
[~, frmError] = step(crcDet, navBits(1:300)');

只有CRC校验通过的消息才被送去解析星历参数。

5.5 星历参数解析(ephemeris.m

CNAV电文类型10和11联合提供轨道参数(开普勒要素),类型30提供时钟和电离层参数。代码需要至少三个消息类型:Type 10(轨道参数第一部分)、Type 11(轨道参数第二部分)、以及Type 30-37中的某一个(时钟校正参数)。这就是为什么 postNavigation.m 中需要至少48000 ms的数据------消息帧每12秒一个,至少3个需要36秒,加上帧边界对齐所需容差12秒,总共48秒。

解析的参数包括:半长轴偏差 ΔA、偏心率 e、轨道倾角 i₀、升交点赤经 Ω₀、近地点角距 ω、平近点角 M₀,以及各自的变化率;时钟参数 a_f0、a_f1、a_f2 等。这些参数直接用于后续的卫星位置计算。

第六步:PVT解算(postNavigation.m

目的:计算接收机的位置(经纬度和高度)、速度和时钟偏差。这是整个接收机最终的目标输出。

6.1 伪距测量
复制代码
[navSolutions.rawP(:, currMeasNr), transmitTime, localTime] = ...
    calculatePseudoranges(trackResults, subFrameStart, TOW, ...
    currMeasSample, localTime, activeChnList, settings);

基本原理:伪距 = (本地接收时间 - 卫星发射时间) × 光速。卫星发射时间可以从已解码的CNAV消息中的TOW(Time of Week)和码相位计数推算;本地接收时间由采样时钟和初始偏移(startOffset = 68.802 ms)估算。由于接收机时钟与GPS系统时间存在偏差,这里的"伪距"与真实几何距离相差一个共同的时钟偏差项 c · δt。

6.2 卫星位置计算
复制代码
[satPositions, satClkCorr] = satpos(transmitTime(activeChnList), ...
    [trackResults(activeChnList).PRN], eph);

利用星历参数,根据开普勒轨道方程,代入信号发射时刻,可以计算出每颗卫星在ECEF坐标系中的精确位置和时钟修正量。时钟修正量包括相对论效应修正------因为卫星在高速运动且处于不同的引力势中,卫星钟走的速率与地面钟不同。

6.3 最小二乘定位

这是GNSS定位的核心方程。对于第 j 颗卫星:

ρj​=(xj​−x)2+(yj​−y)2+(zj​−z)2​+c⋅δt+ϵj​

其中 ρj​ 是经卫星钟差修正后的伪距,(xj​,yj​,zj​) 已知,(x,y,z,δt) 是四个未知数。至少需要4颗卫星才能联立求解。

使用迭代的最小二乘法:

复制代码
[xyzdt, el, az, DOP] = leastSquarePos(satPositions, clkCorrRawP, settings);

每一步迭代计算雅克比矩阵(各卫星的方向余弦),利用测量残差更新位置和钟差估计。迭代直到位置更新量小于阈值或达到最大迭代次数。

6.4 坐标转换

ECEF结果转换为经纬度(WGS84大地坐标),再转换为UTM坐标,最后转换为ENU(东北天)坐标。ENU坐标对用户最直观------在水平面上相对于参考点的东向、北向和垂直偏差。

6.5 DOP计算
复制代码
navSolutions.DOP(:, currMeasNr) = ...

DOP(精度因子)值反映了当前卫星几何分布对定位精度的放大效应。GDOP(几何精度因子)越小越好------当卫星在天空均匀分布时DOP最小;当卫星扎堆在头顶一小片天区时,垂直方向的DOP会很差。

整体框图总结

复制代码
IF数据文件 (*.bin)
    │
    ▼
┌──────────────────────┐
│  1. IF信号读取       │  fread() 按字节读取8位I/Q样本
│  postProcessing.m    │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  2. 信号捕获          │  FFT并行码相位搜索(粗捕获 + 精频率 + CL同步)
│  acquisition.m       │  输出:PRN号、多普勒频率、码相位
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  3. 通道初始化        │  按信号强度排序,分配12个通道
│  preRun.m            │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  4. 信号跟踪          │  DLL(二阶, 2Hz) + PLL(三阶, 18Hz)
│  tracking.m          │  CM数据 + CL导频联合跟踪
│                      │  输出:20ms I_P/Q_P 相关值
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  5. 导航电文解码      │  Viterbi译码(1/2卷积码) → 同步字检测 → CRC-24Q → 星历解析
│  CNAVdecoding.m +    │  输出:轨道参数、钟差参数、电离层参数
│  ephemeris.m         │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│  6. PVT解算           │  伪距计算 → 卫星位置 → 最小二乘迭代 → 坐标转换
│  postNavigation.m    │  输出:ECEF经纬度/高度, UTM, ENU, DOP
└──────────────────────┘