ARTICLE DETAIL

资讯详情

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

OneApiConnect(一) Fins欧姆龙通讯协议实现源代码:从报文结构到可复用配置

OneApiConnect(一) Fins欧姆龙通讯协议实现源代码:从报文结构到可复用配置 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 区是0x82CIO 区是0xB0WR 区是0xB1HR 区是0xB2。区域代码填错PLC 会返回非零 EndCode比如0x1101表示区域代码不支持0x0201表示地址越界。把这些 EndCode 做成错误码表排障时能省很多时间。2. TaoToken 前置统一 Key 与 API 通道管理多设备接入凭据工控项目做到后面麻烦的往往不是协议本身而是凭据管理。一台工控机对接上百台 PLC如果每台设备、每个采集服务都各自存一份密钥改一次密码就要满机器找配置文件运维成本很高。TaoToken 在这里的角色是给多设备接入提供一个统一的 Key 和 API 通道把设备凭据、模型调用、编码 Agent 的接入点收敛到一处管理。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。为什么工控上位机开发会用到它因为现在的采集网关不只是读寄存器还要把数据送到上层做分析或者用编码助手帮你生成协议解析代码。Fins 的报文拼装、字节序转换、错误码映射这些活用 AI 编码工具辅助能快不少而 TaoToken 的 Coding Plan 就是给长期编码和 Agent 场景准备的。你可以把它理解成一个统一的接入层设备侧用一套 Key 管理模型侧用同一个通道调用不用在每台机器上分别配。前置准备分三步。第一步在 TaoToken 控制台创建一个 API Key控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建后复制保存Key 只在创建时完整显示一次。第二步确认你要用的模型 ID模型对话页面在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 里面能看到当前可用的模型列表和对应的 Model ID。第三步如果你要用 Claude Code 这类编码工具接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL 和鉴权头的写法。这里要强调一个原则TaoToken 是接入通道不是替代你的编辑器或 PLC 编程软件。Fins 的源代码还是在你自己的工程里编译TaoToken 负责的是把 AI 编码能力和设备凭据管理统一起来。多设备场景下你可以给不同产线、不同项目分配不同的 Key出问题能按 Key 追溯不用把所有设备共用一个凭据。对于长期做编码和 Agent 的朋友Coding Plan 页面在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它适合需要持续调用、批量生成协议代码的场景。API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以在这里轮换和吊销 Key。Claude Code 的接入入口在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite Anthropic 相关配置在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeanthropicutm_campaignrewrite 。把凭据管理前置做好后面 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 默认端口是 9600TCP 连接。节点号在连接请求响应里会返回配置里可以先填一个预期值程序连上后用响应里的实际节点号覆盖。超时建议 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是 FINS00 00 00 1A是长度 2600 00 00 02是数据收发命令。控制段80 00 02 00 01 00 00 01 00 00ICF 是 0x80DA1 是 0x01SA1 是 0x00。命令段01 01 82 00 64 00 00 0201 01是读82是 DM 区00 64是地址 10000 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 当前是 1234DM101 是 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_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 适合做这种验证性调用。生成后自己对照报文再核一遍尤其是长度和字节序别直接信。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_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite Anthropic 配置在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeanthropicutm_campaignrewrite 对照文档检查。还有一个容易忽略的点Fins 连接建立后如果长时间不通讯PLC 侧可能会断开。程序里要做心跳或定期读检测到断开后重连。重连时节点号要重新获取不能沿用旧值。多设备场景下每台设备的连接状态独立维护一台断了不影响其他台。排障时把日志分级Fins 报文用 DEBUG连接状态用 INFO错误用 ERROR。这样正常运行时日志量可控出问题时能快速过滤。错误码表建议做成映射把0x1101这种数字翻译成可读描述排查时不用翻手册。6. 继续深入把 Fins 读写接入统一通道Fins 的报文结构吃透之后读写链路本身不复杂复杂的是多设备、多协议、多项目的凭据和接入管理。把 Fins 连接对象做成可复用的类每台设备一个实例共享统一的接入配置这样新增设备只加一条配置记录不用改代码。TaoToken 在这里承担的是统一 Key 和 API 通道的角色设备凭据和模型调用都收敛到一处换项目时只改配置。如果你要继续做编码和 Agent 相关的开发Coding Plan 页面在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 适合长期调用场景。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。模型对话验证在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。最后给一个实用技巧Fins 的读写函数签名里地址用字符串传内部再解析成区域代码加偏移。这样上层调用时写DM100、CIO0.01这种直观地址解析层负责转换。解析层单独写单元测试把区域代码、地址范围、位号都覆盖到比在设备上试错快得多。字节序转换也放在这一层读写函数只处理主机序数据边界清晰后面加新区域类型也容易。
返回列表