
本质上,频谱分析仪就是一台“把信号从时域拆成各个频率成分”的功率测量仪:横轴是频率,纵轴是每个频率上的电平(电压/功率,通常用 dBm、dBV 表示)。
为Zynalog ADC设计一个Python接口实现频谱分析仪

是德
可以先把它想成:
示波器:看波形随时间怎么变 频谱仪:看“这堆波形里面都有哪些频率、各占多大能量”
下面分两大类讲:
1)传统模拟扫频型频谱分析仪(尤其是 RF 那种几 GHz 的)
2)现代 FFT 频谱分析仪(ADALM2000/示波器里内置的 Spectrum)

大多数人的FFT功能都是来自于示波器的数学功能
这是老派 RF 频谱仪的工作原理,也是很多教科书里讲的那种。
要测输入信号在每个频率点上的功率,做法:用一个可调的本振(LO),把“想看的频段里某个频率点 f_c”混频搬到一个固定的中频 IF;后面放一个带宽很窄的中频滤波器(RBW 滤波器),只让这一小段频带通过;检波、整流、取对数 → 得到“这个小频带里的总功率”;把 LO 的频率慢慢扫过整个频段,边扫边在屏幕上画点,于是横轴就是频率、纵轴就是电平。
可以理解为:
用一个“窄带可移动小窗”在频率轴上扫来扫去,每次只看这个小窗里多少能量,慢慢把整条频谱画出来。
在扫频型频谱仪里,这几个是核心概念:

这些概念可以从这个文档里面看,应该是大厂里面独一份的存在

目前是德的这个频谱分析仪可以做到110GHz,太恐怖了
RBW(Resolution Bandwidth)
相当于“频率小窗”的宽度;RBW 越小,越能看清两条很近的谱线;同时,噪声功率 ∝ RBW,所以 RBW 越小,噪声底越低(曲线更干净)。
Sweep Time(扫频时间)
RBW 越小,中频滤波器越窄、越“慢”,每个频点要等更久让输出稳定;所以扫描整个频段要更长时间;仪器通常会根据 Span、RBW、VBW 自动计算一个最小 Sweep Time。
VBW(Video Bandwidth)
是对“检波后曲线”的低通滤波;VBW 越小,曲线越平滑(多做平均),但刷屏更慢;有时你会看到“毛毛糙糙的噪声→调小 VBW→平滑噪声曲线”的效果。
这套逻辑和目前在做噪声积分、ENBW 的时候其实是一脉相承的:
RBW ≈ 滤波器相当的噪声带宽; RBW 越小,单位 Hz 的噪声功率密度看上去就越低。
现代的低频 / 中频频谱分析、音频分析、ADALM2000/示波器的软件频谱,基本都是 FFT 型,思路完全不一样。(就是使用数值的方式进行纯技术,属于大力出奇迹的做法)
对于 FFT 型频谱仪(包括 Scopy 的 Spectrum Tool),流程大致是:
前端模拟链路(有时是直接输入):输入信号 → 衰减/放大 → 抗混叠滤波器 → ADC;对 RF 型的,还会先下变频到一个中频再数字化。
采样 & 缓冲
用 ADC 以采样率 fs 连续采样,得到时域序列 x[n];一次分析用 N 点:x[0...N-1]。
加窗(Windowing)
为了减少频谱泄漏,对采样序列乘一个窗函数 w[n](Hann, Hamming, Blackman 等);得到 xw[n] = x[n] * w[n]。
FFT
计算:
得到一组复数 X[k],每个 k 对应频率:
计算幅度/功率
幅度谱:
功率谱或 PSD(功率谱密度)可以按不同约定换算;
再变成 dB:
或功率:
多次重复采样 + FFT,做平均(线性 / 对数 / 峰值保持),降低闪动;对应扫频型里的 VBW/平均功能;最后显示横轴频率(0~fs/2),纵轴电平,和扫频型频谱仪一样的图。
对于 FFT 频谱,频率分辨率是:
想要更好的分辨率:增大 N(采更长时间的数据),或者减小采样率 fs;FFT 里的“等价 RBW”就是 Δf 再乘上窗函数的 ENBW;
所以 FFT 频谱仪里,看到的 “RBW = XXX Hz” 本质就是
对我们来说这很好理解:在之前的文章中做 ENBW、噪声积分时算过很多类似的东西。
可以一次性获取整个带宽的频谱(0~fs/2),速度快;对低频/中频、小信号噪声分析特别方便;和数字系统结合紧密,可以做各种高级处理(相干平均、跨谱、FRF 等)。
但是缺点明确,比如最重要的就是带宽受 ADC 采样限制(fs/2);对非常高频的 RF 场景,需要用下变频+宽带 ADC 才能实现;而且对动态范围、杂散、抖动等指标要求很高,硬件成本不低。
走进 10G 采样的直采 ADC:奇历士CAE2200(带时域测试版)可以使用这样的ADC直接计算。
前面写过网络分析仪,现在可以把三者放在一起看:
示波器:看时间波形 x(t),偶尔可以附带一个 FFT 功能。
频谱分析仪(Spectrum Analyzer):只看 x(t) 的频谱 |X(f)|(功率/幅度),不关心输入输出关系。
网络分析仪(Network Analyzer): 自己产生激励 x(t),同时测输入和输出,算:
看的是“系统”的频率响应,而不是单个信号的频谱。
从信号处理角度看:
频谱仪:给定一个信号,算它的 FFT。
网络分析仪:给定输入、输出两路信号,算它们的比值/相关性(相当于测系统传递函数)。
像 ADALM2000(M2K)、以及很多数字示波器里都有“频谱 / Spectrum / FFT”模式,其实就是:前端模拟链路 + ADC;固定采样率 fs 采集一段数据;在 FPGA/ARM 或 PC 上做窗函数 + FFT;把结果以 dB/√Hz 或 dBV 画成频谱。
用代码重现一遍 “示波器 → 导出数据 → FFT → 频谱图” 的流程,那就是自制频谱分析仪了。
ADALM2000 的频谱分析模式,本质就是:前端模拟链路 → ADC 采样 → 一些数字预处理(抽取/滤波/校准)→ FFT → 标定和显示;我按“从输入电压到屏幕上的 dB 曲线”这个顺序,把整个链路建模一遍,以后可以直接照这个框架用 Python 重现任意一个ADC的频谱分析逻辑。


这是把“纯 FFT 数字结果”变成“真实电压/噪声”的关键。


M2K 的频谱分析器如果显示的是“dBV/√Hz”,则会把每个 bin 的 RMS 电压除以 √ENBW,从而得到近似“噪声密度”单位。
之前文章写过HDL 结构,可以在频谱模式下这样简化:
模拟前端 → AD9963,12-bit,100 MSPS;在FPGA 内部接口:100 MHz 时钟、使用12 bit×2 通道数据。
AXI_ADC_DECIMATE(数字抽取 + 滤波)
CIC + FIR 抽取,得到一个较低采样率 (比如 1 MSPS 或 100 kS/s)这个滤波器本身就决定了一部分前端带宽/抗混叠特性;将一段长度 N 的数据被搬到内存,供 ARM / PC 读取。
给一个“12 bit ADC + FFT 频谱分析”的建模示例:模拟一个带 1 kHz 正弦 + 白噪声的输入;采样率 100 kS/s,N = 16384 点;使用 Hann 窗;计算并显示:线性频谱(dBV);噪声底(dBV/√Hz)。
这里只是仿真,不直接用 M2K;以后可把“产生 x[n]”改成“从 M2K 读回数据”。
信号频率 bin: 164 对应频率: 1000.9765624999999 Hz
估计 Vrms: 0.13912767159019965 V


脚本做了从 ADC 参数 → FFT → 最终 dB 频谱的完整建模流程:模拟了前端模拟信号 + 噪声 → 量化 → 窗函数 → FFT;用 CG 把窗口带来的幅度缩减补回来;最后用 ENBW 把噪声转换成“密度”单位。
ADALM2000 的频谱分析仪确实就是“ADC 采样 + 数字预处理 + FFT + 标定 + 显示”,但每一步(采样率、窗函数、CG/ENBW、单位换算)都决定了看到的频谱形状、噪声底和数值的含义。
import numpy as np
import matplotlib.pyplot as plt
# 1. ADC / FFT 参数
fs = 100_000.0 # 采样率 100 kS/s
N = 16384 # FFT 点数
B = 12 # 12 bit ADC
V_FS = 1.0 # 满量程 ±1 V,示例
# 2. 构造模拟输入信号:1 kHz 正弦 + 白噪声
f_sig = 1000.0
A_sig = 0.2 # 峰值 0.2 V → 约 0.141 Vrms
t = np.arange(N) / fs
x_analog = A_sig * np.sin(2 * np.pi * f_sig * t)
# 设定输入噪声密度 50 nV/√Hz(随便举例)
e_n = 50e-9
# 对应到每个采样点的时域噪声标准差:sigma = e_n * sqrt(fs/2)
# (粗略关系,准确推导更复杂,这里当示范)
sigma_noise = e_n * np.sqrt(fs/2)
noise = np.random.normal(scale=sigma_noise, size=N)
x_analog += noise
# 3. ADC 量化建模:12 bit,范围 ±V_FS
# 3.1 限幅
x_clip = np.clip(x_analog, -V_FS, V_FS)
# 3.2 量化步长
Delta = 2 * V_FS / (2**B)
codes = np.round(x_clip / Delta) # 量化到整数码
codes = np.clip(codes, -(2**(B-1)), 2**(B-1)-1)
# 3.3 再转换回电压(这一步和 M2K 驱动做的类似)
x_digital = codes * Delta
# 4. 窗函数:Hann
window = np.hanning(N)
CG = np.sum(window) / N # 相干增益
ENBW_factor = np.sum(window**2) * N / (np.sum(window)**2)
df = fs / N
ENBW = df * ENBW_factor
xw = x_digital * window
# 5. FFT(只看正频)
X = np.fft.rfft(xw)
freqs = np.fft.rfftfreq(N, d=1/fs)
# 6. 从 FFT 幅值恢复单频正弦的峰值、RMS、电压谱等
# 6.1 幅度谱(峰值,针对单频信号)
# A_est[k] = 2 * |X[k]| / (N * CG)
A_est = 2 * np.abs(X) / (N * CG)
Vrms_line = A_est / np.sqrt(2)
# 6.2 把正弦的那个 bin 标出来看看
k_sig = np.argmin(np.abs(freqs - f_sig))
print("信号频率 bin:", k_sig, "对应频率:", freqs[k_sig], "Hz")
print("估计 Vrms:", Vrms_line[k_sig], "V")
# 7. 噪声谱:计算近似“电压噪声密度”(V/√Hz)
# Vrms_line 是每个 bin 的等效 RMS(包含信号+噪声)。
# 对于噪声,可以除以 sqrt(ENBW) 得到近似噪声密度。
Vn_density = Vrms_line / np.sqrt(ENBW)
# 8. 转换为 dBV 与 dBV/√Hz
spec_dBV = 20 * np.log10(Vrms_line / 1.0 + 1e-20)
spec_dBV_Hz = 20 * np.log10(Vn_density / 1.0 + 1e-30)
# 9. 作图
plt.figure(figsize=(10, 6))
plt.semilogx(freqs, spec_dBV, label="Line spectrum (dBV)")
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude (dBV)")
plt.grid(which="both", ls=":")
plt.legend()
plt.title("FFT Spectrum (windowed, Hann)")
plt.tight_layout()
plt.show()
plt.figure(figsize=(10, 6))
plt.semilogx(freqs, spec_dBV_Hz, label="Noise density (dBV/√Hz)")
plt.xlabel("Frequency (Hz)")
plt.ylabel("Noise density (dBV/√Hz)")
plt.grid(which="both", ls=":")
plt.legend()
plt.title("Estimated Noise Spectral Density")
plt.tight_layout()
plt.show()