简介:面向GPS信号模拟与软件定义无线电学习者的PLUTO-GPS-SIM工程源码包,用于生成GPS基带信号IQ数据流,并借助ADALM-Pluto SDR在GPS L1频点发射。用户可通过命令行设定静态位置,结合GPS广播星历文件(brdc)构建卫星星座,计算视野内卫星的模拟伪距与多普勒,再生成数字化I/Q样本送入Pluto。资源共14个文件,以C源码与头文件为核心,辅以Makefile构建脚本、README说明文档、OCXO改装原理图PDF与实物照片、circle.csv示例数据及LICENSE等,压缩包约3.02MB,结构紧凑便于二次开发。已有2340人学习下载。读者可从中获得完整的信号生成流程实现、星历解析与伪距多普勒计算思路,以及Pluto本振精度不足导致频偏与温漂的注意事项,适合具备一定C语言与SDR基础的开发者参考。
1. 一块巴掌大的 SDR 板,怎么把 GPS 信号“造”出来
手里只有一块 ADALM-Pluto,想验证 GPS 接收机、做信号完整性实验,或者给教学演示搭个可控的卫星信号源,最省事的路径不是买昂贵的射频记录回放仪,而是用软件把基带信号算出来,再交给 SDR 发射。pluto-gps-sim 就是干这件事的:它用 C 语言实现 GPS L1 C/A 码的基带信号生成,输出 IQ 数据流,最后通过 ADALM-Pluto 的发射通道把信号打出去。整套东西不依赖图形界面,编译完就是命令行工具,适合想搞清楚 GPS 信号结构、又不想被商业仿真软件绑住的从业者。它解决的核心问题是“信号可控”——卫星编号、多普勒、码相位、发射增益都能自己定,而不是被动接收天上那几颗星。适合做 GNSS 算法验证、SDR 射频链路调试、以及通信课程里需要可复现信号源的人。
2. GPS L1 C/A 基带生成:从码相位到 IQ 采样
2.1 为什么选 L1 C/A 而不是别的信号
GPS 有三个常见民用频点,L1 1575.42 MHz 上的 C/A 码是门槛最低的一个。码率 1.023 Mcps,码长 1023,周期 1 ms,一个码周期正好对应 2046 个采样点(按 2.046 MHz 采样率算)。这个整数关系不是巧合,它让基带生成可以用查表和整数索引完成,不需要复杂的重采样。pluto-gps-sim 选这条路径,本质上是把“信号结构简单”换成了“实现可控”。L1 C/A 的扩频增益约 43 dB,接收端即使信号功率低到 -130 dBm 量级也能解出,所以发射端对幅度精度的要求没那么苛刻,SDR 的 12 位 DAC 足够用。
选 C/A 码还有一个现实原因:它的 Gold 码生成逻辑是公开的,两个 10 级线性反馈移位寄存器,抽头位置固定,用 C 写出来不到五十行。相比之下 L1C 或者 L5 的副码和调制方式复杂得多,调试成本高。做信号源验证时,越简单的信号越容易定位问题——信号发不出去,你至少能确定不是调制方式写错了。
2.2 基带生成的三个核心参数
生成一段可用的 GPS 基带信号,绕不开三个参数:采样率、中频、卫星 PRN 号。采样率决定一个码周期里有多少个点,常见取 2.046 MHz、4.092 MHz 或 5 MHz。取 2.046 MHz 时每个码片两个采样点,实现最简单;取 4.092 MHz 时每个码片四个点,信号频谱更干净,但数据量翻倍。中频是把基带搬到载波上的偏移量,Pluto 的发射本振可以设,所以中频一般取 0 到几 MHz 之间,避免和直流泄漏混在一起。
PRN 号决定用哪颗卫星的码。GPS 的 32 个 PRN 对应不同的抽头组合,生成时查一张表即可。多普勒频移是第四个隐含参数,做静态验证时可以设 0,做动态场景模拟时按卫星仰角和相对速度算,典型值在 ±5 kHz 以内。
/* 简化示意:按 PRN 生成一个码周期的 C/A 码采样 */ #define SAMPLE_RATE 2046000.0 #define CHIP_RATE 1023000.0 #define CODE_LEN 1023 #define SAMPLES_PER_CODE 2046 static int ca_code[CODE_LEN]; void gen_ca_code(int prn) { /* 两个 10 级 LFSR,抽头由 PRN 查表决定,此处省略表 */ int g1[10] = {1,1,1,1,1,1,1,1,1,1}; int g2[10] = {1,1,1,1,1,1,1,1,1,1}; for (int i = 0; i < CODE_LEN; i++) { int out1 = g1[9]; int out2 = g2[9]; ca_code[i] = out1 ^ out2; /* 模二加得到 C/A 码片 */ int fb1 = g1[2] ^ g1[9]; /* G1 反馈抽头 3、10 */ int fb2 = g2[1] ^ g2[2] ^ g2[5] ^ g2[7] ^ g2[8] ^ g2[9]; /* 右移并注入反馈 */ for (int j = 9; j > 0; j--) { g1[j] = g1[j-1]; g2[j] = g2[j-1]; } g1[0] = fb1; g2[0] = fb2; } }上面这段只演示码片生成,实际工程里还要把码片按采样率展开成采样点,再乘上载波。展开时用整数倍关系最稳:每个码片重复SAMPLE_RATE / CHIP_RATE次,2.046 MHz 下正好是 2。如果采样率不是码率的整数倍,就得做插值,相位会漂,长序列跑下来码相位对不齐,这是后面避坑章节要展开的点。
2.3 从码片到 IQ:载波搬移与量化
码片序列是 ±1 的实数,要变成 Pluto 能吃的 IQ 数据,得乘上复载波。设中频为 f_if,采样点为 n,则 I 路是code[n] * cos(2π f_if n / fs),Q 路是code[n] * sin(2π f_if n / fs)。这里有个细节:C/A 码是 BPSK 调制,理论上只有实部,但 SDR 发射通道要的是复数基带,所以 Q 路不能省,否则 Pluto 的 IQ 校正会出问题。
量化位宽按 Pluto 的 12 位 DAC 来,实际写文件时常用 16 位有符号整数,留出余量。幅度不要顶满,峰值控制在满量程的 60% 到 70%,给滤波器的过冲留空间。生成完的 IQ 数据按交错格式存成二进制文件,I 在前 Q 在后,小端序,这是 Pluto 的 IIO 接口能直接读的格式。
# 生成 10 秒、PRN 1、中频 0、采样率 2.046 MHz 的 IQ 文件 ./pluto-gps-sim -p 1 -f 0 -r 2046000 -d 10 -o gps_prn1.bin # 查看文件大小,验证采样点数是否对得上 ls -l gps_prn1.bin # 期望大小 = 10 * 2046000 * 2(IQ) * 2(16bit) = 81,840,000 字节命令里的-p是 PRN 号,-f是中频,-r是采样率,-d是时长秒数,-o是输出文件。跑完先看文件大小,这是最便宜的校验手段:算出来的字节数和实际不符,说明采样率或者时长参数没生效,别急着往下走。
3. 把 IQ 流灌进 ADALM-Pluto:发射链路配置
3.1 Pluto 的发射通道怎么开
ADALM-Pluto 默认上电后发射通道是关的,需要先通过 IIO 接口把tx的ensm_mode设成fdd或者tx,再把raw模式打开才能灌数据。常见做法是用 libiio 的 C 接口,或者先用 iio_attr 命令行把通道配好,再用脚本推流。pluto-gps-sim 本身只负责生成文件,推流这一步要么用项目自带的发送程序,要么自己写一个 libiio 循环。
发射本振频率要设成目标载波减去中频。比如想让信号落在 1575.42 MHz,中频设了 0,本振就设 1575.42 MHz;中频设了 1 MHz,本振设 1574.42 MHz。采样率要和生成文件时一致,Pluto 的发射采样率上限在 61.44 MHz 量级,2.046 MHz 完全在范围内,但要注意它的时钟分频是整数关系,设 2.046 MHz 时实际可能落到 2.083 MHz 附近,这个偏差会让码率轻微偏移,做长时间相干积累时要留意。
# 配置 Pluto 发射通道(设备名按实际枚举结果替换) iio_attr -c ad9361-phy voltage0 sampling_frequency 2046000 iio_attr -c ad9361-phy voltage0 rf_bandwidth 2000000 iio_attr -c ad9361-phy altvoltage1 frequency 1575420000 iio_attr -c ad9361-phy voltage0 hardwaregain -10 # 打开发射使能 iio_attr -c ad9361-phy voltage0 ensm_mode fddhardwaregain是发射增益,单位 dB,范围大概 -89 到 0。做近距离验证时别开太高,-20 dB 以下配合衰减器更安全,直接接天线全功率发射在多数场景下不合规,这一点后面避坑章节会专门说。rf_bandwidth设成和采样率同量级,太窄会把 C/A 码的主瓣切掉,太宽会引入带外噪声。
3.2 推流循环与欠载处理
推流的核心是一个循环:从文件读一块缓冲区,写进 Pluto 的cf-ad9361-dds-core-lpc设备的voltage0输出通道。缓冲区大小建议取采样率的整数分之一,比如 2046 的整数倍,这样每次写入的码相位是连续的。Pluto 的 USB 2.0 接口带宽有限,2.046 MHz 采样率、16 位 IQ 的码率约 8.2 MB/s,USB 2.0 理论 60 MB/s 够用,但实际受主机调度影响,缓冲区太小会欠载。
欠载的表现是发射信号出现周期性断裂,接收端看到的是码相位跳变。判断方法很简单:在推流程序里统计每次write的返回值,如果返回字节数小于请求值,就是欠载。解决手段是加大缓冲区、提高线程优先级,或者把采样率降到 1.023 MHz 试试。我一般会把缓冲区设成 16384 个采样点,约 8 ms 的数据,给调度留足余量。
/* libiio 推流循环骨架 */ struct iio_buffer *buf = iio_device_create_buffer(tx_dev, 16384, false); while (running) { ssize_t nbytes = read(fd, iq_buf, buf_size); if (nbytes <= 0) break; memcpy(iio_buffer_start(buf), iq_buf, nbytes); ssize_t written = iio_buffer_push(buf); if (written < nbytes) { fprintf(stderr, "underrun: %zd/%zd\n", written, nbytes); } }iio_buffer_push返回实际推入的字节数,小于请求值就是欠载信号。这段骨架里没做重试,实际用的时候欠载了要么补零要么重读,补零会让码相位断掉,重读更稳。缓冲区 16384 是经验值,采样率越高越要加大。
3.3 用接收端验证信号是否真的发出去了
发出去不等于发对了。最直接的验证是用另一块 SDR 或者一台 GPS 接收机在近场接收。没有第二块 SDR 时,可以用 Pluto 自己的接收通道做环回,加一个衰减器把 TX 接到 RX,看频谱上 1575.42 MHz 附近有没有 C/A 码的 sinc 包络。频谱仪上看,C/A 码的主瓣宽度约 2.046 MHz,零点在 ±1.023 MHz 处,旁瓣依次衰减。
更严格的验证是解调:用开源的 GNSS 软件接收机处理录下来的 IQ,看能不能捕获到对应 PRN 的卫星。捕获成功说明码相位和载波都对;捕获到了但跟踪不稳,多半是采样率偏差或者增益设置问题。这一步别跳过,很多“信号发不出去”的翻车其实是参数对不上,频谱上看着有能量,解调一跑就露馅。
4. 避坑与排查:五个真实翻车点
4.1 采样率不是码率整数倍,码相位越跑越偏
现象:短时间接收正常,跑几分钟后捕获不到,或者跟踪环频繁失锁。原因:生成时用了 2.5 MHz 这类非整数倍采样率,每个码周期的采样点数不是整数,累积下来码相位漂移超过半个码片。解决:采样率取 1.023 MHz 的整数倍,2.046 MHz 或 4.092 MHz 最稳;实在要用别的值,在生成端做相位累加器补偿,别指望接收端追。
4.2 发射增益开满,近场接收机前端饱和
现象:接收机就在天线旁边,信号强度显示很高,但就是解不出星。原因:Pluto 发射增益设到 0 dB,近场功率远超接收机线性范围,前端 LNA 饱和产生互调。解决:发射增益降到 -30 dB 以下,中间串 20 到 30 dB 衰减器,接收端看到的功率落在 -80 到 -100 dBm 之间最合适。做实验时先小功率试,逐步加。
4.3 中频设 0,直流泄漏把码频谱顶歪
现象:频谱上 1575.42 MHz 处有一根很高的尖峰,C/A 码的包络被压得看不清。原因:中频设 0 时基带信号中心落在直流,Pluto 的 DAC 和混频器有直流偏置,泄漏到射频上形成强单音。解决:中频挪到 1 MHz 到 2 MHz,让信号避开直流;或者在生成端加一个简单的直流阻断,但挪中频更省事。
4.4 文件字节序或交错格式不对,推流全是噪声
现象:推流程序不报错,频谱上也有能量,但完全是噪声,捕获不到任何 PRN。原因:IQ 文件按大端序写,或者 I、Q 分开存而不是交错,Pluto 按小端交错读,数据全错位。解决:生成端统一小端序、I 在前 Q 在后交错;用xxd看文件头几个字节,和预期值对一下,别嫌麻烦。
4.5 USB 带宽不够,推流欠载导致码相位跳变
现象:接收端偶尔能捕到,但跟踪时断时续,码相位有规律地跳。原因:缓冲区太小或者主机负载高,iio_buffer_push周期性欠载,每次欠载丢一段数据,码相位不连续。解决:缓冲区加到 16384 采样点以上,推流线程提优先级,关掉主机上占 USB 带宽的其他设备;还不行就降采样率到 1.023 MHz。
5. 进阶:用多 PRN 叠加做场景模拟与验证闭环
单 PRN 只能验证链路通不通,要做接近真实的场景,得把多颗卫星的信号叠在一起。做法不复杂:每个 PRN 单独生成一段 IQ,按各自的功率比例相加,再统一量化。功率比例按仰角算,低仰角的卫星信号弱一些,典型动态范围控制在 10 dB 以内,避免量化时弱信号被截断。叠加时要注意各 PRN 的码相位是独立的,不需要对齐,但采样率必须一致,否则没法直接相加。
import numpy as np # 读取多个单 PRN 的 IQ 文件,按功率权重叠加 def load_iq(path): raw = np.fromfile(path, dtype=np.int16) return raw[0::2] + 1j * raw[1::2] # I 偶 Q 奇,转复数 signals = [load_iq(f"gps_prn{p}.bin") for p in (1, 7, 13)] weights = [1.0, 0.7, 0.5] # 按仰角粗估的相对功率 n = min(len(s) for s in signals) combined = sum(w * s[:n] for w, s in zip(weights, signals)) combined /= np.max(np.abs(combined)) * 1.4 # 留 30% 余量防削顶 iq = np.empty(combined.size * 2, dtype=np.int16) iq[0::2] = (combined.real * 32767).astype(np.int16) iq[1::2] = (combined.imag * 32767).astype(np.int16) iq.tofile("gps_multi.bin")这段脚本的关键在归一化那行:除以峰值再乘 1.4,是把峰值压到满量程的 70% 左右,给叠加后的过冲留空间。权重按仰角粗估只是权宜,真要精确得按自由空间路径损耗算,但做链路验证够用。叠加完推流,接收端应该能同时捕获到三颗星,捕获的载噪比排序和权重排序一致,就说明叠加没出错。
验证闭环我一般这么走:先用单 PRN 确认发射链路和接收端都正常,再上多 PRN 看捕获数量,最后改多普勒参数看跟踪环的动态响应。每一步只改一个变量,出问题能立刻定位到是哪一环。有个习惯是从那以后每次改采样率或者中频,都先把文件大小和频谱包络过一遍再推流,省得在接收端瞎猜。希望帮到你。
本文还有配套的精品资源,点击获取