ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ST语言数组+FOR循环:PLC批量模拟量采集高效编程实践

ST语言数组+FOR循环:PLC批量模拟量采集高效编程实践 写这类型的东西最开始是因为一个很实际的项目现场几十个温度传感器用传统梯形图一个通道一个通道地写程序量翻倍不说改一个量程要改几十处。后来换用ST语言用数组配合FOR循环做批量采集程序短了结构也清楚了。这篇文章就把我在这类项目里的完整思路和踩坑过程分享出来主要面向用ST语言做PLC/DCS程序开发的工程师也适合刚接触结构化文本、想找一种更高效数据采集方式的人。读完你应该能直接用这套模式处理自己的批量采集需求。1. 为什么设备数据采集要用“数组FOR循环”1.1 批量采集的痛点做过现场的都懂只要是做过程控制或者设备监控的几乎都遇到过这种场面一台设备上有十几路或者几十路模拟量可能是温度、压力、流量也可能是一排开关量反馈。传统做法是在梯形图里一路一路写每一路都要有硬件地址、工程单位转换、报警比较、上传变量写得人头晕眼花后面维护更是灾难。更麻烦的是如果现场传感器换了个型号量程变了你就得把所有相似线段全部改一遍漏改一个就是事故。批量采集的本质是“一群结构相同、地址连续的信号”但很多PLC本身没有批量处理这种同构信号的原生帮助梯形图又天生是面向单个触点和线圈的。这个场景下ST语言的优势就出来了它允许你用数组把同样类型的变量打包管理允许你用FOR循环把重复操作变成一条几句代码的循环体。数组解决“怎么存”FOR循环解决“怎么取和算”两者合起来批量采集就从一个笨重的工程变成了一段干净的逻辑。1.2 数据结构的核心数组数组在PLC程序里你可以把它理解成一个带编号的储物柜。编号就是索引储物柜里放的必须是同一类东西比如全部是REAL型温度值、全部是BOOL型运行状态。这正好适合批量采集的场景同一批传感器传回来的物理量本质上就是一份连续数据类型相同的数值清单。很多人刚用数组时会纠结索引从0开始还是从1开始。这完全看具体PLC环境比如在TIA Portal的ST里可以定义ARRAY[0..99]或ARRAY[1..100]在CodeSys里也类似。我习惯用1起始的数组因为这样索引号可以直接对应到通道号1号通道就是数组第1个元素排查故障时很直观。但如果做的程序要跟C代码、上位机脚本交互用0起始会更顺手。这不是原则问题但一定要在项目开始前定好并且在统一位置写注释否则移植和协作时容易混乱。1.3 FOR循环解决什么如果说数组是数据的容器FOR循环就是批量操作的流水线。它的核心逻辑是给定一个起始值和一个结束值循环变量逐步改变循环体重复执行。针对批量采集FOR循环能替你完成三件事——循环取值、循环转换、循环存表。举一个特别直白的例子你要读16个模拟量通道没有数组和FOR循环代码长这样AI0 : UINT_TO_REAL(INT(IN_ANA[0])) * 0.1; AI1 : UINT_TO_REAL(INT(IN_ANA[1])) * 0.1; AI2 : UINT_TO_REAL(INT(IN_ANA[2])) * 0.1; ...有数组和FOR后变成FOR i : 0 TO 15 DO RawValue[i] : UINT_TO_REAL(INT(IN_ANA[i])); EngValue[i] : RawValue[i] * Scale[i] Offset[i]; END_FOR;代码量从几十行缩到几行关键是后续如果要增加通道数量只要改范围数字就行不用重复复制粘贴。这就是这两个东西组合起来的真正价值。2. ST数组的定义与FOR循环的基础语法2.1 数组怎么切一维、二维与自定义结构先讲一维数组。在TIA Portal或CodeSys的ST编辑器中声明方式基本一致VAR aTemp : ARRAY[1..24] OF REAL; aAlarm : ARRAY[1..24] OF BOOL; iCnt : INT; END_VAR这段声明建立了一个24个元素的实数数组。注意PLC的ST声明通常放在变量区FOR循环写进程序区别搞混。如果你要采集的数据带辅助信息比如每个通道还有上限、下限、单位、是否参与控制建议直接用结构体数组。结构体可以在类型定义区新建TYPE SensorData : STRUCT Value : REAL; HighLimit : REAL; LowLimit : REAL; Alarm : BOOL; END_STRUCT END_TYPE VAR aSensors : ARRAY[1..24] OF SensorData; END_VAR二维数组也用得上但采集场景里用二维数组并不多见除非你是按“区域点位”组织数据。我见过一个项目用二维数组ARRAY[1..8, 1..16] OF REAL存8个区域、每区16个温度程序里用两层FOR循环访问。这样做的缺点是变量表看起来不直观而且跨区处理时索引容易混。能用结构体数组尽量优先考虑结构体数组二维数组留给矩阵运算或配方管理更合适。2.2 几个容易踩坑的声明细节数组声明最容易出三个问题第一索引类型。虽然很多ST编译器允许用INT、DINT或UINT作为数组下标但FOR循环的循环变量最好不要用较小的INT当索引非常大的数组下标。比如采集上千个点INT最大值32767一般够用但如果你定义的是ARRAY[0..65535]必须用DINT或UINT否则循环变量溢出会让程序直接进死循环或越界严重时PLC会报故障。第二初始化。PLC的全局变量和局部变量在网络首次扫描时一般会自动清零但为了排查方便还是建议在变量声明区直接给数组赋初值。比如aOffsets : ARRAY[1..4] OF REAL : [0.0, 0.5, 0.0, -0.2];这种写法在ST里单独看是支持的但如果你想给结构体数组整体初始化不同平台语法有差异我不建议在通用ST里依赖它而是做一个初始化功能块在程序启动时用FOR循环把所有元素按设定值填一遍这样移植性最好。第三指针复合。很多老手喜欢用指针访问数组来提升性能比如把数组地址传给扫描功能块。但不建议新手一开始就玩ADR()和指针运算因为不同PLC处理指针的方式差异很大一个地址错位就是数据错乱。等你在纯FOR循环版本上跑通了再考虑用指针优化也不迟。2.3 FOR循环语法细节ST里和C不同ST里的FOR循环长这样FOR i : 0 TO 23 BY 1 DO aData[i] : ReadSensor(i); END_FOR;注意几点BY关键字后面跟步长默认是1所以BY 1可以省略。但如果你要隔点采样比如每2个点取1个就写BY 2。循环变量i不需要在FOR语句里声明但必须在变量区声明。部分编译器不允许循环体内部修改循环变量的值所以你别指望在循环体里给i赋别的值来跳转那是不符合结构化文本规范的。想提前退出循环要用EXIT。FOR循环的范围边界是包括两个端点的也就是说FOR i : 1 TO 5会执行i1,2,3,4,5五次和C语言的“”语义不同。刚转过来的人很容易多算一次。这里也顺便说下循环嵌套。批量采集经常要处理“所有传感器里面的所有通道”这种场景比如数据存进二维数组外层循环管行里层循环管列。嵌套本身不复杂但要注意变量别重复以及数组访问顺序尽量让内存连续访问按行方向访问。有些PLC编译器对二维数组访问顺序做优化访问顺序反了性能会差不少。3. 批量采集的完整实现从传感器到上位机3.1 一个具体场景24路温度采集我拿实际项目来演示一台热处理设备24个热电偶测温通道温度范围0-1300℃。信号经过温度变送器转成4-20mA接到PLC的模拟量输入模块模块配置为0-27648西门子整数格式。要求每100ms采集一次然后换算成实际温度超过上限或者低于下限要报警并把所有温度值顺序打包给HMI和上位机。如果在梯形图里干没有数组的情况下你得建至少24个模拟量转换变量、24个报警变量、24个工程量变量然后在OB1里轮询处理或者在FB里写24次重复逻辑。用ST后核心结构只要两层一个结构体数组和一个FOR循环。3.2 第一步采集原始值并存入数组采集的前提是知道硬件地址。不同PLC地址写法差异大CodeSys用%IW西门子用%IW或符号地址。为了示例通用我定义一个功能块接口ReadAnalogChannel(rChannelID : INT) : REAL内部封装了从模拟量模块读取原始整数值并转换成百分比或工程量温度的逻辑。实际项目里这通常是一个模拟量驱动封装好的功能块你只需要调接口就行。主逻辑里先准备好采集标志防止多个任务同时触发IF NOT bBusy THEN bBusy : TRUE; FOR i : 1 TO 24 DO rawTemp[i] : ReadAnalogChannel(i); END_FOR; bBusy : FALSE; END_IF;这里rawTemp是ARRAY[1..24] OF REAL用来存放未做量程线性化的原始温度值可能还是百分比或二进制比例。注意把采集动作包在bBusy里是为了处理那些超时或者异常的情况。有些模拟量模块要求采集请求之间保持最小间隔不加这个防护循环太快把模块请求队列塞满会出现偶发的读取乱序。3.3 第二步工程量转换、报警、滤波一气呵成原始值不能直接显示要做线性换算。标准公式是工程值 (原始值 / 量程上限) * (工程量上限 - 工程量下限) 工程量下限对应代码FOR i : 1 TO 24 DO // 判断传感器是否断线或超量程 IF rawTemp[i] 2.0 THEN bFault[i] : TRUE; engTemp[i] : 0.0; ELSE engTemp[i] : rawTemp[i] * 0.05; // 假设4-20mA对应0-1300℃比例系数可配 IF engTemp[i] SpecHigh[i] THEN bAlarmHigh[i] : TRUE; ELSE bAlarmHigh[i] : FALSE; END_IF; IF engTemp[i] SpecLow[i] THEN bAlarmLow[i] : TRUE; ELSE bAlarmLow[i] : FALSE; END_IF; END_IF; END_FOR;关于断线判断模拟量模块对传感器断线通常会有特殊码值比如值小于2.0或接近0这个要根据你的模块手册来定。实际项目里我见过一个很经典的坑变送器坏了输出0mA原始值变成0没做断线判断系统直接把这路温度判成最低温结果工艺误认为是真的低温导致后续把加热器开到最大。断线判断这步不能省。如果你有滤波需求也建议放在这个循环里做。最简单的滑动平均用三个周期数值求平均或者写一阶惯性滤波filtTemp[i] : filtTemp[i] * 0.7 engTemp[i] * 0.3;一阶滤波的系数选择是门学问。系数大滤波深度高但响应慢系数小响应快但噪声大。建议根据采样周期和工艺要求去调不要拍脑袋。3.4 第三步批量上传给上位机或HMI采集和转换只是第一步最终数据要交给上位机或HMI。上位机一般要读一整组数据如果每个变量单独建标签会非常痛苦。所以我们通常把最终的工程温度数组直接映射到上位机地址区或者在PLC内做一个“上传缓冲区”。最直接的做法是复制数组到通讯区FOR i : 1 TO 24 DO ComData[i] : filtTemp[i]; END_FOR;你会看到这就是一个最简单的数组拷贝。但有些PLC指令集里有更高效的块移动指令比如西门子的MOVE_BLK或者CodeSys里的MOVE对数组的支持。实际上在ST里可以直接赋值整个数组ComArray : filtTemp;前提是两个数组类型完全一致。这比FOR循环拷贝要快不少代码也更简洁。优先用整体赋值但如果你要对数据做排序、筛选、间隔抽取那还是得FOR循环手动操作。上传方式也有讲究。如果上位机走Modbus TCP你可以把数组映射到保持寄存器区比如建立几百个Modbus地址每次采完一轮把数据写进去。如果用OPC UA直接把PLC里的数组变量暴露出去即可但要注意上位机的NAV节点浏览缓存问题改完数组长度后可能需要重启服务器才能看到新数组。4. 实战中的性能陷阱与排查技巧4.1 致命陷阱FOR循环里面塞通信指令这是新人在ST里写循环最容易犯的一个错误。FOR循环本身是CPU在极短时间内顺序执行代码它不是像人一样一条条慢慢做的。看似虽然只用了一次FOR循环但你如果在循环体里调用了一个读取串口指令、一个数据库写入指令、或者一个动态请求Modbus的功能块性能就完全变味了。举个例子有朋友在一段循环里调用了一个读远程IO卡的功能块这个功能块底层的执行时间可能就要50ms。循环24次那就是1200ms。而PLC扫描周期通常只有几ms到几十ms。这样一来自己程序的扫描周期直接被循环拖长十倍CPU占用率飙升别的逻辑全部变慢甚至看门狗超时导致PLC停机。正确做法是先用指令级方式把数据从通信模块硬件缓冲一次性搬运到数组或者让通信中断服务把新值更新到一个镜像区循环内只做纯内存运算。一句话循环里别做IOIO全部提前。4.2 扫描周期与循环次数的平衡PLC执行ST代码本质上还是在扫描周期里执行指令。循环次数越多执行时间越长。24次当然无所谓但如果采集点有几千个还在一个周期里全量扫描CPU可能会不堪重负。我的经验是分批处理。比如有2000个点要求1秒更新一次。2000次全量循环几百毫秒太悬了。可以把2000个点分成5组每200ms扫描400个点用“当前组”变量选择数组区域IF bTimerElapsed THEN FOR i : (nBatch * 400 1) TO (nBatch * 400 400) DO // 处理第 i 个点 END_FOR; nBatch : nBatch 1; IF nBatch 5 THEN nBatch : 0; END_IF; END_IF;这种方法叫时间片轮询。它牺牲了一些更新延迟但换取的是周期稳定。对于大批量数据采集比在一个周期内憋个大循环要合理得多。别迷信“PLC很强大”扫描周期的每个毫秒都是省出来的。4.3 数组越界与数据类型不一致数组越界在ST里不像C语言那样能自由地写到随机内存大多数PLC运行时会检查索引范围越界会让功能块报错、甚至触发系统故障停机。但别因此忽视它因为运行时检查只针对“索引变量确实超出定义范围”的情况最危险的反而是索引变量因为运算错误产生了一个看起来“合法”但语义错误的地址。比如通道编号是1到24但你在上位机配方里传了一个0或者25进来FOR循环就不会执行或者会跳过最后一个数据。这类问题很隐蔽HMI上可能显示24号通道数据一直是上次的值。所以建议循环开始前做一次范围检查IF (nChannel 1) AND (nChannel 24) THEN // 访问数组 END_IF;再讲数据一致性。FOR循环内读硬件地址时如果你的硬件模块在扫描中途更新了数据可能出现半批是新值、半批是旧值的“撕裂”情况。这在有的项目里会造成误动作。解决办法是先用一个中断安全的地方把硬件值缓冲到镜像数组再在普通扫描中处理镜像。另一个办法是循环里给硬件加锁但不太现实。考虑实际场景时最好通过扫描周期调整或直接接受可容忍的微小不一致。4.4 常见错误速查表我把ST数组循环采集里最常遇到的几类问题整理成表方便排查现象可能原因解决建议循环变量从不为某值FOR范围写错或BY步长导致跳过该值打印循环变量检查范围端点含义最后几个元素全部为0数组定义长度小于FOR范围检查数组上限和循环终值是否匹配程序扫描周期突然拉长数倍循环体内调用通信、延时功能块把IO直接移出循环断线故障导致温度直接变成零未做断线判断增加阈值判断并置故障位修改传感器后读数整体偏移量程上限/偏移参数没有循环赋值增加参数化数组配置循环内计算耗时报警响应滞后循环里做了重计算把报警判断放在独立小权重循环或中断里上位机读到的数据顺序错乱数组映射到通信区时偏移不对检查通信映射地址必要时在PLC里挪到固定区域FOR循环没有EXIT出口逻辑卡死循环变量被外部修改或无限循环避免循环内修改计数器用EXIT显式退出4.5 诊断调试的小技巧对付数组循环最好用的是“观察窗”和“强制表”。在TIA Portal或CodeSys的调试环境里你可以把所有数组元素展开在线监视每个元素实时值。排查问题时我习惯先在循环体外单独给某个特殊索引赋值测试比如把i临时强制成5看看数组第5个元素是否变化。这样能快速判断问题是出在采集源还是循环逻辑里。还有一个很实用的小招在循环体里临时加一个累加器用累加值判断循环是否完整执行。比如nLoopCnt : 0; FOR i : 1 TO 24 DO // 采集逻辑 nLoopCnt : nLoopCnt 1; END_FOR;调试期观察nLoopCnt终点值是不是24秒查循环有没有跑满。这个变量是不占多少资源的上线前删掉就行。5. 结束前分享一个我踩过最值得说的坑有一次在现场调试程序写好下载采集功能一直正常直到某个时刻温度值出现了整段跳变。回放趋势图发现是每间隔一段时间16号以后的数据集体偏移了约5%。排查了两天最后才定位到我的FOR循环里用了数组作为量程参数而这个数组在上位机在线整定过程中被修改了偏偏整定的数据是一个错误配方给16到24通道写入了错误的量程。程序逻辑本身没毛病问题是参数数组被外部无节制地覆盖。从那以后我立了个规矩量程、报警限等关键参数数组要么用只读区存两份要么在写入时增加校验机制把写入数组的函数集中管理不要开放成任意写入变量。尤其在使用数组做批量采集时存储变量的意外修改比逻辑bug更难发现因为故障现象往往像是“传感器坏了”实际是数据配置坏了。先排查数据配置源再动传感器这个顺序值得记下来。
返回列表