简介:面向信号处理、ADC/数模混合测试与电子工程学习者的MATLAB参数评估脚本,围绕SNR、SNDR、THD、ENOB与SFDR五项指标展开,可用于读取采样数据、进行FFT频谱分析并计算信噪比、谐波失真、有效位数与无杂散动态范围,适合课程实验、系统性能验证和结果复核。压缩包仅4KB,共2个文件,包含1个m脚本与1个txt数据文件,脚本体量轻便,便于直接放入MATLAB路径测试,文本文件可作为示例采样输入。已有4558人学习下载,说明其在相关指标计算场景中具有一定参考价值。读者可据此理解从时域数据到频域指标的完整计算流程,并借助脚本核对各参数定义、频谱峰值提取与功率比计算思路,减少自行搭建分析程序时的重复劳动。
1. 从一段 8192 点码流开始:五个动态指标在一份 MATLAB 程序里怎么落地
一段 8192 点的 ADC 输出码流摆在面前,很多人的第一反应是 fft 一画、频谱一瞅,凭肉眼估个 SNR 就交差。真到要出测试报告,这套目测法立刻露馅:同一段数据两个人能差出 6 dB,ENOB 更是各说各话。这套 SNR / SNDR / THD / ENOB / SFDR 的 MATLAB 计算程序,干的就是把这件事从「看图说话」变成「公式可追溯」——去直流、加窗、归一化、归并基波与谐波 bin、扣掉 ENBW 偏差,最后吐出五个能直接写进表格的 dB 值。它适合做数据转换器评估的工程师、做 ADC 行为级仿真的同学,以及要给流片结果出动态性能报表的人。下面按「定义怎么对 → 代码怎么写 → 参数怎么定 → 哪里会翻车」的顺序走一遍,改几行就能跑自己的数据。
2. 指标定义与频谱前处理:为什么相干采样比窗函数更值得先做对
2.1 五个指标的定义式与相互换算
先把符号统一。设基波功率为 Ps,除基波外的噪声功率为 Pn(不含谐波),谐波总功率为 Pd,带内最大杂散功率为 Pspur。五个指标全部由这四个量派生,没有第五个独立变量。
| 指标 | 定义式 | 单位 | 关键约定 |
|---|---|---|---|
| SNR | 10·log10(Ps / Pn) | dB | 分母不含谐波 |
| SNDR / SINAD | 10·log10(Ps / (Pn + Pd)) | dB | 分母含谐波,也叫 SINAD |
| THD | 10·log10(Pd / Ps) | dB | 结果是负值,报告里常写成 -THD |
| ENOB | (SNDR − 1.76) / 6.02 | bit | 必须用 SNDR,不能用 SNR |
| SFDR | 10·log10(Ps / Pspur) | dBc | 分母是带内最大单根杂散 |
四个量之间有一条硬约束:SNDR 一定不高于 SNR,且满足
1/10^(SNDR/10) = 1/10^(SNR/10) + 1/10^(THD/10)这条式子是后面所有排查的地基。任何一次计算,只要把 SNR 和 THD 代进去反算出的 SNDR 与实际输出的 SNDR 对不上,说明功率归并那一步就有 bin 被重复计入或者漏计了,不用再往下看。
理想量化器的理论值是另一把尺子。满量程正弦输入下,N 位理想 ADC 的 SNR = 6.02N + 1.76 dB;输入幅度回退到满量程的 A 倍(A<1)时,再减 20·log10(A)。这条式子决定了你测出来的数「该是多少」,也就决定了程序有没有写错。
2.2 相干采样、频谱泄漏与频率字 J
FFT 只能分辨 N 个离散频点,频率分辨率 Δf = fs/N。如果输入频率 fin 恰好落在某个 bin 上,即 fin = J·fs/N 且 J 为整数,能量就集中在这一根谱线里,用矩形窗即可,这叫相干采样。反之,fin 落在两根谱线之间,能量会向两侧扩散,形成频谱泄漏,旁瓣能抬到 -13 dBc 量级,这时候你根本分不清哪根是谐波、哪根是泄漏的裙边。
只要求 J 是整数还不够。若 J 与 N 有公因子,采样点在相位上会周期性重复,等效于只采了 N/gcd(J,N) 个不同的相位点,频谱上会出现成对的镜像谱线,看起来像杂散。所以相干采样的完整条件是:N 取 2 的幂,J 取奇数且与 N 互质。取 J 为质数是最省事的做法,质数与 2^M 自动互质。
现实里信号源和采样时钟往往不同源,很难严格相干。这时有两条路:一是加窗(Hann、Blackman-Harris),把泄漏压下去,代价是主瓣变宽、噪声被抬高等效 1.5 到 2 倍;二是做频率估计后重采样。工程上九成场景走第一条路,但必须记住:加窗之后基波能量分布在多根 bin 上,只取峰值那一根会系统性低估信号功率。
提示:判断数据是否相干,最直接的办法是把峰值 bin 号打印出来,和设定的 J 比对。偏差 0 到 1 个 bin 说明相干;偏差好几根 bin,先别怀疑程序,去查信号源和时钟是不是共参考。
2.3 上手第一步:先验证采样是不是相干的
拿到数据别急着算什么 SNR,先花十秒钟确认频谱形态。
% check_coherent.m —— 判断一段采集数据是否满足相干采样 code = load('adc_capture.txt'); % 每行一个码值 x = double(code(:)) - mean(code); % 去直流,均值比首点稳 N = numel(x); Jexp = 787; % 设计时设定的频率字 w = hann(N, 'periodic'); % 周期型 Hann,适合 FFT X = fft(x .* w); P = 20*log10(abs(X(1:N/2+1)) + eps); % dB 幅度谱 [~, i0] = max(P(2:end)); % 跳过直流 bin 找峰 k0 = i0 + 1; fprintf('峰值 bin = %d,理论 J = %d,偏差 = %d bin\n', k0, Jexp, k0 - Jexp); fprintf('实测 fin = %.6f MHz,理论 = %.6f MHz\n', ... k0/N*1e-6*100e6, Jexp/N*100e6);逻辑上这段代码只做三件事:去均值消掉直流、加周期 Hann 压低泄漏、在排除直流 bin 之后找最大值位置。参数上,hann(N,'periodic')的周期型比对称型更适合 FFT,因为它保证 w(1) 与 w(N) 首尾相接,不会在边界引入额外跳变;找峰时从索引 2 开始,是为了避开 ADC 失调造成的直流大峰,那根谱线幅度往往比基波还高,不排除必然找错。
偏差落在 0 到 1 个 bin 内,说明数据可用,接着往下算。偏差是三根以上 bin,通常有三类原因:信号发生器与采样时钟没共参考、频率字 J 在配置文件里被改过、采集时丢点导致等效采样率变了。这三种情况算出来的 SNR 都不可信,别硬着头皮往下走。
3. 计算链路拆解:从 fft 输出到五个 dB 数值的函数怎么写
3.1 函数骨架与输入输出约定
整个程序收敛成一个函数,输入是码流和几个配置项,输出是一个结构体。这样做的好处是扫频、批量跑不同频点时不用重复写代码,也方便把结果直接堆成 table。
函数签名设计成adc_metrics(code, fs, fmax, Nh, winName, L),后四个参数都给默认值,只有code必填。fs只影响频率轴的换算,不影响 dB 结果;fmax决定统计带宽,这个参数最容易被忽略,后面单独讲。
输入码型必须先统一。ADC 输出常见三种:偏移二进制、二进制补码、无符号整数。偏移二进制要减掉中点(2^(B-1)),补码要按位宽做符号扩展,无符号整数在去均值之后其实也等价处理了。这一步做错,整个频谱会多出一堆本不存在的谐波,属于典型的「程序没错、数据喂错」。
3.2 幅度归一化、单边谱与 ENBW 噪声修正
这一段是整份程序最核心、也最容易写错的地方,值得单独拆开说。
对加窗后的序列做 FFT,得到 X(k)。要把它还原成物理功率,需要除以窗函数的相干增益 S1 = Σw(n)。单边谱还要对除直流和奈奎斯特 bin 之外的所有 bin 乘 2,因为实信号的负频率半边是镜像。
归一化之后的第 k 根谱线功率为
P(k) = |X(k)|^2 / S1^2 , 1 < k < N/2 时再乘 2对一根幅度为 A 的正弦,这样算出来 P = A²/2,正好是它的平均功率,信号功率这一路就对了。
噪声这一路不能照搬。窗函数把噪声能量在频域上抹开,等效带宽被展宽了 ENBW 倍:
ENBW = N · Σw²(n) / (Σw(n))² 单位是 bin矩形窗 ENBW = 1,Hann 窗 ENBW = 1.5,Blackman-Harris 窗 ENBW ≈ 2。所以噪声功率求和之后必须除以 ENBW 才能还原。这一步漏掉,Hann 窗下 SNR 会凭空好 10·log10(1.5) = 1.76 dB,而且这个偏差很隐蔽——结果看起来依然「合理」,只是系统性偏乐观。这是我在对比两家实验室数据时踩过的坑,两边差 1.8 dB,查了两天才定位到窗函数修正。
3.3 基波、谐波与杂散的 bin 归并
有了归一化功率谱,接下来是「哪些 bin 算信号、哪些算谐波、哪些算噪声」的划分。逻辑是建三张掩码。
基波掩码以峰值 bin k0 为中心,左右各扩 L 根,L 是主瓣半宽,矩形窗取 0,Hann 取 2,Blackman-Harris 取 4。谐波掩码对 h = 2 到 Nh,计算 kh = mod(h·k0, N),再折叠到 0 到 N/2 区间:kh > N/2 时取 N - kh。折叠这一步不能省,因为高次谐波超过奈奎斯特频率后会折回带内,它依然是真实的失真能量,必须计入 THD。
折叠会带来一个新问题:两根不同次谐波可能折到同一根 bin 上,或者某次谐波恰好折回基波附近。前者重复计数会让 THD 虚高,后者会把基波能量算进谐波里。处理办法是维护一个已占用 bin 的集合,每次扩掩码前先检查交集,有冲突就跳过。
噪声就是「带内、且不在直流掩码、基波掩码、谐波掩码里」的所有 bin 之和,再除以 ENBW。杂散则在「带内、排除直流与基波」的 bin 里取最大值。SFDR 按标准定义是把谐波也算作杂散的,但很多数据手册会额外给一个「不含谐波的最大杂散」,所以函数里两个都输出,用的时候按报告口径选。
3.4 用一段合成码流跑通全流程
把上面的逻辑写成一个完整函数。
function m = adc_metrics(code, fs, fmax, Nh, winName, L) %ADC_METRICS 计算 ADC 的 SNR / SNDR / THD / ENOB / SFDR % code : N×1 输出码,double 或整型均可 % fs : 采样率 Hz,缺省 1 % fmax : 统计带宽上界 Hz,缺省 fs/2 % Nh : 计入的谐波个数(不含基波),缺省 5 % winName : 'rect' | 'hann' | 'blackmanharris',缺省 'hann' % L : 主瓣半宽(bin),缺省按窗型自动取 code = double(code(:)); N = numel(code); if nargin < 2 || isempty(fs), fs = 1; end if nargin < 3 || isempty(fmax), fmax = fs/2; end if nargin < 4 || isempty(Nh), Nh = 5; end if nargin < 5 || isempty(winName), winName = 'hann'; end switch lower(winName) case 'rect', w = ones(N,1); Ldef = 0; case 'hann', w = hann(N,'periodic'); Ldef = 2; case 'blackmanharris', w = blackmanharris(N,'periodic'); Ldef = 4; otherwise, error('未知窗型: %s', winName); end if nargin < 6 || isempty(L), L = Ldef; end S1 = sum(w); S2 = sum(w.^2); ENBW = N * S2 / S1^2; % 等效噪声带宽,单位 bin x = code - mean(code); % 去直流 X = fft(x .* w); P = abs(X(1:N/2+1)).^2 / S1^2; P(2:end-1) = 2 * P(2:end-1); % 单边谱,直流与奈奎斯特 bin 不乘 2 kmax = max(3, min(N/2+1, floor(fmax/fs*N) + 1)); band = false(1, N/2+1); band(1:kmax) = true; [~, i0] = max(P(2:kmax)); % 跳过直流找基波 k0 = i0 + 1; idx_dc = 1 : min(1+L, kmax); idx_sig = max(1,k0-L) : min(kmax, k0+L); mask_dc = false(1,N/2+1); mask_dc(idx_dc) = true; mask_sig = false(1,N/2+1); mask_sig(idx_sig) = true; mask_h = false(1,N/2+1); Psig = sum(P(idx_sig)); Pdist = 0; for h = 2:Nh kh = mod(h*k0, N); kh = min(kh, N - kh); % 折叠到 0..N/2 if kh <= 1+L, continue; end % 折进直流区,无法分辨 if abs(kh - k0) <= L, continue; end % 折回基波,跳过 if kh > kmax, continue; end % 落在统计带宽外 idx = max(1,kh-L) : min(kmax, kh+L); mask_h(idx) = true; Pdist = Pdist + sum(P(idx)); end Pnoise = sum(P(band & ~(mask_dc | mask_sig | mask_h))) / ENBW; Ptmp = P; Ptmp(~band) = 0; Ptmp(mask_dc | mask_sig) = 0; m.SFDR = 10*log10(Psig / max(Ptmp)); % 含谐波,标准 SFDR Ptmp2 = Ptmp; Ptmp2(mask_h) = 0; m.SFDR_nh = 10*log10(Psig / max(Ptmp2)); % 不含谐波的最大杂散 m.SNR = 10*log10(Psig / Pnoise); m.SNDR = 10*log10(Psig / (Pnoise + Pdist)); m.THD = 10*log10(Pdist / Psig); m.ENOB = (m.SNDR - 1.76) / 6.02; m.fin = k0 / N * fs; m.k0 = k0; m.ENBW = ENBW; m.Psig = Psig; m.Pnoise = Pnoise; m.Pdist = Pdist; end逻辑上分五步走:窗与增益预计算、功率谱归一化、掩码划分、功率归并、指标换算。每一步的中间量都塞进结构体输出,是为了后面排查时能直接看Psig和Pnoise的比值,而不用重新跑一遍 FFT。
参数上有三处需要按数据改:winName在相干采样下改成'rect'并配L = 0,结果最干净;fmax在带通采样或过采样场景下不要给fs/2,否则带外噪声会全部算进来,SNR 无谓地变差;Nh一般取 5,ENOB 超过 14 bit 时取 7 到 9,因为高次谐波在那时已经浮出噪底。
跑一遍合成数据验算:
% demo_adc_metrics.m —— 12 bit 合成码流验证 clear; clc; rng(7); fs = 100e6; N = 2^13; J = 787; B = 12; assert(gcd(J,N) == 1, 'J 与 N 不互质,不是相干采样'); fin = J/N*fs; % 约 9.607 MHz t = (0:N-1)'/fs; A = 0.999; % 贴近满量程 x = A*sin(2*pi*fin*t) + 2e-5*randn(N,1); LSB = 1/2^(B-1); code = round(x / LSB); % 有符号码,约 ±2046 m = adc_metrics(code, fs, fs/2, 5, 'hann', 2); fprintf('SNR=%.2f SNDR=%.2f THD=%.2f ENOB=%.2f SFDR=%.2f\n', ... m.SNR, m.SNDR, m.THD, m.ENOB, m.SFDR);逻辑上先按相干条件构造正弦,加一点点白噪模拟前端,再量化成 12 bit 码。参数上A = 0.999是为了留一点满量程余量又不太影响理论值,2e-5的噪声远小于量化噪声(LSB/√12 ≈ 1.4e-4),对结果的贡献不到 0.01 dB。
跑出来 SNR 应该在 73.9 到 74.0 之间,ENOB 接近 12.0。如果差得远,九成是归一化或者 ENBW 那一步写错了,回去对一遍S1、S2的定义。
4. 参数怎么定:N、J、fmax、谐波数这四组值的取值边界
4.1 采样点数 N 与频率字 J 的搭配
N 决定频率分辨率和单 bin 噪底。噪声在频域上被摊到 N/2 根 bin 里,单 bin 噪底相对基波大约是
噪底(dBc) ≈ -(SNR + 10·log10(N/2))12 bit、N = 8192 时,
本文还有配套的精品资源,点击获取