一句话:用西门子 CM PtP(点对点)串口模块的自由口(Freeport)模式,
配合 Send_P2P / Receive_P2P 指令,在 S7-1200 / S7-1500 上用纯 SCL
自己实现一个 Modbus RTU 从站。不调用西门子自带的 Modbus 指令,导入源码即可用。
适用模块
- S7-1200:CM 1241 RS422/485(6ES7241-1CH32-0XB0)
- S7-1500:CM PtP RS422/485 BA(6ES7540-1AB00-0AA0)或 HF 版本
两个平台用的是同一套 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 报「块已存在」)。
二、导入步骤
- 先导共享 FC:项目树 → 外部源文件 → 添加
CRC16校验_GB2312.scl→ 右键 → 从源生成块,得到CRC16校验(FC)。若这个工程里已经导过本套代码的主站块,FC 已存在,这一步跳过(别重复导)。
- 项目树 → 外部源文件 → 添加新的外部源文件 → 选
modbus_自由口从站_GB2312.scl。 - 在该源文件上右键 → 从源生成块 → 生成 1 个块:
modbus_自由口从站(FB)。 - 不要先手动新建同名块,会冲突。
- 在全局 DB 里声明五块数据区(大小必须和下面一模一样,
VAR_IN_OUT的数组长度是硬匹配的)。 - 在 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] | FC03 | PLC 程序 | 主站读的保持寄存器 |
写数据 | Word[2000] | — | FC06 / FC16 | 主站写给 PLC 的保持寄存器 |
输入寄存器 | Word[2000] | FC04 | PLC 程序 | 只读 |
线圈 | Bool[2000] | FC01 | FC05 / FC15 | 读写同址,可回环 |
离散输入 | Bool[2000] | FC02 | PLC 程序 | 只读 |
地址基准(最容易踩的一个坑)
本块用的是 Modbus 协议地址,从 0 开始。
| 组态软件里的叫法 | 协议地址 | 本块内部下标(起始地址=0) |
|---|---|---|
| 40001 | 0 | 读数据[0] |
| 40010 | 9 | 读数据[9] |
| 00001 | 0 | 线圈[0] |
| 30001 | 0 | 输入寄存器[0] |
| 10001 | 0 | 离散输入[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) | 读线圈 | 8 | 5 + ⌈N/8⌉ | 1..2000 | 线圈 读 |
| 02 (0x02) | 读离散输入 | 8 | 5 + ⌈N/8⌉ | 1..2000 | 离散输入 读 |
| 03 (0x03) | 读保持寄存器 | 8 | 5 + 2N | 1..125 | 读数据 读 |
| 04 (0x04) | 读输入寄存器 | 8 | 5 + 2N | 1..125 | 输入寄存器 读 |
| 05 (0x05) | 写单个线圈 | 8 | 8(回显) | 1 个 | 线圈 写 |
| 06 (0x06) | 写单个寄存器 | 8 | 8(回显) | 1 个 | 写数据 写 |
| 15 (0x0F) | 写多线圈 | 9 + ⌈N/8⌉ | 8 | 1..1968 | 线圈 写 |
| 16 (0x10) | 写多寄存器 | 9 + 2N | 8 | 1..123 | 写数据 写 |
上限都是 Modbus 规范值,不是随手定的——因为一帧 RTU 最长 256 字节,取到上限时帧长恰好 255 字节。
- 位打包规则:同一字节内「地址小的在低位」。线圈 0 → byte0 的 bit0,线圈 8 → byte1 的 bit0,以此类推。
- FC05 的值只认两个:
0x0000(断开)或0xFF00(闭合)。 填别的(比如常见误填的0x0001)一律回异常 03。
一个要留意的取舍:保持寄存器分「读」「写」两块
为了方便「PLC 自己填的数据」和「主站下发的数据」分开放、互不干扰,保持寄存器区故意分成两块。代价是:
主站用 FC06/FC16 写进写数据[5]的值,用 FC03 读地址 5 是读不到的——FC03 读的是读数据[5]。
这是工程上的取舍,不符合标准 Modbus 语义。需要读写回环的场合,请用线圈区(FC01/FC05/FC15 共用同一片线圈),它是真正读写同址的。
五、参数速查
输入参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
硬件标识符 | UInt | — | CM PtP 模块的硬件标识符,设备组态里查,填十进制数 |
从站号 | UInt | 1 | 本从站地址,合法 1..247。填 0 或 >247 → 参数错误,一概不应答 |
起始地址 | UInt | 0 | 地址偏移:主站地址 = 内部下标 + 本值 |
超时时间 | Time | T#2s | 发送兜底超时。波特率低于 1200 时要调大 |
使能 | Bool | TRUE | FALSE = 完全不应答(HMI 上可以当「停用」开关用) |
输出参数
| 输出 | 类型 | 语义 | 稳态/脉冲 |
|---|---|---|---|
发送完成 | Bool | Send_P2P 的 DONE | 脉冲(单扫描) |
发送错误 | Bool | Send_P2P 的 ERROR | 脉冲 |
发送信息 | Word | Send_P2P 的 STATUS | 实时 |
接收完成 | Bool | Receive_P2P 的 NDR | 脉冲 |
接收错误 | Bool | Receive_P2P 的 ERROR | 实时 |
接收信息 | Word | Receive_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#6100 | CRC 校验错,帧已丢弃 | 波特率 / 校验位 / 停止位不符 |
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 的「字节数字段」与实际长度或数量对不上;请求帧长度与功能码不符 |
永远静默丢弃、不回异常帧的四种情况(规范要求):
- CRC 错——线路上本来就有干扰,再回一帧只会让总线更乱;
- 站地址不是本机(也不是广播)——同一条 485 上挂多台从站时这是常态,回错误帧会撞坏别家的应答;
- 帧长不足 4 字节——连最短的合法帧都不到;
- 收到功能码 ≥ 0x80 的帧——那是「应答帧」,不该出现在从站门口。多半是 485 的 A/B 接反形成自环回, 或者总线上还挂着另一个主站。
发送时序:为什么「应答慢一拍」
扫描 k :收到请求 → 解析 → 组好应答帧,置"发送待发"
扫描 k+1 :启动 Send_P2P(REQ 单扫描脉冲)
扫描 k+n :DONE 到,解锁,可以收下一帧
故意隔一个扫描周期:RS485 半双工,刚收完帧收发器还在收态,同一个扫描里就怼着发, 方向切换来不及,头几个字节容易被削掉。隔一个扫描(OB1 里通常几毫秒)最省事又够用。
七、CM PtP 模块组态要点(少一样都通不了)
- 设备组态 → 端口 改成「自由口(Freeport)」,波特率跟主站一致。
- 数据格式:8 数据位 + 偶校验 + 1 停止位(Modbus RTU 标准)。
- 接收结束判据:必须选「字符间延时」,值按 ≈3.5 个字符时间折算:
选「收到 N 个字符」或「收到指定字符」都会把 Modbus 帧切碎(表现为诊断码波特率 建议值 9600 4 ms 19200 2 ms 38400 1 ms 115200 1 ms(最小值) 6103)。关于「消息结束」两种判据的实测结论:
「通过消息超时识别消息结束」(如 200ms)也能正常工作——代价是 NDR 固定迟到 一个消息超时周期,主站的应答超时必须大于「消息超时 + 从站应答时间」。
「通过字符间超时识别消息结束」是 Modbus 标准分帧方式(帧内间隙 < 判据、帧间静默 > 判据), NDR 即到即交,推荐。两者都不该把 Modbus 帧切碎;若缓冲里只剩帧尾几个字节, 说明帧头在线路上就丢了,优先查适配器 / 接线,而不是改判据。 - 硬件标识符:设备组态 → 属性 → 系统常数里查,填十进制数。
- 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] |
九、已知限制
- 诊断类功能码不支持:07 / 08 / 0B / 0C / 11 / 17。主站发这些会收到异常 01—— 这是规范允许的行为,不是 bug。
- 只做 RTU,不做 ASCII(Modbus ASCII 用
:/CRLF 定界、LRC 校验,是另一套东西)。 - 一个硬件标识符只能挂一个实例。要多从站地址共用一条串口,得改代码(按
接收报文[0]分派到不同数据区)。 - 不做帧间隔计时:帧的结束交给 CM PtP 的硬件组态判断(见第七节),块里不自己数 3.5 个字符时间。
- 数据区规模固定 2000 点(由
VAR_IN_OUT的数组声明决定)。要改小得同时改 FB 声明和全局 DB 声明, 另外程序体里那几处2000的边界判断也要一起改。
_最后更新:2026-10-07_