В ST результат NaN: деление на ноль, переполнение и что видит оператор
Почему в Structured Text появляется NaN, как ломаются сравнения и аварии, защита знаменателя и IS_NAN, отличие от ошибки массива и отображение на HMI.

На мнемосхеме расход в трубе вдруг превращается в прочерк, а иногда в огромное число вроде 1e38. В отладчике переменная Flow_m3h показывает #QNAN или просто не участвует в сравнениях - авария «низкий расход» не срабатывает, зато насос уходит в ручной режим по другой цепочке. Технолог спрашивает: «датчик сдох?» Мультиметр на 4-20 мА в норме. Вы открываете ST-блок расчёта массового баланса и видите деление на Density, которое в этот момент равно нулю после холодного пуска.
NaN (Not a Number) в Structured Text - не мистика компилятора, а результат недопустимой операции с REAL/LREAL: деление на ноль, корень из отрицательного, переполнение при умножении, иногда логарифм от нуля. ПЛК редко «падает» от одного NaN, зато сравнения x > limit с NaN ложны, PID с NaN на входе разгоняет выход, HMI рисует мусор. Статья разбирает источники, защиту в коде и что увидит смена. Это не про выход за границы массива (там другое поведение runtime) и не урок ST с нуля.
Короткий ответ
В IEC 61131-3 ST переменная типа REAL становится NaN после недопустимой арифметики: главный триггер на объекте - деление на ноль, реже SQRT от отрицательного значения или комбинация переполнения. NaN «заражает» цепочку: любое сравнение с NaN даёт FALSE, оператор на HMI видит прочерк, ноль или старое значение - зависит от драйвера экрана. Защита: проверять знаменатель до деления, использовать IS_NAN() / IS_VALID() где доступно, подставлять безопасное значение и выставлять quality Bad. Отличие от ошибки индекса массива: NaN не останавливает задачу ПЛК, а тихо ломает логику.
Откуда берётся NaN в расчётах ПЛК
На пуске котельной плотность пара берётся из таблицы по давлению. Пока давление ещё не в рабочем диапазоне, интерполяция выдаёт 0 или отрицательный сырой сигнал после кривой. Формула Flow := K * dP / Density при Density = 0.0 на REAL даёт NaN в CODESYS и большинстве runtime по IEEE 754. Аналогично Ratio := InputA / InputB при обрыве B по полевой логике не всегда обнуляет знаменатель - иногда там остаётся мусор из предыдущего цикла.
SQRT(Negative) и LN(Zero) - второй по частоте источник после деления. Инженер пишет SQRT(dP) для расхода через диафрагму, забывает, что при обратном потоке dP может уйти в минус на доли секунды при переключении задвижек.
Переполнение REAL при умножении больших счётчиков импульсов иногда даёт Inf, а последующие операции - NaN. На HMI Inf может отображаться как 1.#INF или уехать в предел шкалы. Оператор воспринимает это как «залипший датчик».
Поведение сравнений и аварий
В ST выражение IF Temp > 80.0 THEN Alarm := TRUE при Temp = NaN никогда не выполнится - сравнение с NaN ложно. Авария высокой температуры молчит, хотя на экране уже нечисло. Обратная ловушка: IF NOT (Temp > 80.0) тоже не срабатывает как ожидают новички - NaN не «меньше» порога.
Цепочки IF x <> x THEN в старом коде иногда используют как детектор NaN (NaN единственное значение, не равное самому себе). В современных средах лучше явные функции проверки из стандарта или библиотеки Util.
Для межблокировок опасно полагаться на то, что «любое ненулевое - правда». NaN в BOOL не превращается автоматически - сначала идёт неявное преобразование в сравнениях REAL, и логика уже сломана выше.
Что видит оператор на HMI
Панель оператора получает REAL по Modbus или внутренней шине. Драйвер тега при NaN/Inf часто показывает: последнее корректное значение, ноль, пустое поле или ****. Поведение разное у разных HMI - смена не обязана знать про IEEE 754, она видит «цифра пропала». Если тренд не фильтрует нечисла, линия обрывается или улетает вверх - технолог звонит ночью.
Правильная схема: в ПЛК до отправки на экран переменная Flow_display уже очищена, плюс бит Flow_BadQuality или код причины. Оператор видит «–» и жёлтую иконку качества, а не случайный мусор. Связь с сигналом 4-20 мА и Bad quality - там про полевой канал; здесь про математику внутри ПЛК, но на экране симптом похож.
Защита в коде: знаменатель, лимиты, IS_NAN
Минимальный паттерн перед делением:
IF ABS(Denominator) < 1E-6 THEN Result := 0.0; Result_Bad := TRUE; ELSE Result := Numerator / Denominator; Result_Bad := FALSE; END_IFПорог 1E-6 подбирают под физику: для плотности пара 0.01 может быть уже опасно, для расхода в т/ч - другой epsilon.
После цепочки расчётов полезен финальный барьер:
IF IS_NAN(Result) OR IS_INF(Result) THEN Result := LastGoodResult; Result_Bad := TRUE; END_IFLastGoodResult обновляют только когда NOT Result_Bad. Так NaN не уходит на насосный PID.
Для SQRT используйте MAX(Input, 0.0) только если отрицательное физически бессмысленно; иначе - Bad quality и не считать расход.
Подробный синтаксис и типовые ошибки компилятора ST - в справочнике IEC 61131-3; философия «почему ST на объекте» - в статье про выбор ST.
Отличие от выхода за границы массива
Обращение Arr[i] при i вне диапазона в CODESYS на многих target даёт останов задачи, watchdog, переход в Safe - это громко и заметно. NaN тихий: цикл ПЛК крутится, индикатор RUN горит, только логика «не туда». Поэтому на ревью кода массивы ищут статическим анализом, а деления - глазами в каждом FB расчёта.
Строковые операции и CONCAT к NaN не относятся. Путаница бывает, когда в одном FB и массив уставок, и деление - при ошибке индекса инженер думает «NaN», а в журнале - Task exception.
Переполнение, Inf и накопление ошибок
Счётчик Total_m3 накапливает Flow * dt. Если Flow на один цикл стал Inf из-за ошибочного масштаба AI, сумма тоже ломается. Периодическая проверка IS_VALID(Total_m3) и ограничение прироста за цикл (MAX_DELTA_PER_SCAN) спасают от нереалистичного скачка на тренде.
При конвертации INT в REAL перед делением следите за порядком: (INT_VAR / INT_VAR2) в целых даёт 0, потом присвоение в REAL - другая ошибка, не NaN, но на экране тоже «ноль расхода при живой линии».
Отладка на объекте и в симуляции
В online watch добавьте рядом с результатом: знаменатель, флаги Bad, предыдущее Good значение. При появлении NaN смотрите стек вычисления в том же цикле - какая переменная первой стала нечислом.
На стенде воспроизведите ноль в знаменателе принудительным force (осторожно на объекте с механизмами). Проверьте, что авария и HMI ведут себя по паспорту. Симулятор ПЛК без поля полезен для отработки граничных формул до FAT.
NaN и PID: почему насос «уезжает»
Регулятор с NaN на процессной переменной или обратной связи часто выдаёт на выходе тоже NaN или застревает в последнем числе до появления нечисла. Anti-windup не спасает, если сравнение e := SP - PV уже ложно из-за NaN в PV. Перед PID ставят блок валидности: при Bad или IS_NAN(PV) заморозить выход или перейти в ручной режим с фиксированным процентом.
На тренде оператор видит ровную линию уставки и прочерк PV - насос при этом может крутиться на 100 % по старому выходу. В проекте явно: IF PV_Bad THEN Out := SafeSpeed.
Журнал и постмортем после смены
Если авария «низкий расход» не сработала, а NaN был секунду - без архива тренда не докажете причину. Архивируйте сырой и расчётный тег с 1 с дискретностью. В ST добавьте счётчик NanEvents с инкрементом при IS_NAN(Result) - на утреннем разборе видно, было ли это разовое деление на пуске или хроническая ошибка масштаба AI.
LREAL, INT и неявные ловушки
Деление двух INT в ST до присвоения в REAL обрезает дробь - это не NaN, но на экране «ноль расхода». Явно приводите операнды к REAL до деления. LREAL откладывает переполнение, но не отменяет деление на ноль - проверка знаменателя всё равно нужна.
Массовый баланс и плотность на холодном пуске
Типовой сценарий NaN на пищевых и химических линиях: считают массовый расход через объёмный и плотность. Плотность с анализатора или таблицы при старте равна 0 до первого валидного анализа. Вся цепочка расчёта заражается NaN на три секунды - хватает, чтобы сорвать импульс дозирования. Решение: не считать массу, пока Density_Valid FALSE, и не открывать клапан дозы по «предварительному» расходу.
Отображение на встроенном экране и Modbus
Если REAL с NaN уходит в Modbus-регистр как сырой IEEE 754, HMI может показать случайное большое число вместо прочерка. Перед записью в регистр для SCADA подставляйте 0 или специальный код при Bad - и отдельный бит качества. Иначе диспетчерская и местный экран разойдутся, хотя в watch ПЛК уже виден QNAN.
Условие, результат и защита в коде
| Условие / операция | NaN / Inf | Защита в коде |
|---|---|---|
| `a / 0.0` при REAL | NaN (или Inf по реализации) | Проверка `ABS(b) > epsilon`; Bad flag |
| `SQRT(x)` при x < 0 | NaN | `IF x < 0 THEN Bad`; иначе SQRT |
| `LN(0)` или `LN(negative)` | NaN / -Inf | Проверка диапазона до вызова |
| Переполнение `a * b` | Inf, далее NaN в цепочке | Ограничение множителей; IS_VALID |
| Сравнение `x > limit` при NaN x | Всегда FALSE | Сначала `IF NOT IS_NAN(x)` |
| Накопитель `Sum := Sum + x` | Sum портится навсегда | Не накапливать при Bad/NaN |
| HMI тег REAL с NaN | Прочерк / старое / мусор | Отдельный display + quality в ПЛК |
| Деление после фильтра AI | Ноль на пуске при обрезанном сигнале | Задержка расчёта до Valid AI |
Вопросы с пуска
ПЛК остановится от NaN?
Обычно нет - задача продолжит цикл. Останов будет от watchdog, если NaN заблокировал критичный код по таймауту логики - редкий косвенный случай.
Помогает ли замена REAL на LREAL?
Точность выше, NaN от деления на ноль не исчезает. Меньше переполнение - да.
Оператор видит NaN как ноль - это нормально?
Нет для эксплуатации. Нужен явный Bad quality или прочерк с подписью.
Можно ли фильтром на HMI скрыть NaN?
Костыль. Источник исправляют в ПЛК, иначе аварии и отчёты останутся ложными.
Чем отличается от обрыва 4-20 мА?
Обрыв ловят по току и Bad quality входа; NaN может быть при живом токе и неверной формуле.
