简介:用于计算信噪比(SNR)、信号噪声加失真比(SNDR)、总谐波失真(THD)、有效位数(ENOB)和无杂散动态范围(SFDR)的MATLAB脚本,面向信号质量评估、ADC性能测试及相关课程设计场景,适合需要量化分析频域参数的工程师和初学者。压缩包共2个文件,包含1个M代码脚本与1个TXT数据文件,整体仅4KB;脚本从TXT读取原始序列,完成滤波与加窗预处理,再借助FFT频谱分析分离谐波与杂散分量,最终输出各项指标并给出可复现的分析流程。已有4556人浏览学习,用户可对照代码深入理解各指标的物理意义与算法差异,也可将脚本迁移至自己的采集数据中进行快速验证。压缩包体量虽小,却覆盖了信号链性能评估的核心计算方法,对硬件调试、算法优化与理论学习均有直接参考价值。
1. SNR的MATLAB计算程序:先把采样条件理顺,指标才有意义
说到SNR的MATLAB计算程序,做ADC测试或信号链调试的工程师都会先想到那几个公式,但实际跑过一遍就知道,同样的采集数据,换一个FFT点数、换一种窗函数、甚至噪声bin的选取范围不同,算出来的SNR、SNDR、THD和ENOB能差出好几个dB。这套程序的价值不在于公式本身,而在于把相干采样、频谱预处理、谐波识别这些前置条件全部封装好,输入采样序列后直接得到五个动态指标以及定位好的频谱信息。适合需要评估ADC、DAC、Sigma-Delta调制器输出数据,或是在数字信号链里想快速确认性能边界的从业者。
2. 相干采样与FFT预处理:在算SNR之前,先把频谱“摆正”
2.1 相干采样的数学条件与三个数值约束
动态参数计算的第一步不是打开MATLAB敲FFT,而是先确认采样数据是否满足相干采样条件。相干采样的定义式很简洁
Fin / Fs = M / N其中Fin为输入信号频率,Fs为采样率,N为FFT点数,M为采样窗口内的整数周期数。工程上更看重三个约束:M和N必须是正整数,M与N互质,且M * Fin落在第M个FFT bin上(bin索引M+1,因为第1个bin是直流)。互质的目的是让量化噪声、时钟抖动等非理想因素尽可能均匀分布到各个频点,避免周期性噪声堆叠在部分bin上造成假杂散。
实际选参数时我一般先定N,比如4096或8192,再根据Fin/Fs的比例找一个接近的M。不要随手把Fin设成1MHz、Fs设成10MHz就开跑,那样得到的频谱是不对齐的,后面所有指标都会带上频谱泄漏误差。
2.2 窗函数的选择:相干采样下也未必能省
很多教程说“相干采样不需要加窗”,这句话只对理想正弦源成立。ADC前端的时钟抖动、PLL杂散、电源耦合噪声会让实际采样频率和理想Fs有微小偏差,这种情况下不加窗的频谱旁瓣会掩盖低幅度杂散,导致SFDR和THD被严重低估。
常见做法是:如果数据来源是理想的仿真输出,确定已经严格满足相干采样条件,用矩形窗,即不加窗,保留最高频率分辨率;如果数据来自真实采集卡或带有同步误差,优先选Hann窗或Blackman-Harris窗。Hann窗主瓣宽度约4个bin,对谐波幅度影响在0.1dB以内,适合SNR估算。Blackman-Harris窗旁瓣抑制更好,但主瓣更宽,对间隔很近的杂散分辨力下降。
加窗会在幅度谱上引入相干增益损失,计算时必须做幅度修正。代码里我通常把Hann窗的修正系数设为2,即幅值乘以2再参与功率求和,这样信号幅度的读取与不加窗时一致。
2.3 直流分量与频谱排序:功率计算前的小事
FFT结果里第一个bin是直流分量。如果ADC输入端存在直流失调,这个bin的功率会非常大,不剔除的话噪声功率虚高、SNR被拉低。计算动态指标时,直流bin必须从所有功率求和中排除,并且不能计入杂散。
另外要处理的是单边谱与双边谱的换算。MATLAB的fft输出是双边谱,取前N/2+1个点后,除了直流和奈奎斯特点之外的所有bin幅度都要乘以2,才是单边谱线性幅度。这个步骤很多人漏掉,漏掉后信号幅度显示只有实际值的一半,THD和SFDR的分贝值反而不受太大影响,但在同时打印信号幅度时会对不上数据手册。
% 单边谱幅度修正示例 X = fft(x); N = length(x); mag = abs(X(1:N/2+1)) / N; mag(2:end-1) = 2 * mag(2:end-1);这段代码做了幅度归一化与单边谱换算。除以N是为了让FFT结果对应正弦波的真实幅度,对中间所有bin乘2是因为双边谱能量分散在正负频率两侧,取单边后要合并。直流bin和奈奎斯特bin(末端最后一个点)不乘2。
3. SNR、SNDR、THD、SFDR和ENOB的计算流程:从幅度谱到五个指标
3.1 FFT幅度谱的规范化与bin定位
拿到频谱后,第一步是定位信号所在bin。在相干采样条件下,信号bin索引直接等于M+1,但为了兼顾非相干数据,我一般写成峰值搜索:在预期信号bin附近搜索幅度最大值,并返回精确索引,便于后续谐波定位。
实际使用中,直接取整数值很快。如果用了非整数倍周期采样,就会出现栅栏效应,信号峰值落在两个bin之间,此时应当在信号bin和紧邻的两个bin之间做抛物线插值估计真实峰值幅度。插入代码示例:
function result = calc_adc_dynamic(x, Fs, M, numH) N = length(x); X = fft(x); mag = abs(X(1:N/2+1)) / N; mag(2:end-1) = 2 * mag(2:end-1); if M > 0 sig_bin = M + 1; % 相干采样直接定位 else [~, sig_bin] = max(mag(2:end-1)); % 搜索峰值,去掉DC sig_bin = sig_bin + 1; end sig_amp = mag(sig_bin); sig_power = sig_amp^2;参数说明:x为输入采样序列,Fs为采样率,M为采样窗口内信号周期数,numH为要计入THD的谐波次数。M传0时启用峰值搜索模式。sig_power这里直接用幅度平方表示功率,没有除以2,原因是后续噪声和谐波也用同样的幅度平方累加,比值与分贝差不受影响,代码更简洁。
3.2 谐波分量识别与THD计算
谐波位置理论上在k*M+1(k=2,3,...),但实际硬件数据可能因非线性产生轻微偏移。绝大多数ADC测试场景谐波精确落在整数bin上,直接按倍数找bin即可。
需要确认的是谐波是否超过奈奎斯特频率,一旦超过会混叠回基带,位置不再是线性倍数关系。这时要先将谐波频率对Fs取模,再映射到bin索引。下面是完整谐波聚合流程:
harmonic_bins = zeros(1, numH - 1); harmonic_power = 0; for k = 2:numH f_harm = k * M * Fs / N; % 第k次谐波频率 f_alias = mod(f_harm, Fs); if f_alias > Fs / 2 f_alias = Fs - f_alias; % 折叠回奈奎斯特区间 end bin_h = round(f_alias / Fs * N) + 1; if bin_h >= 2 && bin_h <= N/2 harmonic_bins(k-1) = bin_h; harmonic_power = harmonic_power + mag(bin_h)^2; end end说明:谐波的频率搜索范围在kM±2个bin内,但工程上相干采样的理想数据直接锁定kM+1即可。谐波功率累加时用幅度平方,并且不对超出奈奎斯特范围的谐波做二次混叠校正以外的事。需要留意某些谐波可能与信号bin或其它杂散重合,那会导致THD偏大,后面避坑部分再单独说。
3.3 SNR与SNDR的区别及噪声功率计算
这是整个程序中最容易理解错的地方。SNDR计算的是信号功率与噪声加失真总功率之比,分母包含所有非信号、非直流的bin功率,谐波也在里面;而SNR的分母是噪声功率,不包括谐波,但通常包括随机噪声和除了规定谐波以外的有界杂散,具体取决于测试标准。
我的程序实现如下:
total_power = sum(mag(2:N/2).^2); % 排除直流与奈奎斯特 noise_power = total_power - sig_power - harmonic_power; SNR = 10 * log10(sig_power / noise_power); SNDR = 10 * log10(sig_power / (noise_power + harmonic_power));注意:如果同时存在多个非谐波杂散,按IEEE标准它们应纳入SNR的噪声项。有些规范要求SNR分母只含噪声不含杂散,但工程上分不清杂散来源时,通常把非谐波杂散算入SNR分母,并将最大杂散单独报告为SFDR。3.4 SFDR与ENOB的换算
SFDR定义是信号功率与最大杂散功率之比。这里的最大杂散指除信号和谐波以外的最大频谱分量,但如果杂散恰好落在谐波位置,算不算谐波就有讲究。我的原则是:只要不是信号本身的整数倍频率,一律当杂散看,最大杂散搜索范围覆盖非信号、非指定谐波的全部频点。
spur_mask = true(size(mag)); spur_mask(sig_bin) = false; spur_mask(harmonic_bins(harmonic_bins > 0)) = false; spur_power = max(mag(spur_mask).^2); SFDR = 10 * log10(sig_power / spur_power); ENOB = (SNDR - 1.76) / 6.02;ENOB是从SNDR换算来的有效位数,公式的1.76dB对应理想量化器的信噪比下限,6.02dB每bit的斜率只适用于满幅正弦输入。换算时如果SNDR单位是dBc,直接用;如果是dBFS,则要先加满幅回退值修正,否则ENOB会偏低。大多数数据手册的SNDR是dBc,直接算即可。
4. 避坑:相干采样、量化器与幅度的五个常见问题
4.1 FFT点数N与信号周期数M不互质,SNR起伏不定
现象:同样一组原始数据,将N从4096改成8192,SNR变化超过3dB。
原因:如果M和N不互质,量化噪声会周期性地聚集在某些bin上,等效于叠加了一个人为杂散,噪声功率被高估。这也是为什么手册上给出的性能参数总是在“互质+相干”条件下测量的。
解决:选参数时先计算最大公约数,保证gcd(M, N) = 1。MATLAB中直接检查gcd(M, N),不为1时调整M或N。程序内可以加一条警告语句,检测到不互质就提示“条件不满足相干采样,结果仅供参考”。
4.2 信号幅度过大导致ADC削波,THD虚高
现象:输入正弦波幅度接近满幅,计算出的THD突然从-80dBc恶化到-50dBc。
原因:ADC输入超出线性范围,输出信号出现限幅削波。削波产生大量高次谐波,并且能量分布与输入幅度不呈线性关系。
解决:回退输入幅度至少1dBFS,多数ADC数据手册的测试条件是在-1dBFS到-6dBFS之间。验证方法:将输入幅度降低6dB后重新计算,如果THD基本不变或改善超过6dB,则存在削波问题。程序可以将输入序列的最大值与量化满幅比较,自动提示是否接近削波边界。
4.3 量化器位数设置不当,算法结果与理想值对不上
现象:用理想16bit量化器生成数据,SNR算出来只有50dB,与理论值98dB差太多。
原因:数据可能被当作浮点连续信号处理了,并没有量化过程;或者量化范围设置不对,比如16bit ADC采样满幅为±1,但信号幅度只有0.01,实际有效量化位数远低于16bit。
解决:确认仿真的量化器输出是否真正经过舍入。范围设置上信号幅度至少要覆盖量化器满幅的50%以上,理想情况下满幅正弦输入时理论SNR约(6.02 * N + 1.76)dB。程序在导入数据时增加一个建议值显示,提醒使用者确认量化步长与信号幅度的匹配关系。
4.4 谐波范围选取不当,THD与SFDR互相污染
现象:固定取前10次谐波,但ADC输出存在明显的21次谐波,THD结果反而比只取前5次时更好。
原因:超过奈奎斯特频率的谐波发生混叠,混叠分量恰好落在信号附近或与大杂散重叠。THD分母中混叠谐波功率被重复计算,或某个强混叠谐波被排除在谐波集合之外,进入SFDR搜索范围,导致SFDR误报。
解决:先绘制频谱图观察谐波分布,确定主要谐波次数范围,再设置numH参数。常规ADC测试取2到9次谐波,但宽带Sigma-Delta调制器可能需要取到20次以上。程序支持手动输入谐波次数,同时会输出各次谐波功率表,方便核对。
4.5 直流分量与奈奎斯特点处理错误
现象:SNR在不同采样率下结果不一致,但信号频率和幅度都没变。
原因:FFT的直流bin直接计入噪声功率,或者奈奎斯特频率bin未乘2导致幅度被低估,最直接的影响是ENOB计算结果偏移0.1到0.3bit。
解决:统一在功率求和时排除直流bin和奈奎斯特点。该处理在程序内部已完成,但如果使用者拿这段代码去改其它脚本,很容易在循环边界少写一个点。建议所有频谱处理脚本都写成mag(2:end-1),从索引上杜绝边界问题。
5. 从脚本到函数:输入输出结构设计与批量处理
5.1 输入参数的定义与默认值
上面已经给出了核心计算逻辑,实际工程使用时要封装成带默认参数的函数,避免每次测试都重新设置一堆变量。推荐输入参数分组为:信号参数、FFT参数、算法参数、数据格式参数。
function result = analyze_adc_dynamic(x, params) arguments x double params.Fs double = 10e6 params.M double = 0 params.N double = length(x) params.numH double = 10 params.window char = 'rect' params.inputRange double = 1.5 end % 默认M=0表示自动峰值搜索 % window支持'rect'/'hann'/'bh'arguments块是MATLAB R2019b之后的语法,用于声明默认值并校验输入类型。使用params.Fs=10e6的好处是所有可调参数都集中在一个结构体里,调用时按需修改,不会出现脚本里到处都是全局变量。
5.2 输出结构体的组织
输出结果建议统一使用结构体,字段名与数据手册对应,方便批量后处理和生成报告。字段设计如下:
result.SNR: SNR值,单位dBc result.SNDR: SNDR值,单位dBc result.THD: THD值,单位dBc(谐波功率与信号功率之比) result.SFDR: SFDR值,单位dBc result.ENOB: ENOB值,单位bit result.signalBin: 信号所在bin索引 result.signalAmp: 信号线性幅度 result.harmonicBins: 谐波bin索引数组 result.harmonicPower: 各次谐波功率 result.spurBin: 最大杂散bin索引 result.spurAmp: 最大杂散线性幅度 result.spectrumMag: 修整后的单边幅度谱保留spectrumMag是为了让调用方直接绘图,不用重算FFT,省掉每次调试都要复制频谱修正代码的重复操作。harmonics数组单独列出也便于与频谱图核对。
5.3 批量导入数据:CSV、MAT、WAV三种来源
实际测试最常见的数据来源是逻辑分析仪导出的CSV、MATLAB仿真保存的MAT文件、或者音频采集卡的WAV文件。批量处理时先做一个统一的载入层,自动识别文件类型并返回标准化数据:
function x = load_signal_from_file(filename) [~, ~, ext] = fileparts(filename); switch lower(ext) case '.csv' raw = readmatrix(filename); x = raw(:, 2); % 第二列为采样数据 case '.mat' S = load(filename, 'x'); x = S.x; case '.wav' [x, Fs] = audioread(filename); otherwise error('不支持的文件格式'); end end这里需要注意CSV文件常常包含时间戳或电压单位等额外信息,直接readmatrix会把第一列时间当数据读进来,所以强制取第二列。MAT文件必须检查变量名,习惯上将变量命名为x,但不同工具导出的变量名可能不同,可以先用who查看再动态获取。
批量测试时我一般建一个文件名清单,循环调用主函数,并将所有指标汇总为一个表格:
fileList = dir('data/*.csv'); results = struct(); for i = 1:length(fileList) x = load_signal_from_file(fullfile(fileList(i).folder, fileList(i).name)); r = analyze_adc_dynamic(x, struct('Fs', 10e6, 'M', 421, 'numH', 10)); results(i).name = fileList(i).name; results(i).SNR = r.SNR; results(i).SNDR = r.SNDR; % 按需保留更多字段 end T = struct2table(results); writetable(T, 'dynamic_metrics_summary.csv');批量处理时最容易出错的是每份数据的采样率或输入频率与参数不匹配。建议在载入函数返回值中附带原始采样率信息,并在主函数里加入断言校验:如果数据的长度与声明N不一致,自动截断或报警,而不是静默计算出一个错误结果。
6. 用理想量化器验证算法正确性,再延伸报告与出图
6.1 理想ADC数据生成与参数自检
验证算法最快的方式是构造一个理想16bit ADC模型,用相干采样生成无噪声正弦数据,程序计算出的SNR应当逼近理论值98.08dB。若偏差超过0.5dB,说明频谱修正或噪声bin范围有问题。
N = 4096; M = 421; Fs = 10e6; Fin = M / N * Fs; t = (0:N-1).'/Fs; x = sin(2*pi*Fin*t); Q = 2^15; % 16bit有符号量化,半码 xq = round(x * Q) / Q; r = analyze_adc_dynamic(xq, struct('Fs', Fs, 'M', M)); fprintf('SNR=%.2fdB, SNDR=%.2fdB, ENOB=%.2fbit\n', r.SNR, r.SNDR, r.ENOB);这段验证代码里用的量化方式是最小值加均匀量化。生成的数据没有加噪声,因此SNDR与SNR差异极小,THD应低于-100dBc。看到ENOB在15.9bit以上就说明链路通了。
6.2 进一步自查:加入削波与谐波观察指标联动
验证算法正确解析数据后,再做两个破坏性测试:将信号幅度提高6dB迫使削波发生,观察THD明显恶化而SFDR可能变化不大;在信号中注入一个单频杂散,确认SFDR准确报告该杂散幅度,而SNR不受影响或按噪声谱密度计入。这一步能直观理解各指标如何对同一条频谱做不同切片。
6.3 从指标到报告:一次绘制频谱图加关键标注
最后建议把绘制频谱和标注谐波、信号、杂散的逻辑写进同一函数,每次跑完数据直接输出一张带参数标注的频谱图,方便在测试记录里贴图。
从那以后,我每次算SNR之前都会强制走一遍验证流程:先生成理想量化数据确认基线,再叠加一个已知杂散确认SFDR识别没有问题,最后才把跑测试数据的结果当作可信值写入报告。这个习惯帮我挡掉了至少三次因为参数设置错误导致的数据误判。希望帮到你。
本文还有配套的精品资源,点击获取