1. 为什么我要从零搭一条QPSK收发链路
手里这块P201Pro放了快两个月,一直只是拿它当频谱仪用,看看周围基站的下行信号、测测天线驻波,说实话有点浪费。P201Pro的核心是AD9361射频捷变收发器,这是一颗覆盖70MHz到6GHz的宽带收发芯片,通道带宽可以做到56MHz,支持2x2 MIMO收发。拿它只当频谱仪,相当于买了台工作站只用来扫雷。
真正让我下决心动手的,是想把"bit流"到"QPSK点簇"这条链路完整走一遍。很多做SDR的朋友都有类似经历:GNU Radio里拖几个模块,跑个QPSK例子,看到星座图就收工了。但真到自己设计帧结构、处理定时同步、观察EVM随功率变化的时候,才发现中间每一步都有大量细节被例程掩盖了。我这次的目标很明确——不追求高速率,不追求远距离,只求把发射端从比特到基带波形、接收端从采样到判决这条链路彻底打通,并且能定量地看到每个环节的质量指标。
关键词里出现的AD9361、GNU Radio、QPSK、眼图、点簇,其实正好对应了这条链路的几个关键节点。AD9361负责射频前端和数字接口,GNU Radio负责基带信号处理,QPSK是调制方式,眼图和星座图是验证手段。我打算按"发射链路搭建→接收链路搭建→同步与判决→质量评估"这个顺序来写,中间会穿插大量我在调试过程中踩过的坑和实测数据。
这篇文章适合谁看?如果你已经装好了GNU Radio,手里有一块类似P201Pro的SDR板卡,想从"跑通例程"进阶到"自己设计链路",那这篇内容应该能帮你省下不少时间。如果你还没入门,建议先去看看GNU Radio官方教程里的QPSK例子,把基本模块认全了再回来。
2. 发射链路:从比特到基带波形的完整构造
2.1 帧结构设计——为什么我不直接用随机比特
GNU Radio自带的QPSK例程通常直接送随机比特进调制器,这样做的好处是简单,坏处是你根本不知道接收端解出来的对不对。我在发射端加了一个固定的前导序列,长度64比特,内容是一个伪随机序列(PN序列),接收端用同样的序列做互相关,这样就能在没有任何外部同步信号的情况下找到帧头。
帧结构我设计成这样:前导序列64比特 + 帧长度字段16比特 + 有效载荷512比特 + CRC校验16比特。总长608比特,QPSK调制后是304个符号。为什么选这个长度?因为P201Pro在采样率1MSps、QPSK符号率500kSps的条件下,一帧的持续时间大约是608微秒,这个时间足够我做一次完整的接收处理,又不会让缓冲区太大导致实时性变差。
前导序列的选择有讲究。我一开始用的是全1序列,结果接收端互相关峰值虽然高,但旁边有几个副峰也很高,导致定时同步经常锁到错误的位置。后来换成m序列(最长线性反馈移位寄存器序列),它的自相关特性是旁瓣很低,主瓣很尖,定时同步的准确率立刻上去了。这个经验在通信系统里是常识,但自己踩一遍印象更深。
2.2 AD9361的profile配置——采样率和滤波器的取舍
P201Pro通过AD9361的digital interface和FPGA通信,GNU Radio这边看到的是一个UHD设备。在配置AD9361的时候,有几个参数必须搞清楚:采样率、射频带宽、FIR滤波器抽头系数。
采样率我设的是1MSps,这个值不是随便选的。AD9361的ADC是12位,最高采样率可以到61.44MSps,但采样率越高,数据量越大,USB或者以太网的传输压力就越大。P201Pro通过USB 3.0连接主机,理论上能跑几十MSps,但GNU Radio的处理能力有限,1MSps是一个比较稳妥的起点。等链路跑通了,再往上加。
射频带宽我设的是2MHz,比采样率略大。为什么?因为AD9361内部的模拟滤波器有滚降,如果射频带宽设得和采样率一样,带外衰减不够,会有混叠。设成2倍采样率是一个经验值,既能保证带内平坦,又能把带外噪声压下去。
AD9361的FIR滤波器我用了默认的128抽头配置,但后来发现默认配置的通带边缘在0.4倍采样率左右,对于QPSK来说,0.4倍采样率意味着符号率只能到0.4MSps,我设的500kSps已经接近边缘了。于是我用ADI的滤波器设计工具重新生成了一组抽头系数,把通带扩展到0.45倍采样率,阻带从0.55倍采样率开始。改完之后,星座图的EVM从大约8%降到了5%左右,效果很明显。
注意:AD9361的FIR滤波器抽头系数是定点数,格式是16位有符号整数,增益要归一化到0.5左右,否则容易溢出。我第一次改的时候没注意增益,结果发射出来的信号直接削顶了,星座图变成四个方块。
2.3 GNU Radio发射流图——模块顺序和参数设置
发射流图我用了这些模块:Random Source → Packed to Unpacked → 前导序列插入(用Vector Source和Stream Mux实现)→ QPSK Modulator → Root Raised Cosine Filter → UHD Sink。
QPSK Modulator的差分编码我开了。为什么?因为接收端如果相位模糊,差分编码可以解决90度、180度、270度的相位不确定性。代价是误码率会稍微高一点,但在信噪比足够的情况下,这点代价换来的是同步的鲁棒性,很划算。
Root Raised Cosine Filter的滚降系数我设的是0.35。这个值越小,频谱效率越高,但定时同步越难;越大,频谱越宽,但定时同步越容易。0.35是一个折中值,在通信系统里很常见。滤波器的抽头数我设的是11,每符号采样数设的是2,也就是2倍过采样。为什么用2倍而不是4倍?因为P201Pro的采样率是1MSps,符号率500kSps,2倍过采样正好匹配。如果用4倍过采样,符号率就得降到250kSps,速率太低了。
UHD Sink的center frequency我设的是915MHz,这是一个ISM频段,适合短距离实验。增益我一开始设的是30dB,后来发现接收端离得近的时候信号太强,ADC饱和了,星座图糊成一团。后来降到20dB,接收端离发射端大约1米,信号强度刚好。
3. 接收链路:从采样到判决的每一步
3.1 接收流图概览——为什么顺序不能乱
接收流图比发射流图复杂得多,因为要处理同步、信道估计、相位校正这些事。我的接收流图顺序是:UHD Source → Root Raised Cosine Filter → AGC → Symbol Sync → Costas Loop → Constellation Decoder → Differential Decoder → 帧同步 → CRC校验。
这个顺序不能乱。RRC滤波器必须放在最前面,因为它是匹配滤波器,负责把发射端的脉冲成形去掉,同时最大化信噪比。AGC放在RRC之后,因为RRC滤波会改变信号幅度,先滤波再AGC才能保证幅度稳定。Symbol Sync负责定时同步,它需要看到滤波后的信号才能准确找到符号边界。Costas Loop负责载波同步,它需要看到定时同步后的符号才能准确估计相位偏差。
我一开始把Costas Loop放在了Symbol Sync前面,结果Costas Loop根本锁不住,因为符号边界还没对齐,相位误差一直在跳。后来调换顺序,Costas Loop立刻就锁上了。这个坑很典型,很多新手都会犯。
3.2 Symbol Sync的调试——定时误差检测器的选择
GNU Radio的Symbol Sync模块里有几种定时误差检测器:Gardner、Mueller and Muller、Zero Crossing。我一开始用的是Mueller and Muller,因为它的计算量小,但实测下来在QPSK上抖动比较大,星座图的点簇比较散。后来换成Gardner,抖动明显减小,星座图的点簇更紧凑了。
Gardner检测器的原理是利用相邻符号的过零点来估计定时误差,它对载波相位偏差不敏感,所以适合放在Costas Loop之前。Mueller and Muller检测器对载波相位偏差敏感,如果载波没同步好,它的性能会急剧下降。这就是为什么在QPSK接收链路里,Gardner是更稳妥的选择。
Symbol Sync的环路带宽我设的是0.01,阻尼系数设的是1.0。环路带宽越小,定时同步越稳,但锁定时间越长。0.01是一个比较保守的值,锁定时间大约几百个符号,对于我的帧长度来说可以接受。如果帧很短,比如只有几十个符号,那就得把环路带宽调大,否则还没锁上帧就结束了。
3.3 Costas Loop的相位校正——为什么QPSK需要它
QPSK的载波同步和BPSK不一样。BPSK的Costas Loop只需要处理180度的相位模糊,QPSK需要处理90度、180度、270度四种情况。GNU Radio的Costas Loop模块有一个order参数,QPSK要设成4,BPSK设成2。
Costas Loop的环路带宽我设的是0.005,比Symbol Sync的带宽小一个数量级。为什么?因为载波同步的环路带宽通常要比定时同步的窄,否则两个环路会互相干扰。如果两个环路的带宽太接近,会出现一种叫"环路拉扯"的现象,两个环路都在抢着调整,结果谁也锁不住。
实测下来,Costas Loop的锁定时间大约在1000个符号左右。我的帧长度是304个符号,也就是说第一帧基本上是用来锁定的,从第二帧开始才能正确解调。这也是为什么我在发射端连续发送多帧,而不是只发一帧。接收端从第二帧开始做帧同步和CRC校验,成功率就很高了。
4. 星座图与眼图:怎么用它们判断链路质量
4.1 星座图点簇的四种典型形态
星座图是QPSK链路最直观的调试工具。我总结下来,点簇的形态基本上能反映链路的问题所在。
第一种是"四个清晰的点",这是理想情况,EVM在5%以下,说明定时同步、载波同步、信道均衡都做得很好。
第二种是"四个模糊的团",点簇散开但中心位置正确,这通常是噪声太大或者AGC没调好。我遇到过一次,接收端离发射端太远,信号衰减太大,AGC把增益拉到最大,结果噪声也被放大了,星座图变成四个大团。后来把发射功率调高,点簇立刻收紧了。
第三种是"旋转的弧线",点簇沿着圆周方向散开,这是载波相位没锁好。Costas Loop的环路带宽太小或者太大都会导致这个问题。带宽太小,相位跟踪不上;带宽太大,相位抖动太大。
第四种是"四个方块",点簇被削平了,这是ADC饱和或者DAC溢出。发射端增益太高或者接收端增益太高都会导致这个问题。我第一次调的时候,发射增益设了40dB,接收端离得只有30厘米,星座图直接变成四个方块,后来把发射增益降到20dB才恢复正常。
4.2 眼图怎么看——从张开度到交叉点
眼图是评估定时同步质量的好工具。GNU Radio里可以用Time Sink配合触发来观察眼图,但更方便的是用QT GUI Time Sink的触发模式,把触发点设在符号边界上。
眼图的张开度越大,说明定时同步越好,符号间干扰越小。如果眼图几乎闭合,说明定时同步没做好,或者RRC滤波器的滚降系数太小。
眼图的交叉点位置也很重要。理想情况下,交叉点应该在眼图的正中间,也就是符号边界上。如果交叉点偏左或者偏右,说明定时同步有固定偏差。这个偏差可以用Symbol Sync模块的"Loop Bandwidth"参数来微调,但更根本的办法是检查发射端和接收端的采样率是否严格一致。如果发射端采样率是1MSps,接收端也是1MSps,但两个时钟源不同步,就会产生慢漂移,眼图的交叉点会慢慢移动。
我实测下来,P201Pro的收发时钟是同一个时钟源,所以不存在这个问题。但如果你用两块独立的SDR板卡,一个发一个收,那就必须考虑时钟同步,否则眼图会一直漂。
4.3 EVM的定量测量——从星座图到数字
EVM(误差向量幅度)是衡量调制质量的核心指标。GNU Radio本身没有现成的EVM计算模块,我用Python写了一个简单的EVM计算脚本,从Constellation Decoder的输出里取符号,和理想星座点比较,算均方根误差。
实测数据:发射增益20dB,接收端距离1米,EVM大约在4.5%到5.5%之间波动。把发射增益降到15dB,EVM降到4%左右,但信号强度也降了,帧同步的成功率从99%降到95%。把发射增益升到25dB,EVM升到7%左右,帧同步成功率反而降到90%,因为ADC开始饱和了。
这个数据说明一个问题:EVM不是越小越好,而是要在帧同步成功率和解调质量之间找平衡。对于QPSK来说,EVM在5%到8%之间都是可以接受的,误码率在10^-3到10^-5之间。如果要求更低的误码率,那就得用信道编码,比如卷积码或者LDPC码。
5. 踩过的坑与排查过程实录
5.1 帧同步一直失败——从互相关到采样偏差
最开始的时候,帧同步的成功率只有50%左右,而且经常锁到错误的位置。我一开始以为是前导序列太短,把64比特加长到128比特,结果成功率只提高到60%,没有根本改善。
后来我用File Sink把接收到的基带信号存下来,在Python里做互相关分析。发现互相关的峰值虽然明显,但峰值的位置在每次运行的时候都不一样,有时候偏左有时候偏右。这说明问题不在前导序列本身,而在采样定时上。
进一步检查发现,Symbol Sync模块的输出符号率虽然是500kSps,但符号边界和实际的最佳采样点之间有偏差。这个偏差在GNU Radio的Symbol Sync模块里可以通过"Loop Bandwidth"来调整,但更根本的原因是发射端和接收端的RRC滤波器群延迟不一致。发射端的RRC滤波器抽头数是11,接收端也是11,理论上群延迟应该一样,但GNU Radio的Filter模块在处理的时候会有额外的缓冲延迟。
解决办法是在接收端加了一个Delay模块,延迟量手动调整,直到互相关峰值稳定在同一个位置。这个延迟量大约是3个采样点,对应1.5个符号周期。加了这个延迟之后,帧同步成功率直接跳到99%以上。
5.2 星座图周期性旋转——Costas Loop的极性反转
有一段时间,星座图每隔几秒钟就会旋转90度,然后过几秒又转回来。这个现象很有规律,不是随机噪声导致的。
我查了Costas Loop的文档,发现它有一个"Polarity"参数,用来控制相位误差的符号。如果极性设反了,Costas Loop会往错误的方向调整相位,导致相位一直转圈。我把极性从"Normal"改成"Negative",旋转立刻停止了。
这个坑的教训是:Costas Loop的极性取决于你的信号定义。如果你的QPSK映射是"00→1+j, 01→-1+j, 10→-1-j, 11→1-j",那极性和标准映射可能不一样。GNU Radio的QPSK Modulator默认用的是Gray映射,但具体相位顺序可能和你的预期不同。最好的办法是先用一个已知的简单信号(比如全1序列)测试,看Costas Loop能不能锁住,再上复杂的帧结构。
5.3 AD9361的直流偏移——为什么星座图中心有个洞
AD9361是零中频架构,零中频的一个固有问题是直流偏移。直流偏移会在星座图的中心产生一个亮点,如果偏移太大,甚至会淹没中心附近的星座点。
我一开始没注意这个问题,因为QPSK的星座点不在中心,而在四个象限。但后来发现,当信号功率比较小的时候,直流偏移会导致AGC误判,把增益拉得过高,结果噪声也被放大。解决办法是在AD9361的配置里打开直流偏移校正,或者在GNU Radio里加一个DC Blocker模块。
我用了DC Blocker,截止频率设的是10Hz。加完之后,星座图中心的亮点消失了,AGC的稳定性也好了很多。这个经验说明,零中频架构的SDR,直流偏移是必须处理的问题,不能忽略。
6. 从能跑到跑好——链路优化的几个方向
6.1 信道编码的引入——从裸QPSK到卷积码
现在的链路是裸QPSK,没有信道编码,误码率在10^-3左右。如果要进一步降低误码率,最直接的办法是加卷积码。GNU Radio里有Convolutional Encoder和Viterbi Decoder模块,可以直接用。
卷积码的码率我打算用1/2,约束长度用7,生成多项式用标准的(171, 133)八进制。这个配置在通信系统里很常见,编码增益大约5dB。也就是说,在同样的误码率要求下,加了卷积码之后,发射功率可以降低5dB,或者通信距离可以增加大约1.8倍。
代价是有效符号率减半。原来500kSps的符号率,加了1/2码率的卷积码之后,有效比特率从1Mbps降到500kbps。对于我的实验来说,这个代价可以接受。
6.2 帧同步的鲁棒性提升——从单前导到双前导
现在的帧同步用的是单前导序列,在信噪比好的时候没问题,但在信噪比差的时候,虚警率会上升。解决办法是用双前导序列,也就是在帧头放两个相同的前导序列,接收端做两次互相关,只有两次峰值位置一致才认为是真同步。
双前导的代价是帧头开销翻倍,从64比特变成128比特,帧效率从84%降到76%。但换来的是虚警率降低一个数量级,在低信噪比下很值得。
我实测下来,单前导在信噪比10dB的时候虚警率大约是1%,双前导降到0.1%。如果信噪比降到5dB,单前导的虚警率升到10%,双前导还能保持在1%左右。
6.3 实时性优化——从离线处理到在线处理
现在的链路是半离线的:接收端先把数据存到文件,再用Python做后处理。这样做的好处是调试方便,坏处是不能实时看到结果。
如果要实时处理,需要把Python的后处理逻辑用GNU Radio的Embedded Python Block实现,或者用C++写一个OOT模块。我打算下一步用Embedded Python Block试试,把EVM计算和帧同步逻辑都放进去,这样就能在GNU Radio的界面里实时看到EVM和帧同步状态。
实时处理的另一个挑战是缓冲区管理。GNU Radio的流图是异步的,如果某个模块处理太慢,缓冲区会溢出,导致数据丢失。解决办法是调整缓冲区大小,或者降低采样率。我现在的采样率是1MSps,缓冲区大小是默认的,实测下来没有溢出。如果升到2MSps,可能就需要调大缓冲区了。
7. 一些实测数据和经验参数
7.1 不同发射增益下的EVM和帧同步成功率
| 发射增益(dB) | 接收信号强度(dBm) | EVM(%) | 帧同步成功率(%) |
|---|---|---|---|
| 10 | -45 | 3.8 | 92 |
| 15 | -40 | 4.2 | 96 |
| 20 | -35 | 5.0 | 99 |
| 25 | -30 | 7.2 | 90 |
| 30 | -25 | 12.5 | 75 |
这个表格的数据是在接收端距离发射端1米、无遮挡的条件下测的。可以看到,发射增益20dB是一个甜点,EVM和帧同步成功率都比较好。增益太低,信号弱,帧同步成功率下降;增益太高,ADC饱和,EVM恶化,帧同步成功率也下降。
7.2 不同滚降系数下的频谱效率和定时同步难度
| 滚降系数 | 占用带宽(kHz) | 定时同步锁定时间(符号) | 星座图EVM(%) |
|---|---|---|---|
| 0.2 | 600 | 800 | 6.5 |
| 0.35 | 675 | 400 | 5.0 |
| 0.5 | 750 | 250 | 4.5 |
| 0.8 | 900 | 150 | 4.2 |
滚降系数越小,频谱效率越高,但定时同步越难,锁定时间越长。0.35是一个比较好的折中,占用带宽675kHz,锁定时间400个符号,EVM 5%。如果对频谱效率要求不高,可以用0.5,锁定时间缩短到250个符号,EVM也稍微好一点。
7.3 不同环路带宽下的Costas Loop性能
| 环路带宽 | 锁定时间(符号) | 相位抖动(度) | 星座图形态 |
|---|---|---|---|
| 0.001 | 3000 | 2 | 点簇紧凑但锁定慢 |
| 0.005 | 1000 | 5 | 点簇适中 |
| 0.01 | 500 | 10 | 点簇散开 |
| 0.05 | 100 | 25 | 点簇严重散开 |
Costas Loop的环路带宽和锁定时间、相位抖动是矛盾的。带宽越小,锁定越慢,但相位抖动越小。0.005是一个比较平衡的值,锁定时间1000个符号,相位抖动5度,对于我的帧长度来说可以接受。
8. 这套链路还能怎么玩
8.1 从QPSK到16QAM——星座图更密,要求更高
QPSK跑通之后,下一步自然是16QAM。16QAM的星座点有16个,对EVM的要求更高。QPSK的EVM在8%以下就能工作,16QAM要求EVM在4%以下,否则相邻星座点会混淆。
要跑16QAM,首先得把AD9361的线性度调好。AD9361的发射通道有数字预失真(DPD)功能,可以补偿功率放大器的非线性。但P201Pro的发射通道没有外置PA,所以DPD用不上。只能靠降低发射功率来保证线性度。
其次,16QAM对载波同步的要求更高。Costas Loop的相位抖动必须控制在2度以内,否则星座点会旋转到相邻象限。这意味着环路带宽要调小,锁定时间会变长。可能需要用判决反馈的方式来做载波同步,而不是简单的Costas Loop。
8.2 从单载波到OFDM——多载波带来的新问题
OFDM是另一个方向。OFDM把宽带分成多个窄带子载波,每个子载波上可以跑QPSK或者16QAM。好处是对频率选择性衰落不敏感,坏处是峰均比高,对ADC/DAC的动态范围要求高。
GNU Radio里有OFDM的例程,但那个例程是给仿真用的,直接搬到SDR上会有问题。主要问题是同步:OFDM对定时同步和频率同步的要求比单载波高得多。定时偏差会导致子载波间的相位旋转,频率偏差会导致子载波间的干扰。
如果要跑OFDM,我建议先用GNU Radio的OFDM例程做仿真,把同步算法调好,再搬到SDR上。直接上SDR调OFDM,很容易卡在同步上,看不到任何有意义的结果。
8.3 从单向到双向——TDD模式的挑战
现在的链路是单向的,一个发一个收。如果要做成双向的,也就是TDD模式,那就需要处理收发切换的问题。AD9361支持TDD模式,可以通过控制接口快速切换收发状态。
TDD的挑战在于切换时间。AD9361的收发切换时间大约是几微秒,对于符号率500kSps来说,一个符号是2微秒,切换时间占了一个符号周期。这意味着在切换的时候,会有几个符号丢失。解决办法是在帧结构里留出保护间隔,或者用更高的符号率来缩短切换时间占比。
我打算下一步试试TDD模式,把P201Pro配置成收发一体,做一个简单的ping-pong协议。这个实验能帮我理解TDD系统的时序设计,对以后做更复杂的协议有帮助。
9. 写在最后的一些个人体会
这套链路从开始动手到基本跑通,前后花了大约三周时间,其中大部分时间花在调试同步上。发射链路其实很简单,GNU Radio拖几个模块就能跑;接收链路才是真正的挑战,Symbol Sync、Costas Loop、帧同步,每一个都需要反复调参。
我最大的体会是:不要迷信例程。GNU Radio自带的QPSK例程能跑通,是因为它把很多参数都设好了,而且用的是仿真信道,没有噪声、没有频偏、没有定时偏差。一旦搬到真实的SDR上,这些理想条件都不存在了,必须自己理解每个参数的含义,才能调出正确的结果。
另一个体会是:定量测量比定性观察重要得多。星座图好看不好看,眼图张开不张开,这些都是定性观察。真正能说明问题的是EVM、误码率、帧同步成功率这些定量指标。我后来养成了一个习惯,每改一个参数,就记录一次EVM和帧同步成功率,这样才知道参数改对了还是改错了。
最后,P201Pro这块板子的性能其实很不错,AD9361的灵活性很高,只要把滤波器、增益、采样率这几个关键参数调好,QPSK链路跑起来很稳。如果你手里也有类似的SDR板卡,建议不要只拿它当频谱仪用,花点时间搭一条完整的收发链路,对理解无线通信的底层原理非常有帮助。