ARTICLE DETAIL

资讯详情

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

欧姆龙FINS通信实战:用libfins实现PLC数据采集

欧姆龙FINS通信实战:用libfins实现PLC数据采集 简介Omron FINS通信协议C实现库libfins源码包面向工业自动化开发者用于解决PLC与上位机之间的网络通信与数据交互问题。资源包含完整源码与配套文档共155个文件其中61个C源文件覆盖连接管理、命令构建与解析、数据类型转换、错误处理及多线程通信等核心模块85个Markdown文档提供协议解析与使用说明另有Makefile、安装说明和许可证文件整个压缩包仅198KB轻量易部署。已有470人学习下载。通过研读源码与文档读者可掌握FINS帧结构、命令交互流程、寄存器读写方式以及常见网络故障的排错思路代码模块划分清晰可作为二次开发基础快速搭建Omron PLC通信测试程序或集成到现有自动化系统中。适合具备一定C基础、正在对接Omron设备的中级工程师参考。1. libfins欧姆龙 FINS 通信的另一个正确打开方式当现场要采集几十台欧姆龙 PLC 的数据而你又不想为每台上位机安装并维护 FinsGateway 时libfins 往往是更值得认真看的选项。它把欧姆龙 FINS 协议里常用的以太网帧组装、节点寻址、内存区读写和响应校验压缩成一组 C 接口让上层业务代码不用反复拼接 socket 缓冲区。这个标题的完整含义并不复杂libfins 是针对 FINS 协议的开源实现omron 则是它服务的对象。适合读这篇文章的是已经能独立完成 TCP/UDP 编程、又不想花一周读协议文档的工控软件工程师。接下来会按“协议原理 → 最小工程 → 批量实战 → 调试技巧”的顺序讲最后你会发现难的不是库本身而是对 FINS 内存模型的准确理解。2. FINS 帧结构、内存区与 libfins 的对象模型2.1 FINS 命令帧10 字节帧头 命令码 参数区FINS 是欧姆龙定义的以太网协议可以在 UDP 或 TCP 上承载。UDP 和 TCP 的默认端口都是 9600但大部分现场采集优先选 UDP原因是 PLC 侧资源占用更小上位机重发逻辑也更简单。一条 FINS 请求的最外层结构是“固定帧头 命令码 命令参数”其中固定帧头就是 10 个字节每个字节都有特定含义。字段长度含义常见取值ICF1信息控制字段0x80 表示指令帧RSV1保留字段固定 0x00GCT1网关允许数量通常 0x02DNA1目标网络号0x00 表示本地网络DA11目标节点号PLC 的 FINS 节点号DA21目标单元号0x00 表示 CPU 单元SA11源节点号上位机的节点号SA21源单元号上位机组号冗余时有用SID1服务标识符每次请求循环使用 0~255后面的数据区第一个字是命令码常用的是 0x0101 读字区域、0x0102 写字区域、0x0103 读多个存储区域。以读 DM 区为例在命令码后需要继续跟上内存区代码、起始地址和读取字数。libfins 的工作就是把这些字段按大端序打包成字节流再把 PLC 返回的响应帧拆回结构体。这里容易踩的第一个坑是把 DA1 和 SA1 写反导致 PLC 收到请求后不回应因为目标节点号对不上。typedef struct { uint8_t icf; uint8_t rsv; uint8_t gct; uint8_t dna; uint8_t da1; uint8_t da2; uint8_t sa1; uint8_t sa2; uint8_t sid; } fins_header_t;这个结构体只是帮助理解帧头布局实际发包时不一定要按结构体强转因为结构体可能存在对齐填充。更稳妥的写法是直接定义uint8_t buf[10]逐个字节赋值后再拼接命令段。重点要理解da1和sa1一个指向 PLC一个指向本机两个都是逻辑节点号而不是 IP 地址这也是 FINS 和普通 Modbus TCP 寻址方式的最大区别。2.2 欧姆龙内存区CIO、WR、HR、DM 的编码与边界Modbus 协议把寄存器映射成 4 类地址而欧姆龙 FINS 直接用内存区名称寻址。这种设计的优势是程序可读性高劣势是上位机要做一张区域映射表。常见的 FINS 内存区代码和寻址范围需要记住实际调试时基本每天都在看这些值。内存区区代码寻址方式典型用途CIO 区0x30字地址 位地址外部 I/O、输入输出继电器WR 区0x31字地址内部工作位HR 区0x32字地址 位地址掉电保持区DM 区0x02字地址数据存储器最常用EM 区0xA8当前 Bank扩展数据存储器FINS 请求里的起始地址是用 3 字节表示的高 16 位是字地址最低 4 位是位地址。比如 CIO 区地址 5.03字地址就是 5位地址就是 3在请求包里通常被拼成一个 3 地址字节。DM 区只允许字访问所以位地址必须是 0。很多不熟悉欧姆龙的开发者会用十进制去拼位地址结果发送出去后 PLC 返回 0x1101表示“内存区或地址非法”。建议在写采集程序前先用 CX-Programmer 的地址视图确认目标地址的字号和位号再决定调用的是卫健委读字接口还是读位接口。2.3 libfins 分层设计总线、协议、PLC 对象libfins 的组织方式通常可以分成三层传输层负责 socket 的创建、连接、超时读取协议层负责 FINS 帧的组装和解析PLC 对象层给上层应用提供语义化的读写方法。这种分层在工业协议库里比较常见问题是不同分支的 API 命名并不一致有的叫fins_plc有的叫fins_client但职责基本类似只要配置了 IP、端口和节点号就能以“对象”为单位操作。fins_plc_t *plc fins_plc_create(); fins_plc_set_remote_node(plc, 10); fins_plc_set_transport(plc, FINS_TRANSPORT_UDP); fins_plc_connect(plc, 192.168.0.10, 9600);这段代码表达的是配置动作不是正式 API 调用实际命名以你引入的头文件为准。需要把握的是调用顺序必须先设置节点号再设置传输方式和 IP因为库内部很可能是先填充帧头的 DA1 字段然后才建立 socket。如果你先 connect 再设置节点号某些实现会认为节点号已经锁定导致后续命令发出去的目标节点是默认值。三层分离带来的好处是当现场网络从单 PLC 变为多 PLC 时只需要创建多个上层对象传输层会自动分配独立的 socket 和接收缓冲。协议层则负责保证 SID 不冲突并在收到响应时根据 SID 路由到正确的请求上。3. 用 libfins 在 Linux 上跑通 FINS 读写的完整过程3.1 源码构建先确认用 configure 还是 cmake拿到 libfins-master 这样的源码包后第一件事不是直接make而是确认构建系统。不同时期的分支工程组织方式不一样常见的是 autotools 的configure和 CMake 的CMakeLists.txt两种。先用命令看根目录tar -xzf libfins-master.tar.gz cd libfins-master ls configure CMakeLists.txt Makefile 2/dev/null如果看到configure按 autotools 的标准流程走./autogen.sh ./configure --prefix/usr/local make -j$(nproc) sudo make install如果看到CMakeLists.txt就用 CMake 方式mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc) sudo make install参数说明--prefix决定头文件和库文件的安装目录默认/usr/local通常够用。CMake 方式里-DCMAKE_BUILD_TYPE只在单配置生成器下有效如果用的是 Visual Studio 生成器就需要换成--config Release。安装完成后确认/usr/local/lib下是否有 libfins 相关文件再用pkg-config --cflags libfins检查是否被系统识别。3.2 最小读写示例UDP 连接下读取 DM 区 12 个字下面的 C 代码展示最典型的 FINS 读操作流程代码风格尽量保持通用核心逻辑是先创建对象再配置远程节点连接后调用读字函数。#include stdio.h #include stdint.h #include libfins.h int main(void) { fins_plc_t *plc; uint16_t data[12] {0}; int ret, i; plc fins_plc_create(); if (!plc) { return -1; } fins_plc_set_remote_node(plc, 10); ret fins_plc_connect(plc, 192.168.0.10, 9600); if (ret ! 0) { printf(connect failed: %d\n, ret); fins_plc_destroy(plc); return -1; } ret fins_read_words(plc, FINS_MEMORY_DM, 0, 12, data); if (ret 0) { for (i 0; i 12; i) { printf(D%04u 0x%04X (%u)\n, i, data[i], data[i]); } } else { printf(read error: %d\n, ret); } fins_plc_destroy(plc); return 0; }参数说明里最重要的三个值是节点号 10、目标 IP 192.168.0.10、读取长度 12。FINS_MEMORY_DM是对内存区码 0x02 的封装fins_read_words内部会完成地址编码和响应解析。注意这里用的是 UDPconnect并不触发真正的数据包错误会推迟到第一次读取时暴露。如果读取返回非 0不要立刻怀疑 PLC 断电先确认 PLC 的 FINS 节点号是不是 10。3.3 编译链接选项与头文件不匹配处理写完后编译时最常见的错误是找不到头文件或链接不到库。编译命令通常这样写gcc -o fins_demo fins_demo.c -L/usr/local/lib -lfins -I/usr/local/include链接选项-lfins对应系统里的libfins.so或libfins.a如果安装后库文件名带版本号比如libfins.so.1需要检查是否建立了软链接。还有一部分 libfins 分支依赖 OpenSSL 或 libpcap编译时需额外加-lssl -lcrypto具体看pkg-config --libs的输出。如果头文件报错说找不到某个内部数据结构大概率是安装的头文件路径和源码版本不一致建议重新编译而不是手改头文件。3.4 PLC 不回包时的排查顺序如果程序已经连接成功但读取一直卡在超时不要反复重启程序按下面的顺序排查效率更高用 Wireshark 抓包看请求是否已经发到 PLC确认源端口和目标端口都是 9600。检查 PLC 侧的 FINS 节点号是否等于代码里的set_remote_node值。检查上位机 IP 与 PLC IP 是否在同一网络跨网段时确认路由是否允许 UDP。如果请求帧能抓到响应帧没有多半是 PLC 的单元号或网络号不对。如果抓到响应帧但程序仍然超时查看响应帧里的端到端响应码对照 FINS 协议表定位地址错误。在批量项目中这个排查顺序能避免把问题带到下一层。很多人一遇到超时就去改重试次数结果把根本性的地址错误掩盖了。4. 批量操作、多连接与数据转换的 FINS 实战参数4.1 常用 FINS 命令码与读写场景对照操作命令码参数结构使用场景读字区域0x0101区域码 起始地址 字数采集 DM、CIO 数据写字区域0x0102区域码 起始地址 数据下发温度设定读位区域0x0101区域码 地址 位数读取输入点、故障位写位区域0x0102区域码 地址 位数据控制电机启停多区域读0x0103每区域参数组合一条命令读多个区域现场最常见的错误是把“读位区域”也当成读字来调用。在欧姆龙 FINS 中位地址在请求包里的编码方式是字地址加位号例如 CIO 100.05 对应的地址编码是字地址 100、位地址 5。如果在fins_read_words里只传 100库无法知道你指的是整字还是位的起始读取结果自然不对。因此先确认目标数据是字变量还是位变量再决定调用哪一类接口。4.2 分块读 DM 区一次读多少字才合适FINS 协议对单次读取的字数有型号限制CJ 系列一般允许最多 960 字NJ/NX 系列较新的型号可以更大但网络层也会限制 UDP 包长度。即使协议允许也不建议一次就读 900 多字因为响应帧会接近 1800 字节容易触发 IP 分片增加丢包和重组的概率。实际工程里更稳妥的策略是把单次读取长度限制在 128 字既能满足大部分数据采集周期又不会让响应包超过 MTU 1500 字节。分块逻辑可以封装成一个函数#define CHUNK 128 int read_large_dm(fins_plc_t *plc, uint16_t start, uint16_t count, uint16_t *out) { uint16_t done 0; while (done count) { uint16_t n count - done CHUNK ? CHUNK : count - done; int ret fins_read_words(plc, FINS_MEMORY_DM, start done, n, out done); if (ret ! 0) { return -1; } done n; } return 0; }这里的start done表示每次从上次结束的地方继续读out done让写入目标缓冲区时不重叠。需要特别注意的是PLC 返回的数据是原始二进制数值不是字符串形式的十六进制所以打印时要用格式化函数处理。如果读取的变量是浮点数返回的原始数据是两个 16 位寄存器组成的 32 位浮点不要直接强转需要先拼成uint32_t再通过memcpy到float变量。4.3 多 PLC 采集时的线程模型与 SID 冲突同时接多台 PLC 是工控上位机的常态。常见的多 PLC 方案是每个 PLC 一个线程每个线程维护独立的 plc 句柄和独立 socket。这样设计可以避免两个线程共用同一个 UDP socket 后接收线程无法判断响应属于哪个 PLC。需要理解的是UDP 的connect只是把远端地址绑定到 socket 上本地端口由内核自动分配多线程多 socket 并不会冲突。只有在手动绑定相同本地端口时才需要加SO_REUSEADDR。SID 字段的冲突是另一个隐患。每条 FINS 请求的 SID 在 0 到 255 之间循环如果两个线程共用一个 SID 计数器PLC 的响应返回后库内部可能把响应路由到错误的请求上。libfins 如果按每个 plc 对象维护 SID那它天然避免了多对象间的冲突如果你自己管理重发逻辑务必保证同一请求超时重发时 SID 不变否则 PLC 会认为这是一条新请求原来的响应即使到达也会被丢弃。4.4 有符号数、BCD 码与字符串的转换欧姆龙 PLC 的 DM 区本身不区分数据类型一个地址里的 0x8000 既可以解释为 32768也可以解释为 -32768。使用 libfins 读回来的uint16_t数组要按 PLC 程序里的变量类型做转换。带符号整数直接用类型转换是最常见的做法int16_t speed (int16_t)data[3];如果 PLC 程序中用的是 BCD 格式比如 D100 保存 0x1234其表示的十进制值就是 1234。BCD 转二进制的逻辑不复杂但要注意每一位必须是 0 到 9否则输入非法。以下函数可以放到通用工具模块里uint16_t bcd_to_bin(uint16_t bcd) { return ((bcd 12) 0x0F) * 1000 ((bcd 8) 0x0F) * 100 ((bcd 4) 0x0F) * 10 (bcd 0x0F); }需要注意欧姆龙某些型号支持“负数 BCD”表示即最高位为符号位此时直接按上式转换会得到巨大数值。遇到负数 BCD 时先判断符号位再对剩余位做 10 的补码转换。字符串则没有统一的编码标准常见 ASCII 码会按每两个 ASCII 字符占一个字的规则存储转换时也需要按字序重组。5. 用 libfins 时值得收藏的三个调试技巧5.1 用 Python 模拟 FINS 响应端没有真实 PLC 时可以用 Python 起一个 UDP 服务监听 9600模拟单条读响应让 libfins 的流程在开发机上先跑通。下面这段代码只模拟了“读请求返回成功”但足够验证帧组装和超时逻辑。import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind((0.0.0.0, 9600)) while True: data, addr s.recvfrom(4096) if len(data) 12: resp bytearray(data[:10]) resp.extend([0x01, 0x01]) resp.extend([0x00] * 24) s.sendto(bytes(resp), addr)这段代码的关键是把请求的前 10 字节原样复制到响应这样 SID 才会匹配。如果 libfins 的实现在收包时会校验 SID这一步不复制就会始终报超时。模拟写入操作也很简单检查data[10] 0x01和data[11] 0x02然后回一个命令码 0x0101 的成功帧即可。5.2 Wireshark 抓包过滤与 FINS 响应码定位到了现场Wireshark 是排查 FINS 通信的底牌。显示过滤器写udp.port 9600抓到帧后 Wireshark 会自动识别 FINS 协议并展开各字段。重点关注响应帧里的“end code”字段这是 PLC 端返回的错误码。常见的值包括0x1101 表示目标地址不存在或数据类型错误0x1103 表示地址溢出0x1001 表示命令过长0x0105 表示目标节点不存在。把这些码整理成一张对照表贴在工位上比每次现查说明书高效得多。5.3 大小端转换与浮点数拼接的边界FINS 网络字节序是大端常见 CPU 是小端。如果库本身已经解析不需要额外处理如果自行拼接帧写入命令行字节序最容易丢分。检验方法是写入一个已知值 0x1234再从 PLC 读回来如果显示 0x3412就是少了htons转换。提示判断本机大小端可以用uint16_t x 1;然后取地址看首字节也可以直接用htons(1)的返回值做条件判断。工控机上 x86 和 ARM 基本都是小端但在通信边界上仍然显式转换最稳妥。浮点数拼接同样如此32 位浮点在 FINS 里占两个字先收到的字是高 16 位还是低 16 位取决于 PLC 程序里浮点的存储顺序。遇到与预期相反时把两个字交换顺序再用memcpy到float变量即可。本文还有配套的精品资源点击获取
返回列表