ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++与NI-VISA驱动数字万用表:从SCPI指令到产线采集实战

C++与NI-VISA驱动数字万用表:从SCPI指令到产线采集实战 简介面向需要以C控制NINational Instruments数字万用表板卡的开发者dmm.zip提供了一份可直接调用的DMM驱动程序。压缩包内仅含1个dmm.cpp源文件大小约2KB代码虽精简但覆盖了设备初始化、测量参数配置、数据采集、错误处理与设备关闭等关键操作可帮助理解驱动与硬件交互的基本流程。已有283人学习下载适合电子测量、自动化测试及仪器控制方向的入门与进阶参考。借助这份驱动接口开发者可在自己的应用程序中快速接入NI万用表完成电压、电流、电阻等参数的自动化读取同时还能学习C驱动开发中常见的API调用模式与异常处理思路为构建更复杂的测试系统打下基础。1. 拧开万用表手动读数再抄 Excel这在产线里就是瓶颈dmm.zip 这个标题指向的正是那类 DMM 驱动包——数字万用表的底层驱动和示例代码核心目标就一个让 C 程序替人眼去读表几十毫秒读一次自动判断合不合格。做自动化测试的工程师对这套东西都有共同认知NI-VISA 做桥梁SCPI 指令做语言万用表从黑匣子变成可控的采样节点。它适合正在 LabVIEW 和 C 之间做上位机选型的人也适合刚接手产线仪器集成、要把数字万用表接进自己程序的工程师。这篇笔记按我实际做过的路径来讲先把选型逻辑说清再给最小可编译的 C 示例最后把连不上、读数飘、跑一半卡死这几类现场拆开。2. 为什么选 NI-VISA 而不是直接写串口DMM 驱动的选型逻辑2.1 NI-VISA 不只是一套驱动是给万用表开的“统一插座”数字万用表常见的物理接口有 USB、RS-232、LAN、GPIB 四类。直接写串口时你要自己处理波特率、停止位、流控、换行符差异还要面对每一家厂商自定义的二进制帧格式GPIB 更麻烦总线时序、寻址、EOI 结束信号都要自己管。NI-VISA 这一层把 USB、GPIB、串口、LAN 全部收敛成一个 API上层代码只面对一个资源字符串。比如同样是打开设备GPIB 表写GPIB0::1::INSTRUSB 表写USB0::0x2A8D::0x1301::MY58002237::INSTR串口表写ASRL1::INSTR。你不需要关心这条字符串背后是 USB 批量传输还是 GPIB 总线仲裁VISA 替你做了。这也是我推荐先选 NI-VISA 的原因它不是某个万用表厂商的私有协议而是 IVI 基金会制定的标准接口NI-VISA 只是最常见的实现。Keysight 也提供自己的 VISA 库但生态和示例数量都不如 NI-VISA。LabVIEW 默认带它Python 的 pyvisa 默认后端也是它换语言时心智模型完全一致。VISA 提供的核心操作只有四个打开会话、写指令、读回复、关会话再加一个设置超时的属性。它不翻译 SCPI只做可靠传输。把 VISA 理解成“插座”SCPI 才是“普通话”这两层分工要清楚排错时才不会混在一起。2.2 SCPI 不是协议是万用表听得懂的普通话SCPI 全称 Standard Commands for Programmable Instruments基于 IEEE 488.2 定义。它是一套 ASCII 文本指令你在串口助手里手动敲*IDN?表也会老老实实回一行设备信息。这种可读性让调试门槛低了一大截——程序不通时先拿串口助手或 VISA 交互工具直接敲指令马上能判断是链路问题还是指令拼写问题。常用指令其实就十几条核心是下面这一组指令作用典型回复*IDN?查询设备身份RIGOL TECHNOLOGIES,DM3058,...:CONF:VOLT:DC配置直流电压测量无:READ?触发一次测量并返回结果1.23456789E-02:MEAS:VOLT:DC?配置触发读取一条完成1.23456789E-02:FETCh?读最近一次缓存结果1.23456789E-02:SENS:VOLT:DC:RANG 10固定量程到 10V无:SENS:VOLT:DC:NPLC 1设置积分时间电源线周期数无注意几个细节指令不区分大小写层级之间用冒号查询指令必须带问号多通道仪器用1、2这种参数选区。回复一般是科学计数法 ASCII 字符串1.23456789E-02直接按double解析就行单位不会出现在数字里不需要处理。我的习惯是代码里统一用小写指令手册里抄来的大写指令格式化一遍再贴进去避免大小写混着眼花。2.3 NI-VISA、NI-DMM、厂商 DLL 三条路线怎么选调一块万用表常见做法有三条路路线优点缺点适合场景NI-VISA SCPI通用、示例多、换表只改资源串性能一般单次读写约几毫秒大多数产线测试、数据采集NI-DMM 驱动自带量程/精度/校准高层 APILabVIEW 集成好绑定 NI 自家 DAC 卡或特定表通用性差需要高级触发同步的测试系统厂商自定义 DLL速度最快可拿到私有测量状态换品牌就重写文档可能不全高速分选、批量一致性要求高的产线我一般会按“先通用后特化”的顺序方案设计阶段先用 NI-VISA 把流程跑通SCPI 控制量程、触发、读取如果后面发现单次读数的吞吐不够再把底层实现替换成厂商 DLL上层业务代码不感知。这样换来换去只动一个适配层文件血泪经验告诉我千万别把 VISA 调用散落在业务代码里。3. 用 C 在 VSCode 跑通第一行 DMM 读取最小工程与编译参数3.1 先装 NI-VISA再用 NI MAX 把资源名挖出来Windows 上先去 NI 官网下载 NI-VISA 运行时安装包。要注意区分 Runtime 版和 Development 版只下 Runtime 的话头文件visa.h和导入库visa32.lib是没有的后面编译会卡在找不到头文件。直接下载完整版安装包即可里面包含头文件、库和 NI MAX 控制台。装完后在开始菜单打开 NI MAX左侧“设备和接口”里能看到已连接的仪器。选中设备右键属性VISA 资源名就是 C 程序里要用的字符串。先用 NI MAX 自带的 VISA 交互工具打开会话敲一条*IDN?确认能收到回复再去写代码。这一步能筛掉一半问题。Linux 下我常用 Ubuntu在官网下载对应的 deb 包安装装完还得处理设备权限否则 USB 表打不开具体做法放第 5 章避坑里展开。Windows 平台相对省心插上表装完驱动在 NI MAX 里就能看到。这里强调一点NI-VISA 是 32/64 位两套库同时装的程序编译时位数必须和库位数对齐否则链接报错很隐晦。3.2 最小 C 工程查询设备 ID 并读回直流电压先给一个能直接编译运行的完整程序。这个文件命名dmm_read.cpp逻辑只有四步打开默认资源管理器、打开设备、发指令读电压、关闭。#include visa.h #include cstdio int main() { ViSession rm VI_NULL; // 资源管理器会话 ViSession dmm VI_NULL; // 仪器会话 ViStatus st; // 1. 打开默认资源管理器 st viOpenDefaultRM(rm); if (st VI_SUCCESS) { printf(viOpenDefaultRM failed: 0x%x\n, st); return -1; } // 2. 打开仪器会话。资源名从 NI MAX 里复制不同机器不要照搬 st viOpen(rm, USB0::0x2A8D::0x1301::MY58002237::INSTR, VI_NORM, VI_NORM, dmm); if (st VI_SUCCESS) { printf(viOpen failed: 0x%x\n, st); viClose(rm); return -1; } // 3. 先查设备身份确认链路通 viPrintf(dmm, *IDN?\n); char idn[256] {0}; viScanf(dmm, %t, idn); printf(device: %s\n, idn); // 4. 一条命令完成配置触发读取直流电压 viPrintf(dmm, :MEAS:VOLT:DC?\n); double voltage 0.0; viScanf(dmm, %lf, voltage); printf(voltage: %.6f V\n, voltage); viClose(dmm); viClose(rm); return 0; }逻辑说明viOpen的第二个参数是资源名VI_NORM表示默认超时和默认打开模式步骤 3 的*IDN?是万能探路指令能回来说明物理链路、驱动、会话三层都正常步骤 4 里:MEAS:VOLT:DC?是“懒人指令”它把配置、触发、测量、读取串成一步适合第一版验证后面做连续采样时要拆开用:CONF加:READ?。参数说明viScanf的格式串里%t是 VISA 特有的读一个以换行为终结符的字符串%lf直接解析回复里的科学计数法 ASCII 为 double。viPrintf的指令串末尾必须有\n很多表把换行当作命令结束符漏了会一直等超时。返回码检查这里只做了简单判断实际项目建议再调viStatusDesc拿错误描述方便打日志。编译方式我推荐 VSCode CMake MSVC 工具链最省事。先给 CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(dmm_read CXX) # NI-VISA 安装路径按你机器上的实际路径调整 set(VISA_ROOT C:/Program Files/IVI Foundation/VISA/Win64) find_path(VISA_INCLUDE_DIR visa.h PATHS ${VISA_ROOT}/Include) find_library(VISA_LIB visa32 PATHS ${VISA_ROOT}/Lib/x64) add_executable(dmm_read dmm_read.cpp) target_include_directories(dmm_read PRIVATE ${VISA_INCLUDE_DIR}) target_link_libraries(dmm_read ${VISA_LIB})逻辑说明find_library找的是导入库visa32.lib。注意一个历史坑64 位系统上 NI-VISA 的导入库文件名仍然叫visa32.lib它其实是 64 位的库文件在Lib/x64目录下。找错目录链到 x86 版本链接能过但运行时直接崩玄学得很。编译时建议直接打开“x64 Native Tools Command Prompt for VS”在里面执行cmake和build确保整个工具链都是 x64。如果你用 MinGW 的 g 去链 MSVC 的visa32.lib多半会报一堆 undefined reference这是因为 MSVC 的导入库格式 g 不认。能用是运气不能用很正常别在这上面浪费时间。3.3 第一次编译失败的排查顺序新手编译这块最常见的三个报错按出现频率排fatal error: visa.h: No such file or directory——头文件路径没配。检查includePath是否指向VISA_ROOT/Include另外确认装的是完整版而非 Runtime。cannot open input file visa32.lib——库路径错了。确认Lib/x64目录存在且 CMake 里拼的路径与实际安装版本一致。unresolved external symbol viOpenDefaultRM——链接没生效。先确认target_link_libraries写到了可执行目标上再确认编译的是 x64 配置。三个都排完还不行回到 NI MAX 手动敲*IDN?如果 NI MAX 里也失败那就是驱动安装或设备本身的问题不是代码问题。我自己的开发环境是 VSCode CMake MSVCtasks.json 里直接调cmake --build没有额外脚本。头文件路径第一次配好后基本不用再动真正要经常改的是资源名——每次换机器、换表第一件事就是改资源字符串所以我建议直接把资源名提成命令行参数后面讲。4. 从 dmm.zip 到连续采集把单次读数改造成产线可用的上位机4.1 dmm.zip 这类包里一般躺着四类东西拿到一个以 dmm.zip 命名的压缩包内容虽然各家不同但按我开了几十个这种包的经验常见是四类内容驱动安装包、示例代码、SCPI 编程手册、固件升级工具。驱动安装包不用多说示例代码多数是 C 或 C# 写的控制台程序价值在于告诉你厂商推荐的指令序列和初始化流程SCPI 编程手册是最容易被忽略的但恰恰最重要——没有它指令拼写全靠猜猜错就超时。固件工具一般用不上除非表有已知 bug。判断一个包全不全先找三个东西安装程序、带 main 函数或 Program.cs 的源码、PDF 格式的手册。缺了手册就要去厂商官网按型号搜 Programmer Manual。有些包是二手转存的文档可能被精简过这种情况更要谨慎示例代码里的资源名、量程参数未必适合你的型号照着抄之前先确认表型一致。提示包里示例代码的编译器和库版本可能很老比如 VC6 时代写的工程文件。不需要照搬它的工程结构只抄指令序列和初始化流程自己重新组织工程比强行移植旧工程省得多。4.2 把单次读取改造成连续采样均值滤波与量程固化第 3 章的懒人指令:MEAS:VOLT:DC?适合验证链路但做连续采集时会暴露两个问题自动量程切换很慢几十毫秒到几百毫秒不等导致每次读数延迟不稳定读数本身带噪声单次测量值不好直接用。所以产线上连续采样我一般会这么做#include visa.h // 读取 n 次平均值。range 固定量程比如 10 表示 10V double readAverage(ViSession dmm, int n, int range) { viPrintf(dmm, :SENS:VOLT:DC:RANG %d\n, range); // NPLC150Hz 电网下一个电源线周期约 20ms积分时间越长噪声越小 viPrintf(dmm, :SENS:VOLT:DC:NPLC 1\n); double sum 0.0; for (int i 0; i n; i) { viPrintf(dmm, :READ?\n); double v 0.0; viScanf(dmm, %lf, v); sum v; viDelay(dmm, 10); // 毫秒给仪器内部触发留时间 } return sum / n; }逻辑说明先把量程固定为 10V避免自动量程每次测量前先做一次探测读数延迟会小很多。NPLC 是电源线周期数设 1 表示积分一个电网周期50Hz 下约 20ms这个值越大噪声抑制越好但单次测量越慢。viDelay的单位是毫秒这里给 10ms 是保守值保证上一次测量的数据总线归位后再触发下一次。参数怎么调读数噪声大就加 NPLC 到 2 或 5或者加大n测量速率要求高就把 NPLC 降到 0.1省下的时间换噪声没有白拿的好处。均值滤波对白噪声有效对突变信号会平滑掉峰值如果测量对象是开关电源纹波这种带脉冲的光靠均值不够要结合最大值采样这就超出 DMM 本身的带宽了——先确认表的能力边界别拿 10Hz 采样率的万用表去测纹波。产线上还有一种常见需求是“数字放大”传感器输出的是毫伏级信号表读到的是3.21E-04程序里乘上校准系数再显示成工程单位。这种放大要在均值之后做并且保留足够的有效位先转换后放大会在低量程引入不必要的量化损失。4.3 别把采集放进 UI 线程一个实测过的生产者消费者模型单次读取只要几十毫秒但如果放在界面的按钮事件里直接调点一次卡一次采样率高一点界面就完全没响应。我的做法是采集线程只管读数和判级UI 线程定时取最新值刷新。#include queue #include mutex #include thread #include atomic std::queuedouble g_queue; std::mutex g_mtx; std::atomicbool g_running{true}; void collectThread(ViSession dmm) { while (g_running) { double v readAverage(dmm, 10, 10); std::lock_guardstd::mutex lock(g_mtx); if (g_queue.size() 200) { g_queue.pop(); // 队列塞满就丢最旧的数据 } g_queue.push(v); } }逻辑说明采集线程里只做读数和入队判断逻辑可以放在读取之后、入队之前UI 线程每 200ms 从队列尾部取最新值刷新中间的数据被丢弃也没关系显示本来就只看当前值。队列加个上限防止采集比消费快时内存疯涨。参数说明队列上限 200 按每读一次约 100ms 估算约等于 20 秒的缓冲量足够 UI 线程从容取数。如果后面要对接 TDEngine 这类时序库做数据入库我在实际项目里的做法是另起一个入库线程消费同一份数据不占用采集线程时间。VISA 会话本身不是线程安全的同一个ViSession千万别被两个线程同时读写——入队前完成所有 VISA 操作是铁律不然程序会以随机时间点崩给你看。5. DMM 驱动避坑连不上、读不准、跑到一半卡死的三类现场5.1 现象NI MAX 里能看到设备C 里 viOpen 就是返回负数这个比想象中常见得多。原因通常有两个资源名是复制别人代码里的人家机器的 USB 接口编号和你不同或者同一块表在 Windows 上换了个 USB 口资源名尾部数字变了。VISA 资源名不是固定的“设备名”它包含物理接口序号换口就变。解决办法是程序里自己枚举不要硬编码ViFindList list; ViUInt32 num 0; char desc[256] {0}; // 枚举所有 USB 接口的 INSTR 类设备 viFindRsrc(rm, USB?::*::INSTR, list, num, desc); for (unsigned i 0; i num; i) { printf(device: %s\n, desc); ViSession tmp; if (viOpen(rm, desc, VI_NORM, VI_NORM, tmp) 0) { viPrintf(tmp, *IDN?\n); char idn[128] {0}; viScanf(tmp, %t, idn); if (strstr(idn, DM3058)) { // 通过 *IDN? 回复过滤出目标设备 dmm tmp; break; } viClose(tmp); } if (i num - 1) viFindNext(list, desc); } viClose(list);现象终结方案把这段枚举逻辑封装成openByModel(const char* model)程序启动时扫描一遍找到匹配型号就返回会话句柄。产线上设备多、存在拔插乱序时这个方法比手抄资源名可靠得多。5.2 现象编译过了运行时报 0xc000007b 应用错误这个报错几乎实名指向位数不匹配。原因你的程序是 x86 编译的却链到了 x64 的visa32.lib或者反过来。NI-VISA 安装时 32 位和 64 位库都在但 CMake 里路径写错一位就串了。另一个可能原因是系统的 Visual C Redistributable 没装全NI-VISA 运行时依赖它。解决分两步查先用 Dependency Walker 或直接在 x64 命令行下重新编译一遍把整个工具链统一到 x64再确认系统装全了 VC 2015-2022 Redistributable x64。我遇到过几次死活报错最后发现是开发机上装过精简版运行库卸载重装官方完整版就好。判断依据很简单在 NI MAX 里能通信说明驱动本身是好的问题出在你的进程加载链上。5.3 现象程序跑起来第一次读数永远是 0 或上一个量程的旧值这是 DMM 驱动开发最典型的“假成功”链路通了、指令发了、回复也收到了但值不对。原因是仪器从上电到测量就绪有时间差加上自动量程在第一次测量前要先探测信号你的:MEAS?抢在它 ready 之前执行了。SCPI 本身没有类似 TCP 握手那样可靠的完成语义你发出指令后立刻读读到的是空或缓存。解决按顺序做三件事第一viOpen之后先发*RST复位再发*IDN?等待回复这一来一回就给了几百毫秒第二量程固定不用自动量程:SENS:VOLT:DC:RANG 10在业务开始前设一次第三正式测量前先读 3 次丢掉的预热循环值稳定后再进入采集主循环。这些步骤看着笨实际上省去了和仪器状态机做精细交互的时间。5.4 现象连续采集跑着跑着卡死把超时改到 10 秒也没用一开始以为是仪器响应慢把VI_ATTR_TMO_VALUE调大结果照样卡。真实原因通常是指令回复的换行符没消费干净。很多表的回复带\r\nviScanf按%lf读数字后结尾的换行残留在缓冲区里下一轮viPrintf发指令时这些残留被仪器当作前置垃圾触发解析错误或直接忽略。解决给当前会话的 IO 缓冲区加一个“使用前清空”的习惯动作。读完整条回复后再补一次viScanf吃行尾或者在每次发送前把缓冲区里的陈旧数据清掉。// 发送前清空输入缓冲区避免上一轮残留换行符干扰 viSetAttribute(dmm, VI_ATTR_IO_IN_BUF_SIZE, 0); viClear(dmm); // 有些表支持 *CLS 清状态效果类似说明viClear对 USB 表通常执行一次设备清空对 GPIB 表等效于总线 clear*CLS是 SCPI 层的状态寄存器清除不一定会清 IO 缓冲两者配合用才完整。这套组合是我从一次连续跑 4 小时在凌晨三点卡死的现场里总结出来的之后批量读写前都先清一次再没犯过。5.5 现象Ubuntu 下装完 NI-VISAUSB 设备打开报错Linux 上装 NI-VISA 后USB 设备默认没有访问权限运行时会报缺少权限或设备找不到。原因是 udev 规则没装。解决是把 NI-VISA 自带的配置工具跑一遍或者手动写 udev 规则# 查看设备是否被系统识别 lsusb | grep -i 2A8D # 手动添加 udev 规则Vendor ID 换成你表的实际值 echo SUBSYSTEMusb, ATTR{idVendor}2a8d, MODE0666 | sudo tee /etc/udev/rules.d/50-dmm.rules sudo udevadm control --reload-rules sudo udevadm trigger现象终结方案Ubuntu 下还有一个 32 位程序在 64 位系统上缺 32 位库的问题如果你的上位机必须 32 位编译要额外装gcc-multilib和对应的 32 位 VISA 库。Linux 本身不是 DMM 上位机的主流平台但一旦涉及嵌入式工控机Linux 反而是更常见的选择提前把权限问题解决能省很多事。6. 验证数据可信交叉校验与把驱动做成产线命令行工具读回来的电压没人信这比读不到更致命。我的验证套路是两步走先用第二块表并联测同一个信号源比较两块表读数差再用精密电压源做台阶测试从 1V 到 10V 每档记录偏差画一条误差曲线。并联测量要注意负载效应第二块表的输入阻抗会拉低信号选高输入阻抗的表或者用缓冲输出。台阶测试每个点采 20 次算平均值和标准差。标准差反映重复性平均值和标准源的偏差反映准确度。如果标准差在仪表说明书标称精度的 3 倍以内说明驱动和量程配置没额外引入噪声如果偏差超差先怀疑量程设大了把量程调低一档重测。做完验证我会把程序重构成一个命令行工具思考的是怎么让产线脚本用它dmm_read.exe -p USB0::0x2A8D::0x1301::MY58002237::INSTR -r 10 -n 20 -t 3000-p指定资源名-r固定量程-n采集次数-t超时毫秒。退出码 0 表示测量值在上下限内1 表示超限2 表示设备故障——这样 LabVIEW、Python、甚至 shell 脚本都能直接消费这个工具的输出不必重写驱动层。这台工具机的源码和编译脚本我会和 dmm.zip 里那份示例代码放一起作为团队后续换表时的起点。自己这些年调过的表从台式万用表到高速数据采集卡都有最大的教训就是别急着写业务先把“读一次值”这件事用最笨的方法验证 100 遍确保数字可信再往上盖楼。希望这些做法对你有帮助动手调表顺利。本文还有配套的精品资源点击获取
返回列表