资讯动态

libnodave实战:在LABVIEW与C#中调用C语言PLC通讯库

发布时间:2026/10/20 15:23:44 来源:尧图企业网站定制
简介libnodave-0.8.5.zip 是一套面向工业自动化开发者的西门子 PLC 通信库适合需要在 LABVIEW、C# 等环境中与 S7 系列 PLC 进行数据交互的工程师与教学研究人员。它基于西门子 Dave 通信协议实现跨平台支持 Linux、Mac OS 等非 Windows 系统可完成基本读写、周期性数据采集、状态监控与故障诊断等任务。压缩包共 120 个文件约 1.71MB以 C 源码与头文件为核心辅以 HTML 文档、可执行测试程序、批处理脚本及少量 C# 示例另含 DLL、SO 库文件与 Makefile 构建脚本便于二次开发与移植。目前已有 339 人学习下载。借助源码、头文件与配套文档读者可理解 S7 协议结构与数据类型掌握在 LABVIEW 中通过外部函数节点或 DLL 调用、在 C# 中借助 DllImport 实现通信的方法并参考测试程序完成 IP、站号与通讯参数配置快速搭建自定义 PLC 通信程序。1. 拆开 libnodave-0.8.5.zip一个 C 语言写的 PLC 通讯库怎么在 LABVIEW 和 C# 里跑起来如果你手头有一台西门子 S7 系列 PLC又不想被某家组态软件绑死那 libnodave 大概率是你绕不开的一个名字。它就是一个用纯 C 写的、跨平台的通讯库核心文件不多编译出来就是一个动态库加几个头文件。我第一次拿到libnodave-0.8.5.zip的时候第一反应是“这玩意儿怎么连个像样的文档都没有”但真正把它的nodave.c翻一遍之后发现它把 MPI、PPI、DP 这些协议栈都塞进了一个不到一万行的 C 文件里这种“黑匣子”式的实现反而让它在 LABVIEW 和 C# 里调用时特别省心——你不需要懂协议细节只要会填连接参数和读写地址就行。这篇笔记就是把我从解压到在 LABVIEW 和 C# 里跑通读写的一整套流程拆开适合那些需要把 PLC 数据接进上位机、但又不想买商业驱动的工程师。2. 编译与接口封装把 C 源码变成 LABVIEW 和 C# 能吃的动态库libnodave 本身只提供 C 接口而 LABVIEW 和 C# 都不直接吃 C 的源码所以第一步必须把它编译成动态链接库然后再做一层薄封装。这一步如果偷懒后面调用时会遇到一堆“找不到入口点”或者“堆栈不平衡”的玄学问题。2.1 源码结构与编译选项解压libnodave-0.8.5.zip之后你会看到根目录下有一堆.c和.h文件核心是nodave.c、nodave.h、setport.c和openSocket.c。其中nodave.c是协议实现setport.c负责串口初始化openSocket.c负责 TCP 连接。编译时最关键的是决定用哪种通讯方式如果你走以太网比如 CP343-1 模块需要定义DAVE_USE_TCP如果走串口PC Adapter 或 CP5611需要定义DAVE_USE_SERIAL。我一般会在编译命令里直接加宏定义而不是去改源码里的#define这样方便切换。以 Windows 下用 MinGW 编译为例命令大概是这样gcc -shared -o libnodave.dll nodave.c setport.c openSocket.c \ -DDAVE_USE_TCP -DDAVE_USE_SERIAL -lws2_32这里-shared生成动态库-lws2_32是链接 Windows 的 socket 库。如果你用 Visual Studio就建一个 DLL 工程把这三个.c文件加进去然后在预处理器里定义DAVE_USE_TCP和DAVE_USE_SERIAL最后链接ws2_32.lib。编译出来的libnodave.dll就是后面 LABVIEW 和 C# 都要加载的文件。注意32 位和 64 位一定要和你的上位机软件保持一致。LABVIEW 2018 之后的版本默认是 64 位而很多老版本的 C# 工程还是 32 位混用会直接报“不是有效的 Win32 应用程序”。2.2 导出 C 接口给 LABVIEW 调用LABVIEW 调用 DLL 用的是“调用库函数节点”它只认 C 风格的导出函数所以你不能直接把nodave.c里的内部函数暴露出去得自己写一层 wrapper。我一般会新建一个libnodave_wrapper.c里面只放几个最常用的函数打开连接、读字节、写字节、关闭连接。这样 LABVIEW 那边只需要配置四个节点就能完成基本读写。// libnodave_wrapper.c #include nodave.h #include string.h static daveInterface *di; static daveConnection *dc; // 导出函数连接 PLC __declspec(dllexport) int __stdcall ConnectPLC(char *ip, int rack, int slot) { di daveNewInterface(daveOpenSocket(ip, 102, 1), IF1, 0, daveProtoISOTCP, daveSpeed187k); dc daveNewConnection(di, 2, rack, slot); // MPI 地址固定为 2 return daveConnectPLC(dc); } // 导出函数读字节 __declspec(dllexport) int __stdcall ReadBytes(int area, int dbNum, int start, int len, unsigned char *buffer) { return daveReadBytes(dc, area, dbNum, start, len, buffer); } // 导出函数写字节 __declspec(dllexport) int __stdcall WriteBytes(int area, int dbNum, int start, int len, unsigned char *buffer) { return daveWriteBytes(dc, area, dbNum, start, len, buffer); } // 导出函数断开连接 __declspec(dllexport) void __stdcall DisconnectPLC() { daveDisconnectPLC(dc); daveFreeConnection(dc); daveFreeInterface(di); }这段代码里__stdcall是关键LABVIEW 默认用 stdcall 调用约定如果你用 cdecl调用时堆栈会乱掉表现就是第一次调用正常第二次直接崩溃。daveNewInterface里的daveOpenSocket参数分别是 IP、端口 102、以及一个超时标志。rack和slot根据你的 PLC 硬件组态填S7-300 一般 rack0、slot2S7-400 可能是 rack0、slot3。area参数在nodave.h里有定义比如daveDB表示数据块daveFlags表示 M 区。编译完这个 wrapper 之后在 LABVIEW 里右键“调用库函数节点”选择libnodave.dll然后手动添加这四个函数参数类型按 C 声明一一对应。读回来的buffer用“字节数组”接收再转成数值或字符串。2.3 C# 侧的 P/Invoke 封装C# 这边用DllImport做 P/Invoke 调用比 LABVIEW 更灵活因为你可以直接在 C# 里把字节数组转成结构体。我一般会建一个静态类PlcClient把四个导出函数包成方法。using System; using System.Runtime.InteropServices; public static class PlcClient { [DllImport(libnodave.dll, CallingConvention CallingConvention.StdCall)] public static extern int ConnectPLC(string ip, int rack, int slot); [DllImport(libnodave.dll, CallingConvention CallingConvention.StdCall)] public static extern int ReadBytes(int area, int dbNum, int start, int len, byte[] buffer); [DllImport(libnodave.dll, CallingConvention CallingConvention.StdCall)] public static extern int WriteBytes(int area, int dbNum, int start, int len, byte[] buffer); [DllImport(libnodave.dll, CallingConvention CallingConvention.StdCall)] public static extern void DisconnectPLC(); }调用时注意CallingConvention.StdCall必须和 C 侧的__stdcall一致否则会抛PInvokeStackImbalance异常。另外byte[]在 C# 里是引用类型P/Invoke 会自动 pin 住内存不需要手动fixed。读上来的字节数组可以用BitConverter.ToUInt16转成 short或者用Encoding.ASCII.GetString转成字符串。如果你要读浮点数记得 PLC 里是高位在前还是低位在前libnodave 不做字节序转换这个坑后面会细说。3. 连接参数与读写地址把 PLC 的 DB 块映射到上位机变量编译和封装只是把路铺好真正决定你能不能读到正确数据的是连接参数和地址映射。这一章把常见的连接方式、参数含义和读写示例拆开讲尤其是那些在文档里找不到、只能靠试错才能确定的细节。3.1 以太网与串口连接的参数差异libnodave 支持三种物理层ISO-TCP以太网、MPI串口或 CP5611、PPIS7-200。在daveNewInterface里第一个参数是daveOpenSocket或daveOpenPort返回的句柄第二个参数是接口名称随便填第三个参数是通讯协议第四个参数是通讯速率。以太网连接时daveOpenSocket的端口固定是 102这是 ISO-TCP 的标准端口。但有些 CP343-1 模块的插槽号不是 2而是 1 或 3这时候daveNewConnection里的slot参数就要跟着改。我遇到过一台 S7-300 用 CP343-1 做通讯rack0、slot2 死活连不上后来把 slot 改成 1 就通了。这个参数没有固定规律只能根据硬件组态里的机架和插槽号来试。串口连接时daveOpenPort需要指定 COM 口和波特率。PC Adapter 的波特率一般是 38400但 MPI 网络的波特率可能是 187.5k。这里有个容易翻车的地方daveOpenPort的第二个参数是波特率但如果你用的是 USB 转串口线COM 口号可能会变最好在代码里枚举所有 COM 口或者让用户手动选。// 以太网连接示例 di daveNewInterface(daveOpenSocket(192.168.0.1, 102, 1), IF1, 0, daveProtoISOTCP, daveSpeed187k); dc daveNewConnection(di, 2, 0, 2); // MPI地址2机架0插槽2 // 串口连接示例 di daveNewInterface(daveOpenPort(/dev/ttyUSB0, 38400), IF1, 0, daveProtoMPI, daveSpeed38400); dc daveNewConnection(di, 2, 0, 2);daveNewConnection的第二个参数是 MPI 地址S7-300 默认是 2S7-400 可能是 3 或 4。如果你不确定可以在 PLC 硬件组态里看“MPI 地址”那一栏。这个参数填错的话daveConnectPLC会返回一个负数错误码具体含义在nodave.h里有定义比如-5表示超时-6表示协议错误。3.2 读写 DB 块、M 区和 I/Q 区的地址计算libnodave 的地址计算方式和西门子 PLC 的地址表示法不太一样。PLC 里你写DB1.DBW0在 libnodave 里要拆成areadaveDB、dbNum1、start0、len2。这里的start是字节偏移不是位偏移。如果你要读DB1.DBX0.0开始的 8 个位start还是 0但len是 1读回来之后自己按位解析。M 区对应daveFlagsI 区对应daveInputsQ 区对应daveOutputs。这些常量在nodave.h里定义值分别是 0x01、0x02、0x03 等。读写的时候daveReadBytes的返回值是 0 表示成功非 0 表示失败。失败时不要只看返回值还要调用daveGetErrorText获取具体错误描述。// 读 DB1.DBW0长度2字节 unsigned char buf[2]; int res daveReadBytes(dc, daveDB, 1, 0, 2, buf); if (res ! 0) { printf(读失败: %s\n, daveGetErrorText(res)); } // 写 M0.0 开始的 1 个字节 unsigned char val 0xFF; res daveWriteBytes(dc, daveFlags, 0, 0, 1, val);这里有个血泪经验daveReadBytes的len参数不能超过 PDU 长度。S7-300 的 PDU 一般是 240 字节S7-400 可能更大。如果你一次读 500 字节会返回-7数据长度错误。解决办法是分段读每次读 200 字节左右然后在应用层拼起来。3.3 字节序与数据类型转换libnodave 读回来的字节数组是原始字节不做任何类型转换。如果你要读一个 REAL浮点数PLC 里是高位在前大端而 Windows 是小端所以你需要手动翻转字节序。我一般会写一个辅助函数// C# 里把 4 字节大端转成 float byte[] raw new byte[4]; PlcClient.ReadBytes(0x01, 1, 0, 4, raw); Array.Reverse(raw); // 大端转小端 float value BitConverter.ToSingle(raw, 0);如果是读字符串PLC 里的字符串格式是“第一个字节是最大长度第二个字节是当前长度后面是字符”所以你不能直接Encoding.ASCII.GetString得先取第二个字节作为长度再从第三个字节开始转。byte[] strBuf new byte[20]; PlcClient.ReadBytes(0x01, 1, 10, 20, strBuf); int len strBuf[1]; string s Encoding.ASCII.GetString(strBuf, 2, len);这些转换逻辑看起来简单但如果你不知道 PLC 的存储格式读出来的浮点数会是一个完全离谱的值比如1.0变成-2.3e38这时候不要怀疑通讯库先检查字节序。4. 避坑与排查连接失败、数据错位和内存泄漏的常见原因这一章记录的是我在实际项目里踩过的坑每一个都对应一个具体的现象、原因和解决办法。如果你刚开始用 libnodave建议先扫一遍能省下不少调试时间。4.1 连接返回 -5 或 -6但 ping 得通现象daveConnectPLC返回-5超时或-6协议错误但用 ping 命令能通端口 102 也开着。原因最常见的是 rack 和 slot 填错了。S7-300 的 CPU 直接带网口时rack0、slot2 通常是对的但如果你用的是 CP343-1 模块slot 可能是 1 或 3。另一个原因是 PLC 的“允许来自远程对象的 PUT/GET 通讯访问”没有勾选这个选项在硬件组态的安全设置里。解决先确认硬件组态里的机架和插槽号然后检查 PLC 的访问权限。如果还是不行用daveOpenSocket的第三个参数改成 0关闭超时重试看看能不能连上。4.2 读回来的数据全是 0 或乱码现象daveReadBytes返回 0成功但 buffer 里全是 0 或者随机值。原因地址偏移算错了。比如你要读DB1.DBW2在 libnodave 里start应该是 2而不是 1。如果你按字地址算每个字是 2 字节DBW2的字节偏移就是 2。另一个原因是 DB 块没有下载到 PLC 里或者 DB 块的“优化访问”没有关闭。S7-1200/1500 默认开启优化访问这种 DB 块不能用绝对地址读写必须关掉优化访问才能用 libnodave。解决先在 PLC 里监控 DB 块的值确认地址正确。然后检查 DB 块的属性把“优化的块访问”取消勾选。如果用的是 S7-1200/1500还需要在防护设置里允许 PUT/GET 通讯。4.3 长时间运行后内存泄漏现象程序跑几个小时之后内存占用越来越高最后崩溃。原因daveNewInterface和daveNewConnection每次调用都会分配内存如果你在循环里反复连接和断开却没有调用daveFreeConnection和daveFreeInterface内存就会泄漏。解决把连接和断开放在程序初始化时做一次不要放在循环里。如果确实需要重连确保每次daveDisconnectPLC之后都调用daveFreeConnection和daveFreeInterface。在 C# 里可以用try...finally保证释放。try { PlcClient.ConnectPLC(192.168.0.1, 0, 2); // 读写操作 } finally { PlcClient.DisconnectPLC(); }4.4 LABVIEW 调用时崩溃或报“堆栈不平衡”现象LABVIEW 里调用libnodave.dll时第一次正常第二次直接崩溃或者报“调用库函数节点时发生错误”。原因调用约定不匹配。LABVIEW 默认用 stdcall如果你的 DLL 导出函数用的是 cdecl就会导致堆栈不平衡。另一个原因是参数类型不匹配比如 C 里的int是 32 位LABVIEW 里如果选成 64 位整数也会出问题。解决在 C 代码里显式加__stdcall在 LABVIEW 的调用库函数节点里把参数类型改成“有符号 32 位整数”。对于unsigned char *参数LABVIEW 里选“字节数组”并勾选“数组按引用传递”。4.5 读写速度慢一次要几百毫秒现象每次读写都要等很久尤其是读多个变量时。原因libnodave 默认每次读写都建立一次 PDU 交换如果你在循环里逐个读变量效率会很低。另一个原因是daveSetTimeout设得太长默认可能是 5000 毫秒。解决把多个连续地址的变量合并成一次读比如读DB1.DBB0到DB1.DBB99一次读 100 字节然后在应用层拆分。另外可以把daveSetTimeout设成 1000 毫秒减少等待时间。daveSetTimeout(dc, 1000); // 设置超时为1秒5. 进阶技巧用批量读写和异步轮询把采集周期压到 50ms 以内当你把基本的读写跑通之后下一步就是优化采集速度。libnodave 本身是同步阻塞的但你可以通过批量读写和异步轮询把单次采集周期压到 50ms 以内。这一章分享两个我常用的技巧一个是合并地址一个是多线程轮询。5.1 合并连续地址减少 PDU 交换次数假设你要读 10 个浮点数分别位于DB1.DBD0、DB1.DBD4、DB1.DBD8……一直到DB1.DBD36。如果你逐个读需要 10 次 PDU 交换每次大概 10ms总共 100ms。但如果你一次读DB1.DBB0开始的 40 字节只需要 1 次 PDU 交换时间直接降到 10ms 左右。unsigned char bigBuf[40]; daveReadBytes(dc, daveDB, 1, 0, 40, bigBuf); // 然后按 4 字节一组解析 for (int i 0; i 10; i) { float val; memcpy(val, bigBuf i * 4, 4); // 注意字节序转换 }这个技巧的前提是你的变量地址是连续的。如果地址不连续但间隔不大也可以把中间的无关数据一起读回来然后在应用层丢弃。比如你要读DB1.DBD0和DB1.DBD100中间隔了 96 字节你可以一次读 104 字节只取前 4 字节和后 4 字节。虽然浪费了一点带宽但比两次 PDU 交换快得多。5.2 多线程轮询与连接池如果你需要同时采集多个 PLC或者一个 PLC 里有多组不连续的地址可以用多线程。每个线程持有一个独立的daveConnection这样互不干扰。但要注意libnodave 本身不是线程安全的你不能多个线程共用一个dc句柄。// 每个线程独立连接 Thread t1 new Thread(() { PlcClient.ConnectPLC(192.168.0.1, 0, 2); while (true) { byte[] buf new byte[40]; PlcClient.ReadBytes(0x01, 1, 0, 40, buf); // 处理数据 Thread.Sleep(50); } }); t1.Start();这里Thread.Sleep(50)是控制轮询周期但实际周期会略大于 50ms因为读写本身也要时间。如果你要精确控制可以用Stopwatch计算耗时然后动态调整 Sleep 时间。注意多线程下不要频繁连接和断开最好每个线程在启动时连接一次然后一直保持直到程序退出。频繁重连不仅慢还容易触发 PLC 的连接数限制。5.3 验证采集周期的方法怎么知道你的采集周期到底是多少我一般会在代码里加一个计时器记录每次读写的耗时然后打印出来。如果发现某次耗时特别长可能是网络抖动或者 PLC 在忙。连续记录 100 次取平均值和最大值就能看出稳定性。Stopwatch sw new Stopwatch(); for (int i 0; i 100; i) { sw.Restart(); PlcClient.ReadBytes(0x01, 1, 0, 40, buf); sw.Stop(); Console.WriteLine($第{i}次耗时: {sw.ElapsedMilliseconds}ms); Thread.Sleep(50); }如果平均值在 60ms 左右最大值不超过 100ms那这个采集周期就是可用的。如果最大值经常超过 200ms就要检查网络交换机是不是拥塞或者 PLC 的通讯负载是不是太高。从那以后我每次做 PLC 采集都会先跑一遍这个计时循环确认周期稳定了再写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑