一句话:让本 PLC 当 Modbus RTU 从站,上位机(DCS / 触摸屏 / 另一台 PLC / 上位软件)
用 FC03 读、FC06/FC16 写本站数据。所有脏活(端口初始化、重试、状态分类、诊断计数)
都封在块里,外面只要给一个硬件标识符。
硬件要求
- S7-1200:CM 1241 RS485(6ES7241-1CH32-0XB0)或 CB 1241 RS485 通信板
- S7-1500:CM PtP RS422/485 系列
- 项目里要有 Modbus RTU 指令库(
Modbus_Comm_Load / Modbus_Slave;老库叫 MB_COMM_LOAD / MB_SLAVE)
一、文件清单
| 文件 | 用途 |
FB_ModbusRtuSlave.scl | 主版本(UTF-8 + BOM)。阅读、留档、后续修改以它为准 |
FB_ModbusRtuSlave_GB2312.scl | 导入博图用这个(GBK / ANSI 编码,中文注释不乱码) |
FB_ModbusRtuSlave_EN.scl | 纯 ASCII 版,怕编码出问题就用它(注释为英文) |
本块不依赖 CRC16 辅助 FC——CRC 校验由库指令内部完成,所以下载包里只有 1 个 FB,导一次就完事。
(对比:自由口版需要自己算 CRC,所以要额外导一个共享 FC。)
二、上手三步
第 1 步:导入
项目树 → 外部源文件 → 添加新的外部源文件 → 选中 .scl → 从源生成块
- 不要先手动新建同名块,会冲突。本文件只含 1 个 FB。
- 中文注释用
_GB2312.scl;怕编码出问题用 _EN.scl。
导入后 30 秒自检(重要):双击生成的 FB,看接口表 ——
Input / Output / InOut / Static 四栏都必须有变量。
如果全是空的、代码区一堆 #xxx 划红线,说明声明段没被解析,
生成的块是残废的,删掉重新来。
(踩过一次:源码里写了老 S7-SCL 的 VAR_CONSTANT ... END_CONST,
TIA 只认 ... END_VAR 结尾,结果接口整个没建起来。现在这份源码已改用 VAR 段放常量,不会再踩。)
第 2 步:确认「非优化访问」(最容易漏的一步)
源码里已写死属性 { S7_Optimized_Access := 'FALSE' },导入后还是再看一眼:
右键 FB → 属性 → 取消勾选「优化的块访问」。
这一步不做,MB_HOLD_REG 会直接报 16#818C,一条都不通。
手册原文:保持寄存器数据块必须支持绝对地址访问,优化访问没有绝对地址。
第 3 步:在 OB1(或循环中断 OB)里无条件调用一次
"FB_ModbusRtuSlave_DB"(
bEnable := TRUE,
uPortHwId := 269, // ← 改成你的模块硬件标识符
uBaud := 9600,
uParity := 2, // Modbus RTU 常用偶校验
uSlaveAddr:= 1 // ← 本站站号
);
调用频率有硬要求:手册规定主站的响应超时内,Modbus_Slave 至少要被执行 2 次。
上位机 RESP_TO 设 1000 ms,本块的调用周期就不能大于 500 ms。
放 OB1 里一般没问题;OB1 周期长(> 200 ms)就丢进循环中断 OB(OB30 之类)。
不能塞进条件分支。必须每周期无条件调用一次,缺调用就等于从站不在线。
三、数据在哪:背景 DB 里的 aHoldReg
导入后打开自动生成的背景 DB(例如 FB_ModbusRtuSlave_DB),里面有个 aHoldReg:
| Modbus 地址 | 背景 DB 里的变量 |
| 40001 | aHoldReg[0] |
| 40002 | aHoldReg[1] |
| …… | …… |
| 41000 | aHoldReg[999] |
PLC 自己读写数据就是直接操作这个数组:
// 主站块采集的第 3、4 个字 → 交给上位机的 40001、40002
"FB_ModbusRtuSlave_DB".aHoldReg[0] := "FB_ModbusRtuCycle_DB".aData[2];
"FB_ModbusRtuSlave_DB".aHoldReg[1] := "FB_ModbusRtuCycle_DB".aData[3];
// 上位机写下来的设定值 → 取出来用
#rSetValue := WORD_TO_INT("FB_ModbusRtuSlave_DB".aHoldReg[10]); // 40011
主站侧一律按 40001 起算,对应下标 0,不是 1。差一位是这个坑的高发区。
FC03 一次最多读 125 个,FC16 一次最多写 123 个;超了从站回异常码。
FC01/05/15 线圈类不进这个数组——那是直接映射到 CPU 的过程映像区(Q0.0 起),与本块无关。
四、引脚速查
输入
| 引脚 | 类型 | 默认 | 说明 |
bEnable | Bool | TRUE | 总使能,FALSE 停止响应(端口配置保持) |
uPortHwId | UInt | 269 | 硬件标识符,设备组态里查,不是槽位号 |
uBaud | UDInt | 9600 | 波特率,只认手册列的那几种 |
uParity | UInt | 0 | 0 无 / 1 奇 / 2 偶 |
uMode485 | USInt | 4 | 端口工作模式,485 二线半双工就是 4,见下节 |
uSlaveAddr | UInt | 1 | 本站站号 1..247(0 是广播地址,不能用) |
uFlowCtrl | UInt | 0 | 流控制,RS485 半双工用 0 |
uRtsOnDly / uRtsOffDly | UInt | 0 | RTS 延时,一般不管 |
uRespTo | UInt | 1000 | 响应超时,对从站影响不大 |
tHostTimeout | Time | T#10s | 这么久没来帧就算上位机掉线 |
tRetry | Time | T#5s | 端口初始化失败后的重试间隔 |
bTrackChange | Bool | TRUE | 追踪哪个寄存器被改写 |
bErrClr | Bool | — | 上升沿:清故障锁存与错误计数 |
bReload | Bool | — | 上升沿:重新初始化端口(改波特率后用它) |
端口 MODE:485 半双工必须写 4(不改就是一帧不通)
MODE 不是 Modbus_Comm_Load 的引脚,是它背景数据块里的 Static 变量。
西门子官方文档(Entry ID 47756141)原文:MODE 默认 0,需「通过起始值」改成 4。
| MODE | 含义 |
| 0 | 全双工 RS232 ← 出厂默认 |
| 1 | 全双工 RS422 四线(点对点) |
| 2 | 全双工 RS422 四线(多点主站) |
| 3 | 全双工 RS422 四线(多点从站) |
| 4 | 半双工 RS485 二线 ← 用 485 就填这个 |
不改的后果:波特率、校验、站号全都对,bReady 也是 TRUE,但一帧都不通,
STATUS 常挂 818B / 80C8。这是 485 调试最隐蔽的一个坑 ——
因为它不会报「参数错」,只是静默不通。
本块的处理:在 FB 内部每拍把 uModeEff 写进 #instLoad.MODE,
且在 REQ 上升沿之前写 —— 指令只在 REQ 触发时才把配置刷进模块,写晚了就白写。
| 配套诊断 | 说明 |
uModeAct | 回读端口实际生效的 MODE。HMI 上放一个显示,看到 4 就安心 |
bModeFault | uMode485 填了 0..4 以外的值,已钳回 4 并持续报警 |
输出 · 健康度
| 引脚 | 说明 |
bReady | 端口就绪,可以通信 |
bNdr | 上位机刚写入新数据(脉冲一拍) |
bDr | 上位机刚读取数据(脉冲一拍) |
bCommBeat | 通信心跳,每成功一帧翻转一次(接 HMI 闪灯) |
bHostLost | 上位机掉线(超过 tHostTimeout 没来过一帧) |
tIdleTime | 距上次成功通信多久了 |
输出 · 故障码与分类(本块的重点)
| 引脚 | 说明 |
bError / wStatus | 当前拍有错 / 实时状态字 |
bErrLatch / wLastErr | 故障锁存 / 锁存的最后错误码(排查看这个) |
uErrSrc | 错误来源:1 端口初始化,2 从站指令 |
bErrCfg | 端口组态类:硬件标识符、波特率、校验 |
bErrHold | 保持区类:指针无效、越界、DB 被优化访问 |
bErrFrame | 帧格式类:CRC 错、字节数不对、功能码不支持 |
bErrPara | 地址 / 长度参数非法 |
bErrBusy | 端口忙(上位机催太急,或两个主站抢线) |
bErrOther | 上面没覆盖的码 |
bLineNoise | 线路干扰:CRC 坏帧占比 > 5% |
bParaFault | 本站站号不在 1..247 |
输出 · 计数器与追踪
| 引脚 | 说明 |
dwReqCnt / dwReadCnt / dwWriteCnt | 成功处理 / 读 / 写 次数 |
dwErrCnt | 故障次数(按错误沿计,持续报错不刷屏) |
wReqTotal | 从站收到的请求总数(含坏帧、广播、别的站号) |
wCrcErr | CRC 错误帧计数(Word,65535 回绕) |
wExcCnt | 回给主站的异常响应数(越界、不支持的功能码) |
wSuccCnt | 无协议错误的请求数 |
bDataChanged / wLastChgIdx | 有值被改写 / 最近被改写的下标(0 = 40001) |
wReqTotal / wCrcErr / wExcCnt / wSuccCnt
是西门子系统手册(Table 13-85)明列的公开静态变量,
本块把它们引出来,不用自己解析报文就能看总线质量。
五、排错表:看一眼就知道查什么
| 现象 | 看哪个引脚 | 原因 / 怎么办 |
参数全对,但静默不通(bReady 是 TRUE,就是没数据) | uModeAct | 端口 MODE 不是 4。485 二线半双工必须 4,uModeAct 回读应为 4 |
一条都不通,bReady 一直 FALSE | bErrCfg + wLastErr=0001 | uPortHwId 填错。去设备组态 → 模块属性 → 硬件标识符抄,常见 269 / 271 |
同上,wLastErr=0002 | bErrCfg | 波特率不在手册列表里(比如填了 14400) |
同上,wLastErr=0003 | bErrCfg | 校验参数只能是 0 / 1 / 2 |
同上,wLastErr=8180 | bErrCfg | MB_DB 没连上 → 见第六节退路方案 |
| 通了但数据全是 0 / 读到乱数 | bErrHold + wLastErr=818C | 背景 DB 被设成优化访问了,取消勾选 |
| 上位机报「非法地址」 | bErrHold + wLastErr=8383 | 主站读的地址超出 40001..41000;或 FC01/05/15 线圈类不走这个数组 |
| 时通时断,偶尔丢帧 | wCrcErr 增长、bLineNoise | 终端电阻没接 / 屏蔽层两端都接地 / 波特率校验与上位机不一致 |
| 上位机轮询快了就出错 | bErrBusy + wLastErr=8200 | 上位机发太快,或者总线上有两个主站 |
| 上位机读写的地址很怪(差 1) | — | 主站侧 40001 对应下标 0,不是 1 |
| 上位机显示掉线 | bHostLost | 先看 wCrcErr 是不是在涨:涨 = 线路问题;不涨 = 上位机真停了或站号填错 |
dwErrCnt 一秒涨几千 | — | 正常的,指针类错误是每拍都报。看 wLastErr 是多少,bErrClr 清掉再观察 |
状态码分类速查(西门子官方码)
| 码 | 点亮哪个分类位 | 含义 |
| 0000 / 7000 / 7001 / 7002 | — | 正常或执行中,不报警 |
| 8188 | bErrPara | 写只读区或 MODE 非法 |
| 8189 | bErrPara | 数据地址非法 |
| 818A | bErrPara | 数据长度非法 |
| 818B | bErrHold | 数据区指针无效(大小不对) |
| 818C | bErrHold | 指针指向了优化访问的 DB |
| 8383 | bErrHold | 访问越出 MB_HOLD_REG 范围 |
| 8200 | bErrBusy | 端口正忙 |
| 8380 / 8381 / 8382 / 8384 / 8385 / 80C8 | bErrFrame | 帧坏了:CRC / 功能码 / 长度 / 超时 |
| 0001 / 0002 / 0003 / 0004 / 8180 | bErrCfg | 端口初始化阶段(uErrSrc = 1)的组态类错误 |
六、退路方案(编译报错时看这里)
源码里 MB_DB 写成 #instSlave.MB_DB(多重实例静态量),
诊断计数器写成 #instSlave.Bad_CRC_Count 等。
个别 TIA 版本不允许从多重实例访问静态变量,编译会报「无法访问该元素」。遇到就把从站指令改成独立实例:
- 在 FB 外面(比如 OB1 里)拖一个
Modbus_Slave,让它生成自己的背景 DB,例如 DB_MbSlave。
- 在
DB_MbSlave 里加一个数组 aHoldReg : ARRAY[0..999] OF Word(该 DB 设为标准访问)。
- 把 FB 里这几处改掉:
// 状态机 STEP 5
MB_DB := "DB_MbSlave".MB_DB,
// 状态机 STEP 10
MB_HOLD_REG := "DB_MbSlave".aHoldReg,
// 诊断计数器段
#wReqTotal := "DB_MbSlave".Request_Count;
#wCrcErr := "DB_MbSlave".Bad_CRC_Count;
#wExcCnt := "DB_MbSlave".Exception_Count;
#wSuccCnt := "DB_MbSlave".Success_Count;
- 数据追踪那段也要跟着改成扫
"DB_MbSlave".aHoldReg。
另一条退路:如果你的库是老版本(指令叫 MB_COMM_LOAD / MB_SLAVE),
把 VAR 里两个类型名换掉即可,不要两个版本混用(手册明令禁止)。
七、和自由口从站怎么选
| 本块(库指令版) | 自由口版 |
| 依赖 | 需要 Modbus RTU 指令库 | 不需要库,纯 SCL 自己组帧 |
| 保持寄存器 | MB_SLAVE 管,1000 个 | 自己管,2000 点,随便改 |
| 诊断 | 库自带 CRC / 异常 / 请求计数 | 全靠自己加 |
| 线圈(FC01/05/15) | 库直接映射到过程映像 | 自己实现 |
| 改协议细节(非标波特率等) | 改不了 | 随便改 |
| 推荐场景 | 常规 Modbus 从站(省事、稳、诊断全) | 非标协议、库不可用、要 8 条功能码全支持 |
典型网关架构(两个块一起用)
[现场仪表 1..N] --485--> CM1241 --主站轮询块--> aData[]
|
(拷贝,自己写一行赋值)
v
[上位机 DCS] <--485-- CB1241 <--FB_ModbusRtuSlave-- aHoldReg[40001..41000]
两个块各占一个口、各用一个实例,互不干扰。中间的拷贝就一行赋值,别在拷贝里做复杂运算。
八、容易踩的坑(按现场出现频率排序)
- 背景 DB 被顺手设成优化访问 →
818C,一条不通。
uPortHwId 填了槽位号 → 0001。要去设备组态抄「硬件标识符」。
- 端口 MODE 没设成 4 → 参数全对却静默不通,STATUS 挂
818B。
- 调用周期太长 → 上位机超时。手册要求 RESP_TO 内至少执行 2 次。
- 主站地址差 1 → 40001 是下标 0,不是 1。
- 上位机用 FC01/FC05/FC15 读「线圈」 → 那些走 CPU 过程映像(Q0.0 起),跟
aHoldReg 没关系。
- 同一口上跑了两个实例 → 抢线,两边都不通。一个 485 口只能有一个本块实例。
- 改了波特率没给
bReload → 改动不生效,要上升沿重新初始化一次。
- PLC 自己改
aHoldReg 也会触发 bDataChanged → 以为是主站写的,其实是自己写的。
_最后更新:2026-10-10_