第一次在客户程序里看到连续三行L 16#1002的时候,我盯着监视表看了半天。旁边是一段没有任何注释的 FC,里面全是T LD 0、T LD 2、T LD 4这样的操作。后来才知道,这是老工程师在手工给 SFC20 搭 ANY 指针。从那以后,我就把 ANY 指针的 10 个字节记成了肌肉记忆——搞懂它,西门子博途里的 STL 程序基本就通了一半。
这篇内容适合两类人:一类是刚接触 STL、被P#DB10.DBX0.0 BYTE 10这种写法绕晕的新手;另一类是已经在用 TIA Portal 写 S7-300/400、想把手里的数据搬运、批量填充、工艺轴数据镜像这类活干得更干净的老手。我会把L 16#1002这串常量的每个位都拆开讲,再把手工构造 ANY 指针的完整套路、实测代码和踩坑过程都放出来,你照着抄就能用。
1. 先从一段陌生的 STL 代码说起:ANY 指针到底解决什么问题
1.1 为什么 PLC 里需要"带类型、带长度"的指针
很多人在学 STL 时最先接触的指针是 32 位的 POINTER,比如P#M10.0、P#DB10.DBX20.0。这种指针只告诉你"数据在哪",不告诉你"数据是什么、有多少"。它相当于一个门牌号——你知道该去哪间房,但不知道房间里装的是桌子还是椅子,更不知道要搬几张。
问题来了:西门子标准功能里,像 SFC20(BLKMOV)、SFC21(FILL)这类块,必须知道"要处理的数据长什么样、有多少个",才能正确操作。假如 SFC20 的SRCBLK参数只给一个 32 位地址,CPU 就不知道复制多少个字节;如果固定死复制 10 个字节,那你传 REAL 数组、传 WORD 数组时又全乱套了。
所以 S7 提供了一种更聪明的指针——ANY。它就相当于一张"搬运转单":从几号楼的几号房间开始,把桌子搬走 10 张。门牌号、物品类型、数量全部写在同一张单子上。ANY 指针固定占用 10 个字节,比 32 位 POINTER 多出来的空间,正好用来装类型和长度信息。
1.2 ANY 指针在程序里的三种出场方式
ANY 指针最常见的出场方式有三个,你可以对着自己的程序排查一下:
- 第一种:FC/FB 的形参类型直接声明为 ANY。比如一个整数变量参数,你声明成
VAR_INPUT srcBlk : ANY; END_VAR,调用时编译器会自动把实参的地址、类型、长度打包成 10 字节传给块内部。 - 第二种:调用 SFC20、SFC21 时,直接在实参位置写
P#DB10.DBX0.0 BYTE 10。这是"符号化"的 ANY 写法,编译阶段就把 10 个字节生成好了。 - 第三种:程序运行期间,自己在内存里手工拼这 10 个字节。
L 16#1002这种常量就是出现在这种场景里的。
前两种用起来省事,但都有一个致命限制:目标地址和长度必须在编程时就确定。如果一套设备要支持"操作员在 HMI 上随便选 DB 号、随便填字节数",那你就必须用第三种方式——运行时手工构造 ANY 指针。
1.3 哪些场景必须手工构造 ANY
我在现场遇到最多的三类需求是:跨 DB 不定长数据搬运、批量填充一组连续地址、把工艺轴的速度或位置数组整体镜像到归档区。这些场景的共同点是"源/目标在运行前不知道,必须由上位机或配方动态指定"。
还有一个隐藏场景:你手上有一个老项目,原来的程序员把一堆搬运功能写成了同一个 FC,参数就是 ANY。你要扩展功能,但不想改动原块接口,那就只能在调用前手工拼一个新的 ANY 塞进去。这种情况在维护老产线时特别常见。
2. L 16#1002 的字节级拆解:10 字节结构逐位看
2.1 16#10 是格式标记,16#02 才是真正的类型
很多人第一眼看到L 16#1002,会以为这是个长度 1002 或者别的什么数值。其实 16#1002 是 16 位十六进制数,拆成两个字节就是:
- 高字节
16#10:固定值,表示"这是一个 ANY 指针"。 - 低字节
16#02:数据类型编码,表示"数据的基本类型是 BYTE"。
所以L 16#1002这条指令的意思就是:把"ANY 指针格式标记 + BYTE 类型编码"组成的一个字压入累加器。接下来如果你把它存到 ANY 变量所在内存的前两个字节,ANY 指针的"类型骨架"就搭好了。
这里有个极易混淆的点:10 字节 ANY 结构里,第 0 字节和第 1 字节分别存放格式标记、类型编码,不是把类型编码放在第 0 字节。旧手册里有些人把第 0 字节叫 Syntax ID,第 1 字节叫 Type ID。记住这个区分,后面排查错误码时会省很多时间。
2.2 完整的 10 字节 ANY 内存布局
下面这张表是 ANY 指针在内存里的标准布局,我强烈建议你把它抄在笔记本上:
| 字节偏移 | 含义 | 示例值(DB10.DBB20,10 个字节) |
|---|---|---|
| 0 | Syntax ID,固定 16#10 | 16#10 |
| 1 | 数据类型编码(Type ID) | 16#02(BYTE) |
| 2 | 重复次数高字节 | 16#00 |
| 3 | 重复次数低字节 | 16#0A(10) |
| 4 | DB 号高字节 | 16#00 |
| 5 | DB 号低字节 | 16#0A(DB10) |
| 6 | 存储区标识 | 16#84(DB 区) |
| 7 | 字节地址高字节 | 16#00 |
| 8 | 字节地址低字节 | 16#14(20) |
| 9 | 位地址 | 16#00 |
看到第 2、3 字节了吗?重复次数 10 在内存里是00 0A,高位在前;DB 号 10 也是00 0A。这说明西门子用的是大端字节序。写代码时必须按"字"或"双字"来整体传送,如果用字节传送,10存到高字节还是低字节很容易搞反。
2.3 常见数据类型编码对照表
第 1 字节的类型编码是整个 ANY 的灵魂。我把最常用的一组列出来:
| 类型编码 | 完整首字常量 | 对应数据类型 | 单元素占用字节 |
|---|---|---|---|
| 16#01 | 16#1001 | BOOL | 1 位 |
| 16#02 | 16#1002 | BYTE | 1 |
| 16#03 | 16#1003 | CHAR | 1 |
| 16#04 | 16#1004 | WORD | 2 |
| 16#05 | 16#1005 | INT | 2 |
| 16#06 | 16#1006 | DWORD | 4 |
| 16#07 | 16#1007 | DINT | 4 |
| 16#08 | 16#1008 | REAL | 4 |
| 16#0E | 16#100E | STRING | 不定 |
| 16#13 | 16#1013 | ANY(嵌套) | 10 |
实际项目中我 90% 的时间只在用 1002、1004、1005、1007、1008 这五个。遇到 UDT 类型的数组时,ANY 的第 1 字节填的是 UDT 内部基本元素的类型编码,重复次数仍然填元素个数,而不是填整个 UDT 占用的总字节数。这一点后面实战案例里会再强调。
3. 手工拼 ANY 指针的完整套路:三个步骤一块校验
3.1 第一步:先把"首字"写好,同时想清楚类型
手工构造 ANY 指针时,第一步永远是确定你要处理的数据类型,然后写首字。处理字节流用 16#1002,处理 WORD 数组用 16#1004,处理 REAL 数组用 16#1008。
确定类型时有个原则:SFC20 复制的是"连续内存块",所以 ANY 里的类型编码应该反映"最小操作单元"的类型。比如你要把一个由 50 个 REAL 组成的数组整体搬走,首字就要写 16#1008,重复次数填 50,而不是填 200(字节数)。CPU 会根据 Type ID 自己算出 50×4=200 字节。
3.2 第二步:填重复次数,区分"元素个数"和"字节数"
重复次数是新手最容易翻车的字段。它的单位是"元素个数",不是"字节数"。换句话说:
- 操作 BYTE 数组时,元素个数和字节数相等。
- 操作 INT/WORD 数组时,重复次数 = 字节数 ÷ 2。
- 操作 REAL/DWORD 数组时,重复次数 = 字节数 ÷ 4。
我见过一位同事想把 200 个字节清零,结果重复次数填了 200,类型却写了 16#1008(REAL),CPU 直接按 200 个 REAL 去处理,一下子越界访问了 800 字节,程序立刻报错。所以填完重复次数后,一定要自己心算一遍总字节数。
3.3 第三步:填 DB 号、存储区标识和起始地址
剩下三组数据按偏移 4、6、8 分别写入:
- 偏移 4-5:DB 号。如果访问的是 I/Q/M 区而不是 DB 区,这里必须写 0。
- 偏移 6:存储区标识。I 区是 16#81,Q 区是 16#82,M 区是 16#83,DB 区是 16#84。写错这一位,SFC 会把 DB 数据当成 M 区数据去读,结果全是天文数字。
- 偏移 7-8:起始字节地址,高字节在前。
- 偏移 9:位地址,范围 0 到 7。访问多字节数据类型(WORD、REAL、DINT)时,位地址必须为 0,字节地址也要尽量对齐到偶数地址,否则在 S7-1200/1500 上可能触发对齐错误。
3.4 在一个非优化全局 DB 里拼 ANY,然后实测 SFC20
工地上的图纸永远不如一次实测来得实在。下面这段代码我建议你放到一个 S7-300/400 的测试项目里跑一遍。
先在全局 DB 里声明两个 ANY 变量。注意,这个 DB 必须关闭"优化块访问"选项,否则后面的 DBW 绝对寻址不允许使用:
DATA_BLOCK "DB_GENERAL" SRC_ANY : ANY; DST_ANY : ANY; END_DATA_BLOCK然后在 FC 里这样填:
// 源:DB10.DBB0,10 个字节 L 16#1002; // 格式标记 + BYTE 类型 T "DB_GENERAL".DBW 0; // SRC_ANY 偏移 0-1 L 10; // 重复次数 10 T "DB_GENERAL".DBW 2; // SRC_ANY 偏移 2-3 L 10; // DB 号 10 T "DB_GENERAL".DBW 4; // SRC_ANY 偏移 4-5 L W#16#8400; // 区域标识 DB + 地址高字节 0 T "DB_GENERAL".DBW 6; // SRC_ANY 偏移 6-7 L W#16#0000; // 地址低字节 0 + 位地址 0 T "DB_GENERAL".DBW 8; // SRC_ANY 偏移 8-9 // 目标:DB20.DBB0,10 个字节,代码结构完全一样 L 16#1002; T "DB_GENERAL".DBW 10; // DST_ANY 偏移 0-1 L 10; T "DB_GENERAL".DBW 12; // DST_ANY 偏移 2-3 L 20; T "DB_GENERAL".DBW 14; // DST_ANY 偏移 4-5 L W#16#8400; T "DB_GENERAL".DBW 16; // DST_ANY 偏移 6-7 L W#16#0000; T "DB_GENERAL".DBW 18; // DST_ANY 偏移 8-9 // 调用 SFC20 CALL "BLKMOV" (SFC20) SRCBLK := "DB_GENERAL".SRC_ANY; RET_VAL := #retVal; DSTBLK := "DB_GENERAL".DST_ANY;跑完之后,用监视表看一下 DB10.DBB0 到 DBB9 的内容是不是被完整复制到了 DB20.DBB0 到 DBB9。复制成功后,你再故意把偏移 6 的字从W#16#8400改成W#16#8300,运行一遍,你会看到 DB20 里全是些莫名其妙的 M 区数据——这就是"区域标识写错"的现场教学。
4. 实战案例:动态复制、批量填充与工艺轴数据镜像
4.1 做一个"任意 DB、任意偏移、任意长度"的复制 FC
把上面的逻辑封装成一个通用 FC,你就可以在 HMI 上任意指定源 DB、目标 DB、起始地址和长度。接口如下:
FUNCTION "FC_COPY_DYN" : VOID VAR_INPUT srcDB : INT; // 源 DB 号 dstDB : INT; // 目标 DB 号 start : INT; // 起始字节地址 count : INT; // 复制的字节数 END_VAR VAR_TEMP retVal : INT; END_VAR实现部分关键的地址拆分逻辑如下:
// 源 ANY 首字 + 重复次数 L 16#1002; T "DB_GENERAL".DBW 0; L #count; T "DB_GENERAL".DBW 2; // 源 DB 号 L #srcDB; T "DB_GENERAL".DBW 4; // 区域标识 + 地址高字节(start 超过 255 时取高字节) L W#16#8400; T "DB_GENERAL".DBW 6; L #start; SRW 8; // 右移 8 位,得到高字节 T "DB_GENERAL".DBW 7; // 覆盖地址高字节 // 地址低字节 + 位地址 L #start; T "DB_GENERAL".DBW 8; // 低字节直接写入偏移 8 L 0; T "DB_GENERAL".DBW 9; // 位地址 0 // 目标 DB 按同样逻辑填到 "DB_GENERAL".DST_ANY,然后调 SFC20这段代码的精髓在于:SRW 8之后存高字节,然后再用原值存低字节。STL 里对字节变量的传送只取累加器的低 8 位,所以T "DB_GENERAL".DBW 7和T "DB_GENERAL".DBW 8这种写法需要你心里清楚自己到底在写哪个字节。我习惯在这两行之间加注释,防止三个月后自己都看不懂。
4.2 用 SFC21 做批量填充:给一大片内存写固定值
复制解决了"搬"的问题,"填"的问题交给 SFC21(FILL)。比如你要把 DB30 的前 100 个字节全部写成 16#55 当作缓冲区初值:
CALL "FILL" (SFC21) BVAL := P#DB31.DBX0.0 BYTE 1; // 填充值放在 DB31.DBB0 RET_VAL := #retVal; BLK := P#DB30.DBX0.0 BYTE 100; // 目标区域如果目标区域要在运行时动态指定,依然用前面手工拼 ANY 的方法,把目标 ANY 的重复次数改成count、地址改成起始字节地址,然后传给BLK参数即可。
一个实用技巧:SFC21 的BVAL最好单独用一个 DB 变量保存填充值,不要直接写常量。因为这样你可以在 HMI 上做一个"填充值"输入框,调试时随便改,不用每次重新下载硬件组态。
4.3 工艺轴数据镜像:一次 SFC20 代替整段 FOR 循环
说一个实际场景。现在很多设备上用西门子博途的工艺对象做运动控制,一组工艺轴的位置、速度数据会存在 DB 里,通常是ARRAY[0..9] OF REAL这种结构。如果你想把当前实际位置数据整体镜像到一个"趋势记录区"或者"HMI 归档区",很多人会写 FOR 循环逐点搬运:
FOR i := 0 TO 9 DO "DB_TREND".pos[i] := "DB_AXES".actualPos[i]; END_FOR;这当然能跑,但每个扫描周期都执行 10 次读写,扫描时间白白浪费。更好的办法是给这个 REAL 数组构造一个 ANY:
L 16#1008; // REAL 类型 T "DB_GENERAL".DBW 0; L 10; // 10 个 REAL T "DB_GENERAL".DBW 2; L 15; // 假设 DB15 T "DB_GENERAL".DBW 4; L W#16#8400; T "DB_GENERAL".DBW 6; L W#16#0000; // DB15.DBD0 T "DB_GENERAL".DBW 8; CALL "BLKMOV" (SFC20) SRCBLK := "DB_GENERAL".SRC_ANY; RET_VAL := #retVal; DSTBLK := "DB_TREND".pos; // 编译器自动生成目标 ANY这样一次 SFC20 调用就把 40 个字节搬过去了,CPU 的块搬运指令是底层优化过的,比 FOR 循环快一个数量级。工艺轴数量越多、数组越长,这个优势越明显。
5. 我踩过的坑:ANY 指针报错排查完整过程
5.1 故障现场:程序调用 SFC20 后直接进 STOP
有一次调试一个搬运工位,程序一执行到 SFC20 就报错停机。监控RET_VAL返回了W#16#80A3一类的错误码。这类错误的字面意思是"数据类型信息无效",但真正的原因五花八门,我当时顺着下面的链路一步步排查,才找到病根。
5.2 坑一:把首字当成字节去存,Byte 变量丢了高字节
我最开始写的代码是:
L 16#1002; T "DB_GENERAL".DBB 0; // 这是错的!T DBB只传送累加器的低 8 位,结果16#10被丢掉了,内存里只剩02 00。ANY 的第 0 字节不是 16#10,系统就不认这是 ANY 指针,要么立即报错,要么把后续字节全读错。
正确的姿势是用T DBW按字传送,或者用T LW 0这种按字操作。记住:ANY 的前两个字节必须作为一个整体来写。
5.3 坑二:重复次数填了字节数,导致越界访问
上面说过的那个"200 个字节填成 200 个 REAL"的案例,其实就是我同事踩的。错误码表现也是 80A3 一类的长度错误。排查方法很简单:在监视表里同时看 ANY 内存区和RET_VAL,把重复次数除以类型宽度验算一遍,心算一下总字节数是否超出目标 DB 的实际长度。
5.4 坑三:S7-1200/1500 的优化块访问与绝对地址冲突
现在很多人用 TIA Portal 直接写 S7-1200/1500,这部分尤其要注意。S7-1200/1500 的新 DB 默认勾选了"优化块访问",优化块里没有固定的物理偏移地址,你写在程序里的DBW 0、DBW 2这种绝对地址根本不允许使用,编译器直接报错。
解决方式有两种:
- 在 DB 属性里取消"优化块访问"勾选,让它变成非优化 DB,才能用绝对地址填 ANY。
- 或者不在 DB 里手工拼 ANY,而是把 ANY 变量声明在 FC 的接口里,通过符号名整体传递。
我个人建议:S7-1200/1500 项目里如果实在要用 ANY,优先用 FC 接口声明,不要用绝对偏移。S7-300/400 没有这个烦恼,但也要遵守"非优化块才能被 ANY 直接寻址"的规则。
5.5 坑四:区域标识和 DB 号组合写错
再讲一个隐蔽的坑:访问 DB 区时,区域 ID 必须写 16#84,DB 号必须写实际 DB 号;访问 M 区时,区域 ID 写 16#83,DB 号必须写 0。两者是配套的。我有一次只把区域 ID 改成了 83,忘了把 DB 号清零,结果 SFC20 拿着"DB 号 10 + M 区标识"去读,直接访问了不存在的 M 扩展区,程序马上 STOP。
排查这种问题时,我习惯用监视表把 ANY 的 10 个字节全部列出来,对照上面那张表逐项核对。字节偏移 4、5 和偏移 6 是连在一起的,千万别只看其中一个。
5.6 避坑排查链路总结
下面是我在项目里固定使用的排查顺序,你可以直接抄:
- 先看
RET_VAL错误码范围,确认是不是 ANY 相关错误; - 打开监视表,把源、目标 ANY 的 10 个字节原样列出来;
- 验证字节 0 是否为 16#10,字节 1 是否为目标类型编码;
- 计算"重复次数 × 类型宽度 = 总字节数",和实际数据区长度对比;
- 核对偏移 4、5 的 DB 号与偏移 6 的区域标识是否匹配;
- 核对起始地址+总字节数是否超出源/目标 DB 的末尾;
- 最后检查源和目标区域是否重叠,重叠时 BLKMOV 的行为可能和预期不一致。
6. 从 STL 的 ANY 到 TIA 的 VARIANT:什么时候还该手写
6.1 ANY 并没有消失,只是多了个更省事的替代品
TIA Portal 在 S7-1200/1500 上推荐用 VARIANT 类型做通用参数传递,它比 ANY 更灵活,还能配合MOVE_BLK_VARIANT这类指令使用。但老项目、老 FC、标准 SFC 里,ANY 仍然是底层事实标准。你不可能因为"有了 VARIANT"就绕过 SFC20 的 ANY 参数要求。
所以我的建议很务实:新项目、S7-1200/1500 项目优先用 VARIANT;S7-300/400 项目、维护老程序、调用标准 SFC 时,老老实实把 ANY 的 10 字节结构啃下来。
6.2 值得手写 ANY 的四个场景
- 做通用配方管理功能:从配方 DB 到工作 DB 的批量加载,源/目标由上位机动态指定。
- 做工艺轴数据归档:把一大片模拟量、位置量、速度量定期镜像到趋势缓冲区。
- 做通信报文缓冲:把通信接收区的一整段数据拷进解析 DB,长度随报文变化。
- 老项目扩展:原 FC 接口里有 ANY 参数,你又不想改原块内部逻辑,只能在调用前动态构造。
6.3 最后分享一个减少出错的小技巧
把所有手工构造 ANY 的代码统一收口到一个全局 DB 里,并给每个字段写清楚注释。我现在的做法是:在DB_GENERAL里放一组"模板 ANY"变量,用SRC_ANY、DST_ANY作为固定工作区,任何 FC 需要动态搬数据时,先填这 10 个字节,再调用 SFC20。这样整个项目里只有一个地方在填 ANY,排查时也只需要盯这一块内存。
我个人的体会是,看懂 ANY 指针之后,STL 里很多用 FOR 循环逐字搬运的代码都可以替换成一次 SFC20 调用,扫描时间肉眼可见地降下来。手写 ANY 之前先把那张 10 字节结构表画在纸上,写错一位比写错一段逻辑更难查——因为逻辑错了会立刻报错,而 ANY 写错了,可能要到设备动作异常才会暴露。