手头的ADALM2000,越玩越好玩,这篇分享一个有趣的,但是平时研发不常见的仪器:网络分析仪(可绘制电路的波特图、奈奎斯特图和尼科尔斯图)测量范围:1Hz 至 10MHz

看一个是德的仪器

这个是测试的结果

测量的时候就是这样的方案,一边激励,一边测量

专业的仪器测量的范围都很宽,而我们这个小玩具才最大到10M
但是对了解原理没有什么问题。

任何线性系统(电路、滤波器、放大器、传感器前端……)都可以抽象成:
在频域就是:
所以 网络分析仪做的事情,本质就是测出:
然后画成两张图:

幅频特性: → dB/Hz 曲线(Bode 图的上半部分)
相频特性: → 相位-频率(Bode 图下半部分)
这就是“网络分析”的本体。
仪器内部有一个“源”(source):
正弦扫频:或多频合成(chirp、multisine);幅度、频率都由仪器精确控制和知道(这点很关键:自己知道“它给了什么”)。
把激励加在 DUT 上,比如:低通滤波器、运放电、LC 谐振、传感器、功放、变压器……

测量点一般有两处:“输入端”电压 作为参考 ,“输出端”电压 作为响应
用两个同步 ADC 通道,以相同采样率采:
CH1:参考信号 (或电流)
CH2:响应信号
同步采样保证:相位关系是可信的,不被通道延时差搞乱。
对采来的数据做频域分析,一般有两条路:
单频正弦 + 锁相放大式算法:
对每个频点 f,采一小段时间窗的数据,和 做相关:
这相当于在目标频率上做一个窄带锁相检测,SNR 很高。
一段波形做 FFT:
对整个时间窗做 FFT:
找到对应频率 bin ,然后:
不管哪种方式,得到的 都是一个复数,包含幅度和相位。
然后画幅度:(dB),相位(度):
为了得到频率响应,就让频率从 扫到:
线性扫:步进 Δf,比如 100 Hz、200 Hz、300 Hz…
对数扫:每 decade 固定点数,比如 10 Hz → 100 Hz → 1 kHz → 10 kHz…

每个频点重复上面的采集 + 计算,就得到整条 Bode 曲线。
Scopy 里的 Network Analyzer 工具,本质就是用一个集成的:两个 AWG 通道(DAC)+ 两个 Scope 通道(ADC)

直接用内部
来实现一个 低频 FRA(频率响应分析仪)。
典型连接方式(最常见的用法):
AWG CH1 输出一个正弦波 → 通过电阻/电路输入到 DUT。
Scope CH1 测量 输入端电压 Vin(参考)
Scope CH2 测量 输出端电压 Vout(响应)
在 HDL 里是这样走的:DDR 中缓存好一段正弦波样点(比如一个周期 N 点):通过 AXI_DMAC_A/B → AXI_DAC_INTERPOLATE → AXI_AD9963(TX) → 板外 DUT → 板上前端 → AXI_AD9963(RX) → AXI_ADC_DECIMATE → AXI_ADC_TRIGGER/FIFO → AXI_DMAC → DDR;ARM + Linux从 DDR 读取采样数据,再交给上位机软件(Scopy)做频域计算。
里面有 插值/抽取 和 触发/FIFO 在这里就派上用场了:
插值让 AWG 内部数据率低一些,减少 DDR 读带宽;最终仍以高 DAC 速率输出;抽取让采样数据率低一些,减少 USB 传输和处理负担;然后触发 + FIFO可以保证在每个频点上采到的波形都是“稳定状态”,并且有足够的 pre-trigger/post-trigger 波形用来做 FFT。
用 M2K 输出扫频正弦 + 采样数据;做 FFT / 计算 H(f);画 Bode 图。
先生成扫频正弦(swept sine);对每个频点,生成理想输入 x(t),通过一个“虚拟 DUT”(比如一阶 RC 低通)得到输出 y(t);用 FFT 在该频点对应的 bin 上算:
画幅频 + 相频 Bode 图。
未来只要把 simulate_dut() 换成真实的 M2K 采集函数即可。

主要改这两块:
替换 simulate_dut():
现在版本是用理论幅相算出来的 y(t);换成“调用 libm2k / Scopy后端”:用 AWG 输出频率为 f_sig 的正弦;用 Scope 同步采集两路数据 vin[n], vout[n];然后把这两个数组丢给 measure_transfer_fft() 即可。
示意式伪代码大概是:
def acquire_from_m2k(f_sig, fs, n_cycles, vin_amp):
# 1. 配置 AWG:频率 f_sig、幅度 vin_amp
# 2. 配置 Scope:采样率 fs,通道1测 Vin,通道2测 Vout
# 3. 等待系统稳定(可以丢掉前几十个周期)
# 4. 抓取 N 点数据 → vin, vout
return t, vin, vout
然后在主循环里改成:
# 原来:
t, vin = generate_sine(...)
vout = simulate_dut(t, vin, f_sig, fc=fc_dut)
# 替换为:
t, vin, vout = acquire_from_m2k(f_sig, fs, n_cycles, vin_amp)
频点、周期数、平均次数
可以加上“每个频点重复测量 M 次再平均”,提高 SNR:
或者先在 ADC 侧做叠加平均,再 FFT。
import numpy as np
import matplotlib.pyplot as plt
def generate_sine(f_sig, fs, n_cycles, amplitude=1.0):
"""
生成某个频率下的正弦波测试信号。
f_sig: 被测信号频率 (Hz)
fs: 采样率 (Hz)
n_cycles: 采样的周期数
amplitude: 正弦幅度
"""
T = n_cycles / f_sig # 总时长
N = int(np.round(T * fs)) # 采样点数
t = np.arange(N) / fs
x = amplitude * np.sin(2 * np.pi * f_sig * t)
return t, x
def rc_lowpass_H(f, fc):
"""
一阶 RC 低通的理论传递函数 H(j2πf) = 1 / (1 + j f/fc)
f: 频率 (Hz) 或 numpy 数组
fc: 截止频率 (Hz)
"""
s = 1j * f / fc
return 1.0 / (1.0 + s)
def simulate_dut(t, x, f_sig, fc=1e3):
"""
用“理想一阶 RC 低通”来模拟 DUT。
这边不做真正的滤波器卷积,而是直接用理论幅度/相位来构造 y(t):
y(t) = |H(f)| * sin(2π f t + φ)
这样能保持算法结构,又不依赖 SciPy 等库。
将来你用 M2K 时,把这一函数替换成“采集真实 y(t)”即可。
"""
H = rc_lowpass_H(f_sig, fc)
mag = np.abs(H)
phase = np.angle(H)
y = mag * np.sin(2 * np.pi * f_sig * t + phase)
return y
def measure_transfer_fft(x, y, fs, f_sig):
"""
用 FFT 在 f_sig 对应的频率 bin 上估计 H(f) = Y/X。
x, y: 两路同步采样信号 (numpy 数组)
fs: 采样率 (Hz)
f_sig: 当前扫频点 (Hz)
返回: H_est (复数),包含幅度和相位
"""
# 统一长度
N = min(len(x), len(y))
x = x[:N]
y = y[:N]
# 加窗,减小泄漏(这里用 Hann 窗)
window = np.hanning(N)
xw = x * window
yw = y * window
# 只做 rFFT(实信号),只关注正频
X = np.fft.rfft(xw)
Y = np.fft.rfft(yw)
freqs = np.fft.rfftfreq(N, d=1.0/fs)
# 找到最接近 f_sig 的 bin
k = np.argmin(np.abs(freqs - f_sig))
# 估计传递函数
# 注意:真实系统里要处理 X[k] ≈ 0 的情况,这里简单略过
H_est = Y[k] / X[k]
return H_est
def sweep_network_analyzer(
f_start=10.0,
f_stop=1e5,
points_per_decade=20,
fs=1e6,
n_cycles=200,
vin_amp=1.0,
fc_dut=1e3
):
"""
扫频测量 DUT 的幅频 + 相频(这里 DUT 用一阶 RC 低通模拟)。
将来如果接 M2K,只要改“获取 y(t)”那一步即可。
参数含义:
- f_start, f_stop: 扫频起止频率 (Hz)
- points_per_decade: 每十倍频点的采样数(对数扫频)
- fs: 采样率 (Hz)
- n_cycles: 每个频点采多少个周期
- vin_amp: 输入正弦幅度
- fc_dut: 虚拟 RC 低通的截止频率 (Hz)
"""
# 生成对数扫频频点
n_decades = np.log10(f_stop) - np.log10(f_start)
n_points = int(n_decades * points_per_decade)
freqs = np.logspace(np.log10(f_start), np.log10(f_stop), n_points)
mag_db_list = []
phase_deg_list = []
for f_sig in freqs:
# 1) 生成输入正弦
t, vin = generate_sine(f_sig, fs, n_cycles, amplitude=vin_amp)
# 2) 通过 DUT(此处为仿真;真实 M2K 时用采集)
vout = simulate_dut(t, vin, f_sig, fc=fc_dut)
# 3) 用 FFT 算传递函数
H_est = measure_transfer_fft(vin, vout, fs, f_sig)
mag_db = 20 * np.log10(np.abs(H_est))
phase_deg = np.angle(H_est, deg=True)
mag_db_list.append(mag_db)
phase_deg_list.append(phase_deg)
mag_db_array = np.array(mag_db_list)
phase_deg_array = np.unwrap(np.deg2rad(phase_deg_list)) # 弧度展开
phase_deg_array = np.rad2deg(phase_deg_array)
return freqs, mag_db_array, phase_deg_array
def plot_bode(freqs, mag_db, phase_deg, title="Simulated Network Analyzer"):
"""
画一个双子图 Bode 图:上幅度,下相位。
"""
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6))
ax1.semilogx(freqs, mag_db)
ax1.set_ylabel("Magnitude (dB)")
ax1.grid(True, which="both", ls=":")
ax2.semilogx(freqs, phase_deg)
ax2.set_ylabel("Phase (deg)")
ax2.set_xlabel("Frequency (Hz)")
ax2.grid(True, which="both", ls=":")
fig.suptitle(title)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
# 示例:被测 DUT 是 fc = 1 kHz 的一阶 RC 低通
fc = 1e3
freqs, mag_db, phase_deg = sweep_network_analyzer(
f_start=10,
f_stop=1e5,
points_per_decade=20,
fs=1e6,
n_cycles=200,
vin_amp=1.0,
fc_dut=fc
)
plot_bode(freqs, mag_db, phase_deg,
title=f"Simulated RC Lowpass (fc = {fc:.0f} Hz)")
https://www.analog.com/cn/resources/analog-dialogue/studentzone/studentzone-july-2019.html