LOWER_BOUND:数组从 0 还是 1 开始,别再靠猜
数组下限是 0 还是 1,别靠猜。这条指令让通用块真通用,但它只在优化块里可用,DIM 填错连输出都不能信。
一块求平均值的通用功能块交付两周后,客户换了个数组:原来接的是 ARRAY[0..9],新工程是 ARRAY[1..500]。
编译 0 错误、0 警告。平均值偏大——循环从 1 开始写死了,漏掉第一个元素,还多读了一次不存在的下标。
这种错误编译器抓不到,因为它不是语法问题,是你对数组下限的假设。而数组下限在真实工程里恰好有 0 和 1 两种习惯,同一个厂的两台设备都可能不一样。
LOWER_BOUND 就是为这件事存在的:块里不再假设,问数组自己要答案。
普通参数为什么解决不了
把参数声明成 Data : Array[0..9] Of Real,块就只服务这一种数组。换数组要改块接口,改接口要动所有调用点,动完还得重新编译测试——一个求平均的块,因为长度不同被复制成五个版本,这在老项目里非常常见。
ARRAY[*] 换了个思路:声明时不写上下限,调用方接 ARRAY[0..9] 或 ARRAY[1..500] 都行。代价是块内部不知道长度,也不知道下限——所以必须有一条指令去问。
这也是为什么它有两个伙伴指令:LOWER_BOUND 取下限,UPPER_BOUND 取上限,两个一起给出 FOR 循环的范围。
下限为什么会不一致
它不是一个“标准”,而是声明的一部分。IEC 61131-3 只规定数组要声明上下限,不规定从几开始;于是习惯就分了两派:一派从 0 开始(和 C 系语言、很多老 PLC 的间接寻址写法对得上),一派从 1 开始(和电工图纸上的“第 1 路、第 2 路”对得上)。
同一份项目里的两个块,完全可能一个 0 起一个 1 起,因为它们是不同年代、不同人、甚至从不同品牌移植过来的。这和“谁写错了”没关系——只要块里出现写死的下标,它就只对当时那个数组成立。
还有一种更隐形的:块本来只服务一个数组,后来现场加了两台设备,工程师把 ARRAY[0..9] 改成 ARRAY[0..19]。下限没变,代码照样对;但下一次是有人把下限改成 1,代码就静默错了。所以真正需要的不是“知道答案”,是“不去假设”。
引脚与返回值
| 引脚 | 类型 | 含义 |
|---|---|---|
| EN | BOOL | 使能输入 |
| ENO | BOOL | 盒没有执行,或 DIM 无效时为 FALSE |
| ARR | ARRAY[*] | 要检查的数组参数 |
| DIM | UDINT | 第几维,从 1 开始数 |
| OUT | DINT(返回值) | 那一维的下限下标 |
它没有 busy、没有超时、没有异步状态:同一次调用就出结果。
返回值是声明里的下限,不保证是 0 或 1——构造这个数组的人写多少就是多少。源文提到负数下限的情况也存在:声明成 ARRAY[-5..5] 的数组,LOWER_BOUND 就返回 -5,这时候用 DINT 当下标循环是完全正确的。
三条硬规则,缺一个就白搭
第一,只有 S7-1500,而且是优化块。 按源文与西门子指令文档的口径,这一族指令标的是 S7-1500;ARRAY[*] 这种声明也要求优化块访问。块属性里“优化的块访问”那一项如果没勾,就是未优化块。顺手把这段代码复制到 S7-1200 的块里,你不会得到“不太好用”,你会得到编译错误。
第二,DIM 从 1 数。 一维数组是 1。二维数组取第二维(列)是 2,不是 0、也不是 1。
第三,DIM 越界时 ENO 是 FALSE,并且 OUT 不能用。 这里最容易踩:OUT 不会变成 0,它会保留上一次扫描留下的值。你如果只判断"有没有报错"而没看 ENO,就会拿一个陈旧的下标继续跑循环——结果可能不崩,只是数据错。
求平均值的完整写法
功能块的接口这样声明,注意 InOut 里的 [*]:
FUNCTION_BLOCK "FB_ArrayAverage"
VAR_INPUT
Data : ARRAY[*] OF Real; // 变长数组,长度由调用方决定
END_VAR
VAR_OUTPUT
Average : Real;
Count : DInt;
END_VAR
VAR_TEMP
i, Lo, Hi : DInt;
Sum : Real;
END_VAR
BEGIN
#Lo := LOWER_BOUND(ARR := #Data, DIM := 1);
#Hi := UPPER_BOUND(ARR := #Data, DIM := 1);
#Sum := 0.0;
FOR #i := #Lo TO #Hi DO
#Sum := #Sum + #Data[#i];
END_FOR;
#Count := #Hi - #Lo + 1; // 元素个数,注意 +1
#Average := #Sum / DINT_TO_REAL(#Count);
END_FUNCTION_BLOCK
三个地方值得停一下:
FOR的起点是#Lo,不是 1。 整个写法的价值就在这一行。- 分母是
Hi - Lo + 1。 0..9 是 10 个元素,不是 9 个。+1漏掉是这类代码最高频的错误,而且只在特定数据下才看出来。 - 下标变量用
DInt。LOWER_BOUND的返回值是DInt,用Int接在 500 以上会溢出。
调用方接任何数组都行,编译期的块接口不用改:
"FB_ArrayAverage_DB"(Data := "TankLevels"); // ARRAY[0..9]
"FB_ArrayAverage_DB_2"(Data := "ZoneTemps"); // ARRAY[1..500]
两个变体,比原例更常用
二维数组取某一行。 DIM 填 2 拿到列的下限,行号由外部指定:
#cLo := LOWER_BOUND(ARR := #Matrix, DIM := 2);
#cHi := UPPER_BOUND(ARR := #Matrix, DIM := 2);
FOR #c := #cLo TO #cHi DO
#Sum := #Sum + #Matrix[#Row, #c];
END_FOR;
只对有效元素求平均。 现场数据经常带"空槽位"(0 或 -9999 表示没接传感器),这时候元素个数和有效个数是两回事,分母要单独计数,不能再用 Hi - Lo + 1:
#n := 0;
FOR #i := #Lo TO #Hi DO
IF #Data[#i] <> 0.0 THEN
#Sum := #Sum + #Data[#i];
#n := #n + 1;
END_IF;
END_FOR;
IF #n > 0 THEN
#Average := #Sum / DINT_TO_REAL(#n);
END_IF;
后一个版本还顺手躲掉了除零。
一个真实的接口演进:三步走到 ARRAY[*]
现场很少一上手就写变长参数。多数项目是三步走过去的,每步都能独立交付:
第一步,写死。 块只服务当前那一个温度组:
VAR_INPUT
Data : ARRAY[1..8] OF Real;
END_VAR
FOR #i := 1 TO 8 DO ...
能跑,但下一个项目要么复制块、要么改接口。
第二步,把长度变成参数。 这一步是性价比最高的一步:不改下标起点,只把“8”换成输入引脚。
VAR_INPUT
Data : ARRAY[1..8] OF Real; // 上限仍然是编译期常量
Count : Int; // 1..8
END_VAR
FOR #i := 1 TO #Count DO ...
好处是调用方可以只处理前 5 个通道;代价是下限依旧写死,块只对 1.. 开头的数组成立。现场常见的“通用块”就停在这里,然后被下一个 0 起数组打回原点。
第三步,交给指令。 数组参数改成 [*],循环起点改成 LOWER_BOUND 的返回值,长度用 Hi - Lo + 1。这一版才真正与数组声明解耦,也才值得放进库。
判断停留在哪一步,有一条硬指标:这个块会不会被别的项目调用。 只在一个项目里用的块,停在第一步最省事;要进公共库的,直接做到第三步。
六个真实的坑
| 坑 | 现象 | 怎么躲 |
|---|---|---|
FOR 从 1 或 0 写死 |
编译干净,结果偏一点,换数组才暴露 | 起点一律用 LOWER_BOUND 的返回值 |
DIM 填 0 |
ENO 变 FALSE,OUT 是陈旧值 |
明确"从 1 数",二维列用 2 |
把 OUT 当成元素个数 |
当成个数用,循环少跑一个 | 个数是 Hi - Lo + 1;较新的指令集里另有直接给元素个数的指令,看你装的版本有没有 |
| 在非优化块或 S7-1200 上写 | 编译直接报错 | 指令族标的是 S7-1500 + 优化块 |
二维数组 DIM 记反 |
行列颠倒,越界或全读同一行 | 声明时把维度含义写进注释 |
分母漏 +1 |
平均值偏大,边界数据才看得出来 | 分母单独占一个变量,别内联算 |
怎么验:拿六种数组喂进去
这类块最容易“看着对”,所以验收不靠读代码,靠喂数据。六种输入就能把上面大部分坑扫出来:
| 输入 | 能暴露什么 |
|---|---|
ARRAY[0..9] OF Real |
FOR 从 1 写死时少算第一个元素 |
ARRAY[1..500] OF Real |
Int 当下标溢出、分母漏 +1 |
ARRAY[0..0] OF Real(单元素) |
分母为 1、循环只跑一次 |
二维数组,DIM 故意填错 |
ENO 变 FALSE 后 OUT 是陈旧值 |
| 含空槽(0 或 -9999)的数组 | 有效计数与元素个数是否分开 |
| 全空槽 | 除零保护是否生效 |
前两种是必须的,后四种看块的实际用途。这一步在仿真里跑最快:程序跑仿真控制器,每次改完重跑一轮,不用等现场。
梯形图那边:这个盒比你想的常见
LOWER_BOUND 不是 SCL 专属。在博途的梯形图指令里,它在"基础指令 → 移动操作"下面,一个盒,左边两个输入(数组、维度),右边一个输出,ENO 的语义和 SCL 侧完全一样。
有一处差别值得记住:ARRAY[*] 这种变长声明只能在 SCL 块的接口里写。所以自己写 LAD 时,这个盒的数组引脚接的是具体数组,下限是已知的,它多少有点"退化成一个常数"的意思——这时候直接写 0 或写 1 更清楚。
它真正有用的场景是读别人的程序。迁移产物、厂家提供的库、SCL 与 LAD 互转出来的块,里面经常出现这个盒。这时候需要四看:
- 看声明:数组参数是
[*]还是写死的上下限——写死的就不用担心下限。 - 看调用点:接进来的数组从几开始,决定实际跑了什么。
- 看
DIM常量:填 1 还是 2,二维数组在这里最容易读反。 - 看输出接到哪:它出的是下标,不是个数;接到计数器或比较指令上,基本就是谁把语义搞错了。
想把这一页当作指令字典来查,Learn-LAD 的西门子梯形图指令手册里每条指令一页:符号与引脚、最小示例、真值时序表、常见坑;配套的60 个可导入博途的示例每例都带 SCL 版本,可以用来对照"同一段循环逻辑在梯形图里长什么样"。
什么时候反而不该用它
它不是"更专业"的写法,是有成本的:一个 ARRAY[*] 参数意味着这个块再也不能用普通方式声明接口,维护的人要先理解变长参数才能改它。
| 情况 | 更合适的做法 |
|---|---|
| 块只服务一个固定数组 | 接口里写死 ARRAY[0..9],FOR 也写死 |
| 长度固定、只是代码里不想出现数字 | 用常量(c_Count)比用指令更直观 |
| 数组来自通讯缓冲区 | 长度通常由协议固定,直接按结构写 |
| 同一个块被十几个项目复用 | 用 ARRAY[*],好处才开始抵得上成本 |
| 需要元素个数做校验 | 用 Hi - Lo + 1,或较新指令集里直接给个数的指令 |
一条经验判断:如果这个块一年内只会被一个项目用,别引入变长参数。
三种写法放一起对比,选起来更直观:
| 写法 | 对数组的要求 | 维护成本 | 适合 |
|---|---|---|---|
| 下标写死 | 只能一个数组 | 最低 | 一次性逻辑、专用块 |
| 长度传参 | 下限固定 | 中 | 同一下限、长度会变 |
ARRAY[*] + 两条指令 |
不限制 | 最高 | 入库复用、厂家库、接手别人代码 |
五个常被问到的问题
能用在 FC 里吗? 能。它是 SCL 块接口的一个特性,FB 和 FC 都行;梯形图块不支持 ARRAY[*] 这种声明。
拿它当长度用行不行? 不行。它给的是边界,不是个数;二维数组里这个区别尤其明显——两维各取一次上下限,你才会知道这块数组究竟有多大。
输出为什么是 DInt 而不是无符号? 因为下限可能是 0,也可能被声明成负数(比如 -5..5)。用无符号接,等于把这种数组排除在外。
能不能用 LOWER_BOUND 拿上限? 不能,那是 UPPER_BOUND 的活。两个一起用才是完整的循环范围。
不想引入指令,有别的办法吗? 有,就是前面第二步:把长度做输入参数。但那只是把“长度”变量化,下限仍然写死在块里,打回原点的原因还是它。
边界,这五条别忽略
- 源文是一条指令页,约 500 词。 它不是评测、也不是实测报告;梯形图侧对照、六个坑、判断表与两个变体是我们补的。
- S7-1500 / 优化块是文档口径。 具体固件与版本的细节差异,以你本机编译器的行为为准——同一个块在别人机器上编译过,不代表在你这里也过。
- 负数下限这类声明先自己试。 逻辑上成立,但项目里的数组声明习惯各不相同,别把博客当规范。
- 编译通过 ≠ 边界正确。 这类错误只有用例和仿真能抓:给 0..9 和 1..500 各跑一遍,是验证这类通用块的最低要求。四层验证该怎么做,见《AI PLC 编程:7 个免费实战课,每课都能验收》。
- 示例代码是教学用。 上现场前要按自己的命名规范、块接口与错误处理重构,尤其是除零和空数组。
- 别把它当成“更专业”的写法。 变长参数是维护成本,不是技术等级:一个只有本项目用的块引入
[*],实际是把阅读门槛抬高了一档。
来源:PLCcode.ai,《LOWER_BOUND in TIA Portal: Read an Array's Low Limit》,2026-10-28。原文链接:https://plccode.ai/tia-portal-lower-bound-instruction。引脚定义、DIM 从 1 起、ENO 与越界行为、"只有 S7-1500 + 优化块"、求平均值示例出自原文;梯形图侧对照(依据西门子 LAD 指令文档)、两个变体、六个坑与两张判断表为我们整理。封面为图库实拍(索引卡片柜),非原文配图。
本文由汇智网编译整理,转载请注明出处。
这是「AI × 工业 / PLC」专题的一部分。完整地图见:AI × 工业 / PLC 完全指南