☰
开源VNA2:1MHz-6GHz矢量网络分析仪的射频设计与C++校准实现
2026/10/16 22:08:03 网站建设 项目流程

简介:这套资源是1MHz-6GHz USB矢量网络分析仪VNA2的第二版设计包,适合射频硬件工程师、嵌入式开发者以及仪器DIY爱好者学习参考。新版将RF芯片替换为性价比更高的型号,供电改为5V以支持USB直接供电,并利用微控制器配置FPGA,免去JTAG编程器,同时调整RF布局增强了端口间隔离。资源包共704个文件,约10.67MB,主要包含C/C++固件源码(.h、.cpp)、FPGA的VHDL设计文件(.vhd)、PCB版图(.brd/.sch)、Qt界面文件(.ui)以及PDF说明、S2P测试数据等,完整覆盖从硬件底层到PC端软件的各个环节。通过源码和文档可以了解FPGA与MCU的联合配置方式、USB供电下的射频电源方案,以及上位机处理四个S参数的测量逻辑。已有1151人学习,对想要自行搭建宽带网络分析仪或研究射频测量系统的开发者来说,是一份具有较强工程参考价值的资料。

1. 拆解 VNA2:1MHz 到 6GHz 的 USB 矢量网络分析仪到底强在哪

VNA、RF、C++ 这三个词凑在一起,基本就说明这份资源不是玩具级的 Arduino 测距仪,而是一台真正能进实验室的矢量网络分析仪。VNA2 是它的第二版,频率范围从 1MHz 拉到 6GHz,上位机用 C++ 写,硬件通过 USB 和电脑通信。对做天线、滤波器或者射频匹配电路的人来说,这意味着不用再排队蹭实验室的台式 VNA,也能在桌面上快速看 S11、S21 的扫频曲线。它的价值不在“能测”,而在“这个频段范围下,校准和误差修正怎么做才不至于白测”。新手拿到手先跑通软硬件链路,熟手则可以直接参考它的射频前端架构和 C++ 处理管线,省掉自己从零趟坑的时间。

2. 射频前端设计:从 1MHz 到 6GHz 的信号链路与四个容易失衡的环节

2.1 反射桥与定向耦合器的分工:为什么低频段和高频段要分开处理

矢量网络分析仪的核心任务是分离入射波和反射波,然后比较两者的幅度和相位。在 1MHz 到 6GHz 这个跨度里,没有任何一种单一的无源结构能在全部频段都保持良好的方向性。传统微波定向耦合器基于四分之一波长耦合线,在 6GHz 时四分之一波长只有几毫米,做在 PCB 上没问题;但到了 1MHz,四分之一波长接近几十米,根本不可能集成。所以第二版最常见的处理方式是分两段:低频段用阻性反射桥,高频段用定向耦合器或者继续用反射桥的改进型。

阻性反射桥本质上是一个惠斯通电桥,把三个电阻和一个被测阻抗接成桥路,从电桥的不平衡端提取反射信号。它的优点是带宽极宽,从直流到 6GHz 都能工作,缺点是方向性通常只能做到 15 到 20dB,插损约 6dB。这意味着小反射测量时会受到方向性误差的干扰,但可以通过后续的校准算法补偿。很多开源 VNA 项目干脆全频段都用反射桥,VNA2 这类方案在第二版里改进的是桥臂电阻的寄生参数:贴片电阻在高频会有引线电感和并联电容,导致桥在 4GHz 以上失衡。常见做法是用 0402 封装电阻对称布局,并且在桥的两个输出端加等长微带线补偿相位。

如果选择了高频段用定向耦合器,耦合器的方向性和平坦度会更好,但工作带宽受到耦合线结构的限制。常见做法是采用宽边耦合或多节耦合器,比如三节渐变耦合器可以覆盖 2GHz 到 6GHz,低频段再用反射桥接续。这样设计的好处是高频段测量精度更高,坏处是两条路径之间的转换点需要额外的校准和切换开关。我更推荐直接全频段反射桥加高精度校准,因为 VNA2 的使用场景大多是调试天线和匹配网络,方向性误差只要校准做得扎实,实际测量结果完全够用。

2.2 频率合成与混频方案:低中频单段扫频的取舍

VNA 的接收机结构决定了动态范围和测量速度。常见的低成本方案是把反射桥输出的射频信号直接送到宽带检波器,只测幅度不测相位,但那不叫矢量网络分析仪。要得到相位信息,必须把射频信号下变频到一个固定的中频,然后通过 ADC 采集 IQ 数据。VNA2 这类 1MHz 到 6GHz 的仪器,激励源和本振通常都由锁相环频率合成器产生。

频率规划的关键是让本振频率始终比射频信号高或者低一个固定中频。比如中频选 10.7MHz,扫频到 2GHz 时本振就设 2.0107GHz,扫频到 6GHz 时本振就设 6.0107GHz。整个扫频本振必须跟着射频源同步变化,这要求固件里有一个同步状态机,而不是两个独立模块各扫各的。第二版常见改进是加入一个额外的低频频段直通路径:1MHz 到 30MHz 直接通过运放缓冲进 ADC,不再经过混频器,因为混频器在极低本振频率下往往没有足够的驱动功率,导致转换增益骤降。

另一个取舍是镜像抑制。低中频方案镜像频率距离射频信号只有两倍中频,比如射频 2GHz、中频 10.7MHz,镜像就在 2.0214GHz。如果前端没有滤波,镜像信号和真实信号会同时下变频并叠加。常见做法是在反射桥输出后加一个低通滤波器,截止频率设在 6.5GHz 左右,这样大部分频段内镜像落在滤除范围外。但低频段镜像落在通带内,所以要依赖混频器本身的镜像抑制度。我在复现时习惯用双平衡混频器并仔细匹配中频端口的平衡阻抗,这比额外加一个跟踪滤波器简单得多。

2.3 双通道接收:参考信号和反射信号同时数字化的时机

矢量测量需要两个通道:一路采样激励源的参考信号,一路采样从被测件反射回来的信号。VNA2 这类 USB 设计通常把 ADC 放在设备端,也就是混频后的两路中频信号分别经过可调增益放大器,然后送给同一颗双通道 ADC。这样做的好处是两路信号共享同一个采样时钟和混频本振,相位漂移被完全抵消,只剩下被测件本身带来的相位差。

实际操作时,第一版常见的坑是两路中频放大器的增益不匹配。虽然校准可以归一化幅度,但如果放大器在不同增益档位下相位延迟不一致,扫频曲线就会出现不连续的台阶。VNA2 第二版通常会减少增益档位数量,或者用高分辨率数字电位器做连续增益控制,确保在校准频段内相位响应单调。ADC 的采样率不用太高,中频如果选 10.7MHz,采样率 20MSPS 甚至 25MSPS 就能满足,后续在 C++ 软件里做数字下变频和抽取滤波。

如果你打开 VNA2 的原理图,会发现信号链路里还有一个容易被忽略的细节:参考通道的中频信号幅度往往比反射通道大很多,因为参考信号直接从激励源耦合,几乎不经过衰减;而反射信号经过反射桥后本身就有 6dB 以上损耗,遇到低反射器件时还会更弱。所以两个通道的增益必须独立调节,软件里要有自动增益控制的逻辑。常见做法是在每次扫频开始前做一次功率探测,把两路中频信号的幅度都调整到 ADC 满量程的 70% 左右,这样能最大程度利用 ADC 的动态范围。

3. C++ 控制与处理软件:把 IQ 采样变成 S 参数的核心路径

3.1 USB 传输结构:命令口与数据口分离的两种预案

VNA2 通过 USB 和上位机通信,传输结构直接影响扫频速度和数据完整性。最稳妥的做法是设备端实现一个复合 USB 设备,一个 HID 接口用来传输命令,一个批量传输接口用来传输采样数据。HID 接口的优点是操作系统自带驱动,不需要额外安装,而且数据包有握手,命令不容易丢失;缺点是传输速率低,不适合大数据量。批量传输接口则相反,速度可以到 40MB/s 级别,但驱动安装稍微麻烦一点。

我在实际使用这类资源时发现,很多第二版工程会把命令和数据都塞进批量传输端点,省掉 HID,理由是 Windows 和 Linux 下用 libusb 操作批量端点非常直接。这种做法的问题是设备枚举时如果被系统识别为未知设备,需要手动安装驱动。如果资源包里提供了驱动签名的 INF 文件,那问题不大;如果没有,调试时就会卡在这一步很久。我的建议是保留 HID 做低速命令通道,比如设置频率、设置功率、触发单次扫描,同时用批量端点传输 IQ 数据。C++ 代码里用两个线程分别处理,能避免扫频过程中命令排队拥堵。

代码骨架可以这样组织:

// usb_link.h class UsbVna { public: bool init(); // 枚举设备,打开接口 bool send_cmd(uint8_t cmd, uint32_t arg); bool read_iq_block(std::complex<float>* buf, size_t count); private: void* hid_handle; // 命令通道 void* bulk_handle; // 数据通道 };

这个类把 USB 通信抽象成两个独立的通道。init 函数里会分别打开 HID 和批量接口,如果批量接口打开失败,就回退到纯 HID 模式,只是扫频速度会慢。send_cmd 函数用来下发设置命令,比如设置起始频率、终止频率、扫描点数,以及驻留时间,命令数据包只有 64 字节,走 HID 完全够用。read_iq_block 则从批量端点读取一块复数 IQ 数据。第二版工程资源里经常看到类似的抽象层,这比把 USB 底层操作散落在业务代码里要清晰得多。

3.2 扫频状态机:频率驻留、中频滤波与平均次数的配平

扫频逻辑并不是简单地逐个设置频率然后立即读数据。射频前端和混频器在频率跳变之后需要一段建立时间,中频滤波器也需要积累足够的周期才能给出稳定输出。如果驻留时间太短,测得的数据点会叠加瞬态响应,S11 曲线上出现毛刺;如果驻留时间太长,扫一次全频段的耗时无法接受,尤其是设置了 4001 个扫描点的时候。

一个常用的经验公式是驻留时间取中频滤波器带宽倒数的五到十倍。比如中频带宽设为 1kHz,那么对应的建立时间至少是 5ms。VNA2 的软件里通常会把中频带宽的档位和驻留时间绑定,而不是让用户单独设置。实际扫频状态机可以拆成几个阶段:跳频、功率稳定、中频滤波器充电、采样、切换下一个频点。C++ 里可以用一个有限状态机来实现,也可以用最简单的 for 循环加 sleep。

// 扫频参数表 struct SweepConfig { double start_hz; double stop_hz; int points; int if_bw_hz; // 中频带宽 int averages; // 重复平均次数 }; void run_sweep(UsbVna* vna, const SweepConfig& cfg, std::vector<std::complex<float>>& s11_vec) { double step = (cfg.stop_hz - cfg.start_hz) / (cfg.points - 1); uint32_t dwell_us = 1'000'000 / cfg.if_bw_hz * 10; // 10倍时间常数 for (int i = 0; i < cfg.points; ++i) { double f = cfg.start_hz + step * i; vna->send_cmd(CMD_SET_FREQ, (uint32_t)f); std::this_thread::sleep_for(std::chrono::microseconds(dwell_us)); std::complex<float> ref, meas; vna->read_iq_block(&ref, 1); vna->read_iq_block(&meas, 1); s11_vec[i] = meas / ref; // 原始未校准反射系数 } }

这段代码有三个值得注意的地方。C++ 标准库里的std::complex<float>可以直接存储 IQ 数据,复数除法的结果就是反射系数的幅相表示。驻留时间用微秒级整数计算,避免浮点数换算的精度问题。扫频步进用points - 1做分母,保证起始频率和终止频率都能落到扫描点上,很多第一版资源在这里写错,导致最后一点频率比设定值低一个步长。

3.3 从时域 IQ 到频域 S11:标准复数运算的落地写法

ADC 采样到的中频信号在时域上是正弦波,幅度和相位信息分别落在 I 和 Q 两个分量上。经过数字下变频之后,IQ 数据就变成了一个复数序列,实部是 I 路,虚部是 Q 路。要得到单个频点的反射系数,需要对这个复数序列做平均或者低通滤波,提取直流分量。VNA2 上位机里最常见的做法是对采样序列做相干累加,也就是把 N 个复数直接相加再除以 N,这样可以有效抑制随机噪声。

平均次数的选择要看中频带宽和期望的动态范围。中频带宽越窄,滤波器抑制噪声的能力越强,但扫描时间越长;平均次数越多,随机噪声降低越明显,但同样耗时。常见做法是把中频带宽设为几个档位:100Hz、1kHz、10kHz、100kHz,平均次数在 1 到 16 次之间可调。对于 6GHz 频段的天线调试,1kHz 带宽加 4 次平均已经能稳定测出 -40dB 左右的反射系数。

std::complex<float> extract_center_iq(const std::complex<float>* buf, size_t n) { std::complex<double> acc(0, 0); for (size_t i = 0; i < n; ++i) { acc += buf[i]; } return static_cast<std::complex<float>>(acc / static_cast<double>(n)); }

这段代码的逻辑是纯矢量运算,没有任何窗函数处理,因为在扫频式 VNA 里每个频点是稳态信号,不需要像 FFT 分析仪那样做加窗抑制泄漏。实际使用中如果发现噪声偏大,先不要急着增加平均次数,而是检查中频带宽是否设置得太宽。我遇到过一个典型情况:USB 供电的 VNA 在 5GHz 以上噪声明显变大,换了个独立供电的 hub 就好了,这说明电源纹波对 ADC 的影响远大于软件平均次数的提升效果。

4. 校准:SOLT 误差模型为什么会在 6GHz 附近“失真”

4.1 误差盒模型与三个待解误差项

所有 VNA 测量到的原始 S11 都不是真实值,而是经过系统误差污染之后的值。单端口反射测量可以抽象成一个误差盒模型,包含三个误差项:方向性误差 E00、源匹配误差 E11、反射跟踪误差 E10E01。方向性误差来自反射桥本身有限的方向性,即使接一个完美的 50Ω 负载,反射通道也会测到一点残余信号。源匹配误差来自激励源输出阻抗不完全匹配,导致被测件的反射信号在源端被二次反射。反射跟踪误差则是参考通道和反射通道增益不一致造成的幅度和相位偏差。

这三个误差项和真实反射系数之间的数学关系是:

S11m = E00 + (E10E01 × S11a) / (1 - E11 × S11a)

其中 S11m 是测量值,S11a 是真实值。只要在测量前分别接上三个已知反射系数的标准件,测出对应的 S11m,就能反解出 E00、E11 和 E10E01。这就是 SOLT 校准里 S(短路)、O(开路)、L(负载)三个标准件的用途。校准之后,任意被测件的真实 S11a 可以通过求解上式得到。

高频段的麻烦在于,标准件的反射系数不再等于理想值。理想开路是反射系数 1,但实际开路标准件在 6GHz 时存在边缘电容,反射系数会偏离 1;短路标准件有残余串联电感,同样偏离 -1。如果校准软件不修正这些寄生参数,校准后测一个标准的 50Ω 负载,会看到曲线上下漂移。第二版工程的校准代码里必须有每个标准件的频率响应模型,通常是用电容或电感量级结合电长度来定义的。

4.2 标准件定义:开短路模型的寄生参数修正

实际校准件定义包含两个部分:参考面偏移和寄生参量。参考面偏移指的是短路和开路标准件作为物理器件,它的实际短路点或开路点与连接器参考面之间存在一小段传输线,这可以用一个电长度来表示。寄生参量则是开路端的边缘电容和短路端的残余电感。在低频段,这些参量影响很小,可以忽略;但到了 6GHz,一个 0.3pF 的边缘电容就会让开路标准的反射系数从 1 变成约 0.87 的相位偏移。

C++ 里实现校准件模型时,通常用一个类封装三个标准的表现:

struct CalStandard { double offset_length_m; // 参考面偏移长度 double offset_loss; // 偏移损耗,单位奈培/米 std::complex<double> gamma_open(double f) { double cap = 0.15e-12; // 开路边缘电容,典型值 std::complex<double> z(0, -1.0 / (2 * M_PI * f * cap)); std::complex<double> z0(50, 0); return (z - z0) / (z + z0); } std::complex<double> gamma_short(double f) { double ind = 0.05e-9; // 短路残余电感,典型值 std::complex<double> z(0, 2 * M_PI * f * ind); std::complex<double> z0(50, 0); return (z - z0) / (z + z0); } };

这里的关键参数是开路边缘电容和短路残余电感,它们的数值必须和实际校准件的物理结构匹配。不同品牌的开路头边缘电容差异可以很大,从 0.1pF 到 0.5pF 都有。如果你在 5GHz 以上校准后测开路出现负反射系数,多半是电容模型设置得过小或过大。高频校准失败时,先不要怀疑硬件,把标准件模型换一组参数再试一次。

4.3 C++ 误差修正实现:从校准件测量到待测件还原

校准流程在代码里分两步:第一步采集三个标准件的原始 S11m,第二步代入公式求解误差项。为了数值稳定性,通常不直接解那个有理式,而是构造线性方程组。定义中间变量后,三个标准件分别提供一组方程,用高斯消元或矩阵求逆求解:

struct SinglePortCal { std::complex<double> e00, e11, e10e01; void compute(const std::vector<double>& freq, const std::vector<std::complex<double>>& m_open, const std::vector<std::complex<double>>& m_short, const std::vector<std::complex<double>>& m_load) { // 理想负载时,S11a=0,所以测量值 e00 = m_load e00 = m_load[0]; // 其余两个误差项由开路/短路测量值反解 std::complex<double> go = gamma_open(freq[0]); std::complex<double> gs = gamma_short(freq[0]); std::complex<double> denom = m_open[0] - m_short[0]; e11 = ( (m_open[0] - e00) / go - (m_short[0] - e00) / gs ) / ( (m_open[0] - e00) / go * gs - (m_short[0] - e00) / gs * go ); e10e01 = (m_open[0] - e00) * (1 - e11 * go) / go; } std::complex<double> correct_s11(std::complex<double> m) { std::complex<double> diff = m - e00; return diff / (e10e01 + e11 * diff); } };

这段代码展示了单点频率的校准计算,实际工程需要在外层再套一层频率循环,逐频点计算。e00 等于负载测量值是个很漂亮的简化,因为负载标准真实反射系数接近 0,所以在误差模型里可以直接读取。但要注意,6GHz 时负载标准件的残余反射不再是零,用理想 0 会造成方向性校准不精确。VNA2 这类工程里通常会额外提供一个可选的滑动负载校准,或者用多次连接负载取平均来减少随机误差。

还有一个实用技巧:校准之后立即用开路标准回测一次,观察测量值与理想模型之间的偏差。这个偏差直接反映校准质量。在 C++ 里可以写一个verify_calibration函数,计算回测开路和短路的复误差。如果偏差超过一定阈值,自动提示重新校准。从实际经验看,6GHz 频段下校准质量受连接器重复性影响很大,每次重新连接标准件都会引入微小的相位变化,所以校准标准件需要使用高质量的连接器,并且尽量使用扭矩扳手。

5. 避坑笔记:6GHz 测量中反复翻车的 5 个排错记录

5.1 现象→原因→解决:五个真实高发坑

坑一:1MHz 附近 S11 测出正增益

现象:接一个 50Ω 负载,低频段 S11 显示 -2dB 甚至 -1dB,比 0dB 还高,看起来像有源器件。

原因:反射桥的低频响应受隔直电容影响。为了给后级放大器提供直流偏置,很多设计会在信号路径上串隔直电容,电容和桥臂电阻形成高通滤波,导致 1MHz 附近反射通道幅度失真。

解决:把隔直电容容值加大到微法级别,或者改用无源偏置结构。我在复现时换了 1uF 的陶瓷电容并联 0.1uF 高频电容,低频段响应立刻恢复正常。软件层面不要试图通过校准修正,这种频率相关的幅度塌陷很难用误差模型精确补偿。

坑二:4GHz 以上 S11 曲线出现周期性波纹

现象:扫频曲线在 4GHz 以上叠加了周期约 300MHz 的波纹,幅度 0.3dB 左右。

原因:反射桥到 ADC 混频器之间的信号走线太长,且没有做阻抗匹配,形成了驻波。波纹周期对应走线电长度的两倍,300MHz 周期意味着电长度约 0.5m,这在 PCB 上显然不可能,很可能是连接线缆造成的。

解决:把反射桥输出到混频器输入的线缆换成半刚性同轴,尽量缩短长度。如果走线在 PCB 上,需要保证特性阻抗 50Ω 并有足够的过孔接地。

坑三:校准后测短路出现螺旋状偏差

现象:校准完成回测短路标准,反射系数幅度接近 1,但相位在 5GHz 以上偏离理想 -1 超过 20 度。

原因:校准件模型里的参考面偏移长度设错了。相位偏差随频率线性增长,典型就是电长度误差。

解决:在主控软件里加一个端口延时的微调参数,把回测相位偏差拟合出一条直线,斜率就是延时误差。用这个误差值修正校准件模型再重新校准,通常一步就能修平。

坑四:中频带宽从 10kHz 改成 100Hz 后,扫描曲线整体偏移

现象:窄带模式下测量的 S11 幅度和宽带模式下差 0.5dB。

原因:中频滤波器带宽改变会引入不同的群延迟,如果增益放大器的相位响应不是完全平坦,带宽变化就导致相位偏移。虽然校准能够部分修正,但校准件套件里若只保存单一带宽下的误差项,切换带宽后修正就会失效。

解决:每个中频带宽档位单独做一次校准,或者在软件里对相位做群延迟补偿。VNA2 上位机参数里通常有一项 IF Bandwidth,切换时必须确认当前校准数据对应的带宽值。

坑五:USB 连接下扫到一半曲线出现断点

现象:扫频曲线大部分正常,但随机几个频点数据为 0 或者明显跳变。

原因:上位机读取数据超时。USB 批量传输在高负载和系统休眠策略下可能出现瞬间断流,而固件侧没有重新发送机制。

解决:在批量读取代码里加超时重试逻辑,超时后重新发起一次读请求;同时把每批读取的复数个数控制在 256 个以内,减少单次传输的数据量。我习惯用 128 或 256 作为块大小,扫频结束后再拼接完整曲线。

6. 验证与进阶:把校准结果做成可信的测量数据

6.1 三个低成本验证动作

拿到校准后的 VNA 数据时,第一件事不是急着测待测件,而是验证系统本身的可信度。手里不需要高级验证件,用校准套件里的短路和开路标准就能做基础验证。回测开短路时,记录每个频点的反射系数和理想模型之间的误差。如果幅度偏差在 0.1dB 以内、相位偏差在 3 度以内,这套校准可以进入正式使用。如果偏差较大,回到第五章节的避坑记录里依次排查。

第二个验证动作是用一段已知长度的同轴线缆加一个精密负载,测这段线缆的输入阻抗。初学者常忽略的是,线缆本身的损耗和相位延迟会随频率变化,直接用理想公式去反推长度往往对不上。更好的做法是测量 S11 后做 IFFT 变换到时域,观察反射脉冲的位置。在 C++ 里可以借助 FFT 库,把频域复数数据补零到 4096 点再反变换,得到的时域波形里能看到线缆末端负载的反射峰。通过反射峰的时延换算线缆长度,和物理实际长度对比,偏差应该在几个百分点以内。

第三个动作是测一个已知衰减器的 S21。虽然抛出的标题是 S11 为主,但 VNA2 这类设备通常会提供传输测量功能。软件里加一个直通校准后,测一个 10dB 衰减器,观察 1MHz 到 6GHz 的插损波动范围。合格的衰减器在工作频带内插损波动应该在 0.5dB 以内。如果波动明显,可能是端口之间的串扰或校准件直通延迟没有完全消除。

这些验证动作不需要额外购买昂贵仪表,完全用配套软件和校准件就能完成。从那以后,我每次拿到这类 VNA 资源,都会强制在正式测试前走一遍开短路回测、时域反射定位和衰减器插损这三个验证动作,确认校准质量过关才敢把数据写进报告。希望这些经验能帮你把 VNA2 的每一分硬件性能都榨出来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询