1. 从零上手任意波形信号发生器:为什么值得花时间搞明白
任意波形信号发生器(Arbitrary Waveform Generator,简称AWG)在电子测试测量领域算是个“万能选手”。它能产生正弦波、方波、三角波这些常规波形,也能根据你自定义的数据点输出任意形状的信号——比如模拟传感器输出、复现某个电路故障时的异常波形、生成通信调制信号等等。如果你在做电路调试、器件测试、自动化验证,手里有一台AWG却只会按面板上的几个预设按钮,那基本等于买了一台跑车只在小区里遛弯。
我最初接触AWG是在一个电源纹波测试项目里,需要给被测板卡注入一个带特定谐波成分的干扰信号。手动在面板上拧旋钮根本没法精确控制每个周期的数据点,后来改用Python通过pyvisa库走SCPI命令远程控制,效率直接翻了好几倍。这篇内容就是把我从零摸索AWG编程的过程整理出来,包括环境搭建、SCPI命令基础、波形数据下发、常见坑的排查。不管你是刚入行的测试工程师,还是做嵌入式开发需要信号注入的软件人员,只要有一点Python基础,都能跟着走一遍。
核心关键词先摆出来:任意波形信号发生器、AWG、Python、pyvisa、SCPI。这几个词贯穿全文,后面每个环节都会围绕它们展开。你不需要事先了解SCPI协议,也不需要买昂贵的商业软件,一台支持LAN或USB接口的AWG、一台电脑、Python环境,就够了。
2. 动手前的整体设计:为什么选Python加pyvisa这条路
2.1 控制AWG的几种方式对比
在决定用Python之前,我对比过几种常见的AWG控制方案,这里直接给个表格,方便你根据自己情况选:
| 控制方式 | 典型工具 | 适合场景 | 主要限制 |
|---|---|---|---|
| 面板手动操作 | 设备前面板按键/旋钮 | 简单波形、临时调试 | 无法批量、无法精确控制数据点 |
| 厂商上位机软件 | 各品牌自带GUI | 常规测试、快速出结果 | 跨品牌不通用、自动化能力弱 |
| 脚本编程控制 | Python + pyvisa | 自动化测试、自定义波形 | 需要写代码、需要理解SCPI |
| 嵌入式控制 | MCU/FPGA发命令 | 产线集成、实时性要求高 | 开发周期长、调试成本高 |
选Python + pyvisa的理由很直接:跨平台、跨品牌、生态好。pyvisa是Python对VISA(Virtual Instrument Software Architecture)库的封装,而VISA是测试测量行业事实上的标准I/O接口。只要你的AWG支持USBTMC、TCP/IP或GPIB中的任意一种,pyvisa基本都能通吃。换一台不同品牌的设备,底层SCPI命令可能有差异,但Python代码框架几乎不用大改。
2.2 SCPI命令的基本逻辑
SCPI(Standard Commands for Programmable Instruments)是一套标准命令语言,核心思想是用树状层级结构描述仪器功能。比如设置通道1的输出频率,命令是:
SOURce1:FREQuency 1000拆开看:SOURce是根节点,1是通道号,FREQuency是子节点,后面跟参数值。查询当前频率则用:
SOURce1:FREQuency?注意问号,这是SCPI的查询语法。命令不区分大小写,但习惯上大写字母表示缩写形式,小写字母表示可省略部分。比如SOURce可以写成SOUR,也可以写全SOURCE。
提示:不同厂商对SCPI命令的扩展程度不同。Keysight、Tektronix、RIGOL、Siglent这些主流品牌的基础命令高度一致,但高级功能(如序列模式、调制深度)的命令格式会有差异。写代码前务必查对应型号的编程手册。
2.3 整体流程设计
我习惯把整个控制流程拆成四步:
- 环境准备:安装Python、pyvisa、VISA后端驱动,确认设备能通
- 连接与识别:用pyvisa枚举资源、建立连接、读取设备ID
- 基础控制:设置波形类型、频率、幅度、偏置、输出开关
- 任意波形下发:构造数据点数组、转换成设备要求的格式、通过SCPI下发
这个顺序的好处是每一步都有明确的验证点。连接不通就别急着写波形数据,基础参数没设对就别碰任意波形模式。很多新手一上来就想直接下发自定义波形,结果连设备都没连上,排查起来一头雾水。
3. 环境搭建:Python、pyvisa和VISA后端一个都不能少
3.1 Python安装与版本选择
Python安装本身没什么好说的,官网下载安装包一路下一步就行。但有几个细节值得注意:
- 版本选3.8以上。pyvisa从1.11版本开始对Python 3.6的支持就逐渐弱化了,现在主流是3.9到3.12。我目前在用3.11,稳定性和库兼容性都很好。
- 安装时勾选“Add Python to PATH”。这个选项如果不勾,后面在命令行里敲
python会提示找不到命令,还得手动配环境变量。 - 建议用虚拟环境。
python -m venv awg_env然后激活,避免和系统里其他项目的库版本冲突。尤其是你如果同时在做数据分析、爬虫之类的项目,numpy版本冲突是常有的事。
如果你之前完全没碰过Python,建议先花半小时过一遍基础语法——变量、列表、循环、函数。不需要学到面向对象,能看懂for循环和列表操作就够了。网上有很多免费的Python入门教程,搜“Python基础语法”就能找到。
3.2 pyvisa安装与VISA后端配置
pyvisa本身只是一个Python库,它需要调用底层的VISA库才能和仪器通信。安装分两步:
pip install pyvisa然后安装VISA后端。常见的选择有:
- NI-VISA:National Instruments出品,功能最全,支持USB、LAN、GPIB。缺点是安装包大(几百MB),启动稍慢。
- Keysight IO Libraries Suite:Keysight(原Agilent)的VISA实现,对自家设备支持最好。
- RIGOL VISA:RIGOL设备用户可以用这个,体积小。
- pyvisa-py:纯Python实现的VISA后端,不需要装厂商驱动,但功能有限,部分设备可能不支持。
我个人的建议是:如果你用的是NI或Keysight的设备,直接装对应厂商的VISA。如果是RIGOL、Siglent等品牌,装NI-VISA最省事,兼容性最好。pyvisa-py可以作为备选,但遇到USB连接问题时优先换厂商VISA。
安装完VISA后,在Python里验证:
import pyvisa rm = pyvisa.ResourceManager() print(rm.list_resources())如果输出类似('USB0::0x2A8D::0x1102::MY12345678::INSTR',)的元组,说明VISA后端正常工作,设备也被识别到了。如果输出空元组,说明要么设备没连好,要么VISA后端没装对。
3.3 连接方式选择:USB还是LAN
AWG通常支持USB和LAN两种连接方式。USB即插即用,适合桌面调试;LAN适合远程控制或多设备组网。从编程角度看,两者在pyvisa里的用法几乎一样,只是资源地址不同:
- USB资源地址格式:
USB0::0xVENDOOR_ID::0xPRODUCT_ID::SERIAL::INSTR - LAN资源地址格式:
TCPIP0::192.168.1.100::INSTR
USB的优点是稳定、延迟低,缺点是线缆长度受限。LAN的优点是灵活,缺点是网络配置可能出问题(IP冲突、防火墙拦截等)。我一般桌面调试用USB,机架测试用LAN。
注意:用LAN连接时,建议给AWG分配固定IP,或者在路由器里做DHCP绑定。否则设备重启后IP变了,代码里的地址就得跟着改,很烦。
4. 核心实操:从连接设备到下发任意波形
4.1 建立连接与设备识别
连接设备的标准写法:
import pyvisa rm = pyvisa.ResourceManager() # 替换成你实际枚举到的地址 awg = rm.open_resource('USB0::0x2A8D::0x1102::MY12345678::INSTR') awg.timeout = 5000 # 超时5秒 print(awg.query('*IDN?'))*IDN?是SCPI的通用识别命令,几乎所有仪器都支持。返回内容通常包含厂商、型号、序列号、固件版本,比如:
Keysight Technologies,33522B,MY12345678,3.02这一步能通,说明物理连接和基本通信没问题。如果报错VisaIOError,先检查设备是否开机、线缆是否插紧、VISA地址是否正确。
4.2 基础波形参数设置
在玩任意波形之前,先把基础波形控制跑通。以设置通道1输出1kHz正弦波、幅度2Vpp、偏置0V为例:
awg.write('SOURce1:FUNCtion SINusoid') awg.write('SOURce1:FREQuency 1000') awg.write('SOURce1:VOLTage:AMPLitude 2') awg.write('SOURce1:VOLTage:OFFSet 0') awg.write('OUTPut1:STATe ON')每条命令的含义:
SOURce1:FUNCtion SINusoid:设置通道1波形为正弦SOURce1:FREQuency 1000:频率1000HzSOURce1:VOLTage:AMPLitude 2:幅度2VppSOURce1:VOLTage:OFFSet 0:偏置0VOUTPut1:STATe ON:打开通道1输出
设置完可以用查询命令确认:
print(awg.query('SOURce1:FREQuency?')) print(awg.query('SOURce1:VOLTage:AMPLitude?'))返回的字符串可能需要转成浮点数再比较,因为仪器返回的格式可能是+1.00000000E+03这种科学计数法。
4.3 任意波形数据的构造与下发
这是整篇内容的核心。任意波形的本质是一串按时间顺序排列的电压采样点,AWG按照设定的采样率依次输出这些点,经过DAC和滤波后形成模拟波形。
假设我要生成一个自定义的“阶梯波”,每个周期10个点,幅度从0到1V递增。步骤如下:
第一步:确定采样率和点数
采样率决定了波形的时间分辨率。如果我要输出一个1kHz的阶梯波,每个周期10个点,那采样率至少是10kHz。但实际AWG的采样率通常远高于此(比如100MSa/s),所以需要把波形数据“填充”到更高的点数。
这里有个关键概念:AWG的波形内存深度。比如设备支持16384个点,你下发10个点,设备可能会自动插值或者循环播放。不同设备的处理方式不同,查手册确认。
第二步:构造数据数组
用numpy构造数据最方便:
import numpy as np points = 1000 # 总点数 cycles = 10 # 包含10个周期 samples_per_cycle = points // cycles waveform = [] for i in range(cycles): for j in range(samples_per_cycle): waveform.append(j / samples_per_cycle) waveform = np.array(waveform)第三步:归一化到设备要求的范围
大多数AWG要求波形数据是-1到1之间的浮点数,或者-32768到32767之间的整数。以浮点为例:
waveform_normalized = waveform * 2 - 1 # 映射到-1到1第四步:通过SCPI下发数据
不同厂商的下发命令格式不同。以Keysight 33500系列为例:
# 设置任意波形模式 awg.write('SOURce1:FUNCtion ARB') # 下发数据,使用二进制块传输 data_bytes = waveform_normalized.astype('<f4').tobytes() awg.write_binary_values('SOURce1:DATA:ARBitrary ', waveform_normalized, datatype='f', is_big_endian=False)write_binary_values是pyvisa提供的二进制数据传输方法,比逐点写ASCII字符串快几个数量级。datatype='f'表示单精度浮点,is_big_endian=False表示小端序。具体参数要看设备手册。
下发完成后,设置采样率:
awg.write('SOURce1:FUNCtion:ARBitrary:SRATe 1000000') # 1MSa/s awg.write('OUTPut1:STATe ON')4.4 参数计算:采样率、频率和点数的关系
这三者的关系是新手最容易搞混的地方。公式很简单:
输出频率 = 采样率 / 每周期点数或者反过来:
每周期点数 = 采样率 / 输出频率举个例子:采样率设为1MSa/s,我想要输出1kHz的波形,那每周期需要1000个点。如果波形内存深度只有16384点,那最多能存16个周期。
实际设置时,我习惯先确定输出频率和波形细节要求,再反推采样率。比如要输出一个含高次谐波的复杂波形,最高谐波频率是100kHz,根据奈奎斯特采样定理,采样率至少要是200kSa/s,实际取5到10倍余量,设1MSa/s比较稳妥。
注意:采样率不是越高越好。高采样率意味着波形内存被快速消耗,能存储的波形周期数减少。而且部分AWG在高采样率下会有更大的量化噪声。根据实际需求平衡。
5. 常见问题与排查技巧实录
5.1 连接类问题
问题一:list_resources()返回空
排查顺序:
- 设备是否开机?USB线是否插紧?
- 设备管理器里能否看到仪器?Windows下看“通用串行总线控制器”或“其他设备”
- VISA后端是否安装?装完后是否重启过?
- 换一个USB口试试,前置面板的USB口供电可能不足
问题二:连接超时
awg.timeout = 5000 # 默认2000ms可能不够有些设备响应慢,尤其是刚开机或正在执行长命令时。把超时设大一点,5000到10000ms。
问题三:LAN连接不上
先ping设备IP,通了再检查:
- 防火墙是否拦截了VISA的端口
- 设备是否设置了固定IP,和电脑是否在同一网段
- 资源地址里的IP是否正确
5.2 波形下发类问题
问题一:下发的波形输出不对
最常见的原因是数据格式不匹配。检查:
- 数据类型是float32还是int16?
- 字节序是大端还是小端?
- 数据范围是-1到1还是0到1?
我踩过一次坑:设备要求int16,我传了float32,结果输出完全乱套。后来用awg.query('SOURce1:DATA:ARBitrary?')读回数据对比才发现。
问题二:波形有台阶感
这是点数不够导致的。增加每周期点数,或者开启设备的插值/滤波功能。部分AWG支持SOURce1:FUNCtion:ARBitrary:FILTer命令设置输出滤波器。
问题三:输出幅度不对
任意波形的幅度受两个因素影响:波形数据的归一化范围,以及VOLTage:AMPLitude的设置。如果数据范围是-1到1,幅度设为2Vpp,那输出就是2Vpp。如果数据范围是-0.5到0.5,输出就只有1Vpp。
5.3 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 枚举不到设备 | 驱动未装/线缆问题 | 重装VISA、换线换口 |
| 连接超时 | 超时设置太短 | 增大timeout值 |
| 波形输出乱 | 数据格式不匹配 | 查手册确认数据类型和字节序 |
| 波形有台阶 | 点数不足 | 增加点数或开启插值 |
| 幅度不对 | 归一化范围不对 | 检查数据范围与幅度设置 |
| 频率不对 | 采样率与点数不匹配 | 按公式重新计算 |
| 输出无信号 | 输出未开启 | 发送OUTPut1:STATe ON |
5.4 独家避坑经验
经验一:先读后写。在修改任何参数之前,先用查询命令读当前值。这样出问题时能快速定位是哪个参数设错了。
经验二:分步验证。不要一次性写完所有代码再跑。先连设备,再设基础波形,再下发任意波形,每一步都验证输出。这样出问题容易定位。
经验三:保存和复用配置。SCPI支持*SAV和*RCL命令保存/调用仪器状态。调试好的配置存下来,下次直接调用,省去重复设置。
经验四:注意命令终止符。pyvisa的write默认会加换行符,但有些设备要求\n或\r\n。如果命令不生效,检查awg.write_termination和awg.read_termination设置。
经验五:用*OPC?同步。下发长命令后,用awg.query('*OPC?')等待操作完成。直接发下一条命令可能导致设备还没处理完就收到新命令,出现不可预期的行为。
6. 进阶方向:从单次下发到自动化测试
6.1 波形序列与循环播放
基础玩法是下发一个波形然后循环输出。进阶玩法是利用AWG的序列模式(Sequence Mode),把多个波形片段按顺序播放,每个片段可以设置不同的循环次数和跳转条件。这在模拟复杂信号场景时非常有用,比如模拟一串通信数据包。
以Keysight为例,序列模式的命令大致是:
awg.write('SOURce1:FUNCtion ARB') awg.write('SOURce1:FUNCtion:ARBitrary:SEQuence:NEW') awg.write('SOURce1:FUNCtion:ARBitrary:SEQuence:SEGMent1:...')具体命令格式各厂商差异较大,需要查对应手册。但核心思路是一样的:定义片段、设置播放顺序、启动序列。
6.2 与自动化测试框架集成
如果你在做产线测试或批量验证,可以把AWG控制封装成Python类,和万用表、示波器、电源的控制代码整合到一个测试脚本里。典型流程:
- 电源上电
- AWG输出激励信号
- 示波器采集响应
- 万用表测量关键点电压
- 判断结果、记录数据
- 断电、进入下一个被测件
这种自动化测试的效率是手动操作的几十倍,而且一致性更好。pyvisa的跨品牌特性让这套框架可以适配不同厂商的设备,换设备时只需要改资源地址和少量SCPI命令。
6.3 用Python做波形可视化
调试阶段,把要下发的波形先画出来看看,能避免很多低级错误:
import matplotlib.pyplot as plt plt.plot(waveform_normalized[:200]) plt.title('Waveform Preview') plt.xlabel('Sample Index') plt.ylabel('Amplitude') plt.grid(True) plt.show()matplotlib是Python最常用的绘图库,安装pip install matplotlib即可。画出来的波形和预期不符,就别急着下发,先改数据。
6.4 性能优化:二进制传输与批量操作
逐点用ASCII字符串下发波形数据非常慢。1000个点可能要好几秒。用二进制块传输能快几十倍:
awg.write_binary_values('SOURce1:DATA:ARBitrary ', waveform_normalized, datatype='f')另外,把多条SCPI命令合并成一条发送也能减少通信开销。SCPI支持用分号分隔多条命令:
awg.write('SOURce1:FUNCtion SINusoid;FREQuency 1000;VOLTage:AMPLitude 2')但注意,不是所有设备都支持这种写法,而且合并后出错时不好定位是哪条命令的问题。调试阶段还是分开写,稳定后再合并。
7. 一些个人体会
AWG编程这件事,入门门槛其实不高,但坑不少。我最初卡在VISA驱动上折腾了一下午,后来发现是USB线的问题。又有一次下发波形死活不对,查了半天才发现是字节序搞反了。这些经验在官方手册里不会写,只有实际踩过才知道。
另一个体会是:不要怕查手册。SCPI命令那么多,没人能全记住。遇到不认识的命令,直接翻编程手册的SCPI命令章节,通常都有详细说明和示例。厂商的示例代码也是很好的参考,虽然风格可能比较老旧,但命令格式是准确的。
最后说一个实用技巧:如果你手头没有实体AWG,可以用一些厂商提供的仿真软件或者SCPI模拟器来练手。虽然不能完全替代真实设备,但至少能把代码框架跑通,等有设备了直接改资源地址就能用。