ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#通过NI-VISA远程控制NI仪器:从环境搭建到SCPI实战

C#通过NI-VISA远程控制NI仪器:从环境搭建到SCPI实战 简介一份以C语言编写的NI-VISA仪器远程控制示例源码包面向需要掌握Visa API与常用仪器通信方式的嵌入式或测试测量开发者特别适合刚接触GPIB、USB、TCP/IP等接口编程的人群。压缩包共56个文件包含18个C源代码文件、18个dsp与18个vcproj工程文件以及sln/dsw解决方案与工作区文件整体仅93KB便于快速浏览工程结构与直接编译。内容覆盖VXI、GPIB、USB、TCP/IP、串口等常见接口的读写与中断示例提供资源查找、读写操作、设备锁定、异步I/O、异步SRQ、触发路由等典型场景代码能帮助读者理解打开默认资源、发送控制命令、捕获异步事件及错误处理等关键流程。除基础通信演示外还包含共享系统、等待中断等高级用法并配有多种编译工程文件方便在Visual Studio中直接打开调试。目前已有188人学习下载适合作为学习C语言配合NI-VISA进行设备编程的入门参考与实践模板。1. 这台仪器只能手按用 C# 把 NI 设备远程控制拆穿做产线测试或实验室自动化的工程师大概率碰到过这样的场景一台 NI 的信号发生器或者万用表操作员每天要手动按几百次面板数据记在纸上再敲进电脑。等项目量上来第一反应是“能不能用代码远程控制”。但一查资料就发现NI 官方给的工具多是 LabVIEW 或者 TestStand项目组里没人会用 Python 写又担心交付后别人维护不了而 C# 做上位机是厂房里最常见的技术栈却很少有人把 VISA 这条链路讲透。这个标题里的visa_c_demo_instrument_c_NI_remotecontrol_VISAC#_源码说白了就是一套用 C# 通过 NI-VISA 远程控制 NI 仪器的最小可运行方案。它能解决的核心问题是把仪器面板上的按键变成代码里的函数调用把读数变成变量把人工记录变成自动落库。适合做产线测试系统、计量校准软件、实验室数据采集平台的工程师参考。全文我会按“环境怎么搭、最小代码怎么写、SCPI 指令怎么配合、踩了哪些坑”这个顺序把一个值得投入的落地路径直接铺开。2. 先把 VISA 这条链路拆开NI-VISA 运行时与 C# 工程的引用关系2.1 远程控制 NI 仪器为什么要走 VISA 而不是直接发网口命令很多人第一次接触远程控制仪器时会有一个直觉仪器有网口我直接用 TCP 连上去发字符串不就行了这个思路在少数设备上可用但放在 NI 生态里不是一个好选择。NI 的台式仪器、PXI 机箱、USB 接口的采集卡它们对外暴露的通信入口并不统一。有的走 GPIB有的走 USB-TMC有的走 TCP/IP甚至同一台设备在不同固件版本下支持的协议都有差异。VISA 是一个抽象层它把这些物理总线统一成一种资源描述和一套 I/O 函数让你写代码时不用关心背后到底是 USB 还是网口。另一个现实原因是驱动和会话管理。NI-VISA 不仅做管道转发还会管理仪器会话、超时、事件回调以及和 NI 驱动的底层交互。你直接用 Socket 去连网口遇到超时重连、缓冲区溢出、固件回显差异时所有逻辑都要自己写。而 VISA 把这些收敛成了viOpen、viWrite、viRead这几个函数就算换了总线类型上层代码基本不动。对 C# 工程师来说这意味着一套代码能同时覆盖 GPIB 仪器和 LAN 仪器省掉的维护成本非常可观。当然走 VISA 也不是没有成本。它要求目标 PC 安装 NI-VISA 运行时而且 32 位和 64 位版本要跟你编译的 C# 目标平台匹配。这个问题第一次遇见会绕很久我后面会详细讲。2.2 安装 NI-VISA 和确认 C# 能引用的程序集在开始写 C# 代码之前先把运行环境配好。最常用的方案是安装 NI-VISA 运行时。安装包可以从 NI 官网下载安装完成后会在系统里注册一个叫Ivi.Visa.dll的程序集另外还有一个通用的VisaNS.dll。这两者之间的关系常常让人困惑VisaNS是 NI 对 VISA 规范的 .NET 封装官方名字叫 NI-VISA .NET Class Library而Ivi.Visa是 IVI Foundation 的 VISA .NET 接口NI 也提供了实现。对于新项目我更推荐用Ivi.Visa因为它的接口定义更干净异步方法齐全Community 版本持续在更新。装好运行时后建议立刻打开 NI 自带的NI MAXMeasurement Automation Explorer在左侧“设备和接口”里确认仪器已经被识别。比如一台 NI 的 PXI-5406 信号发生器识别为PXI1 Slot2。然后右键创建一个 VISA 会话这会帮你生成类似TCPIP0::192.168.1.100::INSTR或者PXI1::2::INSTR这样的资源地址。这个资源字符串就是后面 C# 代码里open时最重要的参数建议先把它复制下来存好。在 C# 工程里引用方式有两种。一种是直接右键“添加引用”浏览到 NI-VISA 安装目录下的Ivi.Visa.dll另一种是用 NuGet 搜索Ivi.Visa项目会拉取最新的前向兼容包。我一般用 NuGet 方式因为依赖关系更清晰而且方便团队其他人拉代码时自动还原。以下是一个最小工程的 csproj 片段用来确认依赖已经进来Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet6.0-windows/TargetFramework PlatformTargetx64/PlatformTarget /PropertyGroup ItemGroup PackageReference IncludeIvi.Visa Version5.5.0 / /ItemGroup /Project这里面有两点要特别说明。第一PlatformTarget设置为x64如果你的仪器是 USB-TMC 连接且 NI-VISA 装的是 32 位版本那你必须把目标平台改成x86。判断依据是 NI MAX 的“帮助-关于”里面会写清楚当前运行时位数。第二TargetFramework用net6.0-windows或者 .NET Framework 4.7.2 都可以Ivi.Visa 这个库对两者都支持但如果你在工控机上跑老系统建议用 4.7.2避免装额外的 .NET 运行时。配置完成后写一段最基础的代码验证能否枚举到仪器。这一步的意义是提前排查运行时位数和资源地址问题不至于后面写了一大段业务代码才发现连接不上。using Ivi.Visa; using var session GlobalSession.Instance; var resources session.FindResources(?*); foreach (var res in resources) { Console.WriteLine(res); }FindResources(?*)是 VISA 的 Resource Manager 提供的资源枚举方法?*是通配过滤表达式表示列出所有类型资源。你会在输出里看到类似USB0::0x3923::0x7530::MY500010::INSTR这样的地址这就是你的仪器地址。如果这里什么都打印不出来先别急着写业务代码去 NI MAX 里看设备是否在线以及当前进程的位数是否和 NI-VISA 匹配。能列出资源说明底层链路已经通了接下来的代码才有意义。3. 用 C# 写第一个 NI 远程控制程序打开会话、写指令、读数据3.1 一个最小可运行的 VISA C# Demo确认资源列表能出来之后就可以写真正控制仪器的代码了。最常见的演示方式是向仪器发送*IDN?查询指令读回仪器的厂商、型号和序列号。这一步能证明整条命令链路是通的。下面是一个最小但完整的控制程序using Ivi.Visa; using System; using System.Linq; class Program { static void Main(string[] args) { string resource TCPIP0::192.168.1.100::INSTR; using var rm GlobalSession.Instance; using var io rm.Open(resource, AccessModes.Exclusive, 5000) as IMessageBasedSession; if (io null) { Console.WriteLine(无法打开会话); return; } var response io.QueryString(*IDN?\n); Console.WriteLine(仪器返回 response.Trim()); } }再说说这段代码的逻辑。rm.Open是打开 VISA 会话第二个参数AccessModes.Exclusive表示独占模式避免别的进程同时往仪器里写指令第三个参数 5000 是超时时间单位是毫秒。as IMessageBasedSession是必须的转型因为Open返回的是IVisaSession只有实现了消息接口的会话才能进行QueryString这样基于字符串的读写。QueryString(*IDN?\n)干了两件事写入*IDN?指令然后读取仪器返回的数据。注意这里的换行符\n只要仪器说明里没有特别说明VISA 下大部分设备把\n当命令终止符。有些老设备要求\r\n这个需要在 NI MAX 的 VISA 会话里测试确认。超时设置是一个关键参数。Open里的 5000 只是建会话时的超时真正读写时的超时是在会话属性里单独设置的我会在 3.3 节讲。另外using关键字保证了会话和 ResourceManager 的释放顺序先释放io再释放rm顺序错了偶尔会导致进程退出时异常。3.2 实测控制 NI 信号源频率和幅值怎么远程设定*IDN?只是热身真正到业务里你需要的是设频率、设幅度、开输出。这里用一台常见的 NI 信号发生器举例。它的 SCPI 指令通常长这样FREQ 1000000设置频率为 1 MHzVOLT 0.5设置幅度为 0.5 VOUTPut ON打开输出。对应到 C# 代码就是连续三次io.WriteString。public void ConfigureSignalGenerator(IMessageBasedSession io) { io.WriteString(FREQ 1000000\n); io.WriteString(VOLT 0.5\n); io.WriteString(OUTPut ON\n); Console.WriteLine(信号源已配置); }很多人第一次写到这里会踩一个坑WriteString之后仪器没反应但也不报错。原因多半是命令终止符不匹配。VISA 的会话有一个属性叫TerminationCharacter它表示一条命令的结束标志。NI 的信号发生器有的默认支持\n有的却要求\n后面再跟一个额外的结束位。我在实践中习惯的做法是先用*IDN?把会话跑通再看厂商提供的编程手册里的“Command Termination”章节。如果设备要求\r\n就把WriteString里的字符串改成FREQ 1000000\r\n同时把会话的TerminationCharacter设为\n。这两者听起来冲突但实际上一个是发送侧的命令结束符一个是接收侧的数据读终止符不要混在一起。另一个值得注意的点是WriteString和QueryString不要在不理解仪器状态的前提下傻写。比如你刚开机仪器可能处于本地锁定状态面板按键把手动控制权占了这时候发FREQ指令会返回错误。先发一条SYST:REM或者SYSTem:REMote把仪器切到远程模式。这个指令在 Keysight 和 NI 设备上都通用建议在配置流程开头先发一次。3.3 会话超时和缓冲区设置参数说明与默认值的坑VISA 会话里最常被忽视的两个参数是Timeout和读缓冲区大小。默认超时是 1000 毫秒这在很多 NI 设备上不够用。比如设备正处于自校准状态或者你发的是一条需要长时间扫描的测量指令1000 毫秒必然超时然后 VISA 抛出一个IOTimeoutException。我在代码里会这么显式设置io.Timeout 10000; io.SetAttribute(VisaAttribute.ReadBufferSize, 4096);第一行把超时改成 10 秒第二行把读缓冲区改为 4K 字节。为什么默认的 2048 不够因为有些仪器返回波形数据时头部会有一段 ASCII 元信息接着是二进制数据块总长度可能超过 2048。读缓冲区不够时 VISA 不是帮你分段而是直接截断导致数据解析错位。这个坑特别隐蔽因为程序不报错但解析出来的数值是错的。当你需要读取大量波形数据时正确的做法不是把缓冲区无限加大而是使用ReadByteArray加上循环读取或者直接把数据格式设为二进制避免 ASCII 带来的体积膨胀。SCPI 里面有一种二进制传输格式头部是#9000000000这种长度前缀C# 侧解析稍微麻烦一点但能极大缩短传输时间。这个我放在第 6 章专门讲。4. NI 设备与 C# 代码之间的配合SCPI 指令集和示波器/万用表远程测量的落地写法4.1 SCPI 指令是远程控制的“方言”看懂它才能不靠抄代码VISA 是一个管道SCPI 才是仪器真正听得懂的语言。而 SCPI 的指令结构有很强的规律性所有指令按子系统:关键字:关键字的层级组织比如SENSe:VOLTage:DC:RANGe是万用表直流电压量程。NI 的仪器手册里会给出“Command Reference”章节里面的指令按功能模块组织一般来说看懂了CONFigure、MEASure、READ这三类前缀就能解决大部分测量需求。一个常见的新手误区是从网上抄了一段代码直接改仪器型号就上线。实际上 Keysight、NI、Rohde Schwarz 虽然都兼容 SCPI 标准但底层实现细节有差异。比如某型号的 NI 万用表测量直流电压的指令是MEAS:VOLT:DC?而另一型号的采集卡则要求用READ?触发一次内部扫描。抄代码前先确认真实设备对应的指令否则会出现“能连接但读不到数据”这种模糊故障。下面这段代码演示了用 C# 远程控制一台 NI 数字万用表测量直流电压并且同时采集两次读数来做平均值验证。实测过这类代码在产线工位上很常用public static double MeasureDcVoltage(IMessageBasedSession io) { io.WriteString(CONF:VOLT:DC 10\n); // 配置为直流电压测量量程 10V io.WriteString(SAMP:COUN 5\n); // 采样 5 次 var raw io.QueryString(READ?\n); var values raw.Trim().Split(,) .Select(double.Parse).ToArray(); return values.Average(); }在这段代码里CONF:VOLT:DC 10把仪器设置为直流电压测量模式量程 10V。SAMP:COUN 5让设备内部连续采样 5 次。READ?触发一次测量并返回所有采样值返回值是一个逗号分隔的字符串。用 LINQ 的Select转成 double 数组再算平均值能滤掉单次采样的随机抖动。要注意的是NI 万用表在CONF之后不是马上就能READ?有些型号需要等待内部继电器稳定这时一个Thread.Sleep(200)可能比设更长超时更有效。另外量程设小了会进入过载状态返回9.9E37这种溢出值量程设大了精度打折。经验做法是先用自动量程CONF:VOLT:DC不带参数读一次再根据读数手动固定量程。4.2 NI 示波器远程读波形二进制数据传输是唯一靠谱的路示波器是另一个高频远程控制对象。示波器和万用表最大的区别在于数据量大、格式复杂。你不可能用READ?把一整个波形以 ASCII 字符串传回来那会让传输时间爆炸。NI 的示波器如 PXIe-5160它的波形读取流程一般是配置触发设置水平时基发送INIT准备采集然后FETCH:WAV:CH1?获取波形数据。默认返回的可能是 ASCII 数组但在远程控制场景下要明确要求二进制格式。SCPI 里二进制传输的格式一般如下数据以#开头后面第一个数字表示长度字段占几位再后面那几位数字表示数据总字节数之后紧跟着原始二进制数据。例如#9000001234表示长度字段占 9 位数据长度是 1234 字节。C# 里解析时要先用 VISA 读取到#开头解析头部再循环调用ReadByteArray读剩下的部分。public static byte[] ReadWaveformBinary(IMessageBasedSession io, int expectedPoints) { io.WriteString(WAV:FORM BIN\n); // 切换为二进制传输 io.WriteString(WAV:DATA? CH1\n); var header io.ReadString(11); // 读取 #9000000000 头 int countLen int.Parse(header.Substring(1, 1)); int dataLen int.Parse(header.Substring(2, countLen)); byte[] buffer new byte[dataLen]; int offset 0; while (offset dataLen) { int n io.ReadByteArray(buffer, offset, dataLen - offset); offset n; } return buffer; }这段代码里有几个参数值得单独说明。io.ReadString(11)是精确读 11 个字符因为#9000000000恰好是 11 个字节如果长度状态位是 8那头部总长度就是 10。保险做法是先读 1 个字节确认#再读 1 个字节确认位数最后按位数读取长度文本。ReadByteArray的循环读取是因为 VISA 底层不保证一次Read把请求的数据全部返回你需要循环直到填满缓冲区。这里最容易翻车的点是WAV:DATA? CH1后面不要加换行符因为查询命令本身以\n结束加了反而可能触发一些仪器的格式错误。4.3 NI 设备控制里“软面板”和“程序控制”权限冲突的经典处理NI 设备往往自带软面板或者你装了 NI Measurement Automation Explorer 之后会习惯性地在里面手动操作设备验证。这在调试时很方便但上线时问题就来了如果软面板或 NI MAX 的测试面板还开着会话你的 C# 程序再去打开同一台仪器就会得到一个资源冲突异常或者更糟——程序不发错误但指令不执行。原因是 NI-VISA 的资源默认允许共享但很多 NI 设备的固件不允许两个 session 同时对仪器发指令尤其是在测量指令执行过程中。C# 里直接的表现是Open成功但QueryString一直超时。解决方式分两步。第一步C# 代码打开会话时明确使用AccessModes.Exclusive。第二步在程序中设置一个开关启用时自动检查 NI MAX 中的连接状态并提示操作员先关闭 NI MAX 的测试面板。做产线交付时我一般会建议客户把 NI MAX 的“自检”作为离线维护流程的一部分而不是调试完继续挂着。还有一个更容易忽略的场景如果你用 NIMAX 里的“软面板”打开过设备然后什么都没点直接关闭某些 NI 驱动并不会立刻释放底层句柄Windows 会把它挂在后台。这时 C# 程序打开会成功但发送第一条指令就会卡住。遇到这种情况解决方法是去 Windows 服务里重启一下NI-VISA相关的服务进程。我不会说具体服务名因为不同版本有差异但路径在“服务”里找带NI前缀的就对了重启服务后再跑程序大概率会恢复。5. VISA C# 远程控制避坑5 个常见的翻车现场与排查路径5.1 AccessViolationException 崩了整个程序现象C# 程序调用 VISA 函数后有时会直接抛出一个AccessViolationException而这时候代码根本没操作指针纯托管代码也会崩。原因这是 NI-VISA 内部非托管代码的问题常见诱因是传入的缓冲区太小或者内存被其他线程释放。解决把 NIMAX 和 NI-VISA 运行时都卸载重装一次同时把工程的“Prefer 32-bit”关掉保持 in x64 下运行实测能解决掉八成此类崩溃。另外要注意Ivi.Visa的异步回调不要传一个 lambda 就直接跑回调内部不要立即释放会话。5.2 超时时间设太短导致的假死现象现象命令发送不频繁但每隔几分钟程序就会卡死在一个QueryString上。原因默认超时 1000 毫秒遇到 NI 信号发生器切换量程需要 600 毫秒以上加上命令排队时间导致 VISA 抛出超时异常。不是程序假死而是在异常被吞掉了。解决除了设置io.Timeout 10000外还应该在每次超时之后主动调用io.Clear()去清理仪器内部缓冲区。如果不清理仪器会认为上一条命令还在等待执行后续所有命令都会排队延迟越来越严重。5.3 字符串编码错误中文注释没乱码但仪器指令乱码现象向仪器发送的 SCPI 指令看起来都对但仪器返回SYNTAX ERROR。原因C# 的WriteString默认使用 ASCII 编码而 NI-VISA 在某些版本会使用系统 ANSI 代码页。当指令里含°符号例如某些温度单位时编码不一致会导致指令解析失败。解决所有 SCPI 指令强制写成纯 ASCII 字符集不要使用任何 Unicode 字符。如果是频率单位写成MHZ而非MHz后面的小写字母其实没问题但遇到带°的单位时查询返回的字符串两端要手动做 Trim。一般我不建议在仪器指令里包含除 ASCII 外的任何字符。5.4 首次打开会话正常第二次打开永远失败现象程序第一次运行完美第二次运行Open时直接抛出一个非 0 的状态码。原因上一次程序的会话没有被正确释放。资源管理器的释放顺序有问题或者是程序崩溃导致内核会话没关闭。解决除了用using声明外建议在FormClosing事件里加一行全局代码关闭所有静态预留的 Session 实例。另外可以调试时观察 NI MAX 里的“VISA 会话数”如果每次运行后都 1说明释放顺序有误。5.5 UI 线程卡死与后台采集的矛盾现象在 WinForms 里放一个“开始采集”按钮点击后界面卡住最后提示Not Responding。原因采集逻辑在 UI 线程执行VISA 的读取是阻塞式的。解决所有 VISA 操作必须放到Task.Run或者BackgroundWorker里。同时不要在每个循环里都Invoke更新 UI可以做数据批量打包后定时刷新。这块代码不贴了就是最普通的异步模式但绝大多数第一次做仪器采集的人都会在这里翻车提前预防能省很多调试时间。6. 进阶用法事件回调、波形流式读取和与 TDMS 落盘结合的落地技巧走到这一步单条命令控制仪器已经跑通了下一步通常是把这套能力变成一个稳定运行的采集程序。这里分享三个我认为最值得放的进阶技巧都是真实工位上验证过的。第一个是事件回调。NI-VISA 支持在收到仪器 SRQ 中断时触发回调而不是轮询状态位。SCPI 里很多仪器有*OPC操作完成位。C# 里可以通过RegisterEventCallback订阅VisaEventType.ServiceRequest。这适合用在“仪器测量完成再取数”的场景能把 CPU 占用降下来也能让响应更及时。但要注意回调函数里不能做阻塞操作只能发信号量或者置个标志位数据读取仍然要放到主工作线程里。第二个是波形流式读取。前面 4.2 节里的二级制读取如果波形点数很大比如 1M 个点一次读回来会让内存压力很大。更稳妥的方式是打开新式VISA 属性配置的块模式或者按块读取。但如何分块取决于仪器本身是否支持WAV:STREAM指令NI 的高端示波器是支持的。如果你用的是一台普通的 NI 设备从实用角度讲把波形数据解析成short[]之后立刻转成物理量不要让byte[]在内存里驻留太久。第三是和 TDMS 文件格式结合。NI 的测量数据落盘推荐使用 TDMSC# 侧开源的TDMS Reader可以直接把采集的内存数据按通道写入文件。这样做的最大收益是数据文件既能被 NI MAX 直接打开回放又能在 C# 程序里快速检索某一段波形。我的习惯是远程控制程序里先保证数据流不断交换机记录文件按小时切分文件名带设备名和时间戳。这样万一现场设备有问题回放定位非常快。说到底VISA C# 这套组合价值不在于某个 API 用得多炫而在于它让你有了一条不用切换语言就能覆盖 NI 全系仪器的路径。我做了几年的产线自动化最大的一个教训是永远不要把采集逻辑写死在页面事件里先把会话层、指令层、业务层分开后面加设备、加流程才不用推倒重来。也希望这篇笔记里的实现细节和踩坑记录能帮到你让你少走几个我走过的弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进