1. 为什么我要从零搭一条 QPSK 收发链路
把一段 bit 流变成空口上的 QPSK 点簇,再在接收端把它还原回来,这件事听起来像是教科书里一章的内容,但真正动手做一遍,你会发现中间隔着一堆"文档里不会写、但不过就卡死"的细节。我这次用的硬件是 P201Pro(AD9361 射频前端方案),软件侧是 GNU Radio,目标很朴素:发端把一串已知的 bit 映射成 QPSK 符号、成型滤波、上变频发出去;收端做同步、匹配滤波、判决,最后在星座图上看到四个干净的点簇,最好还能顺手看一眼眼图。
为什么选 QPSK 作为切入点?因为它是一个"刚刚好"的调制方式。BPSK 太简单,看不出同步和频偏处理的必要性;16QAM 又太敏感,链路没调通之前你根本分不清是算法问题还是射频问题。QPSK 每个符号 2 bit,星座点少、判决门限清晰,一旦点簇糊成一团,你几乎可以立刻判断是频偏、相位噪声还是定时没对齐。对于第一次搭 SDR 收发链路的人来说,QPSK 是性价比最高的练手对象。
这篇总结面向的是已经摸过 GNU Radio、手里有一块 AD9361 类板子、但链路还没真正跑通的同好。我会把整条链路拆成几个阶段:先讲清楚 AD9361 初始化里那个让人头大的 0x247 寄存器问题,再讲发端链路怎么搭、收端同步怎么做,最后讲星座图和眼图怎么调出"能看"的效果。中间会穿插我自己踩过的坑,尤其是那些"寄存器读出来不对、PLL 没锁、CP OVRG 被置高"的排查过程——这些才是真正卡人的地方。
先说结论:这条链路能跑通,但前提是你得先把射频前端伺候好。软件侧的 GNU Radio 流图再漂亮,如果 AD9361 没正常初始化,你看到的星座图永远是一团噪声。所以我把射频初始化放在最前面讲,这不是跑题,这是整条链路的地基。
2. AD9361 初始化:0x247 读出 0x80 与 RX PLL 未锁的排查链路
2.1 现象描述:寄存器读回值不对,PLL 死活不锁
我第一次上电配置 AD9361 的时候,遇到的现象非常典型:初始化流程走到一半,读 0x247 寄存器,期望值应该是某个配置后的状态,结果一直读出 0x80。同时伴随两个症状——CP OVRG(Charge Pump Overrange)被置高,RX PLL 没有锁定。这三个现象是连锁的,不是独立问题。
0x247 这个寄存器在 AD9361 的寄存器映射里属于 RX 相关配置区,读回 0x80 意味着某一位被强制置位,而这个位通常和校准状态或者 PLL 状态相关。CP OVRG 置高说明电荷泵的输出超出了正常工作范围,这直接导致 PLL 环路无法锁定。PLL 不锁,RX 链路就没有本振,后面的一切都无从谈起。
很多人遇到这个问题的第一反应是"是不是我寄存器配置顺序错了",然后开始翻手册逐条对照。这个方向没错,但效率很低。我的建议是先建立一个排查链路,从电源和参考时钟开始,逐级往上查,而不是一上来就怀疑配置序列。
2.2 根因定位:从参考时钟到电荷泵的逐级排查
我当时的排查顺序是这样的,你可以直接复用:
- 参考时钟检查:AD9361 的参考时钟输入是否稳定、频率是否正确。用示波器或者频谱仪看一眼,确认没有停振或者频率偏移。参考时钟不对,PLL 的鉴相器就没有正确的比较基准,电荷泵自然会过载。
- 电源域检查:AD9361 有多个电源域,尤其是 VCO 和电荷泵相关的供电。如果某一路电源纹波过大或者电压偏低,电荷泵的工作点就会偏移,CP OVRG 被置高是必然的。
- SPI 通信检查:确认寄存器写入是否真正生效。有时候 SPI 时序不对,你写进去的值根本没进芯片,读回来的自然是默认值或者错误值。0x247 读出 0x80 有可能就是写入没生效,芯片保持在一个中间状态。
- 初始化序列检查:AD9361 的初始化有严格的顺序要求,尤其是 PLL 使能、校准触发、滤波器配置这几步。顺序错了,PLL 可能在一个未校准的状态下被使能,直接导致不锁。
我最后定位到的根因是参考时钟的驱动能力不足,导致 PLL 鉴相器输入信号幅度偏低,电荷泵为了补偿这个偏差而进入过载状态。换了一个驱动能力更强的时钟源之后,CP OVRG 不再置高,RX PLL 顺利锁定,0x247 的读回值也恢复正常。
这里有个经验:AD9361 的很多"寄存器问题"其实是模拟前端问题。数字侧读出来的异常值,往往是模拟侧工作点不对的映射。所以排查的时候不要只盯着寄存器手册,要抬头看看射频和电源。
2.3 初始化顺序里最容易搞反的几步
AD9361 的初始化序列很长,但有几步的顺序特别容易搞反,我列出来给你提个醒:
- 先配置参考时钟和电源相关寄存器,再使能 PLL。如果先使能 PLL 再配时钟,PLL 会在一个错误的参考下尝试锁定,大概率失败。
- 先触发校准,再使能 RX/TX 通道。校准需要在通道关闭的状态下进行,通道开着的时候校准结果会被干扰。
- 滤波器配置要在 PLL 锁定之后。因为滤波器的时钟来自 PLL,PLL 没锁的时候配滤波器,时钟频率不对,滤波器系数加载会出错。
提示:如果你用的是现成的驱动或者 GNU Radio 的 AD9361 模块,这些顺序通常已经封装好了。但如果你是自己写初始化代码,这几步一定要按顺序来。我见过太多人因为顺序问题卡了好几天。
2.4 一个可复用的初始化自检清单
为了避免每次上电都重新排查一遍,我整理了一个自检清单,初始化之后逐项确认:
| 检查项 | 正常表现 | 异常时的可能原因 |
|---|---|---|
| 参考时钟频率 | 与配置值一致,幅度足够 | 时钟源驱动不足、停振 |
| 电源电压 | 各域电压在手册范围内 | 电源纹波大、电压偏低 |
| SPI 回读 | 写入值与回读值一致 | SPI 时序错误、片选问题 |
| CP OVRG | 未置高 | 电荷泵过载,查参考时钟和电源 |
| RX PLL 锁定状态 | 锁定位置位 | 参考时钟、环路滤波器参数 |
| 0x247 读回值 | 与预期一致 | 上述任一环节异常都可能导致 |
这个清单我每次换板子或者改配置都会跑一遍,能省下大量瞎猜的时间。
3. 发端链路:从 bit 流到 QPSK 符号的 GNU Radio 实现
3.1 流图整体结构与我为什么这样分层
发端的 GNU Radio 流图我分成了四层:信源层、映射层、成型层、射频层。这样分层的好处是每一层都可以单独测试,出问题的时候能快速定位是哪一层的锅。
信源层就是一个简单的随机 bit 源或者文件源,产生 0/1 序列。映射层把每 2 个 bit 映射成一个 QPSK 符号,映射规则我用的是格雷码,因为格雷码相邻星座点只差一个 bit,误判的时候误码率最低。成型层做脉冲成型,我用的是根升余弦(RRC)滤波器,滚降系数 0.35。射频层就是把基带信号送到 AD9361 的 TX 通道。
为什么用 RRC 而不是直接矩形脉冲?因为矩形脉冲的频谱是 sinc 函数,旁瓣衰减慢,带外泄漏严重,实际空口上会干扰相邻信道。RRC 能在保证无码间干扰的前提下压缩频谱,是 SDR 链路里的标准做法。滚降系数 0.35 是一个折中:太小了频谱效率高但实现难度大,太大了频谱浪费。0.35 是工程上常用的值。
3.2 QPSK 映射与格雷码的实操细节
QPSK 映射本身很简单,四个星座点,每个点对应 2 bit。但映射规则有讲究。我用的是这样的映射:
- 00 → (1+j)/√2
- 01 → (-1+j)/√2
- 11 → (-1-j)/√2
- 10 → (1-j)/√2
注意这里的 √2 归一化,是为了让平均符号功率为 1。如果不归一化,后续的增益控制会很难做。
格雷码的好处在这个映射里体现得很明显:相邻星座点之间只有 1 bit 不同。比如 00 和 01 相邻,只差最低位;01 和 11 相邻,只差最高位。这样在接收端判决的时候,即使因为噪声判到了相邻点,也只会错 1 个 bit,而不是 2 个 bit。
在 GNU Radio 里,你可以用Constellation QPSK模块直接做映射,也可以自己写 Python Block。我建议先用现成模块跑通,再考虑自己实现,因为现成模块的归一化和映射规则已经调好了,省得你在细节上浪费时间。
3.3 RRC 成型滤波器的参数计算与实测调整
RRC 滤波器的关键参数有两个:滚降系数和符号跨度(span)。滚降系数我选了 0.35,符号跨度我选了 8 个符号。这两个参数决定了滤波器的阶数和计算量。
滤波器的阶数计算公式是:阶数 = span × samples_per_symbol。假设每个符号 4 个采样点,span 是 8,那阶数就是 32。这个阶数在 GNU Radio 里跑起来毫无压力,但如果你把 samples_per_symbol 提到 16,阶数就变成 128,计算量会明显上升。
实测下来,samples_per_symbol 选 4 到 8 之间比较合适。太少了波形不够平滑,成型效果差;太多了计算量大,而且对实际性能提升有限。我用的是 4,配合 span=8,在频谱仪上看带外抑制已经足够好了。
注意:RRC 滤波器在发端和收端要成对使用。发端用一个 RRC,收端再用一个匹配的 RRC,两者卷积起来就是一个完整的升余弦响应,这样才能实现无码间干扰。如果你只在发端做 RRC,收端不做匹配滤波,那接收端的符号间干扰会很严重。
3.4 送数到 AD9361 TX 通道时的增益与采样率匹配
发端最后一步是把基带信号送到 AD9361 的 TX 通道。这里有两个参数要匹配:采样率和增益。
采样率方面,AD9361 的 TX 采样率要和 GNU Radio 流图的输出采样率一致。如果不一致,要么在流图里做重采样,要么在 AD9361 里配置对应的采样率。我一般是在 GNU Radio 里把采样率设成 AD9361 支持的某个值,比如 1.92 MSPS 或者 3.84 MSPS,这样不用额外做重采样。
增益方面,TX 增益不能设太大,否则信号会失真,星座图会发散;也不能太小,否则空口上信噪比不够。我的做法是先用一个较小的增益(比如 -10 dB),在频谱仪上看输出功率,然后逐步增加,直到输出功率合适且星座图不发散。这个过程需要反复试,没有一劳永逸的值,因为不同的板子、不同的天线、不同的距离都不一样。
4. 收端同步:让 QPSK 点簇从一团糊变成四个点
4.1 接收链路的分层与同步顺序
收端比发端复杂得多,因为你要处理的是"未知"的信号。收端链路我分成五层:射频接收层、下变频层、匹配滤波层、同步层、判决层。
同步层是核心,它又包含三个子步骤:粗频偏估计与校正、细频偏估计与校正、定时同步。这三个步骤的顺序不能乱:先粗频偏,再细频偏,最后定时。因为频偏会影响定时误差的估计,如果先做定时再做频偏,定时结果会被频偏带偏。
为什么频偏这么重要?因为 AD9361 的收发本振是独立的,两个本振之间总有频率偏差,再加上多普勒效应,接收到的信号中心频率和发端不完全一致。QPSK 对频偏很敏感,哪怕只有几度的相位旋转,星座点就会开始糊。所以频偏校正是收端同步的第一要务。
4.2 粗频偏估计:用 FFT 找能量峰值
粗频偏估计我用的是 FFT 方法。原理很简单:把接收到的信号做 FFT,找到能量峰值的位置,和理论上的中心频率比较,差值就是粗频偏。
具体操作上,我在 GNU Radio 里用一个FFT Sink观察接收信号的频谱,手动找峰值,然后算出差值。这个方法比较笨,但胜在直观,适合第一次调链路的时候用。等链路跑通了,可以换成自动的频偏估计模块,比如Polyphase Clock Sync配合Costas Loop。
粗频偏的估计精度不需要太高,因为它只是给细频偏提供一个初始值。一般粗频偏能把偏差压到几百 Hz 以内,细频偏再把它压到几 Hz 以内,就足够 QPSK 判决了。
4.3 细频偏校正:Costas 环的参数整定
细频偏校正我用的是 Costas 环。Costas 环是一个锁相环,专门用于抑制载波调制信号的载波恢复。它的关键参数是环路带宽和阻尼系数。
环路带宽决定了 Costas 环跟踪频偏的速度和精度。带宽太宽,跟踪快但噪声大;带宽太窄,噪声小但跟踪慢,而且可能锁不住。我一般从环路带宽等于符号率的 1% 开始试,比如符号率是 1 MSPS,环路带宽就从 10 kHz 开始,然后根据星座图的收敛情况调整。
阻尼系数一般取 0.707,这是巴特沃斯响应的标准值,能在跟踪速度和稳定性之间取得比较好的平衡。如果你发现星座图在收敛过程中振荡,可以适当增大阻尼系数;如果收敛太慢,可以适当减小。
提示:Costas 环的整定是个细活,不要指望一次就调好。我的经验是先把环路带宽设大一点,让星座图先收敛,然后再逐步减小带宽,观察星座点的聚拢程度。每次调整后等几秒钟,让环路稳定下来再看效果。
4.4 定时同步:为什么 Gardner 算法在 QPSK 上很好用
定时同步我用的是 Gardner 算法。这个算法在 QPSK 上特别好用,因为它不需要知道载波相位,对频偏和相位偏移不敏感,可以在载波恢复之前或者同时进行。
Gardner 算法的核心思想是利用相邻符号的过零点来估计定时误差。具体来说,它计算当前符号和上一个符号的差值,乘以中间采样点的值,得到定时误差信号。这个误差信号经过环路滤波器后,控制采样时刻的调整。
在 GNU Radio 里,Polyphase Clock Sync模块内部就实现了 Gardner 或者类似的定时同步算法。你只需要设置好samples per symbol和环路带宽两个参数。samples per symbol 要和发端一致,环路带宽一般取符号率的 1% 到 5% 之间。
实测下来,Gardner 算法在 QPSK 上的收敛速度很快,一般几百个符号之内就能锁定。锁定之后,星座图上的四个点簇会明显聚拢,眼图也会张开。
5. 星座图与眼图:怎么判断链路是真的通了
5.1 星座图"能看"和"好看"之间的差距
星座图能看出四个点簇,说明链路基本通了。但"能看"和"好看"之间差距很大。能看的星座图,四个点簇是糊的,点与点之间有重叠;好看的星座图,四个点簇是紧致的,点与点之间界限分明。
影响星座图质量的因素很多:频偏残余、相位噪声、定时误差、信噪比、非线性失真。排查的时候要一个一个排除。我的做法是先看星座图的整体形状:如果四个点簇是旋转的,说明有残余频偏;如果点簇是发散的,说明信噪比不够或者有非线性失真;如果点簇是椭圆形的,说明定时同步没做好。
5.2 眼图张不开的三种典型原因
眼图是判断符号间干扰的直观工具。眼图张得越开,符号间干扰越小,判决越可靠。眼图张不开,一般有三种原因:
- 成型滤波器不匹配:发端和收端的 RRC 滤波器参数不一致,导致整体响应不是升余弦,符号间干扰严重。检查两端的滚降系数和 span 是否一致。
- 定时同步没锁:采样时刻没有对准符号的最佳采样点,眼图自然张不开。检查定时同步模块的环路带宽和收敛状态。
- 频偏残余太大:频偏会导致星座点旋转,眼图上的线条会变粗,看起来像张不开。检查 Costas 环的锁定状态。
我遇到过一次眼图死活张不开的情况,最后发现是收端的 RRC 滤波器滚降系数设成了 0.5,而发端是 0.35。改成一致之后,眼图立刻张开了。这种低级错误在调试初期很常见,所以每次改参数都要两边一起改。
5.3 从点簇质量反推链路问题的排查表
我把常见的星座图问题和对应的排查方向整理成了一张表,你可以直接对照:
| 星座图表现 | 可能原因 | 排查方向 |
|---|---|---|
| 四个点簇旋转 | 残余频偏 | Costas 环带宽、粗频偏估计 |
| 点簇发散、模糊 | 信噪比不足、非线性失真 | TX 增益、接收增益、滤波器 |
| 点簇呈椭圆 | 定时同步未锁 | Gardner 环路带宽、samples per symbol |
| 点簇偏移中心 | 直流偏置 | 基带直流消除、AD9361 配置 |
| 只有两个点簇 | 映射错误 | QPSK 映射规则、格雷码 |
| 点簇完全糊成一团 | 频偏太大、PLL 未锁 | 射频初始化、参考时钟 |
这张表是我调试过程中慢慢总结出来的,每次遇到问题先查表,能省不少时间。
6. 几个让我卡了最久的坑与最终解法
6.1 采样率不匹配导致的"幽灵"符号间干扰
这个坑我卡了整整两天。现象是星座图上的点簇总是有点糊,但眼图看起来又还行,信噪比也不算差。我一开始怀疑是频偏没校正好,反复调 Costas 环,没用。后来怀疑是滤波器问题,把 RRC 参数翻来覆去改,也没用。
最后发现是发端和收端的采样率不严格一致。发端 GNU Radio 流图的采样率设的是 1.92 MSPS,但 AD9361 实际工作的采样率因为时钟分频的关系,是 1.92 MSPS 的一个近似值,两者之间有微小的偏差。这个偏差在短时间内看不出来,但累积起来就会导致符号定时漂移,产生"幽灵"符号间干扰。
解法是让 GNU Radio 流图的采样率和 AD9361 的实际采样率严格对齐。具体做法是查 AD9361 的时钟配置,算出实际采样率,然后在 GNU Radio 里设成同一个值。如果实在对不齐,可以在收端加一个重采样模块,把采样率校正过来。
6.2 直流偏置怎么悄悄毁掉你的星座图
直流偏置是另一个隐蔽的坑。AD9361 的零中频架构天然会有直流偏置,如果不在基带消除,星座图会整体偏移中心。偏移不大的时候,你可能只觉得星座图"有点歪",但判决的时候误码率会明显上升。
我的解法是在收端加一个直流消除模块。GNU Radio 里有现成的DC Blocker模块,直接拖进去就行。参数方面,截止频率设得低一点,比如符号率的 0.1%,这样既能消除直流,又不会影响有用的低频分量。
注意:直流消除要在匹配滤波之前做,否则滤波器会把直流偏置扩散到整个频带,更难消除。
6.3 射频增益分配:发端太大和收端太大都不行
增益分配是个经验活。发端增益太大,信号会进入 AD9361 的非线性区,星座图会发散;收端增益太大,噪声也会被放大,信噪比反而下降。我一开始把发端增益设得很大,想着"信号强一点总没错",结果星座图糊得一塌糊涂。后来把发端增益降下来,收端增益适当提高,星座图立刻清晰了。
我的经验是:发端增益以输出信号刚好不进入非线性区为准,收端增益以接收信号幅度接近 ADC 满量程的 70% 到 80% 为准。这个比例能在保证信噪比的同时留出足够的余量,避免削顶。
6.4 从"能跑"到"跑得稳"的最后一步
链路能跑通之后,下一步是让它跑得稳。我做了三件事:
- 加自动增益控制(AGC):手动设增益在实验室里没问题,但实际环境里信号强度会变,AGC 能自动调整增益,保持接收信号幅度稳定。
- 加频偏跟踪:粗频偏和细频偏只能校正静态频偏,如果收发本振有漂移,还需要频偏跟踪。Costas 环本身有一定的跟踪能力,但如果漂移太大,需要额外的跟踪环路。
- 加误码率统计:在收端加一个误码率统计模块,实时监控链路质量。误码率突然上升的时候,能第一时间发现,而不是等到星座图糊了才反应过来。
这三件事做完之后,链路才算真正"跑得稳"。我在实际测试中连续跑了几个小时,误码率一直保持在 1e-5 以下,星座图也没有明显退化。
7. 写在最后:一些个人体会
这条链路从零到跑通,前后花了大概两周时间,其中大部分时间花在射频初始化和同步调试上。如果让我给后来者一句建议,那就是:先把射频前端伺候好,再碰基带算法。我见过太多人一上来就调 Costas 环、调 Gardner,结果射频根本没初始化好,怎么调都是白费。
另外,GNU Radio 的流图不要一次搭完,要分层搭、分层测。发端先单独测,用频谱仪看输出;收端先单独测,用信号源灌已知信号;两端都单独测通了,再连起来联调。这样出问题的时候,你能快速定位是哪一端的锅。
最后,星座图和眼图是两条链路的"体检报告",但不要只盯着它们看。频谱、时域波形、误码率,这些都要结合起来看。有时候星座图看着还行,但误码率很高,那说明问题在判决逻辑或者编码上,而不是在射频或者同步上。
这套东西后续还可以扩展,比如换成 16QAM 看看高阶调制的同步难度,或者加个信道编码看看误码率能降到多少。但那是下一步的事了,先把 QPSK 这条链路跑稳,比什么都重要。