
第一次在客户程序里看到连续三行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。这种指针只告诉你数据在哪不告诉你数据是什么、有多少。它相当于一个门牌号——你知道该去哪间房但不知道房间里装的是桌子还是椅子更不知道要搬几张。问题来了西门子标准功能里像 SFC20BLKMOV、SFC21FILL这类块必须知道要处理的数据长什么样、有多少个才能正确操作。假如 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.DBB2010 个字节0Syntax ID固定 16#1016#101数据类型编码Type ID16#02BYTE2重复次数高字节16#003重复次数低字节16#0A104DB 号高字节16#005DB 号低字节16#0ADB106存储区标识16#84DB 区7字节地址高字节16#008字节地址低字节16#14209位地址16#00看到第 2、3 字节了吗重复次数 10 在内存里是00 0A高位在前DB 号 10 也是00 0A。这说明西门子用的是大端字节序。写代码时必须按字或双字来整体传送如果用字节传送10存到高字节还是低字节很容易搞反。2.3 常见数据类型编码对照表第 1 字节的类型编码是整个 ANY 的灵魂。我把最常用的一组列出来类型编码完整首字常量对应数据类型单元素占用字节16#0116#1001BOOL1 位16#0216#1002BYTE116#0316#1003CHAR116#0416#1004WORD216#0516#1005INT216#0616#1006DWORD416#0716#1007DINT416#0816#1008REAL416#0E16#100ESTRING不定16#1316#1013ANY嵌套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×4200 字节。3.2 第二步填重复次数区分元素个数和字节数重复次数是新手最容易翻车的字段。它的单位是元素个数不是字节数。换句话说操作 BYTE 数组时元素个数和字节数相等。操作 INT/WORD 数组时重复次数 字节数 ÷ 2。操作 REAL/DWORD 数组时重复次数 字节数 ÷ 4。我见过一位同事想把 200 个字节清零结果重复次数填了 200类型却写了 16#1008REALCPU 直接按 200 个 REAL 去处理一下子越界访问了 800 字节程序立刻报错。所以填完重复次数后一定要自己心算一遍总字节数。3.3 第三步填 DB 号、存储区标识和起始地址剩下三组数据按偏移 4、6、8 分别写入偏移 4-5DB 号。如果访问的是 I/Q/M 区而不是 DB 区这里必须写 0。偏移 6存储区标识。I 区是 16#81Q 区是 16#82M 区是 16#83DB 区是 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.DBB010 个字节 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.DBB010 个字节代码结构完全一样 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 做批量填充给一大片内存写固定值复制解决了搬的问题填的问题交给 SFC21FILL。比如你要把 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 数组构造一个 ANYL 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#84DB 号必须写实际 DB 号访问 M 区时区域 ID 写 16#83DB 号必须写 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 项目优先用 VARIANTS7-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 写错了可能要到设备动作异常才会暴露。