写请求先进队列,调度 FC 逐条发出去。写总闸一关任务照进队列不丢;某一帧失败、超时、掉线,队列自己往下走。
西门子 S7-1200 / S7-1500 · TIA Portal SCL · 基于系统指令 MB_CLIENT 封装 · 版本 v2.1
3 种编码任选其一导入 TIA Portal · 包内 12 个文件
ModbusWriteCache_Types(生成 UDT + 全局 DB)
→ FC_ModbusWrite_PutTask(入队)
→ FC_ModbusWrite_CacheRun(出队调度)。
两个 FC 的接口引用了 Types 里定义的 UDT,顺序反了报「未定义数据类型」。
ENABLE_WRITE=FALSE 停发,队列里的任务还在,恢复后继续走。80Cx)先断开再冷却重连;协议层异常码(838x)直接丢弃,不做无意义的重试。AddTrigger 按单周期脉冲处理,一周期里调 5 次就是 5 条任务全进队列。往从站写数据,最朴素的做法是直接调 MB_CLIENT。设备不在线的时候,这一调就等于把整个程序吊死:指令一直等不到 DONE,后面的写请求全堵在后面,一个都发不出去,而 HMI 上看起来一切正常。
写缓存队列的做法是把"提交"和"发送"拆开:
FC_ModbusWrite_PutTask 把写请求扔进队列,函数立刻返回;FC_ModbusWrite_CacheRun 每周期调一次,从队列里取一条发给 MB_CLIENT;| 场景 | 直接调 MB_CLIENT | 用本写缓存队列 |
|---|---|---|
| 对端 TCP 半开(受理了但永不返回) | 状态机永久停在等结果,后续写请求全部滞留 | 兜底超时 → 断线重连 → 丢弃并锁存错误码,状态机回 0 |
请求未被受理(16#8200 端口占用) | 同样永久卡住 | 等受理超时 → 报自定义码 16#FFFE,回 0 |
从站回异常码(16#8383) | 重试策略一刀切,白试 3 帧 | 协议层异常不重试,1 帧就判定失败 |
| 同一扫描周期提交 5 条 | —— | 5 条全进队列,逐条发出 |
| 想临时停写 | 只能靠外面不再调 | 置 ENABLE_WRITE=FALSE,任务照进队列不丢 |
本包只发写缓存队列这一个功能块(三件套)。同批的另外几个块 (多从站轮询主站、自由口主站、RTU 从站等)各有各的测试状态,没测完的我不往站上放。
| 文件 | 生成的块 | 职责 |
|---|---|---|
ModbusWriteCache_Types | UDT_ModbusWriteTaskDB_ModbusWriteCache | 数据层:一条写任务的结构 + 全局 DB(队列、状态机、组态、统计、定时器) |
FC_ModbusWrite_PutTask | FC_ModbusWrite_PutTask | 入队:调用方提交写请求,校验参数后写进环形队列 |
FC_ModbusWrite_CacheRun | FC_ModbusWrite_CacheRun | 出队:状态机驱动 MB_CLIENT,处理超时、重试、断线与统计 |
// ① 提交写请求:每周期可以调多次
"FC_ModbusWrite_PutTask"(
SlaveID := 1, // 从站号 1..247,0 是广播(拒收)
RegAddr := 40101, // 保持寄存器地址(带区号)
RegValue := 16#1234, // 要写的值
AddTrigger:= "写启动脉冲"); // 必须单周期脉冲!
// ② 调度:每周期调一次,全工程只调一次
"FC_ModbusWrite_CacheRun"(
InterfaceId := 64, // PLC 的 PROFINET 口硬件标识符
ID := 21, // 连接号,必须唯一
ADDR1 := 192, ADDR2 := 168,
ADDR3 := 0, ADDR4 := 51,
RemotePort := 502);
队列非空且总闸开
0 空闲 ──────────────► 10 等受理 ──BUSY──► 20 等结果 ──DONE──► 出队(成功) ──┐
▲ │ │ │
│ │T_ACCEPT 超时 │T_REQ 超时 / ERROR │
│ ▼ ▼ │
│ ┌──────────────────────┐ │
│ │ 30 判定 │ │
│ │ · 818x/838x → 丢弃 │ │
│ │ · 80Cx/超时 → 60 断线│ │
│ │ · 其余 → 原地重试 │ │
│ └────────┬─────────────┘ │
│ ▼ │
└───────────────────── 60 断开 ──► 65 冷却 ──► 回到 10(重试) │
└──────────────────────────── 丢弃(失败) ◄─────────────────────────────┘
| 状态 | 含义 | 兜底 |
|---|---|---|
| 0 | 空闲,队列空或总闸关 | — |
| 10 | 已置 REQ,等 MB_CLIENT 受理 | T_ACCEPT 超时 → 16#FFFE → 30 |
| 20 | 已受理,REQ 落低,等 DONE/ERROR | T_REQ 超时 → 16#FFFF → 30 |
| 30 | 判定:丢弃 / 原地重试 / 先重连 | 保证 REQ 至少一拍低电平 |
| 60 / 65 | 断开保持 T_DISC → 释放 → 冷却 T_RECONN | 之后回 10 重试 |
成功和丢弃都不单占状态,就地出队 —— 每个任务固定 4 拍。
| 文件 | 编码 | 用途 |
|---|---|---|
ModbusWriteCache_Types.scl | UTF-8 + BOM | 阅读与留档(完整中文注释) |
ModbusWriteCache_Types_EN.scl | 纯 ASCII | 首推导入版,最稳 |
ModbusWriteCache_Types_GB2312.scl | GBK 无 BOM | 想看中文注释时用这版导入 |
三个块都是这三份编码,同一个块要导同一编码版本,别混着导。
_GB2312 是派生文件,由脚本自动生成。改了逻辑要改主文件后重新派生 —— 永远不要手工改 _GB2312,下次派生会全部覆盖。
.scl → 从源生成块。ModbusWriteCache_Types → 生成 UDT + 全局 DB 两项。FC_ModbusWrite_PutTask,最后导 FC_ModbusWrite_CacheRun。MB_CLIENT_DB / MB_CLIENT_PARA —— 本包故意不含,那两个 DB 里是现场 IP 与连接号,重导会冲掉。CacheRun 每周期调一次、全工程只调一次;PutTask 每周期可调多次。DB_ModbusWriteCache.VER,是 16#21 就说明新代码生效了。导入ModbusWriteCache_Types会覆盖现有的DB_ModbusWriteCache: 队列清空、统计清零、组态值回到默认。队列本来就是非保持的,但请在停机试车的窗口里做, 别在线路上正跑着东西的时候导。
MB_CLIENT_DB(在 OB 里拖一次 MB_CLIENT 指令,让 TIA 自动生成)+
MB_CLIENT_PARA(一个 T_CON_IP_v4 结构:
InterfaceId=64 / ID=21 / ActiveEstablished=1 /
RemotePort=502 / LocalPort=0)。
两个 FC 的输入接口完全没变,原有的调用语句不用改;新增的都是输出引脚,可以全部不接(状态在 DB 里都有)。
全部在 DB_ModbusWriteCache 里,HMI 或监视表直接改(DB 是非保持的,掉电回默认值)。
| 参数 | 默认 | 什么时候要改 |
|---|---|---|
QUEUE_MAX_LEN / QUEUE_CAPACITY | 100 / 99 | 改队列深度时,同时改 TaskQueue 数组长度 |
RETRY_MAX | 3 | 总尝试次数(含第一次)。局域网 3 够,别贪大 |
ENABLE_WRITE | TRUE | 临时停写(检修 / 联调)置 FALSE,任务仍会入队不丢 |
RETRY_ON_PROTO_ERR | FALSE | 网关偶发帧错、想连异常码也重试时置 TRUE |
ADDR_OFFSET | 0 | 地址习惯是 0 基寄存器号就填 40001 |
TRIG_MODE | 0 | v2.1 起已废弃,FC 不再读它。保留字段只为不动 DB 结构 |
T_ACCEPT | T#1s | 跨网段 / 无线可放宽到 T#2s |
T_REQ | T#6s | 必须比 MB_CLIENT 背景 DB 里的响应超时长(那里默认 5s),否则本块先报超时,拿不到真正的错误码 |
T_DISC / T_RECONN | 300ms / 500ms | 一般不用动 |
DB_ModbusWriteCache 的 S7_Optimized_Access 必须是 'FALSE'。
MB_CLIENT 的 MB_DATA_PTR 只能指向非优化 DB 或 M 区;一旦被「顺手优化」成 TRUE,
MB_CLIENT 会稳定报 16#818C 并且一条都写不出去。
AddTrigger 必须是单周期脉冲用 R_TRIG 把电平取成单周期脉冲再喂给每个调用点。接成电平会每周期重复写同一个值,
TrigHoldWarn 输出会亮一拍提示。同一个脉冲可以喂给多个调用点,
v2.1 起同一扫描周期里多处提交互不干扰。
RegAddr 直接对应 MB_CLIENT 的 MB_DATA_ADDR:
写保持寄存器要填 40101 这种带区号的地址;填 100 会被当成线圈区,发出去的是 FC05 而不是 FC16。
地址习惯是 0 基寄存器号,就在 ADDR_OFFSET 里填 40001。
v2.1 只改了 FC_ModbusWrite_PutTask 的入队触发语义,ModbusWriteCache_Types 本次只改了注释、DB 结构一个字节没变。
不要重导 Types —— 重导会把在线整定过的 T_ACCEPT / T_REQ / RETRY_MAX 一起重置。
MB_StatusLatched = 16#818CMB_DATA_PTR 指向了优化访问的 DB。把 DB_ModbusWriteCache 的「优化的块访问」关掉 —— 别把它改成优化。
WriteLocked = TRUEENABLE_WRITE 是 FALSE。打开写总闸。
PutTask 的 MultiCall 亮了同一周期不是第一次调用。v2.1 起这只是诊断位,不是故障,不用管。v2.0 时期接了报警的,这次要拆掉。
StatusLive 一直 16#7004这是正常的(连接已建立、无作业在跑)。不是故障,别当错误码查。真卡住时 STATUS 是 16#7005 或 16#8200。
16#8200 端口正在处理另一个请求同一连接号被两处用了,或者上一作业没收尾。检查全工程是不是有第二个 MB_CLIENT 用了同一个 ID。
DB 是 NON_RETAIN。有意的:写任务绝不能在重启后被重放 —— 那等于把设备写回历史状态,比重发一条新写请求危险得多。
已在 S7-1510SP-1 PN 上导入 TIA 工程实跑,验证了两项:
PutTask 多次、多处各喂同一个脉冲,每一条都进了队列,没有静默丢弃(这正是 v2.1 修的那个缺陷)。其余边界场景(兜底超时、断线重连、失败丢弃、协议异常码分类)目前是仿真结论,没有在真机上逐条复现 —— 页面上不把它们说成实测过。
配套仿真脚本用 Python 1:1 复刻了两套状态机的离散递推,并对 MB_CLIENT 建模了四种搭档行为(正常 / 报错 / 受理后永不返回 / 根本不受理)—— 只跑成功路径是测不出问题的。
python test_ModbusWriteCache_simulation.py| 场景 | 原稿 | 本版 v2.1 |
|---|---|---|
| 同周期调 5 次(同一个单周期脉冲) | 5 帧 | 5 帧全进 |
| 触发保持 20 拍(没取沿,属于接错) | 发 20 帧同样的写 | 发 4 帧 + TrigHoldWarn 报警 19 拍 |
| 对端半开 5000 拍后 | 状态机永久停在 2,失败计数 0,后续 5 条全滞留 | 超时 → 重连 → 丢弃并锁存 16#FFFF,状态机回 0 |
| 请求未被受理 | 永久停在 2,7004 重发分支触发 0 次 | 报 16#FFFE,回 0 |
从站回 16#8383 | 发 3 帧白试 | 发 1 帧 |
| 满队列里「刚好空出一格」那一拍提交 | 静默丢弃 | 正常入队 |
诚实声明:写缓存队列本身已在真机上跑通(S7-1510SP-1 PN,验证了正常写入通路与同周期多次提交)。 但上面那张对照表里的「原稿」那一列,以及超时兜底、断线重连、失败丢弃这些边界分支,是仿真 + 手册核对的结论, 没在真机上逐条复现 —— 这里不把它们说成实测过。
换到自己的工程时,仍建议在停机窗口做,先置ENABLE_WRITE=FALSE观察状态机跑通,再开闸。
站上的规矩没变 —— 没跑通的不放上来。同批的其余几个块还在整理,没测完的我不往站上放。
MB_CLIENT 指令与相应版本授权。
用着有问题、或者想要什么功能,欢迎
来信。