ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MOVE_BLK_VARIANT实战:PLC通信数据块高效搬运与报文解析

MOVE_BLK_VARIANT实战:PLC通信数据块高效搬运与报文解析 搞PLC通信的兄弟应该都干过这种事从TCP或者Modbus收回来一帧报文得赶紧把里面有用的数据拆出来丢到各个功能块要用的DB里。早些年做S7-300的时候我都是写个FOR循环一个一个字节往外搬后来S7-1200/1500普及博途里能直接用MOVE_BLK、MOVE_BLK_VARIANT了情况才好转。今天重点聊MOVE_BLK_VARIANT因为在PLC通信的数据块传输场景里它基本就是目前西门子平台下最省事、最高效的批量搬运工具特别适合做报文分发、缓冲区映射、DB数据对外发布这类活儿。这篇内容不是罗列指令说明而是把我在实际项目里怎么用这条指令解决通信数据搬移问题、有哪些坑、为什么这么写完整捋一遍。适合正在用博途做S7-1200/1500通信开发的工程师也适合被“通信数据量大、程序体量膨胀”折磨的初学者。你看完可以直接把思路抄到自己的项目里。1. 通信搬数据的老办法到底卡在哪1.1 一条条搬CPU时间全耗在循环上我最早做Modbus TCP从站和主站程序时数据区规划得很粗协议里每台设备返回32个寄存器32台设备就是1024个WORD。当时最直观的做法就是循环FOR i : 0 TO 1023 DO DB_Comm.rawData[i] : DB_Master.readBuffer[i]; END_FOR;这种写法不是不能用而是效率低。循环每执行一次就要做一次索引计算、一次越界检查、一次内存读写编译优化再厉害也比不上系统提供的一条搬移指令。数据量小的时候无所谓可通信数据一多主循环扫描周期就会肉眼可见地拉长。尤其当你还在循环里插了类型判断、字节交换之类的逻辑程序变长以后光看代码就够头疼的。还有个问题FOR循环搬运在S7-300老平台上还能忍在S7-1500上明明有更高效的系统指令却还用老办法等于白白浪费了硬件性能。后来我把通信缓冲区的数据搬迁全部改成批量移动指令后周期时间掉了一截代码也干净很多。1.2 MOVE_BLK能批量但类型限制太死博途里有一条老牌指令MOVE_BLK也是做批量复制的。入门时会觉得它比循环省事MOVE_BLK(SRCBLK : DB_Comm.rawData, COUNT : 100, DSTBLK DB_Process.data);但用一阵子就会发现它不够灵活。MOVE_BLK要求源和目标的数据类型一致想复制ARRAY OF BYTE到ARRAY OF BYTE可以想从一个BYTE数组拆出一段变成WORD数组、REAL数组它干不了。真实通信场景里报文通常是按字节排列的但业务程序要的是WORD状态字、REAL工程量、INT计数器这种“原始字节流”到“业务数据结构”的转换MOVE_BLK完全帮不上忙还得靠循环加类型转换一点点抠。另一个限制是MOVE_BLK在FB/FC里做通用封装时非常别扭。我想写一个能够接收任意长度数组的搬运函数形参类型就很难定义因为MOVE_BLK要求实参和形参都是具体的数组类型没法用VARIANT这种万能指针来接。于是要么一个数据类型写一个函数要么把形参定义成带固定上限的大数组又丑又浪费内存。1.3 MOVE_BLK_VARIANT解决了两件大事MOVE_BLK_VARIANT是博途从V13 SP1开始提供的新一代批量移动指令。它最大的特点就是形参采用VARIANT类型也就是说不管你的源数据是ARRAY OF BYTE、ARRAY OF WORD还是某个结构体的一部分只要符合VARIANT指针的指向规则它都能接。这样一来解决了两件大事第一再也不需要为每种数据类型单独写一个复制函数。一个通用FC就能接收不同类型的数据区内部用一条MOVE_BLK_VARIANT指令完成搬运调用的时候把DB变量、结构体成员、数组片段往管脚上一挂就行。第二它可以处理源和目标类型不一致的情况。你要把一个字节数组里的20个字节拆到10个WORD变量里去它可以直接复制并转换你要把通信缓冲区的BYTE数据同步到某段REAL数组只要元素个数对得上也能一条指令办完。说白了MOVE_BLK_VARIANT就是为“通信报文解析”和“数据块之间批量映射”这类需求准备的。我后来的项目里凡是涉及通信接收缓冲区、发送缓冲区、HMI数据发布区、第三方设备寄存器映射能换这条指令的地方基本都换了程序维护成本降了不是一点半点。2. 深入指令内部这些细节不知道容易翻车2.1 管脚定义和SCL调用方式MOVE_BLK_VARIANT在博途里的指令路径是基本指令 - 移动操作 - MOVE_BLK_VARIANT。LAD/FBD里可以直接拖但我个人习惯用SCL因为通信数据处理本来就是逻辑为主SCL看起来更像一段可维护的算法。指令管脚如下管脚类型说明SRCBLKVARIANT源数据区可以是数组、结构体成员、DB变量COUNTINT要复制的元素个数DSTBLKVARIANT目标数据区RET_VALINT错误代码0表示正常SCL里最简单的调用方式是把它当函数用#errCode : MOVE_BLK_VARIANT( SRCBLK : CommDB.rxBuffer, COUNT : 48, DSTBLK : ProcessDB.telegram.raw );注意MOVE_BLK_VARIANT属于函数而不是函数块本身不占背景DB所以可以直接用RET_VAL接收返回值。调用前最好把RET_VAL放到一个INT型临时变量里方便后面统一判断而不是直接忽略返回值。我第一次用的时候就没管RET_VAL结果数据没搬过去查了半天才发现是源区域长度定义短了。这个返回值就是指令给自己的诊断口不接就等于闭着眼开车。2.2 COUNT到底是字节数还是个数必须搞清楚这是最容易翻车的地方。MOVE_BLK_VARIANT的COUNT并不是传输的字节总数而是“数据元素个数”。至于每个元素多大取决于源数据区SRCBLK的元素类型。举两个例子如果源是ARRAY[0..63] OF BYTECOUNT : 48那就是搬48个字节。如果源是ARRAY[0..31] OF WORDCOUNT : 48那就是搬48个WORD实际占96个字节。所以不要一上来就按照报文长度去填COUNT必须先把源数组的元素类型看清楚。我在现场就见过有人把COUNT填成报文总字节数结果源数组总共才64个BYTE他填了120指令直接报访问错误数据一动不动。报文是48字节而源数组是ARRAY[0..23] OF WORD时COUNT应该填48因为它要搬48个WORD吗不对WORD数组的COUNT表示WORD个数48个WORD就是96字节。实际报文只有48字节那COUNT应该填24因为24个WORD正好48字节。这个换算过程不复杂但确实需要在脑子里过一遍。建议做法在程序里不要直接写魔法数用CONST常量定义好“字节长度”和“元素个数”之间的关系或者在变量注释里写明当前COUNT是按照什么类型计的。这样半年后回来看代码不至于对着一个数字发呆。2.3 源和目标数据类型不一致时到底发生了什么MOVE_BLK_VARIANT一个很有吸引力的地方是支持源和目标数据类型不同。比如源是ARRAY OF BYTE目标是ARRAY OF WORD它会把每两个字节拼成一个WORD再存进去相当于边搬边转换。但这里有个隐藏陷阱它的转换方式是“按数值转换”不是“按字节原样映射”。我举一个最典型的反面教材通信收到的报文里有一段IEEE 754格式的浮点数一共12个REAL占48字节。如果源是ARRAY OF BYTE目标直接定义成ARRAY OF REAL然后MOVE_BLK_VARIANT去复制你会得到一堆完全不对的小数。因为指令把每个BYTE当成0到255的整数转换成REAL后得到的是0.0到255.0范围内的浮点数并没有把这4个字节解释成IEEE 754编码。按位原样映射的正确做法是借助博途里的AT视图。我在DB里建一个结构体先用BOOLEAN/BYTE数组接住原始字节再在这个字节数组上增加一个AT视图把同一片存储区映射成REAL数组。在博途DB编辑器里这样操作在“ProcessDB”里新增一个结构体变量名字叫telegram类型是Struct。在telegram下先建一个raw类型是ARRAY[0..47] OF BYTE。右键raw选择添加AT视图然后新建一个变量values类型是ARRAY[0..11] OF REAL。这样raw和values共享同一段物理存储区。程序里先用MOVE_BLK_VARIANT把通信缓冲区的48个字节搬到telegram.raw再去读telegram.values拿到的就是用IEEE 754解析好的浮点数。全程不需要循环不需要字节拼接也不需要写转换函数。这个思路解决了我之前很大的痛点以前解析通信报文碰到浮点数就得自己写4字节转REAL的函数现在一个AT视图加一条MOVE_BLK_VARIANT就搞定了。2.4 VARIANT形参在FB/FC里的正确姿势MOVE_BLK_VARIANT真正厉害的地方在于它可以和用户自定义函数块的VARIANT输入参数配合。这样你可以写一个通用的“通信数据分发”函数调用时传入不同的数据源和目标函数内部不需要关心具体是什么类型。我常用的做法是建一个FC接口定义如下输入参数 pSource : Variant输入参数 iCount : Int输入参数 pDest : Variant输出参数 eCode : Int内部实现就一句话#eCode : MOVE_BLK_VARIANT( SRCBLK : #pSource, COUNT : #iCount, DSTBLK : #pDest );然后在主程序里反复调用每次传入不同的数组区和长度#fcMove( pSource : CommDB.rxBuffer, iCount : 24, pDest : ProcessDB.statusWords );这种写法关键的好处是你不需要为每个数据块重复写复制逻辑整个项目里只有一处地方在真正执行“搬数据”。后面如果想把每次搬运都加上日志、错误统计只需要改这一个FC全项目都跟着生效。不过要留意VARIANT形参不能直接拿去做某些算术操作它的主要用途就是把实参原样转交给MOVE_BLK_VARIANT这类系统指令。另外SCL编译时对VARIANT实参的检查比较宽松运行时才会发现类型或地址问题所以调试阶段必须盯着RET_VAL别等数据丢了才回去翻。3. 实战TCP报文到业务DB的一次性分发3.1 场景和变量规划下面用一个真实度很高的场景展示完整过程。假设现场有一台S7-1500做主站通过TCP接收一台第三方设备发来的报文报文长度固定48字节。48字节里前4字节是设备状态字两个WORD中间32字节是12个浮点测量值最后12字节是6个INT型计数器然后还有2字节的CRC尾巴。总而言之典型的“固定报文 多类型数据”通信结构。我规划的变量如下“CommDB”负责通信底层rxBuffer : ARRAY[0..63] OF BYTE存放TCPUDP通信收上来的原始报文。“ProcessDB”负责业务逻辑telegram : Struct内部包含raw字节视图和values浮点视图。statusA : WORD设备状态字。statusB : WORD备用状态。counters : ARRAY[0..5] OF INT计数器数组。“AlarmDB”负责告警联动后面可能还要被HMI读取。这样分工的好处是底层通信缓冲区、业务数据、对外发布区域完全分离互不干扰。3.2 核心SCL代码实现接收到完整一帧后先在通信功能的完成信号里触发一次分发。分发逻辑如下IF #recvDone THEN // 把整帧报文搬到业务结构的原始字节区 #err : MOVE_BLK_VARIANT( SRCBLK : CommDB.rxBuffer, COUNT : 48, DSTBLK : ProcessDB.telegram.raw ); // 搬运成功后再做数据拆分 IF #err 0 THEN // 前4字节是两个WORD用MOVE_BLK_VARIANT按WORD拆过去 #err : MOVE_BLK_VARIANT( SRCBLK : CommDB.rxBuffer[4], COUNT : 2, DSTBLK : ProcessDB.statusA ); // 从报文偏移20起的6个INT映射到计数器数组 // 偏移计算4字节状态 32字节浮点 36字节再往后12字节才是6个INT #err : MOVE_BLK_VARIANT( SRCBLK : CommDB.rxBuffer[36], COUNT : 6, DSTBLK : ProcessDB.counters ); END_IF; END_IF;这里有几个细节说明一下。第一条MOVE_BLK_VARIANT搬运了48个字节目标不是ARRAY OF BYTE吗目标我用了ProcessDB.telegram.raw它是一个ARRAY[0..47] OF BYTE所以COUNT填48就是48个字节。搬完之后telegram.values已经在AT视图里自动对应好12个浮点数业务程序后续直接用telegram.values[0]这类写法访问就行不用再写任何浮点解析函数。第二条MOVE_BLK_VARIANT演示了数组片段的用法。源我写的是“CommDB”.rxBuffer[4]这个写法表示从数组下标4开始的一段区域VARIANT能够识别这个起点。COUNT : 2因为目标是两个WORD变量而“CommDB”.rxBuffer[4]处按BYTE算每两个字节组成一个WORD所以COUNT2就表示两个WORD。第三条MOVE_BLK_VARIANT从偏移36处开始复制6个INT元素到计数器数组。这里COUNT6目标ARRAY[0..5] OF INT源是按BYTE数组取的指令会自动把12个字节合并成6个INT。是不是感觉自己被一条指令取代了以前二十多行代码3.3 实际调试记录和性能对比我在1511-1PN上实测过这种写法。以前用FOR循环逐字节解析48字节报文主扫描周期在收到报文那个瞬间会冒出一个明显的小尖峰数据量一旦上了几百字节尖峰会更高。改成MOVE_BLK_VARIANT加AT视图之后这段解析逻辑从几十条指令变成几条系统搬移调用扫描周期的波动基本看不到了。我没有在项目现场专门跑高精度计时基准去测微秒级数值因为工业现场更关心的是“周期稳定”和“程序可维护”。但从PLC程序体的印象流对比来说MOVE_BLK_VARIANT带来的改善是肉眼可见的尤其当你有多个通信通道、多个数据块需要同步时程序体积和出错概率都能降下来。调试时还有一个经验如果通信完成信号和MOVE_BLK_VARIANT执行之间有严格的时序要求建议把分发动作放在定时中断OB里或者用信号沿触发不要在每个扫描周期都无条件搬数据。通信缓冲区可能在两个扫描周期之间被覆盖不注意时序就会出现“明明收到数据了搬出来却是旧内容”的情况。4. 配套场景从HMI、组态王到第三方设备的数据快速打通4.1 给昆仑通泰触摸屏导DB块数据时的加速思路昆仑通泰的触摸屏在工控圈用得很广但它老版本组态软件对西门子DB块的支持一直不是特别友好。很多情况下它不能直接解析优化访问的DB块你辛辛苦苦在博途里建了各种符号名变量到了MCGS那边要么看不到要么只能靠绝对地址去蹭。实际项目里我常用的方案是在PLC里单独建一个“HMI_ExportDB”专门给触摸屏读。这个DB块用非优化访问变量约定好排列顺序。每个主循环或固定周期内用MOVE_BLK_VARIANT把业务数据源整体同步过去MOVE_BLK_VARIANT( SRCBLK : ProcessDB.telegram.values, COUNT : 12, DSTBLK : HMI_ExportDB.displayValues );触摸屏只需要读HMI_ExportDB这一个区域变量地址固定数据刷新稳定。你也不用在触摸屏端做复杂的地址映射。业务DB怎么优化访问都行反正对外发布靠一块专门的DB两边互不打扰。4.2 组态王加PLCSIM仿真时数据区的映射很多人问组态王SCADA怎么用博途来仿真其实核心问题是通信链路和数据区可见性。PLCSIM仿真的时候组态王走以太网驱动连到虚拟网卡但优化访问的DB它一样读不到。如果你只是开发阶段想验证画面和逻辑最省事的做法是PLC里放一个“SimExportDB”把需要展示的模拟量数组、状态字、报警数组全部用MOVE_BLK_VARIANT同步进去。仿真用的数据区越简单越好。我一般会在FB接口里定义输出参数数组然后在OB1里统一同步到仿真DB。这样画面组态时只面对一个固定的、非优化的数据块无论是填测试值还是看趋势曲线都方便。等到换真机联调时只需要把同步目标换成真正的外部通信DB程序主体不用动。4.3 和变频器Modbus总线联动时的批量寄存器映射现场用一台S7-1500通过Modbus控制32台变频器是很常见的需求。博途里用MB_MASTER去读保持寄存器读回来的数据通常是一段连续的WORD数组。假设每台变频器需要4个寄存器分别对应状态字、频率给定、电流、报警码32台就是128个WORD。没有MOVE_BLK_VARIANT的时候我得写一个超级长的CASE或者FOR循环按设备号逐个把数据从数组里摘出来再塞到每台设备对应的结构体里。中间还要处理字节序、上下限钳位代码量很大。用MOVE_BLK_VARIANT之后我可以把“读取缓冲区”和“设备结构数组”的元素宽度对齐然后按每台变频器调用一次搬运FOR i : 0 TO 31 DO MOVE_BLK_VARIANT( SRCBLK : ModbusCommDB.readBuffer[i * 4], COUNT : 4, DSTBLK : VFD_DATA.devices[i].status ); END_FOR;甚至可以把源按每台设备切片把目标指向结构体数组的对应字段。这样看起来仍然有循环但循环里只有一条系统指令效率远高于逐寄存器手动搬。再加一个外围判断哪台设备通信超时就跳过哪台的同步逻辑清晰得很。5. 高频故障与排查我踩过的那些坑5.1 RET_VAL非0怎么排查用MOVE_BLK_VARIANT一段时间后你会发现大部分问题都集中在RET_VAL返回非0也就是指令执行失败。最常见的两类原因一是源或目标地址超出范围。比如“CommDB”.rxBuffer定义成ARRAY[0..63] OF BYTE你COUNT填了100那肯定越界。指令在运行时才检查这个所以编译阶段根本发现不了。排查方法是把源数组、目标数组的元素数目和COUNT逐个对一遍。我习惯在代码注释里直接写清“源数组长度64字节本次搬运48字节”减少后续误操作。二是数据类型不兼容。虽然MOVE_BLK_VARIANT支持不同类型转换但也不是所有组合都能转。比如从ARRAY OF BOOL往ARRAY OF REAL搬这种转换就不在支持范围内。遇到这种组合老老实实用AT视图把原始字节接住再用AT视图去映射想要的数据类型。排查建议写一个简单FC封装MOVE_BLK_VARIANT把RET_VAL、当前时间、源标识、目标标识一起存到一个诊断DB里。现场出故障先看诊断DB里最后一次报错的位置和数据很快就能锁定问题区域。5.2 优化块访问和绝对地址的兼容问题TIA里新建的DB默认启用优化块访问好处是PLC可以按符号名自由管理变量不用管偏移量。但优化块对第三方系统不友好尤其是HMI、组态王、Modbus TCP服务端经常找不到想要的地址。解决思路就是“内部优化外部映射”。内部业务DB保持优化访问专心用MOVE_BLK_VARIANT和大结构体做逻辑对外发布时再单独建非优化DB用MOVE_BLK_VARIANT把内部数据同步过去。这样既享受了优化DB的便利又保住了外部系统的访问能力。如果你从老项目升级过来遇到过“原来DB地址是DB1.DBB0现在打开一看根本找不到绝对地址”的问题八成是启用了优化块访问。要对外提供服务时在DB属性里把“允许通过PUT/GET从HMI/OPC访问”勾上或者直接新建非优化DB来承接。5.3 博途在线连接和“添加设备就转圈”的处理经验很多新人装好博途准备下载程序时点“添加设备”就一直在转圈。这个和MOVE_BLK_VARIANT本身没直接关系但确实会影响程序联调。我遇到这种问题的排查顺序一般是第一确认电脑网卡和PLC的IP在同一网段最好给本地连接固定一个IP不要用DHCP。第二把Windows防火墙临时关掉或者把博途加入白名单。博途扫描设备走的是底层以太网协议防火墙拦截后界面就会一直转圈。第三如果是笔记本把不用的虚拟网卡、VMware网卡全部禁用。这些虚拟网卡会干扰西门子设备发现机制排错时很容易让人怀疑人生。第四实在不行在博途左侧“在线访问”里直接双击对应的网卡类型让它单独扫描一次可访问设备。这一步能直接看到底层能不能找到PLC比“添加设备”的转圈提示靠谱得多。5.4 版本兼容速查和注意事项MOVE_BLK_VARIANT从博途V13 SP1开始提供S7-1200需要固件4.0以上S7-1500全系列都支持。如果你还在用V13之前的古董版本指令列表里是找不到它的。V16、V17、V18、V20这些版本我都用过指令用法基本一致只是界面和块属性看起来略有不同核心调用逻辑没有变化。还有一个版本相关的坑如果在老版本项目里用了MOVE_BLK_VARIANT后来用新版博途打开一般没问题但反过来新版博途里的某些数据块属性、AT视图定义拿回老版本打开可能会丢失或报错。跨版本升级项目前最好先备份然后在虚拟机里用新版本验证一遍。另外COUNT用的是INT最大32767。如果一次性要搬的数据超过这个数比如要搬运10万个字节MOVE_BLK_VARIANT做不到一次性完成得分段搬或者考虑用S7-1500的优化存储结构。实际项目里也很少有人会一次搬几万字节但知道这个上限设计缓冲区大小时心里就有数。6. 一点个人经验什么时候别硬用这条指令最后说句实话MOVE_BLK_VARIANT也不是万能的。如果你的数据量特别小比如每次只搬两三个WORD那用不用它差别不大循环反而更直白。还有如果你需要按位处理数据比如把多个BOOL组合成一个BYTE再发送MOVE_BLK_VARIANT帮不上忙那种情况还是老老实实做位操作。但凡是“整段数据从一个区域移动到另一个区域”的活儿别犹豫优先考虑MOVE_BLK_VARIANT。它能让你的程序更短、更好读也更能发挥S7-1200/1500平台的优势。配合AT视图处理字节到浮点的映射后通信报文解析这块基本可以告别手写转换函数的日子了。我自己的项目里这条指令已经成为通信程序的标配每次新开一个通信项目第一个建好的功能块就是它。
RELATED READING

延伸阅读

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