ARTICLE DETAIL

资讯详情

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

ST语言数组结合FOR循环实现PLC批量采集的实战指南

ST语言数组结合FOR循环实现PLC批量采集的实战指南 做自动化项目时间长了你会发现最折磨人的往往不是算法而是那些成百上千行“长得一模一样”的重复代码。比如16路温度采集用梯形图写就得一路一路加指令、建变量、改地址改一个量程要翻半天程序要是哪一天业主说“我们要扩到32路”那整个画面几乎等于重做。后来我切到ST语言用数组配合FOR循环做批量采集代码量直接砍掉一大半而且扩展起来只需要改一个数字。这篇就聊聊ST数组实战里怎么用FOR循环把批量采集这件事做干净、做稳、做出可维护性。这篇文章适合谁看如果你是刚接触结构化文本的PLC工程师想知道数组和循环在产线上怎么落地或者你已经用ST写过一些逻辑但批量采集时总遇到越界、死循环、扫描周期超时这些坑那这篇的内容应该都能对得上。我会从数组和FOR循环的基础语法讲起再给几个可以直接抄的实战模板最后把调试中常见的坑和排查思路一并整理出来。1. 项目背景与整体设计思路1.1 批量采集到底解决的是什么问题我们先明确一个词批量采集。它不是“一次把一堆数据抓过来”那么简单它背后真正的痛点是变量太多、操作太重复、逻辑太碎。举个例子一套污水处理设备需要实时采集进水温度、出水温度、pH值、溶解氧、液位等24个点。用梯形图写常规做法是每一路都单独做一次模拟量输入映射、一次工程量换算、一次上限下限报警。24路写下来画面里全是密密麻麻的方块和连线看起来就头大。更要命的是如果第8路温度变送器坏了要换量程你得找到那一块改里面两三个参数有时候还会漏改报警限值。如果你用ST数组把这24路数据装进一个数组那么“采集换算报警”就是用同一个FOR循环去遍历数组元素每路的差异只是下标不同。这样代码结构完全统一量程表可以单独用一个数组保存以后要改某一组量程直接改数据表逻辑一行都不用动。这才是批量采集真正的价值用循环结构去消化重复模式把“逐点编程”变成“批量配置”。1.2 为什么ST语言比梯形图更适合做这件事先说清楚ST是什么。STStructured Text是IEC 61131-3标准里的文本化编程语言语法风格接近Pascal/C现在主流PLC基本都支持比如西门子的SCL、施耐德的Unity Pro和SoMachine、倍福TwinCAT、科控/汇川等。它不像梯形图那样强调“触点-线圈”的电气思维而是更像写程序有变量声明、函数、循环、数组这些结构化编程能力。梯形图的强项是逻辑控制、互锁、安全回路电工背景的人看着直观但它的弱点是处理数据能力弱尤其是大批量数据搬运、计算、查找、排序这类任务梯形图写起来又慢又难看。ST弥补了这一点。你可以把ST理解成“PLC里的C语言”数组就是一块连续的数据空间FOR循环就是一把尺子把数组从头量到尾每格处理一遍。所以在批量采集这个场景里ST几乎是天然最优解硬件采集点很规律通道号是连续整数正好对应数组下标处理动作一致正好用循环统一执行数据要批量上送也正好用循环打包。1.3 方案选型与适用范围当然ST数组FOR循环也不是万能的用之前得想清楚场景。适合用的多路模拟量采集与工程量换算。通过Modbus/Profibus等总线批量读取多个从站设备的数据寄存器地址连续或轮询规律。对一段数组做统一处理比如批量清零、批量取平均值、批量报警判断。配方数据、工艺参数的批量下装与读取。数据打包/拆包比如把十路温度合成一个数组整体发给上位机。不适合用的安全联锁逻辑例如急停回路、光幕保护这类逻辑必须用硬逻辑或梯形图安全回路不能把安全判断放到循环里让扫描周期决定生死。单个点在时序上差异极大的控制比如某个阀门需要单独时序动作循环结构反而不方便不如状态机清晰。我做一个批量采集项目时一般会先把数据点按“类型、工程单位、量程、报警上下限”整理成一张表然后在ST里用数组和常量定义映射。这样程序代码和工艺数据分离调试的时候改数据表就行不用频繁改逻辑。2. ST数组与FOR循环的核心语法细节2.1 数组的声明和初始化别在这些基础地方翻车ST声明数组的通用格式是ARRAY[下标下限..下标上限] OF 数据类型。不同平台细节稍有区别但大框架一致。VAR aiRaw : ARRAY[0..15] OF INT; (* 16路模拟量原始值 *) aiTemp : ARRAY[0..15] OF REAL; (* 16路工程量温度值 *) strTags : ARRAY[1..8] OF STRING(20); (* 8个字符串标签 *) arrMatrix : ARRAY[0..3, 0..7] OF INT; (* 4行8列二维数组 *) END_VAR这里最容易魔幻的地方是下标起点。有的平台默认从0开始CoDeSys系列、西门子SCL有的老派PLC习惯从1开始还有的可以配置。所以写代码前先确认你用的控制器到底从几开始。一旦搞错要么浪费一个元素要么越界直接报错。数组初始化也很有讲究。如果你不对数组赋初值PLC上电后里面是随机值或者上次遗留值这在采集场景里很危险因为下一次计算前某些通道可能还没刷新用的却是旧数据。推荐在初始化块里用循环批量赋初值或者在声明时写初始值VAR aiTemp : ARRAY[0..15] OF REAL : [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; END_VAR要是嫌16个0打得累也可以直接在初始化逻辑里用FOR循环清零后面我会给完整代码。2.2 FOR循环的语法以及它和梯形图扫描的区别ST里最简单的FOR循环长这样FOR i : 0 TO 15 DO aiTemp[i] : INT_TO_REAL(aiRaw[i]) * 0.1 - 50.0; END_FOR;如果要从15往下走到0用DOWNTOFOR i : 15 DOWNTO 0 DO aiTemp[i] : 0.0; END_FOR;如果要隔一个处理一个比如只处理偶数下标可以用步长BY 2FOR i : 0 TO 14 BY 2 DO aiTemp[i] : 1.0; END_FOR;循环变量i一般声明为INT也可以用DINT。这里有个关键点FOR循环在PLC里是在同一个扫描周期内全部执行完的。也就是说如果循环体里面有1000条指令那这一个扫描周期就会增加这1000条指令的消耗。这和梯形图每个梯级按扫描执行还不一样梯形图每扫描一次只执行当前状态组合而FOR循环是“一次性把整个循环跑完”。所以循环体内的工作量直接决定扫描周期长度后面第4章我会讲怎么避免这个坑。2.3 循环体里的铁律别动循环变量这是一个很深的教训。我曾经在一个循环体里为了做条件跳转顺手改了循环变量i结果程序直接卡死看门狗超时CPU进入STOP。FOR循环的循环变量在标准逻辑里是自动递增/递减的你如果主动给它赋值循环的边界就会被打破要么死循环要么漏处理数据。正确做法是循环体内只使用i作为下标或计数器不要对它赋值。如果中间某个条件满足要提前退出用EXITCoDeSys等支持或者设计一个“跳过”逻辑比如用IF包裹处理语句FOR i : 0 TO 15 DO IF enable[i] THEN aiTemp[i] : INT_TO_REAL(aiRaw[i]) * 0.1; END_IF; END_FOR;这样遇到底层通道屏蔽、通道故障等情况可以只处理“启用”的点而循环下标始终由FOR语句自己管理。3. 批量采集的三种实战写法3.1 实战一16路模拟量采集与工程量转换先来一个最基础的完整例子。假设现场有16路温度传感器输入信号是4-20mA接到模拟量模块PLC内部原始值为0~27648以西门子习惯举例温度量程是-50℃~150℃那么工程值计算公式为工程值 原始值 / 27648 * (150 - (-50)) (-50)如果原始值对应aiRaw[i]那ST代码可以这样写VAR aiRaw : ARRAY[0..15] OF INT; (* 模拟量模块原始值 *) aiTemp : ARRAY[0..15] OF REAL; (* 转换后的温度工程值 *) i : INT; END_VAR FOR i : 0 TO 15 DO aiTemp[i] : INT_TO_REAL(aiRaw[i]) / 27648.0 * 200.0 - 50.0; END_FOR;这段代码看着简单但里面藏着很多优化空间。首先是硬件映射。在施耐德、西门子这些PLC里输入模块的每个通道都会映射到过程映像区的一个地址比如%IW3.0、IW64等。你可以直接在ST里把aiRaw[i]的地址改成与通道对应的输入字但更好的做法是用配置工具把连续的输入通道映射到同一块内部数据区让ST代码更整洁。其次是“坏值处理”。变送器断线时电流可能掉到0mA原始值直接变成0工程值会算出-50℃以下这个假数据如果传给HMI或上位机会造成误报警。所以实战中我一般会在循环内加一个范围判断FOR i : 0 TO 15 DO IF (aiRaw[i] 500) AND (aiRaw[i] 27000) THEN aiTemp[i] : INT_TO_REAL(aiRaw[i]) / 27648.0 * 200.0 - 50.0; ELSE aiTemp[i] : -9999.0; (* 故障标识值 *) END_IF; END_FOR;阈值500和27000需要根据实际传感器校验它的意思是“原始值低于某个小值视为断线高于某个大值视为超限这两种情况都别当真”。3.2 实战二Modbus批量读取从站数据批量采集不光是模拟量还有通信数据。现场8台电表每台要读电压、电流、功率、电度等10个寄存器如果一台一台写通信指令程序会非常冗余。可以用二维数组来存储批量结果VAR holdingRegs : ARRAY[0..7, 0..9] OF INT; (* 8台仪表每台10个寄存器 *) i : INT; j : INT; commDone : BOOL; commTrig : BOOL; END_VAR理论上的循环思路是FOR i : 0 TO 7 DO FOR j : 0 TO 9 DO holdingRegs[i,j] : ReadHoldingRegister(i, j); END_FOR; END_FOR;但实际现场我绝不会这么写。因为ReadHoldingRegister这种底层读函数如果真的发出一个Modbus报文它需要等待从站返回等待时间短则几十毫秒长则几百毫秒。如果把这个函数放到FOR循环里在一个扫描周期内连续调用PLC的扫描周期会被卡到几百毫秒甚至几秒看门狗不超时才怪。所以通信类批量采集的正确做法是用状态机把一个长循环拆成多个扫描周期来做。比如用当前通信状态来选择这一次扫描只处理“某一台仪表”CASE commStep OF 0: (* 正在读取第commIndex台 *) IF commTrig THEN commDone : FALSE; StartRead(commIndex); (* 启动读仪表commIndex *) commStep : 1; END_IF; 1: (* 等待完成标志 *) IF commDone THEN FOR j : 0 TO 9 DO holdingRegs[commIndex, j] : ReadResult[j]; END_FOR; commIndex : commIndex 1; IF commIndex 8 THEN commIndex : 0; END_IF; commStep : 0; END_IF; END_CASE;这里StartRead和ReadResult是通信功能块或底层驱动的封装commDone是通信库反馈的完成标志。状态机的本质是把原来循环里的“读取动作”分散到多个PLC扫描周期中每个周期只处理一台仪表既避免了扫描周期暴涨又保证8台仪表都能按顺序被处理完。这才是工业现场真正能跑的批量采集思路。3.3 实战三数组的查找、筛选与字符串处理批量采集不一定全是数字有时候还要处理文本型数组。热词里的“数组分割并显示包含某一字符”在ST里咋做我直接给一个能用的模板在一个字符串数组中找出所有包含关键字“OK”的元素并把它们的下标存到结果数组。VAR strArr : ARRAY[1..10] OF STRING(20); matched : ARRAY[1..10] OF INT; matchCnt : INT : 0; i : INT; END_VAR matchCnt : 0; FOR i : 1 TO 10 DO IF FIND(strArr[i], OK) 0 THEN matchCnt : matchCnt 1; matched[matchCnt] : i; END_IF; END_FOR;FIND是ST字符串处理函数返回子串第一次出现的位置找不到返回0。类似函数还有CONCAT拼接、LEFT取左段、RIGHT、MID等看具体平台手册。这里要留意的是字符串数组的长度一定要给够否则下标越界或截断的坑又会出现。还有STRIN这些函数在不同平台的名字可能不一样用前查一下在线帮助别照搬别人的代码。还有一个非常常见的批量需求把实数数组转成字符串数组或者反过来。比如上位机要求以字符串格式下发一批设定值你就需要用REAL_TO_STRING转换再循环写入字符串数组。这种转换类操作特别适合FOR循环因为转换规则一致只要处理好格式和精度就行。3.4 实战四施耐德PLC循环灯的移位实现热词里有个“施耐德PLC循环灯”和“开关控制led循环点亮程序左移点亮后不亮”顺手讲一下。很多初学者想把8个LED做成“流水灯”左移循环点亮。最容易想到的是用数组移位VAR led : ARRAY[0..7] OF BOOL; i : INT; END_VAR (* 左移前先把每一个元素的值赋给前一个位置 *) FOR i : 7 DOWNTO 1 DO led[i] : led[i-1]; END_FOR; led[0] : FALSE; (* 最低位补0 *)这段代码看起来没问题但实际跑起来你会发现灯会越移越少最后全灭。为什么因为左移本质上会丢位最低位补0后整个数组里的“1”会逐步被推到最高位然后消失。这正是热词里说的“左移点亮后不亮”。要做一个真正的循环流水灯应该在移完之后把“被挤掉的最高位”循环回最低位lastBit : led[7]; (* 先保存最高位 *) FOR i : 7 DOWNTO 1 DO led[i] : led[i-1]; END_FOR; led[0] : lastBit; (* 最高位回到最低位 *)这样就能实现环形移位灯永远循环不会熄灭。这个案例虽然是“点灯”但背后的批量移位逻辑跟数据队列、环形缓冲区、历史数据滚动更新是完全一样的。掌握了数组循环的移位技巧这些场景都能通用。4. 常见问题与排查技巧实录4.1 数组越界程序直接被CPU报错停掉数组越界是所有ST批量采集里最常见、最致命的问题。我见过不止一次程序在编译时没有问题一运行到某条FOR语句CPU直接报“Array Index Out of Range”然后STOP。原因就是循环终值写得比数组上限大。排查方法很简单看一眼循环语句的TO后面的值和数组定义的上限。比如定义ARRAY[0..15]最多只能访问[15]如果你的循环写的是FOR i : 0 TO 16那必然炸。更稳妥的写法是使用常量CONSTANT CH_COUNT : 16; END_VAR VAR aiTemp : ARRAY[0..CH_COUNT-1] OF REAL; i : INT; END_VAR FOR i : 0 TO CH_COUNT-1 DO aiTemp[i] : 0.0; END_FOR;这样以后改通道数量只需要改CH_COUNT一个常量数组定义和循环边界自动跟着变不会再出现“数组长度改了但循环边界忘了改”的低级错误。4.2 扫描周期暴涨一个FOR循环把PLC堵死前面说过FOR循环是在一个扫描周期内连续执行完的。如果循环体里面有大量数学运算、字符串处理、通信等待那么这一个周期会非常漫长。在实时性要求高的产线上这可能直接导致伺服联动卡顿、人机界面刷新迟缓。我的处理习惯是把大循环拆成小批次。比如要把1000个数据点全部处理一遍不需要一个周期干完可以定义offset变量每个周期只处理100个点10个周期完成一轮VAR offset : INT : 0; i : INT; END_VAR FOR i : 0 TO 99 DO IF (offset i) 1000 THEN ProcessData[offset i] : RawData[offset i] * Scale[offset i]; END_IF; END_FOR; offset : offset 100; IF offset 1000 THEN offset : 0; END_IF;这种“分段循环”既保留了FOR循环的结构简洁又不会让单周期负担过重。特别是配合通信、数据记录、上位机读写时我会尽可能把循环体里的“重操作”拆出去。4.3 数据一致性问题批量采集到的不是同一时刻的数据假如你在一个FOR循环里连续读16路模拟量输入这时候要搞明白模拟量模块的转换是并行的还是顺序的如果模块内的ADC是逐个通道扫描转换那你在循环里读到的第0路和第15路其实不是同一物理时刻的值。对于温度、液位这种慢变量无所谓但对于流量、压力这种瞬时值误差就会显现。解决办法有两个一是利用模块的同步采样能力如果硬件支持多通道同步采集在循环外先触发一次同步采样等转换完成后再循环读取结果。二是做数据缓冲。在定时中断任务里执行一次快速采集存到缓冲区数组主循环里只对缓冲区做计算和显示。这样保证了采集和处理分离数据快照的一致性更好。4.4 数据类型不匹配ST强类型不能拿INT直接当REAL用ST是强类型语言这一点和C有一点像但更严格。你把INT变量直接赋值给REAL数组元素编译器直接报错。必须用转换函数最常见的是INT_TO_REAL(x) DINT_TO_REAL(x) REAL_TO_INT(x) REAL_TO_STRING(x)批量采集里尤其是模拟量原始值INT要参与公式计算时我习惯先把中间结果存到REAL变量或REAL数组再进行浮点运算。避免在循环里反复做类型转换也会降低代码可读性。涉及字符串和数字互转更要注意。用INT_TO_STRING或REAL_TO_STRING转换后字符串的长度可能随着数值变化而变化字符串数组声明长度要留足否则截断后数据会丢。4.5 在线调试时怎么看数组内容ST数组不像单个变量那么好监视。很多PLC软件里你在Watch窗口添加一个数组名比如aiTemp能看到展开的元素列表但如果你给数组添加了“平滑显示”或者“按索引显示”记得把显示范围调正确。调试中我最常用的手段是在循环结束处打个断点逐周期看数组内容是否有更新。用数据追踪工具录制数组曲线比如施耐德的控制诊断、西门子的Trace功能能直观看到批量数据变化。在HMI上临时做一个表格控件直接把数组内容刷到画面上现场调试比盯软件变量窗口更直观。如果你发现数组某一特定下标的值永远是0别急着怀疑程序先用监视工具确认那个下标的物理地址有没有接错线或者通道是否在硬件配置中被禁用了。4.6 我自己的避坑清单写ST数组和FOR循环这几年我给自己整理了一份非常精简的检查清单每次写完批量采集逻辑都会过一遍写死数组长度前确认这个项目未来是否有扩展计划。有的话用常量代替字面量。循环变量定义成与数组下标完全匹配的数据类型。别用REAL当循环变量会精度丢到亲妈都不认识。循环体内不修改循环变量退出条件只依赖FOR语句本身。通信类、文件类、耗时类操作坚决不直接塞进FOR循环体除非确认阻塞时间可以接受。每个数组元素在使用前先赋初值尤其是上电首周期。对采集值做“坏值拦截”哪怕只是简单的上下限范围判断也能救你于假报警、假跳车之中。在程序里加一个“循环完成计数器”每一轮循环结束置位一次调试时能直观看到循环有没有正常跑完。就比如那个“循环完成计数器”我在一个项目里靠它抓到了循环体里偶发的跳转异常。当时数组里有198个数据偶尔某一个点会漏处理我就在循环外面加了一个loopCnt : loopCnt 1配合在线监视发现漏处理的次数和某个条件分支触发的次数刚好一致后来定位到是条件分支里错误地修改了数组下标。这种问题的隐蔽性很高没有完成计数器我可能得翻很久代码。批量采集这件事看着简单真正做深了其实考的是你对数据边界、执行效率、数据一致性的理解。用ST数组和FOR循环就像给这些重复劳动找了一把好用的尺子尺子量得准不准还得靠你在实战里一次一次校准。希望这篇把我在现场踩过的坑和沉淀下来的经验都传给你让你少走点弯路。
返回列表