简介:欧姆龙FINS协议动态库为工业自动化通信场景量身打造,面向需要连接PLC、HMI、传感器等设备的Delphi、VC++、VB开发者。它封装了FINS协议核心操作,支持多串口并发与多线程通信,可高效完成寄存器读写、远程I/O控制和设备诊断。压缩包共42个文件,大小仅1.32MB,核心为DLL动态库,配套h接口头文件、chm详解手册、TXT说明文档以及Delphi/VC/VB三套示例工程;每套示例均含源码和可执行程序,另有预编译DLL实例可直接引用。已有874人学习下载,适合自动化系统设计者、设备维护人员及工业通信入门者。通过阅读手册和运行示例,可快速掌握协议调用方式,参照现有代码改造集成,缩短项目开发周期。
1. 上位机读欧姆龙PLC,绕不开的FINS协议:这份动态库帮你把帧拼好
做上位机开发的工程师早晚会遇到这么一个活:产线上一排欧姆龙PLC,MES要采几百个点位,组态软件走OPC虽方便,但点位一多轮询慢、改配置还要重启服务,碰上老项目连OPC服务都没装。这时候最直接的办法就是用FINS协议自己去收数据。FINS本身不复杂,但帧组装、地址换算、返回码判断这些细节非常琐碎,自己从头写一遍既费时间又容易在边界条件上翻车。这份欧姆龙FINS协议动态库把UDP/TCP通信、帧封装、命令收发、错误码解析都封装好了,C#、Python、C++都可以直接调。适合做数据采集、设备联网、MES对接的软件工程师,也适合刚接手欧姆龙PLC通信项目、不想把时间耗在协议细节上的新人。
2. 先弄清FINS帧怎么拼:协议结构、选型理由和库的封装边界
2.1 UDP还是TCP:工业现场该怎么选
FINS协议在以太网上有两种跑法,FINS/UDP默认端口9600,FINS/TCP也走9600端口。实际项目里我看到的情况是,中小型产线、单机通信绝大多数用UDP,因为不需要建立连接,PLC侧配置简单,上位机只管发包收包,断了重发就行。FINS/TCP则适合跨网段、需要稳定长连接的场景,但它多了一套TCP握手和三次握手维护的逻辑,万一网络抖动,TCP连接断开的恢复倒比UDP麻烦。
如果你这个项目PLC数量少、点位采集频率不高,我建议优先用UDP模式。动态库里两种协议都做了封装,通过一个协议类型参数切换,底层对调用方透明。
| 对比项 | FINS/UDP | FINS/TCP |
|---|---|---|
| 端口 | 9600 | 9600 |
| 连接管理 | 无,发包即发 | 需建立套接字连接 |
| 可靠性 | 依赖上层重试 | TCP保证传输 |
| 典型场景 | 单台PLC、本网段 | 跨网段、长连接 |
| 配置复杂度 | 低 | 中 |
2.2 FINS帧结构:十个固定字节和两个命令字节
FINS请求帧由10个字节的FINS头和后面的命令码、参数、数据组成。头10字节分别是ICF、RSV、GCT、DNA、DA1、DA2、SNA、SA1、SA2、SID,含义如下:
| 字节 | 名称 | 说明 |
|---|---|---|
| 1 | ICF | 帧类型标志,0x80表示需要响应 |
| 2 | RSV | 保留字节,固定0x00 |
| 3 | GCT | 网关计数,通常0x02,表示允许网关转发 |
| 4 | DNA | 目标网络号,本地网络设0x00 |
| 5 | DA1 | 目标节点号,即PLC的FINS节点地址,范围1~254 |
| 6 | DA2 | 目标单元号,0x00表示CPU单元 |
| 7 | SNA | 源网络号,本地网络设0x00 |
| 8 | SA1 | 源节点号,即上位机的FINS节点地址 |
| 9 | SA2 | 源单元号,上位机一般0x00 |
| 10 | SID | 服务标识,每次请求换一个值,用于匹配响应 |
连接后的数据是命令码MRC和MRS各1字节。比如读字数据,MRC=0x01、MRS=0x01;写字数据,MRC=0x01、MRS=0x02。紧跟着的是参数区,包括要访问的内存区代码、起始地址、读多少个数据、是否带掩码等。
很多同学第一次抓包看到这一串00 00 00就懵了,其实只要把每个字段的职责拆开,帧结构是很好记的。动态库里这些字段全部由API参数生成,你不用自己拼,但接口文档里会暴露节点号和SID的设置,这两项搞错最容易出问题。
2.3 地址与字节序:DM区地址换算最容易错的一步
FINS访问PLC内存区,靠的是“内存区代码+两个字节的地址”。最常见的DM区代码是0x82,后面跟两个字节的地址,高位在前。比如要访问DM100,地址就是0x0064,实际字节序是0x00 0x64。如果你习惯Modbus那套寄存器编号,在这里特别容易把十进制地址直接塞进去。
位访问是另一个坑。FINS里读写位地址时,地址计算方式是“字地址×16+位号”。比如CIO区的W100.05,实际地址是100×16+5=1605,十六进制是0x0645。很多新手直接传100进去,读出来的是W100.00的数据,对不上号还以为协议有问题。
字节序则是第三个高频翻车点。FINS默认是高位在前的,也就是大端序。你读出来一个16位数值,如果上位机按小端解析,高低字节就是反的。这份动态库处理I/O数据时内部统一按FINS的字节序收发,调用方不用调整,除非你在配置里显式改成小端兼容模式。我一般建议不要改,保持和协议一致最稳妥。
2.4 动态库把哪几块封装掉了
看完帧结构,你就知道为什么值得直接用一个封装好的动态库。库内部主要做了四件事:一是把上面那10个字节的FINS头根据配置拼好;二是把内存区代码和地址换算成协议要求的字节序;三是把读写请求的响应解析成结构体,返回码、实际数据都拆开;四是处理超时重试和通信状态管理。调用方要关心的只剩连接参数、内存区类型、起始地址和长度。
这份资源的源码包里带的动态库导出接口大致是这样一组:初始化连接、设置PLC节点号、读取字数据、写入字数据、读取位数据、写入位数据、获取最后错误信息。每个函数入参都带有地址类型枚举和长度字段,返回值统一是0成功、非0错误码。接下来我直接演示怎么用。
3. 第一次调用:初始化、读DM区、写线圈
3.1 连接配置:节点号、IP和超时
用库之前先做三件事:在PLC的以太网设置里确认FINS UDP服务端口是9600;记下PLC的FINS节点号,很多工程默认是1或2;给你的上位机软件安排一个还没被占用的节点号,比如0x20(十进制32)。
动态库初始化需要四个参数:PLC的IP地址、PLC的节点号、本机节点号、超时毫秒数。超时建议先给2000ms,调试通了再往下调。以下以C#调用为例:
// 引用动态库接口 [DllImport("FinsLib.dll", CallingConvention = CallingConvention.Cdecl)] static extern int FINS_Init(string ip, byte nodeNo, byte localNode, int timeoutMs); // 初始化连接 int ret = FINS_Init("192.168.1.10", 1, 32, 2000); if (ret != 0) { // 查错:IP不通、节点号冲突和网段不对是前三名 Console.WriteLine("Init failed: " + ret); return; }逻辑说明:初始化只是记录配置并打开发送通道,并不会像TCP那样立刻建立连接,真正的通信发生在读写调用时,UDP发出去才知道通不通。参数里超时指的是“发出请求后等待响应”的最长时间,超过就判定超时。
3.2 连续读DM区的完整示例
下面演示读PLC的DM100到DM109,共10个字,这是采集现场温度、压力、计数之类的典型场景。命令是0101,内存区代码0x82,起始地址0x0064,数目0x000A:
// 准备缓冲区,FINS_ReadWord 按ushort数组返回 ushort[] data = new ushort[10]; int err = FINS_ReadWord(0x82, 100, 10, data); if (err != 0) { Console.WriteLine("Read error: " + FINS_GetLastError()); return; } for (int i = 0; i < 10; i++) { Console.WriteLine("DM" + (100 + i) + " = " + data[i]); }参数说明:第一个参数0x82是DM区代码;第二个参数100是十进制起始地址,库内部会转成0x0064;第三个参数是连续读取的字数;数据放进ushort数组。读回来的数值已经是按FINS字节序拼好的,直接使用即可。如果读出来负数或者诡异的大数,多半是PLC里存的是有符号整数或浮点,需要按地址的数据类型再做转换。
3.3 写入与按位控制:把输出点拉起来
写数据和读数据对称,命令是0102。假设要向DM200写入一个16位的设定值,比如进料速度,代码这样写:
ushort setValue = 1200; int wErr = FINS_WriteWord(0x82, 200, setValue); if (wErr != 0) { Console.WriteLine("Write error: " + FINS_GetLastError()); }按位控制输出点要小心。很多设备启停信号在CIO区的位地址上,读写位用的是0101+位访问模式,内存区代码和字访问不同。库里的FINS_WriteBit接口要求传入字地址和位号,内部自动完成乘16换算:
// CIO 100.05 输出点置ON bool state = true; int bitErr = FINS_WriteBit(0xB0, 100, 5, state);逻辑说明:0xB0是CIO区代码,100是字地址,5是位号。写位操作对PLC的扫描周期很敏感,如果写入后立刻又写同一个地址,建议在两次写入之间加至少一个PLC扫描周期的间隔,否则第二个请求可能被当成重复操作丢弃。
3.4 返回码怎么查:先看主码再看副码
FINS响应帧里返回码也是两字节,主码在前副码在后。最常见的几个:0x0104表示内存区代码错误,说明你传的区代码和这个PLC型号不匹配;0x0105表示地址不在范围,地址换算越界了;0x1001一般是通信配置错误,节点号或网络号没对上。库接口FINS_GetLastError会返回拼接后的错误码字符串,你在代码里可以直接打出来。判断错误时不要只盯着第一个字节,主码相同副码不同,定位的问题完全不同。
4. 把库接进业务程序:轮询周期、批量读和线程模型
4.1 轮询周期与扫描时间的匹配
上位机采数据,轮询周期不是越短越好。FINS请求到了PLC,要等PLC的程序扫描到相应位置才会响应,一个扫描周期10ms到50ms不等。如果上位机每隔50ms发一堆读请求,PLC的响应就会明显排队,表现是请求总是超时。
我一般把轮询周期定在200ms以上,具体按“PLC扫描周期×2+网络抖动余量”来算。假如扫描周期20ms,乘以2再留足余量,100ms够用,但200ms更保险。MES归集数据基本用秒级甚至分钟级,不需要追求极限。
还有一个容易忽略的点:如果PLC程序里对同一区域做频繁的DM区块搬迁、文件存储操作,FINS请求会被CPU任务优先级挤到后面,响应时间不稳定。这时候别盲目调短超时,先观察一下是不是PLC本身忙。
4.2 批量读:别把连续地址拆成单发
很多项目里新人习惯用一个循环,从DM100读到DM200,每次读1个字。这在点位少的时候没问题,点位一多就暴露两个问题:一是大量小包在网络上排队,UDP包数量暴增;二是每个包独立计算超时,一次网络抖动会导致一批数据全部超时重读。
正确做法是连续地址尽量合并成一条命令。FINS支持一次最多读取很多个字(具体上限看PLC型号,CP系列一般可以一次读几百个字),把100个连续地址读出来放到数组里,比100次单点读快得多。不连续的地址再分组合并,比如DM100~DM110一组、DM200~DM205一组。库里接口支持传数组长度,内部按长度组装命令参数,不用你拼多条帧。
// 坏做法:循环100次 for (int addr = 100; addr < 200; addr++) { ushort one; FINS_ReadWord(0x82, addr, 1, &one); } // 好做法:一次读完 ushort[] batch = new ushort[100]; FINS_ReadWord(0x82, 100, 100, batch);同样的数据量,后者在网络上的包数量是前者的百分之一,PLC侧的处理中断也少了一个数量级。这一点在后续点位扩容到上千个时尤其明显。
4.3 超时、重试和发送队列
FINS的UDP通信有一个天然特性:丢包不会自己恢复,必须靠上层重试。动态库里通常内置了重试机制,会配置“最大重试次数”。现场调试建议先设3次重试、每次超时2000ms,稳定运行一到两周后再看日志里的重试率,把超时慢慢压到500ms。
还有一点,重试逻辑不要自己写在业务层,不要用循环套for循环去发命令。动态库内部的重试是在请求ID一致的前提下做的,业务层只要发起一次调用,库替你处理重发。如果业务层再手动重发,会遇到同一个请求被执行两次的危险——写操作重复执行在设备控制上可能是大问题。
4.4 多线程访问:一个连接必须串行
FINS协议本身没有多路复用,同一时刻一个通道只能有一个请求在等响应。客户端如果开了四个线程同时读DM区,响应回来时线程之间会拿错数据,表现是读出来的值有时候是上一次请求的。
解决办法是在库外面套一个锁,或者用发送队列。我习惯把动态库封装成一个单例服务,内部用一个Queue维护请求任务,后台单线程循环处理,读写请求进来排队。这样既避免了锁的复杂度,又方便全局控制轮询节奏。如果追求吞吐量,就开多个FINS连接分别配置不同节点号,但PLC侧也要支持多连接,这一点一般小项目用不到。
// 用一个信号量串行化FINS访问 private static readonly SemaphoreSlim finsLock = new SemaphoreSlim(1, 1); async Task<string> ReadWithLockAsync() { await finsLock.WaitAsync(); try { // 这里调用FINS_ReadWord等接口 } finally { finsLock.Release(); } }参数说明:SemaphoreSlim的第二个参数是最大并发数,设为1就是强制串行。注意锁的范围要覆盖一次完整的“请求-响应”,不能只锁请求不锁响应。否则响应结果在锁外共享,还是会串数据。
5. FINS联调避坑清单:五个把新人卡住的现场问题
5.1 请求发出去PLC不响应,返回超时
现象:调用FINS_ReadWord不报错,就是超时,程序卡在等待响应的位置。 原因:最常见的是UDP端口不通。PLC默认FINS服务在9600端口,但如果PLC程序初始化过程中覆盖了通信设置,或者有别的上位机软件占用了本地9600端口,请求就发不出去。 解决:先用命令行ping通PLC的IP;再检查本地端口占用,Windows下用netstat -ano | findstr 9600;最后确认PLC的以太网配置里FINS服务没有被动关闭。
5.2 返回错误码0x1001/0x100F:先查节点号,不要改代码
现象:刚连上的时候读写正常,重启上位机或切换了PLC之后,报0x1001或0x100F。 原因:FINS头里的DA1(目标节点)和SA1(源节点)配置不对。很多PLC工程里节点号是1,上位机源节点号如果也配成1,就冲突了。 解决:打开PLC的系统设置,查看FINS节点地址,把它填到库的PLC节点号参数里;上位机源节点故意避开1,用32或其他空闲值。改完参数重新初始化即可,不用动代码逻辑。
5.3 数据读出来了但数值对不上:字节序和大端问题
现象:读取一个整型数值,PLC里监控是256,上位机读出来变成1,或者反过来。 原因:FINS按大端传输数据,比如整数0x0100,线上是先01后00。如果上位机按小端解析成ushort,0x0100会被读成0x0001。 解决:库内部默认已处理好字节序,前提是你走的读字接口返回的是ushort数组。如果你拿到的是原始byte数组自己拼,注意要把高字节放前面。自己拼数据时先打印十六进制确认,不要直接按移位拼。
5.4 找半天居然没有CRC:FINS没有校验字段
现象:有人拿着Modbus的经验分析FINS报文,满帧找CRC校验位,找半天没有,甚至怀疑抓包工具漏了字段。 原因:FINS协议本身不带有应用层校验字段,它依赖下层的UDP校验或TCP校验来保证传输完整性。 解决:不要再按Modbus的思维去补加校验。如果你对可靠性要求高,就在应用层给业务数据加时间戳和序号,收到时判断是否过期,而不是去协议里找校验位。动态库不会额外生成任何校验字节,这是设计如此。
5.5 写成功了但PLC里没变化:地址和软元件没对上
现象:FINS_WriteBit返回成功,但PLC程序里的输出点没动作,监控上看还是OFF。 原因:大概率地址用错了。编程软件里看到的“100.05”不一定对应FINS访问的字地址100位5。不同系列PLC的软元件编号和FINS内存区映射存在偏移,比如有的型号里CIO区的实际起始地址要加一个固定偏移量。 解决:在编程软件帮助里查“FINS内存映射表”,确认你用的区代码和地址范围。对不上的情况,把CIO字地址按映射表做加法换算,库接口入参传换算后的值。写线圈这种动作完成后,去PLC的监视画面确认,不要只在代码里看返回码。
6. 调试FINS最有用的一招:把请求响应打成日志
6.1 在调用层记录完整的回显
联调FINS通信最怕的是“黑匣子状态”:数据错了不知道错在请求还是响应。我拿到动态库先做的第一件事不是看功能,而是封装一个带日志的访问层。每次请求记录发送的节点号、内存区代码、起始地址、长度;每次响应记录错误码、数据前几个字的十六进制。
void LogFinsCall(string action, ushort[] data, int error) { StringBuilder sb = new StringBuilder(); sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); sb.Append(" | ").Append(action); sb.Append(" | err=").Append(error); if (data != null && data.Length > 0) { sb.Append(" | first=").Append(data[0].ToString("X4")); } File.AppendAllText("fins.log", sb.ToString() + Environment.NewLine); }参数说明:日志记录三个关键点:发生时间精确到毫秒,方便和PLC日志对时间戳;错误码直接记返回的十进制或十六进制;数据只要前几个字的十六进制就够定位字节序问题了。不要全量输出,几千个点位时会刷爆日志。
6.2 抓包验证帧序和SID
日志只能说明应用层的行为,网络层对不对还得靠抓包工具。抓FINS包要注意过滤条件。UDP模式抓包过滤写udp.port == 9600就行;TCP模式写tcp.port == 9600。抓包时留意一个关键字段:FINS头的SID。正常调试中,每个请求帧的SID和对应的响应帧SID必须一致,如果发现响应SID和最近发出的请求对不上,说明有别的连接在跟你抢会话,或者有重播。
响应帧的源端口在UDP模式下和请求帧的目的端口相同,都是9600;但有些PLC型号回应时源端口会变,抓包时别惊讶。
另外,不要开着Wireshark去反复触发大量读写,抓包文件会迅速膨胀。每轮调试只录一个场景,比如“只读DM100一次”,截两三个包就够了,分析完立刻停抓。
多层的经验总结下来,我现在每次接新的欧姆龙PLC项目,都会强制自己走一遍这个流程:先查PLC节点号并核对内存映射表,再抓包确认帧头和SID,然后把带日志的访问层跑通,最后才调业务逻辑。这套顺序帮我避开了绝大多数低级翻车。希望这次讲到的协议细节和踩坑记录,能让你省掉好几个加班的晚上。
这份资源包里带了可直接调用的动态库、示例工程和通信调试文档,照着上面的步骤改一下IP和节点号就能跑起来。
本文还有配套的精品资源,点击获取