数组与字符串操作
数组与字符串是 PLC 编程中最常用的数据类型。本章详细说明 SCL 中数组声明、初始化、访问、切片操作,以及字符串的声明、处理函数与格式化转换,并给出常见陷阱与工业现场最佳实践。
1. ARRAY 声明
1.1 一维数组
VAR
// 声明显式上下界
a1 : ARRAY[0..9] OF INT; // 10 个元素, 索引 0 ~ 9
a2 : ARRAY[1..10] OF INT; // 10 个元素, 索引 1 ~ 10
a3 : ARRAY[-5..5] OF REAL; // 11 个元素, 索引 -5 ~ 5
// 常用模式: 从 0 或 1 开始
dataBuf : ARRAY[0..255] OF BYTE; // 通信缓冲区
axisPos : ARRAY[1..8] OF LREAL; // 8 轴位置数组
END_VAR
上下界规则:
- 下界和上界都是 DINT 范围的整数常量(或常量表达式),不能是变量。
- 下界可以小于、等于或大于 0;负数索引在需要查表偏移时很实用。
- 元素个数 = 上界 - 下界 + 1。
1.2 二维数组
VAR
// 二维数组: 行优先 (ROW-MAJOR)
mat : ARRAY[1..3, 1..4] OF INT; // 3 行 × 4 列 = 12 个元素
// 矩阵应用
matrix3x3 : ARRAY[1..3, 1..3] OF LREAL; // 3×3 变换矩阵
ioTbl : ARRAY[1..16, 1..8] OF BOOL; // 16 模块 × 8 通道
END_VAR
1.3 三维数组
VAR
cube : ARRAY[1..5, 1..6, 1..7] OF INT; // 5×6×7 = 210 个元素
END_VAR
1.4 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 边界写错 | ARRAY[0..10] 是 11 个元素,不是 10 个 |
| 负索引溢出 | 下界为负数时,索引运算要小心符号错误 |
| 数组大小超出内存 | 大型 ARRAY[*] 或 ARRAY[0..10000] 可能超出任务可用内存,需检查剩余内存 |
2. 数组初始化
2.1 字面量初始化
VAR
// 一维初始化
a1 : ARRAY[0..4] OF INT := [10, 20, 30, 40, 50];
// 二维初始化 (行优先)
a2 : ARRAY[1..2, 1..3] OF INT := [
[1, 2, 3],
[4, 5, 6]
];
// 三维初始化
a3 : ARRAY[1..2, 1..2, 1..2] OF INT := [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
];
END_VAR
2.2 重复语法
VAR
// 10 个元素全部初始化为 0
zeros : ARRAY[0..9] OF INT := [10(0)];
// 混合: 前 3 个 100, 后 7 个 0
mixed : ARRAY[0..9] OF INT := [3(100), 7(0)];
// 二维重复: 全部 0
mat : ARRAY[1..3, 1..4] OF INT := [12(0)];
END_VAR
重复语法 [N(value)] 展开为 N 个连续的 value,在需要大数组初始化为统一值时非常高效。
2.3 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 元素个数不匹配 | 初始值列表长度必须等于数组元素个数,否则编译错误 |
| 重复语法嵌套 | 二维数组不能写 [3([4(0)])],必须展开或 [12(0)] |
| 初始化覆盖 | 声明时初始化与运行时赋值会覆盖,注意时序 |
3. 数组索引
3.1 索引方式
PROGRAM Main
VAR
arr : ARRAY[0..9] OF INT := [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
val : INT;
i : INT;
END_VAR
// 直接索引
val := arr[0]; // 10
val := arr[9]; // 100
// 变量索引
i := 5;
val := arr[i]; // 60
// 表达式索引
val := arr[i - 1]; // 50
END_PROGRAM
3.2 零基 vs 一基
| 特性 | 零基 [0..N-1] | 一基 [1..N] |
|---|---|---|
| 习惯 | 嵌入式/C 背景工程师 | 传统 PLC 工程师 |
| 循环 | FOR i := 0 TO 9 | FOR i := 1 TO 10 |
| 偏移量 | 索引 = 偏移 | 索引 = 偏移 + 1 |
| 建议 | 数据缓冲区、通信协议 | 轴号、模块号、通道号 |
DarraRT 建议: 统一团队风格。数据缓冲区用零基,物理设备编号用一基。不要在同一个项目中混用两种风格。
3.3 边界检查
// 安全写法: 索引前检查边界
IF idx >= LOWER_BOUND(arr, 1) AND idx <= UPPER_BOUND(arr, 1) THEN
val := arr[idx];
ELSE
// 记录错误日志, 不访问
LogError('数组索引越界', idx);
END_IF
运行时边界检查:
- 调试模式下默认启用,越界触发运行时异常。
- 发布模式下可以关闭以提升性能,但建议始终保留(工业现场数据越界可能导致不可预测的后果)。
- 使用
LOWER_BOUND/UPPER_BOUND函数获取数组边界,不要硬编码。
4. 多维数组
4.1 行优先顺序
SCL 多维数组采用 行优先 (Row-Major) 存储,即最右侧下标变化最快。
VAR
mat : ARRAY[1..3, 1..4] OF INT;
row, col : INT;
idx : INT;
END_VAR
// 行优先遍历: 外循环行, 内循环列
FOR row := 1 TO 3 DO
FOR col := 1 TO 4 DO
mat[row, col] := (row - 1) * 4 + (col - 1);
END_FOR;
END_FOR;
// 等价于线性访问:
// mat[1,1]=0, mat[1,2]=1, ..., mat[3,4]=11
END_PROGRAM
4.2 嵌套循环遍历
VAR
cube : ARRAY[1..5, 1..6, 1..7] OF REAL;
x, y, z : INT;
sum : REAL;
END_VAR
sum := 0.0;
FOR x := 1 TO 5 DO
FOR y := 1 TO 6 DO
FOR z := 1 TO 7 DO
sum := sum + cube[x, y, z];
END_FOR;
END_FOR;
END_FOR;
END_PROGRAM
4.3 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 索引顺序写反 | mat[row, col] 与 mat[col, row] 语义完全不同 |
| 大维数组性能 | 三维以上数组嵌套循环性能开销大,考虑用一维线性化替代 |
| 内存布局依赖 | 不要依赖具体的存储布局做指针运算(SCL 不保证) |
5. 数组切片 (DarraRT 扩展)
DarraRT SCL 支持数组切片语法,用于快速取子数组。
VAR
src : ARRAY[0..9] OF INT := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
dst : ARRAY[0..4] OF INT;
sub : ARRAY[0..2] OF INT;
END_VAR
// 取连续片段: src[2..4] => [2, 3, 4]
dst[0..2] := src[2..4];
// 切片赋值
dst[0..4] := src[5..9]; // [5, 6, 7, 8, 9]
// 步长切片 (DarraRT 扩展)
// sub := src[0..8 STEP 2]; // [0, 2, 4, 6, 8] (如支持)
END_PROGRAM
约束:
- 切片两端必须为常量或变量,且切片长度必须与目标长度匹配。
- 切片不能跨任务或跨 FB 实例传递(仅限局部使用)。
- 步长切片为 DarraRT 扩展,跨平台移植时需注意兼容性。
6. 数组整体赋值
VAR
a : ARRAY[0..4] OF INT := [1, 2, 3, 4, 5];
b : ARRAY[0..4] OF INT;
c : ARRAY[0..9] OF INT;
END_VAR
// 整数组拷贝: 类型和大小必须完全一致
b := a; // OK: b = [1, 2, 3, 4, 5]
// 以下会编译错误: 大小不匹配
// c := a; // ERROR: 大小不同
// 逐元素拷贝 (大小不同时的替代方案)
FOR i := 0 TO 4 DO
c[i] := a[i];
END_FOR;
END_PROGRAM
规则:
- 整数组赋值要求左右两侧的类型和维数大小完全一致。
- 结构体数组同样支持整体赋值(要求结构体类型一致)。
- 如果数组大小不同,必须用
FOR循环逐元素拷贝。
7. ARRAY[*] 变长数组
FUNCTION SumArray : REAL
VAR_INPUT
arr : ARRAY[*] OF REAL; // 变长数组参数
END_VAR
VAR
i : DINT;
s : REAL;
END_VAR
s := 0.0;
FOR i := LOWER_BOUND(arr, 1) TO UPPER_BOUND(arr, 1) DO
s := s + arr[i];
END_FOR;
SumArray := s;
END_FUNCTION
约束:
ARRAY[*]只能用于 函数/功能块的形式参数,不能用于全局变量或局部变量。- 在函数体内必须通过
LOWER_BOUND/UPPER_BOUND获取实际边界,不能硬编码。 - 调用时传入不同大小的数组不出错,函数自动适配。
- 不支持
ARRAY[*, *]二维变长(仅支持一维)。
8. STRING 声明
8.1 基本声明
VAR
// 默认长度: 80 字符
s1 : STRING; // 等效 STRING[80]
// 指定最大长度
s2 : STRING[20]; // 最大 20 字符
s3 : STRING[255]; // 最大 255 字符
// 声明时初始化
s4 : STRING[20] := 'Hello PLC';
s5 : STRING[100] := '工业自动化控制';
// WSTRING (Unicode)
ws1 : WSTRING; // 默认 80 个宽字符
ws2 : WSTRING[50] := "中文Unicode支持";
END_VAR
8.2 STRING 与 WSTRING 对比
| 特性 | STRING | WSTRING |
|---|---|---|
| 编码 | ASCII / ANSI (单字节) | Unicode (UTF-16, 每个字符 2 字节) |
| 默认长度 | 80 字符 | 80 个宽字符 |
| 最大长度 | 255 字符 | 255 个宽字符 |
| 内存占用 | 长度 + 1 字节 | (长度 + 1) × 2 字节 |
| 适用场景 | 英文数字、短标签、IO 描述 | 中文、日文、多语言文本 |
| 兼容性 | 所有 PLC 通用 | 部分老型号不支持 |
DarraRT 建议: 凡是涉及中文显示或国际化文本,统一使用 WSTRING;内部标签、报警编码、通信协议字段使用 STRING。
9. 字符串操作函数
9.1 函数速查表
| 函数 | 语法 | 说明 |
|---|---|---|
LEN | LEN(s) | 返回字符串当前实际长度(不含结尾空字符) |
LEFT | LEFT(s, n) | 返回字符串最左边 n 个字符 |
RIGHT | RIGHT(s, n) | 返回字符串最右边 n 个字符 |
MID | MID(s, start, n) | 返回从 start 开始的 n 个字符 |
CONCAT | CONCAT(s1, s2) | 拼接两个字符串 |
FIND | FIND(s1, s2) | 返回 s2 在 s1 中首次出现的位置,未找到返回 0 |
INSERT | INSERT(s1, s2, pos) | 在 s1 的 pos 位置插入 s2 |
DELETE | DELETE(s, start, n) | 删除 s 中从 start 开始的 n 个字符 |
REPLACE | REPLACE(s1, s2, start, n) | 将 s1 中从 start 开始的 n 个字符替换为 s2 |
9.2 使用示例
PROGRAM StringDemo
VAR
s, s1, s2, result : STRING[100];
pos, len : INT;
END_VAR
s := 'DarraRT PLC';
// LEN
len := LEN(s); // 10
// LEFT / RIGHT
result := LEFT(s, 5); // 'Darra'
result := RIGHT(s, 3); // 'PLC'
// MID
result := MID(s, 1, 4); // 'Darr' (从位置 1 开始取 4 个)
// CONCAT
s1 := 'Hello';
s2 := ' World';
result := CONCAT(s1, s2); // 'Hello World'
// FIND
pos := FIND(s, 'RT'); // 6 (位置从 1 开始计数)
pos := FIND(s, 'XYZ'); // 0 (未找到)
// INSERT
result := INSERT('Hello', ' AB', 6); // 'Hello AB' (在位置 6 插入, 即末尾)
// DELETE
result := DELETE('Hello World', 6, 6); // 'Hello' (删除位置 6 开始的 6 个字符)
// REPLACE
result := REPLACE('Hello World', 'PLC', 7, 5); // 'Hello PLC' (替换位置 7 开始的 5 个字符)
END_PROGRAM
9.3 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 索引从 1 开始 | 字符串位置从 1 开始计数,不是 0 |
| 超出长度截断 | 结果超出目标变量最大长度时自动截断,无警告 |
| FIND 返回 0 表示未找到 | 0 不是有效位置,位置从 1 开始 |
| WSTRING 函数兼容 | 部分函数不支持 WSTRING,需用 WSTRING 专用版本 |
| 空字符串处理 | LEN('') = 0,LEFT('', n) = '',FIND('', 'x') = 0 |
10. 字符串格式化与转换
10.1 数值转字符串
VAR
realVal : REAL := 123.456;
intVal : DINT := -789;
strReal : STRING[20];
strInt : STRING[20];
strResult : STRING[50];
END_VAR
// REAL → STRING
strReal := REAL_TO_STRING(realVal); // '1.234560e+002'
// DINT → STRING
strInt := DINT_TO_STRING(intVal); // '-789'
// 字符串拼接
strResult := CONCAT('Value = ', strReal);
END_PROGRAM
10.2 字符串转数值
VAR
s : STRING[20] := '123.456';
r : REAL;
di : DINT;
err : BOOL;
END_VAR
// STRING → REAL
r := STRING_TO_REAL(s); // 123.456
// STRING → DINT (会截断小数)
di := STRING_TO_DINT(s); // 123
// 非法字符串转换: 返回 0 (不抛异常)
s := 'ABC';
r := STRING_TO_REAL(s); // 0.0
// 建议先验证字符串格式
END_PROGRAM
10.3 类型转换函数表
| 转换方向 | 函数 | 示例结果 |
|---|---|---|
| REAL → STRING | REAL_TO_STRING(1.5) | '1.500000e+000' |
| LREAL → STRING | LREAL_TO_STRING(1.5) | '1.500000000000000e+000' |
| DINT → STRING | DINT_TO_STRING(-123) | '-123' |
| INT → STRING | INT_TO_STRING(99) | '99' |
| TIME → STRING | TIME_TO_STRING(T#5s) | 'T#5s' |
| DATE → STRING | DATE_TO_STRING(D#2026-07-26) | 'D#2026-07-26' |
| STRING → REAL | STRING_TO_REAL('1.5') | 1.5 |
| STRING → DINT | STRING_TO_DINT('123') | 123 |
| STRING → TIME | STRING_TO_TIME('T#5s') | T#5s |
11. 字符串陷阱与工业现场最佳实践
11.1 截断问题
VAR
sSmall : STRING[10] := 'Hello';
sLong : STRING[20] := 'This is a long message';
BEGIN
// 截断: 目标变量不足以容纳源字符串
sSmall := sLong; // sSmall = 'This is a ' (长度 10, 自动截断)
// 截断发生后无错误标志, 数据静默丢失
// 工业现场: 报警消息截断可能导致关键信息丢失!
END_VAR
最佳实践:
- 字符串目标变量长度应 ≥ 源字符串实际最大长度。
- 对重要的报警消息,使用
WSTRING[255]或STRING[255]避免截断。 - 通信协议中的字符串字段必须严格按规范长度声明。
11.2 性能考虑
// 反模式: 在循环内反复拼接字符串 (性能差)
FOR i := 1 TO 100 DO
result := CONCAT(result, DINT_TO_STRING(i)); // 每次分配新字符串
END_FOR;
// 推荐: 预分配 + 直接写入数组
VAR
buf : ARRAY[0..255] OF CHAR; // 字符数组
idx : INT := 0;
END_VAR
FOR i := 1 TO 100 DO
// 直接写入 buf, 避免反复分配
idx := WriteIntToBuf(buf, idx, i);
END_FOR;
字符串性能准则:
- 避免在高速循环(扫描周期 < 1ms)内做字符串拼接或格式化。
- 大量字符串操作调度到慢速任务(如 100ms 周期)。
- 固定格式使用
REAL_TO_STRING等标准函数,不手写格式化。 - 使用
ARRAY OF CHAR替代字符串做底层缓冲区操作。
11.3 编码与国际化
VAR
// 中文用 WSTRING
wsChinese : WSTRING[50] := "温度传感器 #1 异常";
// 英文等 ASCII 用 STRING
sTag : STRING[30] := 'Temp_Sensor_01';
// 混用时的转换
wsConverted : WSTRING[50] := STRING_TO_WSTRING(sTag);
sConverted : STRING[50] := WSTRING_TO_STRING(wsChinese); // 中文可能丢失!
END_VAR
警告: WSTRING_TO_STRING 将 Unicode 转为单字节 ANSI 时,非 ASCII 字符(中文、日文等)会丢失。需要保留多语言文本时,全程使用 WSTRING。
12. 数组处理模式
12.1 查找
FUNCTION FindFirst : INT
VAR_INPUT
arr : ARRAY[*] OF INT;
target : INT;
END_VAR
VAR
i : DINT;
BEGIN
FOR i := LOWER_BOUND(arr, 1) TO UPPER_BOUND(arr, 1) DO
IF arr[i] = target THEN
FindFirst := i;
RETURN;
END_IF;
END_FOR;
FindFirst := -1; // 未找到
END_FUNCTION
12.2 排序 (冒泡)
FUNCTION SortArray : BOOL
VAR_INPUT
arr : ARRAY[*] OF INT;
END_VAR
VAR
i, j : DINT;
n : DINT;
temp : INT;
BEGIN
n := UPPER_BOUND(arr, 1) - LOWER_BOUND(arr, 1) + 1;
FOR i := 0 TO n - 2 DO
FOR j := 0 TO n - 2 - i DO
IF arr[LOWER_BOUND(arr, 1) + j] > arr[LOWER_BOUND(arr, 1) + j + 1] THEN
temp := arr[LOWER_BOUND(arr, 1) + j];
arr[LOWER_BOUND(arr, 1) + j] := arr[LOWER_BOUND(arr, 1) + j + 1];
arr[LOWER_BOUND(arr, 1) + j + 1] := temp;
END_IF;
END_FOR;
END_FOR;
SortArray := TRUE;
END_FUNCTION
12.3 过滤
FUNCTION FilterPositive : INT
VAR_INPUT
src : ARRAY[*] OF INT;
dst : ARRAY[*] OF INT;
END_VAR
VAR
i, j : DINT;
low, high : DINT;
BEGIN
low := LOWER_BOUND(src, 1);
high := UPPER_BOUND(src, 1);
j := 0;
FOR i := low TO high DO
IF src[i] > 0 THEN
dst[j] := src[i];
j := j + 1;
END_IF;
END_FOR;
FilterPositive := j; // 返回有效元素个数
END_FUNCTION
12.4 归约 (求和)
FUNCTION SumArray : REAL
VAR_INPUT
arr : ARRAY[*] OF REAL;
END_VAR
VAR
i : DINT;
s : REAL;
BEGIN
s := 0.0;
FOR i := LOWER_BOUND(arr, 1) TO UPPER_BOUND(arr, 1) DO
s := s + arr[i];
END_FOR;
SumArray := s;
END_FUNCTION
13. 内存布局考虑
13.1 数组内存布局
ARRAY[1..3, 1..4] OF INT (12 × 2 = 24 字节)
行优先存储:
地址偏移 元素
0 [1,1]
2 [1,2]
4 [1,3]
6 [1,4]
8 [2,1]
10 [2,2]
... ...
22 [3,4]
13.2 字符串内存布局
STRING[10]:
字节 0: 当前长度 (BYTE)
字节 1-10: 字符数据 (CHAR[10])
总计: 11 字节
WSTRING[10]:
字节 0-1: 当前长度 (INT)
字节 2-21: 宽字符数据 (WCHAR[10] = 2 字节 × 10)
总计: 22 字节
13.3 内存对齐与优化
VAR
// 良好: 相同类型连续声明
buf1 : ARRAY[0..127] OF INT;
buf2 : ARRAY[0..127] OF INT;
// 较差: 不同类型交错导致填充字节
arr1 : ARRAY[0..9] OF INT; // 20 字节
b1 : BOOL; // 1 字节 + 3 填充
arr2 : ARRAY[0..9] OF INT; // 20 字节
END_VAR
内存优化建议:
- 大数组放在
VAR块顶部,避免中间插入小类型变量导致对齐填充。 - 通信缓冲区使用
ARRAY[0..N] OF BYTE而非STRING[N],避免长度字节干扰。 - 全局 DB 中的数组尽量放在连续地址区域,方便过程映像映射。
- 使用
UNION或AT覆盖实现在同一内存区域复用不同类型。 - 字符串数组
ARRAY[0..N] OF STRING[M]占用(N+1) × (M+1)字节,注意总内存预算。
14. 总结
| 主题 | 要点 |
|---|---|
| ARRAY 声明 | 指定上下界,元素个数 = 上界 - 下界 + 1 |
| 初始化 | 字面量 [...],重复语法 [N(value)] |
| 索引 | 零基用于缓冲区,一基用于物理设备;运行时检查边界 |
| 多维数组 | 行优先,最右侧下标变化最快 |
| 切片 | DarraRT 扩展,长度必须匹配 |
| 整数组赋值 | 类型、大小完全一致才允许 |
ARRAY[*] | 仅限函数参数,用 LOWER_BOUND / UPPER_BOUND 取边界 |
| STRING | 默认 80 字符,最大 255;中文用 WSTRING |
| 字符串函数 | 位置从 1 开始,FIND 未找到返回 0 |
| 格式化转换 | REAL_TO_STRING / DINT_TO_STRING 等;非法输入返回 0 |
| 截断 | 自动截断不报错,目标长度必须足够 |
| 性能 | 高速循环内避免字符串操作;大数组注意内存布局 |