公司动态
CODESYS ST语言数组实战:从基础到工业批量数据处理
在工业自动化项目中处理批量数据是家常便饭比如管理几十个传感器的温度值、记录上百个工件的加工参数或者控制一系列执行器的状态。如果每个数据都单独定义一个变量代码会变得冗长且难以维护。最近在优化一个设备控制项目时我再次体会到在 CODESYS 环境下使用 ST结构化文本语言结合数组功能能极大地简化这类批量数据操作的逻辑让程序结构清晰、效率倍增。本文将系统梳理 CODESYS 中 ST 语言数组的完整用法从基础声明、初始化到高级的遍历、排序及与函数块FB的配合并提供一个从零开始的实战案例帮助无论是刚接触 CODESYS 的新手还是希望优化代码结构的工程师都能快速掌握这一高效工具。1. CODESYS、ST语言与数组核心概念解析在深入代码之前我们有必要厘清几个核心概念及其相互关系这有助于理解为什么这个组合如此强大。1.1 什么是 CODESYSCODESYS 是一个独立的、硬件厂商中立的软件开发平台用于编程控制器PLC和自动化组件。你可以把它理解为一个“自动化界的集成开发环境IDE”。它的核心优势在于其符合 IEC 61131-3 国际标准该标准定义了五种编程语言而 ST 语言就是其中之一。这意味着无论你使用倍福Beckhoff、施耐德Schneider还是其他支持 CODESYS 运行时Runtime的 PLC其编程理念和语法都是相通的极大地提升了代码的可移植性和工程师的技能复用率。1.2 ST结构化文本语言简介ST 语言是一种高级的、类似于 Pascal 或 C 的文本化编程语言。它特别适合处理复杂的数学运算、算法逻辑以及数据管理。与梯形图LD或功能块图FBD相比ST 在处理循环、条件分支和大量数据操作时代码更加紧凑和直观。其语法结构清晰支持IF...THEN...ELSE、CASE...OF、FOR、WHILE等标准控制语句这使得它成为实现数组操作的理想选择。1.3 数组在自动化编程中的价值数组是一种数据结构用于存储相同数据类型的多个元素并通过一个索引下标来访问每个元素。在 PLC 编程中数组的价值主要体现在批量处理无需为每个同类型数据创建单独变量例如可以用一个数组aTemperature: ARRAY[1..20] OF REAL;来管理20个温度值。简化代码通过循环可以轻松地对数组中所有元素执行相同操作如求和、求平均、寻找最大值等。提升可维护性数据组织有序当需要增加或减少数据点时通常只需修改数组大小和循环边界而非大量变量名。便于与HMI/SCADA交互许多上位机软件支持直接映射数组变量方便进行数据监视和批量参数设置。将 CODESYS 的平台能力、ST 语言的算法表达力与数组的数据组织能力相结合就能构建出既强大又优雅的自动化解决方案。2. 环境准备与项目创建在开始编写代码前我们需要搭建好开发环境。本文基于 CODESYS Development System V3.5 SP11 版本进行演示但核心语法在 V3.x 版本中基本通用。2.1 软件安装与配置获取软件从 CODESYS 官网下载 Development System 安装包。对于学习用途可以使用免费的完整功能版。安装运行安装程序按照向导完成安装。建议安装路径不要包含中文或空格。新建项目启动 CODESYS点击 “File” - “New Project”。在 “Standard project” 模板中为项目命名例如ST_Array_Demo。选择设备型号。对于纯逻辑学习和仿真可以选择 “CODESYS Control Win V3” 或 “Device (PLC) simulation”。这里我们选择 “Device (PLC) simulation” 以便运行和测试。选择 “Structured Text (ST)” 作为默认的 POU程序组织单元语言。项目结构创建完成后你会在 “Application” 下看到PLC_PRG(主程序)、MAIN等默认生成的 POU。我们将在PLC_PRG中编写主要逻辑也可以创建新的 POU 来封装功能。2.2 理解 CODESYS 中的数据类型在声明数组前必须明确其元素的数据类型。CODESYS ST 语言支持丰富的数据类型基本类型BOOL,BYTE,WORD,DWORD,SINT,USINT,INT,UINT,DINT,UDINT,REAL,LREAL,STRING等。派生类型用户可以基于基本类型定义自己的类型如TYPE stSensorData : STRUCT Value : REAL; Status : BOOL; Timestamp : DATE_AND_TIME; END_STRUCT END_TYPE之后就可以声明该类型的数组aSensorLog: ARRAY[1..100] OF stSensorData;。3. ST语言中数组的核心语法详解本章节将详细拆解数组的声明、初始化、访问以及各种操作。3.1 数组的声明与初始化数组声明的通用语法为变量名 : ARRAY[下限..上限] OF 数据类型;下限和上限定义了数组的索引范围。在 CODESYS 中索引通常是整数并且可以是负数或任意整数但必须满足下限 上限。常见的起始索引是 0 或 1。多维数组可以声明二维、三维甚至更多维数组。// 一维数组存储10个整数 aiProductionCount : ARRAY[1..10] OF INT; // 二维数组模拟一个3x4的矩阵实数 aMatrix : ARRAY[1..3, 1..4] OF REAL; // 三维数组例如表示一个仓库货架层、行、列 aWarehouse : ARRAY[1..5, 1..10, 1..20] OF STRING(20);初始化数组可以在声明时直接进行// 声明并初始化一维数组 aiPreSetValues : ARRAY[1..5] OF INT : [10, 20, 30, 40, 50]; // 声明并初始化二维数组按行展开 aInitMatrix : ARRAY[1..2, 1..3] OF REAL : [1.1, 1.2, 1.3, 2.1, 2.2, 2.3]; // 更清晰的写法CODESYS支持 aInitMatrix2 : ARRAY[1..2, 1..3] OF REAL : [[1.1, 1.2, 1.3], [2.1, 2.2, 2.3]];3.2 访问与修改数组元素通过索引访问数组元素aiProductionCount[1] : 100; // 将第一个元素赋值为100 rSingleValue : aMatrix[2, 3]; // 获取第2行第3列的元素 aWarehouse[2, 5, 8] : Part-ABC-123; // 设置三维数组中的一个元素重要提示在 ST 中直接使用超出声明范围的索引如aiProductionCount[11]会导致运行时错误。安全的做法是在访问前检查索引有效性。3.3 使用循环遍历数组这是数组最强大的功能之一。FOR循环是遍历数组的绝佳搭档。// 示例计算一维数组所有元素的总和 VAR aiSampleData : ARRAY[1..8] OF INT : [5, 3, 8, 1, 9, 2, 7, 4]; iSum : INT : 0; iIndex : INT; END_VAR FOR iIndex : 1 TO 8 BY 1 DO iSum : iSum aiSampleData[iIndex]; END_FOR // 循环结束后iSum 的值为 39对于多维数组需要使用嵌套循环// 示例遍历并打印二维数组所有元素 VAR aGrid : ARRAY[1..3, 1..4] OF REAL : [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]; iRow, iCol : INT; rValue : REAL; END_VAR FOR iRow : 1 TO 3 DO FOR iCol : 1 TO 4 DO rValue : aGrid[iRow, iCol]; // 在实际PLC中可以将rValue发送到日志或HMI // 这里我们假设有一个调试函数 LogReal(rValue) END_FOR END_FOR3.4 数组的常用操作赋值、比较、函数整体赋值CODESYS ST 支持相同类型和维度的数组进行整体赋值。VAR aSource, aDest : ARRAY[1..5] OF INT; END_VAR aSource : [1,2,3,4,5]; aDest : aSource; // aDest 现在也是 [1,2,3,4,5]比较不能直接用或比较整个数组。需要逐个元素比较或编写一个专用的比较函数。内置函数CODESYS 提供了一些用于数组的内置函数非常实用SIZEOF(arr)返回数组的总字节数。要获取元素个数需要用SIZEOF(arr) / SIZEOF(arr[1])。LOWER_BOUND(arr, dim)和UPPER_BOUND(arr, dim)动态获取数组指定维度的下限和上限。这是编写通用函数的关键。VAR aiDynamicArray : ARRAY[5..15] OF INT; iLow, iHigh, iCount : INT; END_VAR iLow : LOWER_BOUND(aiDynamicArray, 1); // 返回 5 iHigh : UPPER_BOUND(aiDynamicArray, 1); // 返回 15 iCount : iHigh - iLow 1; // 计算元素个数结果为 114. 完整实战案例仓库温度监控与报警系统让我们通过一个模拟的“仓库温度监控系统”来综合运用上述知识。该系统需要监控仓库内 8 个区域的温度计算平均温度、最高/最低温度及其位置并在任何区域温度超限时触发报警。4.1 需求分析与数据结构设计监控8个区域使用一维数组aTemperatures存储温度值REAL。记录温度状态使用另一个一维数组abAlarm存储对应的报警状态BOOL。计算统计值需要实时计算平均温度、最高温度、最低温度及其区域编号。报警逻辑当某个区域温度超过设定上限如 30.0°C时对应报警位置位。4.2 创建程序组织单元POU我们在PLC_PRG中实现主逻辑同时创建一个函数块FB_TemperatureMonitor来封装核心功能提高代码复用性。第一步创建数据类型和全局变量在PLC_PRG的上级通常是GVL全局变量列表或PLC_PRG内部定义VAR_GLOBAL CONSTANT // 定义常量 ciNumOfZones : INT : 8; crTempHighLimit : REAL : 30.0; END_VAR VAR_GLOBAL // 主温度数组和报警数组 aZoneTemperatures : ARRAY[1..ciNumOfZones] OF REAL; abZoneAlarms : ARRAY[1..ciNumOfZones] OF BOOL; // 统计结果 rAverageTemp : REAL; rMaxTemp : REAL; rMinTemp : REAL; iMaxTempZone : INT; iMinTempZone : INT; END_VAR第二步创建函数块FB_TemperatureMonitor右键点击 “Application” - “Add Object” - “Function Block”。FUNCTION_BLOCK FB_TemperatureMonitor VAR_INPUT // 输入温度数组和数组大小 aTemperatures : ARRAY[*] OF REAL; // 使用可变数组*使其能接受不同大小的数组 iArraySize : INT; // 传入实际数组大小 rLimit : REAL; // 报警上限 END_VAR VAR_OUTPUT // 输出报警数组和统计信息 abAlarms : ARRAY[*] OF BOOL; rAvg : REAL; rMax : REAL; rMin : REAL; iIdxOfMax : INT; iIdxOfMin : INT; END_VAR VAR i : INT; rSum : REAL : 0.0; END_VAR在函数块主体中编写算法// 初始化最大值和最小值假设第一个元素为初始值 rMax : aTemperatures[LOWER_BOUND(aTemperatures,1)]; rMin : aTemperatures[LOWER_BOUND(aTemperatures,1)]; iIdxOfMax : LOWER_BOUND(aTemperatures,1); iIdxOfMin : LOWER_BOUND(aTemperatures,1); rSum : 0.0; // 遍历数组 FOR i : LOWER_BOUND(aTemperatures,1) TO LOWER_BOUND(aTemperatures,1) iArraySize - 1 DO // 1. 累加求和用于计算平均值 rSum : rSum aTemperatures[i]; // 2. 寻找最大值及其索引 IF aTemperatures[i] rMax THEN rMax : aTemperatures[i]; iIdxOfMax : i; END_IF // 3. 寻找最小值及其索引 IF aTemperatures[i] rMin THEN rMin : aTemperatures[i]; iIdxOfMin : i; END_IF // 4. 判断报警 abAlarms[i] : aTemperatures[i] rLimit; END_FOR // 5. 计算平均值 IF iArraySize 0 THEN rAvg : rSum / iArraySize; ELSE rAvg : 0.0; END_IF4.3 在主程序中调用并测试在PLC_PRG中实例化函数块并编写测试逻辑PROGRAM PLC_PRG VAR fbTempMon : FB_TemperatureMonitor; // 实例化功能块 // 模拟的温度数据可以来自实际的AI模块 aSimulatedTemps : ARRAY[1..8] OF REAL : [25.5, 28.0, 31.2, 29.8, 26.1, 24.9, 30.5, 27.3]; i : INT; END_VAR // 每周期调用一次功能块 fbTempMon( aTemperatures : aSimulatedTemps, iArraySize : 8, rLimit : 30.0, abAlarms abZoneAlarms, // 输出连接到全局报警数组 rAvg rAverageTemp, rMax rMaxTemp, rMin rMinTemp, iIdxOfMax iMaxTempZone, iIdxOfMin iMinTempZone ); // 模拟温度数据变化例如每10个周期让某个区域温度上升 // 这里使用一个简单的计数器来模拟变化 VAR nCycleCounter : UINT : 0; END_VAR nCycleCounter : nCycleCounter 1; IF nCycleCounter MOD 10 0 THEN // 每10个周期随机让一个区域温度增加2度模拟环境变化 aSimulatedTemps[(nCycleCounter/10) MOD 8 1] : aSimulatedTemps[(nCycleCounter/10) MOD 8 1] 2.0; END_IF // 注意实际项目中aSimulatedTemps 应由真实的I/O映射或通讯数据填充4.4 运行与在线监控登录到你的 PLC 或启动仿真器。将项目下载到目标设备。在 CODESYS 开发环境中打开 “Online” - “Watch Tables”。添加需要监控的变量aSimulatedTemps,abZoneAlarms,rAverageTemp,rMaxTemp,iMaxTempZone等。在线运行后你将看到abZoneAlarms数组中对应超温区域温度30.0的位置变为 TRUE。同时统计信息会实时更新。可以尝试在线修改aSimulatedTemps中的某个值观察报警和统计结果如何动态响应。5. 常见问题与排查思路在使用 ST 语言和数组时你可能会遇到以下典型问题。问题现象可能原因排查步骤与解决方案编译错误“Array index out of bounds”访问数组时使用的索引超出了声明的范围。1. 检查数组声明ARRAY[下限..上限]。2. 检查循环变量i的终值确保i 上限。3. 使用LOWER_BOUND()和UPPER_BOUND()函数代替硬编码的索引边界。运行时错误或PLC进入故障状态除零错误如计算平均值时数组大小为0或非法操作。1. 在除法运算前检查除数是否为零。2. 确保传入功能块的iArraySize参数有效大于0且不超过数组实际大小。3. 使用调试器单步执行定位出错的具体行。数组赋值不成功或值未更新1. 数据类型不匹配。2. 在错误的程序周期中操作。3. 变量未正确连接到功能块输出。1. 确认赋值语句左右两侧的数据类型完全一致。2. 确保数组操作代码在周期性执行的任务如PLC_PRG中。3. 检查功能块调用时输出参数是否使用了正确连接到了目标变量。使用SIZEOF得不到元素个数SIZEOF返回的是字节数不是元素个数。使用公式元素个数 SIZEOF(arr) / SIZEOF(arr[第一个元素])来计算。对于可变数组使用UPPER_BOUND - LOWER_BOUND 1更安全。多维数组初始化或访问混乱对多维数组在内存中的排列顺序理解有误。记住CODESYS 中多维数组是“行主序”。ARRAY[1..2,1..3]初始化时顺序是[1,1], [1,2], [1,3], [2,1], [2,2], [2,3]。使用嵌套循环时外层循环对应第一维行。功能块处理可变数组时出错功能块内部使用了硬编码的索引未使用LOWER_BOUND。在功能块内部永远不要对输入数组使用如aTemperatures[1]这样的硬编码索引。始终使用aTemperatures[LOWER_BOUND(...)]作为起始点并结合传入的iArraySize进行循环。6. 最佳实践与工程建议掌握基础语法后遵循以下最佳实践能让你的代码更加健壮、可维护和高效。6.1 命名规范与代码组织数组命名使用前缀表明其类型和用途。例如a表示 Arrayi表示 INTr表示 REAL。aiProductionCount(INT数组),arTemperatures(REAL数组),astLogEntries(结构体数组)。索引变量在循环中使用有意义的索引变量名如iRow,iCol,nZoneIndex避免简单的i,j尤其是在多层嵌套循环中。常量定义数组大小、索引边界、阈值等应定义为常量CONSTANT集中管理避免“魔法数字”散落在代码中。VAR_GLOBAL CONSTANT ciMaxZones : INT : 100; crCriticalTemp : REAL : 85.5; END_VAR6.2 安全性与健壮性边界检查在访问数组元素前特别是当索引来自外部输入如HMI设定、通讯数据时必须进行有效性检查。IF (iInputIndex LOWER_BOUND(aMyArray,1)) AND (iInputIndex UPPER_BOUND(aMyArray,1)) THEN rValue : aMyArray[iInputIndex]; ELSE // 处理错误记录日志使用默认值或置位故障标志 bIndexError : TRUE; END_IF初始化显式初始化数组避免使用未定义的随机值。可以在声明时初始化或在程序启动时如PLC_PRG的第一个周期用循环进行初始化。处理可变数组当编写通用函数或功能块时使用ARRAY[*]可变数组并结合LOWER_BOUND/UPPER_BOUND使代码能适应不同大小的输入。6.3 性能考量循环优化对于大型数组将循环内不变的计算移到循环外。避免在循环内调用耗时长的函数或功能块。内存占用合理规划数组大小。对于非常大的数组考虑是否真的需要全部存储在内存中或者是否可以分块处理。使用内置函数对于常见的操作如找最大值如果 CODESYS 库提供了经过优化的函数如MAX函数可接受数组输入需查库手册优先使用库函数。6.4 与高级功能的结合联合体UNION与数组可以创建联合体数组用于实现类似 C 语言中“变体”的数据结构节省内存或在同一存储区域解释不同数据类型。指针与数组高级用户可以使用指针来高效地操作数组内存但这需要非常小心避免内存越界。文件操作与数组可以将数组数据整体保存到文件或从文件加载用于配方Recipe管理或数据记录。使用FileRead/FileWrite功能块时注意数据格式。通过将 ST 语言的逻辑控制能力与数组的数据组织能力深度结合你可以在 CODESYS 平台上构建出处理复杂数据流和算法的强大控制程序。从简单的数据集合到模拟复杂的矩阵运算、状态机或配方管理数组都是不可或缺的工具。建议从文中的实战案例出发尝试修改和扩展其功能例如添加温度历史趋势记录使用二维数组或结构体数组、实现更复杂的统计滤波算法等在实践中巩固和深化理解。