简介:面向信号处理、电子信息类领域的学习者与工程师,提供了一套MATLAB计算程序包,专门用于SNR(信噪比)、SNDR(信号到噪声加失真比)、THD(总谐波失真)、ENOB(有效位数)、SFDR(无杂散动态范围)五大指标的自动化计算与评估。资源由2个文件组成,包含解析txt数据的核心源码与一份示例数据,整体压缩包仅4KB,轻量易用,目录结构简单,适合电子工程、通信工程等相关专业学生及从业者快速部署,也能帮助理解FFT频域分析和谐波提取等信号处理方法。程序内置读取txt文本数据的预处理流程,可提取基波、谐波及杂散分量,并给出各参数计算结果,解决手动计算步骤繁琐、易出错的问题。已有4556人学习下载,m源码逻辑清晰,txt示例数据可直接运行验证,帮助读者从时域数据到频域指标建立完整链路;无论是评估ADC性能、验证通信链路,还是开展信号质量审计,都可替换自有数据快速完成性能评估。
1. 拿到ADC采样数据,第一件事不是画波形,而是算这五个动态指标
在ADC芯片评估或数据采集链路调试里,光看输出波形是判断不出性能好坏的。真正决定一颗ADC能用在多少位精度场景里的,是SNR、SNDR、THD、SFDR、ENOB这五个动态指标。它们没法从示波器上直接读出来,标准做法是对采样序列做FFT,再从频谱里把信号功率、谐波功率、噪声功率拆开算。用MATLAB算这套指标已经是很常见的做法,但很多工程师第一次写出来的程序跟手册上的典型值差好几个dB,问题往往不是采集本身,而是FFT分析里的细节没做对。这篇文章从五个指标的定义讲到可以直接运行的MATLAB程序,再列出我做过多次ADC评估后整理的几个必踩的坑,适合手头已经有采样数据、正准备评估性能的硬件和嵌入式工程师。
2. SNR、SNDR、THD、SFDR、ENOB:五个指标的口径与换算
2.1 SNR与SNDR:做一个频谱上的减法,就明白两个数的差别
SNR(信噪比)的定义是信号功率与噪声功率之比。这里的噪声有讲究——它指频谱中除了直流分量、信号本身、谐波失真之外所有残余分量的总功率,包括量化噪声、热噪声、参考电压抖动等。计算公式是:
SNR = 10·log10(Ps / Pn)
其中Ps是信号功率,Pn是剔除谐波后的噪声功率。注意这个口径里没有谐波——也就是说,ADC的非线性失真再严重,SNR这个数字也不会因此变差,这是很多人容易误解的地方。
SNDR(信噪失真比)就不同了,它把谐波失真功率Pd也算进分母:
SNDR = 10·log10(Ps / (Pn + Pd))
所以SNDR一定小于等于SNR。两个指标之间的差值,直接反映了谐波失真在整体性能里的占比。比如某ADC标称SNR=60dB、SNDR=58dB,差的这2dB说明谐波失真功率大约是噪声功率的一半多一点,这时你去优化谐波会明显提升系统可用精度;如果SNR和SNDR只差0.3dB,说明谐波非常小,得去查信号源和采样时钟的干净程度。
理想N位ADC的理论SNR上限是6.02N + 1.76dB,这是满量程正弦输入、只考虑量化噪声时的经典结论。实际测出来的SNR如果接近这个值,说明量化噪声已经占主导,其他噪声源抑制得不错;如果差得多,就要查时钟抖动、电源噪声这些因素。频谱上观察这两个指标的办法很简单:在幅频曲线上把信号峰圈出来,把谐波峰圈出来,剩下的全部看成噪声,然后按上面的公式组合。
2.2 THD与SFDR:一个看总失真,一个看最坏毛刺
THD(总谐波失真)衡量的是所有谐波分量的总功率与信号功率之比。工程上通常只看前5到9次谐波,因为高次谐波幅度衰减快、频段超出关注范围。公式:
THD = 10·log10(Pd / Ps)(dB)
工程中THD是负值,比如-50dB;数据手册有时写成百分比,换算关系是THD% = sqrt(10^(THD_dB/10)) × 100%。一个-50dB的THD对应约0.316%。
SFDR(无杂散动态范围)则是信号功率与整个频谱里最大杂散分量功率之比:
SFDR = 10·log10(Ps / Pspur)
这个最大杂散通常就是3次或5次谐波,但也可能是非谐波的毛刺,比如时钟泄漏、电源相关的spur。SFDR很有实战意义——在频谱感知、窄带通信这类应用里,一个大spur落进目标频段会直接堵死接收机,所以SFDR往往比SNR更牵动系统级选型。THD和SFDR的视角差异在于:THD关注所有谐波的总能量,SFDR关注单个最坏分量。一颗ADC可能THD很漂亮,但某个非谐波spur特别突出,这时候SFDR就会成为瓶颈。
2.3 ENOB与理想位数的关系:看似简单的公式其实只说了一半
ENOB(有效位数)是最直观的指标,它回答一个问题:这颗ADC在实际信号频率下,等效于多少位的理想ADC。换算公式:
ENOB = (SNDR - 1.76) / 6.02
公式来源就是理想N位ADC的SNR公式反推。例如SNDR=59.3dB时,ENOB = (59.3 - 1.76) / 6.02 ≈ 9.56bit。一颗标称16位的ADC在高频输入下ENOB掉到12bit是常见的事,这不代表芯片坏,而是动态性能受采样时钟抖动、内部非线性等因素限制。
这里有一个常见误用:有人拿SNR去算ENOB,得出的位数会比SNDR算出来高,因为SNR不含谐波失真。严格评估系统能吃多少位精度应该用SNDR。还有一个容易被忽略的点:公式里的1.76dB只对满量程正弦输入成立,如果输入信号幅度不满量程,ENOB会比真实值低,因为它把信号功率变小也归因到了"噪声变大"上。下面这张表把这五个指标的口径和公式汇总,后面程序里逐个实现的就是这些量。
| 指标 | 分子 | 分母 | 含义 |
|---|---|---|---|
| SNR | 信号功率 | 噪声功率(剔除直流和谐波) | 纯噪声下的信号清晰度 |
| SNDR | 信号功率 | 噪声功率 + 谐波功率 | 含失真的综合信噪比 |
| THD | 谐波总功率 | 信号功率 | 非线性失真总量 |
| SFDR | 信号功率 | 最大杂散分量功率 | 频谱里最大毛刺的威胁程度 |
| ENOB | - | (SNDR - 1.76) / 6.02 | 等效有效位数 |
3. FFT之前的三件小事:相干采样、窗函数和去直流
3.1 相干采样:让信号能量老实待在一个bin里的前提
相干采样指的是采样频率fs与输入信号频率fin满足关系:
fin / fs = M / N
其中M是整数(信号周期数),N是FFT点数。当这个条件满足时,N点FFT后信号能量恰好落在第M+1个bin处,不会向相邻bin泄漏。如果不满足,信号能量会扩散到多个bin,出现频谱泄漏,信号峰变矮、旁边出现"裙边",直接把SNR算小。严重时还会把邻近的谐波峰淹没,导致识别错误。
工程上怎么判断是否相干?看FFT频谱里信号峰两侧的泄漏程度:如果旁瓣几乎看不见,说明接近相干;如果信号峰旁边毛刺较多、呈裙状,就要警觉。实际测量中,要做到精确相干,信号源频率精度必须足够。例如fs=10MHz、N=8192,要满足相干条件,fin应该是10MHz × M / 8192。M选一个接近需求的整数,比如选M=997,fin=1.2167MHz。现代信号源一般能设到Hz级精度,问题不大;但如果采样时钟本身有漂移,就得靠窗函数兜底。
选择M还有一个原则:M太小,信号bin和谐波bin靠得太近,容易互相干扰;M太大,谐波次数高时bin接近Nyquist频率,可能混叠。一般让fin落在fs的1/8到1/4之间比较稳妥,比如fin = fs/8附近,谐波到8次都还在带宽内。
3.2 窗函数:非相干时靠它兜底,但代价也要心里有数
如果做不到相干采样,就得加窗。常用窗函数的选择直接影响计算结果:
- Hanning窗:主瓣宽度约4个bin(-3dB宽度),旁瓣衰减约-31dB,适合大多数场景
- Blackman窗:主瓣更宽约6个bin,旁瓣衰减约-58dB,适合需要压低远旁瓣的场景
- 矩形窗(不加窗):主瓣最窄2个bin,但旁瓣只有-13dB,只适合相干采样
加窗后信号能量依然会散开,所以取信号功率时不能只取峰值那一个bin,而要在信号峰周围取±binDelta个bin的功率累加。binDelta要和窗函数匹配:Hanning取±3,Blackman取±5。取多了会把噪声算进去,取少了又会漏掉信号能量,SNR都会失真。
加窗还会降低信号幅度,计算功率前需要按窗的相干增益做校正。相干增益等于窗序列的平均值,对Hanning窗约等于0.5,Blackman窗约等于0.42。在程序里用sum(w) / N直接算最稳妥,比查表更准。另外要记住,加窗只是减少泄漏,不是消除。如果频率偏差大到跨好几个bin,加窗也救不回来,所以源头还是要尽量做相干采样。
3.3 去直流与数据长度:预处理里最容易被忽略的两步
去直流是最容易被忽略的一步。ADC输出带直流偏置时,直流分量在FFT后出现在0频附近,它的泄漏会污染低频区域,还会让总功率计算出现偏差。程序里用x = x - mean(x)做一次就行,代价很小,但能避免很多后续问题。
记录长度N的选择有三个原则:第一,尽量是2的幂,FFT最省时间,也便于和信号源频率匹配取整;第二,N要覆盖足够多的信号周期,至少几十个周期,否则谐波之间的频域间隔太近、分不开;第三,N要和采集时设置的相干采样点数一致,不要硬取一个不相干的值。
多次采集取平均的做法在SNR测试里也很常见。FFT本身是单次快照,噪声功率估计的方差较大,对同一输入信号重复采集多次、对功率谱取平均,可以压低方差。平均次数可以从4次到64次,看测试时间预算。注意,取平均应该在功率谱域进行,不要对时域波形直接平均——那会把噪声也平均掉,得到的SNR虚高。
4. MATLAB计算程序:从频谱到五个指标的完整实现
4.1 可直接复制的最小实现
下面这个函数是我常用的结构,输入一条ADC采样序列、采样率、信号频率,输出五个指标。默认取前5次谐波,使用Hanning窗做泄漏抑制。
function metrics = calc_adc_metrics(x, fs, fin, nHarmonics) % 计算ADC动态指标 SNR/SNDR/THD/SFDR/ENOB % 输入: % x ADC采样序列,长度建议为2的幂 % fs 采样率,单位 Hz % fin 输入信号频率,单位 Hz(用于初步定位信号bin) % nHarmonics 参与THD计算的谐波次数,默认5 % 输出: % metrics 结构体,包含SNR/SNDR/THD/SFDR/ENOB及中间量 if nargin < 4 nHarmonics = 5; end N = length(x); x = x(:); % 统一为列向量 % ---- 1. 去直流 ---- x = x - mean(x); % ---- 2. 加窗 + 幅度校正 ---- w = hanning(N, 'periodic'); coherent_gain = sum(w) / N; % Hanning窗相干增益约0.5 xw = x .* w ./ coherent_gain; % 恢复加窗造成的幅度衰减 % ---- 3. 单边功率谱 ---- X = fft(xw, N); X = X(1:floor(N/2)+1); % 取单边谱 P = abs(X).^2 / N^2; % 归一化功率 P(2:end-1) = 2 * P(2:end-1); % 单边谱补偿因子,DC和Nyquist除外 P([1 end]) = 0; % 丢弃DC与Nyquist,避免干扰 % ---- 4. 信号bin定位 ---- signal_bin_est = round(fin / fs * N) + 1; search_lo = max(2, signal_bin_est - 100); search_hi = min(length(P), signal_bin_est + 100); [~, peak_idx] = max(P(search_lo:search_hi)); signal_bin = search_lo + peak_idx - 1; bin_win = 3; % Hanning窗取±3个bin sig_bins = max(1, signal_bin-bin_win):min(length(P), signal_bin+bin_win); signal_power = sum(P(sig_bins)); % ---- 5. 谐波功率 ---- harmonic_power = 0; harmonic_bins = zeros(nHarmonics, 1); for h = 2:(nHarmonics+1) h_bin = h * signal_bin; % 第h次谐波的理论bin位置 if h_bin > length(P) break; end h_bins = max(1, h_bin-bin_win):min(length(P), h_bin+bin_win); harmonic_power = harmonic_power + sum(P(h_bins)); harmonic_bins(h-1) = h_bin; end % ---- 6. SFDR:找信号之外的最大谱峰 ---- P_spur = P; P_spur(sig_bins) = 0; % 排除信号区域 [sfdr_power, sfdr_bin] = max(P_spur); % ---- 7. 噪声功率 = 总功率 - 信号功率 - 谐波功率 ---- total_power = sum(P); noise_power = total_power - signal_power - harmonic_power; if noise_power <= 0 error('噪声功率非正,请检查数据或增大FFT点数'); end % ---- 8. 计算并输出 ---- SNR = 10 * log10(signal_power / noise_power); SNDR = 10 * log10(signal_power / (noise_power + harmonic_power)); THD = 10 * log10(harmonic_power / signal_power); SFDR = 10 * log10(signal_power / sfdr_power); ENOB = (SNDR - 1.76) / 6.02; metrics = struct('SNR', SNR, 'SNDR', SNDR, ... 'THD', THD, 'SFDR', SFDR, 'ENOB', ENOB, ... 'signal_bin', signal_bin, 'sfdr_bin', sfdr_bin, ... 'harmonic_bins', harmonic_bins, ... 'signal_power', signal_power, ... 'noise_power', noise_power, ... 'harmonic_power', harmonic_power); end代码里有几个地方值得展开说明。去直流用的是x = x - mean(x),这对绝大多数ADC采集数据都够用了;如果数据里有过大的直流漂移,建议分段去直流或用高通滤波,但不要在FFT计算里引入额外滤波器,以免改变噪声谱形状。
加窗与幅度校正这步,coherent_gain = sum(w) / N对Hanning窗会得到约0.5,xw = x .* w ./ coherent_gain把加窗后衰减的幅度补回来。这样做的含义是让信号幅度和原始对齐,后续功率谱里信号和谐波、噪声的比值关系不会因为窗函数失真——当然,如果嫌麻烦不改这个值,五个指标的比值结果其实也不变,因为信号和谐波噪声都被同比例缩放。但保留校正在调试时更直观,信号的FFT峰值会跟时域幅度对得上。
功率谱计算用了abs(X).^2 / N^2再加单边谱补偿因子。这是严格按照Parseval定理做的归一化,sum(P)等于去直流后时域信号的平均功率。很多老代码用abs(X).^2 / N然后比较相对值,在相干采样且不关心绝对功率时结果一样,但一旦加窗或频谱泄漏,相对关系会被破坏,所以我建议直接用归一化写法。
信号bin定位这里,我用round(fin/fs*N)+1做理论位置预估,然后在±100个bin范围内搜索局部峰值。之所以不全谱找最大峰,是因为如果频谱里有个spur比信号还高,全谱搜索会找错位置。±100个bin的余度对大多数信号源频率偏差都够用了,如果你确定fin很准,可以缩到±10个bin。
SFDR部分特意没有排除谐波区域,因为SFDR定义里的杂散分量本来就包含谐波。如果你在做测试时更关心非谐波spur,可以把谐波附近的bin也置零再求最大值,不过那就不叫标准SFDR了,得在报告里注明算法差异。
4.2 谐波个数与bin范围:两个最影响结果的参数
nHarmonics的默认值取5,这是很多数据手册的惯例。但在实际测试时,我建议先让程序输出谐波列表(metrics.harmonic_bins),再用findpeaks看看超出噪底20dB以上的谐波到第几次。如果7次谐波还有明显峰,就把nHarmonics改成7或9。谐波数取少了,THD绝对值会偏小(失真没算全),取多了则会把噪底的随机尖峰当成谐波,THD偏大。两者都可能差1dB以上。
bin_win参数对应窗函数主瓣宽度。Hanning窗取3,Blackman窗取5。取太小信号功率算漏,SNR偏低;取太大噪声功率混进来,SNR照样偏低。判断bin_win是否合适有个土办法:把signal_bin附近的P值画出来看,如果信号两侧3个bin外还有明显高于噪底的分量,就该加大bin_win;如果3个bin内已经落到噪底,那取3就够了。
4.3 用理想信号验证算法:没有这一步,后面全是玄学
验证方法是生成一个已知SNR和THD的测试信号,喂给函数,看输出跟设定值是否一致。下面这段脚本可以作为每次改完代码后的自测:
% 生成理想测试信号,验证calc_adc_metrics fs = 10e6; N = 8192; M = 997; % 信号周期数,保证相干采样 fin = M * fs / N; t = (0:N-1)' / fs; A = 1.0; % 信号幅度,满量程 % 目标SNR = 60 dB,添加白噪声 SNR_target = 60; P_signal = A^2 / 2; P_noise = P_signal / 10^(SNR_target/10); noise = sqrt(P_noise) * randn(N, 1); % 加入2次和3次谐波,幅度分别为信号的1%和0.5% x = A*sin(2*pi*fin*t + 0.3) + ... 0.01*A*sin(2*pi*2*fin*t) + ... 0.005*A*sin(2*pi*3*fin*t); x = x + noise; % 调用被测函数 metrics = calc_adc_metrics(x, fs, fin, 5); fprintf('SNR = %.2f dB\n', metrics.SNR); fprintf('SNDR = %.2f dB\n', metrics.SNDR); fprintf('THD = %.2f dB\n', metrics.THD); fprintf('SFDR = %.2f dB\n', metrics.SFDR); fprintf('ENOB = %.2f bit\n', metrics.ENOB);这个验证脚本的逻辑是:相干采样保证信号和2次、3次谐波都严格落在整数bin上,然后检查程序输出的数值。理论上THD应该是10·log10((0.01^2+0.005^2)) = 10·log10(0.000125) ≈ -39.0dB,SNR应该在60dB附近。如果程序输出明显偏离这个值,优先检查信号bin定位和谐波bin计算逻辑。我习惯把这段脚本留在项目里作为回归测试,每次改动后先跑它,再处理实测数据。
5. 避坑指南:实测ADC动态指标时的五个典型翻车现场
5.1 信号源失真:测出来的SNR其实是信号源的
现象:用普通信号发生器做输入,SNR和SNDR怎么测都比手册典型值低10dB甚至更多,而且压低信号幅度也不改善。
原因:信号发生器内部DDS或PLL产生的正弦波本身有0.1%左右的谐波失真。这些谐波被ADC如实采进去,程序把它们当成ADC的失真来算。实际被评估的是信号源,不是ADC。
解决:在信号源输出端加一个低通滤波器,把谐波压到噪底以下;或者用无源晶体振荡器搭建纯正弦信号源。另外可以在测试前先用频谱仪观察信号源输出,确认它的谐波至少比待测ADC的预计谐波低20dB。
5.2 频谱泄漏:相干采样没做好,SNDR直接掉3dB
现象:程序输出的SNR反复横跳,有时候同一组数据跑两遍结果都不同——当然,如果用的是随机噪声信号,重复跑应该一样的。更典型的是:设定fin和采样率明明是整数关系,但频谱里信号峰旁边还是有一圈泄漏,SNDR比理论预期低3dB左右。
原因:信号源频率输出有偏差,或采样时钟本身不稳,实际fin和理论值差了零点几个ppm。相干条件被破坏,信号能量扩散到几十个bin,而程序只取了主瓣内±3个bin,漏掉了一部分信号功率。
解决:先用窗函数兜底,把bin_win调到±5甚至±7,看SNDR是否回升;再回头校准信号源频率。如果加窗后泄漏仍然明显,说明信号源和采样时钟之间的同步问题不是窗能解决的,得用PLL锁定两者频率。
5.3 谐波截断:THD在5次和9次之间差出1dB
现象:nHarmonics从5改成9,THD的绝对值从-42dB变到-43dB,看似只差1dB,但系统规格可能刚好卡在-42.5dB的边界上。
原因:高次谐波没有完全衰减。在很多高速ADC里,5次之后谐波依然清晰可见,截断到5次会漏掉一部分失真能量。
解决:写一个简单的循环,把1到15次谐波的功率全部算出来画成柱状图,人工确认谐波衰减到噪底的位置,再把nHarmonics设为那个位置。不要凭默认值直接出报告。
5.4 FFT点数不足:信号和谐波挤在同一个bin里
现象:程序报错说噪声功率非正,或者THD特别大,一看频谱发现信号bin和2次谐波bin挨得太近。
原因:fin相对fs偏低,M较小,导致信号bin附近几个bin内混进了2次谐波。比如N=2048、M=100,信号在bin101,2次谐波在bin202,本身不重叠;但如果N=256、M=100,信号在bin101,2次谐波在202,距离还可以,而4次谐波在404已经超出Nyquist。问题往往出在信号本身占的bin数太少,加窗后主瓣变宽,覆盖了谐波的位置。
解决:增大N,或者把fin/fs的比例降低,让信号和谐波在频域上分得开。一般建议信号bin至少在100以上,这样前5次谐波能到500,FFT点数至少1024。对于需要精细评估的场景,N取8192或16384更稳。
5.5 SFDR的杂散定位:最大谱峰和随机噪声尖峰要分清
现象:SFDR计算结果在多次测试中抖动很大,有时一次-80dB,下一次-65dB,数据手册上的典型值却稳定在-75dB。
原因:FFT是单次快照,噪底本身是随机起伏的。如果真正的spur只比噪底高几个dB,某次测量中噪声的随机尖峰可能反超spur,程序就会选错"最大杂散"。
解决:一是多次采集求平均功率谱再算SFDR,随机噪声尖峰被平掉,确定性spur会浮出来;二是把sfdr_bin打印出来,人工确认它是不是在整数倍信号频率附近——确定性spur通常有明确来源(时钟、电源),位置固定。如果sfdr_bin每次测试都在不同位置跳,几乎可以断定是噪底尖峰被误判了。还有一种做法是只看确定性分量,即把低于噪底+10dB的谱线全部清零后再找最大值,这能避免大部分误判,但报告里要写明这个阈值。
6. 进阶:批量测试与自动校准,把程序用成测试工具链
把单点计算扩成批量测试脚本并不复杂,核心是管理好每组数据的元数据。我一般会把采集参数(fs、fin、N、M、目标频率)、温箱温度、供电电压都存在一个MAT文件里,然后循环调用calc_adc_metrics:
% 批量测试脚本:遍历文件夹内所有采集数据 files = dir('adc_test_*.mat'); results = table(); for i = 1:length(files) load(files(i).name, 'x', 'fs', 'fin'); m = calc_adc_metrics(x, fs, fin, 7); results = [results; table({files(i).name}, m.SNR, m.SNDR, ... m.THD, m.SFDR, m.ENOB, ... 'VariableNames', {'file','SNR','SNDR','THD','SFDR','ENOB'})]; end writetable(results, 'adc_report.csv');这里有个细节:批量测试时nHarmonics不要再用默认5,先跑一份数据把谐波图打出来,确定一个对所有电压和温度条件都合理的通用值。有些ADC在低温或低压下谐波分布会变化,最好在批量脚本开头加一个探查模式,自动检测谐波直到噪底。
每次实测之前,先跑一遍4.3节的理想信号校准,这件事我建议养成习惯。校准通过不代表实测数据一定对,但校准失败的很大概率是算法本身被改坏了,此时去分析实测数据纯属浪费几个小时。我自己吃过一次亏——某次从代码库拉了一个"优化过"的版本,没有重跑校准,直接拿去测试某高速采集板,结果SNDR比之前低了5dB,查了一天最后发现是bin_win被改成了1。从那以后,校准脚本就成了所有改动合并前的必经关卡。希望这套程序和这些思路能帮到你,在ADC评估这条路上少走几步弯路。
本文还有配套的精品资源,点击获取