简介:面向GPS信号模拟与软件定义无线电学习者的PLUTO-GPS-SIM工程源码包,基于Takuji Ebinuma的开源工作实现:由命令行指定静态位置或读取GPS广播星历文件,计算视野内卫星的伪距与多普勒,生成数字化GPS基带I/Q样本,再经ADALM-Pluto SDR在L1频段发射,适合做SDR入门实验与信号生成原理验证。包内共14个文件、约3.02MB,以C源码与头文件为核心,辅以Makefile构建脚本、说明文档、OCXO改装原理图PDF与实物照片、示例CSV数据及许可证,便于编译复现与硬件改造参考。已有2339人学习下载。需注意Pluto板载振荡器存在频偏与温漂,作者另附恒温晶振改装资料,读者可据此理解时钟精度对GPS信号生成的影响,并借助源码与文档完成从星历解析到射频发射的完整链路实践。
1. 一块巴掌大的 SDR 板,怎么把 GPS 信号“造”出来
手里只有一块 ADALM-Pluto,想验证 GPS 接收机在弱信号、冷启动、动态场景下的表现,最直接的办法不是等天上卫星,而是自己造一段 GPS 基带信号,再让 SDR 把它发射出去。pluto-gps-sim 就是干这件事的:用 C 语言生成 GPS L1 C/A 码的基带 IQ 数据流,交给 ADALM-Pluto 发射,接收端就能像对待真实卫星一样去捕获、跟踪、解算。它适合做 GNSS 算法验证、接收机灵敏度测试、教学演示的从业者,也适合想把“信号生成—射频发射—接收验证”整条链路跑通的新手。下面按“它怎么造信号、怎么发出去、哪里会翻车”的顺序拆开讲。
2. GPS L1 C/A 基带信号是怎么被“拼”出来的
2.1 从导航电文到 IQ 采样:一条信号的完整生成链
GPS L1 频点 1575.42 MHz,C/A 码码率 1.023 Mcps,一个码周期 1023 个码片,对应 1 ms。pluto-gps-sim 的核心工作,是把“导航电文比特 → C/A 码扩频 → 载波调制 → 复基带 IQ”这条链在软件里走完,输出的是采样率可配置的复数 IQ 流。理解这条链,后面调参数才不会瞎试。
生成顺序大致是:先按 50 bps 的导航电文速率把比特流排好,每个比特持续 20 ms,也就是 20 个 C/A 码周期;每个码周期用对应 PRN 的 1023 位 Gold 码做扩频;扩频后的 ±1 序列再乘上载波,落到基带就是 I/Q 两路。这里有个容易忽略的点:软件生成的是复基带,中心频率为 0,真正的 1575.42 MHz 搬移是 Pluto 内部本振完成的,所以你在代码里看到的相位累加器只负责基带内的多普勒和码相位。
C/A 码本身是 10 级线性反馈移位寄存器产生的 Gold 码,不同 PRN 对应不同的抽头组合。生成时如果 PRN 号写错,接收端捕获相关峰就会对不上,现象是“明明发了信号却搜不到星”。常见做法是把 PRN 和抽头表做成常量数组,生成前先自检前 10 个码片是否与标准序列一致。
/* 简化示意:单个 PRN 的 C/A 码生成核心逻辑 */ #define CA_LEN 1023 static int ca_code[CA_LEN]; void gen_ca_code(int prn, int *out) { /* 10 级 G1/G2 寄存器,初值全 1,这是 GPS 规范约定 */ int g1 = 0x3FF, g2 = 0x3FF; /* 不同 PRN 对应 G2 的不同抽头异或组合,此处用查表值代替 */ int tap1 = prn_tap1[prn], tap2 = prn_tap2[prn]; for (int i = 0; i < CA_LEN; i++) { int g2_out = ((g2 >> tap1) ^ (g2 >> tap2)) & 1; out[i] = (g1 & 1) ^ g2_out; /* Gold 码 = G1 输出异或 G2 抽头输出 */ int fb1 = ((g1 >> 2) ^ (g1 >> 9)) & 1; /* G1 反馈抽头 3、10 */ int fb2 = ((g2 >> 1) ^ (g2 >> 2) ^ (g2 >> 5) ^ (g2 >> 7) ^ (g2 >> 8) ^ (g2 >> 9)) & 1; /* G2 反馈抽头 */ g1 = ((g1 << 1) | fb1) & 0x3FF; g2 = ((g2 << 1) | fb2) & 0x3FF; } }这段代码的关键在三个参数:prn决定抽头组合,tap1/tap2决定 G2 输出取哪两位异或,反馈抽头决定寄存器推进方式。码片输出是 0/1,实际调制时要映射成 +1/-1,映射反了会导致相关峰极性相反,接收端可能锁到反码上。生成完 1023 个码片后建议打印前 10 位和公开的标准序列比对一次,这是最省事的自检。
2.2 采样率、码相位与多普勒:三个必须先定死的参数
IQ 流的采样率决定了每个码片被采多少个点。C/A 码率 1.023 MHz,若采样率取 2.046 MHz,每码片 2 个采样点,数据量小但相关峰较粗;取 4.092 MHz 则每码片 4 点,捕获更稳,文件也翻倍。pluto-gps-sim 这类工具通常允许在配置里指定采样率,常见取值是 2.046、4.092、5.0、10.0 MHz。采样率不是越高越好,要兼顾 Pluto 的基带带宽和主机写文件的吞吐。
码相位初值决定信号从码周期的哪个位置开始,做多接收机对比测试时要固定它,否则两次运行的捕获结果没法对齐。多普勒频移则是给载波相位累加器加一个频率偏移,模拟卫星相对运动;静态场景设 0 即可,动态场景按 ±5 kHz 范围给值。这三个参数一旦定下,整段 IQ 的“性格”就定了,后面发射端只是忠实搬运。
| 参数 | 典型取值 | 影响 | 调错的现象 |
|---|---|---|---|
| 采样率 | 2.046 / 4.092 MHz | 每码片采样点数、文件大小 | 太低相关峰钝,太高主机写盘跟不上 |
| 码相位初值 | 0 ~ 1022 码片 | 捕获搜索起点 | 多次运行结果无法对齐 |
| 多普勒频移 | -5000 ~ +5000 Hz | 载波频偏 | 接收端频率搜索范围不够就捕不到 |
| 信号幅度 | 归一化到 ±1 内 | 量化精度、削顶 | 幅度过大削顶,相关峰畸变 |
参数定完后,生成脚本一般会输出一个二进制 IQ 文件加一个描述文件(记录采样率、中心频率、格式)。描述文件别嫌麻烦,后面 Pluto 发射时采样率填错,整段信号就废了,血泪经验是“生成时写什么,发射时就填什么,中间不做任何换算”。
3. 把 IQ 流灌进 ADALM-Pluto:发射链路的实操步骤
3.1 环境准备与 Pluto 连通性自检
ADALM-Pluto 通过 USB 以网卡方式枚举,主机侧通常能看到一个 192.168.2.x 网段的接口。第一步不是急着发信号,而是确认能连上并读到设备信息。常见做法是用 libiio 的命令行工具先探活,能列出上下文和通道,再谈发射。
# 查看 Pluto 是否被识别为网卡设备 iio_info -u ip:192.168.2.1 # 若成功,会打印设备树、可用通道、采样率范围等信息 # 确认 tx 通道存在且能设置采样率 iio_attr -u ip:192.168.2.1 -c cf-ad9361-dds-core-lpc voltage0 sampling_frequencyiio_info能通说明 USB 网络和 libiio 都正常;如果超时,先查主机网卡是否拿到同网段地址,再查 USB 线是否只供电不传数据。iio_attr读采样率是为了确认发射通道可写,有些固件版本对 tx 通道的采样率范围有限制,超出范围会静默失败,现象是“命令返回成功但发出去的是噪声”。
这一步的坑在于:Pluto 的默认 IP 可能被改过,或者主机上有多块网卡导致路由走错。排查方法是ip route看 192.168.2.0/24 是否指向正确的接口,必要时手动加路由。别一上来就怀疑板子坏了,多数连不上都是主机网络配置问题。
3.2 用 IIO 缓冲把 IQ 文件推上发射通道
连通之后,核心动作是把二进制 IQ 文件按块写入 tx 缓冲。pluto-gps-sim 生成的通常是交错的 int16 或 float32 复样本,写入前要确认格式与 Pluto 期望的一致。下面是一段典型的推流逻辑,用 libiio 的 C 接口示意,实际脚本语言不限。
/* 简化示意:把交错 IQ 文件按块写入 Pluto tx 缓冲 */ struct iio_context *ctx = iio_create_context_from_uri("ip:192.168.2.1"); struct iio_device *tx = iio_context_find_device(ctx, "cf-ad9361-dds-core-lpc"); struct iio_channel *ch_i = iio_device_find_channel(tx, "voltage0", true); struct iio_channel *ch_q = iio_device_find_channel(tx, "voltage1", true); iio_channel_enable(ch_i); iio_channel_enable(ch_q); struct iio_buffer *buf = iio_device_create_buffer(tx, 4096, false); /* 每次 4096 个复样本,循环直到文件读完 */ while (fread(iq_block, sizeof(int16_t), 2 * 4096, fp) == 2 * 4096) { /* 把交错 IQ 拆到 I/Q 两个通道的缓冲里 */ for (int n = 0; n < 4096; n++) { ((int16_t *)iio_buffer_first(buf, ch_i))[n] = iq_block[2 * n]; ((int16_t *)iio_buffer_first(buf, ch_q))[n] = iq_block[2 * n + 1]; } iio_buffer_push(buf); /* 阻塞式推送,节奏由 Pluto 的采样率决定 */ }iio_device_create_buffer的第二个参数是每块样本数,4096 是个折中值:太小则系统调用频繁、容易欠载,太大则首块延迟高、循环播放时衔接不上。iio_buffer_push是阻塞的,它的返回节奏由 Pluto 按采样率消费数据的速度决定,所以主机侧不需要额外限速。格式上,Pluto 的 tx 通道通常接受 16 位有符号整数,若生成的是 float32,要先做缩放和类型转换,缩放系数错了会表现为“信号幅度异常”或“接收端信噪比很差”。
推流时还要注意循环与单次发射的区别:做捕获测试通常需要信号持续若干秒,文件不够长就循环读,但循环点如果落在码周期中间,会在拼接处产生相位跳变,接收端跟踪环可能失锁。稳妥做法是让文件长度是 1 ms 码周期的整数倍,循环点自然对齐。
3.3 发射参数与接收端验证的闭环
发射侧还有两个参数要盯:中心频率和发射增益。中心频率必须设到 1575.42 MHz,设错频点接收机根本不会去看;发射增益从低往高调,先用较小值加衰减器,避免近距离直接灌饱和接收前端。验证闭环的做法是拿一台已知能工作的 GPS 接收机(或 SDR 接收加捕获脚本)放在近场,看能否在预期多普勒附近出现相关峰。
验证时先看捕获,再看跟踪。捕获阶段关注相关峰是否在正确码相位附近、峰值是否明显高于噪底;跟踪阶段关注载波环和码环是否稳定收敛。如果捕获有峰但跟踪失锁,多半是 IQ 文件里码相位不连续或采样率与描述文件不符;如果连峰都没有,回头查 PRN、频点和增益。这个闭环跑通一次,后面换场景只是改参数重生成,链路本身不用再动。
4. 避坑与排查:五个让信号“发了个寂寞”的典型问题
4.1 现象:接收端完全搜不到星,相关峰淹没在噪底
原因通常有三层:频点没设到 1575.42 MHz、PRN 与接收端搜索的卫星号不匹配、发射增益过低导致近场信噪比不够。解决顺序是先确认 Pluto 的 tx 本振频率读数,再核对生成时用的 PRN 和接收端配置是否一致,最后逐步提高增益并加合适衰减。别一上来就拉满增益,饱和后的互调产物会让频谱看起来“很热闹”但相关峰更差。
4.2 现象:能捕获但跟踪几秒就失锁
多数是 IQ 文件循环拼接处的相位跳变,或者文件长度不是 1 ms 的整数倍。检查生成脚本里每个码周期的采样点数是否为整数(采样率除以 1.023e6 要能整除或至少保证码相位连续),循环读文件时确认拼接点落在码周期边界。另一个可能是多普勒设得太大,超出了接收端载波环的牵引范围,把频偏调回 ±2 kHz 内再试。
4.3 现象:iio_buffer_push 报错或频繁欠载
块大小和主机吞吐不匹配是主因。4096 样本在 4.092 MHz 采样率下约 1 ms 的数据量,主机若同时在做重 IO 就容易来不及。解决是把块调大或降低采样率,并确认 IQ 文件在内存盘上而不是慢速外设。欠载的典型表现是发射信号出现周期性断裂,接收端表现为信噪比忽高忽低。
4.4 现象:信号幅度异常,接收端前端饱和
生成时归一化没做好,int16 满量程附近大量削顶,频谱上出现带外杂散。检查生成脚本的幅度缩放,留 3~6 dB 余量;发射链路里串固定衰减器,别靠软件把幅度压到极小来“凑”,那样量化噪声占比会上升。幅度和增益是一对,调的时候一次只动一个。
4.5 现象:换一台主机就连不上 Pluto
USB 网卡枚举的网段和主机已有网段冲突,或者防火墙拦了本地链路。排查用ip addr看是否拿到 192.168.2.x 地址,ip route看路由是否指向正确接口,必要时临时关掉冲突网段。这类问题跟信号生成无关,但会让人误以为“工具坏了”,先排除网络再怀疑代码。
5. 进阶:把单星信号扩成多星场景与可复现的验证习惯
单 PRN 跑通之后,真正有价值的是多星叠加。做法不复杂:对每个 PRN 独立生成一段 IQ,按复数逐点相加,再统一量化成 int16。叠加时每个星的幅度要按预期功率分配,别简单等幅相加,否则强星会把弱星的相关峰压下去。多普勒给不同值,码相位给不同初值,这样接收端才能同时捕获多颗并做定位解算。叠加后文件会变大,采样率和时长要提前算好主机内存和写盘速度。
| 场景 | PRN 数 | 采样率 | 时长 | 用途 |
|---|---|---|---|---|
| 单星捕获 | 1 | 4.092 MHz | 10 s | 验证链路与相关峰 |
| 多星定位 | 4~6 | 4.092 MHz | 30 s | 接收机定位解算 |
| 弱信号灵敏度 | 1 | 2.046 MHz | 60 s | 降幅度测捕获门限 |
| 动态多普勒 | 1~4 | 5.0 MHz | 20 s | 跟踪环动态应力测试 |
验证习惯上,我一般会在每次生成后先做三件事:打印文件头几个样本确认格式、用脚本算一下文件长度是否等于“采样率 × 时长 × 2 通道 × 字节数”、拿一段已知正确的短信号做回归对比。这三步花不了两分钟,但能挡掉大半“参数填错”的低级翻车。多星叠加时再加一步:单独发射每颗星确认都能被捕获,再叠加,出问题时能快速定位是哪颗星的参数错了。
从那以后我每次改完生成参数,都强制先跑一遍单星短文件的捕获回归,确认相关峰位置和幅度在预期范围内,再去做多星和长时长。这个习惯帮我省掉了不少“以为是算法问题、其实是文件拼错”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取