在工业自动化项目中,处理批量数据是家常便饭,比如管理几十个传感器的温度值、记录上百个工件的加工参数,或者控制一系列执行器的状态。如果每个数据都单独定义一个变量,代码会变得冗长且难以维护。最近在优化一个设备控制项目时,我再次体会到,在 CODESYS 环境下,使用 ST(结构化文本)语言结合数组功能,能极大地简化这类批量数据操作的逻辑,让程序结构清晰、效率倍增。本文将系统梳理 CODESYS 中 ST 语言数组的完整用法,从基础声明、初始化到高级的遍历、排序及与函数块(FB)的配合,并提供一个从零开始的实战案例,帮助无论是刚接触 CODESYS 的新手,还是希望优化代码结构的工程师,都能快速掌握这一高效工具。
1. CODESYS、ST语言与数组:核心概念解析
在深入代码之前,我们有必要厘清几个核心概念及其相互关系,这有助于理解为什么这个组合如此强大。
1.1 什么是 CODESYS?
CODESYS 是一个独立的、硬件厂商中立的软件开发平台,用于编程控制器(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(程序组织单元)语言。
- 在 “Standard project” 模板中,为项目命名,例如
- 项目结构:创建完成后,你会在 “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_TYPEaSensorLog: 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; // 计算元素个数,结果为 11
4. 完整实战案例:仓库温度监控与报警系统
让我们通过一个模拟的“仓库温度监控系统”来综合运用上述知识。该系统需要监控仓库内 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表示 Array,i表示 INT,r表示 REAL。aiProductionCount(INT数组),arTemperatures(REAL数组),astLogEntries(结构体数组)。 - 索引变量:在循环中,使用有意义的索引变量名,如
iRow,iCol,nZoneIndex,避免简单的i,j,尤其是在多层嵌套循环中。 - 常量定义:数组大小、索引边界、阈值等应定义为常量(
CONSTANT),集中管理,避免“魔法数字”散落在代码中。VAR_GLOBAL CONSTANT ciMaxZones : INT := 100; crCriticalTemp : REAL := 85.5; END_VAR
6.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 平台上构建出处理复杂数据流和算法的强大控制程序。从简单的数据集合到模拟复杂的矩阵运算、状态机或配方管理,数组都是不可或缺的工具。建议从文中的实战案例出发,尝试修改和扩展其功能,例如添加温度历史趋势记录(使用二维数组或结构体数组)、实现更复杂的统计滤波算法等,在实践中巩固和深化理解。