> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# LOWER_BOUND：数组从 0 还是 1 开始，别再靠猜
- URL: https://www.hubwiz.com/blog/lower-bound-tia-portal-array-limits/
- Published: 2026-10-30T15:50:51.000Z
- Updated: 2026-10-30T16:00:07.000Z
- Description: 数组下限是 0 还是 1，别靠猜。这条指令让通用块真通用，但它只在优化块里可用，DIM 填错连输出都不能信。
- Author: admin
- Tags: PLC, 博途, SCL, TIA Portal, 梯形图

一块求平均值的通用功能块交付两周后，客户换了个数组：原来接的是 `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` 里的 `[*]`：

```iecst
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 以上会溢出。

调用方接任何数组都行，编译期的块接口不用改：

```iecst
"FB_ArrayAverage_DB"(Data := "TankLevels");     // ARRAY[0..9]
"FB_ArrayAverage_DB_2"(Data := "ZoneTemps");    // ARRAY[1..500]

```

## 两个变体，比原例更常用

**二维数组取某一行。** `DIM` 填 2 拿到列的下限，行号由外部指定：

```iecst
#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`：

```iecst
#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[*]`

现场很少一上手就写变长参数。多数项目是三步走过去的，每步都能独立交付：

**第一步，写死。** 块只服务当前那一个温度组：

```iecst
VAR_INPUT
    Data : ARRAY[1..8] OF Real;
END_VAR
FOR #i := 1 TO 8 DO ...

```

能跑，但下一个项目要么复制块、要么改接口。

**第二步，把长度变成参数。** 这一步是性价比最高的一步：不改下标起点，只把“8”换成输入引脚。

```iecst
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 互转出来的块，里面经常出现这个盒。这时候需要四看：

1. **看声明**：数组参数是 `[*]` 还是写死的上下限——写死的就不用担心下限。
2. **看调用点**：接进来的数组从几开始，决定实际跑了什么。
3. **看 `DIM` 常量**：填 1 还是 2，二维数组在这里最容易读反。
4. **看输出接到哪**：它出的是下标，不是个数；接到计数器或比较指令上，基本就是谁把语义搞错了。

想把这一页当作指令字典来查，[Learn-LAD 的西门子梯形图指令手册](https://tia.hubwiz.com/learn-lad/?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=lower-bound-tia-portal-array-limits)里每条指令一页：符号与引脚、最小示例、真值时序表、常见坑；配套的[60 个可导入博途的示例](https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/)每例都带 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 个免费实战课，每课都能验收》](https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/)。
- **示例代码是教学用。** 上现场前要按自己的命名规范、块接口与错误处理重构，尤其是除零和空数组。
- **别把它当成“更专业”的写法。** 变长参数是维护成本，不是技术等级：一个只有本项目用的块引入 `[*]`，实际是把阅读门槛抬高了一档。

**来源**：PLCcode.ai，《LOWER\_BOUND in TIA Portal: Read an Array's Low Limit》，2026-10-28。原文链接：[https://plccode.ai/tia-portal-lower-bound-instruction](https://plccode.ai/tia-portal-lower-bound-instruction?ref=hubwiz.com)。引脚定义、`DIM` 从 1 起、`ENO` 与越界行为、"只有 S7-1500 + 优化块"、求平均值示例出自原文；梯形图侧对照（依据西门子 LAD 指令文档）、两个变体、六个坑与两张判断表为我们整理。封面为图库实拍（索引卡片柜），非原文配图。

本文由汇智网编译整理，转载请注明出处。

> **这是「AI × 工业 / PLC」专题的一部分。**完整地图见：[AI × 工业 / PLC 完全指南](https://www.hubwiz.com/blog/ai-industrial-stack/)