资讯动态

西门子PLC用SCL实现冒泡排序:从数组边界到扫描周期实践

发布时间:2026/10/21 9:23:52 来源:尧图企业网站定制
刚看到这个题目我也愣了一下西门子PLC跟冒泡排序这俩词能放在一起大多数PLC工程师玩SCL的时间都花在PID、通信、轴控制上谁会去研究排序后来真在一个配方排序需求里写了一遍发现非但不离谱写出来的代码还意外地简洁而且这种“笨算法”放在PLC这种资源有限的设备上反而有种微妙的合适。这篇文章主要想讲清楚三件事第一现场到底什么样的业务会逼着PLC做排序第二用TIA博途的SCL语言写冒泡排序代码长什么样第三写完为什么需要特别关注数组边界和扫描周期。我按S7-1200/1500系列的习惯来讲编程软件用TIA博途的V15以后版本语言用SCL数组以INT和REAL为主。如果你正在给PLC加数据整理能力或者纯粹好奇算法怎么进工业控制器这篇文章应该能让你少走不少弯路。1. 先搞清楚PLC为什么需要排序而且是冒泡排序1.1 从一次被现场数据“恶心到”的经历说起前年我调过一条线上面装了一个测振动的传感器信号本身没问题但现场环境太恶劣时不时蹦出几个尖峰。按平均值滤波尖峰会被摊进结果里导致设备误报按乘法平均或者滑动平均参数又得反复试。后来我换了思路连续采10次数据排序然后直接取中间那个值。这招叫中位值滤波抗尖峰能力比平均值强一个量级。问题是排序这件事在PLC里该怎么做我当时第一反应是“PLC里还能写排序”第二反应是查SCL手册看有没有现成的排序指令。答案是没有。西门子PLC的指令集里没有SORT你只能自己写。就是这段经历让我把冒泡排序这门“大学计算机课”里才会出现的东西正儿八经地搬进了TIA博途。1.2 配方排序、批次统计、HMI展示不止采样滤波需要排序如果你觉得中位值滤波太小众那再看看这几个场景现场有多组配方要轮换生产PLC需要按“优先级”字段从小到大处理优先级相同按更新时间排。生产管理系统下发了一批批次数据PLC要按时间顺序逐批执行但下发的顺序经常是乱的。HMI上要显示某台设备近24小时的温度曲线数据存在PLC的数组里需要按采样序号排好再刷新画面。质量统计需要把一批产品的测量值从低到高排列方便找出中位数、最大值、最小值判断分布情况。这些需求有个共同点数据量不大少则几个多则几十上百个但必须在PLC内部完成。如果你把“排序”这个动作推给上位机那就意味着PLC要把原始数组原封不动传出去再由上位机处理再回传结果。一来一回多占通信资源还会引入延迟。更麻烦的是HMI脚本里做排序看起来方便但脚本一多就卡界面。让PLC在源头把数据排好通信和画面都清爽。1.3 冒泡排序的时间成本在PLC里真的可以接受吗很多工程师一听“冒泡排序”就想到O(n²)觉得在PLC这种讲究扫描周期的设备里是禁忌。这话对了一半前提是数据量大。场景变了结论完全不一样。数组元素个数最坏情况比较次数10万次循环周期的PLC上估算耗时1045微秒级几乎可以忽略20190还是可以忽略501225扫描周期内多出零点几毫秒1004950毫秒级仍需谨慎但不至于失控1000约50万不建议用冒泡排序现场里“需要排序的数组”一般不超过100个元素。比如中位值滤波采10个点配方队列20条批次统计50条这些数据量对PLC来说根本谈不上压力。真正的问题是别把排序放进每个扫描周期都执行的逻辑里而是放在“数据采集完成”或“触发命令”之后执行一次。所以说冒泡排序贵不贵要看它被调用多少次。你想明白这件事之后就会觉得这个算法在PLC里不是不能用反而是最稳的。2. 动手前先选型环境、语言和数据类型2.1 为什么选SCL而不是梯形图或语句表我见过有人用梯形图写排序写了两个多小时出来的东西谁也看不懂。梯形图的强项是位逻辑、互锁、时序不是数组下标和循环。STL虽然能做循环和间接寻址但代码跳来跳去稍微改一个条件就要逐行核对维护成本极高。SCL则接近Pascal和C的混合体有FOR、WHILE、IF、EXIT有数组、结构体还能直接访问数据块。写完跟高级语言差不多现场工程师一看就明白。TIA博途里SCL可以和梯形图混用你大可以把业务逻辑留在梯形图里把“排序”这种纯数据处理的算法放进SCL函数里。用一句行业里的话说梯形图解决“哪一秒做什么”SCL解决“一堆数据怎么算”。排序明显属于后者。2.2 数组类型、变量声明和FC/FB的选择在TIA博途里数组的下标不一定要从0开始你可以写ARRAY[1..10] OF INT也可以写ARRAY[0..9] OF REAL。这个自由度看着方便其实非常坑后面我会专门讲。排序函数我用FC而不是FB。为什么因为FB需要实例背景数据块而这个排序函数没有需要保持的内部状态不需要实例DB。FC只接收传入的数组原地排序用完就结束干净利落。参数类型用VAR_IN_OUT而不是VAR_IN。VAR_IN是把调用时的数据复制一份进去函数内部改的是副本调用方一点变化都看不到VAR_IN_OUT则类似C语言里的指针函数直接修改原数组。这点很关键冒泡排序是原地排序我们就是要让传入的数组被改掉而不是拿到一个排好序的副本。另外VAR_IN_OUT参数不能传常量必须传数据块中的变量或InOut定义的输入。所以调用时你传的一定是“DB_Test.Arr”而不是一个立即数。2.3 固定长度数组还是动态数组刚上手的时候我建议直接用一个固定长度的数组比如ARRAY[0..9] OF INT。代码短、容易调、不绕弯。等你把这个函数跑通了再考虑封装成通用排序库。通用版本要用到ARRAY[*]可变数组。TIA博途对可变数组有一些限制它只能用于FC/FB的VAR_IN_OUT不能用于VAR_IN访问它必须通过LOWER_BOUND和UPPER_BOUND获取实际上下界。这两个函数返回DINT类型注意别跟INT混着用。从工程实践角度看固定长度更适合项目交付因为排查问题速度最快动态数组更适合做成标准库换项目直接调用。两种思路都不错看你更在意“这个项目少出问题”还是“以后复用起来省事”。3. 核心代码拆解一个能直接抄的SCL冒泡排序3.1 第一版代码长度10的INT数组排序先给固定数组版本。我在TIA博途里建了一个FC名字叫FC_BubbleSort代码直接写在SCL区FUNCTION FC_BubbleSort : Void VAR_IN_OUT arr : ARRAY[0..9] OF INT; END_VAR VAR_TEMP i : INT; j : INT; temp : INT; swapped : BOOL; END_VAR BEGIN #swapped : TRUE; FOR #i : 0 TO 8 DO IF NOT #swapped THEN EXIT; END_IF; #swapped : FALSE; FOR #j : 0 TO 8 - #i DO IF #arr[#j] #arr[#j 1] THEN #temp : #arr[#j]; #arr[#j] : #arr[#j 1]; #arr[#j 1] : #temp; #swapped : TRUE; END_IF; END_FOR; END_FOR; END_FUNCTION这段代码会把arr里的10个整数按升序排列也就是从小排到大。如果你要降序只要把比较符号从改成就行。调用时我在OB1里用一个上升沿触发避免每个扫描周期都去跑排序FC_BubbleSort(arr : DB_Test.Arr);3.2 逐行看关键逻辑交换、早停、边界处理冒泡排序的原理很简单从数组第一个元素开始两两比较如果前一个比后一个大就交换位置。这样一趟下来最大的元素就被“冒泡”到了数组末尾。下一趟只需要处理前面的元素再让第二大的元素落到倒数第二个位置循环往复。第一个容易看懵的地方是循环边界。数组长度10为什么外部循环是0 TO 8因为10个元素最多跑9趟就足够把前9个较大值放到正确位置剩下最小的那个自动就在开头了。内部循环上界为什么是8 - #i因为每一趟都会让一个元素沉底下一趟要处理的长度就少一个。#i0时内部要两两比较到arr[8]和arr[9]#i8时内部只剩arr[0]和arr[1]比较然后整个排序结束。第二个重点是swapped这个变量。它不是必选项但加上之后能省不少时间如果某一趟从头到尾都没有发生任何交换说明数组已经有序就可以提前退出。比如你传入的数组本来就是1,2,3,4,5,6,7,8,9,10第一趟内部循环一次交换都没发生第二趟一开始检测到swappedFALSE直接EXIT整个排序立刻结束。这在接近有序的数据上效果很明显。第三个重点是交换三行代码。PLC里没有“一条语句交换两个变量”的语法必须借助临时变量temp。这也是我最担心有人抄错的地方如果直接写arr[j] : arr[j1]再arr[j1] : arr[j]那数组里两个位置就变成同一个值了冒泡排序直接变“值覆盖”。3.3 通用版本数组长度变了也不慌固定数组版本好归好但换一个使用场景就得改FC。如果你想做一个能复用的排序函数可以用动态数组版本。TIA博途里写起来也不复杂FUNCTION FC_BubbleSortDyn : Void VAR_IN_OUT arr : ARRAY[*] OF INT; END_VAR VAR_TEMP lo : DINT; hi : DINT; i : DINT; j : DINT; temp : INT; swapped : BOOL; END_VAR BEGIN #lo : LOWER_BOUND(#arr, 1); #hi : UPPER_BOUND(#arr, 1); IF #hi #lo THEN RETURN; END_IF; #swapped : TRUE; FOR #i : 0 TO #hi - #lo - 1 DO IF NOT #swapped THEN EXIT; END_IF; #swapped : FALSE; FOR #j : #lo TO #hi - #i - 1 DO IF #arr[#j] #arr[#j 1] THEN #temp : #arr[#j]; #arr[#j] : #arr[#j 1]; #arr[#j 1] : #temp; #swapped : TRUE; END_IF; END_FOR; END_FOR; END_FUNCTION这个版本的要点是先用LOWER_BOUND和UPPER_BOUND拿到数组的实际上下界然后所有循环边界都用#lo、#hi来推导。这样不管是ARRAY[0..9]还是ARRAY[5..14]都能正确排序。数组只有一个元素时#hi #lo直接返回避免做无意义的比较。需要注意ARRAY[*]动态数组参数在旧版本TIA博途中支持有限而且要求访问方式是“优化的块访问”。如果仿真或编译报接口错误先把程序块的优化访问打开再说。4. 实测记录数据真的排对了吗CPU扛得住吗4.1 测试用例设计乱序、逆序、相等元素写代码最容易犯的错就是“看着逻辑对实际一跑就露馅”。我的习惯是准备三组测试数据乱序数据5, 3, 9, 1, 7, 2, 8, 4, 6, 0用来验证常规排序是否正确。完全逆序数据9, 8, 7, 6, 5, 4, 3, 2, 1, 0这是冒泡排序最坏情况能逼出所有交换逻辑的问题。含相等元素的数据4, 2, 4, 1, 2, 0, 4, 3, 1, 2验证相等的元素不会被错误交换排序结果不会出现“丢数”。现场调试时我还会加上边界值测试比如数组里包含-32768和32767也就是INT的最小值和最大值。如果比较逻辑里某个临时变量溢出或者边界判断差一位极端值最容易暴露问题。4.2 用监控表和PLCSIM验证排序结果TIA博途里验证排序结果不需要焊板子也不用接PLC开PLCSIM仿真就能搞定。我的完整操作流程是这样的在全局数据块DB_Test里建一个ARRAY[0..9] OF INT类型的变量比如叫Arr。在OB1里加一个BOOL变量DoSort用上升沿触发排序调用。把PLC程序下载到PLCSIM并启动仿真。打开监控表把DB_Test.Arr[0]到DB_Test.Arr[9]全部添加进去。用监控表的“写入值”功能把测试数据逐项写进去。把DoSort置位让它产生一个上升沿触发排序。再回到监控表看数组的值。正常情况下你会看到数组从乱七八糟的初始值变成升序排列。如果哪一位不对把监控表切换到二进制视图逐位比对马上就能定位到是交换逻辑错了还是循环边界多了一个或少了一个。我特别提醒一下测试顺序先测逆序数据再测乱序最后再测含相等元素的数据。逆序数据对冒泡排序来说压力最大能触发每一趟的交换动作乱序数据代表一般情况含相等元素的数据则验证排序的稳定性。4.3 性能观察扫描周期的影响与CPU负载性能方面我用博途里自带的“周期时间”监视功能观察过。对10个INT元素的数组做一次冒泡排序消耗的时间微乎其微在正常OB1循环里根本感觉不到。到了100个元素耗时也仍然在毫秒量级不会导致CPU进入超时状态。但有一个前提不要在每个扫描周期都去排序。正确做法是让排序只在数据采集完成、配方下发、或人工触发的时候执行一次。否则1000个元素的数组每周期排一次CPU负载肯定会明显抬升严重时还会把OB1的循环时间顶爆触发周期超时诊断。使用REAL数组时比较和交换比INT慢一些这也是正常的。如果你需要处理的是结构体数组比如每条记录包含“序号、温度、时间戳”建议用索引排序而不是直接搬整个结构体后面第5部分会细说。5. 写进PLC之后绕不开的坑边界、时序、再优化5.1 数组下标从1还是0开始一个低级却致命的错误TIA博途的数组可以随便定义上下界这本来是好事但照搬网上代码时特别容易出事。网上许多示例都写ARRAY[0..9]你项目里可能写的是ARRAY[1..10]两种写法跑同样一段循环结果完全不同。我见过有人把ARRAY[0..9]代码直接套到ARRAY[1..10]上内部循环从0 TO 8开始等于永远在访问arr[0]而这个数组根本没有下标0。PLC诊断区直接报“区域长度错误”CPU进入STOP。现场生产还没开始就被一个数组下标问题干停了。所以最稳妥的做法是要么统一固定使用ARRAY[0..n-1]要么直接采用5.3版本里用LOWER_BOUND动态获取下界的写法。如果你决定用固定版本请记住一个检查习惯写完FOR循环先看循环变量的起始值和数组下界是否一致看循环内部最大访问下标是否小于数组上界。5.2 中断OB里排序的时序风险排序函数本身没问题但放在哪里调用直接决定了它是否安全。如果你把它放在循环中断OB里比如一个每10毫秒执行一次的OB那就得算清楚了排序一次要多久会不会超过中断的余量时间假设你的中断OB里除了排序还有通信处理、模拟量采集等逻辑留给排序的时间窗口可能不到2毫秒。如果数组突然涨到几百个元素最坏情况下的排序时间可能超过这个窗口结果就是中断超时PLC会触及时序错误甚至导致OB无法完成。我自己的习惯是所有排序都放到主程序OB1的数据处理段里并且用一个上升沿触发。除非你能接受排序对循环时间的确定性影响并且在现场反复实测过否则别把排序放进周期中断OB。5.3 还能怎么优化早停、索引重排、有效长度冒泡排序在PLC里还能优化但请记住优化一定要围绕现场实际需求而不是单纯追求“更快”。第一个优化是早停。代码里已经有swapped判断了这个一定保留。在生产现场真正乱序的数据占少数大部分时候数组已经接近有序早停能帮你省下很多无谓循环。第二个优化是索引排序。如果你的数组元素不是INT而是结构体比如一条配方有“名称、温度、时间、优先级”四个字段直接交换结构体变量意味着每次交换要搬一整块数据。更聪明的做法是额外建一个ARRAY[0..9] OF INT的索引数组排序时只交换索引不搬动实际配方数据。这样比较的时候用实际数据判断交换的时候只交换索引值最后按索引顺序去读配方内容就行。代码逻辑会复杂一点但对大数据块很友好。第三个优化是控制有效长度。现场数据不一定是固定10条可能是“今天收集了7条明天收集了13条”。我的做法是在数据块里定义一个有效长度变量比如Length : INT排序时把循环边界从数组固定上界改成有效长度。这样可以避免每周期都给整个数组的所有元素排序浪费CPU还容易被无效数据干扰结果。如果你只是想找最大值或最小值完全不需要完整排序。冒泡排序跑K趟就能把K个最大值放到数组尾部你要几个值就跑几趟。但如果目标是中位数建议老实排完因为部分排序后中间位置的值是否已经稳定边界判断很容易出错为省这一点扫描时间去冒逻辑风险不值得。排序函数写完之后我在FC的注释区留了一句话“原地升序排序数组下界与上界由调用方保证。”这句话看起来简单实际上涵盖了我在这个函数上踩过的所有坑边界、原地修改、调用方责任。后来我把这个库文件夹共享给同事他拿到手两分钟就接进了项目连问都没问我。这大概就是这段代码最大的价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑