ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Delphi调用libnodave读写西门子S7-300 PLC通信实战

Delphi调用libnodave读写西门子S7-300 PLC通信实战 简介libnodave-0.8.4.tar.gz是一套专为西门子S7系列PLC设计的开源通信库源码包面向需要在Delphi、VB、VC环境下开发上位机与PLC数据交换应用的工业自动化工程师。压缩包完整收录库的核心实现与多语言封装可通过工业以太网或MPI协议与S7-200、S7-300、S7-400通信支持输入输出读写、诊断、块操作等常用功能。整个包共334个文件大小约3.02MB既有c源码、h头文件、编译成的dll/so等库文件也有Delphi的pas/dfm源码、VB的bas模块、可运行的exe示例以及大量html文档和bat编译脚本方便对照不同开发环境快速理解并复用。已有171人学习下载。资源内附多个测试程序与示例工程能够帮助开发者快速理解初始化连接、读写数据、错误处理等关键流程同时通过buildwin、vcwinmake等脚本可轻松重新编译适合在产品中深度集成或定制S7通信功能的开发者作为参考。1. 一个 Delphi 老项目为什么会翻出 libnodave-0.8.4 这个包拿到libnodave-0.8.4.tar.gz_300_Libnodave Delphi_delphi libnodave_lib这个包名的任务九成是同一类需求老 Delphi 上位机要直接读西门子 S7-300 的 DB 块、M 区和 I/O不想被组态软件绑死。libnodave 是少见的开源 S7 协议实现纯 C、体积小0.8.4 虽然年头久但 S7 通信协议本身十几年没大改所以它到今天还能在产线机器上跑。对 Delphi 开发者来说真正要处理的从来不是协议而是把 C 的接口、cdecl 调用约定和字节序正确翻译进 Pascal 工程。这篇文章按我实际会做的顺序把协议要点、DLL 封装、读写代码和现场排错一次讲透适合 Delphi 用得顺手但第一次碰 PLC 通信的人。2. libnodave 的协议栈与连接模型ISO-on-TCP、rack 和 slot2.1 先弄清 libnodave 替你实现了哪一层S7-300 走以太网时物理链路是普通 TCP端口固定 102但 TCP 之上不是裸数据而是 RFC 1006 定义的 ISO-on-TCP 封装TPKT COTP最上层才是西门子的 S7 PDU。完整握手顺序是TCP 三次握手、COTP 连接请求、S7 Setup Communication协商双方 PDU 长度之后才能发 read/write 请求。libnodave 0.8.4 的价值在于把这些全部包掉了。你调用daveConnectPLC它内部完成 COTP 握手和 S7 通讯建立调用daveReadBytes它负责拼读请求、解析响应。Delphi 端不需要拼任何报文也没必要理解 TPKT 头但必须知道它默认走的是daveProtoISOTCP这个协议标识而不是裸 TCP否则连握手第一步就错。库本身支持三套通讯方式PPI 用于 S7-200 串口MPI 用于 S7-300/400 加 CP5611 卡ISO-on-TCP 用于以太网。本文只讲 ISO-on-TCP因为 90% 的 Delphi 项目都是用网线直连 CPU 的集成 PN 口或 CP343-1 模块。2.2 adapter 和 connection 两层对象libnodave 0.8.4 的连接模型是两层daveInterface管网络或串口的适配层传入 IP 地址、接口类型、协议、速度daveConnection是一次到具体 CPU 的会话携带 rack 和 slot。常见的 C 初始化顺序长这样daveInterface di; daveConnection dc; daveNewInterface(di, 192.168.0.1, daveTCP, daveProtoISOTCP, 7); daveNewConnection(dc, di, daveProtoISOTCP, 0, 2); daveConnectPLC(dc);第 5 个参数 7 在 TCP 方式下没有实际意义是给串口波特率保留的位置照抄就是对。一个 interface 理论上能挂多个 connection但工控现场一台上位机连一台 PLC 的场景一个连接足够别把模型做复杂。这层模型直接决定了 Delphi 封装的结构必须同时保留 interface 和 connection 两个句柄释放顺序是先断开再daveFree顺序反了在重连时会出野指针。2.3 rack 和 slot 是 CPU 的坐标不是随便填的daveNewConnection里那两个整数是西门子通信服务用来定位 CPU 的rack 是机架号slot 是 CPU 在机架上的插槽号。S7-300 的 CPU 通常插在 0 号机架第 2 槽S7-400 是第 3 槽。填错了不会报语法错而是握手能成功、读写全失败这是最迷惑人的一种现象。PLC 类型rackslot备注S7-300 集成 PN 口02最常见配置S7-300 CP343-102走 CP 也是 2S7-40003CPU 在槽 3S7-1200/1500010.8.4 无 TLS一般连不上见第 5 章现场如果 fopen 的配置同型号但读不到数据先用 Step7 在线看一眼 CPU 的机架和槽位比反复调超时参数有效率得多。3. 把 libnodave 0.8.4 以 DLL 接进 Delphicdecl 调用约定与最小封装3.1 为什么选 DLL不直接翻译成 Pascal看到 C 源码的第一反应是翻译成 .pas但 0.8.4 的daveInterface和daveConnection结构体里有 socket、缓冲区和一堆状态字段逐字段翻译工作量大且 GCC 的内存布局和 Delphi 的 record 对齐规则不一定一致翻完还得反复调试。我一般直接走 DLL用 MinGW 把 C 源码编成libnodave.dllDelphi 侧只写 external 声明。库更新时只换 DLL 文件工程代码一行不改。这里有个绕不开的合规问题libnodave 是 GPL 授权闭源商业 Delphi 软件直接链接有传染风险常见做法是把它独立成一个通讯服务进程通过本地 IPC 和上位机交换数据把它和主程序从法律和工程两个层面隔离。3.2 先把调用约定钉死是 cdecl不是 stdcall这是 Delphi 接所有 C 库的第一个坑。libnodave 用 GCC 编译默认调用约定是 cdecl参数从右往左入栈、由调用方清栈。Delphi 的 external 声明如果不写cdecl默认按当前编译器的 register/stdcall 约定处理栈不平衡轻则偶尔返回值错乱重则直接访问违例。unit LibNodave; interface type PDaveInterface Pointer; PDaveConnection Pointer; const // 与 dave.h 一致 daveProtoPPI 0; daveProtoMPI 1; daveProtoISOTCP 2; daveSerial 0; // 串口方式 daveTCP 1; // 以太网方式 // 访问区常量第 4 章要用 daveInputs 1; daveOutputs 2; daveFlags 3; daveDB 4; daveTimer 7; daveCounter 8; function daveNewInterface(iface: PAnsiChar; ifacetype, protocol, speed: Integer): PDaveInterface; cdecl; external libnodave.dll; function daveNewConnection(di: PDaveInterface; protocol, rack, slot: Integer): PDaveConnection; cdecl; external libnodave.dll; function daveConnectPLC(dc: PDaveConnection): Integer; cdecl; external libnodave.dll; function daveReadBytes(dc: PDaveConnection; area, dbnr, start, len: Integer; buf: Pointer): Integer; cdecl; external libnodave.dll; function daveWriteBytes(dc: PDaveConnection; area, dbnr, start, len: Integer; buf: Pointer): Integer; cdecl; external libnodave.dll; function daveReadBits(dc: PDaveConnection; area, dbnr, start, len: Integer; buf: PByte): Integer; cdecl; external libnodave.dll; function daveWriteBits(dc: PDaveConnection; area, dbnr, start, len: Integer; buf: PByte): Integer; cdecl; external libnodave.dll; function daveDisconnectPLC(dc: PDaveConnection): Integer; cdecl; external libnodave.dll; procedure daveFree(dc: PDaveConnection); cdecl; external libnodave.dll; function daveStrerror(err: Integer): PAnsiChar; cdecl; external libnodave.dll; implementation end.传入 IP 地址时注意用PAnsiChar不要直接传 UnicodeString。Delphi 2009 之后字符串默认是 UTF-16PAnsiChar(192.168.0.1)这种写法会自动做一次隐式转换但前提是变量声明为 AnsiChar 类型否则拿到的指针指向的是会被立刻释放的临时内存。这个声明把PDaveInterface和PDaveConnection当成不透明指针假定 DLL 内部在传 nil 时会自己 malloc 结构体这就是daveFree存在的意义。如果你拿到的 DLL 变体不处理 nil跑通的第一步就会崩判断办法很简单daveNewConnection返回后直接调一次daveConnectPLC正常返回就说明结构体已经分配好了。3.3 建立连接和释放的最小代码var di: PDaveInterface; dc: PDaveConnection; err: Integer; begin di : daveNewInterface(PAnsiChar(192.168.0.1), daveTCP, daveProtoISOTCP, 7); if di nil then Exit; dc : daveNewConnection(di, daveProtoISOTCP, 0, 2); if dc nil then Exit; err : daveConnectPLC(dc); if err 0 then begin // 这里用 daveStrerror 翻译错误别只记 err 数字 Log(daveStrerror(err)); Exit; end; // ... 业务读写代码 ... daveDisconnectPLC(dc); daveFree(dc); end;daveDisconnectPLC和daveFree必须成对出现且顺序不能换。前者关 socket、发断开请求后者释放堆内存。如果只 Free 不 DisconnectTCP 连接会留给系统超时回收PLC 侧会认为连接还活着重连时报资源占用。3.4 DLL 导出名与 32/64 位的问题用 gcc 直接编 C 源码不会像 C 那样产生名称修饰导出名就是daveNewInterfaceDelphi 用external libnodave.dll就能对上。如果哪天拿到的是 C 工程编的 DLL就要用 .def 文件锁导出名LIBRARY libnodave.dll EXPORTS daveNewInterface daveNewConnection daveConnectPLC daveReadBytes daveWriteBytes daveReadBits daveWriteBits daveDisconnectPLC daveFree daveStrerror另一个现实问题是 0.8.4 是 32 位时代的库官方包里只有 32 位产物。现在用 Delphi Community Edition 建工程默认如果选 Win64 编译加载 32 位 DLL 直接失败。偷懒的做法是工程目标平台改 Win32正式项目建议自己用 mingw-w64 编一个 64 位版本源码里没有依赖 32 位整数指针的地方重编一般一次过。4. 用 Delphi 读写 S7-300 的 DB、M 区和位可抄的代码与参数表4.1 读 DB 块的数值按字节取回类型转换自己做daveReadBytes只做一件事从指定区域拷出原始字节。它不解析浮点、不处理字节序S7 协议在网络上是大端传输而 x86 的 Delphi 是小端所以 DBD 里的 REAL 转成 Delphi 的 Single 必须手工倒字节。function ReadS7Real(dc: PDaveConnection; dbnr, byteOffset: Integer; var Value: Single): Boolean; var buf: array[0..3] of Byte; tmp: LongWord; begin Result : False; if daveReadBytes(dc, daveDB, dbnr, byteOffset, 4, buf) 0 then Exit; tmp : (LongWord(buf[0]) shl 24) or (LongWord(buf[1]) shl 16) or (LongWord(buf[2]) shl 8) or LongWord(buf[3]); Value : PSingle(tmp)^; Result : True; end;byteOffset是字节地址不是位地址。读 DB1.DBD4 就传 dbnr1、byteOffset4、len4和 Step7 里看到的绝对地址一致不用额外换算。PSingle(tmp)^ 是把重排好的大端长字直接解释成 Single这个写法比 Move 更直观也避免了中间变量被编译优化掉的问题。整数同理S7 的 INT 是 16 位大端WORD、DINT、REAL 都是大端只有自己写转换函数最可靠。库里虽然带了 daveGetS32 之类的辅助函数但它们读的是内部 PDU 缓冲区不是 Delphi 侧的内存混用容易踩指针偏移的坑我一般不用。4.2 读 M 区的位daveReadBits 的 start 是位索引M 区标志位在 S7 里按字节编址M10.3 是第 10 个字节的第 3 位。daveReadBits的 start 参数单位是位所以要先做10 * 8 3的换算len 也是位数。function ReadM103(dc: PDaveConnection): Boolean; var b: Byte; begin Result : False; // start 字节地址*8 位偏移len 按位算 if daveReadBits(dc, daveFlags, 0, 10 * 8 3, 1, b) 0 then Result : (b and $01) 0; end;daveFlags区读位时 dbnr 传 0daveDB区读位时 dbnr 传 DB 号start 同样按位算。读 DBX 就是daveReadBits(dc, daveDB, 1, 4 * 8 2, 1, b)对应 DB1.DBX4.2。位读返回的字节里只有低位有效高位未定义所以判断时用and $01不要直接if b 0否则同一字节里其他位的数据会让判断失真。4.3 访问区参数速查表访问区area 常量dbnr 参数start 单位典型用途DB 数据块daveDB 4DB 号字节/位工艺参数、配方M 存贮器daveFlags 30字节/位中间标志、报警位输入 I 区daveInputs 10字节数字量输入输出 Q 区daveOutputs 20字节数字量输出定时器daveTimer 70字节定时器当前值计数器daveCounter 80字节计数器当前值I/Q 区读的是过程映像不是外设直接读。如果现场信号变化极快、一次扫描内就要看到最新 IO 状态S7-300 默认的 1ms 刷新周期通常也够用不需要碰外设直读的指令。4.4 写操作与返回值检查写 REAL 就是读的逆过程注意字节序反着拼回去function WriteS7Real(dc: PDaveConnection; dbnr, byteOffset: Integer; Value: Single): Boolean; var buf: array[0..3] of Byte; tmp: LongWord; begin tmp : PLongWord(Value)^; buf[0] : Byte(tmp shr 24); buf[1] : Byte(tmp shr 16); buf[2] : Byte(tmp shr 8); buf[3] : Byte(tmp); Result : daveWriteBytes(dc, daveDB, dbnr, byteOffset, 4, buf) 0; end;每次写操作前要确认 PLC 侧 DB 块已经存在且不是只读保护否则返回错误码。所有 libnodave 读写函数返回 0 表示成功非 0 用daveStrerror(err)翻译常见的是目标区不存在、长度越界、连接已断开三类。二进制浮点数写入后如果不放心读回来比对一次再置完成位这是工控上位机写联锁信号时的标准防御姿势。大数据块读写要分块。0.8.4 在 Setup Communication 阶段协商的 PDU 长度有限单次daveReadBytes超过约 240 字节会直接失败。读一个 100 字节的 DB 没问题读 2000 字节的配方就要循环按 240 字节一段拆或者只读当前画面需要的片段不要贪多。5. 现场排错与边界返回码、阻塞超时、线程模型与新型 PLC5.1 让库把话说清楚daveStrerror 和调试开关连不上的时候不要盯着 err 的整数猜先在错误分支里加一行daveStrerror(err)输出。0.8.4 头文件里还有daveSetDebug和daveDebugAll之类的开关打开后每次 S7 请求的收发状态会打到 stderr能直接看出是握手失败还是读写被拒。上线前记得关掉这些输出在轮询场景下会刷爆日志文件。5.2 阻塞 socket 的两种死法超时与断线0.8.4 用阻塞 socket最典型的问题有两个。第一PLC 断电或网线被拔daveConnectPLC可能卡几十秒才返回这在界面线程里就是假死。第二连接建立后 PLC 突然重启连接状态不会主动通知下一次daveReadBytes才返回错误。应对方案是把整个通讯逻辑放进一个独立 worker 线程UI 只显示线程状态机的最后结果读写循环里发现错误就走重连先daveDisconnectPLC忽略返回值再daveConnectPLC不需要重新daveNewConnection。5.3 一个连接对象不要跨线程libnodave 的daveConnection内部有收发缓冲区两个线程同时对同一个连接调用daveReadBytes请求和响应会互相串读回来的是错位数据。多线程上位机要么给连接加 TCriticalSection要么每个线程各自 New 一个连接。我倾向于后者PLC 侧允许的并行连接数足够代码还不用考虑锁粒度。5.4 验证手段与 S7-1200/1500 的边界调试时用 Wireshark 抓本机 102 端口流量过滤tcp.port 102能看到 COTP 的 TPDU 和 S7 PDU 的长度字段对照daveReadBytes传入的 len 可以确认是否被分块逻辑截断。另一个必查项S7-300 的 CPU 属性里把 PUT/GET 通讯关掉的场景很常见这种要先去硬件配置里勾选允许不完全是代码问题。最后划个边界S7-1200/1500 新固件默认启动 TLS 加密libnodave 0.8.4 不支持别在这上面耗时间。要么在 PLC 侧关闭加密并开启 PUT/GET有安全风险仅限隔离内网要么换 Snap7 或 OPC UA。0.8.4 的归宿就是 S7-300/400 产线柜子里的老伙计。我自己的重连逻辑里还有个习惯每次读之前先检查句柄是否为空再把读操作包在 try/except 里保证 PLC 重启瞬间 UI 不会跟着卡死然后让线程按 500ms 间隔自己把连接拉回来。本文还有配套的精品资源点击获取
返回列表