简介:pluto-gps-sim 是一套面向软件定义无线电(SDR)与卫星导航爱好者的 GPS 基带信号仿真工具,基于 C 语言实现,可生成 GPS L1 频段的数字化 I/Q 样本流,并交由 ADALM-Pluto 平台发射。用户既能通过命令行指定静态位置,也能借助 GPS 广播星历文件(brdc)构建卫星星座,进而计算可见卫星的伪距与多普勒,完成从星历到基带信号的完整链路仿真,适合从事 GNSS 信号模拟、SDR 收发实验的中高级开发者研究。资源包共 14 个文件,约 3.02MB,包含核心源码 plutogpssim.c 与头文件、Makefile 构建脚本、说明文档、OCXO 改装原理图 PDF 及实物照片、示例 CSV 数据等,覆盖编译、运行与硬件改造参考。目前已有 2338 人学习下载。需注意 ADALM-Pluto 内部振荡器在频率精度与温度稳定性上不足以直接满足 GPS 信号生成要求,作者提供了外部恒温晶振改装方案与接线示意,读者可据此理解时钟精度对导航信号的影响并完成硬件优化。
1. 从一条 IQ 数据流说起:为什么用 PLUTO-GPS-SIM 做 GPS 信号源
如果你手上只有一块 ADALM-Pluto,却想在没有外部信号源的情况下让一台 GPS 接收机“看见”卫星,最直接的路子不是去买昂贵的 GNSS 模拟器,而是自己生成基带 IQ 数据流,再交给 Pluto 发射出去。PLUTO-GPS-SIM 这个方向解决的就是这件事:它把 GPS L1 C/A 码的伪随机序列、导航电文、多普勒频移和伪距这些参数,在软件里合成一段复数采样流,然后通过 Pluto 的 DAC 变成射频信号。适合谁?做接收机算法验证的、想复现捕获跟踪链路的、以及需要可控信号源做教学演示的工程师。它不追求替代商用模拟器,但胜在链路透明、参数可改、成本可控。
2. GPS L1 C/A 基带信号到底怎么合成:从码相位到 IQ 采样
2.1 先厘清一个容易混淆的点:基带 IQ 不是射频输出
很多人第一次做这个方案时,会把“生成 IQ 文件”和“Pluto 发射”当成两步独立的事,结果在 Pluto 端发现频谱不对。根本原因是:PLUTO-GPS-SIM 生成的是零中频或低中频的复数采样,采样率通常在 2 MHz 到 10 MHz 之间,而 GPS L1 载频是 1575.42 MHz。Pluto 内部要做的是把这段基带信号搬移到载频上,这个搬移由 Pluto 的混频器和本振完成,不需要你在 IQ 文件里再乘一个 1575.42 MHz 的复指数。如果你在生成阶段就把载频乘进去,Pluto 再搬一次,结果就是频谱跑到 3 GHz 附近,接收机什么都收不到。
常见做法是:生成端只保留伪码、电文、多普勒和伪距带来的码相位变化,输出复数 float32 或 int16 交织的 IQ 文件。采样率的选择要满足 Nyquist,同时留出足够的带宽给 C/A 码主瓣(约 2.046 MHz)。我一般会把采样率设成 4.092 MHz 或 5 MHz,前者是 C/A 码码率的 4 倍,后者方便和 Pluto 的最小采样率对齐。
2.2 用 Python 合成单颗卫星的 C/A 码基带信号
下面这段代码只做一件事:生成一颗卫星、一个码周期内的基带 IQ 数据,不涉及电文调制和多普勒,目的是让你先看到码相位和采样率的关系。
import numpy as np # GPS L1 C/A 码参数 CA_CHIP_RATE = 1.023e6 # 码率 1.023 MHz CA_CODE_LEN = 1023 # 一个码周期 1023 个 chip SAMPLE_RATE = 4.092e6 # 采样率取码率 4 倍 SAMPLES_PER_CODE = int(SAMPLE_RATE / CA_CHIP_RATE * CA_CODE_LEN) # 4092 # 用两个 10 级移位寄存器生成 C/A 码(简化:只演示 G1 序列) def generate_ca_code(prn=1): # 实际实现需要按 PRN 选择 G2 抽头,这里用固定抽头演示 g1 = np.ones(10, dtype=int) g2 = np.ones(10, dtype=int) code = np.zeros(CA_CODE_LEN, dtype=int) for i in range(CA_CODE_LEN): code[i] = (g1[9] ^ g2[9]) & 1 fb1 = g1[2] ^ g1[9] fb2 = g2[1] ^ g2[2] ^ g2[5] ^ g2[7] ^ g2[8] ^ g2[9] g1 = np.roll(g1, 1); g1[0] = fb1 g2 = np.roll(g2, 1); g2[0] = fb2 return code ca = generate_ca_code(prn=1) # 把 ±1 码序列上采样到采样率,得到基带实信号 ca_upsampled = np.repeat(ca * 2 - 1, int(SAMPLE_RATE / CA_CHIP_RATE)) # 转成复数 IQ,虚部置零(单边带基带) iq = ca_upsampled.astype(np.complex64) # 截取一个完整码周期对应的采样点 iq = iq[:SAMPLES_PER_CODE] np.save('gps_baseband_prn1.npy', iq) print(iq.shape, iq.dtype)逻辑说明:generate_ca_code用两个移位寄存器按 GPS ICD 的抽头规则产生 1023 位码序列,这里为了篇幅只演示了固定抽头,实际要按 PRN 号查表。np.repeat完成从码率到采样率的零阶保持上采样,这是最粗糙但最容易验证的做法。iq的虚部为零,因为单颗卫星的 C/A 码是 BPSK 调制,基带只有实部;如果你要做多颗卫星叠加,每颗卫星需要乘上不同的多普勒复指数,虚部才会出现。
参数说明:SAMPLE_RATE必须大于 2 倍码率,否则码片边沿会混叠;SAMPLES_PER_CODE必须是整数,否则码相位会随时间滑动,接收机捕获时会看到相关峰分裂。如果你把采样率设成 4.092 MHz,一个码周期正好 4092 个采样点,这是最省事的配置。
2.3 多颗卫星叠加与多普勒频移的写法
单颗卫星只能验证捕获,要验证定位解算至少需要 4 颗。多颗卫星叠加时,每颗卫星的码相位不同,多普勒也不同。下面这段代码演示 4 颗卫星的叠加方式。
import numpy as np SAMPLE_RATE = 4.092e6 DURATION = 0.01 # 10 ms,对应 10 个码周期 N = int(SAMPLE_RATE * DURATION) # 假设 4 颗卫星的 PRN、码相位偏移(采样点)、多普勒(Hz) sats = [ {'prn': 1, 'code_phase': 0, 'doppler': 1500.0}, {'prn': 2, 'code_phase': 1023, 'doppler': -800.0}, {'prn': 3, 'code_phase': 2046, 'doppler': 2200.0}, {'prn': 4, 'code_phase': 3069, 'doppler': -1200.0}, ] t = np.arange(N) / SAMPLE_RATE baseband = np.zeros(N, dtype=np.complex64) for sat in sats: # 生成该 PRN 的码并上采样(此处复用上一节的函数,略) ca = generate_ca_code(sat['prn']) ca_up = np.repeat(ca * 2 - 1, int(SAMPLE_RATE / 1.023e6)) # 按码相位偏移循环移位 ca_up = np.roll(ca_up, sat['code_phase']) # 截取或平铺到 N 长度 reps = int(np.ceil(N / len(ca_up))) ca_full = np.tile(ca_up, reps)[:N] # 加多普勒复指数 doppler_phasor = np.exp(1j * 2 * np.pi * sat['doppler'] * t) baseband += ca_full * doppler_phasor # 归一化,避免 Pluto 端削顶 baseband = baseband / np.max(np.abs(baseband)) * 0.7 np.save('gps_4sat_baseband.npy', baseband) print(baseband.shape, baseband.dtype)逻辑说明:每颗卫星独立生成码序列,按code_phase做循环移位模拟伪距差异,再乘上多普勒复指数。np.tile把单码周期平铺到整个时长,实际电文调制需要在码周期边界翻转相位,这里为了先跑通链路省略了。归一化到 0.7 是为了给 Pluto 的 DAC 留余量,Pluto 的满量程是 ±1,超过就会削顶,削顶后的信号相关峰会被压扁。
参数说明:DURATION取 10 ms 是因为 GPS 接收机通常用 1 ms 或 10 ms 做相干积分,10 ms 能覆盖一个导航电文比特。doppler的范围一般在 ±5 kHz 以内,地面静态场景下多普勒主要由卫星运动引起,最大约 ±4 kHz。如果你把多普勒设得太大,接收机的捕获频率搜索范围要相应加宽,否则捕不到。
3. 把 IQ 文件喂给 ADALM-Pluto:从文件到射频的完整链路
3.1 Pluto 的发射链路配置与采样率匹配
Pluto 的发射通道有独立的 LO 和采样率设置,很多人翻车的地方在于:只改了tx_sample_rate,没改tx_lo,结果信号出现在错误的频点。GPS L1 是 1575.42 MHz,Pluto 的 LO 范围覆盖这个频点,但要注意 Pluto 在 1.5 GHz 附近的输出功率和杂散表现。我一般会把tx_lo设成 1575.42 MHz,tx_sample_rate设成 4.092 MHz 或 5 MHz,和生成端保持一致。
用 Python 的pyadi-iio库可以直接把 numpy 数组推给 Pluto,不需要先存文件再读。下面这段代码演示从文件加载 IQ 并发射。
import numpy as np import adi # 加载生成好的基带 IQ iq = np.load('gps_4sat_baseband.npy') # Pluto 的 DAC 是 12 位,需要转成 int16 范围 iq_int16 = (iq * 2047).astype(np.int16) # 初始化 Pluto sdr = adi.Pluto(uri='ip:192.168.2.1') # 按实际 IP 改 sdr.tx_lo = int(1575.42e6) # GPS L1 载频 sdr.tx_sample_rate = int(4.092e6) # 与生成端一致 sdr.tx_rf_bandwidth = int(4.092e6) # 射频带宽,至少等于采样率 sdr.tx_hardwaregain_chan0 = -10 # 衰减,避免饱和,先从小功率试 sdr.tx_cyclic_buffer = True # 循环发射,方便接收机持续捕获 # 推数据,注意 Pluto 要求 IQ 交织的 int16 数组 sdr.tx(iq_int16) print('tx started, lo=', sdr.tx_lo, 'fs=', sdr.tx_sample_rate)逻辑说明:tx_lo决定射频中心频率,tx_sample_rate决定基带带宽,两者必须和生成端匹配。tx_rf_bandwidth是模拟前端带宽,设成和采样率相同可以避免带外噪声被放大。tx_hardwaregain_chan0是衰减值,负得越多输出越小,第一次测试建议从 -20 dB 开始,用频谱仪或接收机看信号强度再调。tx_cyclic_buffer=True让 Pluto 循环播放同一段数据,接收机可以反复捕获,不用每次重新推。
参数说明:Pluto 的tx_sample_rate最低可以到 521 kHz,但 GPS C/A 码主瓣约 2 MHz,采样率低于 4 MHz 会切掉边带,相关峰会变宽。tx_rf_bandwidth如果设得比采样率小,信号会被模拟滤波器削掉一部分,接收机看到的 C/N0 会下降。tx_hardwaregain_chan0的范围通常是 -89 到 0 dB,具体看固件版本,不要设成正数。
3.2 用 Pluto 发射后,接收端怎么验证信号真的出来了
发射端配好之后,最直接的验证方法是用另一台 SDR 或者频谱仪看 1575.42 MHz 附近有没有信号。如果没有频谱仪,可以用 Pluto 自己的接收通道做回环,但要注意收发同时工作时的隔离度。更稳妥的做法是用一台 GPS 接收机,看它能不能捕获到卫星。
下面这段代码用 Pluto 的接收通道做频谱扫描,确认发射信号存在。
import numpy as np import adi import matplotlib.pyplot as plt sdr = adi.Pluto(uri='ip:192.168.2.1') sdr.rx_lo = int(1575.42e6) sdr.rx_sample_rate = int(4.092e6) sdr.rx_rf_bandwidth = int(4.092e6) sdr.rx_buffer_size = 2**16 sdr.gain_control_mode_chan0 = 'manual' sdr.rx_hardwaregain_chan0 = 30 # 接收增益,回环时不要太高 # 采一段数据做 FFT samples = sdr.rx() fft = np.fft.fftshift(np.fft.fft(samples)) freq = np.fft.fftshift(np.fft.fftfreq(len(samples), 1/sdr.rx_sample_rate)) plt.plot(freq/1e6, 20*np.log10(np.abs(fft)+1e-12)) plt.xlabel('Frequency offset (MHz)') plt.ylabel('Magnitude (dB)') plt.title('Pluto RX spectrum at 1575.42 MHz') plt.grid(True) plt.show()逻辑说明:rx_lo设成和tx_lo相同,接收通道就会在同一个频点上看基带。rx_buffer_size取 65536 是为了 FFT 的频率分辨率足够看到 C/A 码的频谱形状。gain_control_mode_chan0设成 manual 是为了固定增益,避免 AGC 把弱信号压掉。如果你看到中心附近有一块抬起的频谱,宽度约 2 MHz,那就是 C/A 码的主瓣。
参数说明:rx_hardwaregain_chan0在回环测试时不要设太高,否则接收前端饱和,频谱会变成平顶。rx_sample_rate和rx_rf_bandwidth要和发射端一致,否则看到的频谱会被截断或混叠。如果频谱上只有一根单音,没有展宽的码谱,说明 IQ 数据没有正确推入,检查tx_cyclic_buffer和数组长度。
3.3 从文件发射和从内存发射的差别
Pluto 的发射有两种模式:从内存循环推和从文件流式推。tx_cyclic_buffer=True时,Pluto 把整段数据放进 DDR 循环播放,适合短数据反复发射。如果数据很长,比如几秒的 IQ,Pluto 的 DDR 放不下,就需要用tx()分块推,或者用 IIO 的流式接口。
常见做法是:验证阶段用短数据加循环缓冲,确认链路通了;要做长时间场景时,把 IQ 存成文件,用iio_readdev或pyadi-iio的分块推流。分块推流时要注意块与块之间的相位连续性,如果每块都重新归一化,块边界会出现相位跳变,接收机的跟踪环会失锁。
提示:Pluto 的发射缓冲深度有限,循环发射时如果数据长度不是采样率的整数倍,循环点会出现码相位不连续,接收机可能反复失捕。建议把数据长度取成码周期的整数倍。
4. 避坑与排查:IQ 生成和 Pluto 发射中最容易翻车的 5 个点
4.1 频谱出现在错误频点,接收机完全搜不到
现象:Pluto 的tx_lo设了 1575.42 MHz,但接收机在 GPS L1 上扫不到任何信号,频谱仪上看到信号出现在 3 GHz 附近。
原因:生成 IQ 时把 1575.42 MHz 的载频乘进了基带,Pluto 又搬移了一次,实际输出频率是tx_lo + 1575.42 MHz。
解决:生成端只保留基带,载频搬移交给 Pluto 的tx_lo。检查代码里有没有np.exp(1j*2*np.pi*1575.42e6*t)这样的项,有就删掉。
4.2 相关峰很宽或者分裂,捕获概率低
现象:接收机偶尔能捕到,但相关峰不尖锐,跟踪环频繁失锁。
原因:采样率设得太低,C/A 码的码片边沿被混叠,或者SAMPLES_PER_CODE不是整数,码相位在每个码周期滑动。
解决:采样率至少取 4 倍码率,即 4.092 MHz;确保SAMPLE_RATE / CA_CHIP_RATE * CA_CODE_LEN是整数。如果必须用非整数采样率,在生成端做插值而不是零阶保持。
4.3 Pluto 输出功率太低,接收机在近距离也收不到
现象:Pluto 的tx_hardwaregain_chan0已经设到 0 dB,接收机天线靠近 Pluto 仍然没有信号。
原因:Pluto 在 1.5 GHz 附近的输出功率本身不高,加上tx_rf_bandwidth设得太窄,信号被模拟滤波器衰减。
解决:把tx_rf_bandwidth设成和tx_sample_rate相同或略大;用频谱仪确认 Pluto 端口的实际功率;如果还是不够,加一级外部放大器,但要注意放大器的噪声系数和线性度。
4.4 循环发射时接收机反复失锁
现象:接收机第一次能捕到,过几秒就丢了,然后再也捕不到。
原因:tx_cyclic_buffer=True时,数据循环点的码相位不连续,接收机的跟踪环在循环点被拉偏。
解决:把 IQ 数据长度取成码周期的整数倍,比如 10 ms 对应 10 个码周期;或者在循环点做相位对齐,让首尾码相位连续。
4.5 多颗卫星叠加后信号削顶,相关峰被压扁
现象:单颗卫星时接收机正常,叠加 4 颗后捕获概率反而下降。
原因:多颗卫星叠加后幅度超过 Pluto 的 DAC 满量程,削顶产生互调,相关峰被压扁。
解决:叠加后做归一化,把最大幅度压到 0.7 以下;或者给每颗卫星分配不同的功率权重,模拟实际场景中的功率差异。
5. 进阶技巧:用 Pluto 做闭环验证和参数扫描
5.1 用接收机反馈校准发射功率和频偏
Pluto 的晶振精度有限,tx_lo设 1575.42 MHz,实际可能有几百 Hz 到几 kHz 的频偏。这个频偏会被接收机当成多普勒,如果频偏太大,接收机的频率搜索范围要加宽,捕获时间变长。我一般会先用接收机测出实际频偏,然后在生成端把多普勒整体偏移这个值,相当于做一次开环校准。
具体做法:发射单颗卫星,多普勒设 0,接收机捕获后读出载波频率估计值,这个值就是 Pluto 的频偏。然后在生成多颗卫星时,把每颗卫星的多普勒都减去这个频偏。这样接收机看到的等效多普勒就回到设定值附近。
5.2 参数扫描表:采样率、多普勒、功率对捕获的影响
下面这张表是我在多次测试中总结的参数范围,不是理论极限,但可以作为起点。
| 参数 | 建议范围 | 影响 |
|---|---|---|
| 采样率 | 4.092 MHz 到 10 MHz | 低于 4 MHz 码片混叠,高于 10 MHz 数据量增大但收益有限 |
| 多普勒 | ±5 kHz | 超出范围接收机捕获频率搜索要加宽 |
| 发射衰减 | -20 dB 到 -10 dB | 太高会饱和,太低接收机 C/N0 不够 |
| 数据时长 | 10 ms 到 100 ms | 太短接收机积分不够,太长 Pluto DDR 放不下 |
| 卫星数 | 4 到 8 | 少于 4 颗无法定位,多于 8 颗叠加后削顶风险增大 |
这张表的使用方法是:先按建议范围的中值配一组参数,跑通链路,然后每次只改一个参数,观察接收机的捕获时间和 C/N0 变化。不要一次改多个参数,否则出了问题不知道是哪个引起的。
5.3 一个我踩过的坑:忽略 Pluto 的收发隔离
有一次我用同一块 Pluto 做收发回环,发射功率设得比较高,接收增益也开得大,结果接收到的频谱上全是发射通道的杂散,真正的 GPS 信号被淹没了。后来把发射衰减降到 -30 dB,接收增益降到 20 dB,才看到干净的码谱。Pluto 的收发通道共用一部分射频前端,隔离度有限,回环测试时一定要从小功率开始,逐步加。
另一个习惯是:每次改完参数,先用频谱仪或接收机确认信号存在,再去调算法。不要一上来就盯着捕获结果调代码,很多时候问题出在射频链路上,不在基带算法里。希望帮到你。
本文还有配套的精品资源,点击获取