简介:这是一份面向仪器控制与自动化测试开发者的C语言源码示例包,基于NI-VISA(Virtual Instrument Software Architecture)标准接口,演示如何通过统一API对GPIB、USB、TCP/IP、串口及PXI等总线设备进行远程读写与控制。包内共56个文件,以18个.c源文件、18个vcproj工程文件和18个dsp工程文件为主,另含sln与dsw解决方案文件,整体约93KB,覆盖VXI-VME寄存器访问、GPIB异步SRQ与等待触发、USB TMC读写、TCPIP通信、串口读写及资源查找等典型场景,工程可直接在Visual Studio与旧版MSVC中打开编译。源码围绕viOpenDefaultResource、viWrite、viRead等核心函数展开,并包含错误处理与设备命令构造的实践思路,适合希望掌握VISA会话管理、多接口通信与仪器远程控制的开发者学习改编。目前已有190人学习下载,可作为仪器驱动开发与测试自动化的入门参考。
1. 从一份 C 源码看 VISA 远程控制仪器的真实门槛
很多人第一次接触visa_c_demo_instrument_c_NI_remotecontrol_VISAC#_源码这类工程,是在实验室里被一台程控电源或数字万用表逼出来的:手动按键测几十组数据,抄表抄到怀疑人生,于是想用 C 语言写个程序,通过 NI-VISA 把仪器远程管起来。这个标题拆开看,核心就是三件事——用 C 语言、基于 NI-VISA 运行时、对仪器做远程控制,而VISAC#大概率是作者把 VISA 和 C 语言混写在一起的命名习惯。它解决的是「上位机自动读写仪器」这个刚需,适合做测试测量、产线老化、数据采集的工程师,也适合刚学完 C 语言想找个真实项目练手的人。真正卡住新手的从来不是 C 语法,而是 VISA 资源名怎么写、超时怎么设、仪器回了+1.234E-03这种科学计数法怎么解析——这些才是这份源码背后值得讲透的东西。
2. NI-VISA 在 C 里的运行模型:句柄、会话与超时到底怎么串起来
2.1 为什么不是直接开串口,而要先过一层 VISA
裸写串口或 TCP 当然能通,但每换一台仪器、每换一种总线(USB、GPIB、LAN、串口),你就得重写一遍底层。NI-VISA 的价值在于把「找设备、开会话、读写、关会话」抽象成一套统一 API,C 语言里就是viOpenDefaultRM、viOpen、viWrite、viRead、viClose这几个函数。你写的业务代码只认「资源名」这一个字符串,背后是 USB 还是 LAN 由 VISA 去翻译。这也是为什么标题里强调 NI 而不是别的实现——NI-VISA 是这套抽象在 Windows 和 Linux 上最通用的运行时,LabVIEW、C、C#、Python 都能挂在它上面。
理解这一点很关键:你的 C 程序本身不碰硬件,它只是向 VISA 运行时发指令。所以环境装不对,代码写得再漂亮也是VI_ERROR_RSRC_NFOUND。常见做法是先装 NI-VISA 运行时,再用NI MAX(Measurement & Automation Explorer)确认仪器能被识别、能发*IDN?拿到回应,最后才动 C 代码。跳过 MAX 直接写代码,是新手翻车率最高的一步。
2.2 资源名:一行字符串决定你能不能连上
资源名(Resource Name)是 VISA 编程里最玄学也最要命的东西。它长这样:
USB0::0x2A8D::0x1301::MY59001234::0::INSTR TCPIP0::192.168.1.100::inst0::INSTR GPIB0::22::INSTR ASRL3::INSTR每一段都有含义:USB0是接口类型,0x2A8D是厂商 ID,0x1301是产品 ID,MY59001234是序列号,INSTR表示这是仪器类资源。LAN 仪器用 IP 加inst0,串口用ASRL加 COM 口号。写错任何一段,viOpen就返回负值。
我一般不会手敲这串东西,而是在 NI MAX 里右键仪器选「VISA Test Panel」,或者直接复制 MAX 里显示的完整资源名。手敲序列号少一位、把0和O搞混,是血泪经验里排第一的坑。下面这段最小代码就是拿资源名去开会话:
#include <stdio.h> #include <string.h> #include <visa.h> int main(void) { ViSession rm = VI_NULL; /* 默认资源管理器会话 */ ViSession inst = VI_NULL; /* 仪器会话 */ ViStatus status; ViUInt32 retCount; char idn[256] = {0}; /* 1. 打开默认资源管理器,整个进程只需要一次 */ status = viOpenDefaultRM(&rm); if (status < VI_SUCCESS) { printf("open RM failed: 0x%08X\n", status); return -1; } /* 2. 用资源名打开仪器会话,VI_NULL 表示不指定访问模式 */ status = viOpen(rm, "USB0::0x2A8D::0x1301::MY59001234::0::INSTR", VI_NULL, VI_NULL, &inst); if (status < VI_SUCCESS) { printf("open instrument failed: 0x%08X\n", status); viClose(rm); return -1; } /* 3. 设置超时,单位毫秒,2000 表示 2 秒没回应就报错 */ viSetAttribute(inst, VI_ATTR_TMO_VALUE, 2000); /* 4. 发 *IDN? 查询,注意结尾必须带 \n */ status = viWrite(inst, (ViBuf)"*IDN?\n", 6, &retCount); if (status < VI_SUCCESS) { printf("write failed: 0x%08X\n", status); viClose(inst); viClose(rm); return -1; } /* 5. 读回应,retCount 是实际读到的字节数 */ status = viRead(inst, (ViBuf)idn, sizeof(idn) - 1, &retCount); if (status < VI_SUCCESS) { printf("read failed: 0x%08X\n", status); } else { idn[retCount] = '\0'; printf("IDN: %s\n", idn); } /* 6. 先关仪器会话,再关资源管理器,顺序不能反 */ viClose(inst); viClose(rm); return 0; }逻辑说明:viOpenDefaultRM拿到的是「资源管理器」,它是所有会话的父节点,一个进程开一次就够。viOpen才是真正连仪器。viSetAttribute设超时,这一步很多人漏掉,默认超时可能很长,仪器掉线时程序会卡死。viWrite的第三个参数是字节数,*IDN?\n是 6 个字节,写错长度会导致命令发不全。viRead之后一定要手动补\0,因为 VISA 不会帮你终止字符串。
参数说明:VI_ATTR_TMO_VALUE单位是毫秒,测试测量场景一般设 2000 到 5000;viRead的缓冲区大小要留足,仪器回一串带时间戳的数据可能上百字节,缓冲区给小了会截断。关闭顺序必须是先inst后rm,反过来会让资源管理器提前失效。
2.3 编译链接:Linux 和 Windows 的差异
在 Linux(比如 Ubuntu)上,装完 NI-VISA 后头文件通常在/usr/include,库在/usr/lib,编译命令大致是:
gcc visa_demo.c -o visa_demo -lvisa -lpthread-lvisa是链接 VISA 库,-lpthread是因为 VISA 内部用到线程。如果报cannot find -lvisa,说明运行时没装或路径不对,用find / -name "libvisa*"定位一下。Windows 上用 MSVC 或 MinGW,需要把 NI-VISA 的include和lib目录加进工程,链接visa32.lib。这里有个常见误解:以为装了 NI-VISA 就自动有开发头文件,其实运行时和开发库是两回事,缺头文件就报visa.h: No such file。
3. 把远程控制写扎实:读写、查询与数据解析的完整链路
3.1 命令与查询:别把设置和读取混在一起
仪器命令分两类:设置类(如VOLT 5.0)和查询类(如VOLT?)。设置类发完就完事,查询类发完必须读回应,否则下一次读会拿到上一次的残留数据,这就是典型的「读到的值总是慢一拍」。我一般会封装两个函数,一个只管写,一个写加读:
/* 只写,用于设置类命令 */ int inst_write(ViSession inst, const char *cmd) { ViUInt32 n; char buf[128]; /* 统一补换行,避免调用方忘记 */ snprintf(buf, sizeof(buf), "%s\n", cmd); return viWrite(inst, (ViBuf)buf, (ViUInt32)strlen(buf), &n); } /* 写加读,用于查询类命令,返回解析后的 double */ int inst_query_double(ViSession inst, const char *cmd, double *out) { ViUInt32 n; char buf[256] = {0}; char wbuf[128]; snprintf(wbuf, sizeof(wbuf), "%s\n", cmd); if (viWrite(inst, (ViBuf)wbuf, (ViUInt32)strlen(wbuf), &n) < VI_SUCCESS) return -1; if (viRead(inst, (ViBuf)buf, sizeof(buf) - 1, &n) < VI_SUCCESS) return -1; buf[n] = '\0'; /* 仪器常回 "+1.234E-03\n",strtod 能直接吃科学计数法 */ *out = strtod(buf, NULL); return 0; }逻辑说明:inst_write里统一补\n,是因为不同仪器对行结束符要求不同,但绝大多数接受\n。inst_query_double用strtod而不是atof,因为strtod能处理前导+号和科学计数法,还能通过第二个参数判断是否解析成功。参数说明:缓冲区 256 字节对单值查询足够,但如果仪器一次回一整块波形数据,就得动态分配或者循环读,不能指望一次viRead读完。
3.2 超时、重试与错误码:让程序别一崩到底
VISA 的错误码是负值,常见的有VI_ERROR_TMO(超时)、VI_ERROR_RSRC_NFOUND(找不到资源)、VI_ERROR_IO(读写错误)。直接if (status < 0) exit(1)在生产环境里是灾难,因为一次偶发超时不该让整个测试流程停掉。我一般会做有限重试:
int query_with_retry(ViSession inst, const char *cmd, double *out, int retries) { int i; for (i = 0; i < retries; i++) { if (inst_query_double(inst, cmd, out) == 0) return 0; /* 清空输入输出缓冲,避免残留数据污染下一次读 */ viClear(inst); printf("retry %d for %s\n", i + 1, cmd); } return -1; }逻辑说明:viClear是关键,超时后仪器可能还在慢慢回数据,不清缓冲,下一次读会先拿到旧数据。参数说明:retries一般设 2 到 3,太多会拖慢整体节拍;如果连续三次都超时,基本可以判定仪器掉线或资源名错了,这时候该报错而不是继续重试。这里有个边界:viClear本身也可能超时,所以它不该放在最内层循环里无脑调用。
3.3 多仪器并发:一个资源管理器,多个会话
产线场景经常要同时控电源和万用表。正确做法是开一个rm,然后viOpen两次拿两个inst,分别设超时、分别读写。不要为每台仪器各开一个rm,那样资源浪费且关闭时容易乱。多线程访问同一个inst必须加锁,VISA 会话本身不是线程安全的;如果嫌锁麻烦,就每台仪器一个线程、各自持有自己的会话。这块在源码里如果没处理,跑单台没事,一上多台就随机报错,属于典型的「单机测试全绿、联调全红」。
4. 避坑与排查:那些让 demo 跑不起来的真实原因
4.1 现象:viOpen一直返回VI_ERROR_RSRC_NFOUND
原因:资源名写错,或者仪器根本没被 VISA 识别。USB 仪器常见于没装厂商驱动、只装了 VISA,导致设备在系统里可见但 VISA 层看不到。解决:先开 NI MAX,看设备是否出现在「设备和接口」下;如果没有,装仪器厂商的 USB 驱动;如果有,直接复制 MAX 里的资源名,别手敲。LAN 仪器还要确认 IP 能 ping 通、端口 5025 没被防火墙拦。
4.2 现象:viWrite成功但viRead超时
原因:命令结尾没带\n,仪器一直在等行结束符,自然不回。或者发的是设置类命令却去读,仪器根本不产生回应。解决:所有命令统一补\n;确认命令是查询类(带?)才读。还有一种情况是仪器被设成了「只回显不回应」,需要先发SYST:REM切到远程模式。
4.3 现象:读到的数值总是上一次的
原因:上一次查询的回应没读干净,残留在缓冲区,这次读先拿到旧值。解决:每次查询后确保读到完整回应;超时或异常后用viClear清缓冲。更稳妥的做法是查询前先读一次丢弃,或者用viRead的返回字节数判断是否读全。
4.4 现象:程序退出后仪器还占着,下次连不上
原因:异常路径下没调viClose,会话泄漏。解决:把所有viClose收拢到统一出口,用goto cleanup或封装成函数,保证任何分支都关闭。顺序永远是先关inst再关rm。
4.5 现象:Linux 上编译过但运行报找不到库
原因:libvisa.so不在动态链接器搜索路径里。解决:用ldd ./visa_demo看依赖,缺的话把 VISA 库目录加进/etc/ld.so.conf再ldconfig,或者临时用LD_LIBRARY_PATH指定。别用复制库文件到系统目录这种野路子,升级时会冲突。
5. 进阶技巧:把 demo 变成能长期跑的采集程序
真正上产线的程序,和 demo 的差距不在能不能通,而在「连续跑八小时不出错」。我一般会加三样东西。第一是心跳与重连:每隔一段时间发一次*IDN?,连续失败就关闭会话重新viOpen,而不是让整个程序退出。第二是数据落盘:把每次读到的值连同时间戳写进 CSV,用fprintf加fflush,避免程序崩溃时丢数据。第三是超时分级:单值查询设 2 秒,波形读取设 10 秒,别用一个超时值打天下。
/* 带时间戳的采集循环骨架 */ while (running) { double v; if (query_with_retry(inst, "MEAS:VOLT:DC?", &v, 3) == 0) { fprintf(fp, "%ld,%.6f\n", (long)time(NULL), v); fflush(fp); /* 关键:崩溃也不丢已写数据 */ } else { /* 连续失败,尝试重连 */ reconnect(&inst, rm, res_name); } sleep_ms(500); }验证方法很简单:拔掉仪器网线或 USB,看程序是否按预期重试、重连,而不是卡死或退出;再插回去,看能否自动恢复采集。能过这一关,这份源码才算从「能跑」变成「能用」。我自己在这上面栽过最狠的一次,是没做fflush,跑了六小时的数据在断电后全没了,从那以后所有采集程序第一件事就是确认落盘。希望帮到你。
本文还有配套的精品资源,点击获取