做射频接收链路有一阵子了,之前接触的方案都是外挂高速ADC加FPGA,布线麻烦不算,还要调一堆差分时钟、同步信号。后来换了Zynq UltraScale+ RFSoC平台,第一感觉是“集成度真高”——RF-ADC、RF-DAC、DDC/DUC、混频器、QMC全部塞进一颗SoC里,射频前端直采不再是实验室里遥不可及的东西。但真到自己动手配置RF Data Converter IP的时候,才发现里面可调的参数、寄存器的时序、校调的顺序,每一条都藏着坑。这篇文章就把我从ADC混频器一路调到QMC校调的完整过程写出来,包含NCO频率的计算方法、QMC系数的估计算法、以及实际工程里最容易出问题的几个环节。适合手里有ZCU111或者类似RFSoC板卡的工程师参考,也适合刚接触射频直采的人建立整体概念。
1. 为什么选择Zynq UltraScale+ RFSoC做射频直采
1.1 RF-ADC和RF-DAC到底解决了什么问题
Zynq UltraScale+ RFSoC这个系列,核心卖点就是把射频采样前端集成进SoC。我这边使用的是ZCU111评估板,主芯片是XCZU28DR,片上集成了8路12bit RF-ADC,标称采样率4.096 GSPS,配套的还有8路RF-DAC。按照Nyquist采样定理,4.096 GSPS的采样率理论上可以覆盖到2 GHz以下的信号,实际工程里最常用的是第一Nyquist区或者第二Nyquist区采样,直接把射频信号采进来,省掉了一整条模拟下变频链路。以前做宽带接收机,得用分立混频器、本振源、滤波器、两级中频放大,一条链路打下来又贵又难调;用RFSoC之后,模拟前端变得非常简洁,信号进来之后就是ADC,之后全部是数字处理。
但集成度高也意味着你必须接受它给定的数字处理链路。RF数据转换器内部不是裸的ADC采样那么简单,在采样数据到你手里的AXI-Stream接口之前,它会经过抽取滤波、复数混频、QMC等模块。很多刚上手的人直接在PL端抓ADC原始数据,发现抓回来的数据不是自己想要的,就是因为没搞清楚这些内部模块的默认配置。所以理解RF Data Converter IP内部到底干了什么,是配置的第一步。
1.2 RF Data Converter IP在系统中的位置和职责
在Vivado的Block Design里,RF Data Converter IP是一个软核IP,通过AXI4-Lite接口做寄存器配置,通过AXI4-Stream接口输出采样数据。它通常按Tile来组织,每个Tile对应一组ADC或DAC模拟通道,一个IP实例里可以包含多个Tile,每个Tile又可以包含多个Channel。
IP内部各个模块的职责,我用一张表总结一下。
| 模块 | 作用 | 配置关键点 |
|---|---|---|
| RF-ADC模拟前端 | 采样、增益控制、偏置校准 | 采样率、满量程电压、阻抗匹配 |
| 抽取滤波器(Decimation) | 降低数据率,缓解后端处理压力 | 抽取倍数、滤波器带宽 |
| 复数混频器 | 数字下变频,将射频信号搬到基带 | 混频模式、NCO频率 |
| NCO | 产生混频所需的本地振荡信号 | 频率字、相位字、更新触发 |
| QMC(正交调制校正) | 校正I/Q不平衡和本振泄漏 | 使能位、幅度/相位校正系数 |
| AXI-Stream接口 | 把处理后的数据送出PL | 位宽、数据格式、TREADY信号 |
搞清楚这个结构之后,你配置IP的时候就不会眉毛胡子一把抓了。我个人的习惯是,先看采样率和时钟,再看混频器和滤波器,最后调QMC。这个顺序对应的是信号链路的先后顺序,也符合“先让数据通起来,再谈信号质量”的工程思路。
2. ADC混频器配置:从原理到寄存器操作
2.1 混频器的类型和选择
RF Data Converter里的混频器和传统模拟混频器原理上是一样的,只不过一个处理的是数字序列,一个处理的是连续电压。模拟混频器通过乘法器把信号与本振相乘,输出和频、差频分量;数字混频器也是做乘法,只不过输入信号已经是ADC采样后的数字序列,本振则是NCO(数控振荡器)生成的数字正弦/余弦序列。混频前后的波形差异,很多人第一次用ILA观测时会很困惑:混频前看到的是高频实信号波形,混频后看到的是I/Q两路复信号波形,频率完全不同。
IP里混频器有几种可选模式,实际工程中要根据你的后端处理方式来做选择。
- Real -> Real:输入实信号,输出实信号。这种模式通常只在DAC侧使用,因为DAC如果要直接输出实射频信号,混频器就配成实数混频输出,NCO产生的本振与输入相乘后取实部。
- Real -> Complex:这是ADC接收链路最常用的模式。输入是实信号,混频输出I/Q两路复信号,实现数字下变频到零中频或者低中频。
- Complex -> Complex:输入输出都是复信号,适用于I/Q信号需要进一步变频的场景。
- Bypass:完全旁路混频器,NCO不使能,ADC采样数据直接送往滤波器或AXI-Stream接口。这个模式在纯中频采样、后端自己处理频谱翻转时使用。
配置的时候还有一个容易忽略的点:如果选择了Real->Complex模式,ADC通道会占用两个AXI-Stream输出,一个I路一个Q路,数据率会翻倍。如果你在后端接DMA或者直接进FFT,位宽和数据率都要提前算好,否则很容易出现总线拥塞。我第一次做的时候没注意数据率翻倍这个问题,DMA配置还是按单通道算的,结果采集回来的数据隔一段就跳一个样点,排查了好半天才发现是AXI-Stream带宽不够。
2.2 NCO频率计算与写入流程
NCO本质上是一个相位累加器,它决定了混频器本振的频率。RF Data Converter里的NCO通常使用48位相位累加器,NCO输出频率与采样时钟、频率控制字的关系是固定的。
假设ADC采样时钟为Fs,NCO希望输出的本振频率为F_nco,那么频率控制字ΔP的计算公式为:
ΔP = round(F_nco * 2^48 / Fs)
这里有几个要点。首先,计算出来的ΔP是48位无符号整数,写入寄存器时要注意位宽,超出48位的部分直接截断。其次,这个公式里的Fs一定要用混频器所在数据路径的实际采样率,而不是简单地用ADC的模拟采样率,因为如果前面已经做了抽取,进入混频器的数据率会变低,NCO累加器的时钟域也相应变化。我自己踩过的坑就是:ADC标称4.096 GSPS,前面配了4倍抽取,混频器实际工作在1.024 GSPS,结果我NCO频率计算时还用的4.096 GSPS,算出来的本振频率差了4倍,混频后信号位置完全不对。
写入NCO频率的流程也不是只写一个寄存器就完事,需要按照固定的步骤来。简化后的写流程如下:
- 将NCO频率控制字按高32位、低16位拆分,分别写入对应的频率控制字寄存器。
- 如果需要设置初始相位,同样将相位字拆分写入相位寄存器。
- 写触发更新控制寄存器,置更新位,让NCO重新装载频率字。
- 回读状态寄存器,确认更新完成。
很多寄存器是影子寄存器,你写进去的值不会立即生效,必须有更新触发信号才会进入工作状态。我见过有同事写完频率字寄存器后没有触发更新,然后一遍遍怀疑自己的NCO频率算错了,其实就是少了最后一步。
2.3 混频前后波形的观察方法
混频器配完之后,如何在工程里验证它真的把信号搬到了正确的位置?我通常用两种方式。第一种是在IP内部调试接口上直接观测数据,用ILA(集成逻辑分析仪)抓混频器输出;第二种是把捕获到的数据导出,在MATLAB或Python里做FFT看频谱。
先说ILA观测。如果输入信号的频率是F_signal,NCO频率是F_nco,那么混频器输出会同时出现F_signal - F_nco和F_signal + F_nco两个分量。在数字下变频场景下,后面一般跟着低通抽取滤波器,所以和频分量会被滤掉,留下的是差频分量。在ILA里看I/Q两路波形,如果F_nco不等于F_signal,你会看到I路和Q路各自都是一个缓变的正弦波,频率等于两边的差频;如果F_nco恰好等于F_signal,差频为零,I/Q两路波形会变成接近直流的电平幅值。
第二种方式是做FFT。把混频后的I/Q数据导出,拼成复数序列I + jQ,再做FFT。这里有一个特别需要注意的点:复数序列的频谱不是双边对称的,只有单边谱,F_nco的频率搬移会在正频率和负频率上体现出差异,所以你可以通过观察镜像频谱的位置来判断混频是否正确。如果混频后频谱出现了很强的镜像分量,要么是I/Q两路增益不匹配,要么是相位不正交,这就和下一节的QMC联系起来了。
3. QMC校调:消除I/Q失衡的关键一步
3.1 什么是I/Q失衡,以及为什么会存在
I/Q失衡其实是个非常现实的问题。理论上,I路和Q路应该是幅度完全一致、相位严格相差90度的正交信号。但实际链路里,模拟前端、变压器、放大器、PCB布线,都会让I/Q两路出现幅度偏差和相位偏差。幅度偏差一般用增益误差g来描述,相位偏差用相位误差φ来描述。我举个直观的例子:一个原始的单音信号,经过I/Q失衡的链路后,在频谱上就会出现镜像分量,也就是在负频率或者对应镜像频率位置出现一个不该有的峰值。这个镜像会直接影响接收机的动态范围和信噪比。
在发射链路里,QMC的意义更大。发射的I/Q信号如果存在直流偏置,会直接导致本振泄漏,也就是说本振信号会直接串到发射频点上,这在频谱仪上一看就是在载波中心位置有一条异常谱线,不仅浪费发射功率,还干扰同频段的其它设备。所以发射链路QMC除了校正I/Q不平衡外,还必须包含直流偏置的校正。
RF Data Converter IP中的QMC分接收QMC和发射QMC,两者在寄存器空间上分属不同的地址区域。接收QMC主要校正I/Q增益和相位误差,发射QMC还要额外校正常数项直流偏置。配置的时候不要只盯着系数寄存器,使能位和控制位也要一起检查,否则系数写进去也不会生效。
3.2 QMC校正系数的计算方法
QMC校正系数怎么确定?严格的做法是发射时用外部仪器测量,接收时注入已知单音信号来评估。工程中最常用的方法是单音测量法,基本流程如下。
先在接收通道注入一个单音信号,频率选择在带内比较干净的位置,幅度不要超过满量程的80%,然后采集一段I/Q数据,估计增益误差和相位误差。增益误差可以用I/Q两路信号的均方根幅度比值来估算:
g = sqrt( sum(Q^2) / sum(I^2) )
相位误差可以通过两路信号的相关系数来估算。一个简单的方法是计算I和Q的互相关,再除以I路的自相关:
rho = sum(I*Q) / sum(I^2)
得到g和rho之后,可以用Gram-Schmidt正交化来构造校正矩阵,也就是把Q路信号减去与I路相关的部分,再归一化:
I' = I
Q' = (Q - rho * I) / sqrt(1 - rho^2)
这个校正矩阵的目的是让变换后的I'和Q'正交且幅度一致。实际写入IP寄存器时,通常需要把浮点系数转成定点小数格式。具体的小数位宽看IP版本,PG269文档里写得很清楚。我用的Vivado版本下,QMC系数寄存器是带符号定点数,需要先把计算出的浮点系数乘以对应的比例因子再取整,否则写入完全没效果,甚至可能把信号搞乱。
这里还要强调一个工程细节:计算QMC系数前,一定要先把直流分量去掉。如果I/Q数据带着直流偏置,相关系数和幅度比值都会被带偏,算出来的系数是错的。这也是为什么上面代码里先对I和Q做了去均值处理。我自己调试时,曾经跳过去直流,直接算QMC系数,结果镜像不仅没消掉,反而更差了,原因就是直流偏置污染了系数估计。
3.3 在IP寄存器中写入QMC系数的流程
QMC系数的写流程,不同IP版本略有差异,但整体套路是一致的。以我使用的RF Data Converter IP为例,流程如下。
- 确认通道的混频器和抽取配置已经稳定工作,采集到有效的I/Q数据。
- 先做直流估计,把I路和Q路的均值算出来,如果需要写直流偏置校正寄存器,就先把直流项校正掉。
- 注入单音信号(接收QMC)或者启用测试音(发射QMC),重新采集I/Q数据。
- 在软件里计算I/Q增益误差g、相位误差rho以及对应的QMC系数。
- 把系数按定点格式写入QMC增益寄存器和QMC相位寄存器。
- 置QMC使能位,让校正开始生效。
- 再次采集数据,检查镜像抑制比(Image Rejection Ratio)是否达到要求。如果不够,重复步骤3到步骤6。
我在实际项目里的做法是写一个Python脚本,通过JTAG或者以太网连接板卡,自动完成“注音、采集、计算、写寄存器、回读验证”这个闭环。整个过程跑一轮通常只需要几十秒,比手工在Vivado里点来点去高效得多。调试初期建议先把使能位关掉,验证系数写入通道,确认寄存器回读值和写入值一致,再开使能,否则排查问题时很难分清是算法算错了还是寄存器没写成。
4. 完整实操:一次从Vivado建工程到频谱验证的流程
4.1 工程搭建与IP例化
这部分写一下我从零开始搭建一个RFSoC收发链路验证工程的过程。先新建一个Vivado RTL工程,器件选择XCZU28DR。在Block Design里添加RF Data Converter IP,双击打开配置界面。
配置界面里需要做的事情有这几项。第一,根据板卡上的时钟源设置ADC和DAC的采样率,ZCU111上通常有可编程时钟芯片,可以在Vivado里通过驱动管理,也可以直接读固定的时钟频率。我习惯于先在时钟芯片驱动里把需要的采样率设置好,再回到IP里配置,这样两边能对应上。第二,使能需要用到的ADC/DAC Tile和Channel,配置抽取倍数、混频器模式、NCO频率等参数。第三,把IP的AXI4-Lite从接口接到一个AXI主控上,在ZCU111这种带有PS端的板卡上,我一般直接接在PS的M_AXI_HPM0接口上,这样可以在Linux里通过/dev/mem或者自定义驱动去读写寄存器,调试起来很方便。
连接完AXI接口后,记得把AXI-Stream数据接口接到后续处理逻辑。最简单的验证方式是直接用ILA去抓数据,不需要先搭DMA,先把数据通路打通。
4.2 ADC通道配置与数据捕获
ADC通道配置的核心是确认采样率、抽取倍数、混频器三者的参数匹配。我举一个实际参数例子:假设ADC采样率是4.096 GSPS,信号载频在1.8 GHz,需要下变频到基带。那么混频器NCO频率设置为1.8 GHz,抽取倍数根据后端期望的数据率来选择。如果后端希望数据率是256 MSPS,抽取倍数就是4.096 GHz / 256 MHz = 16倍。抽取倍数会决定信号带宽,务必确保信号本身的带宽小于抽取后有效带宽的一半,否则会产生混叠。
配置完成后,在ILA里抓取混频器输出数据。抓取的时候要注意,ILA的采样时钟是PL逻辑时钟,数据本身是连续流动的,需要设置合适的触发条件,比如用数据有效信号作为触发,或者直接抓一段固定深度的数据。抓回来的I/Q数据通过Vivado的波形导出功能存成CSV,再导入Python进行分析。我一般把I/Q两列分别导出,然后在Python里拼成复数数组,这样可以直接做FFT。
4.3 去直流、归一化和滤波处理
ADC采集回来的基带数据,第一件事就是去直流。射频直采后经过数字下变频,如果混频器或者模拟前端存在直流偏置,频谱中心位置会看到明显的直流分量,严重影响信号质量。去直流最简单的办法就是求均值然后减去。我在嵌入式处理里经常写一段滑动平均滤波来估计直流分量,逻辑很简单:开一个缓冲数组,存放最近N个采样点的值,每次新数据进来,把最旧的值移除,把新值加入,再取平均。下面是C语言实现的一个16点滑动平均滤波函数,在低数据率场景下非常好用。
#define FILTER_TAPS 16 static int32_t buffer[FILTER_TAPS]; static uint8_t idx = 0; static int64_t sum = 0; int32_t moving_average_dc_removal(int32_t sample) { sum -= buffer[idx]; buffer[idx] = sample; sum += buffer[idx]; idx = (idx + 1) % FILTER_TAPS; int32_t mean = (int32_t)(sum / FILTER_TAPS); return sample - mean; }在高数据率的RFSoC场景下,这种软件滤波只能用于低速监控链路。真正的高速数据通路一般直接在PL里用FIR滤波器IP核实现,或者依靠RF Data Converter内部的抽取滤波器自带的高通特性。但不管在哪个环节实现,去直流和归一化的思想是一致的。归一化就是把信号幅度对齐到满量程。RF-ADC的输出通常是二进制补码格式,比如12bit ADC对应-2048到2047的范围,归一化时可以除以2048,让幅度范围落在-1到1之间,这样后续FFT处理和信噪比计算会更方便。
信号滤波方面,采集到的I/Q数据可能会混入带外噪声。最简单的处理方式是做一个FIR低通滤波器,滤除带外噪声后再做FFT分析。这里给一个参考思路:滤波器的通带带宽要大于信号带宽,阻带衰减至少60 dB以上。滤波器设计在MATLAB里用fdatool生成系数,然后导入到Python或C里使用。如果只是做频谱观察,直接对原始I/Q数据做加窗FFT,也能看到大致的频谱形态。
4.4 信噪比与镜像抑制比验证
配置完成后,最终要用指标说话。两个最直观的指标就是信噪比和镜像抑制比。信噪比的计算方法是:采集一段单音信号,做FFT后找到信号峰值所在的频率bin,把信号功率算出来,然后统计除了信号和直流附近的若干bin之外的所有功率,作为噪声功率,两者相除就是SNR。下面是一段Python示例代码,用于计算信号bin及其周围几个bin的总功率。
import numpy as np def compute_snr(complex_signal, fs, signal_freq, exclude_bins=10): N = len(complex_signal) win = np.blackman(N) sig = complex_signal * win spectrum = np.fft.fftshift(np.fft.fft(sig)) freqs = np.fft.fftshift(np.fft.fftfreq(N, 1/fs)) k_signal = int(np.argmin(np.abs(freqs - signal_freq))) signal_indices = range(k_signal - exclude_bins, k_signal + exclude_bins + 1) signal_power = np.sum(np.abs(spectrum[list(signal_indices)])**2) mask = np.ones(N, dtype=bool) center = N // 2 mask[center - exclude_bins*2 : center + exclude_bins*2] = False mask[list(signal_indices)] = False noise_power = np.sum(np.abs(spectrum[mask])**2) snr_db = 10 * np.log10(signal_power / noise_power) return snr_db, spectrum, freqs镜像抑制比的验证方法类似:注入单音信号后,在镜像频率位置寻找峰值功率,然后用信号功率减去镜像功率。比如信号在+1 MHz处,那么镜像应该在-1 MHz处。QMC校准前,镜像抑制比可能只有20~30 dB;校准之后,一般可以提升到50 dB以上。我实测下来,在ZCU111上做好QMC校调和直流校后,镜像抑制比能做到55 dB左右,这个水平对大多数通信接收链路来说已经够用了。
5. 常见问题与排查技巧实录
5.1 采样时钟与逻辑时钟不同步
这类问题非常典型,表面的现象是AXI4-Lite寄存器访问超时,或者回读数据全部是同一个值。我排查时先看时钟,用Vivado的Clock Debug功能确认ADCCLK、DACCLK和PL逻辑时钟是否都稳定。有些问题是板卡上的可编程时钟芯片没有初始化,有些问题是IP配置界面里的时钟频率和实际提供的时钟对不上。RF Data Converter IP对采样时钟的要求很严格,尤其是不同Tile之间如果需要同步,必须保证参考时钟来自同一个源,否则通道间的相位关系是乱的。
遇到这类问题,不要一上来就怀疑代码逻辑,先用回读寄存器的方式确认IP是否已经启动。一般IP会有版本寄存器或者状态寄存器,回读正常再往下查数据通路。
5.2 混频后出现频谱镜像泄漏
频谱上出现镜像峰值,首先确认NCO频率和信号频率的关系。如果信号和本振没有完全对准,镜像位置会出现一个同样强度的信号,这个是正常现象,不是失衡。排除这个情况后,再看I/Q两路数据是否真的正交。如果后端拿到的I/Q数据,其中一路是空的或者两路数据错位了,频谱镜像会非常严重。最常见的原因是后端代码在拼接I/Q数据时时序没对齐,也就是I路和Q路不是同一个采样时刻的数据。检查时需要确认AXI-Stream接口上I/Q数据是否交替输出,还是分两个通道输出,处理逻辑要匹配IP的配置。
如果数据通路没问题,那就是模拟I/Q失衡,需要做QMC校调。
5.3 QMC校准始终不收敛
QMC校准不收敛是大家问得最多的问题。我排查时会依次检查下面几件事。
- 输入信号是否是干净的单音?如果信号本身有很强的杂散,相关系数和幅度估计都会被污染。
- 信号幅度是否在合适的范围内?幅度太小,量化噪声占比大,估计误差大;幅度太大,ADC削波,波形失真,估计结果当然不对。
- 直流偏置是否已经先校掉?这个问题我前面提过,直流偏置不除,I/Q增益和相位的估计全是错的。
- 寄存器系数格式是否正确?QMC系数是定点小数,直接把浮点系数写进寄存器会得到完全错误的结果。
- 更新流程是否正确?有些通道的QMC系数需要先写使能位再写系数,或者反过来,顺序错了一个都生效不了。
把这些问题逐一排查完,QMC校准基本都能收敛。我自己调试时,最常犯的是漏掉直流偏置校准这一步,后来固定成“先直流、后I/Q”的流程,就很少再卡住了。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查与解决建议 |
|---|---|---|
| AXI寄存器访问超时 | 采样时钟未起振或频率不对 | 检查时钟芯片配置、确认IP时钟频率匹配 |
| 混频器输出频率不对 | NCO频率计算用了错误的采样率 | 用混频器实际工作时钟重新计算ΔP |
| 频谱出现镜像峰值 | I/Q失衡、数据错位、本振未对准 | 先验证数据对齐,再做QMC校调 |
| QMC校准后镜像改善不明显 | 直流偏置未消除、系数格式错误 | 按“先直流、后I/Q”流程重新校准 |
| 数据流中断或跳点 | AXI-Stream带宽不足、DMA配置错误 | 核对数据率、检查DMA描述符和中断 |
写在最后
这套RF Data Converter IP的配置流程,我自己前前后后调了好几轮,最大的体会是:一定要按信号链路的顺序去排查,采样时钟没通就不要谈混频,混频不对就不要谈QMC。不要跳步。另一个想提醒的是,QMC校调里“先直流、后I/Q”这个顺序几乎决定了整个校调的成败。很多人一上来就瞄着镜像抑制比,忽略了直流偏置,结果系数怎么算都不对。如果这篇文章能让你的RFSoC调试少走几次弯路,那就算值了。