Delphi调用libnodave读写西门子S7-300 PLC通信实战
2026/9/16 3:10:59 网站建设 项目流程

简介: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 和 slot

2.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 也是 2
S7-40003CPU 在槽 3
S7-1200/1500010.8.4 无 TLS,一般连不上,见第 5 章

现场如果 fopen 的配置同型号但读不到数据,先用 Step7 在线看一眼 CPU 的机架和槽位,比反复调超时参数有效率得多。

3. 把 libnodave 0.8.4 以 DLL 接进 Delphi:cdecl 调用约定与最小封装

3.1 为什么选 DLL,不直接翻译成 Pascal

看到 C 源码的第一反应是翻译成 .pas,但 0.8.4 的daveInterfacedaveConnection结构体里有 socket、缓冲区和一堆状态字段,逐字段翻译工作量大,且 GCC 的内存布局和 Delphi 的 record 对齐规则不一定一致,翻完还得反复调试。

我一般直接走 DLL:用 MinGW 把 C 源码编成libnodave.dll,Delphi 侧只写 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-16,PAnsiChar('192.168.0.1')这种写法会自动做一次隐式转换,但前提是变量声明为 AnsiChar 类型,否则拿到的指针指向的是会被立刻释放的临时内存。

这个声明把PDaveInterfacePDaveConnection当成不透明指针,假定 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;

daveDisconnectPLCdaveFree必须成对出现,且顺序不能换。前者关 socket、发断开请求,后者释放堆内存。如果只 Free 不 Disconnect,TCP 连接会留给系统超时回收,PLC 侧会认为连接还活着,重连时报资源占用。

3.4 DLL 导出名与 32/64 位的问题

用 gcc 直接编 C 源码不会像 C++ 那样产生名称修饰,导出名就是daveNewInterface,Delphi 用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 就传 dbnr=1、byteOffset=4、len=4,和 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 传 0,daveDB区读位时 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. 现场排错与边界:返回码、阻塞超时、线程模型与新型 PLC

5.1 让库把话说清楚:daveStrerror 和调试开关

连不上的时候不要盯着 err 的整数猜,先在错误分支里加一行daveStrerror(err)输出。0.8.4 头文件里还有daveSetDebugdaveDebugAll之类的开关,打开后每次 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 间隔自己把连接拉回来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询