☰
DDS ADDA闭环测试工程:RTL、相干采样与SNR/ENOB复现
2026/10/26 11:25:00 网站建设 项目流程

简介:面向FPGA数字信号处理学习者的DDS与ADDA自环测试工程,围绕DDS IP数字波形合成、DAC输出与ADC回采验证展开,适合已具备Verilog/VHDL基础、希望打通从波形生成到频谱分析链路的开发者。工程采用50MHz时钟驱动DAC输出正弦波,将模拟输出自环至ADC输入,并以MMCM分频得到25MHz采样时钟,配合ILA采集不少于2048个样点,再由Matlab完成频谱分析;同时可通过VIO修改频率字,生成1MHz与3MHz信号验证频谱结果。压缩包共613个文件、约46.11MB,包含Verilog/VHDL源码与仿真文件、XDC约束、Tcl与批处理脚本、比特流、ILA/CSV采样数据及工程配置等,兼顾综合实现与数据分析。已有321人学习,可用于复现实验、核对DDS配置与ADC采样时序,并参考AN108 34针插头安装位置等注意事项减少排错成本。

1. DDS 驱动 ADDA 的闭环测试:这套工程文件省掉的不是写代码的时间

把 DAC 的输出用一根短线直接接回 ADC 的输入,中间只留一级重建滤波器、一级抗混叠和必要的衰减网络,就能用一套 DDS 把整条模拟链路的指标量出来。原理上只有三句话:DDS 发正弦、DAC 打出去、ADC 收回来做 FFT。真正动手时,多数人卡在“发的”和“收的”对不齐:DAC 侧时钟怎么转发、ADC 侧数据位序怎么拼、抓到的 8192 点到底是不是完整周期。这份 DDS ADDA 测试工程文件,价值就在把这条闭环固化成可复现的 RTL 加脚本。它适合两类人:正在调 DDS 波形(相位累加器、查找表、频率控制字)的,和要给一块自制采集板做首轮 SNR、ENOB 摸底的。下面按“先读懂 RTL,再定参数,最后排坑”的顺序拆。

2. 拆工程目录:DDS 相位累加器到 DAC 打拍的三级流水

拿到这类工程,最忌讳从第一个 .v 文件顺着往下读。DDS 加 ADDA 的工程文件通常按功能切得很干净:波形生成、发送接口、接收缓存、上位机脚本四块,剩下的是约束和测试记录。先把 top 层的连线看清楚,再把信号从“数字域”到“模拟域”再到“数字域”的整条路径画在纸上,后面所有参数才有落点。

2.1 目录骨架:先找 top,再看四个子模块

这类工程的文件组织大同小异,常见结构如下表。你手里的工程文件如果目录名不同,按用途对号入座即可,不要被命名带偏。

目录通常放什么阅读优先级
rtl/top、dds_core、dac_if、adc_cap 四个模块第一优先,先看 top 的端口
rtl/lut/ 或 sim/正弦查找表 .hex/.coe 与生成脚本第二优先,确认表点数和位宽
constr/时钟约束、输出延迟约束、引脚分配第三优先,出问题时回来看
sw/上位机取数与 FFT 分析脚本第四优先,决定你怎么看数据
doc/测试记录表、频点与码值对照有空再看,别当说明书读

阅读顺序我一般这么走:先看 top 里clk、rst_n、fcw这几个端口谁驱动谁,确认 DDS 和 DAC 是不是同一个时钟源;再看 dds_core 的位宽参数,这决定了你的频率分辨率;然后看 dac_if 里时钟是怎么送到管脚的,这里是第一个翻车高发区;最后看 adc_cap 的触发条件,它决定了你抓到的是不是一段“干净”的数据。

提示:工程里如果同时存在仿真用的 .hex 和综合用的 .coe,务必确认综合实际读的是哪一个。改了一个忘了另一个,是最常见的“波形怎么都不对”的源头。

2.2 相位累加器与查找表:DDS 的数学怎么落成 RTL

DDS 的核心就一个公式:输出频率f_out = FCW × f_clk / 2^N,其中 N 是相位累加器位宽,FCW 是频率控制字。它的直接推论是频率分辨率f_clk / 2^N——125 MHz 时钟配 32 位累加器,分辨率约 0.029 Hz,这个数字决定了你能不能“卡”到严格相干的频点上,后面第 4 章会反复用到。

工程里的 dds_core 一般长这样,我按可综合的写法整理过:

module dds_core #( parameter PHASE_W = 32, // 相位累加器位宽,决定频率分辨率 parameter LUT_AW = 12, // 查找表地址位宽,决定相位截断杂散 parameter DATA_W = 14 // 幅度位宽,对齐 DAC 位宽 )( input wire clk, input wire rst_n, input wire [PHASE_W-1:0] fcw, // 频率控制字,运行时可变 input wire [PHASE_W-1:0] pinc, // 相位偏移,双通道正交/双音时用 output wire [DATA_W-1:0] dac_data, output wire dac_valid ); reg [PHASE_W-1:0] phase_acc; wire [PHASE_W-1:0] phase_sum = phase_acc + pinc; wire [LUT_AW-1:0] lut_addr = phase_sum[PHASE_W-1 -: LUT_AW]; // 取高位做截断 reg [DATA_W-1:0] sine_lut [0:(1<<LUT_AW)-1]; reg [DATA_W-1:0] lut_q; reg [1:0] vld_sr; initial $readmemh("sine_lut.hex", sine_lut); // 综合前务必确认路径有效 always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= {PHASE_W{1'b0}}; else phase_acc <= phase_sum; // 每拍累加一次,溢出即一个周期 end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin lut_q <= {DATA_W{1'b0}}; vld_sr <= 2'b00; end else begin lut_q <= sine_lut[lut_addr]; // BRAM 读,固定 1 拍延迟 vld_sr <= {vld_sr[0], 1'b1}; // valid 跟着数据一起打拍 end end assign dac_data = lut_q; assign dac_valid = vld_sr[1]; // 与相位→地址→数据的总延迟对齐 endmodule

这段代码里有三个必须理解的点。第一,phase_sum[PHASE_W-1 -: LUT_AW]是取相位的高 LUT_AW 位做地址,低位被丢掉,这就是“相位截断”。经验上地址每多 1 bit,截断引入的最坏杂散改善约 6 dB,但同时 BRAM 用量翻倍;12 bit 地址配 14 bit 数据,在很多板子上是资源和指标折中的甜点。第二,vld_sr打了两拍,是因为从相位到 BRAM 输出一共两拍延迟;如果你的 LUT 用组合读(分布式 RAM)实现,valid 只打一拍就够,多打一拍反而会让第一个数据点错位一格,看上去就是“波形整体挪了一点”。第三,pinc不是摆设:做双音互调测试时,用两路 DDS 各给一个 FCW,把两路数据相加再送 DAC;做 IQ 正交时,让第二路pinc = 2^(N-2)就是 90 度相移。

参数怎么算,举个例子:f_clk = 125 MHz、N = 32、目标f_out = 1 MHz,则FCW = 1e6 × 2^32 / 125e6 = 34359738.368,取整 34359738,实际输出 999999.98 Hz,偏差 0.02 Hz。这个偏差看起来微不足道,但当你要求严格相干采样时,它就是频谱泄漏的根源。

2.3 DAC 侧发送:时钟转发、数据对齐和 valid 数拍

DDS 算得再对,DAC 接口接错了也是白搭。并行高速 DAC 的标准接法是“源同步”:把内部时钟经 ODDR 转发到管脚,作为 DAC 的采样时钟,数据用另一组输出寄存器同时送出,靠 PCB 上等长布线保证建立/保持。FPGA 内部千万不要让工具把时钟和数据走两条不同的路径出去,否则偏斜随温度漂。

// 时钟转发:用 ODDR 把系统时钟送到管脚,避免时钟走全局缓冲后偏斜失控 ODDR #( .DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC") ) u_dac_clk_fwd ( .Q (dac_clk_p), // 直接接 OBUFDS 或单端输出 .C (clk_dac), .CE(1'b1), .D1(1'b1), .D2(1'b0), // 双沿都翻转,得到与 clk_dac 同频的方波 .R (1'b0), .S(1'b0) );

数据侧一般再打一拍输出寄存器,让数据和转发时钟从同一个 IOB 出发:

# 输出方向按源同步写,具体数值必须按 DAC 手册的建立/保持时间换算,不能照抄 set_output_delay -clock [get_clocks dac_clk_tx] -max 1.2 [get_ports dac_data*] set_output_delay -clock [get_clocks dac_clk_tx] -min -0.8 [get_ports dac_data*]

还有两个容易忽略的细节。一个是数据格式:多数高速 DAC 吃的是偏移二进制(offset binary),而 DDS 里累加器自然产生的是二进制补码,中间要么加半量程偏置,要么翻转最高位,少做这一步,输出就是一个单极性半波,看上去“频率对了但只有半个正弦”。另一个是更新率:串行接口的 DAC 建立时间通常在微秒量级,用它做 1 MHz 以上的正弦,幅度会被压得很惨,这不是 DDS 的问题,是接口带宽的问题。

3. 把波形收回来:ADC 采样、触发抓数与上位机取数

发送端通了,接着是接收端。这一段是整个闭环里最容易被低估的部分:很多人以为 ADC 只要时钟给上、数据接上就能出数,实际上位序、眼图中心、跨时钟域这三件事里任何一件没处理,你看到的频谱都会是一团糊。判断自己有没有做对,有个简单标准——输入一个干净的正弦,频谱上除了基波和已知谐波,底噪应该是平的;如果底噪有明显鼓包或者成对出现的小峰,八成是采样侧的问题。

3.1 ADC 接口与采样时钟:先算抖动,再谈位宽

采样时钟的质量直接决定 SNR 上限,公式是SNR_jitter = -20log10(2π × f_in × t_jitter)。代入两个数感受一下:输入 1 MHz、时钟抖动 1 ps,理论上限约 104 dB;输入 100 MHz、同样 1 ps,上限掉到 64 dB。这条曲线说明一件事——评估一块 14 位 ADC(理论 SNR 约 86 dB)时,如果你在 100 MHz 输入下测出 64 dB,那不一定是板子坏了,可能是时钟或信号源的抖动已经把你限死了。选测试频点时必须先做这个估算。

并行 CMOS 输出的 ADC 一般随路送一个 DCO 时钟,接收侧用 IDDR 在双沿采样,再把两拍数据拼成 2×N 位:

// DCO 反相后作为采样时钟,保证采在数据眼图中心 IDDR #( .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), .INIT_Q1(1'b0), .INIT_Q2(1'b0), .SRTYPE("SYNC") ) u_adc_iddr ( .Q1(adc_d0), .Q2(adc_d1), // 同一时钟沿采到的两个相邻样本 .C (adc_dco_buf), .CE(1'b1), .D (adc_data_in), .R(1'b0), .S(1'b0) );

拼接时位序错了,波形会“像被洗过一样”,频谱上出现对称的镜像杂散,这是血泪经验里最典型的一种。处理办法是打开工程里的 IDELAY 扫描逻辑(或者手工改 tap 值),把延迟从 0 扫到最大,每档算一次 SNR,取 SNR 最高那一档的中间值作为工作点——眼图中心找到了,位序问题通常也一并暴露。

串行 LVDS 输出的 ADC 则是另一套:用 ISERDES 做串并转换,注意 bitslip 对齐字边界,帧头对不上时数据会整体循环移位,表现是“波形对但幅度乱跳”。这类接口在工程里一般有独立的状态机,先把 lock 信号拉高再开始采集。

3.2 触发与深存储:怎么保证抓到的 8192 点不是半截

自由跑采集的问题在于你无法控制起点。做 FFT 虽然不要求从正弦的零点开始,但要求这段数据是“连续、不丢点、不含复位毛刺”的。工程里通常的做法是:内部用一块 8192×16 bit 的 BRAM 做深存储,写入由 ADC 采样使能驱动,触发条件可以是软件下发的一个脉冲,也可以是电平跨越阈值;预触发深度设 50%,这样触发点前后的波形都能看到。

跨时钟域是这一步的关键。ADC 数据在采样时钟域,BRAM 和上传逻辑在系统时钟域,中间必须过异步 FIFO。FIFO 深度不够、或者读侧在 FIFO 空的时候硬读,结果就是数据里随机丢点——丢点在时域上几乎看不出来,在频域上表现为底噪整体抬高 3 到 6 dB,非常像“ADC 本身性能不行”。判断方法:把一个已知频率的相干信号灌进去,看基波峰值是否稳定;如果峰值每次采集抖动超过 0.5 dB,先查丢点,别急着换芯片。

存满之后停止写入、置一个 ready 标志,等上位机把数据搬走再清标志重新开始。这个握手如果做成“边写边读”,很容易读到半新半旧的两段数据拼在一起,相位不连续,频谱上会出现宽裙边。

3.3 取数到上位机:把 8192 点搬进 Python

传输链路的选择取决于你的工程里已经有什么。常见的是 UART:8192 点 × 2 字节 = 16 KB,115200 波特率下 8N1 每字节占 10 bit,满打满算要 1.4 秒;换成 921600 就只要 0.18 秒。这个时间对调试体验影响很大——每次改参数等一秒半,一天下来耐心就磨没了。有条件的话优先走 USB 或网口,串口留给兜底。

Python 侧收到的是裸二进制,直接按 16 位小端解析,然后进 FFT:

import numpy as np FS = 100e6 # ADC 实际采样率,必须与你配置的时钟一致 N = 8192 # 单次分析点数,取 2 的幂 def load_samples(path): raw = np.fromfile(path, dtype='<u2') # 无符号 16 位小端 return raw.astype(np.float64) def tone_metrics(x, fs=FS, harmonics=5, half_lobe=3): n = len(x) x = x - x.mean() # 去直流,否则 DC 泄漏会污染邻近 bin w = np.blackman(n) # 非严格相干时用窗压泄漏 spec = np.abs(np.fft.rfft(x * w)) ** 2 k = int(np.argmax(spec[1:]) + 1) # 跳过 DC 找基波 lo, hi = max(k - half_lobe, 1), k + half_lobe psig = spec[lo:hi + 1].sum() # 基波主瓣功率 mask = np.ones_like(spec, dtype=bool) mask[0:2] = False # DC 与 1 号 bin 不计入噪声 mask[lo:hi + 1] = False hpow = 0.0 for h in range(2, harmonics + 1): c = int(round(k * h)) if c + half_lobe >= len(spec): break hpow += spec[c - half_lobe:c + half_lobe + 1].sum() mask[c - half_lobe:c + half_lobe + 1] = False # 谐波单独统计 pnoise = spec[mask].sum() snr = 10 * np.log10(psig / pnoise) sinad = 10 * np.log10(psig / (pnoise + hpow)) sfdr = 10 * np.log10(psig / spec[1:][mask[1:]].max()) enob = (sinad - 1.76) / 6.02 return dict(bin=k, f=k * fs / n, snr=snr, sinad=sinad, sfdr=sfdr, enob=enob) if __name__ == "__main__": print(tone_metrics(load_samples("cap_8192.bin")))

几个参数要盯住。half_lobe=3表示基波主瓣按 ±3 个 bin 统计,用 Blackman 窗时主瓣宽度约 6 个 bin,取 3 正好覆盖一半;如果你换成 Hann 窗,可以收到 2。harmonics=5是排除前 5 次谐波,测 ADC 时一般排到 5 次足够,测 DAC 的镜像时则要单独处理。mask[0:2] = False把 DC 和 1 号 bin 排除掉,是因为加了窗之后直流会向邻近泄漏,不排的话低频噪声会被算高。

注意:加窗是为了在非相干条件下看趋势,带窗算出来的 SNR 会偏低 1 到 2 dB。要报正式指标,必须先让相干采样条件严格成立,再把窗换成全 1 重算一遍。

4. 参数怎么定:频点、采样率、点数与窗函数的取值对照表

同一块板子,换一组测试参数,SNR 能差出 10 dB 以上,这是很多“指标对不上”争议的来源。所以动手之前,先把频率控制字、相干条件、点数、窗函数这四件事一次性定死,写进测试记录表,后面所有数据都基于同一口径。

4.1 频率控制字与实际频点:先算清楚再谈指标

DDS 的频率分辨率是f_clk / 2^N,这意味着你想要的任何频点都会被量化到最近的格点上。做单音测试时这没什么问题;但当你同时用 ADC 的采样率去卡相干条件时,量化误差就变成了频谱泄漏。所以正确的顺序是:先由 FCW 反算实际频点,再拿这个实际频点去设计相干采样。

CLK_DDS = 125e6 # DDS/DAC 系统时钟 PHASE_W = 32 # 相位累加器位宽 FS_ADC = 100e6 # ADC 采样率 N_FFT = 8192 # 分析点数 def fcw_for(f_target, clk=CLK_DDS, w=PHASE_W): fcw = int(round(f_target * (1 << w) / clk)) f_real = fcw * clk / (1 << w) # 量化后的真实频点 return fcw, f_real def coherent_pick(n_fft=N_FFT, fs=FS_ADC, m_min=60): # 选一个与 n_fft 互质的 m,落在合适频段,减少谐波折叠到基波附近 for m in range(m_min, m_min + 200): if np.gcd(m, n_fft) == 1: yield m, m * fs / n_fft

这里m和n_fft互质是相干采样的核心条件:一个周期内采到的 N 个点刚好覆盖信号的整数个周期,且采样点在相位上均匀铺开,不重复。若m与n_fft有公因子,采样点会在相位上“扎堆”,等效于降低了有效分辨率。

我一般会固定N_FFT = 8192、在m60 到 260 之间扫,挑一个既落在关心的频段、谐波又不会折回基波附近的。折回这件事很容易被忽略:如果 3 次谐波的频率超过fs/2,它会镜像回来落在某个位置,万一正好压在基波或 2 次谐波上,你算出来的 SINAD 就是错的。

4.2 相干采样与窗函数:频谱泄漏该由谁背

理想情况下,相干采样成立就不需要窗函数,矩形窗的能量最集中,SNR 也最准。但现实是 DDS 的 FCW 量化、ADC 时钟与 DDS 时钟不同源带来的微小频差、以及时钟本身的漂移,都会让相干条件“差不多但不严格”。这时候泄漏就会发生:基波能量向邻近 bin 扩散,主瓣变宽,噪声统计被污染。

判断方法很直接:把采集到的数据截两段各做 FFT,比较基波峰值位置。如果两次的峰值落在不同 bin,说明相干没锁住;如果落在同一 bin 但主瓣明显比理论宽,说明有频率漂移。前者换参数,后者只能加窗。

场景建议窗函数主瓣宽度(bin)代价
严格相干,用于报指标矩形窗2对频偏零容忍
相干但有时钟漂移Hann4SNR 偏低约 1.5 dB
非相干,日常看趋势Blackman6SNR 偏低约 2 dB,动态范围好
小信号 + 大信号共存Blackman-Harris8主瓣最宽,适合找杂散

选窗的原则是:你要看的是“底噪有多低”就用矩形或 Hann,你要看的是“有没有小杂散”就用 Blackman 系列。两者不要混着报数,更不要拿带窗的结果去和芯片手册的典型值直接对比。

4.3 SNR、SINAD、ENOB、SFDR 的口径必须统一

这四个指标经常被混用,但它们回答的是不同问题。SNR 只算噪声,把谐波也当成信号的一部分排除在外;SINAD 把谐波算进“非理想成分”,所以 SINAD 恒小于等于 SNR,两者之差反映了谐波失真有多严重;ENOB 由 SINAD 换算,ENOB = (SINAD - 1.76) / 6.02,它是最常被引用也最容易被滥用的数字;SFDR 是最差的那根杂散相对基波的高度,跟噪声无关,纯粹看有没有“尖峰”。

实操上有个坑:不同工具对“谐波排除窗口”的定义不同。有的按 ±1 bin,有的按 ±5 bin,同一组数据算出来 SINAD 能差 1 dB 以上。解决办法是在测试记录表里写清口径——主瓣 ±3 bin、谐波排到 5 次、DC 与 1 号 bin 排除、窗函数类型——后面所有数据都照这个来。这份工程文件里的 sw 目录通常就带这样一个配置头,别嫌它啰嗦,它是唯一能让三个月后的你复现出同样数字的东西。

还有一点,DAC 侧的指标和 ADC 侧要分开测。DAC 的关键是 SFDR 和通带平坦度(因为重建滤波器和 sin(x)/x 滚降),ADC 的关键是 SNR 和 ENOB。把两者混在一个闭环里测出来的数字,既不是 DAC 的也不是 ADC 的,只能叫“系统指标”,对外报的时候必须说清楚。

5. 联调避坑:DDS 到 ADDA 闭环上最容易翻车的五个点

下面这五条,是我在这类工程上反复见过的。每条都按“现象、原因、解决”写,遇到问题时可以直接对号入座。

5.1 波形侧:出不来、出歪了、幅度不对

一、输出恒定在量程中点,一点波形都没有。现象:示波器上看 DAC 输出是一条平直的直流,测出来正好是半量程电压。 原因:查找表没被正确初始化。$readmemh的相对路径在仿真时有效、综合时失效,或者综合工具把 BRAM 优化成了全零初值。此时地址怎么变,读出来的都是 0,再经过偏移二进制转换就成了中点。 解决:检查综合日志里$readmemh是否被识别,把 hex 文件转成 .coe 或用initial块加综合属性;最稳的办法是加一个自检计数器,上电后顺序读 16 个地址,通过串口打印出来看是不是正弦的前 16 个点。

二、频率完全正确,幅度只有一半,波形有明显台阶。现象:频谱上基波频率分毫不差,但幅度比预期低 6 dB,时域看波形上下不对称。 原因:位宽没对齐。DDS 输出 14 位,DAC 也是 14 位,但中间某处只接了 13 根线,或者把补码直接当偏移二进制送了出去,导致负半周被截掉。 解决:逐级核对位宽和格式转换点,补码转偏移二进制就是最高位取反(或加半量程),转换点只能有一处,多了会翻两次等于没翻。

三、低频正常,频率一上去幅度就塌。现象:1 MHz 时幅度正常,10 MHz 时掉了 3 dB,30 MHz 时几乎看不到波形。 原因:三个可能——DAC 重建滤波器截止频率太低;输出变压器或运放的压摆率不够;DAC 更新率本身不足,高频点每个周期只有几个采样点。另外 sin(x)/x 滚降在0.4×f_clk附近已经有约 2.7 dB 衰减,这是物理规律,不是故障。 解决:先把滤波器旁路测一次,排除滤波器因素;再算 DAC 的建立时间是否够;最后接受 sin(x)/x 的存在,在软件侧做预补偿(把 LUT 值乘上 1/sinc 的倒数),或者干脆把工作频段限制在0.1×f_clk以内。

5.2 数据侧:抓不全、算不准

四、抓回来的波形“像被洗过”,SNR 只有 30 dB 左右。现象:时域波形毛刺多、台阶乱,频谱底噪整体抬高,还伴随对称的镜像杂散。 原因:IDDR 拼接位序反了,或者采样时钟相位不对,采在了数据跳变沿上。这两种情况的表现极为相似,都是“数据大体对但细节全乱”。 解决:先做静态测试——让 DDS 输出一个极低频的锯齿(或直接让 DAC 输出递增码),这样每次采样值应该是确定的,位序错了会立刻看出规律性的跳变;确认位序没问题后再扫 IDELAY,按 SNR 曲线找眼图中心。这个流程走一遍大概二十分钟,比盲猜快得多。

五、ENOB 每次算都不一样,前后能差 1 bit 以上。现象:同一组硬件配置,连续采十次,ENOB 在 10.2 到 11.5 之间跳。 原因:采集不是严格相干的,每次触发点落在不同相位上,加窗后残余泄漏量随机变化;或者分析脚本里谐波排除窗口、DC 排除范围没固定;也可能是丢点导致的随机底噪抬升。 解决:固定三件事——采集点数必须是 2 的幂、触发后先检查数据连续性(相邻点差分不应出现异常大跳变)、分析脚本的参数写死在配置文件里。做完这三步,重复性通常能收到 ±0.1 bit 以内。如果还跳,回去查 FIFO 有没有溢出标志被忽略。

提示:把每次测试的 FCW、实际频点、采样率、点数、窗函数、脚本版本号一起记进 csv,出问题时按时间倒查,比翻聊天记录靠谱。

6. 扫频与双音:把工程的可信度从“能出波形”推到“指标可复现”

单点测试通过,只说明这套工程在那个频点上能跑。要判断它值不值得作为长期使用的测试平台,还得做两件事:扫频和双音。

扫频的做法是把 FCW 从一个下限步进到上限,每一步采一组数据、算一次 SNR 和基波幅度,最后画两条曲线:幅度平坦度和 SNR 随频率的变化。这两条曲线能暴露很多单点测试看不见的东西——重建滤波器的实际截止点、运放开始压摆的频点、以及 ADC 的 SNR 随输入频率下降的斜率是否和抖动估算吻合。我一般会取 1 MHz 到 40 MHz、每 1 MHz 一个点,40 个点跑完大约五分钟,得到的曲线比任何手册都贴合你手上这块板子。扫频时记得把每步的 FCW 和实际频点都存下来,DDS 的量化会让实际频点略低于目标值,画图时用实际值否则曲线会有细小的锯齿。

双音测试用来量互调。做法是例化两路 DDS,FCW 分别对应 f1 和 f2,两路数据相加后送 DAC(注意加完后要饱和或右移一位防止溢出),FFT 里重点看 2f1-f2 和 2f2-f1 两个三阶产物相对基波的高度,那就是 IMD3。两个频点的间隔有讲究:太近了落在同一个主瓣里分不开,太远了又可能被滤波器滚降影响幅度,我一般取间隔为采样率的 1/40 左右,并且让 f1、f2 都满足与采样率互质的前提。双音对 DAC 的要求比单音高得多,很多在单音下 SFDR 漂亮的板子,一上双音三阶产物就冒出来了,这不是工程错了,是它本来就该暴露。

最后一个我踩过的坑:扫频跑完发现 20 MHz 附近 SNR 突然掉 4 dB,查了半天硬件,最后发现是那个频点的 FCW 量化后刚好和采样率构成了近似整数比,相干条件被破坏,泄漏进了噪声统计。从那以后我每次做扫频之前,都强制先用第 4 章那个脚本把每个频点的实际值和互质条件过一遍,不满足就跳到下一个格点,宁可曲线少一个点也不留一个假坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询