
简介面向GPS信号模拟与软件定义无线电学习者的PLUTO-GPS-SIM工程源码包基于C语言实现可生成GPS基带信号IQ数据流并借助ADALM-Pluto SDR平台在GPS L1频点发射。用户可通过命令行设定静态位置结合GPS广播星历文件brdc指定卫星星座进而计算视野内卫星的模拟伪距与多普勒最终输出数字化I/Q样本适合从事SDR实验、导航信号仿真与接收机验证的中高级开发者参考。资源包共14个文件约3.02MB包含C源码与头文件、Makefile构建脚本、说明文档、OCXO改装原理图PDF及实物照片、星历CSV样例等兼顾代码编译与硬件改造参考。已有2340人学习下载。读者可从中获得完整的信号生成流程实现、星历解析与伪距多普勒计算逻辑以及Pluto外部时钟改装的电路与图示思路便于复现实验并排查频率偏移与温漂问题。1. GPS 信号模拟为什么一段 IQ 数据流就能让接收机“相信”卫星在天上GPS 接收机本质上是个极度固执的听众。它不听你说什么只认码相位和载波相位对不对得上。只要基带信号的 C/A 码相位、多普勒频移、导航电文比特流三者严丝合缝接收机就会老老实实解出伪距、算出位置——哪怕这段信号来自一张桌子上的 SDR 板卡而不是两万公里外的卫星。pluto-gps-sim 这个项目做的正是这件事用 C 代码生成 GPS L1 基带信号的 IQ 数据流再交给 ADALM-Pluto 这类软件定义无线电平台发射出去。搞懂它的价值不在于“伪造 GPS”而在于你能亲手控制信号的每一个比特把接收机当成黑匣子去观察它在各种信号条件下的反应。这是做 GPS 基带算法验证、接收机灵敏度测试、抗干扰研究时绕不开的一环也是理解“伪距是怎么从码相位里长出来的”最直接的方式。2. 信号生成链路从导航电文到 IQ 采样点是怎么一步步算出来的GPS L1 信号的结构是分层的最底层是 10.23 MHz 的基准时钟分频出 1.023 MHz 的 C/A 码率和 1575.42 MHz 的载波C/A 码是 1023 个码片的 Gold 码每 1 ms 重复一次导航电文以 50 bps 的速率调制在 C/A 码上一个比特持续 20 ms也就是 20 个 C/A 码周期。pluto-gps-sim 的生成逻辑必须严格按这个层级往下推任何一层的相位或速率算错接收机就锁不上。2.1 码相位与载波相位的双累加器模型生成端最核心的数据结构是两个相位累加器一个管码相位一个管载波相位。码相位累加器以 1.023 MHz 的速率递增满 1023 归零归零的时刻就是 C/A 码周期的边界载波相位累加器以 1575.42 MHz 加上多普勒频移的速率递增用于查正弦/余弦表生成 I、Q 两路。常见的实现思路是用定点数做累加避免浮点在长时间运行时的精度漂移同时把查表索引控制在 2 的幂次长度上用位掩码代替取模。/* 双相位累加器的核心推进逻辑示意 */ #define CA_LEN 1023 #define CA_RATE_HZ 1023000.0 #define CARRIER_HZ 1575420000.0 #define TABLE_BITS 12 #define TABLE_SIZE (1 TABLE_BITS) static uint32_t ca_phase_acc; /* 码相位累加器单位码片的小数部分 */ static uint32_t carr_phase_acc; /* 载波相位累加器单位载波周期的分数 */ void advance_phase(double dt, double doppler_hz, double *ca_phase, double *carr_phase) { /* 码相位按 1.023 MHz 推进取小数部分保留精度 */ ca_phase_acc (uint32_t)(CA_RATE_HZ * dt * 4294967296.0); *ca_phase (double)ca_phase_acc / 4294967296.0 * CA_LEN; /* 载波相位基准频率叠加多普勒频移 */ carr_phase_acc (uint32_t)((CARRIER_HZ doppler_hz) * dt * 4294967296.0); *carr_phase (double)carr_phase_acc / 4294967296.0; }这段代码里dt是每个采样点的时间间隔由采样率决定比如 4 MHz 采样率下dt就是 250 ns。ca_phase_acc用 32 位无符号整数存小数部分相当于把 1 个码片切成 2^32 份精度足够跑几个小时不丢相位。carr_phase同理只是它的周期对应 1575.42 MHz。注意这里没有直接对 1023 取模而是先算出浮点码相位再在查表时取整这样做的原因是取模会丢掉小数部分导致码相位随时间累积误差——这是生成端最容易翻车的地方之一。2.2 导航电文的比特级拼装导航电文是 50 bps每比特 20 ms正好覆盖 20 个 C/A 码周期。生成端需要维护一个比特缓冲区按子帧结构TLM、HOW、星历/历书参数逐位填充然后在每个 C/A 码周期开始时判断当前处于比特的哪个位置决定这一毫秒输出的是 1 还是 -1。常见的实现是把电文先展开成一个比特数组再用一个计数器bit_index跟踪当前比特序号bit_index / 20定位到具体比特bit_index % 20定位到该比特内的第几个码周期。/* 导航电文比特流的取用逻辑示意 */ static uint8_t nav_bits[1500]; /* 一个子帧 300 比特按需扩展 */ static int bit_index; int get_nav_chip(void) { int bit_pos bit_index / 20; /* 当前处于第几个电文比特 */ int chip_in_bit bit_index % 20; /* 该比特内的第几个码周期 */ int bit_val (nav_bits[bit_pos / 8] (7 - (bit_pos % 8))) 1; bit_index (bit_index 1) % (300 * 20); /* 子帧循环 */ return bit_val ? 1 : -1; /* BPSK 映射1 - 1, 0 - -1 */ }这里bit_pos / 8和bit_pos % 8是把比特序号换算成字节内偏移高位在前符合 GPS 电文的传输顺序。chip_in_bit这个变量在当前实现里只用于调试打印实际调制时并不需要它参与运算因为电文比特在 20 ms 内保持不变直接乘到 C/A 码上即可。但保留它有个好处当你要做比特边沿对齐检查时这个计数器就是最直接的观测点。2.3 IQ 调制与采样率匹配C/A 码和电文比特相乘得到基带 BPSK 符号再乘以载波的正余弦得到 I、Q 两路。这里有个容易被忽略的细节采样率不一定等于码率的整数倍。比如 4 MHz 采样率下每个码片对应约 3.91 个采样点不是整数。生成端必须在每个采样点重新计算码相位和载波相位而不是按“每码片固定采样点数”去推否则码相位会以码片为步长跳变接收机的码环会看到明显的相位抖动。/* 单个采样点的 IQ 生成示意 */ void gen_sample(double ca_phase, double carr_phase, int nav_chip, double *i_out, double *q_out) { int ca_idx (int)ca_phase % CA_LEN; int ca_chip ca_code[ca_idx]; /* ±1 的 C/A 码片 */ double baseband (double)(ca_chip * nav_chip); int tbl_idx (int)(carr_phase * TABLE_SIZE) (TABLE_SIZE - 1); *i_out baseband * cos_table[tbl_idx]; *q_out baseband * sin_table[tbl_idx]; }ca_idx取整后对 1023 取模tbl_idx用位掩码截断到查表长度这两步都是为了让索引落在合法范围内。cos_table和sin_table是预生成的正余弦表长度 4096 足够覆盖一个载波周期查表比实时调sin()快一个数量级在嵌入式或低功耗平台上这是常规操作。输出幅度这里没有做归一化实际项目里通常会乘一个增益系数方便控制发射功率。3. 多普勒与伪距为什么“假卫星”也要算相对运动如果只是发一个静止的 C/A 码接收机大概率能锁上但解出来的位置会离谱到没法看。原因在于 GPS 接收机解算位置靠的是伪距而伪距来自码相位差乘以光速码相位差又依赖于卫星和接收机之间的相对运动产生的多普勒频移。生成端如果不模拟多普勒接收机的载波环会认为卫星静止伪距变化率为零定位解算直接退化。3.1 多普勒频移的计算模型多普勒频移的公式是f_d -(v_r / c) * f_c其中v_r是卫星与接收机的径向相对速度c是光速f_c是载波频率。生成端需要维护一个简化的卫星轨道模型比如圆轨道加倾角按时间推算出卫星位置再和预设的接收机位置做差投影到视线方向上得到径向速度。常见的实现思路是用一个低阶多项式拟合卫星位置避免在生成循环里跑完整的开普勒方程求解毕竟生成端对实时性有要求。/* 简化的多普勒计算示意 */ double compute_doppler(double t, const sat_state_t *sat, const vec3_t *rx_pos) { vec3_t sat_pos sat-pos0; vec3_t sat_vel sat-vel0; /* 一阶近似位置 初值 速度 * 时间 */ sat_pos.x sat_vel.x * t; sat_pos.y sat_vel.y * t; sat_pos.z sat_vel.z * t; vec3_t los vec3_sub(sat_pos, rx_pos); /* 视线向量 */ double range vec3_norm(los); double range_rate vec3_dot(sat_vel, los) / range; return -(range_rate / SPEED_OF_LIGHT) * CARRIER_HZ; }sat_state_t里存的是某一时刻的卫星位置和速度vec3_sub和vec3_dot是基础向量运算。这里用一阶近似是因为在几十秒的生成窗口内卫星轨道的曲率对多普勒的影响很小误差在赫兹量级接收机的载波环完全能跟踪。如果你要做长时间高精度模拟就得换成二阶甚至完整轨道模型但那样计算量会上去生成速度会掉——这是典型的精度和吞吐的取舍。3.2 伪距与码相位的对应关系伪距在生成端的体现是码相位的初始偏移。假设你要模拟某颗卫星距离接收机 20200 km对应的传播时延约 67.4 ms也就是 67.4 个 C/A 码周期。生成端在启动时把该卫星的码相位累加器预置一个偏移量让接收机在相关峰搜索时看到的就是这个偏移对应的码相位。多普勒则体现在载波相位累加器的频率上两者必须同步更新否则接收机解出的伪距变化率和多普勒对不上定位结果会跳。注意码相位偏移和多普勒频移必须来自同一个相对运动模型。如果码相位按静止算、多普勒按运动算接收机的码环和载波环会互相拉扯表现为锁定不稳定或者频繁失锁。3.3 多卫星信号的叠加真实场景下接收机同时看到多颗卫星生成端要把多路信号叠加到同一个 IQ 流里。每颗卫星有独立的码相位、多普勒和电文比特流叠加时直接逐采样点相加即可因为不同卫星的 C/A 码互相关很低叠加后不会破坏各自的相關峰。但要注意幅度控制如果某颗卫星的信号太强会把其他卫星的相关峰淹没接收机的捕获模块可能只锁到强信号。常见的做法是给每颗卫星分配一个增益系数模拟真实场景下的功率差异。/* 多卫星叠加示意 */ for (int s 0; s num_sats; s) { double ca_ph sat[s].ca_phase; double carr_ph sat[s].carr_phase; int nav get_nav_chip_for_sat(s); double i_s, q_s; gen_sample(ca_ph, carr_ph, nav, i_s, q_s); i_acc i_s * sat[s].gain; q_acc q_s * sat[s].gain; }gain这个系数在真实项目里通常按接收功率反推比如强信号给 1.0、弱信号给 0.1模拟 20 dB 的动态范围。叠加后的i_acc和q_acc再做限幅或归一化防止定点输出溢出。4. 避坑与排查生成端最容易翻车的几个地方4.1 码相位累积误差导致相关峰漂移现象是接收机刚开始能锁上跑几分钟后失锁重新捕获又正常。原因多半是码相位累加器用了浮点且没有做小数部分保留每次取整都丢一点时间一长相位就偏了几个码片。解决方法是像 2.1 节那样用定点累加器存小数部分只在查表时取整或者用双精度浮点并定期做相位归零校正。4.2 采样率与码率不整除引发的频谱杂散现象是发射出去的信号频谱上出现规律的杂散接收机的载噪比估计偏低。原因是生成端按“每码片固定采样点数”去推相位导致码相位以码片为步长跳变等效于对基带信号做了非均匀采样。解决方法是每个采样点独立计算相位不依赖码片边界对齐。4.3 电文比特边界与码周期错位现象是接收机能锁码但解不出电文或者电文误码率极高。原因是电文比特的 20 ms 边界没有和 C/A 码周期的 1 ms 边界对齐生成端在比特切换时多算或少算了一个码周期。排查时打印bit_index和码周期计数器的值确认两者在比特边界处同时归零。4.4 IQ 输出幅度溢出现象是发射信号看起来正常但接收机解出的信噪比忽高忽低。原因是多卫星叠加后 IQ 值超出定点范围被截断波形出现削顶。解决方法是在叠加后做归一化或者给每路信号预留足够的动态范围输出前检查最大绝对值是否接近满量程。4.5 多普勒更新频率过低现象是接收机载波环跟踪抖动大伪距变化率不平滑。原因是多普勒频移按秒级更新而载波相位按采样点推进两者时间粒度不匹配。解决方法是在每个采样点或每个码周期更新多普勒至少保证更新率高于载波环带宽的几倍。5. 设计取舍为什么生成端宁可“笨”一点也要保住相位精度pluto-gps-sim 这类工具的设计核心不是算法多精巧而是相位精度能不能在长时间运行下稳住。你会看到它在很多地方选择“笨”办法定点累加器、逐采样点重算相位、查表代替实时三角函数、一阶近似轨道模型。这些选择背后是同一条逻辑——生成端的任何微小误差都会被接收机的跟踪环放大最终表现为失锁或定位跳变。宁可多花一点 CPU也不能让相位漂。设计选择代价换来的收益定点相位累加器代码复杂度上升长时间运行无累积误差逐采样点重算相位计算量增加非整数采样比下码相位平滑查表生成正余弦内存占用生成吞吐提升一个数量级一阶轨道近似多普勒精度略降生成循环里不解开普勒方程验证生成端是否正确最直接的办法是把 IQ 流回灌给一个软件接收机比如开源的 GNSS-SDR 类实现看它能不能稳定锁定、解出的伪距变化率是否和多普勒一致。如果手头没有软件接收机也可以用频谱仪看 C/A 码的频谱包络是不是标准的 sinc 形状旁瓣位置对不对。我一般会在改动生成逻辑后先跑一段 10 秒的 IQ 数据用相关器扫一遍码相位确认相关峰位置和预设的伪距对得上再上真实接收机测试。血泪经验是不要跳过这一步直接接硬件否则失锁了你根本分不清是生成端算错了还是射频前端饱和了。从那以后我每次改相位推进逻辑都强制走一遍“短数据回灌 相关峰位置比对”确认无误再往下走。希望帮到你。本文还有配套的精品资源点击获取