【芯片测试】:相干采样
相干采样Coherent Condition——让 FFT 频谱变干净的关键面向刚接触混合信号 / DSP 测试的工程师。读完你应该能理解 FFT 为什么会算错掌握相干采样的两个条件会用一段几十行的 Python 复现并诊断各种奇怪的频谱。文中所有图都是用 Python 现场跑出来的代码你都可以直接复制运行。0. 先建立背景什么是 DSP-based testing模拟世界里最典型的两类混合信号器件是ADC模数转换器和DAC数模转换器。在测试这类器件时激励信号通常由一台**任意波形发生器AWG**产生它内部其实就是一个 DAC把我们用数学算出来的一串数字变成模拟波形送给被测件DUT。被测信号则由一台**数字化仪 / 采样器digitizer / sampler**采集它内部是一个 ADC把模拟波形重新变回一串数字。也就是说激励是用数学造出来的测量结果也是用数学处理出来的。整套方法建立在**数字信号处理DSP**之上所以业内常把它叫做DSP-based testing。在这套方法里最常用、最强大的工具就是FFT快速傅里叶变换——它把一段时域波形变成频谱让我们一眼看出信号里有哪些频率成分、噪声有多大、有没有谐波失真。而要让 FFT 给出可信、干净的频谱有一个绕不开的前提条件叫做相干采样Coherent Condition。这篇文章就专门讲它。1. 为什么 FFT 需要相干1.1 FFT 的隐含假设信号是无限周期延拓的傅里叶变换的数学基础是假设信号无限连续、无限重复。但实际采集时我们只能截取有限长的一段这一段叫做单位测试周期Unit Test Period, UTP——也就是 FFT 实际看到的那 N 个采样点。FFT 在内部其实是这么脑补的把你给它的这一段 UTP首尾相接、无限循环地拼下去当成一个无限长的周期信号来分析。这里就藏着一个陷阱如果 UTP 里装的不是整数个信号周期那么把它首尾相接时结尾和开头对不上会出现一个接缝不连续点。FFT 看到这个突变就会认为信号里含有大量额外的频率成分——于是频谱被抹脏了。这个现象叫频谱泄漏spectral leakage。1.2 用拼接实验直观感受我们令M UTP 里包含的信号周期数。下面这段代码把一段 UTP 重复拼接 3 次对比M4整数和M4.3含分数周期importnumpyasnpimportmatplotlib.pyplotasplt N64tnp.arange(N)fig,axplt.subplots(1,2,figsize(10,3))fora,M,titlein[(ax[0],4,(a) M4 整数周期),(ax[1],4.3,(b) M4.3 含分数周期)]:onenp.sin(2*np.pi*M*t/N)# 一个 UTPthreenp.concatenate([one,one,one])# 首尾相接 3 次a.plot(three)forbin(N,2*N):a.axvline(b,colorr,ls--,lw.8)# 标出接缝a.set_title(title)plt.tight_layout();plt.show()左图M4三段拼起来平滑连续看不出接缝右图M4.3每个红色虚线处都有一个明显的台阶式跳变。FFT 正是被这种跳变误导才产生泄漏。结论要让频谱干净UTP 必须恰好装下整数个信号周期。2. 相干条件的数学表达把UTP 恰好装下整数个周期写成公式。设Fin 信号频率Fs 采样率N 采样点数一个 UTP 的长度M UTP 内的信号周期数一个 UTP 的时长是N / Fs这段时间里信号走过Fin × N / Fs个周期。要它正好等于整数M就得到相干条件FinFsMN\frac{F_{in}}{F_s} \frac{M}{N}FsFinNM等价地Fin M × (Fs / N)。这里的Fs / N是 FFT 的频率分辨率每个频率 bin 代表的频率宽度。换句话说信号频率必须正好落在某个 FFT bin 的中心上。相干条件要满足两条M 和 N 都必须是整数M 和 N 必须互质最大公约数为 1没有公因子。第 2 条为什么重要第 3 节专门讲。工程实践小贴士因为 FFT 通常要求N 2ⁿ如 512、1024、2048而 2 的幂只有 2 这个素因子所以只要把 M 取成奇数M 和 N 就自动互质了。这就是为什么在 DSP 测试里你会反复看到周期数取奇数的约定。2.1 一个真实的数值例子设采样率Fs 110 MHz采样点数N 512。频率分辨率Fs,N110e6,512print(bin 分辨率 Fs/N ,Fs/N,Hz)# 214843.75 Hz想测一个约 5 MHz 的信号。直接用5.000 MHz对应M Fin×N/Fs 5e6×512/110e6 23.27不是整数不相干。换成离它最近的相干频率取M 23则Fin 23 × 214843.75 4.94140625 MHz正好 23 个整周期落进 UTP。两者频谱对比defquantize(x,bits8):# 模拟 8-bit ADC 量化returnnp.clip(np.round((x*0.50.5)*(2**bits-1)),0,2**bits-1)defspectrum_db(code,bits8):xcode/(2**bits-1)*2-1Xnp.fft.rfft(x)/len(x)*2magnp.abs(X);mag[0]/2return20*np.log10(np.maximum(mag,1e-12))nnp.arange(N)greenquantize(np.sin(2*np.pi*23*n/N))# M23相干yellowquantize(np.sin(2*np.pi*(5e6*N/Fs)*n/N))# 5MHz不相干(M23.27)左边相干一根又细又高的基波谱线底下铺着均匀的量化噪声本底——这正是我们想要的干净频谱。右边不相干基波散开成一座宽宽的山包能量从主谱线泄漏到了周围所有 bin 里噪声本底被整体抬高。如果你拿这个频谱去算信噪比或谐波结果完全不可信。3. 为什么 M 和 N 还必须互质满足M 是整数只是第一关。即使 M 是整数如果它和 N 有公因子量化噪声的分布也会出问题。要看清这一点需要一个很巧妙的工具相位重排。3.1 相位重排phase reshuffle——把整段波形折叠成一个周期思路很简单。相干采样时第n个采样点对应的相位是2π·M·n/N。如果我们按相位大小给所有采样点重新排序就能把分散在 N 个点里、横跨 M 个周期的数据全部折叠回单独一个周期上。这相当于把所有采样点叠在一张单周期正弦模板上于是 ADC 在整条正弦曲线上的表现一览无余。排序的关键量是(M·n) mod N——它代表每个点在一个周期里的相对位置defreshuffle(code,M):Nlen(code)nnp.arange(N)ordernp.argsort((M*n)%N,kindstable)# 按相位位置排序returncode[order]这里有个关键数学事实当 M 与 N 互质时(M·n) mod N在n 0,1,…,N-1上取遍0…N-1的每一个值是一个排列所以 N 个采样点会均匀铺满整条正弦曲线而当 M 与 N 的最大公约数是g时(M·n) mod N只能取到N/g个不同的值每个值被重复g次——也就是说无论你采多少点实际只落在 N/g 个相位位置上。3.2 互质65/512vs 不互质64/512frommathimportgcd N512;nnp.arange(N)forMin(65,64):print(fM{M}: gcd(M,N){gcd(M,N)}, 不同相位点数{N//gcd(M,N)})# M65: gcd1, 不同相位点数512 ← 互质# M64: gcd64, 不同相位点数8 ← 不互质 (512/648)好的情况M6565 和 512 没有公因子上图是采集到的波形下图是相位重排后的结果512 个点均匀散布在 0~255 整条正弦轨迹上。对应的频谱也很干净坏的情况M6464 和 512 的公约数是 64不互质重排后所有点只落在8 个离散的台阶上512 / 64 8其余码值根本没被采到。这意味着量化噪声没有被均匀搅匀而是高度规律化。结果就是频谱出问题量化噪声不再均匀铺在本底上而是全部堆到了少数几根谱线上频谱严重畸变。这正是违反M、N 互质的后果。一句话记住M 是整数保证没有泄漏M 与 N 互质保证量化噪声被均匀打散。两条都满足频谱才真正干净。4. 量化噪声本底有多低——理论公式频谱里那条平平的地板叫噪声本底noise floor对理想 ADC 而言它来自量化噪声。它的理论值可以算出来Noise Floor [dB](6.02n1.76)10log10 (N2)\text{Noise Floor [dB]} (6.02n 1.76) 10\log_{10}\!\left(\frac{N}{2}\right)Noise Floor [dB](6.02n1.76)10log10(2N)n ADC 位数N 采样点数。第一项6.02n 1.76是大名鼎鼎的量化信噪比SNR每多 1 位动态范围约多 6 dB。第二项10·log₁₀(N/2)叫噪声改善因子NIF, Noise Improving Factor体现过采样的好处采的点越多噪声本底被摊得越低因为同样的总噪声功率被分摊到更多 FFT bin 里。n,N8,2048snr6.02*n1.76nif10*np.log10(N/2)print(fSNR项{snr:.2f}dB, NIF项{nif:.2f}dB, 噪声本底{snrnif:.2f}dB)# SNR项49.92 dB, NIF项30.10 dB, 噪声本底80.02 dB怎么用它在离散仿真里这是能达到的理论极限真实测量受各种噪声和杂散影响通常达不到这个水平。反过来如果你的实测动态范围比理论值还好那不是运气好——多半是测试方法里有问题或有取巧需要警惕。5. 实战五种奇怪频谱的诊断下面把方法用起来。被测件是一个理想 8-bit ADC采集 2048 点。我们先有一张正常的参考频谱一根干净基波 平坦本底诊断的核心套路只有两步看频谱形态——不同故障有不同的长相回到原始波形并做相位重排——很多时间域里看不出的缺陷重排后一眼就现形。所有问题波形都按 M81 个周期构造所以重排时统一用reshuffle(code, 81)。问题 1基波裙边散开现象基波主谱线根部散开成一圈裙边。原因这是周期数 M 不是整数的典型表现。这条波形其实是用M 81.01构造的——差那么一点点没对齐 bin就泄漏了。复现N2048;nnp.arange(N)p1quantize(0.9*np.sin(2*np.pi*81.01*n/N))# M 非整数排查方向检查Fin、Fs、N的设置确认Fin/Fs M/N严格成立、M 取了整数。问题 2出现大量谐波现象频谱里冒出一排等间距的谐波成分。直觉谐波一般意味着波形被非线性失真了。但直接看原始波形不一定看得出来。这时相位重排最有用重排成单周期后正弦的顶部和底部被削平了——这是典型的饱和 / 过载clipping。信号幅度太大超出了 ADC 满量程。复现p2quantize(np.clip(1.4*np.sin(2*np.pi*81*n/N),-1,1))# 幅度过大被削顶排查方向降低输入幅度或调整量程让信号落在 ADC 满量程之内。问题 3噪声本底整体偏高现象基波正常但噪声本底比参考高了一截。原始波形乍看完全正常。诊断做相位重排——本该严丝合缝贴在正弦模板上的点有几个明显跳到了曲线之外。说明波形里混进了个别坏点偶发的错误采样值。复现p3quantize(0.9*np.sin(2*np.pi*81*n/N)).copy()idxnp.random.choice(N,2,replaceFalse)p3[idx]np.random.randint(0,256,2)# 注入 2 个随机坏点排查方向几个随机坏点就能把本底抬起来需要进一步定位是采样、传输还是供电环节出了问题。问题 4本底形状很怪 末尾掉点现象噪声本底呈现奇怪的形状。诊断相位重排后能数出有5 个点孤零零地偏离正弦轨迹规律地排在一起。注意重排不保留时间信息它只告诉你有 5 个坏点不告诉你它们在波形的哪个位置。于是回头紧盯原始波形会发现最后 5 个采样点没采对停在了零值附近——很可能是 ADC 没收到足够的采样时钟没能采满需要的点数。复现p4quantize(0.9*np.sin(2*np.pi*81*n/N)).copy()p4[-5:]128# 最后 5 点丢失停在中点(对应信号≈0)排查方向波形末尾掉点常与时钟数量不足有关开头的异常则往往和等待时间不够或触发问题有关。两端都值得重点检查。问题 5噪声向 DC 方向翘起现象基波本身很完美但噪声本底在靠近 DC低频一侧逐渐抬高。诊断原始波形乍看正常但仔细看会发现整体随时间缓缓向上倾斜——信号里叠加了一个直流漂移DC drift / 趋势项。低频的缓慢漂移在频谱上正好表现为 DC 附近能量升高。复现driftnp.linspace(0,0.25,N)# 缓慢上升的趋势p5quantize(0.9*np.sin(2*np.pi*81*n/N)drift)可能原因输入通路是交流耦合或串了隔直电容、电路尚未稳定、需要更长建立时间也可能是器件 / 外围温度未稳或信号源本身带有1/f 噪声。找到并消除漂移源频谱才会恢复干净。6. 小结关键点内容FFT 的隐含假设UTP 被首尾相接、无限周期延拓相干条件Fin/Fs M/N条件一M、N 都是整数 → 没有频谱泄漏条件二M、N 互质 → 量化噪声被均匀打散不互质只命中N/gcd(M,N)个相位实用约定N 取 2 的幂、M 取奇数自动互质噪声本底(6.02n 1.76) 10·log₁₀(N/2)第二项体现过采样增益诊断套路先看频谱形态再回到波形 相位重排诊断速查频谱长相最可能的原因基波裙边散开M 不是整数不相干整齐的谐波饱和 / 过载等非线性失真本底整体偏高个别随机坏点本底形状怪异成串坏点如末尾掉点、时钟不足本底向 DC 翘起直流漂移 / 交流耦合 / 1/f 噪声最重要的一条经验一旦看到奇怪的频谱别急着怀疑算法先回头仔细看原始波形。把几种典型长相记在脑子里配上相位重排这把小工具绝大多数现场问题都能很快定位——这也正是在线调试的乐趣所在。附录完整可运行代码importnumpyasnpimportmatplotlib.pyplotaspltfrommathimportgcd# ---------- 基础工具 ----------defquantize(x,bits8):把 [-1,1] 的模拟信号量化成 0..2^bits-1 的 ADC 码returnnp.clip(np.round((x*0.50.5)*(2**bits-1)),0,2**bits-1)defspectrum_db(code,bits8):对 ADC 码做 FFT返回 dB 幅度谱xcode/(2**bits-1)*2-1Xnp.fft.rfft(x)/len(x)*2magnp.abs(X);mag[0]/2return20*np.log10(np.maximum(mag,1e-12))defreshuffle(code,M):相位重排按 (M*n) mod N 排序把整段波形折叠成单个周期Nlen(code);nnp.arange(N)returncode[np.argsort((M*n)%N,kindstable)]# ---------- 相干条件检查 ----------defis_coherent(M,N):returnfloat(M).is_integer()andgcd(int(M),N)1N2048;nnp.arange(N);M81print(相干?,is_coherent(M,N))# Trueprint(不同相位点数 ,N//gcd(M,N))# 2048# ---------- 噪声本底理论值 ----------defnoise_floor(bits,N):return(6.02*bits1.76)10*np.log10(N/2)print(理论噪声本底 ,round(noise_floor(8,2048),2),dB)# 80.02 dB# ---------- 一个干净的相干频谱 ----------codequantize(0.9*np.sin(2*np.pi*M*n/N))plt.plot(spectrum_db(code));plt.xlabel(FFT bin);plt.ylabel(dB)plt.title(Coherent spectrum (M81, N2048));plt.show()