ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

S7-1200/1500 PUT/GET通讯避坑:偏移、LREAL与DTL解析

S7-1200/1500 PUT/GET通讯避坑:偏移、LREAL与DTL解析

简介:面向工业自动化工程师与PLC通讯调试人员的专题文档,围绕西门子S7-1200、S7-1500与DASSIDirect之间的通讯配置及兼容性问题展开,适合在TIA博途项目中排查PUT/GET访问、数据块优化访问等设置时对照参考。压缩包共1个docx文件,约44KB,以操作步骤和语法说明为主,重点梳理博途侧需修改的连接机制与数据块属性,并指出DASSIDirect不支持S7-1200块项目、直接计数器/定时器访问、周期服务及SAPI等限制,同时给出LREAL、DTL等数据类型的项名语法与间接读取思路。已有2885人学习,可作为通讯调试前的检查清单和排错参考,帮助读者快速定位配置项、理解通讯边界并评估替代通信方案。

1. 一个反直觉的事实:PUT/GET 能通,不代表数据能读对

调试现场最常见的一类误判:上位机 ping 得通 PLC,S7 连接也建起来了,可读回来的 REAL 是一串 1.4e-45 之类的垃圾值,写下去的数石沉大海,PLC 重启后又变回原样。很多人第一反应是网络抖动或驱动版本问题,实际上这类"连得上、读不对"的故障,九成都出在两个复选框和一组字节偏移上。

DASSIDirect 这类走 S7 协议的数据访问服务,思路非常朴素:把 PLC 的内存当成一块带地址的字节数组,按 DB 号加偏移去读写。它不认识符号名,也没有类型元数据,更不会替你去猜"这个变量原来是什么类型"。而 S7-1200/1500 默认的编程模型是符号化、优化过的,编译器可以重排成员、按位打包。两套模型对不上,通讯就会以"看起来通了、其实读错"的方式失败。

把这个错位搞清楚之后,LREAL 读成乱码、DTL 年份变成 1970、Cx/Tx 直读失效这些看似互不相关的怪现象,就有了统一的解释。搞懂它,等于掌握了一条判断链路:问题出在 PLC 侧的寻址模型,还是出在驱动侧的语法拼接。

2. 两个复选框:PUT/GET 权限与优化的块访问到底改了什么

2.1 为什么"允许远程伙伴使用 PUT/GET"是硬门槛

S7 协议里,PUT/GET 是 CPU 作为被动服务端的能力:客户端发读写报文,CPU 校验后直接操作内存。它在 CPU 的属性页面里,路径是「保护与安全」→「连接机制」→ 勾选「允许来自远程伙伴的 PUT/GET 通信访问」。S7-1200 与 S7-1500 出厂默认是关闭的,这不是遗漏,而是安全设计——PUT/GET 不带符号级鉴权,只要知道 DB 号和偏移,就能读写那片内存,所谓"地址即权限"。

不勾这个选项时,典型表现不是连接失败,而是连接建立后在读写阶段被拒,错误码常见 0x8104 一类(功能不可用)。这一点非常容易误导排查方向:连接状态是绿的,于是有人去查网线、查防火墙、换驱动版本,绕一大圈才回到 CPU 的保护设置上。血泪经验是,只要 S7 连接能建立而读写全 Bad,先看这个勾,再看 DB 属性,别急着怀疑网络。

还要注意一个范围问题:这个开关是 CPU 级的,改完需要重新编译并下载硬件组态,只改在线参数不生效。多台 PLC 的现场,逐台确认比事后逐个排查便宜得多。

2.2 关闭"优化的块访问":从符号寻址退回绝对偏移

S7-1200/1500 新建数据块时默认勾选「优化的块访问」。在这个模式下,DB 内部的变量是纯符号寻址的:编译器保留结构信息,可以重排成员顺序、把连续的 Bool 打包进同一个字节、必要时拆分结构体,块内没有稳定的绝对偏移。对 PLC 程序而言这是好事,访问快、在线修改容忍度高;但对一个只会按"DB 号 + 字节偏移"读写的驱动来说,这等于地址根本不存在。

标准访问(取消优化)之后,成员按声明顺序分配,偏移固定,Bool 按位、其余按类型长度和对齐规则排布,外部设备才能按字节精确命中。操作路径是数据块右键 → 属性 → 属性页 → 取消勾选「优化的块访问」,然后重新编译。要注意的是,这个属性是逐块设置的,全局 DB 和需要被外部访问的实例 DB 都得改;漏掉一个,那个块的点位就永远读不到。

还有一个连带影响常被忽略:改成标准访问后,块内变量的在线修改会变得更"硬",某些下载场景下数据会被复位。生产环境里改这个属性,最好安排在停机窗口,别在跑批的时候动。

2.3 偏移量怎么算才不翻车

取消优化后,偏移不再是秘密,但也不能凭感觉。标准访问的排布遵守一套对齐规则:Bool 连续按位打包,八位占满才进位到下一字节;Byte、Char 一字节对齐;Word、Int 从偶地址开始;DWord、DInt、Real 对齐粒度更大;64 位的 LREAL 还要再往上走一级。结构体嵌套时,内层结构体先按自身规则排完,再作为一个整体参与外层对齐。

不同 CPU 家族的对齐粒度并不完全一致,所以工程上的正确做法是:改完属性、编译一次,直接看变量表里的「偏移量」列,那才是权威值。手算只用来交叉验证,或者在没有工程软件的场合做粗排。下面这段脚本是按常见对齐模型做的估算,用来和编译结果对照:

# 按标准访问的常见对齐模型估算 DB 内偏移,仅用于与工程软件编译后输出的偏移列交叉核对 SIZE = {"bool": 1, "byte": 1, "word": 2, "dword": 4, "real": 4, "lreal": 8} ALIGN = {"bool": 1, "byte": 1, "word": 2, "dword": 4, "real": 4, "lreal": 8} def layout(members): offset, bit, result = 0, 0, [] for name, typ in members: size, align = SIZE[typ], ALIGN[typ] if typ == "bool": result.append((name, offset, bit)) # 记录字节偏移与位号 bit += 1 if bit == 8: # 一个字节装满 8 个 Bool 才进位 offset, bit = offset + 1, 0 continue if bit: # 非 Bool 成员不能与位成员共用字节 offset, bit = offset + 1, 0 offset = (offset + align - 1) // align * align # 向上取整到对齐边界 result.append((name, offset, 0)) offset += size return result, offset members = [("start", "bool"), ("running", "bool"), ("speed", "real"), ("count", "dword"), ("total", "lreal")] for name, byte, bitpos in layout(members)[0]: print(f"{name:<8} byte={byte:<3} bit={bitpos}")

这段逻辑的关键在三处:Bool 的位累加与字节进位、非 Bool 成员强制另起字节、以及按对齐粒度向上取整。参数上SIZE决定占用长度,ALIGN决定起始地址必须是几的倍数。实际使用时把成员列表替换成你的 DB 声明顺序即可,脚本输出的偏移和工程软件编译列不一致时,以工程软件为准,并回头检查是不是有嵌套结构体或数组没展开。

3. 语法层:LREAL 和 DTL 的 item name 是怎么拼出来的

3.1 LREAL 的 64 位浮点与区域前缀

LREAL 是 S7-1200/1500 引入的 64 位双精度类型,遵循 IEEE 754 的 64 位浮点定义,正负量级从约 2.2e-308 一直铺到 1.8e+308,中间包含正负零。它在 item 名里的写法是D<B,I>d,LREALm:d是数据块号,范围 1 到 65535;m是起始偏移;前面那个D表示数据块,尖括号里的I表示这是实例 DB,B表示普通全局 DB。数组则追加,v,v是元素个数。

DASSIDirect 对 LREAL 的支持不是全区域覆盖的。原文明确提示,S7-1200/1500 可能并非所有存储区都支持 LREAL 类型,实际能用的主要是数据块和部分位存储区、输入输出区。语法上前缀区分得很细:F/M是位存储区,I/E是输入区,O/A/Q是输出区,PI/PE是外设输入,PO/PA是外设输出。这些前缀看着琐碎,但写错一个字母,读回来的就是另一片内存。

字节序是第二个坑。S7 协议在链路上按大端传输,而多数上位机运行在小端机器上。LREAL 这种 8 字节类型如果端序没转,读出来就是一个完全离谱的量级——通常是极小的非规格化数,看起来像"接近 0 但又不是 0"。看到 1.4e-45 或者 6.9e-39 这类数字,先怀疑端序和偏移,别怀疑 PLC 算错了。

3.2 DTL 的 12 字节结构与 VT_BSTR / VT_DATE 双面

DTL 是 S7-1200/1500 的日期时间类型,PLC 内部固定 12 字节,字段布局是:年份占 2 字节无符号整数,月份、日、星期、时、分、秒各占 1 字节无符号,最后的纳秒占 4 字节无符号整数。年份范围 1970 到 2554,星期取值 1 到 7 对应周日到周六,纳秒上限 999999999。

驱动的巧妙之处在于,同一个 12 字节结构可以按两种规范类型暴露给上位机,全靠 item 名后缀区分:不带后缀时转成VT_BSTR,字符串精度到纳秒,形如1970-1-1-0:0:0.000000000到2554-12-31-23:59:59.999999999;带TREAL后缀时转成VT_DATE,精度降到秒,但上位机侧可以直接当日期时间对象用。后缀紧跟在偏移后面,中间没有逗号,这一点在拼 item 名时最容易写错。

import struct # DTL 在 PLC 内固定 12 字节、大端:Year(2) + Month/Day/Weekday/Hour/Minute/Second(各 1) + Nanoseconds(4) DTL_FMT = ">H6BI" # H=2 字节,6B=6 字节,I=4 字节,合计 12 def parse_dtl(raw: bytes, as_date: bool = False): year, month, day, weekday, hour, minute, second, nanos = struct.unpack(DTL_FMT, raw[:12]) if as_date: # 对应 item 名追加 TREAL 后缀:VT_DATE,秒级精度 return f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}" # 对应不加后缀:VT_BSTR,保留纳秒 return f"{year:04d}-{month:02d}-{day:02d}-{hour:02d}:{minute:02d}:{second:02d}.{nanos:09d}" raw = bytes.fromhex("07E60C1F020A1E2B003B9ACA00") # 一段样例字节流 print(parse_dtl(raw)) # 字符串形态,纳秒可见 print(parse_dtl(raw, True)) # 日期形态,秒级截断

struct.unpack的格式串是这段代码的核心:>H6BI表示大端解析,先取 2 字节年份,再连取 6 个单字节字段,最后取 4 字节纳秒。参数as_date控制的是走哪条转换路径,对应你在 item 名里加不加TREAL。这段解析适合在上位机侧做二次校验——当驱动读回来的 DTL 明显不对劲时,直接拿原始字节跑一遍,就能判断是驱动拼错了还是 PLC 里本来就没写对。

数组场景下,规范类型会升级成VT_ARRAY | VT_BSTR或VT_ARRAY | VT_DATE,语法是D<B,I>d,DTLm,v和D<B,I>d,DTLm,vTREAL。这里有个隐含约束:起始偏移m加上 12 乘以元素个数,不能跨出块尾,否则最后几个元素读到的是邻近内存的内容,年份会跳到 1970 或者 2554 这类边界值。

### 3.3数组、S7String 与 PDU 边界

数组语法统一是"类型 + 起始偏移 + 逗号 + 元素个数",元素个数上限 65534 或 65535 视类型而定,Byte、SINT、CHAR 这类一字节类型每个元素占一字节,Word、UINT 两字节,DWord、DINT、REAL 四字节。字符串有两套写法:S前缀对应 S7 字符串,块内带 2 字节头部(最大长度和当前长度);S7S前缀对应扩展字符串,参数w是净字符数,报文里占w+1,PLC 内的表示占w+2。这两个数字差一是常态,配置时按净字符数填就对了。

真正会卡住现场的是长度上限。周期服务下,单次能读的最长字符串或数组是 PDU 尺寸减去 32 字节;上位监控软件单条字符串最多处理 131 字节;写入(POKE)单次最多 256 字节或 PDU 减去 28 字节,取两者中较小的那个。超过这个长度的数组,必须拆成多段读,否则表现是前几个元素正常、后面全 Bad,或者整条 item 直接进入错误状态。

还有一个原文表格里的小瑕疵值得提一句:INT 数组那一行的规范类型标成了VT_ARRAY | VT_UI1,从 2 字节整型的语义看更像笔误。实操时按VT_ARRAY | VT_I2解析更稳,如果按一字节解析,整个数组会整体错位,症状是后半段数值全部翻倍或变成奇怪的正数。

4. 丢失的能力:Block Items、Cx/Tx 直读与周期服务额度

4.1 Block Items 为什么在 S7-1200 上缺席

Block Items 是一种批量订阅机制,分只读和只写两套 item,作用是把一整块数据当成若干条目一次性注册,减少 item 数量和报文往返。DASSIDirect 对 S7-300、S7-400 和 S7-1500 支持 Block Items,但对 S7-200 和 S7-1200 不支持。原因不难推:Block Items 依赖块级元数据来组织读写范围,S7-1200 的内存模型和块管理更紧凑,没有对应的块描述可供驱动枚举。

落到实施上,这意味着 S7-1200 的点位必须逐条列举,不能指望"整块订阅"省事。点位表几百上千条的场合,驱动侧的 item 数量会明显膨胀,配合后面要讲的周期服务额度,就得提前做分组规划,而不是把整张表一股脑塞进一个设备组。

4.2 Cx/Tx 直读失效与 IEC 计数器定时器的间接路径

在 S7-300/400 的 item 命名里,Cx和Tx加上后缀可以直接订阅计数器与定时器的当前值,比如T1配TREAL读剩余时间、配BCD或S5T读特定编码格式。S7-1200/1500 不支持这条"直读"路径:它们的计数器和定时器本质是 IEC 背景数据块,没有独立于 DB 的地址空间,所以客户端拿Cx、Tx去订阅只会持续失败。

可行的替代路径是走数据块,直接读背景 DB 里的成员。以常见的 IEC 定时器为例,块内有当前值和时间基准,读出来之后由上位机自己换算成秒;计数器同理,读它的当前计数值即可。这条路比直读麻烦,但确定性更高,因为偏移是固定的、类型是明确的,不像直读那样依赖编译器对定时器编号的映射。

顺带提醒KT、S5T、TR、BCD、DT这组后缀。它们的作用是把原始字节按特殊格式重新解释,比如把 16 位整数当成 BCD 码,或者把 32 位值当成时间长度。原文特别注明,不是所有处理器都在内部用同样的方式解释这些格式,S7-1200 就是典型。给 1200/1500 配这些后缀,容易读出"看起来有值、实际含义错位"的数字,稳妥做法是在 PLC 里先转换成基本类型,再让驱动去读。

4.3 Poke Mode 与 Cyclic Services:Device Group 里的两个旋钮

Device Group 是驱动侧组织采集的最小单元,同一组共享更新周期。写行为由 Poke Mode 三档控制,语义差别不小:

模式写值顺序是否折叠写值典型场景
Control严格保序不折叠,每个值都下发批处理、控制类客户端,顺序即语义
Transition保序折叠,只保留第一、第二和最后一个值上位机滑块类连续写,默认档
Full optimization可乱序只下发最后一个值配方下载等高写入量、顺序无关的场景

默认档是 Transition。它存在的理由很具体:上位机滑块拖动时会以很高的频率连续写同一个 item,如果每个值都发出去,总线上全是中间态,画面还会抖动。折叠掉中间值,只保证首尾可见,既省流量又不影响观感。反过来,配方下载这种一次写几百个点的场合,Full optimization 能把报文量压到最低,前提是写入顺序对工艺没有意义。

另一个旋钮是周期服务。当「禁用 S7 周期服务」没有勾选时,可以设置「限制周期服务数量」,取值范围 1 到 150,默认 1。周期服务是 PLC 通信处理器里的稀缺资源,注册进去的 item 有稳定的更新频率、占用更少的总线访问;额度不够时,驱动会自动把剩余 item 降级成轮询报文。所以配得小不是省钱,是逼着一部分点位走轮询、拉长整体刷新时间。

有三类情况建议直接勾上禁用周期服务:一是设备组本身更新周期很长,没必要占着稀缺额度;二是需要强制驱动以快于 100 毫秒的节奏去取数,周期服务的最低周期撑不住这个要求;三是 item 会频繁激活和停用,反复注册注销反而增加开销。这里有一处原文没完全对齐的地方——它一边说 1200/1500 不支持周期服务和 SAPI,一边又给出了周期服务的配置参数。实操上的稳妥解法是:对 1200/1500 直接禁用周期服务,全走轮询,行为可预期,也免去额度分配的复杂度。

5. 避坑排查:五条从现场捡回来的记录

5.1 五条记录

记录一:连接是绿的,所有点位 Bad。现象是 S7 连接状态正常,item 质量全部报错,重启驱动无效。原因有两层:CPU 属性里没勾 PUT/GET,或者 DB 仍然是优化访问。解决是按顺序核两处——先在 CPU 的连接机制里确认 PUT/GET 已勾选并重新下载硬件组态,再逐块确认目标 DB 的「优化的块访问」已取消,编译后对照偏移列检查点位表里的地址是否还停留在旧值。

记录二:REAL 读成 6.9e-39,整数整体错位。现象是浮点数变成非规格化小量,整型数据看着像隔壁变量的值。原因通常是偏移算错,或者结构体成员调整后没重编,点位表用的还是上一版偏移。解决是把 DB 编译一次,导出偏移列逐行比对;临时排查可以按点位地址前后各 dump 16 字节,看数值是否在相邻位置出现,若出现说明偏移偏了几个字节。

记录三:DTL 年份读到 1970,纳秒乱跳。现象是日期字段永远是下限值,或者纳秒部分每次刷新都不同。原因多半是把 12 字节结构当 8 字节读了,或者数组起始偏移不是 12 的倍数导致元素跨边界,还有一种是把不带后缀的字符串形态硬按日期解析。解决是确认语法用DTLm或DTLm,v,起始偏移按结构长度对齐,解析时严格按前 2 字节年份、中间 6 个单字节、末 4 字节纳秒来切。

记录四:配方下发后中间值被"吃掉"。现象是上位机连续写同一批点,PLC 侧只看到部分值,或者只留下最后一个。原因是 Poke Mode 处于默认的 Transition,中间值被折叠;若切到 Full optimization,则只有最后一个值真正下发。解决是按语义选档:控制类客户端用 Control 保证每个写值都按序到达,配方类用 Full optimization 并确保写入顺序无关。

记录五:跑几小时后刷新变慢,item 逐渐转成轮询。现象是初期刷新很快,运行一段时间后周期拉长,日志里能看到轮询报文增多。原因是周期服务额度被长周期设备组占满,或者单次读取的数组超过了 PDU 减 32 字节的上限,被拆包重试。解决是把长周期点位单独拆一个设备组并禁用周期服务,大数组按元素个数分段读,写入侧同样控制在 256 字节或 PDU 减 28 字节以内。

5.2 一条可复用的排查顺序

把上面的经验收成一条固定路径,比临场想更省时间。第一步看连接层:S7 连接是否建立、错误码是什么,0x8104 一类直接指向 PUT/GET 权限。第二步看块属性:目标 DB 是否已取消优化、是否重新编译下载。第三步看地址:偏移量是否与编译输出一致、数组是否越界、起始偏移是否满足对齐。第四步看语法:类型前缀、后缀位置、数组长度、字符串净字符数是否与表里一致。第五步才看性能:周期服务额度、单次读取长度、Poke Mode 与更新周期的匹配度。

顺序不能反。先查性能往往会陷入"调周期、调分组"的循环,而真正的根因在第一步就已经埋下了。这套顺序的价值在于,它把"看起来是通讯问题"拆成了"权限、寻址、语法、负载"四层,每层都有明确的验证手段。

6. 设计取舍:PUT/GET 这条通道还能撑多久

DASSIDirect 走的是 S7 协议的绝对地址通道,它的全部优点和缺点都来自同一个设计前提——地址即契约。好处是简单直接:不需要额外的运行时、不需要在 PLC 侧写任何通信代码,知道 DB 号和偏移就能读写,配置成本极低。代价也很清楚:符号一旦变动,点位表就断;类型信息全部靠约定,读错类型不会被拒绝,只会返回一串能解析但没有意义的字节;PUT/GET 本身是宽权限开关,一旦打开,那片内存对所有知道地址的客户端都是敞开的。

把这些维度摊开对比,取舍就直观了:

维度PUT/GET 绝对地址周期服务OPC UA
寻址方式DB 号 + 字节偏移同左,注册后周期推送符号节点,带类型
类型信息无,靠约定无有,服务端定义
配置成本低,改两个复选框中,需分配额度高,需固件与授权支持
刷新能力受 PDU 与轮询间隔限制稳定,但资源稀缺取决于会话与订阅参数
改造影响块属性变更、需停机与 PUT/GET 相同需 PLC 侧新建接口

较新的固件在 S7-1500 上提供了 OPC UA 服务端能力,S7-1200 的部分型号也在跟进。它解决的是符号与类型的问题:客户端按节点名订阅,PLC 改了成员顺序也不会断,类型由服务端声明,不用再靠偏移猜。但它不是免费的——需要固件与授权支持,需要在 PLC 侧规划接口,老设备的存量点位迁移成本不小。对一套已经稳定运行的产线,为了规避偏移风险而整体切到 OPC UA,未必划算;对新项目,从第一天就用带类型的接口,后患会少很多。

我自己的判断标准是看变更频率:点位表一年不动几次的系统,PUT/GET 加严格偏移管理完全够用,把偏移列导出来纳入版本管理就行;工艺经常调整、DB 结构反复改的系统,绝对地址就是个定时炸弹,越早换符号化接口越好。

从那以后,我每次接手一个 1200/1500 的通讯点位表,第一件事不是打开驱动配置,而是先把 DB 编译后的偏移列导出来,对着点位表逐行核一遍,再核 CPU 的 PUT/GET 开关和每个块的优化属性。这个习惯拦住过太多次"连得上、读不对"的返工。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进