简介: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 一般 rack=0、slot=2,S7-400 可能是 rack=0、slot=3。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)、PPI(S7-200)。在daveNewInterface里,第一个参数是daveOpenSocket或daveOpenPort返回的句柄,第二个参数是接口名称(随便填),第三个参数是通讯协议,第四个参数是通讯速率。
以太网连接时,daveOpenSocket的端口固定是 102,这是 ISO-TCP 的标准端口。但有些 CP343-1 模块的插槽号不是 2,而是 1 或 3,这时候daveNewConnection里的slot参数就要跟着改。我遇到过一台 S7-300 用 CP343-1 做通讯,rack=0、slot=2 死活连不上,后来把 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 默认是 2,S7-400 可能是 3 或 4。如果你不确定,可以在 PLC 硬件组态里看“MPI 地址”那一栏。这个参数填错的话,daveConnectPLC会返回一个负数错误码,具体含义在nodave.h里有定义,比如-5表示超时,-6表示协议错误。
3.2 读写 DB 块、M 区和 I/Q 区的地址计算
libnodave 的地址计算方式和西门子 PLC 的地址表示法不太一样。PLC 里你写DB1.DBW0,在 libnodave 里要拆成area=daveDB、dbNum=1、start=0、len=2。这里的start是字节偏移,不是位偏移。如果你要读DB1.DBX0.0开始的 8 个位,start还是 0,但len是 1,读回来之后自己按位解析。
M 区对应daveFlags,I 区对应daveInputs,Q 区对应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 直接带网口时,rack=0、slot=2 通常是对的,但如果你用的是 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 采集,都会先跑一遍这个计时循环,确认周期稳定了再写业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取