ST программа падает на индексе массива: границы, циклы и данные с HMI
Почему ST падает на индексе массива: границы FOR, пустой массив, данные с HMI, онлайн-диагностика и отличие от NaN и ENUM.

Цикл FOR от 0 до 10, массив с индексами 0…9. На симуляторе программа крутится час. На объекте оператор на HMI выбирает «режим 12» из выпадающего списка, и ПЛК уходит в fault: array index out of bounds. Перезагрузка задачи, линия стоит. В логе CODESYS - исключение в строке с RecipeParams[ModeIndex], а ModeIndex пришёл с экрана без проверки. Другой частый случай: граница цикла FOR i := 1 TO ARRAY_LEN вместо TO ARRAY_LEN - 1 при нулевой индексации - падает на последней итерации, когда массив пустой после очистки рецепта.
Статья разбирает выход за границы массива в Structured Text: неверные границы FOR, пустой массив, значение с HMI вне диапазона, диагностика онлайн. Отдельно - отличие от NaN при делении и от сброса ENUM после загрузки: там программа не «падает» на индексе, хотя симптом на объекте похож. Не даём общий курс ST и не дублируем справочник синтаксиса - только сценарий индекса и практические проверки до пуска.
Короткий ответ
Падение ST на индексе массива - почти всегда индекс < 0 или индекс ≥ размер массива (или LENGTH/UPPER_BOUND). Проверьте границы FOR (0…N-1 при нулевой индексации), не используйте LENGTH как верхнюю границу без -1, валидируйте любой индекс с HMI до обращения к массиву. Пустой массив: LENGTH = 0, цикл FOR i := 0 TO 0 может выполниться один раз - защита IF LENGTH > 0 THEN. Онлайн: смотрите переменную индекса в момент fault, trace Force с экрана, сравните с ARRAY_UPPER_BOUND. От NaN и ENUM отличается: там fault на арифметике или логика идёт по неверной ветке без исключения индекса.
Как в IEC 61131-3 задаются границы массива
В CODESYS массив объявляют с диапазоном: arr : ARRAY[0..9] OF REAL - допустимые индексы 0…9, десять элементов. ARRAY[1..10] - индексы 1…10. Смешение стилов в одном проекте - источник ошибок: модуль A пишет с индексом 1, модуль B читает с 0.
Функции границ: ARRAY_LOWER_BOUND, ARRAY_UPPER_BOUND (или LBound/UBound в некоторых диалектах). Для динамического размера в ST 3.5 - ARRAY_LEN / размер при объявлении фиксирован.
Индексация в FOR должна совпадать с объявлением. FOR i := ARRAY_LOWER_BOUND(arr) TO ARRAY_UPPER_BOUND(arr) - безопасный шаблон при смене размера массива в одном месте объявления.
Обращение arr[i] при i вне диапазона в рантайме CODESYS на целевом ПЛК часто приводит к останову задачи или исключению. Симулятор иногда мягче - не полагайтесь на «в симуляторе не падало».
FOR с неверной границей: классические промахи
TO LENGTH вместо TO LENGTH - 1.
Если массив ARRAY[0..9], LENGTH = 10. Цикл FOR i := 0 TO 10 даёт i = 10 на последней итерации - выход за границу.
Цикл с 1 при нулевой индексации.
FOR i := 1 TO 9 для ARRAY[0..9] - пропуск arr[0] и логическая ошибка; FOR i := 1 TO 10 - падение на i = 10.
Отрицательный индекс после вычитания.
Index := Selected - 1 при Selected = 0 на HMI (оператор не выбрал строку) даёт -1.
Шаг STEP с нарушением границы.
Редко, но FOR i := 0 TO 8 STEP 2 для массива длины 5 - последний i может выйти за UPPER_BOUND при неверном TO.
Вложенные FOR с общим именем i.
Внутренний цикл сбрасывает i внешнего - после внутреннего цикла i непредсказуемо, следующее arr[i] падает.
Шаблон: верхняя граница только через ARRAY_UPPER_BOUND(arr), не «зашитое» число 9, если размер массива меняется при рефакторинге.
Пустой массив и динамическая длина
После операции «очистить список рецептов» или «удалить все строки» LENGTH может стать 0. Код FOR i := 0 TO ARRAY_UPPER_BOUND(arr) при фиксированном объявлении ARRAY[0..9] UPPER_BOUND всё равно 9, но логическая «длина данных» 0 - обход всех ячеек читает старый мусор. Если используется переменная ValidCount и цикл FOR i := 0 TO ValidCount без проверки, при ValidCount = 0 цикл 0 TO 0 выполняется один раз - нужен IF ValidCount > 0 THEN.
Для «пустого» сценария явная ветка:
IF ValidCount = 0 THEN (* не входить в цикл, дефолт *) ELSE FOR i := 0 TO ValidCount - 1 DO ... END_FOR END_IFНа HMI пустой список и индекс «-1» или «0» без выбора - валидировать до передачи в ПЛК.
Данные с HMI: индекс без проверки
Типовой путь: ListBox / ComboBox передаёт номер строки в ModeIndex, RecipeNo, AxisSelect. Оператор или баг экрана шлёт 255, -1, 999. Программа: Params[ModeIndex] := HMI_Value - fault.
Защита на ПЛК (обязательно, HMI не единственный барьер):
IF ModeIndex >= ARRAY_LOWER_BOUND(Params) AND ModeIndex <= ARRAY_UPPER_BOUND(Params) THEN ActiveParam := Params[ModeIndex]; ELSE ActiveParam := DefaultParam; Alarm_InvalidIndex := TRUE; END_IFНа HMI: ограничить список, не давать ручной ввод индекса без роли наладчика, синхронизировать количество строк списка с константой в общем include.
Связка с ролями доступа на панели оператора: ручной ввод индекса - только наладчик, с подтверждением.
При Modbus-карте экрана индекс может приходить как регистр со стороны SCADA - та же проверка на ПЛК. См. Modbus-карта экрана и Bad quality - если quality плохое, индекс может остаться последним good и быть неверным логически.
Диагностика онлайн на объекте
В момент fault открыть Online приложение, найти строку в call stack / exception. Смотреть переменные: индекс, LENGTH, UPPER_BOUND, источник с HMI.
Force индекса с экрана в онлайн без изменения HMI - воспроизвести падение на безопасном стенде.
Включить логирование индекса перед обращением к массиву в debug-сборке (условная компиляция) - на объекте один цикл с записью в ring buffer.
Сравнить поведение симулятора и ПЛК: в симуляторе индекс мог не приходить с HMI, список на экране был короче.
После загрузки без retain проверить, не остался ли индекс из прошлой сессии в RAM при частичной инициализации - редко, но ModeIndex без сброса в INIT при старте.
Отличие от NaN и переполнения
ST: результат NaN, деление и переполнение - fault на делении 0, INF, REAL overflow. Симптом: «программа сошла с ума», значения на экране неверные, но call stack указывает на арифметику, не на array[i].
Проверка: exception message / номер строки. NaN часто продолжает выполнение с poisoned REAL; индекс массива - жёсткий stop.
Отличие от ENUM после загрузки
ENUM и CASE после загрузки: симулятор и объект - переменная ENUM вне допустимых значений после download, CASE не покрывает ветку. Логика идёт в неверную ветку, индекс может вычисляться косвенно (ORD(enum)) и выйти за границу массива, но первичная причина - ENUM, не FOR.
Диагностика: смотреть значение ENUM в онлайн, не только индекс. Инициализация ENUM в каждом POU после старта.
Индекс как результат другого массива или поиска
Index := FindSlot(Array) возвращает -1, если не найдено. Следующая строка Data[Index] - падение. Паттерн: IF Index >= 0 THEN.
Поиск максимума в пустом наборе - MaxIndex не инициализирован, 32767 на INT - выход за границу.
Косвенная индексация: Matrix[Row][Col] - обе координаты валидировать.
Условие, выход за границу и как поймать до пуска
| Условие | Как выходит за границу | Как поймать до пуска |
|---|---|---|
| FOR i := 0 TO LENGTH | i = LENGTH на последней итерации | Статический review; шаблон TO UPPER_BOUND |
| FOR i := 1 TO N при ARRAY[0..N-1] | i = N | Сверить LOWER/UPPER с циклом |
| Индекс с HMI без clamp | Оператор / SCADA шлёт вне списка | Тест всех граничных значений +1/-1 |
| Selected - 1 при Selected = 0 | Индекс -1 | Unit-тест ModeIndex 0 и min списка |
| ValidCount = 0, FOR 0 TO ValidCount | Одна итерация с i=0 | Тест очистки списка на HMI |
| FindSlot = -1 без проверки | Data[-1] | Тест «не найден» в таблице |
| Вложенные FOR, общий i | i после внутреннего цикла | Разные имена переменных цикла |
| ARRAY[ORD(Enum)] без валидного ENUM | ORD вне таблицы CASE | Тест после download все ENUM |
| Константа размера устарела | Расширили массив, цикл старый | Один источник размера - константа |
| Симулятор без HMI | Индекс всегда 0 | FAT с реальным экраном и списками |
| Retain индекса после смены программы | Старый индекс, новый размер массива | INIT сброс или retain-версия структуры |
Статический анализ и код-ревью
Перед пуском: grep по проекту обращений к массивам с переменным индексом. Каждое место - есть ли IF с границами.
Код-ревью чек: нет ли TO LENGTH; все индексы с HMI/Modbus проверены; Find/Search с -1 обработан.
В CODESYS Analyzer / подобных инструментах - предупреждения о потенциальном выходе за границы, если доступны для версии.
Константа MAX_MODES и массив ARRAY[0..MAX_MODES-1] - HMI список не длиннее MAX_MODES.
Паттерны безопасного доступа
Clamp: Index := LIMIT(LOW, Index, HIGH) перед использованием - не лечит логику, но предотвращает fault; лучше alarm при clamp.
Указатель на элемент с проверкой: только после IF в границах.
Отдельная функция GetParam(Index) с return status BOOL - вызывающий код не обращается к массиву при FALSE.
Не дублировать границы «магическими числами» в пяти POU - один include GVL_Constants.
Связь с рецептами и версиями
Оператор загружает рецепт с 15 строк, программа ожидает 10 - индекс строки с HMI 14. Версия рецепта в заголовке и проверка RowIndex < Recipe.RowCount до доступа. Соседняя тема - оператор запустил не тот рецепт, там акцент на версии, здесь - на индекс строки внутри.
Типовые ошибки
Полагаться на HMI «там не выберут больше».
SCADA, скрипт, баг экрана - индекс приходит на ПЛК. Проверка только на ПЛК.
Тест только на симуляторе.
HMI не подключён, индекс 0 - все тесты зелёные.
LENGTH и UPPER_BOUND перепутаны.
Для ARRAY[0..9] UPPER=9, LENGTH=10 - цикл TO 10 падает.
Игнорировать warning компилятора.
Иногда компилятор видит статический выход - редко для динамического индекса.
Вопросы с объекта
Почему падает только на объекте, в офисе нет?
Реальный HMI, оператор, retain, другая версия списка на экране.
Можно ли отключить проверку границ в рантайме?
Не рекомендуется. Лучше валидация и alarm.
Падение в библиотеке - мой код не трогал массив.
Индекс передали в библиотечную функцию - смотрите вызывающий код.
INT16 индекс с HMI как UINT
65535 в знаковом INT - отрицательное или огромное после преобразования - валидировать тип.
На практике
В проектах на CODESYS 3.5 для ПЛК с Linux RT полезно держать общий модуль ArrayGuards с функциями проверки индекса и единым кодом аварии ALM_ARRAY_INDEX. Среда CODESYS и MasterSCADA 4D на одном железе взаимозаменяемы - логика валидации индекса в ПЛК не зависит от того, с какого HMI пришёл номер строки.
