做模拟IC的人,都不会对Virtuoso陌生,但很多人对它的认知停留在画原理图、跑版图和LVS。事实上,用Virtuoso里的Spectre瞬态仿真配合FFT分析来看电路的线性度,是我这几年验证放大器、ADC输入级和Buffer最常用的手段。尤其在需要给出THD、SFDR这类指标时,如果对相干采样没有概念,直接拉一段波形丢进FFT,频谱必然是糊的,算出来的THD比真实值差出几个数量级,还会被当成“仿真收敛问题”排查半天。这篇文章就完整走一遍流程:从相干条件计算、瞬态仿真参数设置、Calculator里的FFT操作,到最终THD的取值和自动化计算,把每一步背后的为什么也讲清楚。适合刚接触Virtuoso仿真、或者已经会用但总是被FFT结果困扰的工程师参考。
1. 相干采样为什么是FFT和THD分析的前提
1.1 非相干采样带来的频谱泄漏
很多人第一次用Virtuoso做FFT,习惯是:瞬态仿真跑完,把输出波形往Calculator的fft函数里一扔,选个1024或4096点,直接看频谱。结果往往很打击人——基波附近的能量散成一大片“裙边”,像是信号被调频了一样,原本应该尖锐的谱线变成了一座小山丘,小信号谐波干脆被埋在泄漏能量里。
这就是典型的频谱泄漏。
FFT的本质是把一段有限时长的信号,当成一个周期信号来做傅里叶展开。如果我们截取的这段波形里恰好包含整数个信号周期,那么基波、谐波都精确落在对应的FFT频率点上,能量集中在一根根谱线里。但一旦窗口长度不是信号周期的整数倍,哪怕差了百分之零点几,基波的能量就会泄漏到相邻的几十上百个频率bin里,产生一大片伪频谱,谐波读数自然不准,THD算出来就是个笑话。
这里还牵扯到栅栏效应。FFT只在离散频率点上采样真实频谱,这些频率点的间隔是fs/N_fft。如果信号频率恰好落在两个bin之间,峰值幅度会被低估,读出来的谐波大小也有偏差。
1.2 相干采样的数学条件和参数选择
相干采样的核心条件可以写成这样一个关系:
fin = (N / M) × fs
其中fin是信号频率,fs是采样频率,M是FFT采样点数,N是这段分析窗口内包含的完整信号周期数。N必须是正整数,M也必须是正整数,而且最好让N和M互质。
M和N互质这一点很容易被忽略,却很重要。如果M和N有公因子,等效于采样点只是在重复落在信号周期的某几个相位上,白白浪费了采样点提供的独立信息;而互质能保证M个采样点均匀覆盖整个信号周期,频谱更干净。
举个例子,假设我们要分析一个10MHz的正弦信号,想用1024点FFT,取N=13个完整周期。那么:
fs = (M × fin) / N = (1024 × 10MHz) / 13 ≈ 787.6923077 MHz
对应的采样周期Ts = 1/fs ≈ 1.26953237 ns。
窗口总时长 = M × Ts = 1024 / 787.6923077 MHz ≈ 1.3 μs,同时也等于N/fin = 13 × 100ns = 1.3μs,两边严格对齐。这就是相干采样的几何意义:分析窗口正好含整数个信号周期。
N为什么取13而不是16?因为1024是2的10次方,如果N取16,两者有公因子,采样相位分布不均匀;而13是一个与1024互质的质数,能获得最好的相位覆盖。在实际工程中我一般会在N=7、11、13、17、19这些质数里挑,优先选那些让fs落在合理范围的值。
1.3 采样率、谐波次数和抗混叠约束
光满足相干条件还不够,还得考虑谐波会不会混叠。假设我们要看前10次谐波,最高频率就是10×fin=100MHz。根据奈奎斯特采样定理,采样率必须大于100MHz的两倍。上面算出来的fs≈787.7MHz远高于200MHz,分析带宽fs/2≈393.8MHz,10次谐波妥妥落在带内,没有混叠风险。
但如果信号频率很高、采样点又取得很密,谐波次数一高就可能超过fs/2,折回到低频区域冒充其他谐波,这时候THD结果也会错。所以记住一个不等式:fs > 2 × K_max × fin,其中K_max是你要看的最高谐波次数。选采样率的时候先把它算清楚,后面能少踩很多坑。
2. Virtuoso瞬态仿真参数设置:给FFT喂干净的数据
2.1 三个关键参数:tstab、tstop、tstrobe
确定了相干采样的频率关系,接下来要把这些参数“翻译”成Spectre瞬态仿真的设置。打开ADE L或ADE Explorer里的tran分析,有三个参数需要特别在意:tstab、tstop、tstrobe。
tstab是电路稳定时间。实际电路上电瞬间会有毛刺和建立过程,直接把这段波形丢进FFT会引入极大的直流偏置和瞬态误差。常规做法是先跑一段让电路完全建立,再从稳定后的某个时刻开始做相干分析。比如电路在2μs后完全建立,就设tstab=2μs。
tstop是总仿真结束时间,至少要等于tstab加上相干窗口长度。针对上面的例子,相干窗口是1.3μs,tstop可以设成tstab + 1.3μs = 3.3μs,也可以再多留一点余量,比如3.5μs,方便在Calculator里微调起始点。
tstrobe是Spectre里非常有用的一个参数,设置后仿真器会以固定时间间隔输出数据,而不是按照内部自适应步长输出。因为相干分析本质上是对信号做等间隔采样,如果采样点时间间隔不均匀,FFT结果会有额外的误差。设tstrobeperiod = 1/fs = 1.26953237 ns,Spectre就会严格按照这个步长“打点”,这等于在仿真层面做了采样保持,后面做FFT时干净得多。
2.2 maxstep和errpreset对谐波精度的影响
很多人容易忽略maxstep。Spectre的瞬态仿真用的是自适应步长,如果maxstep设置得太宽松,仿真器为了速度会在某些区间大步跳过,产生了数值截断误差,这种误差会在频谱里表现为低频噪声抬升,甚至出现看起来像谐波的虚假谱线。
我一般采用errpreset=conservative,同时把maxstep设成最高关心谐波周期的1/20到1/50。看10次谐波,周期是10ns,maxstep可以取0.2ns~0.5ns,取0.2ns更稳。不要担心maxstep太小导致仿真慢,百纳秒级别的仿真在Virtuoso里通常只要几秒到几十秒,这个成本完全值得。
如果仿真时间窗口比较大,比如做到几千个周期,才需要考虑在保证精度和仿真时长之间做权衡,可以把errpreset降到moderate,并用tstrobe固定输出步长。但做THD分析,我建议始终保守一点,宁可慢,不要错。
2.3 后处理的STI窗口与多窗口平均
Virtuoso新版本里做FFT时经常能看到STI(Selective Time Interval)选项,其实就是让你手动选择一段时域区间来做频谱分析。这个功能特别适合处理瞬态切换场景,比如ADC采样过程中某一段保持稳定的数据。通过STI只选取稳定区间,能有效避开建立过程和切换毛刺的影响。
如果电路工作在周期性稳态,比如开关电容电路,还可以做多窗口平均:取多个不重叠的相干窗口分别做FFT,再把幅度谱平均。平均会降低随机噪声底,让更小的谐波浮现出来。Virtuoso里截图波形后多取几段区间跑FFT,人工对比一下就能感受到差别。
3. Virtuoso中FFT分析实操:从波形到频谱
3.1 Calculator中FFT的详细设置与误区
仿真跑完,打开ADE的Calculator波形窗口,选中要分析的信号(比如vout),然后在Calculator函数列表里找到fft函数。旧的界面是在函数面板里点fft,新版本则是在波形窗口里直接右键选择Spectrum或发到计算器。操作路径略有差异,但核心参数是一样的。
以我常用的设置为例,信号是10MHz正弦输出,按照第1节的相干参数,在FFT参数面板里填:
- Sample Count:1024
- Sample Period:1.26953237 ns
- Start Time:2.0 μs
- Stop Time:3.3 μs
- Window:Rectangular(相干采样时用矩形窗)
- Coherent Measurement:手动指定频率(或关闭自动调整)
这里最容易踩的坑是Sample Period。很多人以为是仿真器的输出步长,直接填1n或者干脆不填,结果频率轴完全对不上,频谱上基波位置偏了一大截。记住Sample Period必须等于1/fs,也就是相干采样计算出的采样周期。如果设置了tstrobeperiod,理论上Sample Period应该和它一致,但由于计算器用的是double精度,最好把1.26953237n这种长小数填全,不要四舍五入到1.27n,否则窗口长度会偏离整数周期,频谱又开始泄漏。
3.2 读懂频谱图:bin中心、频率轴和幅度轴
FFT结果出来之后,默认X轴是频率,Y轴是幅度。建议把Y轴切到dB20显示(幅值转成dBV),这样小谐波也能看得很清楚。频率轴的单位保持Hz即可,但要注意查看的是单边谱还是双边谱,Calculator的fft默认通常给出单边谱,也就是说基波的幅值直接就是信号幅度,不需要乘以2。如果看到基波在-6dBV左右,对应的线性幅度就是约0.5V。
每条谱线之间的间隔是频率分辨率Δf = fs/M。在我们这个例子里,Δf = 787.6923077 MHz / 1024 ≈ 769.23 kHz,正好等于fin/N = 10MHz/13。所以基波会出现在第13个bin,2次谐波在第26个bin,3次谐波在第39个bin,依此类推。这个关系非常有用,找谐波的时候不需要在频谱上肉眼乱扫,直接按第13、26、39个bin的位置去找,又快又准。
如果发现基波峰值不在整数bin上,说明相干条件没完全满足,或者Sample Period填错了。此时不要急着读数,先回头检查参数,否则提取的幅度误差很大。
3.3 加窗的选择:什么时候用矩形窗,什么时候用汉宁窗
相干采样条件下,分析窗口内正好是整数个周期,用矩形窗(也就是不加窗)就是最优选择,能量全部集中在一根谱线上,旁瓣泄漏最小。这一点很多从信号处理教材入门的人会疑惑,因为教材里总是强调FFT要加窗。但在相干测量的前提下,矩形窗反而最干净。
只有一种情况我会考虑加窗:信号频率没法自由选择,比如被测信号来自上一个模块的输出,频率是固定的,我们无法通过调整采样率来满足相干条件。这时候非相干引起的泄漏会污染频谱,临时救急可以用Hamming或Blackman窗压低旁瓣。但这样做有个副作用——主瓣会展宽,邻近谱线之间的区分度变差,而且幅度读数也需要按窗函数的相干增益校正。加窗得到的是近似结果,不如回头重新设计采样参数来得干净。
所以我在实际项目中的原则是:能相干,绝不加窗;不能相干,先改采样参数;参数也动不了,才退而求其次加窗,并明确标注THD结果是带误差的。
4. THD计算与自动化脚本
4.1 THD定义与手算步骤
THD(总谐波失真)衡量的是谐波能量相对于基波的比例,定义是:
THD = sqrt(V2² + V3² + V4² + ... + Vn²) / V1
其中V1是基波幅度,V2、V3……是各次谐波幅度。n取到多少,行业内没有完全统一的标准,通常取到5次、9次或10次。如果设计目标里明确提到关注某次谐波,比如差动电路主要看奇次谐波,那就把关注的谐波都纳入计算。标准做法是写到10次,因为超过10次的谐波在绝大多数设计里已经小到对THD数值没有影响。
举一个实测例子。假设频谱上读得基波V1=0.5V,2次谐波V2=3.16mV,3次谐波V3=1.78mV,4次V4=0.5mV,5次V5=0.28mV,更高次的可以忽略。那么:
THD = sqrt(3.16² + 1.78² + 0.5² + 0.28²) / 500 ≈ sqrt(9.986 + 3.168 + 0.25 + 0.078) / 500 ≈ sqrt(13.482) / 500 ≈ 3.672 / 500 ≈ 0.00734
换算成百分比就是0.734%,换算成dBc就是20×log10(0.00734) ≈ -42.7 dBc。如果设计目标是THD小于1%,这个结果就算通过了。
在这类计算里,很容易犯的一个错误是:直接从频谱图上用dBV读数,然后带着dB值去算THD。dB是对数域,不能直接平方求和,必须先转换回线性幅度再计算。
4.2 在Virtuoso里提取谐波幅值的方法
Virtuoso提取谐波幅值至少有三种方式。
第一种是手动方式:在频谱波形窗口里用Cursor工具,光标放到第13、26、39个bin上,直接读出对应的dBV值并记录下来。这个方法直观,适合一两个测试点,但多个工艺角和温度组合跑下来,手都要废掉。
第二种是用Calculator表达式。在Calculator里可以构造复合表达式,比如取特定频率处的幅度:
value(v("/VOUT" ?result "tran") 20e6 ?intrp "linear")
但要注意,直接对时域波形在不同频率点取value,并不是提取FFT谐波的正确方式。更靠谱的是先做FFT,再利用波形运算符提取特定bin的幅度。不同版本的Virtuoso在表达式语法上略有差异,建议直接在计算器里点选FFT结果波形,然后用value或peak函数读取该波形的特定频率点幅度。
第三种就是接下来要说的Ocean脚本,也是我最推荐的方式。
4.3 用Ocean脚本批量计算THD、SFDR和ENOB
如果需要跑多组工艺角或者多组输入幅度,建议把整个流程写进Ocean脚本。Ocean是Spectre自带的交互批处理语言,可以在ADE里通过Ocean窗口运行,也可以在命令行直接调用。下面给一个简化版模板,核心思路是:跑tran、算FFT、读出各次谐波幅度、算THD和SFDR。
; 加载设计、选择仿真器和工艺角 simulator( 'spectre ) design( "/path/to/design/input.scs" ) modelFile( '("/path/to/model/tt.lib" "tt") ) ; 设置瞬态分析参数 analysis( 'tran ?stop "3.3u" ?strobeperiod 1.26953237n ?tstab "2u" ?errpreset "conservative" ?maxstep 0.2n ) desVar( "freq" 10M ) desVar( "am" 0.5 ) ; 运行仿真 run() ; 获取时域输出并做FFT fftResult = fft( v("/VOUT" ?result "tran") 1024 1.26953237n 2u 3.3u ?window "Rectangular" ) ; 提取基波和各次谐波的幅度(频率点在计算结果中可能以bin索引访问) fund = peak( fftResult 10M ?xtype "frequency" ?peakThreshold 0.1 ) h2 = peak( fftResult 20M ?xtype "frequency" ?peakThreshold 0.001 ) h3 = peak( fftResult 30M ?xtype "frequency" ?peakThreshold 0.001 ) h4 = peak( fftResult 40M ?xtype "frequency" ?peakThreshold 0.001 ) h5 = peak( fftResult 50M ?xtype "frequency" ?peakThreshold 0.001 ) ; 计算THD(线性幅度域) thd = sqrt( h2*h2 + h3*h3 + h4*h4 + h5*h5 ) / fund ; 计算SFDR(最大杂散与基波之比,结果用dBc表示) sfdr = 20*log10( fund / max( h2 max( h3 max( h4 h5 ) ) ) ) printf("THD = %.4f %%\n" thd*100 ) printf("SFDR = %.2f dBc\n" sfdr )需要说明的是,不同版本Spectre对fft结果波形的peak函数参数支持不完全一样。如果peak函数读不出值,可以改用value函数结合xlim指定频率范围,或者提取bin值再按Δf换算。关键是思路:先把FFT结果存成波形,再在目标频率附近取局部最大值,这样可以避免噪声尖峰干扰读数。
如果还想算ENOB,可以同时采集噪声和失真,也就是SINAD:
SINAD = 20×log10(V1 / V_noise+dist)
ENOB = (SINAD - 1.76) / 6.02
Virtuoso里可以用snr函数或者自己计算信号功率与噪声失真总功率的比值。因为噪声功率分布在除基波和谐波以外的所有bin中,需要对全频段做积分运算,Ocean里可以这样粗略处理:总功率减掉基波功率和各谐波功率,剩下的就是噪声加失真功率,再代入公式。这个做法虽然不算严格,但在快速评估时非常实用。
5. 常见问题与排查实录
这里整理几个我在实际项目中反复遇到的FFT/THD问题,基本能覆盖大多数翻车现场,建议收藏起来对照排查。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 基波周围出现大面积“裙边”,频谱糊 | 分析窗口长度不是信号周期的整数倍,相干条件不满足 | 按fin=(N/M)×fs重新计算并修改Sample Period/Start/Stop |
| 基波峰不在整数bin中心,读数偏低 | Sample Period填错或四舍五入 | 填完整的小数采样周期,确保窗口长度精确等于N个周期 |
| 谐波呈阶梯状或明显底部抬高 | maxstep太大,数值截断误差被FFT放大 | 减小maxstep,errpreset设为conservative |
| 频谱噪声底像“高台”而非平底 | 加窗导致主瓣展宽,或非相干采样能量泄漏 | 优先保证相干采样并使用矩形窗 |
| FFT频率轴基波位置不对 | Sample Period与tstrobeperiod不一致 | 将两者统一为同一采样周期 |
| 高次谐波突然消失或出现不该有的谱线 | 谐波超过fs/2发生混叠 | 提高采样率fs,或降低关心的最高谐波次数 |
| THD计算值忽大忽小,不稳定 | 取谐波时没有对准bin | 用peak函数读取局部最大值,而不是直接取某频率点 |
5.1 基波旁边那摊“糊”到底是不是电路问题?
排查顺序要理清:先排除相干条件,再怀疑仿真精度,最后才找电路本身。有一次我帮同事看一个LDO的PSR仿真,他发现输出纹波的FFT频谱里基波旁边有一堆乱七八糟的频率分量,一开始怀疑是LDO环路振荡。我让他把瞬态波形窗口的起止时间改成信号周期的整数倍再跑FFT,频谱立刻干净了,问题根本不在电路,而在分析方法。
模拟电路仿真翻车,起码有一半是测量方法本身错了。Virtuoso不会主动告诉你“你这段窗口不是整数周期”,它只会诚实地把频谱算出来给你看,频谱长什么样完全取决于窗口怎么截。
5.2 谐波被底噪淹没时该怎么办
信号幅度本身就很小、或者电路线性度特别好时,谐波幅度可能低到和噪声底持平。FFT的点数决定噪声底的均值:点数每翻倍,理想情况下噪声底降低约3dB。所以第一个手段是增加FFT采样点数,比如从1024提高到8192,这是最直接有效的办法,代价是仿真时间和数据分析量都变大。
第二个手段是多窗口平均。把时域结果均匀切成多个相干窗口,分别做FFT再叠加平均,可以把随机噪声底压得更低。这个方法在Virtuoso里用脚本做比较方便,人工操作重复性太高。
第三个手段是检查仿真精度。如果maxstep太大,数值噪声就成了主要底噪来源,这时候加再多的点数也压不下去,先去把maxstep减小。
5.3 信号频率固定时怎么找相干点
真实项目里经常遇到这样的场景:系统时钟是24MHz,我们关心某个10MHz输入信号下的失真。理论上可以调整FFT点数M来找整数周期关系,比如M=32768时,找N使得fin=(N/M)×fs,N≈13653.33,并不是正好整数。这时候要么把fs微调,要么允许引入少量泄漏再用窗函数补偿。
我个人的习惯是,如果系统允许调整输入信号频率,就直接把fin设成能满足相干条件的值,比如fs=24MHz、M=4096、N=13,fin=76.171875kHz;如果频率不能动,那就尽量取更大的M来降低泄漏的绝对影响,或者接受加窗结果。
值得一提的是,Spectre新版本自带的fft相关函数里有“Coherent Measurement”模式,可以自动搜索最适合相干采样的窗口长度和采样点数,原理上就是替我们在做整数周期的优化。对新手省心,但它不会告诉你为什么自动选择了某个值,所以我还是建议先手动把原理走通,再用自动功能提速。
5.4 关于Virtuoso的一些杂谈:FFT只是工具链的一环
Virtuoso在整个模拟设计流程里的地位当然远不止FFT,从原理图输入、gmoverid辅助设计、前仿后仿到版图物理验证,是一整套庞大体系。在电路结构设计阶段,很多人用gmoverid方法来扫描晶体管偏置,快速确定沟道长度和过驱动电压,然后在相同环境里直接拉起瞬态仿真跑FFT看线性度,整套流程连贯起来非常顺手。这其实是我喜欢Virtuoso的原因:不需要把数据导来导去,同一个设计环境里就能完成“设计—仿真—后处理”的闭环。
FFT和THD分析正是这个闭环里最能体现“仿真工具是放大器”的环节。很多电路设计问题,比如运放输出级交越失真、单端电路的偶次谐波、Class-AB偏置引起的交越毛刺,都透过谐波分布第一时间暴露出来。看到2次谐波异常高,优先怀疑失配或不对称;看到3次谐波拖尾,优先怀疑偏置点和转换速率不够。FFT不只是用来算一个THD数字,它是在帮你做电路诊断。
6. 最后一组实操建议
跑FFT分析这几年,我最大的体会是:规范永远是“先想清楚窗口,再跑仿真”。绝大多数情况下,仿真本身没有任何问题,问题出在分析方式。人很容易在Virtuoso的操作界面上迷失——按下run、点开波形、脑子里全是下一步按钮在哪,而忘了最基础的问题:这段波形里到底包含了多少个完整的信号周期。
所以我每次做THD仿真前,都会在纸上先算一遍相干参数,把M、N、fs、tstab、tstop、maxstep这几个值写成一张便签贴在屏幕边上,再打开ADE。这一系列参数它们互相印证:如果采样率变了,tstrobeperiod和Calculator里的Sample Period必须同步变;如果窗口周期数变了,tstop和Start Time也要跟着调整。只要表格里的数能对上,FFT频谱通常一次就能干净利落地出结果。
最后一个小技巧:在Virtuoso的ADE窗口里,把FFT结果波形保存成一个独立的Output,再用Calculator表达式把这个FFT结果送给THD公式,这样每次重新仿真、扫描工艺角,THD值都会自动刷新,省去了重新取数的麻烦。把第一次人工操作的流程沉淀成脚本或表达式,后续效率会提升一大截。