简介:这套 MATLAB 代码实现北斗三号 B1C 与 B2a 双频软件接收机,面向做 GNSS 基带信号处理、SDR 课程设计或科研验证的高校学生与工程师。它覆盖信号捕获与跟踪(数据支路加导频支路)、导航电文解码、伪距测量生成与位置解算,并支持读取 RF 二进制中频文件做后处理,各模块独立便于逐段调试与改写。压缩包共 87 个文件、约 3.72MB,以 82 个 .m 脚本为主体,另有 md、txt 说明与一份 PowerPoint 文档。目录按信号分为 BDS-3_B1C、BDS-3_B2a 与 Common 三块,init.m、initSettings.m 负责参数配置与启动,postProcessing.m 是顶层处理入口,include 存放导航解码与 PVT 计算函数,IF_Data_Set 附带中频数据及元数据,Doc 中记录各 SDR 采集说明与共性功能。已有 1936 人学习,适合按目录顺序读代码,再换入自采数据验证排错。
1. 从一颗 1575.42 MHz 的卫星说起:这套接收机要解决什么
做北斗三号 B1C/B2a 软件接收机的毕设或课设,卡住人的往往不是公式,而是从射频采样到 PVT 解算这条链路上每一级的数据格式对不上:捕获给出的码相位是采样点序号还是秒,跟踪输出的载波相位要不要折算成半周,电文译码出来的比特属于哪个子帧。这套 BDS-3 B1C/B2a SDR 接收机用 MATLAB 把捕获、跟踪、电文译码、定位解算串成一条能跑通的流水线,整套代码按系统开源的方式组织,函数边界清晰、每一段都能单独拿出来喂数据测试。它适合两类人:要在几个月内交出完整仿真链路和定位结果的学生,以及想把其中某一段算法换掉做对比实验的开发者。
2. 信号体制与总体架构:B1C/B2a 双频的四个处理层级
先把体制差异摆清楚,再谈架构。B1C 和 B2a 不是“换个频点”那么简单,两者在码率、主码周期、调制方式和二次码长度上都不同,这直接决定了捕获搜索空间、相关器采样率和跟踪环路带宽的取值,也决定了哪些模块可以复用、哪些必须分两套写。
2.1 B1C 与 B2a 的体制差异决定了哪些模块必须分开写
按公开接口文件(ICD)取值,两条信号的关键参数如下表。实际项目里建议以手上那份 ICD 版本为准,因为二次码和电文结构在不同版本里有过调整。
| 参数 | B1C | B2a |
|---|---|---|
| 中心频率 | 1575.42 MHz | 1176.45 MHz |
| 码率 | 1.023 Mcps | 10.23 Mcps |
| 主码长 | 10230 码片 | 10230 码片 |
| 主码周期 | 10 ms | 1 ms |
| 调制方式 | 数据分量 BOC(1,1)、导频分量 BOC(6,1) | BPSK(10) |
| 导频二次码 | 1800 码片,周期 18 s | 100 码片,周期 500 ms |
| 数据速率 | 100 sps | 200 sps |
| 工程推荐采样率 | ≥ 24.576 MHz | ≥ 20.46 MHz |
这张表里真正影响代码结构的是三行。第一行是主码周期,B1C 一个码周期 10 ms,做 1 ms 相干积分只能覆盖十分之一个码周期,捕获时必须按 10 ms 整周期相关,否则相关峰被截断;B2a 一个码周期就是 1 ms,积分时长和码周期天然对齐,代码里可以直接按整数周期切块。第二行是调制方式,BOC(6,1) 的子载波频率约 6.138 MHz,自相关主峰极窄,采样率低于 24 MHz 时主峰只剩两三个采样点,早迟鉴相曲线的斜率随采样相位剧烈抖动,跟踪环路会周期性抖一下。第三行是二次码长度,18 s 和 500 ms 差了两个数量级,位同步和二次码剥离的策略不能共用一套。
所以工程上我一般把代码切成两条信号分支:码发生器、相关器重采样、二次码表各一套,环路滤波器和后面的电文解析、PVT 解算共用。共用的部分做成通用函数,分支的部分用sigType字符串区分,避免复制粘贴出两份互相不同步的代码。
2.2 总体数据流:从 IF 采样到 PVT 的六段流水线
整条链路可以拆成六段,每段的输入输出都是明确的数组或结构体,这样任何一段出问题都能单独定位:
第一段是数据源。支持两种输入,一种是提前录好的复基带采样文件(int16或float32交错存放 I/Q),另一种是实时采集卡的流式接口。做毕设建议先用文件回放,可重复性是后面所有调试的前提。
第二段是通道管理。一个通道结构体保存该通道的状态:卫星号、多普勒初值、码相位初值、环路状态、累计积分次数、电文缓冲区。多通道用 cell 数组管理,每轮循环遍历所有通道各推进一个积分周期。
第三段是捕获。对每个候选卫星在给定的多普勒范围内扫描,输出码相位和多普勒的粗估计,以及一个峰值比指标用于判决是否捕获成功。
第四段是牵引与跟踪。捕获给出的估计精度不够直接进 PLL,通常先用叉积鉴频器把频率误差拉到几十赫兹以内,再切到 PLL 和 DLL 闭环。这一段是整个接收机最考验参数的地方。
第五段是位同步、帧同步与电文译码。位同步找到比特边界,帧同步找到子帧头,然后解交织、LDPC 译码、BCH 校验,最终得到星历和钟差参数。
第六段是观测值与 PVT。把码相位、载波相位、多普勒和载噪比折算成伪距和观测向量,做最小二乘或卡尔曼滤波解算,输出位置、速度和钟差。
通道的状态机是这套架构里最容易被忽略的设计点。一个通道不只有“跟踪”一个状态,而是“空闲 → 捕获 → 牵引 → 锁定 → 失锁重捕”五个状态,每个状态有自己的进入条件和退出条件。比如锁定状态下如果连续 N 个积分周期的载噪比估计低于阈值,或者鉴相器输出连续超过门限,就退回捕获状态并扩大搜索范围。没有这个状态机,一段数据里卫星升起落下的时候,通道会一直挂在一个错误的码相位上输出垃圾观测值,后面 PVT 怎么调都发散。
2.3 目录划分与配置集中管理
目录按处理层级划分,一个目录对应一段流水线,跨目录的调用只允许从上往下,不允许反向依赖:
| 目录 | 职责 | 对外接口 |
|---|---|---|
cfg/ | 全部可调参数集中在此 | cfg_default.m返回一个结构体 |
ifdata/ | 采样文件读取、下变频、重采样 | read_if(),to_baseband() |
acq/ | 捕获算法与判决 | acq_parallel(),acq_pmf_fft() |
trk/ | 环路滤波、鉴相器、NCO | trk_epoch(),loop_filter() |
nav/ | 位同步、帧同步、译码、电文解析 | bit_sync(),frame_sync(),decode_lnav() |
pvt/ | 伪距构造、最小二乘、卡尔曼 | ls_pvt(),kf_pvt() |
demo/ | 一键复现脚本 | run_demo.m |
参数全部收在一个结构体里,函数不读全局变量。这样做的直接好处是做对比实验时只改一行配置就能跑两组结果,不用去翻十几个文件找那个写死的采样率:
% cfg/cfg_default.m —— 所有可调参数集中在这里,函数内部不出现魔数 function cfg = cfg_default() cfg.sys.fs = 32e6; % 采样率:B1C 需覆盖 BOC(6,1) 子载波主瓣 cfg.sys.fIF = 0; % 零中频复基带;实数中频采样时填标称中频 cfg.sys.nChn = 12; % 并行通道数,按实测 CPU 负载调整 cfg.b1c.fc = 1575.42e6; % 中心频率 cfg.b1c.codeRate = 1.023e6; % 码率 cfg.b1c.nChip = 10230; % 主码长,一个码周期 = 10 ms cfg.b1c.nSec = 1800; % 导频二次码长度,周期 18 s cfg.b2a.fc = 1176.45e6; cfg.b2a.codeRate = 10.23e6; cfg.b2a.nChip = 10230; % 一个码周期 = 1 ms cfg.b2a.nSec = 100; % 导频二次码长度,周期 500 ms cfg.acq.dopplerSpan = 10000; % 多普勒搜索半宽 Hz,静态场景 ±10 kHz 够用 cfg.acq.dopplerStep = 250; % 频率步进,应 <= 1/(2*Tcoh) 控制扇贝损失 cfg.acq.Tcoh = 2e-3; % 捕获相干积分时长 cfg.acq.threshold = 2.5; % 峰值比门限,虚警与漏警的折中点 cfg.trk.Tcoh = 1e-3; % 跟踪积分周期 cfg.trk.Bn_pll = 15; % PLL 噪声带宽 Hz cfg.trk.Bn_dll = 2; % DLL 噪声带宽 Hz cfg.trk.spacing = 0.5; % 早迟相关器间距,单位码片 cfg.trk.lossCno = 25; % 载噪比低于该值判失锁,单位 dB-Hz enddopplerStep和Tcoh是一对耦合参数:相干积分时长 2 ms 时,频率步进超过 250 Hz 就会出现明显的扇贝损失,峰值被削掉一截,捕获概率下降。很多同学把步进设成 500 Hz 图快,结果弱信号怎么都捕不到,回头查了半天算法,其实是这个参数的问题。spacing在 B1C 通道上要比 B2a 小,因为 BOC 副峰间隔窄,间距取太大容易锁到副峰上去,后面第 5 章还会专门讲这个坑。
3. 捕获与跟踪:从粗捕获到载波环/码环的闭环实现
捕获解决“信号在哪个码相位和哪个多普勒上”,跟踪解决“怎么持续跟住并且越跟越准”。这两段是整套代码里最需要反复调的地方,也是答辩时老师最爱问的地方。
3.1 捕获:并行码相位搜索与 PMF-FFT 的取舍
捕获算法有三类常见选择。时域串行搜索实现最简单,但 B2a 码率 10.23 Mcps、采样率 20 MHz 以上,一个码周期的采样点就有两万多个,逐个码相位试一遍再乘上多普勒维,单颗星就要几秒,12 颗星根本跑不动。并行码相位搜索用一次 FFT 把全部码相位的相关值一起算出来,计算量降一个数量级,是最常用的方案。PMF-FFT 把相干积分块切成若干段分别做部分匹配滤波再做 FFT,抗多普勒能力更强,适合积分时间长、动态大的场景,代价是段数变多以后相关峰有分裂。
对静态或低速场景,我一般先用并行码相位搜索打通链路,跑通了再换 PMF-FFT 做灵敏度对比。核心函数如下:
function acq = acq_parallel(sig, code, fs, codeRate, fD) % sig : 单频点复基带采样,长度至少覆盖一个完整码周期 % code : 一个完整主码周期的码片序列,长度 nChip % fD : 当前搜索的多普勒频点(Hz) nChip = numel(code); Ns = round(fs * nChip / codeRate); % 一个码周期对应的采样点数 t = (0:Ns-1).' / fs; x = sig(1:Ns) .* exp(-1j*2*pi*fD*t); % 剥离多普勒,得到零频复基带 c = resample_code(code, Ns); % 码序列重采样到 Ns 点 R = ifft( fft(x) .* conj(fft(c)) ); % 循环相关:一次 FFT 得到全部码相位 [pk, idx] = max(abs(R)); acq.tau = (idx-1) / fs; % 码相位,单位秒 acq.fD = fD; % 对应多普勒 acq.metric = pk / mean(abs(R)); % 峰值比,用于捕获判决 end这里有几个参数值得说清楚。Ns必须取整,采样率和码率不整除时会引入最多半个采样点的码相位量化误差,换算成距离大概几米,做单点定位可以接受,做载波相位就得用双精度相位累加器来做码 NCO,不能靠重采样。acq.metric用峰值比而不是绝对峰值,是为了让判决门限不随信号功率变化,实测中 2.5 是个比较平衡的取值,虚警和漏警都不至于太离谱。外层再套一层多普勒扫描:
fDs = -cfg.acq.dopplerSpan : cfg.acq.dopplerStep : cfg.acq.dopplerSpan; best = struct('metric', 0); for fD = fDs a = acq_parallel(sig, code, cfg.sys.fs, cfg.b1c.codeRate, fD); if a.metric > best.metric best = a; end end if best.metric > cfg.acq.threshold chn.state = "pullin"; % 捕获成功,进入牵引状态 chn.fD = best.fD; chn.tau = best.tau; end扫描顺序上做个小优化:先按 1 kHz 大步进粗扫找出大致区间,再在峰值附近 ±1 kHz 内用 250 Hz 细扫。这样总的 FFT 次数能省掉六成,效果和全程细扫几乎一样。
3.2 跟踪环路:鉴相器、环路滤波与带宽取值
跟踪的本质是三个闭环:载波环让本地载波跟上信号的多普勒和相位,码环让本地码跟上码相位变化,两者通过积分周期耦合在一起。下面是一个积分周期内三路相关和环路更新的核心部分:
% 环路状态初始化(二阶环,阻尼系数 0.707) T = cfg.trk.Tcoh; theta = cfg.trk.Bn_pll * T / (0.707 + 1/(4*0.707)); pll.kp = 4*0.707*theta / (1 + 2*0.707*theta + theta^2); pll.ki = 4*theta^2 / (1 + 2*0.707*theta + theta^2); pll.acc = 0; for k = 2:nEpoch % 1) 用当前载波 NCO 和码 NCO 生成三路本地信号并相关 Ep = sum(rx .* conj(carr) .* cE); % 超前 Pp = sum(rx .* conj(carr) .* cP); % 即时 Lp = sum(rx .* conj(carr) .* cL); % 滞后 % 2) 鉴相 / 鉴频 if inPullIn errF = angle(Pp * conj(PpPrev)); % 叉积鉴频,±180° 内无模糊 else errF = atan(imag(Pp) / real(Pp)); % 二象限反正切鉴相,斜率线性 end errC = (abs(Ep) - abs(Lp)) / (abs(Ep) + abs(Lp)); % 归一化包络鉴相 % 3) 二阶环路滤波,输出频率修正量 pll.acc = pll.acc + pll.ki * errF; fD = fD + pll.kp * errF + pll.acc; dll.acc = dll.acc + dll.ki * errC; tau = tau + dll.kp * errC + dll.acc; PpPrev = Pp; % 供下一周期鉴频使用 cno(k) = 10*log10(mean(abs(Pp))^2 / (2*var(abs(Pp)))); % 宽带载噪比估计 endkp、ki的取值不是拍脑袋的,由噪声带宽Bn和积分周期T算出来,公式里的theta是环路自然频率和积分周期的乘积。PLL 带宽取 15~20 Hz 是常规值:带宽大,动态适应能力强但热噪声大;带宽小,载噪比估计好看但遇到加速度容易失锁。做车载或无人机场景时我会把带宽提到 25 Hz 并考虑加三阶环,静态场景反而可以压到 10 Hz 换更低的相位噪声。
牵引和锁定的切换逻辑是另一处细节。inPullIn标志在进入跟踪的前若干个周期置真,用叉积鉴频器把残余频偏拉小,判据是连续 5 个周期的鉴频输出绝对值小于 20 Hz,再切到 PLL。直接上 PLL 是新手最常犯的错误,因为残留频偏可能上百赫兹,反正切鉴相器在频偏作用下输出一个接近常数的偏置,环路会一直朝一个方向拉,看起来像是“发散”,实际上只是没做牵引。
errC归一化之后,超前减滞后包络鉴相器的输出和信号幅度无关,环路增益不用跟着载噪比变,这一点在信号功率剧烈变化时很重要。代价是低载噪比下方差偏大,所以通道失锁判据我用的是载噪比估计cno(k)而不是鉴相器输出幅度。
3.3 BOC 副峰与 B1C 导频二次码的处理
B1C 的导频分量是 BOC(6,1),自相关函数有多个副峰,副峰间隔在几十纳秒量级。早迟间距取到 0.5 码片以上时,码环完全可能锁在副峰上,而且锁上之后一切看起来都正常:相关值高、环路收敛、载噪比估计漂亮,只有伪距整体偏移一个固定量。这种错误在单点定位结果里表现为残差一致偏大,但不会发散,特别隐蔽。
工程上有两种处理方式。一是减小早迟间距到 0.2~0.3 码片,把鉴相曲线的线性区限制在主峰附近,代价是鉴相增益下降、码环带宽要相应收窄。二是用双分量合成相关——同时产生 BOC(1,1) 和 BOC(6,1) 两套本地码,把两路相关值加权合成一个单峰的相关函数,再进行鉴相。第二种方式实现复杂一些,但抗副峰能力明显更好,做灵敏度对比实验时也能拿来说明工作量。
二次码的剥离要放在位同步之前。B1C 导频通道没有数据调制,只有 1800 码片的二次码,如果二次码没同步,相干积分超过 10 ms 就会因为二次码翻转而互相抵消。做法是把 1800 个可能的二次码相位各试一遍,对每个相位做若干个主码周期的相干累积再取模平方,峰值对应的相位就是二次码起点。这一步的计算量不小,但只做一次,之后每个主码周期乘上对应相位的二次码符号即可。B2a 的二次码只有 100 码片、周期 500 ms,直接穷举也没有压力。
4. 导航电文与定位解算:从比特到 PVT
前面三段把信号变成了码相位和多普勒,这一段要把它们变成米和米每秒。中间还要跨过电文译码这道坎,因为星历和钟差参数都在电文里。
4.1 帧同步、解交织与 LDPC 译码链路
电文链路的顺序是:相关值解调出符号 → 去二次码 → 位同步 → 帧同步(找到子帧头)→ 解交织 → LDPC 软判决译码 → BCH 校验 → CRC 校验 → 参数解析。B1C 数据分量采用 LDPC 加 BCH 的两级编码,B2a 的编码参数与 B1C 不同,译码器要按信号类型分别配置,这一步在 ICD 里写得很清楚,照抄即可,不要凭印象。
帧同步是最容易出虚警的一步。子帧同步头只有几个比特,纯靠比特匹配的话,随机数据里平均几千个位置就会撞上一个完全匹配的,译码全错还查不出原因。我的做法是同步头粗筛加 BCH 校验确认:
function [frame, nErr] = frame_sync(bits, syncWord, subframeLen) % bits : 位同步后的硬判决比特流,长度覆盖至少两个子帧 % syncWord : 子帧同步头,B1C 与 B2a 取值不同 % subframeLen : 子帧长度(比特),按 ICD 取值 L = numel(syncWord); nErr = inf; frame = []; for s = 1 : numel(bits) - subframeLen e = sum(bits(s:s+L-1) ~= syncWord); if e <= 2 % 允许 2 比特错,先粗筛 cand = bits(s : s+subframeLen-1); if bch_check(cand) % BCH 二次确认,压掉虚警 nErr = e; frame = cand; break; end end end endbch_check用子帧尾部的 BCH 校验位做一次校验,只有同步头和校验同时通过才算帧头。加上这一步之后,虚警率能压下去两三个数量级。另一个常见问题是交织顺序,如果去交织的矩阵按行写入按列读出搞反了,LDPC 译码会一直不收敛,误比特率停在 0.5 附近,看起来像“信号太弱”,实际上是索引错了。
4.2 伪距构造与最小二乘解算
伪距的物理含义是信号发射时刻到接收时刻的时间差乘光速。接收时刻由采样计数直接得到,发射时刻要由码相位、码周期计数、比特计数和二次码计数一层层拼出来,任何一层的整数周跳没处理好,伪距就会跳一个码周期对应的距离(B2a 是 1 ms 对应约 300 km,B1C 是 10 ms 对应约 3000 km),表现为残差里出现巨大的粗差。
拼好伪距之后就是解算。四未知数的最小二乘是最小可跑通的版本,先跑通它再加卡尔曼滤波:
function [pos, dtr, iter] = ls_pvt(rho, satPos, satClk, w, x0) % rho : N×1 伪距观测值(m),已做电离层、对流层、TGD 改正 % satPos : N×3 卫星 ECEF 位置(m),由星历在发射时刻插值得到 % satClk : N×1 卫星钟差(m) % w : N×1 权重,通常取 1/sigma^2,sigma 由载噪比映射 % x0 : 初值 [x y z c*dt],冷启动时可用上一次定位结果或全零 x = x0(:); W = diag(w); for iter = 1:10 d = satPos - x(1:3).'; % 接收机到卫星的向量 rng = sqrt(sum(d.^2, 2)); % 几何距离 H = [-d./rng, ones(size(rng))]; % 设计矩阵,最后一列为钟差系数 dy = rho - (rng + x(4) - satClk); % 观测残差(含钟差) dx = (H.'*W*H) \ (H.'*W*dy); % 加权最小二乘增量 x = x + dx; if norm(dx(1:3)) < 1e-4, break; end % 位置增量小于 0.1 mm 即收敛 end pos = x(1:3); dtr = x(4) / 299792458; % 钟差换算回秒 end设计矩阵最后一列全为 1,对应接收机钟差这个未知数;如果要做多系统联合解算,钟差列会变成每个系统一列,矩阵结构要相应扩展。权重不能全设成 1,载噪比低的卫星伪距噪声可能比高载噪比的大一个量级,不加权的话最小二乘会被一两颗差星带偏。我一般用sigma = a + b * 10^(-C/N0/20)这种形式从载噪比映射噪声,系数靠静态数据的残差统计拟合。
冷启动时初值全零会让第一次迭代的几何距离偏差几千公里,虽然数学上还能收敛,但迭代次数多、容易在几何差的时刻发散。实用做法是先固定高度做一次二维解算,拿到水平位置之后再放开三维,或者直接给一个粗略的已知点坐标。
4.3 双频电离层改正与精度评估
B1C 和 B2a 分别在 1575.42 MHz 和 1176.45 MHz,双频组合是这套接收机相对单频方案最实际的价值:不用外部电离层模型就能消掉一阶电离层延迟。无电离层组合的系数按频率平方算出来:
f1 = 1575.42e6; f2 = 1176.45e6; k1 = f1^2 / (f1^2 - f2^2); % ≈ 2.2604 k2 = f2^2 / (f1^2 - f2^2); % ≈ 1.2604 rho_if = k1 * rho_b1c - k2 * rho_b2a; % 无电离层组合伪距组合之后观测噪声被放大约 3 倍,所以低仰角卫星的组合伪距反而不如单频平滑后的结果,实践里按仰角做加权,或者只在电离层活跃时段启用组合。评估改正效果的方法很直接:同一段数据分别用单频加模型改正和双频组合解算,比较两者的定位残差和重复性,如果双频没有明显改善,大概率是两条频点的伪距在时间对齐上出了问题,比如两个通道的积分周期边界不在同一时刻,差几毫秒乘以卫星运动速度就是米的量级,这一点在做结果对比前必须先核对。
5. 常见问题排查:跑不通时先看的五个地方
调试这套链路,八成的时间花在五个地方。下面按“现象 → 原因 → 解决”整理,遇到问题时按顺序过一遍,比漫无目的地改参数快得多。
现象一:捕获能出峰,但跟踪一两秒就丢锁,反复重捕。原因通常是多普勒步进过大留下的残余频偏超出了牵引环的捕获带,或者相干积分时长和码周期没对齐——B1C 按 1 ms 切块,一次积分只覆盖十分之一个码周期,相关值本身就低。解决办法是把dopplerStep降到 250 Hz 以内、Tcoh至少取 10 ms 对齐码周期,同时确认牵引阶段的鉴频器真的在工作,方法是在牵引期间把鉴频输出打出来看,应该从几百赫兹单调收敛到几十赫兹以内。
现象二:环路锁定正常,载噪比估计也漂亮,但伪距整体偏一个固定量,定位结果有系统性偏差。这是锁到 BOC 副峰的典型特征,B1C 通道尤其容易发生。原因是早迟间距取得过大,鉴相曲线在副峰处也有过零点。解决方式是把间距压到 0.2~0.3 码片,或者在捕获完成后先用几个不同间距的早迟相关值做一次副峰判决,比较主峰和 ±0.5 码片外的相关值比值,比值不够就重捕。
现象三:帧同步能搜到,但 LDPC 译码一直不收敛,误比特率停在 0.5 附近。最常见的是去交织的索引顺序错了,或者比特极性反了。BOC 调制在载波跟踪环路里可能出现半周模糊,解调出来的比特整体反相,同步头还能靠容错匹配上,但后续译码全错。解决方法是用一段已知的电文做比对,先确认极性,再检查交织矩阵的行列顺序,最后才怀疑译码器本身。
现象四:PVT 结果时好时坏,有时高程飞出去几百米。冷启动初值全零、参与解算的卫星几何结构差(都集中在天空一侧)、权重全设成 1,这三个原因经常同时出现。先把权重改成按载噪比映射,再加一个高度约束的二维解算拿初值,最后检查卫星方位角分布,如果可用卫星全在同一象限,宁可等几分钟换几何构型也不要用这组解。
现象五:换了采样率之后精度变差,但代码一行没改。采样率和码率不整除,重采样后的码序列长度取整带来码相位量化误差,最坏情况差半个采样点;采样率降低还会让 BOC(6,1) 的主峰欠采样,鉴相增益下降。解决办法是选采样率时优先用码率的整数倍关系(比如 32 MHz 对 1.023 Mcps 虽然不整除,但可以通过双精度相位累加器消除累积误差),并且在改采样率后重新标定一次码环带宽和间距。
6. 验收与答辩:一套可复现的测试流程和指标清单
验收这件事,不能靠“跑出来一张定位图挺好看”来交差。我一般按四步走,每一步都留下可复现的记录,答辩时按这个顺序展示,评委问什么都能对应到具体数据和具体参数。
第一步是算法级自测。捕获函数用合成信号验证:自己生成一段带已知多普勒和码相位的 B1C 信号,加高斯白噪声,看捕获输出和真值是否一致。跟踪环用同样方式验证,重点看锁定后的载波相位误差和码相位误差是否收敛到零附近。这一步能挡掉大部分索引错位和符号约定的问题,比直接上真实数据调快得多。
第二步是链路级回放。用提前录好的采样文件,固定配置跑一遍完整流程,记录每个通道的捕获结果、锁定时间、平均载噪比、译码成功率和伪距残差。同一个文件连跑三次结果必须完全一致,这是可复现性的底线;如果三次结果不同,说明代码里有随机初始化或者未初始化的状态变量。
第三步是指标量化。下面这张表是我常用的验收清单,数值是按单点定位、静态场景给出的参考范围,动态场景要放宽:
| 指标 | 参考范围 | 备注 |
|---|---|---|
| 捕获灵敏度(B1C 导频) | C/N0 ≥ 32 dB-Hz | 积分 10 ms、虚警率 1e-3 条件下 |
| 冷启动首次定位时间 | 30~60 s | 12 通道并行、无历书辅助 |
| 锁定后载噪比估计偏差 | ≤ 2 dB | 与商用接收机同场景对比 |
| 伪距残差 RMS | 1~3 m | 剔除粗差后统计 |
| 静态水平定位误差 | 1~3 m | 已知点上长时间平均 |
| 静态高程定位误差 | 3~6 m | 高程几何弱,误差通常是水平的两倍 |
| 双频相对单频改善 | 残差下降 20% 以上 | 电离层活跃时段更明显 |
第四步是鲁棒性演示。答辩时最有说服力的不是“我跑通了”,而是“我知道它在什么条件下会坏”。主动做几组破坏性实验:把采样文件里的信噪比降低 3 dB、把采样率砍一半、人为加一个 5 kHz 的多普勒偏移,看链路在哪一步失效、失效时的现象是什么。能把这些现象和原因讲清楚,比展示一条完美曲线更让人信服。
答辩的讲解顺序我建议按数据流走,不要按代码文件走:先讲两条信号的体制差异决定了哪些模块必须分开,再讲捕获用什么算法、为什么选它,然后讲跟踪环路的带宽怎么定、副峰怎么防,最后落到观测值和定位精度。每讲一个模块,配一张对应阶段的中间结果图——捕获的二维相关峰、跟踪的鉴相器输出、载噪比随时间的变化、定位残差的散点。评委不会去读你的代码,但会看你有没有能力把每一张图对应的物理量说清楚。
讲精度的时候一定要谨慎。单点定位在静态场景下水平一两米是正常水平,别把长时间平均后的结果说成“实时精度”,也别拿一次特别好的解算结果当典型值。老师问“你的精度是多少”,正确的回答是给出统计口径:多少颗星、多长时间、什么仰角截止、有没有剔除粗差,然后给出 RMS 而不是最好那一组。
从那以后我每次带这类项目,都会强制走一遍“合成信号自测 → 回放复现 → 指标量化 → 破坏性实验”这四步,哪怕时间再紧也不跳步,因为跳过的那一步,最后都会在答辩前一天的深夜以另一种方式找上门来。这套代码的模块划分和 demo 脚本就是按这个流程组织的,拉到本地先跑合成信号那一组,确认每个函数都能对上真值,再去改cfg里的参数做自己的实验,会比直接扑到真实数据上省很多来回。希望帮到你。
本文还有配套的精品资源,点击获取