ARTICLE DETAIL

资讯详情

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

PLC ST语言数组+FOR循环批量采集,告别重复代码

PLC ST语言数组+FOR循环批量采集,告别重复代码 上个月调一套配料系统现场 12 台给料称、24 个温度点、8 路压力测点加起来小一百个。最开始我在 ST 语言结构化文本程序里老老实实一行行赋值写到一半自己都想笑——同样的代码复制几十遍改一个量程就要翻好几页调试时眼睛都看花了。后来狠下心把这块改成“数组 FOR 循环”批量采集程序瞬间瘦身后面加测点也只是改一个数组下标的事。这篇就把我实际用的写法、踩过的坑、总结出的套路一次性说清楚。先说清楚这套东西解决什么问题在 PLC 程序里凡是“多个结构相同的通道/测点”要做同步采集、换算、报警、统计的场景都是数组的用武之地而 FOR 循环就是帮你把这些数组元素挨个处理一遍的“传送带”。适合正在写 ST 程序、被重复代码折磨的自动化工程师也适合刚学高级语言编程的工控学生。1. 为什么说“数组 FOR 循环”才是批量采集的正解1.1 从手动逐点采集到批量采集差的不是代码量而是思路很多刚接触 PLC 编程的人拿到十几个模拟量通道第一反应是复制粘贴。你问他为什么这么写他说“每个通道都要单独出来做换算啊”。这话没错但站不住脚——通道数量一多程序的可读性和可维护性会断崖式下跌。用数组加循环的思路不是“把同样的代码抄几遍”而是把变量从“一个个散装的点”升级为“一组连续的内存单元”再把“对单个点的处理”抽象成“对整个集合的遍历”。这在计算机领域叫“批量处理”在 PLC 领域说白了就是用最小的代码量覆盖最多的同构对象。一个直观的对比假设有 12 路温度量程 0~100℃PLC 模拟量原始值为 0~27648。逐点写法是rT1 : INT_TO_REAL(aiRaw1) * 100.0 / 27648.0; rT2 : INT_TO_REAL(aiRaw2) * 100.0 / 27648.0; ... rT12 : INT_TO_REAL(aiRaw12) * 100.0 / 27648.0;批量写法是FOR i : 1 TO 12 DO aTemps[i] : INT_TO_REAL(aiRaw[i]) * 100.0 / 27648.0; END_FOR;代码量从 12 行压到 4 行。如果以后变成 32 路前者要补 20 行后者只需要把循环上限改成 32这种差别在大型项目里就是维护地狱和清爽代码的分界线。1.2 数据规模变大之后维护成本才是真正的问题很多人以为批量采集只是为了“少打字”其实多打字从来不是重点。重点是当测点数量变成几十上百逐个编程会导致三个致命问题——改量程要改多处、漏改一处就出隐蔽故障、程序长度长到在线监控都费劲。我自己体验最深的一次有一套设备现场有 16 个压力测点工程师为了省事把量程统一写成了 0~1.6MPa后来其中三个点换成了 0~2.5MPa 的变送器。因为逐点代码里没有集中管理现场改程序时只改了两处漏了一个结果那个通道的显示值偏到离谱排查了两个小时才找到。换成数组集中化管理之后量程表本身就是一个数组换算逻辑同一个公式改数据只需要改对应通道的“量程配置元素”不会再出现改一处漏一处。FOR 循环在这个场景里的价值就是保证“每一个元素都走同一个处理逻辑”既不漏点也不重复。只要循环边界和数组边界对应正确永远不会出现某一路被跳过的情况。2. 数组声明与初始化先把内存模型搭对2.1 ST 语言数组声明的三种常见形态在 ST 语言里数组声明没有统一标准各家 PLC 的编译器略有差异但总的格式类似。最常见的是一维数组VAR aTemps : ARRAY[1..12] OF REAL; aiRaw : ARRAY[1..12] OF INT; END_VAR注意这里ARRAY[1..12]下界是 1 上界是 12表示从 1 开始编号到 12 结束。CODESYS 系统默认从 0 开始但你完全可以写成ARRAY[0..11]关键是循环里的上下限要和数组定义保持一致。我习惯用1..N因为和仪表通道号一一对应写程序时不容易搞混。另一种用得很多的是二维数组适合“多组、每组多个测点”的场景比如 4 组反应釜每组 8 个温度点VAR aReactTemp : ARRAY[1..4, 1..8] OF REAL; END_VAR访问某个元素就写aReactTemp[2, 5]表示第 2 组第 5 个温度。二维数组配双重 FOR 循环就是传说中的“循环嵌套”外循环管组内循环管点。第三种是结构体数组这是我最推荐的方式。把每个测点封装成一个结构体里面既有原始值、工程量、报警上下限还有报警标志TYPE ST_Channel : STRUCT RawValue : INT; // 原始值 EngValue : REAL; // 工程量值 HiLimit : REAL; // 高报警限 LoLimit : REAL; // 低报警限 AlarmFlag : BOOL; // 报警标志 END_STRUCT END_TYPE VAR aChannels : ARRAY[1..16] OF ST_Channel; END_VAR这样每个通道所有属性都放在一起批量采集时一个 FOR 循环就能把所有通道的“属性打包处理”。这个思路是从面向对象借过来的在 ST 里虽然做不到真正的对象但结构体数组已经能解决大部分数据聚合问题。2.2 初始化与清零的工程实践数组初始化这块比很多人想象的更重要。PLC 上电后数组变量如果不是全局声明局部变量会在初次扫描时清零但如果用的是保持型变量RETAIN掉电之前的值会保留这就可能导致一个隐患上电瞬间程序还没扫描完某个数组里残留的是上一次的旧数据。我习惯在程序启动的第一个扫描周期里给数组做一次整体清零或赋初值。ST 语言里没有“数组整体赋值”的语法不能直接写aTemps : 0.0CODESYS 3.5 部分版本支持数组整体赋值但移植性差最稳的办法还是用 FOR 循环IF rstInit THEN FOR i : 1 TO 12 DO aTemps[i] : 0.0; aChannels[i].AlarmFlag : FALSE; END_FOR; END_IF注意清零动作只在 rstInit 为 TRUE 的那个扫描周期执行一次不是每个周期都执行。如果把这段清零代码放到无条件执行区就会出现“采集一次、立刻被清零”的笑话。还有一类初始化是给每个通道的报警上下限赋值。这些值如果来自 HMI 或触摸屏可以在配方下载时写进数组如果是在程序里写死建议放到一个独立的初始化块里aChannels[1].HiLimit : 80.0; aChannels[1].LoLimit : 20.0;这种写法虽然也是逐行但集中在一个区域比散落在程序各个角落好管理得多。3. FOR 循环批量采集的核心写法和扩展加工3.1 模拟量通道的批量采集与工程量换算完整的一套模拟量采集流程不只是把原始值搬进数组更重要的是“工程量换算”。以 4~20mA 温度变送器、量程 0~100℃ 为例PLC 的 AI 通道原始值范围通常是 0~27648西门子或 0~4000部分日系平台。换算公式为工程量 原始值 × (量程上限 - 量程下限) / (原始值上限 - 原始值下限) 量程下限用 ST 写出来FOR i : 1 TO 16 DO aChannels[i].EngValue : INT_TO_REAL(aChannels[i].RawValue) * (aChannels[i].HiLimit - aChannels[i].LoLimit) / 27648.0; END_FOR;这里有个细节INT_TO_REAL一定要写在乘法之前先转成实数再做除法否则整数除法会把小数部分直接丢掉。我第一次写的时候就因为漏了类型转换所有温度显示都是 0查了半天才发现是整数运算精度问题。实际工程中每个通道的量程可能不一样所以量程上下限应该作为每个通道结构体的成员上述代码中的HiLimit和LoLimit而不是写死一个公共常量。这样同一个公式可以适配所有通道这就是“数据驱动”的思路——程序逻辑不变变的只是数据配置。3.2 结构体数组一次循环搞定一整套数据结构体数组配合 FOR 循环可以实现一套非常漂亮的批量采集模式。举个具体的例子16 个测温通道每个通道包含原始值、工程量、报警限值、报警标志。采集程序可以这样写// 第一步采集原始值 FOR i : 1 TO 16 DO aChannels[i].RawValue : aiRaw[i]; END_FOR; // 第二步工程量换算 FOR i : 1 TO 16 DO aChannels[i].EngValue : INT_TO_REAL(aChannels[i].RawValue) * 0.01; END_FOR; // 第三步越限判断 FOR i : 1 TO 16 DO aChannels[i].AlarmFlag : (aChannels[i].EngValue aChannels[i].HiLimit) OR (aChannels[i].EngValue aChannels[i].LoLimit); END_FOR;三步循环结构完全一致只是处理逻辑不同。可以合并成一个循环但我故意拆开写原因有二一是每一步职责单一出问题时容易定位二是在线调试时可以在每个循环后面设置断点精确查看是采集异常、换算异常还是报警判断异常。把三个步骤合并的写法能省扫描时间但实时性要求不是极端苛刻的场合还是以可读性优先。我在自己项目里基本都用分开的写法只有在“一个扫描周期内必须完成所有处理”的场景才会合并成一个循环。3.3 常用数组统计求平均、找极值、生成越限报警采集完数据之后大部分应用场景都需要对数组做统计处理。最常见的三个求平均值、找最大值、找最小值。求平均值的思路是先把整个数组累加再除以元素个数ST 里最好不要用数组做累加申请一个临时累加变量rSum : 0.0; FOR i : 1 TO 12 DO rSum : rSum aTemps[i]; END_FOR; rAvg : rSum / 12.0;找最大值和最小值惯用技巧是先把候选值初始化为第一个元素然后从第二个元素开始逐个比较rMax : aTemps[1]; rMin : aTemps[1]; FOR i : 2 TO 12 DO IF aTemps[i] rMax THEN rMax : aTemps[i]; END_IF; IF aTemps[i] rMin THEN rMin : aTemps[i]; END_IF; END_FOR;如果数组里可能包含坏值比如断了线变成很大的数建议先做可靠性判断再参与极值和平均计算。这正是数组批量处理的优势坏值判断逻辑写一次所有点都生效。越限报警生成则可以充分利用结构体数组的“HiLimit/LoLimit”成员不需要在程序里写死任何阈值循环里直接比较FOR i : 1 TO 16 DO IF aChannels[i].EngValue aChannels[i].HiLimit THEN aChannels[i].AlarmFlag : TRUE; ELSIF aChannels[i].EngValue aChannels[i].LoLimit THEN aChannels[i].AlarmFlag : TRUE; ELSE aChannels[i].AlarmFlag : FALSE; END_IF; END_FOR这里我特意把“正常情况”放在 ELSE 里保证每个周期报警标志都会被刷新而不是“只置位不复位”避免出现“报警了永远消不掉”的经典问题。4. 进阶用数组实现环形采集缓冲和滑动平均4.1 环形缓冲区原理批量采集到数组里的数据如果只是用一次就丢那数组的潜力最多发挥了三成。很多工艺要求“看趋势”或者“做滤波”这时候数据不只是要被处理还要被“存下来”。工业上常用数组模拟环形缓冲区也叫循环队列把最近 N 次采集的值存起来再基于这些历史值做滑动平均、突变检测、趋势预判。环形缓冲区的思路很简单一块固定大小的数组写入位置从 0 递增到数组末尾后回到 0就像一圈转着写的循环队列。关键技巧是通过“取余”或者“越界回绕”控制写指针iPos : iPos 1; IF iPos 16 THEN iPos : 0; END_IF; aBuf[iPos] : rNewValue;这样数组里始终保存着最近 16 次采集的值不需要频繁搬移数据性能很高。乍一听有点像“环形队列”其实本质就是一维数组配合回绕指针。4.2 结合 FOR 循环的滑动平均实现滑动平均滤波是工业现场用得最普遍的滤波手段之一它的计算方式就是把环形缓冲区里所有数据求平均值。用 FOR 循环一次扫过 16 个点rSum : 0.0; FOR i : 0 TO 15 DO rSum : rSum aBuf[i]; END_FOR; rSmooth : rSum / 16.0;16 点滑动平均能有效抑制高频干扰但对阶跃信号的反应也会变慢约 8 个周期所以 N 不是越大越好。我这个项目里温度用 16 点压力用 8 点流量用 32 点都是根据实际波动速度调的。还有一种写法是“递推式滑动平均”利用上一次的求和结果减去最旧的值加上最新的值能省掉 FOR 循环但代价是数值误差会缓慢积累而且代码不好理解。我的建议是测点数量少、扫描周期宽裕时老老实实用 FOR 循环重新求和稳定可靠只有几百个数据点、性能紧张时才用递推法。环形缓冲区还有一个好处可以配合 FOR 循环做“数据有效性扫描”比如统计最近 30 次采集中有几次越限越限比例超过多少就触发联锁。这种趋势报警比单点越限报警可靠得多能有效避免干扰导致的误动作。5. 在线调试与踩坑排查实录5.1 九成问题出在数组越界数组批量采集遇到的头号问题永远都是数组越界。ST 语言里数组越界的行为不同平台差异很大有的一编译就报错有的编译通过了但一运行就 PLC 死机还有一种最可怕——不报错、不死机却把相邻内存地址的数据改掉了导致莫名其妙的数据错乱。我踩过一次典型的越界数组定义成ARRAY[1..12]循环却写成了FOR i : 0 TO 12。当 i0 时访问aTemps[0]其实访问的是数组前面那块内存程序中另一个变量的值被悄悄改写查了一下午才发现是下标错位。从此我立了一条规矩数组定义和 FOR 循环的上下界必须从同一个常量派生。推荐做法是用常量定义边界VAR CONSTANT MIN_CH : INT : 1; MAX_CH : INT : 12; END_VAR VAR aTemps : ARRAY[MIN_CH..MAX_CH] OF REAL; END_VAR循环里也写FOR i : MIN_CH TO MAX_CH这样即使以后改通道数只需要在常量区改一个数字。5.2 FOR 循环里的三个禁忌第一个禁忌循环体内修改循环变量。FOR i : 1 TO 10的循环体里如果出现i : i 1这种操作循环次数就变得不可控可能跳步甚至死循环。ST 语言的 FOR 循环不像 C 语言那么灵活循环变量在每次迭代结束时自动递增人为修改违反直觉编译器不一定报错但运行行为完全不可预测。第二个禁忌循环体内使用延时或等待指令。有的程序员在采集时想每个通道间隔 100ms便在循环体里插入延时函数这会导致整个扫描周期被无限拉长甚至触发 PLC 看门狗。PLC 不是单片机FOR 循环是周期扫描的一部分不是独立的多线程任务。需要延时应该在别处做时序逻辑而不是在循环里硬等。第三个禁忌循环次数过多导致扫描周期超时。假设数组有 1000 个元素每个元素做 5 次浮点运算一次循环就是 5000 次运算虽然 CPU 能扛但如果这个程序块被多个 OB 调用性能影响会被放大。大批量数据处理建议拆帧或者把处理任务分散到多个扫描周期里完成。5.3 在线调试的几个实用技巧我调试数组FOR 循环时最常用的手段是“监视数组”。CODESYS 和博途的在线监视里都能直接展开数组变量看到每个元素的值。但如果数组有几十个元素在 Watch 窗口里一行行看效率太低我会给程序块增加几个“快照临时变量”比如rDbgMax、rDbgMin、rDbgAvg跑完统计循环后直接看这三个变量比翻数组快得多。还有一个土办法临时写一个循环给整个数组赋递增的测试值FOR i : 1 TO 12 DO aTemps[i] : INT_TO_REAL(i) * 10.0; END_FOR;然后看统计结果是不是预期的。如果 12 个元素分别是 10、20、30……120平均值应该是 65最大值 120最小值 10。这套自检方法我用了好多年能快速区分“算法错了”还是“数据本身错了”。在线修改数组元素的操作也要提一下很多 PLC 支持在线写入数组变量在监视窗口里修改一个元素的值可以立即看到下一次扫描时程序如何处理这个新值非常适合测试报警阈值逻辑。调试批量采集代码时我还有一个个人习惯在每个循环结束的位置放一个“包计数”变量iLoopCnt : iLoopCnt 1跑一个周期后这个变量应该严格等于循环次数。这样做能直观发现循环体里是否有跳转、跳出等异常操作。6. 写在最后的实操体会我很长一段时间里都觉得数组是计算机专业才玩的东西PLC 上测点就几十个手写也快。直到被十几个乃至几十个同类型测点反复折磨之后才彻底改变思路。现在我写 ST 程序只要看到“三个以上结构相同、处理逻辑相同”的测点第一反应就是数组加 FOR 循环这已经变成条件反射了。最让我受益的组合是结构体数组存放每个通道的完整信息FOR 循环完成采集、换算、报警三步标准流程再用环形缓冲区存最近几十个周期的数据做滑动平均和趋势判断。这套模式我在配料、温控、环保几类项目里反复使用稳定性和可维护性都不错。如果你现在还在项目里复制粘贴十几个通道的代码我建议你先停一下花半小时把数组和 FOR 循环的语法试通然后把一个小模块改成批量处理。改完你大概率会回来把这篇文章收藏起来——不是我写得多好是这种“一次定义、批量处理”的思维太顺手了用了就回不去。
返回列表