自由口 Modbus RTU 从站

西门子 S7-1200 / S7-1500 用 CM PtP 串口模块的「自由口」模式,把 PLC 做成一个标准 Modbus RTU 从站。
纯 SCL 源码 · 不依赖任何库 · 八条功能码全支持
下载 SCL 源码(zip,含主版本 + 导入版 + 共享 FC)
TIA Portal SCL · 免费,可直接用于工程,出问题欢迎反馈

一句话:用西门子 CM PtP(点对点)串口模块的自由口(Freeport)模式, 配合 Send_P2P / Receive_P2P 指令,在 S7-1200 / S7-1500 上用纯 SCL 自己实现一个 Modbus RTU 从站。不调用西门子自带的 Modbus 指令,导入源码即可用。

适用模块

两个平台用的是同一套 PTP 指令,源码与组态方式完全一致,换模块时只需改一个参数—— 硬件标识符。本源码已在 S7-1200 + CM 1241(RS485 两线制)上实测可用; S7-1500 + CM PtP RS422/485 BA 的用法完全相同。


一、文件清单

下载包里共 4 个文件,分两种用途,别拿错:

文件用途
modbus_自由口从站.scl主版本(UTF-8 + BOM)。阅读、留档、后续修改以它为准。只含 FB
modbus_自由口从站_GB2312.scl导入博图用这个(GBK / ANSI,无 BOM)
CRC16校验.scl共享的 CRC16 辅助 FC,主版本
CRC16校验_GB2312.scl上面的导入版
为什么导入要用 _GB2312 版?
块名和全部变量名都是中文。中文源文件导入博图必须走 GB2312 / ANSI 编码, 用 UTF-8 中文版会乱码,严重时丢变量。主版本只用来阅读和留档,本来就不该直接导入。

为什么 CRC16校验 单独成文件?
它是从站与主站共用的辅助 FC——从站用它算应答帧、主站用它算请求帧,两边必须逐字一致。 拆开后一个工程里只导一次;若两个 FB 各自带一份,导入第二个源文件时同名 FC 会冲突 (TIA 报「块已存在」)。

二、导入步骤

  1. 先导共享 FC:项目树 → 外部源文件 → 添加 CRC16校验_GB2312.scl → 右键 → 从源生成块,得到 CRC16校验(FC)。
    若这个工程里已经导过本套代码的主站块,FC 已存在,这一步跳过(别重复导)。
  2. 项目树 → 外部源文件 → 添加新的外部源文件 → 选 modbus_自由口从站_GB2312.scl。
  3. 在该源文件上右键 → 从源生成块 → 生成 1 个块:modbus_自由口从站(FB)。
  4. 不要先手动新建同名块,会冲突。
  5. 在全局 DB 里声明五块数据区(大小必须和下面一模一样,VAR_IN_OUT 的数组长度是硬匹配的)。
  6. 在 OB1(或循环中断 OB)里调用,每扫描周期执行一次。

全局 DB 声明

读数据     : Array[0..1999] of Word;
写数据     : Array[0..1999] of Word;
输入寄存器 : Array[0..1999] of Word;
线圈       : Array[0..1999] of Bool;
离散输入   : Array[0..1999] of Bool;

OB1 里调用

"modbus_自由口从站_DB"("硬件标识符" := 269,      // 换成你模块的实际值
                      "从站号"     := 1,
                      "起始地址"   := 0,
                      "超时时间"   := T#2s,
                      "使能"       := TRUE,
                      "读数据"     := "Modbus数据".读数据,
                      "写数据"     := "Modbus数据".写数据,
                      "输入寄存器" := "Modbus数据".输入寄存器,
                      "线圈"       := "Modbus数据".线圈,
                      "离散输入"   := "Modbus数据".离散输入);
五个 VAR_IN_OUT 实参必须都传——VAR_IN_OUT 没有默认值。

三、内存模型与地址映射

五块数据区都通过 VAR_IN_OUT 从全局 DB 传进来,地址一律从下标 0 开始,范围 0..1999。

数据区类型谁读谁写说明
读数据Word[2000]FC03PLC 程序主站读的保持寄存器
写数据Word[2000]—FC06 / FC16主站写给 PLC 的保持寄存器
输入寄存器Word[2000]FC04PLC 程序只读
线圈Bool[2000]FC01FC05 / FC15读写同址,可回环
离散输入Bool[2000]FC02PLC 程序只读

地址基准(最容易踩的一个坑)

本块用的是 Modbus 协议地址,从 0 开始。

组态软件里的叫法协议地址本块内部下标(起始地址=0)
400010读数据[0]
400109读数据[9]
000010线圈[0]
300010输入寄存器[0]
100010离散输入[0]

组态软件里填 40001 这类「PLC 地址」时,要么把地址基准改成 0 基, 要么把本块的 起始地址 参数设成 1 做整体偏移。

字节序

Modbus 寄存器在线路上是高字节先发。本块用 TIA 的片段访问取字节:

#发送报文[3 + #循环 * 2] := #读数据[i].%B1;   (* 高字节先发 *)
#发送报文[4 + #循环 * 2] := #读数据[i].%B0;   (* 低字节后发 *)
⚠ %B0 是低字节、%B1 是高字节——和 S7 绝对地址 MB0/MB1 的大端顺序正好相反。这是 TIA 片段访问独有的约定,按老习惯写会把整台设备的数值都搞错位 (0x1234 变成 0x3412)。

四、功能码速查

FC名称请求长度应答长度数量上限落到哪块内存
01 (0x01)读线圈85 + ⌈N/8⌉1..2000线圈 读
02 (0x02)读离散输入85 + ⌈N/8⌉1..2000离散输入 读
03 (0x03)读保持寄存器85 + 2N1..125读数据 读
04 (0x04)读输入寄存器85 + 2N1..125输入寄存器 读
05 (0x05)写单个线圈88(回显)1 个线圈 写
06 (0x06)写单个寄存器88(回显)1 个写数据 写
15 (0x0F)写多线圈9 + ⌈N/8⌉81..1968线圈 写
16 (0x10)写多寄存器9 + 2N81..123写数据 写
上限都是 Modbus 规范值,不是随手定的——因为一帧 RTU 最长 256 字节,取到上限时帧长恰好 255 字节。

一个要留意的取舍:保持寄存器分「读」「写」两块

为了方便「PLC 自己填的数据」和「主站下发的数据」分开放、互不干扰,保持寄存器区故意分成两块。代价是:

主站用 FC06/FC16 写进 写数据[5] 的值,用 FC03 读地址 5 是读不到的——FC03 读的是 读数据[5]。
这是工程上的取舍,不符合标准 Modbus 语义。需要读写回环的场合,请用线圈区(FC01/FC05/FC15 共用同一片 线圈),它是真正读写同址的。

五、参数速查

输入参数

参数类型默认值说明
硬件标识符UInt—CM PtP 模块的硬件标识符,设备组态里查,填十进制数
从站号UInt1本从站地址,合法 1..247。填 0 或 >247 → 参数错误,一概不应答
起始地址UInt0地址偏移:主站地址 = 内部下标 + 本值
超时时间TimeT#2s发送兜底超时。波特率低于 1200 时要调大
使能BoolTRUEFALSE = 完全不应答(HMI 上可以当「停用」开关用)

输出参数

输出类型语义稳态/脉冲
发送完成BoolSend_P2P 的 DONE脉冲(单扫描)
发送错误BoolSend_P2P 的 ERROR脉冲
发送信息WordSend_P2P 的 STATUS实时
接收完成BoolReceive_P2P 的 NDR脉冲
接收错误BoolReceive_P2P 的 ERROR实时
接收信息WordReceive_P2P 的 STATUS实时
接收长度UInt最近一帧字节数(NDR 时有效)实时
校验信息Word诊断码。成功清 0,出错锁存锁存
读写完成Bool一次请求被正常执行完(广播也算)脉冲
读写错误Bool一次请求被回了异常帧脉冲
发送超时Bool锁存:上次发送超时;下次发送成功才清锁存
参数错误Bool从站号不在 1..247稳态(每周期重算)
最后功能码Byte最近一帧请求的功能码锁存
最后异常码Byte最近一次回给主站的异常码(0 = 没回过)锁存
最后起始地址UInt最近一帧请求的起始地址(原始值,未减偏移)锁存
最后数量UInt最近一帧请求的数量字段。⚠ FC05/06 这里是「要写的值」,它俩没有数量字段锁存
好帧计数UDInt站号匹配 + CRC 正确 + 功能码像请求累加
坏帧计数UDInt被丢弃的帧(长度/站号/CRC/回环)累加
异常计数UDInt回给主站的异常帧次数(广播不算)累加
广播计数UDInt收到的广播帧(站地址 0)次数累加
版本指纹Byte在线监视到 16#0A 说明跑的是本版代码实时
校验信息 看「现在健不健康」(成功清零), 坏帧计数 / 异常计数 看「历史上出没出过事」(只增不减)。 只看前者会漏掉偶发错误,只看后者不知道现在恢复没有。

六、诊断码表(校验信息)

码含义下一步查什么
16#0000正常(最近一次请求已正常处理)—
16#6100CRC 校验错,帧已丢弃波特率 / 校验位 / 停止位不符
16#6101发送兜底超时看 发送信息(Send_P2P 的 STATUS);也可能要调大 超时时间
16#6102站地址不匹配,帧已丢弃从站号填错,或主站地址填错
16#6103帧长度非法,帧已丢弃接收结束判据没设成「字符间延时」
16#6105收到 0x80 以上的应答帧,帧已丢弃485 的 A/B 接反(自环回)
16#6106从站号参数非法从站号 必须填 1..247
16#6110功能码不支持 → 已回异常帧 01主站用了 07/08/0B/11/17 等诊断类功能码
16#6111地址 / 数量越界 → 已回异常帧 02主站地址超出 0..1999,或 起始地址 偏移设错
16#6112数据值 / 帧结构非法 → 已回异常帧 03数量为 0 或超限、FC05 的值不是 0000/FF00、FC16 字节数字段对不上
16#6120发送失败看 发送信息(Send_P2P 的 STATUS)
16#6121接收错误看 接收信息(Receive_P2P 的 STATUS)

异常响应

异常码含义什么时候回
0x01非法功能码功能码不在 01/02/03/04/05/06/15/16 里(例如 07、08、0B、11、17 这些诊断类)
0x02非法数据地址起始地址 − 地址偏移 + 数量 > 2000,或地址低于偏移
0x03非法数据值数量为 0 / 超上限;FC05 的值不是 0000/FF00;FC15/16 的「字节数字段」与实际长度或数量对不上;请求帧长度与功能码不符

永远静默丢弃、不回异常帧的四种情况(规范要求):

  1. CRC 错——线路上本来就有干扰,再回一帧只会让总线更乱;
  2. 站地址不是本机(也不是广播)——同一条 485 上挂多台从站时这是常态,回错误帧会撞坏别家的应答;
  3. 帧长不足 4 字节——连最短的合法帧都不到;
  4. 收到功能码 ≥ 0x80 的帧——那是「应答帧」,不该出现在从站门口。多半是 485 的 A/B 接反形成自环回, 或者总线上还挂着另一个主站。

发送时序:为什么「应答慢一拍」

扫描 k    :收到请求 → 解析 → 组好应答帧,置"发送待发"
扫描 k+1  :启动 Send_P2P(REQ 单扫描脉冲)
扫描 k+n  :DONE 到,解锁,可以收下一帧

故意隔一个扫描周期:RS485 半双工,刚收完帧收发器还在收态,同一个扫描里就怼着发, 方向切换来不及,头几个字节容易被削掉。隔一个扫描(OB1 里通常几毫秒)最省事又够用。


七、CM PtP 模块组态要点(少一样都通不了)

  1. 设备组态 → 端口 改成「自由口(Freeport)」,波特率跟主站一致。
  2. 数据格式:8 数据位 + 偶校验 + 1 停止位(Modbus RTU 标准)。
  3. 接收结束判据:必须选「字符间延时」,值按 ≈3.5 个字符时间折算:
    波特率建议值
    96004 ms
    192002 ms
    384001 ms
    1152001 ms(最小值)
    选「收到 N 个字符」或「收到指定字符」都会把 Modbus 帧切碎(表现为诊断码 6103)。
    关于「消息结束」两种判据的实测结论:
    「通过消息超时识别消息结束」(如 200ms)也能正常工作——代价是 NDR 固定迟到 一个消息超时周期,主站的应答超时必须大于「消息超时 + 从站应答时间」。
    「通过字符间超时识别消息结束」是 Modbus 标准分帧方式(帧内间隙 < 判据、帧间静默 > 判据), NDR 即到即交,推荐。两者都不该把 Modbus 帧切碎;若缓冲里只剩帧尾几个字节, 说明帧头在线路上就丢了,优先查适配器 / 接线,而不是改判据。
  4. 硬件标识符:设备组态 → 属性 → 系统常数里查,填十进制数。
  5. RS485 两线制 A/B 别接反。接反的表现正是诊断码 6105。

同一台 PLC 上一主一从,会不会冲突

指令层面:不会。西门子手册明确——「任何时刻,每个通信模块都只能有一条发送指令处于待定状态」。 这条限制是按模块算的,不是按 CPU 算的。两个 CM PtP 各有独立的 UART、独立的硬件标识符、 独立的背景数据块,主站的发送作业和从站的发送作业互不排队。

但一个模块不能同时演两个角色——这是物理层决定的,换角色得重新组态端口,运行中切不了。

要真正「不冲突」,有 6 处必须做到位:

#要点做错的后果
1两个 FB 用各自独立的实例 DB共用一个 DB = 两套状态机打架
2硬件标识符 填各自模块的,别填重两个 FB 抢同一个口。填 0 或填错不报编译错,运行时才失效
3两个端口接两条物理独立的 485 总线这是唯一真正的冲突源
4两条总线各自两端 120Ω 终端反射 + 驱动能力不够
5每个模块的波特率 / 校验在设备组态里分别设两个口的波特率可以不同,这是优势,别强求一致
6主站的应答超时要大于远端从站的处理时间总线被别的节点占着时偶发超时
唯一真正的冲突源是第 3 条:若两个 CM PtP 的 A/B 接到同一根总线上,主站发的请求本机从站也会收到; 一旦站号撞车,本机从站和远端从站会同时驱动总线,波形互相抵消,双方都收到坏帧。
正确做法:主站一条线、从站一条线,两网电气隔离。只想拿一块 PLC 自测主从对接是可以的, 但那条线上只能有这两个节点,且本机从站号必须是远端从站没用的号。

八、常见问题排查

现象原因处理
完全没反应,好帧计数 坏帧计数 都不涨根本没收到帧查线、波特率、硬件标识符、RS485 的 A/B
好帧计数 涨但主站超时应答发出去了但主站不认多半是地址基准没设对(40001 那套),或主站 CRC 校验方式不对
校验信息 = 6100 且频率高CRC 全错波特率 / 校验位 / 停止位不符
校验信息 = 6102 一直不匹配地址对不上从站号填错,或主站地址填错
校验信息 = 6105收到 0x80 以上的应答帧485 的 A/B 接反
校验信息 = 6103帧长不对CM PtP 的接收结束判据没设成「字符间延时」
6100/6103 交替,接收报文 里只有帧尾几个字节(如 0A C5 CD)帧被切碎:消息结束判据触发过早,帧头在重新武装接收的空档里丢了把「字符间延时」加大(9600 下 4ms 起步,USB 转 485 适配器可加到 6~10ms);消息开始设「立即开始」
校验信息 = 6110主站用了诊断类功能码07/08/0B/11/17 本块不支持,规范允许回异常 01
校验信息 = 6111地址越界主站地址超出 0..1999,或 起始地址 偏移设错
校验信息 = 6120 / 6121发送 / 接收指令报错看 发送信息 / 接收信息 里的 STATUS 码
读到的数值整体错位(0x1234 变成 0x3412)字节序写反了记住 %B1 = 高字节、%B0 = 低字节(和 MB0/MB1 相反)
输出一直 0 / 完全没效果块没被每扫描调用,或 使能 是 FALSE,或 参数错误 亮着确认调用位置和 从站号
导入后注释乱码、变量丢失用 UTF-8 中文版导入了改用 _GB2312 版
编译报「数组大小不匹配」全局 DB 里的数组和 FB 声明长度不一致两边都必须是 Array[0..1999]

九、已知限制


_最后更新:2026-10-07_

在现场遇到搞不定的情况?
把「现象 + 校验信息 的值 + 报文的十六进制」发到 26985331@qq.com,对着诊断码表基本都能定位。 用着有问题、或者想要的功能,也欢迎来信。
Modbus 调试助手浮点数位模式转换器局域网设备扫描器BOM 汇总工具表格整理工具