简介:这是一份面向FPGA与射频工程师的RFSoC调试实战记录,围绕Alinx公司软件无线电射频Zynq UltraScale+RFSoC开发板,聚焦ADC采集与DAC输出频率同配置不一致这一典型故障,梳理从原厂demo工程入手到定位根因的完整排查链路,适合具备一定硬件调试经验、希望掌握RFSoC调试流程的技术人员参考。文档为单个docx文件,压缩包约449KB,篇幅紧凑而信息密度较高,完整保留了ILA抓取信号、频谱仪检测DAC输出、信号源验证ADC频谱、核对RF Data Converter配置、审读Vitis代码、确认AXI总线时钟等环节的原始分析过程。已有904人学习。读者可从中获得可直接复用的排查顺序与判断依据,理解Sample per AXI4-Stream Cycle对抽取与插入值计算的影响,避免在同类板卡上重复出现问题;文中对DDS频率推算、时钟分频抓取等细节的推演,也便于对照自身项目环境开展实践验证。
1. 频谱仪上那个 1.502GHz,把我按在工位上查了两天
做 RFSoC 调试的人大多有过这么一刻:代码跑通了,ILA 波形干净漂亮,你以为收工了,结果把 DAC 输出接到频谱仪上,屏幕上的数字跟你在纸上算的差了 6MHz。我盯着那个 1.502GHz 看了很久——按设计,1.5GHz 上变频叠加四路 DDS,理论值该是 1.508GHz。这 6MHz 的偏差不致命,但它意味着从 AXI4-Stream 时钟到抽取因子这一整条链路里,有一个地方在偷偷改我的参数。这套基于 Zynq UltraScale+ RFSoC 开发板的 ADC 与 DAC 调试过程,本质上不是写代码,而是把一个数字对不上的现场,一步步还原成一棵时钟树。适合手里已经有板子、跑通过回环 demo,但发现"频率总觉得不对"的人。
2. 项目定位与技术栈:从 ADC+FPGA+DAC 三片架构到一颗 RFSoC
2.1 三片架构和单芯片架构,差的不只是面积
以前做一套收发链路,板子上至少摆三颗芯片:前端一颗高速 ADC,中间一颗 FPGA 做数字下变频和抽取,后面再接一颗 DAC 把信号送出去。这套架构能跑,但每一段接口都是一次血泪经验——ADC 到 FPGA 的 LVDS 或 JESD204B 链路要调同步,FPGA 到 DAC 又要再来一遍,时钟还得做到同源低抖动,一个锁相环没锁上,整条链路都在飘。
RFSoC 把这套东西塞进一颗芯片里:片上直接集成多通道 ADC、多通道 DAC、数字上/下变频(NCO + 抽取/插入滤波器)以及 ARM 核。片上射频数据转换器和可编程逻辑之间走的是 AXI4-Stream,不再是板级高速串行接口,时钟也是从同一棵时钟树里分出来的。换句话说,你调不通的时候,问题域被压缩了——要么是射频数据转换器 IP 的配置,要么是 PL 侧的 AXI 时钟,要么是 PS 侧 Vitis 里那几行改写寄存器的代码。我这套板子上,ADC 和 DAC 的采样时钟都是 4.9152 GSPS,这个数字后面会反复出现,它是所有频率计算的起点。
技术栈上没什么新鲜的:Vivado 做硬件工程(射频数据转换器 IP、时钟 IP、ILA、AXI4-Stream 数据通路),Vitis 做裸机程序(配置射频数据转换器、驱动时钟芯片、跑 DDS),仪器侧是频谱仪加信号源。真正花时间的不是写,是核对。
2.2 原厂 demo 工程的结构与上电顺序
拿到板子第一步,别急着改自己的逻辑,先跑厂商给的射频回环 demo(这类工程一般叫rf_adaloop或类似名字)。它的数据流非常直白:
DDS(PL 侧产生单音)→ DAC 上变频 → 板内回环或外接 → ADC 下变频 → AXI4-Stream → ILA 抓取 → 导出到 Matlab 做频谱
工程里通常有两部分需要你确认。硬件侧:射频数据转换器 IP 的 Tile 配置(采样率、抽取/插入因子、NCO 频率、每 AXI4-Stream 周期的采样点数、数据位宽)。软件侧:Vitis 应用里有个ChangeSampleFreq()之类的函数,负责在运行时把某些参数重新写一遍。
上电顺序我一般这么走:先只供电不下载,用示波器确认参考时钟在;下载 bit 流,打开 ILA,看 AXI4-Stream 的tvalid是否持续拉高;再用 Vitis 跑程序,串口打印版本和配置回读值。这套流程听着啰嗦,但它能把"到底哪一层没起来"这件事提前切开,比后面在频谱仪前猜要省事得多。
# Vitis 侧命令行构建与下载(工程名按实际替换) xsct connect targets -set -filter {name =~ "APU*"} rst -processor dow rf_adaloop.elf condow下载 ELF、con让 A53 跑起来之后,串口应该能看到程序打印的采样率配置。如果这里没输出,先别怀疑射频,去看串口波特率和psu_init有没有跑完——我见过不止一次是初始化没走完就在那儿分析频谱。
2.3 射频数据转换器 IP 里那几项,必须先看懂再动手
在 Vivado 里双击射频数据转换器 IP,配置页面上真正影响频率的只有四项,但它们的耦合关系很容易被忽略:
| 配置项 | demo 中的值 | 影响 |
|---|---|---|
| 采样率 Sample Rate | 4.9152 GSPS | 所有频率计算的基准 |
| 抽取 / 插入因子 | 5 / 5 | 决定复合采样率 |
| 每 AXI 周期采样点数 | ADC 4 点、DAC 8 点 | 决定 AXI4-Stream 时钟 |
| NCO 频率 | 1.5 GHz | 上/下变频中心 |
ADC 侧数据位宽 64 bit、每周期 4 个采样点,说明每个采样是 16 bit(4×16=64);DAC 侧 128 bit、每周期 8 个采样点,同样是 16 bit。这两个数字不是我随便写的,它们直接决定了后面 AXI 总线时钟该是多少,也是这次翻车的真正源头。很多人(包括当时的我)只记住了"抽取 5 倍",却把"每周期 4 个采样点"当成了一个无关紧要的位宽参数,这就是后面所有痛苦的开始。
3. 核心链路拆解:4.9152 GSPS 采样率是怎么一路"除"到 245.76 MHz 的
3.1 时钟树:从 4.9152 GSPS 倒推 AXI4-Stream 时钟
射频数据转换器的采样时钟是 4.9152 GSPS,它来自板上时钟芯片输出的参考时钟,经片内 PLL 倍频得到。往下走一层,ADC 做了 5 倍抽取,输出到 PL 的复合数据率就是:
4.9152 GSPS / 5 = 983.04 MSPS但这 983.04 MSPS 是"采样点速率",不是 AXI4-Stream 的时钟速率。因为一个 AXI4-Stream 周期里塞了 4 个采样点(64 bit 一次搬完),所以:
AXI4-Stream 时钟 = 983.04 MSPS / 4 = 245.76 MHz合起来就是那行公式:4.9152G / 5 / 4 = 245.76MHz。这三个数字的关系必须在纸上写死,因为它同时出现在三个地方——Vivado 的 IP 配置、PL 侧时钟约束、Vitis 里那段改频率的代码。只要有一处算错,另外两处就会跟着错,而且错得很隐蔽。
我第一次算的时候只用了4.9152/5,得到 983.04 MHz,然后对着 ILA 死活想不通为什么时钟域跑不到这个频率。后来才反应过来,AXI4-Stream 是并行搬样本的,不是串行搬的。
3.2 抽取因子与插入因子,为什么改一个动全身
抽取的作用是把高速采样率降下来,插入(DAC 侧)是反过来。两者在数学上是镜像的,配置错误的表现也很像:频谱位置偏了、带宽不对、或者谐波冒出来。
关键点在于,抽取因子不是一个可以随便改的旋钮。它和 NCO 频率、滤波器通带、AXI 时钟是绑在一起的:
- 抽取因子变大 → 复合采样率变低 → AXI 时钟变低 → 同样一段 PL 逻辑的时序余量变大,但可用带宽变窄;
- 抽取因子变小 → 复合采样率变高 → AXI 时钟逼近 PL 能跑的上限,时序容易崩;
- NCO 频率必须落在复合采样率的奈奎斯特区间内,否则上变频之后信号会折叠回来。
所以我在项目里给自己定了个规矩:抽取因子只在 Vivado IP 里改,不在 Vitis 里改。这条规矩是后面用两天时间换来的,下面会讲为什么。
3.3 Sample per AXI4-Stream Cycle:被忽略的那一项
这是整篇文章的核心。原始 demo 的 Vitis 代码里是这么写的:
sample_rate = 245.76e6; Status = ChangeSampleFreq(&RFdcInstPtr, sample_rate);而这个函数内部做了两件事:
/* 原始代码,存在问题的版本 */ intr_dec = (uint32_t)(FS / sample_rate); /* 这里少除了一项 */ clkwiz(clk_wiz_addr, sample_rate / 4); /* 时钟也被改了 */ XRFdc_SetInterpolationFactor(...); /* 写 DAC 插入因子 */ XRFdc_SetDecimationFactor(...); /* 写 ADC 抽取因子 */代入数字:FS = 4.9152e9,sample_rate = 245.76e6,算出来intr_dec = 20。而 IP 里配的抽取因子是 5。差在哪?差的就是"每 AXI4-Stream 周期 4 个采样点"这一项——20 = 5 × 4。
也就是说,这段代码把 AXI 时钟 245.76 MHz 当成了 ADC 的采样率在用,于是算出来的抽取因子是正确值的 4 倍。同时clkwiz()又把时钟芯片的输出改成了sample_rate/4 = 61.44MHz,导致整个 AXI 时钟树跟着往下掉。
4. 信号链验证:ILA 抓数、Matlab 频谱与频谱仪读数怎么对上
4.1 ILA 探针该挂在哪几个信号上
调这类问题,ILA 是唯一能看见"数字域真相"的工具。我一般挂四个探针:
m_axis_data_tvalid:确认数据流真的在流动;m_axis_data_tdata:64 bit ADC 数据(或 128 bit DAC 数据);- 时钟域对应的
clk:这个探针本身就能告诉你 AXI 时钟跑到了多少; - 一个你自己打的脉冲(比如 DDS 相位回绕标志),用来量周期。
原始调试里最有价值的一步,是"从 ILA 抓取的 DDS 输出看,[63:48]、[47:32]、[31:16]、[15:0] 四段的信号周期都是 32 个时钟"。这句话其实已经把答案说出来了——四个 DDS 通道是并行拼在一起的,每段 16 bit,各占一个采样点位置,所以整体输出频率是单路 DDS 频率的四倍。如果只看其中一段算频率,就会少算 4 倍,这正是我一开始算错的地方。
用 ILA 量时钟有个小技巧:板子上不方便把 AXI 时钟引到外部引脚,就拿一个已知频率的慢时钟去采样它,或者像原文那样,把clock_out和Fabric_clock分频后再抓。分频比要选得让被测时钟在一个采样窗口里能被数到几十个沿,太少分辨率不够,太多计数器溢出。
4.2 把 AXI4-Stream 数据导出来,格式别搞错
ILA 抓到的数据导出成 CSV 之后,最容易翻车的是排列顺序。ILA 导出的通常是"每个时钟一行、每行 N 列样本",而 FFT 需要的是按时间先后排好的一维序列。如果直接对矩阵做fft,得到的是某个通道自己的频谱,不是复合信号的频谱。
我常用的做法是:
% 从 ILA 导出的 AXI4-Stream 数据重建 ADC 采样序列 fs_axi = 245.76e6; % AXI4-Stream 时钟,这是要先核对的关键参数 spc = 4; % 每个 AXI 周期的采样点数(ADC 侧为 4) fs = fs_axi * spc; % 复合采样率 = 983.04 MSPS raw = readmatrix('ila_adc.csv'); % 每行一个时钟周期,4 列 16bit 样本 x = reshape(raw.', [], 1); % 转置后按列展开,还原时间顺序 x = double(x); x = x - mean(x); % 去直流 N = length(x); w = hann(N); % 加窗抑制泄漏 X = abs(fft(x .* w)) / N; f = (0:N-1) * fs / N; % 频率轴,单位 Hz plot(f(1:N/2)/1e6, 20*log10(X(1:N/2)+eps)); xlabel('频率 / MHz'); ylabel('幅度 / dB'); grid on;这段代码里,fs是一切结论的地基。原文中说"频谱只显示 40MHz",我几乎可以肯定问题就出在这里:如果fs填的是 245.76e6 而不是 983.04e6,那么所有频率读数都会被压缩到四分之一,1.510GHz 下变频到 10MHz 的信号就会被算成 2.5MHz 附近,而杂散和折叠则会呈现出完全对不上的形态。所以我在脚本开头永远写一行注释:fs = fs_axi * spc,改之前先确认 ILA 时钟。
reshape那一行也值得多说一句。raw.'是先转置再展开,因为 MATLAB 的reshape是列优先;如果写成reshape(raw, [], 1),你会得到"先取第一列所有行、再取第二列所有行"的顺序,也就是把通道顺序打乱了。信号本身是单音,打乱后频谱看起来也像单音,只是频率位置会错——这种错误最难查。
4.3 频谱仪读数与理论值对表
仪器侧我给自己的要求是:每一个读数都要能写成一行算式。当时的三条记录是这样的:
| 观察点 | 实测 | 理论 | 结论 |
|---|---|---|---|
| DAC 输出(频谱仪) | 1.502 GHz | 1.5G + 7.68M = 1.5077 GHz | 偏差 6MHz,异常 |
| DDS 单路周期(ILA) | 32 个 AXI 时钟 | 245.76/7.68 = 32 | DDS 计算正确 |
| ADC 采集(Matlab) | 40 MHz | 1.510G − 1.5G = 10 MHz | 偏差 4 倍,异常 |
三行放在一起看,规律很明显:DDS 那行是对的,说明 PL 侧逻辑没问题;DAC 输出偏小、ADC 频谱偏大,说明 PS 侧写进射频数据转换器的参数和 IP 里配的参数不一致。这时候就不用再怀疑模拟前端和滤波器了,直接把火力集中到ChangeSampleFreq()上。
顺便说一句,频谱仪读 1.502GHz 时不要只看峰值。把 span 拉开到 100MHz,看有没有一堆等间隔的小尖峰——如果有,说明 DAC 的插入滤波器被配置到了错误的速率上,镜像分量没被滤掉。这个现象在后面删掉clkwiz那一行时出现过一次,非常典型。
5. 常见问题排查:五条从现象到根因的踩坑记录
5.1 现象:频谱仪显示 1.502GHz,比理论值低约 6MHz
原因:clkwiz(clk_wiz_addr, sample_rate/4)把时钟芯片输出改成了 61.44MHz,AXI4-Stream 时钟随之掉到实测 66.41MHz。DDS 的相位累加器以 AXI 时钟为节拍,时钟慢了,同样相位步进下的输出频率自然跟着降,四路拼起来的 7.68MHz 变成了 1.92MHz 左右,1.5G + 1.92M ≈ 1.502GHz。
解决:把clkwiz()的入参从sample_rate/4改成sample_rate。时钟芯片输出必须等于 AXI4-Stream 时钟,不是它的四分之一。
5.2 现象:ADC 采集数据在 Matlab 里算出 40MHz,预期 10MHz
原因:FFT 的采样率参数按 245.76MHz 填的,但实际复合采样率是 983.04 MSPS(245.76 × 4)。频率轴整体被压缩了四倍,10MHz 的差频正好被算到 40MHz 附近。
解决:fs = fs_axi * spc,并且把spc从 IP 配置里核对一遍(ADC 4 点、DAC 8 点,两者不同,别抄错)。
5.3 现象:用分频时钟去抓 AXI 总线时钟,实测 66.41MHz
原因:这是"结果"不是"原因"。clkwiz()已经把时钟源改掉了,所以你测到的就是被改坏之后的频率。很多人到这一步会去怀疑 PLL 配置、怀疑参考时钟,其实应该往上追一层——谁动的时钟。
解决:先去 Vitis 代码里搜clkwiz、set_clk、ClockWizard这类调用。如果 IP 里已经配好,运行时就不该再改。
5.4 现象:删掉clkwiz()之后,频率对了,但频谱上全是谐波
原因:删掉了时钟改写,但intr_dec = FS/sample_rate这一行还在,抽取因子仍是 20 而不是 5。抽取滤波器的通带、阻带和实际数据率不匹配,DAC 侧镜像分量没被抑制,谐波就冒出来了。
解决:两处必须一起改。抽取因子按FS / sample_rate / 4算(4 是每周期采样点数),时钟按sample_rate给。
5.5 现象:照着 demo 改了参数,重新下载后表现依旧
原因:射频数据转换器的部分参数是上电初始化时锁存的,光重新下载 ELF 不一定生效;另外 Vitis 工程可能有多份ChangeSampleFreq的副本(不同源文件里各写了一份),改了一份另一份还在跑。
解决:改完参数后完整复位一次(rst -processor再dow),并且在工程里全局搜一遍函数名,确认只有一处定义。这条我吃过两次亏,后来养成了改完先 grep 再下载的习惯。
提示:射频数据转换器的 IP 配置和 Vitis 运行时代码是"两份真相"。任何一处改动,都要回读另一处确认,别相信自己的记忆。
6. 二次开发:把 Vitis 侧那套频率配置收进一个「只读自检」里
改到最后一版,我的结论和原文一致:在射频数据转换器 IP 的配置界面里把参数设好之后,Vitis 代码不需要再设置,只要复位和使能就行。但"不需要设置"不等于"不需要检查",实际项目里我更愿意加一段只读自检,让板子自己告诉你配错没有。
/* 上电自检:只读回读,不写寄存器 */ #define FS_HZ (4.9152e9) #define SPC_ADC (4U) /* ADC 每 AXI 周期 4 个采样点 */ #define SPC_DAC (8U) /* DAC 每 AXI 周期 8 个采样点 */ void Rfdc_SelfCheck(u32 RfdcBaseAddr, double AxiClkHz) { u32 dec_fact, int_fact; double expect_dec = FS_HZ / AxiClkHz / SPC_ADC; /* 期望:5 */ double expect_int = FS_HZ / AxiClkHz / SPC_DAC; /* 期望:2.5,注意取整 */ XRFdc_GetDecimationFactor(&RfdcInst, 0, 0, &dec_fact); XRFdc_GetInterpolationFactor(&RfdcInst, 0, 0, &int_fact); xil_printf("AXI clk = %.2f MHz\r\n", AxiClkHz / 1e6); xil_printf("ADC dec = %u (expect %.1f)\r\n", dec_fact, expect_dec); xil_printf("DAC int = %u (expect %.1f)\r\n", int_fact, expect_int); if (dec_fact != (u32)expect_dec) { xil_printf("!! 抽取因子与 AXI 时钟不匹配,检查 IP 配置\r\n"); } }这段代码的价值在于把隐式假设显式化。SPC_ADC和SPC_DAC是两个不同的常量,恰恰是这点最容易被忽略——ADC 4 点、DAC 8 点,位宽 64 和 128,如果统一按 4 算,DAC 侧就会差一倍。原文里 DAC 的 DDS 频率算成 7.68MHz 是对的,但那是靠 ILA 量出来的,不是靠公式推出来的,说明当时公式里的这一项确实没走通。
改造建议按三条走:第一,时钟芯片只在 IP 或psu_init里配一次,运行时代码里搜不到clkwiz才算干净;第二,抽取/插入因子如果在运行时确实要动,公式统一写成FS / AXI_CLK / SPC,别写成FS / AXI_CLK;第三,把期望值做成常量表,上电打印一次,和频谱仪读数对不上时先看打印。
验证方法很简单:DAC 接频谱仪看峰值位置,信号源灌 1.510GHz 给 ADC,ILA 抓一段导进 Matlab 看差频是否落在 10MHz。三个数对上,链路就通了。对不上就回到第 5 章的五条里对照现象找。
从那以后我每次拿到一块新板子,第一件事不是跑 demo,而是先在纸上把采样率、抽取因子、每周期采样点数、AXI 时钟这四个数算一遍,再打开 Vivado 核对 IP 里填的是不是这个值,最后才下载程序。听起来多花十分钟,实际上把后面可能的两天全省了。希望帮到你。
本文还有配套的精品资源,点击获取