
简介这份MATLAB程序面向信号处理、数据转换器与通信测试人员用于计算SNR、SNDR、THD、ENOB、SFDR五项核心指标可快速评估信号质量与系统线性度。程序从txt文件读取采样数据经过预处理和FFT频谱分析分离信号、噪声及谐波分量输出各项参数结果适用于ADC性能测评、音频分析、振动监测等场景。压缩包共2个文件含1个txt数据文件和1个m脚本整体仅4KB轻量简洁txt文件提供测试数据m脚本实现完整计算流程便于直接运行与二次修改。已有4556人学习下载。借助该程序读者既能理解各指标的理论定义又能掌握MATLAB工程实现思路可作为信号处理项目中的实用工具模板。1. SNR、SNDR、THD、ENOB、SFDRADC数据到手五个指标一次算清拿到ADC采样数据后很多人第一件事就是想算动态性能然后发现自己被SNR、SNDR、THD、ENOB、SFDR这五个缩写绕晕了。更麻烦的是同样的数据用不同的MATLAB计算程序跑结果能差出两三dB。不是算法不成熟而是频谱分析里那些细枝末节——相干采样、窗函数、谐波区间、单边谱归一化只要有一个不对ENOB就可能虚高或者偏低。这篇文章要讲的就是一套可靠的数据处理流程从原始采样数据出发用MATLAB把五个指标一次算出来并把参数选择、代码逻辑和常见的坑一起说清楚。新手可以直接照代码搭熟手也能快速定位自己脚本里的边界问题。2. 五个指标的数学定义公式写不对别指望程序能算对2.1 从时域到频域为什么用频谱统计而不用滤波写程序之前必须先把定义钉死。SNR、SNDR、THD、ENOB、SFDR表面上描述的是“信号、噪声、失真”之间的关系实际计算时都要落到功率谱上。常见做法是对ADC采集的数据做FFT得到功率谱然后把每个频点归类到“基波信号区”“谐波区”“噪声与杂散区”三块区域再用区域能量做比值。只有把指标映射到频域程序才可复现——时域滤波很难区分信号和靠近信号的相位噪声而FFT天然把各频率成分分开。五个指标在功率谱上的分母分子关系可以整理成下表程序里的统计方式就是照着这张表写的指标信号功率分母被当作“坏功率”的部分SNR基波频带总功率除基波、谐波、DC之外的噪声与杂散功率SNDR基波频带总功率噪声功率 谐波功率 非谐波杂散功率THD基波频带总功率各次谐波功率之和dBc表示时为负值SFDR基波频带总功率除基波与DC外的最大单根频谱线功率ENOB(SNDR_dB − 1.76) / 6.02无关于THD要特别提醒一句主流习惯把THD写成谐波功率和与基波功率的比值工程报告里用“−80 dBc”这种写法意思是谐波功率比基波低80 dB。有些地方也会把THD反过来写成基波比上谐波得到正的dB值。两种写法没有对错但程序里必须全程统一否则前后差着一个负号很容易让看报告的人以为失真很小。SNR和SNDR的差别是很多人第一个翻车点。SNR的分母只算“纯噪声和非谐波杂散”谐波被排除在外SNDR的分母把谐波也包括进去。所以对同一份数据SNDR永远小于等于SNR两者相差越大说明失真越严重。ENOB则由SNDR换算出来本质是把动态范围折算成“理想ADC的有效位数”。1.76和6.02来自理想N位ADC满量程正弦的理论信噪比公式是工程上的标准简化值。2.2 相干采样程序能不能算准的前提理论上FFT本身不算错但频谱泄漏能把结果搅成一锅粥。所谓频谱泄漏就是输入信号频率不在FFT分辨率的整数倍位置上能量从一个频率点扩散到整条频谱底基波附近的能量会同时撑高“信号区”和“噪声区”SNR、SNDR全都会失真。相干采样是解决这个问题的前提。数学表达是下面这个条件f_in / (fs / N) M其中fs是采样率N是FFT点数M是正整数。当M为整数时N点采样窗内刚好包含M个完整周期输入信号频率正好落在第M根谱线上没有泄漏。除了整数约束M最好和N互质这样各次谐波不会在频率轴上和基波或其他杂散撞在一起。举一个具体例子fs 100 MHzN 16384那么分辨率是fs/N ≈ 6.1035 kHz。如果输入信号频率f_in选10 MHzM 10 MHz / 6.1035 kHz ≈ 1638.4不是整数直接测必然泄漏。正确的做法是先定M再反推f_in。比如取M 2003则f_in 100e6 × 2003 / 16384 ≈ 12.22 MHz整数周期落在窗内后续不管用不用窗频谱底都会干净很多。2.3 从频谱到功率归一化与单边化不能省FFT输出的幅值和信号的物理幅值之间隔着一个因子而这个因子跟窗函数有关。为了让程序不依赖窗函数的幅值我习惯直接用周期图法估计功率谱P(k) |X(k)|² / (N × Σw²)其中w是窗函数序列。这个归一化满足Parseval定理把P(k)在整个频带上求和得到的就是加窗后的时域平均功率再经过等效噪声带宽修正能还原原始信号的总功率。用这个定义之后无论加什么窗后面统计功率都不用再单独乘修正系数代码会清爽很多。对实数采样序列FFT结果双边对称通常只取正频率部分中间每个bin要乘2DC和Nyquist不乘。这段处理听起来小但漏了它SNDR会直接差一倍功率算出来低3 dB。正确做法是先得到完整功率谱P再取前N/21个点并把第2到倒数第2个点各自乘2。做完这步得到的p(k)就是后续所有指标统计的原始依据。3. MATLAB计算程序从原始采样数据到五参数输出3.1 数据准备参数声明与测试信号注入先给一个可以直接运行的脚本主体。下面代码用仿真数据替代真实ADC采样目的是验证算法实际测试时把构造x的那几行删掉换成从文件或数据采集卡导入的列向量即可。% adc_dyn_params.m fs 100e6; % 采样率 N 16384; % FFT点数 M 2003; % 相干采样周期数取与N互质的质数 f_in fs * M / N; % 输入信号频率 A 0.9; % 信号幅度约 -0.92 dBFS % ---- 仿真数据真实ADC测试时替换为采集序列 ---- rng(1); % 固定随机种子便于复现 t (0:N-1) / fs; x A * sin(2*pi*f_in*t 0.1); % 基波 x x 0.001*A * sin(2*pi*2*f_in*t); % 二次谐波约 -60 dBc x x 0.0005*A * sin(2*pi*3*f_in*t); % 三次谐波约 -66 dBc x x 0.0002 * randn(N,1); % 白噪声这段代码里M2003并不是拍脑袋选的。它满足两个条件一是2003是质数和N16384互质二是f_in约12.22 MHz在0.1倍到0.3倍fs之间谐波不会过早折叠回基波附近。真实ADC测试中M还需要避开信号源和采样钟之间的整数分频关系但这属于平台调优。替换为真实数据时注意x必须是列向量长度与N一致。如果采集数据点数超过N就先截一段最平稳的N点如果点数不足就重新组织采集参数而不是强行在FFT里补零补零会引入额外频谱结构测量结果不可靠。3.2 加窗与FFT功率谱归一化的两个细节去直流、加窗、FFT、单边化这四步必须按顺序做顺序反了会出现直流泄漏污染低频噪声的问题。% 去直流把均值当作直流偏置减掉 x x - mean(x); % 加窗Hann窗主瓣窄且旁瓣衰减快 w hanning(N); xw x .* w; % FFT X fft(xw, N); % 周期图功率谱满足Parseval定理 P abs(X).^2 / (N * sum(w.^2)); % 单边化DC与Nyquist只出现一次正频率其余bin乘2 p P(1:N/21); p(2:end-1) 2 * p(2:end-1); p(1) 0; % 去掉DC bin避免直流尖峰干扰后续搜索 freq (0:N/2) * fs / N;这里去直流不是可选项。ADC输入通常有直流偏置如果不减DC会成为一个巨大的尖峰其泄漏会把低频段的噪声底抬高。我在代码里还把p(1)直接置零这样后面搜最大杂散时DC尖峰不会误当成杂散。窗函数选择是另一个细节。如果实测信号严格满足相干采样理论上用矩形窗也就是wones(N,1)分辨率最高。但工程中信号源和采样时钟很难完全同步总有微小频率偏差这时矩形窗的旁瓣以1/f衰减基波能量会漏得很远。Hann窗的旁瓣衰减更快主瓣只宽一点是ADC测试里的稳妥折中。我在这里选Hann窗但后面的bin_r参数必须和窗类型配合这一点在第5章避坑里会说清楚。3.3 切割频带基波、谐波、噪声分区的mask得到功率谱之后核心工作就是把频带切成三个区。这个步骤我强烈建议用二值mask来做而不是逐个频点if判断代码可读性好后面调试也方便。bin_r 8; % 目标频点两侧剔除的bin数Hann窗取8足够 % ---- 基波定位先限定f_in附近窗口避免谐波强于基波时误判 ---- f_lo f_in - fs/50; f_hi f_in fs/50; roi find(freq f_lo freq f_hi); [~, idx] max(p(roi)); k_peak roi(idx); % 基波mask base_mask false(size(p)); base_mask(max(2,k_peak-bin_r):min(length(p)-1,k_peakbin_r)) true; % ---- 谐波mask考虑2到6次谐波注意频谱镜像 ---- harm_mask false(size(p)); for h 2:6 f_h f_in * h; f_h_mod mod(f_h, fs); % 超过采样率时折叠回第一Nyquist区 if f_h_mod fs/2 f_h_mod fs - f_h_mod; end k_h round(f_h_mod / (fs/N)) 1; lo max(2, k_h - bin_r); hi min(length(p)-1, k_h bin_r); harm_mask(lo:hi) true; end harm_mask harm_mask ~base_mask; % 防止谐波区占用基波区 % ---- 噪声mask剩下的都是噪声与非谐波杂散 ---- noise_mask true(size(p)); noise_mask(1) false; % 去掉DC noise_mask(base_mask) false; noise_mask(harm_mask) false;谐波镜像处理是这个函数里最容易被删掉的一行。很多人觉得二次谐波频率是2×f_in直接按这个频率索引bin就行没考虑它超过fs/2的情况。实际上FFT频谱是周期性的超过Nyquist频率的分量会镜像回低频区比如f_in30 MHz、fs100 MHz时二次谐波60 MHz会出现在40 MHz位置。不处理镜像THD会低得离谱SFDR也会找错对象。bin_r8对Hann窗来说是保守值。Hann窗半主瓣只有2个bin左右留8个bin是为了覆盖信号源相位噪声和微小非相干带来的额外泄漏。如果换成Blackman-Harris窗建议把bin_r提到12到16。这个值直接决定基波和谐波的能量有没有被完整纳入统计太小会把谐波能量漏进噪声区太大又会把噪声划进信号区。3.4 五参数计算与结果输出mask建立好指标计算就只是几次向量求和了。我把SFDR的搜索也放在这段因为它需要排除基波区后再找最大值。% 三大区域功率 sig_power sum(p(base_mask)); harm_power sum(p(harm_mask)); noise_power sum(p(noise_mask)); % 最大杂散排除DC和基波区谐波也算杂散 spur_mask true(size(p)); spur_mask(1) false; spur_mask(base_mask) false; [spur_power, k_spur] max(p(spur_mask)); % 五参数 SNR 10*log10(sig_power / noise_power); SNDR 10*log10(sig_power / (noise_power harm_power)); THD 10*log10(harm_power / sig_power); ENOB (SNDR - 1.76) / 6.02; SFDR 10*log10(sig_power / spur_power); fprintf(SNR %.2f dB\n, SNR); fprintf(SNDR %.2f dB\n, SNDR); fprintf(THD %.2f dBc\n, THD); fprintf(ENOB %.2f bit\n, ENOB); fprintf(SFDR %.2f dBc (f_spur%.2f MHz)\n, SFDR, freq(k_spur)/1e6);这段逻辑有一个值得注意的设计选择SNR的分母里放了非谐波杂散严格说这是“含杂散的SNR”。如果被测系统有明显的时钟杂散算出来的SNR会比按更严格标准剔除杂散之后的结果低一些。工程对比测试可以接受这个口径但如果要出正式报告应该把杂散逐根识别并排除。第5章的避坑会继续展开这个问题。运行这组仿真代码会得到SNR约70 dB、SNDR约58 dB、ENOB约9.4 bit的结果。这个数字和前面信号模型的理论估算接近说明程序框架是对的。真实ADC芯片手册里ENOB通常标到10到12 bit前提是输入信号、采样时钟、电源都足够干净而我们的仿真数据故意把谐波做到−60 dBc属于偏差的“被测件”所以ENOB低是意料之中。4. 参数设置采样率、窗函数与信号幅度怎么配合4.1 采样点数N与信号频率f_in的配对原则相干采样的公式f_in fs × M / N看起来简单实际取值时有三条约束。N最好取2的幂这样FFT算法效率最高M取与N互质的奇数避免谐波与基波的谱线重叠f_in落在一个合适的频率范围太高会导致高次谐波折叠后混入基波附近太低则容易受低频1/f噪声影响。我一般控制f_in在0.03 fs到0.3 fs之间。下面给几组我常用的示例参数你可以按照同样思路自行组合fs (MHz)NMf_in (MHz)f_in/fs100819299712.170.1210016384200312.220.1225032768200315.280.0625032768409331.220.12表格里的M都是质数N都是2的幂。第三行f_in/fs只有0.06偏低一般不建议低于0.03否则会用到低频段容易被电源纹波和1/f噪声干扰。10 MHz到30 MHz范围内的输入对大多数ADC测试平台来说也比较容易产生干净的信号。还有一点容易被忽略M并不是越大越好。M取值接近N/2时f_in接近Nyquist此时即便基波落在高频它的第二、第三次谐波也可能折叠回基波附近导致谐波区和基波区mask重叠。处理重叠的常见做法是增大bin_r或者减小谐波计算次数但最根本的方法还是把M取在N/8到N/4之间从源头避开。4.2 窗函数什么时候加窗什么时候别加如果输入信号源和采样时钟严格锁定相干采样条件被满足不加窗才是分辨率最高的方案。此时基波能量集中在单个bin上bin_r设成2就够噪声底不会被旁瓣抬起来。但现实中信号源频率漂移几十HzM就不再是整数基波能量沿频率轴散开不加窗的SNR测量结果会很不稳定。这时加窗是必要的但它有代价。我常用的选择是Hann窗原因有三主瓣只有4个bin宽对bin_r的压力小旁瓣衰减快泄漏控制好相干增益为0.5算法稳定不易产生数值问题。如果对SFDR测试中的小杂散检出有更高要求可以用Blackman-Harris窗旁瓣衰减更高但主瓣宽度接近Hann的两倍bin_r也必须相应加大。判断要不要加窗有个经验做法先跑一遍FFT看基波附近的功率谱底是否平坦。如果基波两侧3到5个bin内能量明显高于远处噪声底说明存在泄漏需要加窗或先校正频率。如果频谱底平整矩形窗的结果也未尝不可。我自己在做一致性测试时通常统一固定Hann窗和bin_r8这样不同批次的数据在同一个口径下比较波动最小。4.3 信号幅度满量程选择影响ENOB上限输入信号的幅度会直接影响量化噪声和失真的比例。幅度越小有效位数利用率越低SNR下降幅度太接近满量程ADC进入削顶区THD会突然变差。我见过有工程师为了“充分激励ADC”把输入信号拧到满量程结果模数转换器前级放大器的二次谐波暴涨十几个dB还以为是ADC坏了。实际大多数ADC测试标准都建议输入幅度大约在−0.5到−1 dBFS即刚好低于满量程但不进入明显削顶区域。程序里分子取的是“测出来的基波功率”不是外部设置的幅度所以幅度大小本身不会直接影响SNR计算。幅度过低导致的问题是噪声统计不稳定当信号比噪声低到一定程度基波峰的搜索可能出现偏差特别是当ROI窗口内有其他尖峰时。幅度过高则会让谐波区的能量显著变大SNDR被拉低。把幅度调在−1 dBFS附近并用示波器或ADC自身输出监控是否出现削顶是一个稳妥的起步做法。另外要注意输入幅度在测试报告里要写明是多少dBFS否则别人无法复现你的测试。同一颗ADC−6 dBFS和−0.5 dBFS测出来的ENOB能差0.3到0.5 bit这不是程序的问题是测试条件问题。5. 避坑指南五个最容易翻车的细节5.1 现象基波搜索被大谐波带偏当被测ADC的THD严重到一定程度二次谐波功率会大于基波。此时程序里如果直接用max(p)搜全频谱最大峰基波位置就会跳到二次谐波上。基波mask划错信号功率、谐波功率、噪声分区全部连锁错乱ENOB算出来可能比真实值高出一大截。原因频谱峰值搜索没有输入先验信息算法不知道哪个峰才是想要的基波。 解决先利用已知的信号源频率f_in圈定ROI窗口只在这个窗口里找局部最大值。代码里我用的是f_in ± fs/50相当于±2%的相对带宽。如果信号源有频偏可以适当放宽窗口但别放宽到覆盖二次谐波。5.2 现象加窗后忘记调整bin_r换窗结果突变有人把代码从Hann窗换到Blackman-Harris窗之后同一份数据的SNDR突然低了0.8 dB。查到最后是bin_r还停在8而Blackman-Harris主瓣宽度约±6个bin再加相位噪声扩展部分谐波能量漏到了噪声区被当成噪声扣进了SNDR分母。原因bin_r必须匹配窗函数的主瓣宽度和信号源的相位噪声。 解决每次换窗后画图看基波和谐波附近被mask覆盖的区间是否完整。也可以做一个扫描测试把bin_r从4逐步增到16观察SNR和SNDR随bin_r的变化曲线曲线进入平台区的位置就是当前数据下的合理值。平台之前的点都是mask覆盖不足。5.3 现象SNR和SNDR分母口径不一致报告对不上有些项目报告里的SNR根据“不含杂散”的严格口径计算去掉了所有非谐波尖峰而你的程序里noise_mask只排除了基波、谐波和DC非谐波杂散仍然算在噪声里。两者在同一份数据上可能差2到3 dB不是谁算错了是统计口径不同。原因SNR的定义在业界文档里有多种版本有的把杂散归入噪声有的单独剔除。 解决程序注释里写明口径。如果有明确的杂散尖峰可以做一个spur_mask把非谐波的窄带尖峰也排除后再算“纯噪声SNR”。但测试报告要同时给出SNDR因为SNDR口径相对统一能作为可比对的主指标。5.4 现象谐波频率超过Nyquist后没有镜像THD显著偏小输入频率高于fs/4时三次谐波就可能超过fs/2比如f_in30 MHz、fs100 MHz三次谐波90 MHz在数字谱上等效出现在10 MHz位置。如果程序直接索引90 MHz对应的bin实际没有意义谐波能量出现在10 MHz处THD算出来非常小。原因FFT频谱是周期的模拟域谐波在采样后等效频率要在第一Nyquist区内取镜像。 解决按3.3代码中的方式处理先mod到fs范围再判断是否需要fs减到镜像。这个处理只跟f_in和fs有关不增加复杂度而且适用于任意输入频率建议保留。5.5 现象FFT点数太少ENOB结果打摆子同一组ADC数据N1024和N32768算出的ENOB可能相差0.5 bit。点数少时每个噪声bin的带宽大统计起伏也大个别杂散尖峰的存在会显著改变噪声功率造成反复测试结果不稳定。原因噪声功率估计的方差与参与统计的bin数量成反比点数越多越接近真值。 解决至少取N≥8192并连续采集多帧分别计算ENOB。如果几帧之间ENOB波动超过0.1 bit先检查频谱泄漏和杂散再考虑是不是信号源本身不稳定。另外注意FFT点数同时决定了记录长度点数大了测试时间变长但相比结果可信度这点成本值得。6. 验证程序正确性用仿真信号自检再做正式测试程序写完后第一步不是急着接ADC数据而是用一个已知参数的仿真信号验证整套逻辑。我通常按三个等级来测先只放基波加白噪声验证SNR和理论值一致再加入固定幅度的二次和三次谐波验证THD和SFDR是否对得上预设值最后换几组不同的N和随机种子确认结果稳定。以第3章的仿真模型为例信号幅度0.9基波功率约为0.405。二次谐波幅度0.0009功率4.05e-7三次谐波幅度0.00045功率1.01e-7白噪声方差0.0002功率4e-8。理论上SNDR约58.7 dB对应ENOB约9.45 bit。程序输出接近这个数就说明FFT归一化、mask区间和公式都正常。如果差超过0.3 dB优先检查单边谱有没有漏乘2或者噪声区是否把基波旁瓣带进来了。验证稳定性的方法也很简单把rng的种子换掉跑5次记录每一帧的ENOB波动超过0.1 bit就要查测试条件。还有一种做法是把N从16384改成32768结果应该基本不变。如果N翻倍后ENOB明显变化多半是频谱泄漏在作怪这时优先检查相干采样频率而不是继续调窗函数。这个自检脚本我建议保留成模板每次测试不同频率或不同采样率的ADC时都先跑一遍。我自己曾在一个30 MHz输入信号的测试上翻过车当时谐波镜像处理写错了二次谐波60 MHz被程序索引到频谱末尾的无效位置THD算出来接近−90 dBc折腾了一天。后来把自检脚本固定成流程的一部分凡是修改采样率、输入频率或窗函数都先在仿真信号上过一遍再上实际数据。现在这个习惯帮我省掉了大量排查时间。希望这套程序和参数选择能帮你少走一次弯路。本文还有配套的精品资源点击获取