首页 / 资讯中心 / 文章详情

博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解

博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解 ★ FEATURED ARTICLE
用博途1500PLC判断源字符串中是否含有子字符串这个需求听起来简单我第一次在现场做的时候也以为一条指令就完事了。那是条包装线的扫码工位读码器通过Profinet把条码送进PLC我需要在ABC240315001XYZ这样的字符串里判断有没有2403这个月份编号有的话走甲通道没有走乙通道。当时的直觉是用FIND函数处理后来做标准化模板时才发现很多人真正想搞清楚的是FORMID怎么做因为在字符长度不定、大小写不固定、还要找多个位置的场景里用基础指令组合往往是更通用的底层方案。这篇文章就把FORMID函数的实现原理、完整代码、边界坑一次讲完适合刚开始玩1500PLC的朋友也适合准备把字符串处理逻辑封装进标准库的工程师参考。1. 为什么放着现成的FIND不用偏要写FORMID循环1.1 现场需求复盘一个扫码判断的典型场景先还原一下我在现场遇到的真实需求。读码器读取的条码不是固定格式项目前期供应商给的样例是ABC240315001XYZ后来实际生产时条码前面多了一位工厂代码变成XABC240315001XYZ。这就意味着生产日期字段2403不在固定位置你要是用LEFT取前几位取出来的东西就完全不对用RIGHT从尾部取也会因为流水号位数变化而错位。这种情况下唯一的办法就是在整串字符串里搜索判断源字符串里有没有特定的连续字符片段。很多类似场景都有这个特征目标子串一定存在或一定不存在但你不知道它从第几个字符开始。除了扫码常见的还有HMI输入校验——操作员录入一长串工艺参数必须先判断字符串中是否包含RUN才允许启动再有就是报文解析从通信数据里找出某个关键字再截取后续内容。这类问题的共同点是包含判断只是第一步后面往往还有更多处理。如果第一步就用一个封装好的指令带过去后续扩展时反而被绑住。1.2 FIND函数能做90%的事为何还是要学FORMID必须承认TIA Portal里确实提供了现成指令FIND用起来基本是一行搞定#pos : FIND(IN : #sourceStr, L : #targetStr); IF #pos 0 THEN // 找到了 END_IF;功能上FIND已经能完成判断是否包含这件事。那我为什么还建议你把FORMID这套自己写一遍关键在于工程上的需求往往会从是否包含升级。FIND只能返回第一个匹配位置。如果条码里同一个字段出现多次或者你需要知道所有出现的位置FIND就无解。再比如你希望从源字符串第5个字符之后开始搜索跳过固定前缀FIND也没有提供起点参数。还有大小写不敏感的场景FIND直接比较依然敏感你还是得自己处理字符串变换。FORMID的价值不是替代FIND而是让你手里有一份完全可控的查找逻辑。一旦需求变成找第二个忽略大小写从第N位找你改的是自己的循环而不是去查库函数支持哪些参数。我在封装标准库的时候倾向把这类基础能力用底层逻辑自己实现因为不同版本、不同固件对FIND等字符串指令的行为细节有差异自己写一套反而更可控。1.3 这个需求适合哪些人参考如果你是刚开始接触1500PLC、正在学SCL语言这篇文章帮你把STRING类型、LEN、MID、FOR循环串起来是一个很好的综合练习。如果你在做设备标准化程序要把字符串包含判断封装成一个通用功能块这套代码改改接口就能直接用。如果你在准备设备厂家或设计院的笔试面试用FORMID实现子串查找几乎是字符串环节的必考题理解滑动窗口的思路比背代码有用得多。至于只想要快速结果、不打算深究原理的项目那你直接用FIND就够了完全不丢人。2. 动手之前博途STRING在PLC里的真实存储结构2.1 公式化记忆STRING的布局在1500PLC里STRING变量不是普通的数组它自带管理信息。一个声明为STRING[254]的变量在数据块里实际占用256个字节前面部分字节用来记录最大长度和当前有效长度从后面开始才是字符数据本身。你可以记住一个粗算公式STRING占用字节数 ≈ 声明的最大字符数 2。为什么要强调这个因为很多现场问题是这么来的你声明了STRING[10]觉得10个字符够用但通信发送方一次性传过来13个字符PLC在赋值阶段就把超出的部分直接截断了你后面怎么查找都找不到本来存在的子串。这不是逻辑错是存储容量不够。我习惯把所有可能需要接收外部数据的字符串变量直接声明成STRING[254]至少覆盖绝大多数工业字符串长度避免这类隐性截断。不过要说明一点不同固件版本的STRING管理字节细节略有差异但核心规律一致不要手动去读写那前几个字节统一用LEN(取长度)、MID(截取)这类指令操作编译器会根据当前版本自动处理。2.2 MID和LEN的计数规则与容易踩的索引坑关于索引高级语言程序员最容易在PLC里栽跟头STRING的字符索引从1开始不是0。MID(IN : #sourceStr, L : 3, P : 1)表示从第1个字符开始截3个字符也就是取前三个字符P : 0在PLC里属于非法位置程序运行会报错或返回空串。LEN函数返回的是字符串当前实际包含的字符个数比如LEN(ABC240315001XYZ)返回15。这个当前长度和你声明的最大长度是两回事一个字符都没有时LEN返回0。截取操作里P L - 1不能超过源字符串当前长度。举个例子源字符串15个字符你想从第14位开始截5个字符14 5 - 1 18超出15这时候MID的实际行为可能会返回空串或截断后的残串不同版本略有差异但绝不能依赖这个特性。我在调试字符串程序时第一步永远是先看LEN结果对不对。长度不对后面的截取和比较全是白费。你先在监控表里单独看#srcLen的值确认它和实际字符数一致再往下排查。2.3 和高级语言字符串的差异如果你熟悉C或者Python会发现PLC字符串有几个别扭的地方。第一是索引从1开始前面刚说过。第二是字符串变量声明时必须指定最大长度相当于C语言里char数组加上长度上限而Python那种动态字符串在PLC里不存在。第三是赋值截断静默发生字符串太长不报错只截断这最容易埋雷。拿Python做类比hello[0:2]取he而博途里MID(hello, 2, 1)取he前者是左闭右开起步0后者是左闭右闭再加偏移。理解这个差异之后写SCL的字符串处理就不容易串味道了。3. 核心算法思路滑动窗口法拆解子字符串查找3.1 用传送带比喻理解滑动窗口判断源字符串是否含有子字符串这个问题的核心可以翻译成另一个问题能不能在源字符串中找到一段连续字符和目标子串一模一样我习惯拿传送带打比方。把源字符串想象成传送带上依次排列的盒子每个盒子放一个字符目标子串则是一个固定长度的检测框一次性罩住N个盒子。最开始把检测框对准传送带最左端检查框里的N个字符是否完全等于目标如果不等把检测框向右平移一格再检查一直推到检测框右边界超出传送带末端为止。这个检测框每平移一次就是FOR循环里的一次迭代。每迭代一次用MID函数把检测框对应的那N个字符截出来和目标子串做一次比较。整个过程没有用到任何魔法指令就是机械地逐格比对但绝对可靠。3.2 比较次数到底怎么算一个必须写进注释的公式既然框是固定长度它不可能无限向右移动。设源字符串长度是srcLen目标子串长度是subLen那有效比较次数就是比较次数 srcLen - subLen 1这个公式建议直接写进代码注释不然三个月后自己回来看都容易愣一下。为什么是1举个例子源字符串HELLOWORLD长度10目标子串WORLD长度5。检测框长度5最左端在第1个字符最右端能到的位置是第10-516个字符。位置6时框住的是第6到第10个字符也就是WORLD正好匹配。如果推到位置7框住第7到第11个字符而第11个字符根本不存在。所以起点位置只能是1到6共6次比较。如果目标子串比源字符串还长srcLen - subLen 1变成负数循环根本不该执行结果直接判为不包含。这个边界条件的处理我在第5章详说。3.3 从算法到SCL伪代码在写完整代码之前先看一层伪代码逻辑就清楚了// 初始化还没找到 Found : FALSE; // 取两个字符串的实际长度 srcLen : LEN(Source); subLen : LEN(Target); // 只有目标非空且目标长度小于等于源长度才有必要循环 IF (subLen 0) AND (subLen srcLen) THEN FOR i : 1 TO (srcLen - subLen 1) DO // 从源字符串第i位开始取subLen个字符 temp : MID(Source, subLen, i); // 如果截出来的临时串和目标串相等 IF temp Target THEN Found : TRUE; EXIT; // 找到第一个就可以退出循环 END_IF; END_FOR; END_IF;到这里核心思路已经落地了一半。伪代码里的变量具体用什么类型、在哪里声明、调用方式是什么下一步把它变成能在博途里跑的完整程序。4. 完整实现FC块搭建、代码逐段解读与在线调试4.1 建FC块与变量表这个功能没有保持数据的需求我建议用FC函数而不是FB功能块调用时不需要背景数据块干净。在博途里新建一个FC命名为FC_StrContains接口变量如下变量名数据类型方向说明SourceStrSTRING[254]Input源字符串TargetStrSTRING[254]Input目标子字符串ContainsBOOLOutputTrue表示包含srcLenINTTemp源字符串实际长度subLenINTTemp目标子串实际长度iINTTemp循环变量tmpStrSTRING[254]Temp临时截取串接口里SourceStr和TargetStr都声明成STRING[254]避免长度不够截断的问题。临时变量tmpStr也尽量给到254因为MID截回来的字符串长度可能超过你预想声明短了会直接截断。4.2 完整SCL代码// 功能判断源字符串 SourceStr 是否包含子字符串 TargetStr // 原理滑动窗口 MID截取 FOR循环逐位比较 // 返回Contains True 表示包含 Contains : FALSE; // 取当前实际长度不要用声明的最大长度 srcLen : LEN(IN : SourceStr); subLen : LEN(IN : TargetStr); // 条件过滤 // 1. subLen 0 排除空子串工程上不把空串视为有效匹配 // 2. subLen srcLen 保证待比较窗口不超出源串范围 IF (subLen 0) AND (subLen srcLen) THEN // 关键公式窗口起点只能从 1 到 srcLen - subLen 1 FOR i : 1 TO (srcLen - subLen 1) DO // 从源字符串第 i 个字符开始截取 subLen 个字符 tmpStr : MID(IN : SourceStr, L : subLen, P : i); // 截出来的临时片段与目标子串逐字符比较 IF tmpStr TargetStr THEN Contains : TRUE; EXIT; // 已经找到了不需要继续循环 END_IF; END_FOR; END_IF;4.3 代码逐段解读为什么这样写先看第一个判断条件为什么要求subLen 0理论上讲空字符串是任何字符串的子串但在工程逻辑里没有意义。如果TargetStr传了个空串我们期望结果是不包含而不是True否则后续分支会跑出莫名其妙的结果。再看循环终值srcLen - subLen 1。终值在进入循环时只计算一次如果循环体内修改了srcLen或subLen不会影响循环次数。这里我们在循环体里没改任何长度变量所以安全。为什么用MID而不是LEFT、RIGHT因为LEFT只能从最左边取RIGHT只能从最右边取只有MID支持任意起始位置。滑动的核心就是起始位置在变化所以MID是唯一合适的取串工具。比较tmpStr TargetStr在SCL里是允许的两个STRING类型直接比较内容。但有一个潜在细节如果两个字符串变量声明的最大长度不同比较结果是否受影响我在TIA Portal V16、V17环境里实测内容相同且有效长度相同的情况下比较返回True。如果你遇到诡异的不相等先检查两个字符串的有效长度是否一致比如某个变量末尾多了空格。EXIT语句是个好习惯。源字符串几百个字符时如果第一个位置就匹配成功后面几百次MID就没必要执行了。这个在通信报文解析这种大字符串场景里性能能差出好几倍。4.4 调用与在线监视在OB1里调用FC实参从数据块取FC_StrContains( SourceStr : HMI_Data.ScanCode, TargetStr : 2403, Contains HMI_Data.HasTarget );这里ScanCode是HMI或通信收到的条码字符串目标串可以直接写字面量。在线调试时我强烈建议使用监控表强制赋值而不是干等现场信号把SourceStr强制为ABC240315001XYZTargetStr强制为2403然后运行看看Contains是否为True。为了验证循环过程可以在SCL编辑器的tmpStr : MID(...)这行设置断点在线单步执行观察每一步的i值和tmpStr当前值。你会很直观地看到当i等于4时tmpStr变成了2403然后Contains被置位随后EXIT跳出循环。这里有一个小坑TIA Portal的SCL断点功能要求CPU在线且程序已下载不是所有版本的Step 7都默认开启断点调试。如果断点不可用就退而求其次在IF tmpStr TargetStr前后加两个赋值语句把当前的i值存到一个监控变量里硬数循环过程也行。5. 六个最容易出问题的边界场景与处理办法5.1 空字符串和目标子串为空第一种情况源字符串为空目标子串非空。srcLen 0subLen 0条件subLen srcLen不成立结果直接为False合理。第二种情况源字符串非空目标子串为空。subLen 0被subLen 0挡在循环外结果为False。有人会问空串是不是应该算作包含从字符串理论角度说是但从设备运行安全角度我坚决不建议把空串当成有效匹配。现场的条码字段不可能为空一旦为空更应该认为是数据异常走异常分支而不是走包含匹配成功的分支。第三种情况两者都为空。结果False同上。5.2 子串长度大于源字符串这时srcLen - subLen 1可能是0或负数。负数的FOR循环在SCL里的行为是什么按我的经验TIA Portal的FOR循环如果终值小于起始值循环体一次都不执行结果Contains保持False。逻辑上没问题但我不建议依赖这个行为因为不同版本编译器在这块的处理并不完全一致。最稳妥的就是代码里先判断subLen srcLen条件不满足就直接跳过循环。这也是上面代码里嵌套一层IF的原因。5.3 匹配位置在开头和结尾开头匹配i 1时MID从第1个字符开始截取subLen个字符正好和目标一样。这是最理想的情况循环第一次就命中EXIT直接跳出。结尾匹配i srcLen - subLen 1时MID从最后一个有效起点开始截取取到的正好是源字符串最后几个字符。这个位置是循环的最后一次迭代所以不会漏掉。要特别注意绝对不能把循环终值写死成srcLen否则最后一次截取会越界MID在越界时的行为不可靠。老老实实用srcLen - subLen 1。5.4 大小写敏感问题SCL的字符串比较是大小写敏感的。ABC和abc比较结果是不等于。现场条码经常出现大小写混用的情况供应商这个月发来的条码是大写下个月换了个批次就变成小写了。最省心的处理方式我建议在上位机或HMI侧先把字符串统一转成大写或小写再传给PLC比较。1500PLC里做大小写转换比较麻烦虽然可以逐字符读出来改ASCII码但涉及到在STRING内部修改指定字符代码量不小而且用传统方式给STRING变量内部字符赋值挺别扭不如在上位机处理干净。如果你必须在PLC里做大小写不敏感比较有一个变通思路分别对源字符串和目标子串逐字符转换为大写后再比较。转换的逻辑是遍历每个字符判断ASCII码是否在小写字母范围内是则减32。这个思路本身不难但要注意遍历时使用LEN获取实际字符数且只针对ASCII字符有效。5.5 中文与多字节字符的处理STRING类型本质上是按字节存储字符串的中文汉字在GBK编码下占用2个字节。问题来了LEN和MID在处理中文时按什么单位计算按照我在1500PLC上的实际经验LEN对包含中文的STRING返回的结果可能和字符数对不上MID在截取时也可能出现半个汉字、乱码的情况。原因在于这些字符串指令对STRING类型是按字节位置工作的一个汉字占2个字节当你从某个偏移量截取时很可能正好落在汉字的中间字节上。所以明确建议如果现场可能输入中文不要用这套STRING方案做包含判断改用WSTRING宽字符串类型。WSTRING每个字符占2字节对中文的支持更完善LEN和MID按字符计算也更符合直觉。但WSTRING在通信和HMI上需要额外注意格式匹配这里不展开。简单记住一句话只处理ASCII码和数字用STRING很省心要处理中文绕开STRING。5.6 STRING长度超限与截断这是我最想强调的隐性坑。你把一个字符串赋给STRING变量时如果字符串实际长度超过声明最大长度PLC不会报错而是静默截断。比如声明STRING[10]赋值为ABCDEFGHIJKLMN实际存进去的只有ABCDEFGHIJ后面四个字符直接丢了。这个坑在通信场景特别常见。Profinet或Modbus传输过来的字符串长度往往由对方设备决定你无法保证永远在10个字符以内。一旦截断长度对不上、内容对不上你在PLC里排查半天找不到原因最后发现是声明长度不够。我的习惯是所有接口字符串统一声明为STRING[254]存储变量如果确实担心DB空间占用再按实际需求收紧。但接口参数、临时变量一律宽松。一行代码省几个字节不值得拿一次现场故障来换。6. 进阶玩法返回所有匹配位置与性能优化思路6.1 从是否包含升级为返回所有位置FIND做不到的功能FORMID天生就会。如果你需要知道目标子串在源字符串中出现的所有位置只需把上一版的找到即退出改成找到就记录然后继续循环。// 假设已经声明了输出数组 matches : Array[0..99] OF INT // matchesCounter 记录找到次数 matchesCounter : 0; FOR i : 1 TO (srcLen - subLen 1) DO tmpStr : MID(IN : SourceStr, L : subLen, P : i); IF tmpStr TargetStr THEN matches[matchesCounter] : i; // 记录匹配起始位置 matchesCounter : matchesCounter 1; // 注意不执行 EXIT继续往后找 END_IF; END_FOR;这段代码里数组下标从0开始计数存的是匹配位置1、匹配位置2……如果matchesCounter为0就表示一个位置都没找到。这种能力和FIND函数相比是维度上的优势比如报文里相同关键字出现多次你想每次出现都触发一次处理逻辑就得用这种方案。需要注意数组大小限制。源字符串最长254个子串但匹配次数最多也不会超过128次至少子串长度为1254个字符最多254个匹配位置数组可以声明到254。声明数组时留足余量避免运行时越界。6.2 简单有效的性能优化技巧如果源字符串很长比如几百个字符每次都执行一次MID截取整个子串再比较多少有些浪费。一种常见的提速思路是首字符过滤。在截取整个子串之前先判断源字符串当前位置的字符是否等于目标子串的第一个字符。如果不相等说明这段不可能匹配直接跳过MID节省一次截取操作。FOR i : 1 TO (srcLen - subLen 1) DO // 快速过滤当前字符不等于目标首字符直接跳过 IF SourceStr[i] TargetStr[1] THEN tmpStr : MID(IN : SourceStr, L : subLen, P : i); IF tmpStr TargetStr THEN Contains : TRUE; EXIT; END_IF; END_IF; END_FOR;S7-1500的SCL环境里SourceStr[i]这种索引方式可以访问STRING变量中第i个字符返回Char类型。首字符过滤对于随机分布的字符串能省掉大量MID调用。不过如果目标子串很短、源字符串也不长这点性能提升不大代码可读性略有下降。我的建议是源字符串超过100个字符再考虑这个优化小字符串保持简单逻辑优先。6.3 从第N位之后开始查找的扩展另一个常见需求是从源字符串不从头找而是从第N位之后开始找。这在解析固定报头报文时特别常见前面8位是帧头你要找的关键字在帧头之后才可能出现。在FC接口里加一个Input参数SearchStart : INT然后FOR循环从SearchStart开始FOR i : SearchStart TO (srcLen - subLen 1) DO // 循环体不变 END_FOR;但调用前必须做两个判断SearchStart不能小于1否则相当于从头找SearchStart不能大于srcLen - subLen 1否则循环一次都不执行等于白找。实际项目里这两个边界很容易被忽略传进来的SearchStart是HMI上操作员填的数字随便填个100源字符串总共才20个字符那结果必然是False操作员会以为程序坏了。我在封装标准库时会统一在FC内部再做一次起点校正IF SearchStart 1 THEN SearchStart : 1; END_IF;这样不管你外部传入什么值FC都能给你一个合理的结果。最后再分享一点个人的实操体会。这套FORMID逻辑我写过很多遍踩过的坑总结下来就三条其一长度永远用LEN现取不要凭印象写死其二临时字符串变量的长度一定要够大宁可少吃几个字节也不截断其三循环里找到匹配后务必EXIT否则源字符串一长CPU时间白烧。把这三点记住你基本就不会在这个问题上翻车。往后你要是遇到找所有出现位置从第N位开始找统计某个字段出现次数这些需求都可以从这一篇的思路直接扩展不需要再到处找库函数说明。
阅读完成 · 觉得有帮助?
咨询建站