1. Fins 协议在欧姆龙 PLC 通讯中的报文结构与实现思路
Fins 是欧姆龙 PLC 上使用最广的通讯协议,全称 Factory Interface Network Service,跑在 TCP 或 UDP 之上,用来读写 DM 区、CIO 区、WR 区等寄存器。很多做上位机的朋友第一次接触欧姆龙,会以为它和三菱 MC 差不多,抓个包拼个字符串就能通,结果发现 Fins 的报文分了好几层,头部、控制段、命令段各管一摊,少一个字节 PLC 就直接不回复。这篇就围绕 Fins 欧姆龙通讯协议实现源代码,把报文结构拆开讲清楚,再给一份能直接复制的读写配置,让你一次跑通寄存器读写链路。
先说清楚它适合谁:如果你在做工控上位机、MES 采集、设备联网网关,需要在一台工控机上同时对接几十上百台欧姆龙 PLC,那 Fins 的 TCP 长连接加多线程轮询就是绕不开的基本功。Fins 能做什么?它能按地址读写位和字,支持 DM、CIO、WR、HR 等区域,单次可批量读写连续寄存器,配合 TCP 的 FINS/TCP 头还能做节点寻址。我试过在 Atom E3940 这种低功耗平台上持续读写,CPU 占用能压到 1% 以内,关键就在于报文拼装要一次到位、接收要按长度收全,而不是靠 sleep 硬等。
Fins 的报文结构可以分成三段来看。第一段是 FINS/TCP 头,固定 16 字节,前 4 字节是魔数FINS,接着 4 字节是长度,再 4 字节是命令码,最后 4 字节是错误码。命令码里0x00000000是连接请求,0x00000002是数据收发。第二段是 FINS 控制段,固定 10 字节,包含 ICF、RSV、GCT、DNA、DA1、DA2、SNA、SA1、SA2、SID,其中 DA1 是目标节点号,SA1 是源节点号,命令读写时 CMD1/CMD2 会跟在控制段后面。第三段才是真正的命令数据段,读内存区是0x0101,写内存区是0x0102,后面跟区域代码、起始地址、位号、长度,写操作还要再拼数据。
很多人卡住的地方在于:FINS/TCP 头里的长度字段到底算哪一段。实测下来,这个长度是从 FINS/TCP 头之后开始算的,也就是控制段加命令段的总字节数,不含那 16 字节头本身。如果你把 16 字节也算进去,PLC 会认为报文长度不对,直接丢弃不回复,表现就是连接上了但读不到数据。另一个坑是节点号,连接请求响应里会带回客户端节点地址,后续读写要用这个节点号填 SA1,填 0 有时候能通,但多设备场景下会串。
下面这段是 Fins 读写内存区的核心实现,读操作先拼 FINS/TCP 头,再拼控制段,最后拼内存区读命令,发送后按长度收全再解析:
long CFinsHandle::ReadMemoryData(int nAddr, int nSize, FINS_DATA_TYPE::ENUM nType, __int16* pData) { long nCode = 0; if (nCode = EstablishCommunicationByFins()) { return nCode; } CMyString pSendData; int nAllSize = FINS_TCP_HEAD_SIZE + FINS_CONTROL_HEAD_SIZE + FINS_MEMORY_AREA_READ_SIZE; pSendData.SetSize(nAllSize); char* pBuff = pSendData.GetString(); FINS_TCP_HEAD pHead; pHead.nCommand = FINS_TCP_CMD_DATA; pHead.SetLength(nAllSize - FINS_TCP_HEAD_SIZE); pHead.GetData(pBuff); FINS_CONTROL_HEAD pControlHead; pControlHead.nDA1 = 0; pControlHead.nSA1 = m_nIpNode; pControlHead.nCmd1 = 0x01; pControlHead.nCmd2 = 0x01; pControlHead.GetData(pBuff + FINS_TCP_HEAD_SIZE); FINS_MEMORY_AREA_READ pMemory; pMemory.nAreaCode = nType; pMemory.nAddr = nAddr; pMemory.nBitNo = 0; pMemory.nLength = nSize; pMemory.GetData(pBuff + FINS_TCP_HEAD_SIZE + FINS_CONTROL_HEAD_SIZE); CMyString pRecvData; nCode = SendSyncData(pSendData, pRecvData); if (nCode == 0) { if (nCode = CheckReplyDataIsError(pRecvData.GetString(), pRecvData.Size())) { return nCode; } FINS_TCP_HEAD pHeadReply; pHeadReply.SetData(pRecvData.GetString()); int nMinSize = FINS_TCP_HEAD_SIZE + FINS_CONTROL_HEAD_SIZE + FINS_MEMORY_AREA_READ_FIX_R_SIZE + nSize * 2; if (pHeadReply.GetLength() < nMinSize) { return FINS_REPLY_READ_DATA_TOO_SHORT; } FINS_MEMORY_AREA_READ_REPLY pReplyData; pReplyData.SetData(pRecvData.GetString(), pRecvData.Size()); if (pReplyData.nEndCode != 0) { return FINS_REPLY_READ_DATA_FAIL; } int nReadByte = nSize * 2; if (pReplyData.GetDataBytsSize() == nReadByte) { memcpy(pData, pReplyData.GetData(), nReadByte); } else { return FINS_REPLY_READ_DATA_TOO_SHORT; } } return nCode; }写操作和读操作结构几乎一样,区别在命令码nCmd2 = 0x02,并且要在命令段后面追加要写入的数据,长度字段也要把数据字节算进去。写 DM 区 100 开始的 2 个字,数据段就是区域代码加地址加位号加长度,再跟 4 字节数据。这里有个细节:Fins 的字数据是大端序,__int16直接 memcpy 到报文里在 x86 上会反,需要做字节序转换,否则写进去的值会变成高低字节颠倒。
接收侧的处理同样关键。Fins 是流式 TCP,一次 recv 不一定收全,所以要在 OnDataRecv 里先攒够 16 字节解析 FINS/TCP 头,拿到总长度后再判断是否收全,收全了才触发解析:
void CFinsHandle::OnDataRecv(char* pData, int nSize) { if (nSize > 0) { m_pRecvData.Append(pData, nSize); if (m_pRecvData.Size() >= FINS_TCP_HEAD_SIZE) { FINS_TCP_HEAD pHead; pHead.SetData(m_pRecvData.GetString()); int nAllSize = pHead.GetLength() + FINS_TCP_HEAD_SIZE; if (m_pRecvData.Size() >= nAllSize) { SetRecvComplete(nAllSize); } } } }这套结构跑通之后,读写 DM 区、CIO 区就是换个区域代码的事。DM 区是0x82,CIO 区是0xB0,WR 区是0xB1,HR 区是0xB2。区域代码填错,PLC 会返回非零 EndCode,比如0x1101表示区域代码不支持,0x0201表示地址越界。把这些 EndCode 做成错误码表,排障时能省很多时间。
2. TaoToken 前置:统一 Key 与 API 通道管理多设备接入凭据
工控项目做到后面,麻烦的往往不是协议本身,而是凭据管理。一台工控机对接上百台 PLC,如果每台设备、每个采集服务都各自存一份密钥,改一次密码就要满机器找配置文件,运维成本很高。TaoToken 在这里的角色,是给多设备接入提供一个统一的 Key 和 API 通道,把设备凭据、模型调用、编码 Agent 的接入点收敛到一处管理。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
为什么工控上位机开发会用到它?因为现在的采集网关不只是读寄存器,还要把数据送到上层做分析,或者用编码助手帮你生成协议解析代码。Fins 的报文拼装、字节序转换、错误码映射这些活,用 AI 编码工具辅助能快不少,而 TaoToken 的 Coding Plan 就是给长期编码和 Agent 场景准备的。你可以把它理解成一个统一的接入层:设备侧用一套 Key 管理,模型侧用同一个通道调用,不用在每台机器上分别配。
前置准备分三步。第一步,在 TaoToken 控制台创建一个 API Key,控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建后复制保存,Key 只在创建时完整显示一次。第二步,确认你要用的模型 ID,模型对话页面在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,里面能看到当前可用的模型列表和对应的 Model ID。第三步,如果你要用 Claude Code 这类编码工具,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL 和鉴权头的写法。
这里要强调一个原则:TaoToken 是接入通道,不是替代你的编辑器或 PLC 编程软件。Fins 的源代码还是在你自己的工程里编译,TaoToken 负责的是把 AI 编码能力和设备凭据管理统一起来。多设备场景下,你可以给不同产线、不同项目分配不同的 Key,出问题能按 Key 追溯,不用把所有设备共用一个凭据。
对于长期做编码和 Agent 的朋友,Coding Plan 页面在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它适合需要持续调用、批量生成协议代码的场景。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,可以在这里轮换和吊销 Key。Claude Code 的接入入口在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite ,Anthropic 相关配置在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecodeanthropic&utm_campaign=rewrite 。
把凭据管理前置做好,后面 Fins 读写验证时就不会被环境问题干扰。建议在工控机上用一个独立的配置文件存 Base URL、Key、Model ID,程序启动时读取,而不是硬编码在源码里。这样换设备、换项目只改配置,不动代码。
3. 可复制配置:Fins 命令帧与多设备接入 settings 片段
这一节给可直接复制的配置。先给 Fins 侧的设备参数,再给 TaoToken 侧的接入配置,两边分开存,互不干扰。Fins 设备配置建议用 JSON,每台 PLC 一条记录,包含 IP、端口、节点号、超时和默认区域:
{ "fins_devices": [ { "name": "line1_plc01", "ip": "192.168.1.10", "port": 9600, "node": 1, "timeout_ms": 1000, "default_area": "DM", "poll_interval_ms": 200 }, { "name": "line1_plc02", "ip": "192.168.1.11", "port": 9600, "node": 2, "timeout_ms": 1000, "default_area": "DM", "poll_interval_ms": 200 } ] }Fins 默认端口是 9600,TCP 连接。节点号在连接请求响应里会返回,配置里可以先填一个预期值,程序连上后用响应里的实际节点号覆盖。超时建议 1000ms,太短在网络抖动时会误判,太长会拖慢轮询。轮询间隔 200ms 对大多数采集场景够用,Atom 平台上跑上百台设备也没问题。
TaoToken 侧的接入配置,如果你用的是支持 settings.json 的编码工具,可以这样写。Base URL 用 https://taotoken.net/api ,Key 填你在控制台创建的那串,Model ID 填模型列表里看到的实际值:
{ "api_base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model_id": "你的ModelID", "timeout_ms": 60000, "max_retries": 2 }如果你用的是 TOML 风格的配置,等价写法是:
[taotoken] api_base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model_id = "你的ModelID" timeout_ms = 60000 max_retries = 2三件套必须齐全:Base URL、Key、Model ID。少任何一个,请求都会失败。Base URL 结尾不要多加斜杠,https://taotoken.net/api就是完整前缀,具体路径由客户端拼接。Key 不要提交到代码仓库,用环境变量或本地配置文件加载。
Fins 命令帧这边,给一个读 DM 区 100 开始 2 个字的完整十六进制示例,方便你抓包对照。FINS/TCP 头:46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00,其中46 49 4E 53是 FINS,00 00 00 1A是长度 26,00 00 00 02是数据收发命令。控制段:80 00 02 00 01 00 00 01 00 00,ICF 是 0x80,DA1 是 0x01,SA1 是 0x00。命令段:01 01 82 00 64 00 00 02,01 01是读,82是 DM 区,00 64是地址 100,00 02是读 2 个字。
写命令把命令段改成01 02 82 00 64 00 00 02,后面再跟 4 字节数据。注意长度字段要同步加上数据字节数。这些帧结构对照着代码里的FINS_TCP_HEAD、FINS_CONTROL_HEAD、FINS_MEMORY_AREA_READ三个结构体看,就能明白每个字段落在报文的哪个位置。
多设备接入时,建议每台 PLC 一个独立的 Fins 连接对象,共享同一个 TaoToken 配置。连接对象里维护自己的接收缓冲和节点号,不要跨设备复用,否则接收数据会串。线程模型上,可以用一个 IO 线程管多路连接,也可以用线程池,关键是每个连接的收发要串行化,避免同一连接上并发写导致报文交错。
4. 验证请求:跑通欧姆龙寄存器读写链路
配置就绪后,先做最小验证:连一台 PLC,读 DM100 两个字节,再写回去,确认读到的值和写入值一致。验证顺序建议从连接请求开始,再读,再写,每一步都打印原始报文和返回码,出问题能快速定位。
第一步,建立 Fins 连接。发送连接请求帧,FINS/TCP 头命令码0x00000000,后面跟 4 字节客户端节点信息。响应里命令码应该是0x00000001,错误码为 0,长度字段等于 16 加 8。响应最后 4 字节里第 4 个字节就是客户端节点号,把它存到m_nIpNode,后续读写用。如果这一步失败,先查 IP 和端口,再查 PLC 是否开启了 Fins/TCP 服务。
第二步,读 DM100。用上面的读命令帧,发送后按长度收全。正常响应里 EndCode 是00 00,数据段是 4 字节,对应两个__int16。假设 DM100 当前是 1234,DM101 是 5678,那数据段大端序是04 D2 16 2E。解析时按大端转成主机序,得到 1234 和 5678。如果 EndCode 非零,对照错误码表:0x1101区域代码错,0x0201地址越界,0x0302长度超限。
第三步,写 DM100。把命令段命令码改成01 02,长度改成 2,后面跟 4 字节数据。写入 9999 和 8888,大端序是27 0F 22 B8。发送后响应只有 FINS/TCP 头和 10 字节控制段加 2 字节 EndCode,没有数据段。EndCode 为 0 表示写成功。再读一次 DM100,确认值变成 9999 和 8888,读写链路就通了。
第四步,验证多设备。把配置里第二台 PLC 也连上,两个连接对象分别读各自的 DM100,确认数据不串。这一步能验证节点号和接收缓冲是否隔离正确。如果两台设备读到相同数据,多半是接收缓冲复用了,或者节点号填错导致响应被错误匹配。
验证过程中,建议把每次请求和响应的十六进制打印到日志,格式统一成时间戳加设备名加方向加报文。这样出问题时,抓包和日志能对上。实测下来,Fins 的坑主要集中在长度字段、字节序、节点号这三处,把这三处盯住,读写基本一次通。
如果你用 AI 编码工具辅助生成解析代码,可以把报文结构和错误码表一起喂给它,让它生成对应的结构体解析函数。TaoToken 的模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,适合做这种验证性调用。生成后自己对照报文再核一遍,尤其是长度和字节序,别直接信。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
排障这节按真实报错来。Fins 侧和接入侧的问题分开看,先确认是哪一层。
Fins 侧最常见的现象是连接成功但读不到数据。如果连接请求响应正常,但读命令发出去没响应,先查长度字段。FINS/TCP 头的长度如果不含 16 字节头本身,PLC 会丢弃。对照代码里pHead.SetLength(nAllSize - FINS_TCP_HEAD_SIZE),确认你拼的长度是控制段加命令段。另一个原因是节点号,SA1 填 0 在单设备时可能通,多设备时会串,用响应里返回的节点号。
如果读到了数据但值不对,先查字节序。Fins 字数据是大端,x86 上直接 memcpy 会反。__int16读出来是 0x1234,实际 PLC 里是 0x3412,需要ntohs或手动交换。位数据不用转,但位地址的位号要填对,DM 区按字读写时位号填 0。
接入侧报错里,401 是鉴权失败。先确认 Key 是否正确,有没有多余空格,Base URL 是不是https://taotoken.net/api。Key 创建后只显示一次,如果丢了就重新创建一个。401 也可能是 Key 被吊销,去 API Keys 页面确认状态。
local proxy failed通常是本地网络配置问题。检查工控机的网络设置,确认能正常访问外网,DNS 解析正常。如果工控机在内网隔离环境,需要确认出口策略。这个报错和 Fins 无关,是接入通道的网络层问题,先排除本地网络再查配置。
reading choices一般出现在模型返回解析阶段,表示返回内容里没有预期的选项字段。检查 Model ID 是否填对,模型列表里有的模型不支持某些参数。如果用的是编码工具,确认它请求的接口路径和 Base URL 拼接正确。重试一次,如果持续出现,换一个 Model ID 试。
OAuth 相关报错出现在用 Claude Code 或 Anthropic 接入时。确认接入文档里的鉴权头写法,Base URL 和路径要匹配。OAuth 流程需要回调地址,本地开发时确认回调端口没被占用。如果报 token 过期,重新走一次授权。Claude Code 的接入入口在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite ,Anthropic 配置在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecodeanthropic&utm_campaign=rewrite ,对照文档检查。
还有一个容易忽略的点:Fins 连接建立后如果长时间不通讯,PLC 侧可能会断开。程序里要做心跳或定期读,检测到断开后重连。重连时节点号要重新获取,不能沿用旧值。多设备场景下,每台设备的连接状态独立维护,一台断了不影响其他台。
排障时把日志分级,Fins 报文用 DEBUG,连接状态用 INFO,错误用 ERROR。这样正常运行时日志量可控,出问题时能快速过滤。错误码表建议做成映射,把0x1101这种数字翻译成可读描述,排查时不用翻手册。
6. 继续深入:把 Fins 读写接入统一通道
Fins 的报文结构吃透之后,读写链路本身不复杂,复杂的是多设备、多协议、多项目的凭据和接入管理。把 Fins 连接对象做成可复用的类,每台设备一个实例,共享统一的接入配置,这样新增设备只加一条配置记录,不用改代码。TaoToken 在这里承担的是统一 Key 和 API 通道的角色,设备凭据和模型调用都收敛到一处,换项目时只改配置。
如果你要继续做编码和 Agent 相关的开发,Coding Plan 页面在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合长期调用场景。API Keys 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。模型对话验证在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
最后给一个实用技巧:Fins 的读写函数签名里,地址用字符串传,内部再解析成区域代码加偏移。这样上层调用时写DM100、CIO0.01这种直观地址,解析层负责转换。解析层单独写单元测试,把区域代码、地址范围、位号都覆盖到,比在设备上试错快得多。字节序转换也放在这一层,读写函数只处理主机序数据,边界清晰,后面加新区域类型也容易。