TwinCAT 3 Modbus TCP主从站实战:寄存器映射、字序与轮询

📅 发布时间:2026/9/20 6:33:56
TwinCAT 3 Modbus TCP主从站实战:寄存器映射、字序与轮询
简介面向使用倍福 TwinCAT 3 进行 Modbus TCP 通讯的自动化工程师与设备调试人员这份技术文档围绕实际部署中容易忽略的环节展开解决从安装、地址映射到跨系统配置的常见问题。内容按 WES7 与 CE 系统分别说明 TF6250 的安装路径与差异并指出 TC3 默认地址仅映射到 M 区若需读写 I 区、Q 区变量则要修改或替换 ModbusTCP Server 配置文件同时附上与 Tc2 默认映射地址一致的 32 位、64 位操作系统配置及说明便于旧项目迁移时保持地址对应关系。压缩包仅含 1 个 docx 文件约 485KB轻量易读适合边调试边查阅。该文档已有 311 人学习可帮助读者快速理清 TcModbusSrvCfg.exe 与 TcModbusSrv.xml 两类配置方式的区别掌握重启生效、端口设置以及跨系统替换文件等排错思路减少现场联调时间。1. 从“能 ping 通却读不到寄存器”说起Tc3 里 Modbus TCP 到底卡在哪现场最常见的画面是这样的上位机 ping 得通Telnet 打 502 端口也有响应抓包能看到 TCP 三次握手可 Tc3 这边的功能块就是bError一直置位或者读回来一整串 0。协议栈明明通了人就开始怀疑交换机、怀疑网线、怀疑对面设备的说明书印错了——最后一查八成是 Tc3 这边三件小事没对齐谁在什么时候触发请求、指针指向了哪一段内存、收到的一串 WORD 到底按什么字序解释。Tc3 走 Modbus TCP 有两条相对独立的路。一条是把控制器本身做成从站交给专门的 Modbus TCP Slave 授权去被动应答外部主站另一条是让 PLC 自己当主站靠 Tc2_ModbusTCP 库里的功能块主动去读别人的寄存器。两条路的坑不一样但都会踩到端口占用、过程映像链接、字序换算这几块。这篇按“先分清角色、再写主站、接着做从站、最后调多从站轮询”的顺序往下走代码和参数给到能照着敲的程度。适合第一次在 Tc3 上接 Modbus 设备的人也适合已经接通、但发现浮点数对不上、多从站轮询丢包的工程师。2. Tc3 里 Modbus TCP 的角色分工从站授权库与主站函数库怎么选2.1 Modbus TCP Slave 授权与 Tc2_ModbusTCP 库的边界在 Tc3 的工程里跟 Modbus TCP 打交道第一件事是想清楚“数据往哪个方向流”。从站侧常见的做法是加载 Modbus TCP Slave 功能工程里经常能见到的就是 TF6250 这个型号。它会在 I/O 树下挂出一个从站设备配置界面里直接填寄存器区的起始偏移和数量剩下的报文解析、异常码返回全交给运行时去干。这条路的好处是 PLC 代码干净不需要自己写协议状态机代价是寄存器区的划分和过程映像链接必须在工程里一次做对任何一处对不上外部主站读到的就是零。主站侧走的是 Tc2_ModbusTCP 库。库里核心就是 Client 和 Server 两个功能块PLC 里声明一个实例按周期喂输入、看输出即可不需要额外的从站授权。缺点是连接管理得自己写什么时候触发、超时之后重不重连、多台从站怎么排队全在 PLC 代码里体现。选型我一般按“谁是数据源头”来分Tc3 是采集方就走主站Tc3 是数据提供方才考虑做从站。两边都要当也可以同时开但要注意它们会争抢同一个监听端口需要靠不同的网卡或不同的绑定地址分开别指望在一个 IP 上既监听 502 又主动去连别人的 502。2.2 变量声明与过程映像的链接方式主站侧对变量没有特殊要求普通 PLC 数组和结构体就能用传地址靠ADR()传长度靠SIZEOF()。从站侧完全是另一回事。Slave 设备在 I/O 树下会生成一组过程映像地址你需要把 PLC 里的全局变量逐个链接过去。我的习惯是先把要暴露的数据整理成连续的数组或结构体链接时整块对整块避免一个一个点、点到后面漏了一个寄存器对方读到的后半段永远是 0。场景声明方式链接方式备注主站读远程PLC 普通数组ADR(数组)传指针缓冲区长度要大于等于请求长度主站写远程PLC 普通数组ADR(数组)传指针注意对端寄存器上限从站被读全局数组或结构体与 Slave 的输入映像链接地址对齐别跨寄存器区从站被写全局数组或结构体与 Slave 的输出映像链接解析逻辑放在应用代码里有个细节容易被忽略从站侧的过程映像链接是按位还是按字、是 BYTE 还是 WORD取决于设备描述里选的寄存器类型。选成 BYTE 类型却按 WORD 去链接Tc3 编译时不一定报错但地址会整体偏移一倍外部读到的数据全部错位。2.3 502 端口、网卡绑定与 ADS 路由Modbus TCP 的标准端口是 502。在 Windows 上监听 502 通常需要管理员权限而且系统在部分版本里会把 502 划入保留端口范围普通账号直接绑定会失败。如果 Tc3 运行时的服务账号没有足够权限Slave 设备能配置成功、状态却是“未监听”抓包只看到 RST。双网卡机器上还有一层必须明确指定 Modbus 走哪张网卡。常见做法是给业务网和办公网各配一个网段把 Modbus 从站设备绑定到业务网那张卡的 IP 上别让它自动选。提示Tc3 的 ADS 路由和 Modbus 是两个完全不同的通道。ADS 能连上工程不意味着 Modbus 端口是通的反之亦然。排查时别用“能不能下载程序”来判断 Modbus 是否正常。最后是防火墙。Windows 防火墙默认会拦 502 的入站连接测试阶段可以先关掉或用一条入站规则放行正式部署时改成只放行指定本地 IP 和远端 IP 的组合比全放开要稳妥得多。3. Tc3 侧手写 Modbus TCP 主站函数块调用与状态机3.1 声明实例与收发缓冲区先看变量声明。下面这段是能直接放进 MAIN 或某个 PRG 里的骨架实际工程中按你的设备数量做多实例即可。VAR fbClient : ModbusTCP_Client; // 主站功能块实例 eStep : INT : 0; // 状态机步号 bStart : BOOL; // 外部启动触发 sIpAddr : STRING(15) : 192.168.1.10; nPort : UINT : 502; // 标准 Modbus TCP 端口 nUnitId : UINT : 1; // 网关后区分从站用直连多为 1 nQty : UINT : 10; // 要读的寄存器个数 aRxBuf : ARRAY[0..9] OF WORD; // 接收缓冲区容量要 nQty nErrId : UDINT; // 错误码留存方便诊断 tTimeout : TIME : T#3S; // 单次请求超时 END_VARaRxBuf的长度是所有坑里最要命的一个。请求 10 个寄存器缓冲区只声明了 8 个函数块写越界之后可能不报错、也可能直接把别的变量踩掉现象就变成“读回来的值偶尔会跳”。缓冲区的元素类型用 WORD 还是 BYTE决定了长度按 2 字节还是 1 字节算这一点务必跟库的手册对齐。3.2 用状态机驱动读写请求Modbus TCP 是请求-应答模型一次只能有一个未完成的请求。用一个简单的状态机来串最稳。CASE eStep OF 0: // 空闲等待 IF bStart THEN eStep : 10; END_IF 10: // 触发一次请求先给一个下降沿 fbClient(bExecute : FALSE); eStep : 20; 20: // 上升沿触发参数一次性喂进去 fbClient( bExecute : TRUE, sIPAddr : sIpAddr, nTCPPort : nPort, nUnitID : nUnitId, cbLength : SIZEOF(aRxBuf), pAddr : ADR(aRxBuf), tTimeout : tTimeout ); eStep : 30; 30: // 轮询执行状态 fbClient(); IF NOT fbClient.bBusy THEN IF fbClient.bError THEN nErrId : fbClient.nErrID; // 留档 eStep : 90; ELSE eStep : 100; END_IF END_IF 90: // 出错处理退避后重试 fbClient(bExecute : FALSE); eStep : 0; 100: // 成功把 aRxBuf 交给业务逻辑 eStep : 0; END_CASE逻辑上分三步走先复位bExecute制造下降沿再置位制造上升沿触发请求最后靠bBusy判断请求是否结束。bBusy变为 FALSE 之后才去看bError顺序不能反——bBusy还是 TRUE 的时候bError可能还没定提前读会读到上一次请求的残留值这也是“报错一闪就消失”这类诡异现象的来源。状态机每周期都要被调用一次不能放在某个条件分支里面。有工程师图省事把fbClient()塞进IF bStart THEN里结果bBusy永远停在 TRUE因为没人去推进它。3.3 几个必调参数与取值范围参数常用值说明nTCPPort502少数设备改过端口接之前先确认nUnitID1直连设备多为 0、1 或 255网关后必须区分cbLengthSIZEOF(缓冲区)单次读寄存器一般不超过 125 个tTimeoutT#1ST#5S设太短会误判断线设太长会拖住轮询调用周期10ms50ms必须小于设备响应时间不然会堆积请求单次读寄存器的数量有一条硬性上限读保持寄存器一次最多 125 个写一次最多 123 个。超过之后设备会返回异常码而不是自动分批。要读更多就自己拆成多次请求中间让状态机回到空闲再发起下一轮。3.4 报错时的排查顺序按层往下剥比瞎试快得多。第一步看 TCP 层。抓包确认有没有三次握手如果连 SYN 都被 RST就是端口、防火墙或网卡绑定问题跟 Modbus 一点关系都没有。第二步看单元号。TCP 通了但设备返回异常码多半是nUnitID填错。网关后面挂多台设备时单元号必须跟物理设备对应上。第三步看寄存器地址。地址是 0 基还是 1 基、是保持寄存器还是输入寄存器这两件事每次接新设备都要重新确认一遍没有通用答案。第四步才轮到字节序。数据能读回来但数值不对特别是浮点数和 32 位整数基本就是字序问题。4. 把 Tc3 做成 Modbus TCP 从站寄存器映射与数据类型对齐4.1 四个寄存器区的划分与地址起点Modbus 传统上分四个区线圈、离散输入、输入寄存器、保持寄存器。到了 Modbus TCP 这一层报文里其实只体现功能码加偏移量四个区的概念是靠功能码隐式表达的。区名功能码典型访问Tc3 侧常见映射线圈0x01 / 0x05 / 0x0F读写位BOOL 数组离散输入0x02只读位BOOL 数组输入寄存器0x04只读字WORD 数组保持寄存器0x03 / 0x06 / 0x10读写字WORD 数组在 Tc3 的从站设备配置里填的是每个区的起始偏移和数量。这里最容易出问题的地方是“偏移起点”有些上位机界面上把保持寄存器显示成40001开头实际上发到线上的偏移量是 0也有些配置工具是按 1 基显示。两套习惯混用就会整体错一位现象是“所有数据都往后挪了一个寄存器”。4.2 位、字、双字、浮点的字节序与字序这是最耗时间的一块。Modbus 协议本身规定的数据传输单位是 16 位寄存器一个 32 位浮点要占两个连续寄存器。问题在于这两个寄存器的先后顺序协议没有强制规定各家设备自己定。常见的四种组合高字在前、字内大端很多 PLC 默认低字在前、字内大端高字在前、字内小端低字在前、字内小端x86 上直接内存转出来的形式拼浮点的常见做法是用联合体把 DWORD 拆成两个 WORD再按需要的顺序写进数组TYPE U_DWordToWords : UNION dwValue : DWORD; awWords : ARRAY[0..1] OF WORD; END_UNION END_TYPE VAR uConv : U_DWordToWords; fValue : REAL : 3.14159; awRegs : ARRAY[0..1] OF WORD; END_VAR uConv.dwValue : REAL_TO_DWORD(fValue); awRegs[0] : uConv.awWords[1]; // 高字在前 awRegs[1] : uConv.awWords[0]; // 低字在后这段代码把REAL先转成DWORD再用联合体取出两个 WORD然后手动决定哪个在前。切换字序只需要交换这两行的赋值顺序改完下载下去看上位机显示正不正常即可。浮点数值明显偏大或偏小到离谱比如 3.14 变成 1.07e9 之类就是字序反了。4.3 跟第三方上位机对地址时的换算不同上位机对地址的描述方式差别不小。威纶通触摸屏里配置 Modbus TCP 设备时元件的地址编号通常带一个字母前缀跟寄存器区一一对应选错前缀就会出现“能连上但读不到”。新建工程时设备类型选对、地址前缀跟 Tc3 从站里配置的区对上是通的第一步。Kingscada 这类组态软件在变量定义界面里一般会给出“寄存器类型 地址”两栏这里的地址是十进制的偏移量跟 Tc3 配置界面里的偏移量是同一个数不需要再加 1。汇川 AM 系列做 Modbus TCP 服务端编程时映射表是显式列出来的跟 Tc3 从站的映射表对照着看把“PLC 变量 → 寄存器偏移”这条链在两个工程里各走一遍比对着报文猜要快。注意接到第三方设备时先让对方给一份寄存器表明确每个地址的数据类型和字序。没有这份表就动手后面改起来动一个地址就要动一片。5. 多从站轮询与抓包验证把 Modbus TCP 调稳的几条技巧一台 Tc3 主站下面挂四五台从站是很常见的配置。这时候单靠一个状态机就不够了得加一层调度。我的做法是给每台从站各配一个状态机实例再用一个轮询计数器决定当前把总线交给谁。核心是“同一时刻只有一个在跑”当前实例的eStep回到 0 之后计数器加一切到下一台。切换时给bExecute一个下降沿避免上一台的残留状态影响下一台。// 轮询调度骨架 IF aFb[ nIdx ].eStep 0 THEN nIdx : nIdx 1; IF nIdx nDeviceCount - 1 THEN nIdx : 0; END_IF END_IF aFb[ nIdx ](); // 只推进当前这一台如果某台从站响应特别慢整个轮询周期会被它拖住。稳妥一点的做法是给每台从站设置各自的tTimeout超时之后就把它标记为“本轮跳过”先推进其他设备等下一轮再试。这样一台设备掉线不会让整条链路停摆。抓包是验证阶段最有效的工具。过滤条件就写tcp.port 502看三件事第一请求和应答是否一一对应。有请求没应答是设备侧问题有明显重复的请求是状态机没等应答就发了下一次。第二应答里的字节数是否跟请求的寄存器数量匹配。数量不对说明地址或长度配置有偏差。第三异常应答的功能码。功能码最高位置 1 表示异常后面跟一个异常码常见的是地址越界和数量越界看到这两个码直接回头查配置表就行。最后一条技巧跟缓冲区有关。多从站场景下每个实例都有自己的接收缓冲区声明的时候要按最大请求量留够不要几台从站共用同一个数组——看起来省内存实际上一台从站的数据会覆盖另一台的而且错得很随机很难复现。统一给每台从站独立声明一个ARRAY[0..124] OF WORD的缓冲区占不了多少内存排查的时候能省下大量时间。本文还有配套的精品资源点击获取