Найти в Дзене

Почему ST - самый недооценённый язык программирования ПЛК

Есть такой негласный закон на производстве: если программу ПЛК написал человек, которого уже не найти, - разобраться в ней будет больно. Особенно если она написана на Ladder. Особенно если там 400 ступеней, половина переменных называется %MW100-%MW199, а комментариев нет от слова совсем. Structured Text в этом смысле - другая история. Текст есть текст. Его можно читать сверху вниз. Переменные называются rReactorTemp или bPumpEnabled, а не %MW42. Логика видна без карты сокровищ. Но почему тогда большинство инженеров-автоматчиков всё ещё открывают Ladder первым делом? Привычка. Страх. Ощущение, что "ST - это для программистов, а не для нас". Давайте разберём, так ли это на самом деле. Когда в 1993 году вышел стандарт IEC 61131-3, он описал пять официальных языков программирования ПЛК. Ladder Diagram - для тех, кто привык думать релейными схемами. FBD - для поклонников блок-диаграмм. SFC - для описания последовательных процессов. IL - машинно-ориентированный язык, сейчас практически мёрт
Оглавление

Есть такой негласный закон на производстве: если программу ПЛК написал человек, которого уже не найти, - разобраться в ней будет больно. Особенно если она написана на Ladder. Особенно если там 400 ступеней, половина переменных называется %MW100-%MW199, а комментариев нет от слова совсем.

Structured Text в этом смысле - другая история. Текст есть текст. Его можно читать сверху вниз. Переменные называются rReactorTemp или bPumpEnabled, а не %MW42. Логика видна без карты сокровищ.

Но почему тогда большинство инженеров-автоматчиков всё ещё открывают Ladder первым делом? Привычка. Страх. Ощущение, что "ST - это для программистов, а не для нас". Давайте разберём, так ли это на самом деле.

Откуда вообще взялся ST

Когда в 1993 году вышел стандарт IEC 61131-3, он описал пять официальных языков программирования ПЛК. Ladder Diagram - для тех, кто привык думать релейными схемами. FBD - для поклонников блок-диаграмм. SFC - для описания последовательных процессов. IL - машинно-ориентированный язык, сейчас практически мёртвый. И ST - Structured Text, текстовый язык с синтаксисом, похожим на Pascal.

Все пять языков равноправны по стандарту. Все компилируются в один и тот же исполняемый код на контроллере. Никакого "нативного" преимущества у Ladder нет - это миф, который живёт дольше, чем заслуживает.

В CODESYS 3.5, на котором работает большинство современных российских контроллеров, ST поддерживается в полном объёме. Создать новый POU на ST - буквально два клика. Дальше - просто пишешь код.

Как выглядит ST: первый взгляд без страха

Главный барьер для тех, кто никогда не писал на ST - он выглядит как "настоящее программирование". Переменные, типы, операторы. Но давайте честно: ничего сложного здесь нет.

Объявление переменных выглядит так:

VAR
rTemperature : REAL; (* Температура теплоносителя, °C *)
nPumpRuntime : UDINT; (* Наработка насоса, минуты *)
bPumpEnabled : BOOL; (* Разрешение на пуск *)
tStartDelay : TON; (* Таймер задержки пуска *)
END_VAR

Никаких %MW и адресов памяти. Переменная называется rTemperature - и сразу понятно, что это температура, тип REAL (вещественное число), и в комментарии написана единица измерения. Любой инженер, открывший этот код через год, поймёт его за 10 секунд.

Условие пишется вот так:

IF rTemperature > 85.0 THEN
bPumpEnabled := FALSE;
bAlarmOverheat := TRUE;
ELSIF rTemperature > 75.0 THEN
bAlarmOverheat := FALSE;
bWarningHighTemp := TRUE;
ELSE
bAlarmOverheat := FALSE;
bWarningHighTemp := FALSE;
END_IF

Читается почти как обычный текст. "Если температура выше 85 - запрещаем пуск и выдаём аварию. Если выше 75 - только предупреждение. Иначе - всё в норме." На Ladder эта же логика - три параллельные ветки с контактами сравнения. Работает так же, но смотреть на это куда тяжелее.

Циклы - то, ради чего стоит освоить ST

Вот где становится по-настоящему интересно. На Ladder цикл по массиву - это боль. Нужны указатели, косвенная адресация, и молитвы, чтобы всё не развалилось. На ST - обычный FOR:

(* Ищем самый горячий датчик среди 16 термопар *)
rMaxTemp := arTemperature[0];
FOR i := 1 TO 15 DO
IF arTemperature[i] > rMaxTemp THEN
rMaxTemp := arTemperature[i];
nHotSensorIdx := i;
END_IF
END_FOR

Представьте, что у вас не 16 датчиков, а 64. Или нужно найти среднее по всем каналам, отфильтровав значения с ошибкой. На ST это несколько строк. На Ladder - проект внутри проекта.

Реальный пример из практики: система мониторинга температуры в печи с 32 термопарами. Логика аварии - если больше трёх датчиков одновременно превышают порог в одной зоне. На ST это один небольшой цикл с счётчиком. На Ladder такое вообще не принято делать - проще передать задачу в SCADA. Но зачем загружать сеть и SCADA тем, с чем ПЛК справится сам?

Конечные автоматы: сердце любого технологического процесса

Любой процесс управления - это состояния. "Ожидание", "Пуск", "Работа", "Останов", "Авария". Переходы между состояниями по условиям. Это классический конечный автомат, и ST реализует его через оператор CASE элегантнее, чем любой другой язык из пятёрки МЭК.

CASE eSystemState OF
STATE_IDLE:
IF bStartCommand THEN
eSystemState := STATE_PRIMING;
tPrimingTimer(IN := TRUE, PT := T#30S);
END_IF

STATE_PRIMING:
bPumpPriming := TRUE;
tPrimingTimer(IN := TRUE);
IF tPrimingTimer.Q THEN
tPrimingTimer(IN := FALSE);
eSystemState := STATE_RUNNING;
END_IF
IF NOT bPressureOK THEN
eSystemState := STATE_FAULT;
END_IF

STATE_RUNNING:
bPumpRun := TRUE;
IF bStopCommand OR bEmergencyStop THEN
eSystemState := STATE_STOPPING;
END_IF

STATE_FAULT:
bPumpRun := FALSE;
bFaultOutput := TRUE;
IF bFaultReset AND NOT bEmergencyStop THEN
bFaultOutput := FALSE;
eSystemState := STATE_IDLE;
END_IF
END_CASE

Каждое состояние - отдельный блок. Всё, что происходит в состоянии "PRIMING" (заполнение), находится строго внутри своей ветки. Никакой логики из других состояний туда не залезет. Добавить новое состояние "STANDBY" - просто дописать блок. Не перетаскивать контакты, не перерисовывать схему.

Это не просто удобно. Это делает программу тестируемой и предсказуемой. Каждый переход между состояниями явный. Каждое условие видно.

Функциональные блоки: пишешь один раз - используешь везде

Одна из главных возможностей ST - написать собственный функциональный блок (FB) с чистым интерфейсом входов и выходов. По сути - это как сделать собственный готовый элемент библиотеки.

Возьмём задвижку. На объекте их 30 штук. У каждой - концевики, команды открыть/закрыть, таймаут хода, признак аварии. Если писать логику для каждой отдельно - это 30 одинаковых кусков кода. Копипаст. Кошмар обслуживания.

Правильно - написать один FB_Valve:

FUNCTION_BLOCK FB_Valve
VAR_INPUT
bOpen : BOOL; (* Команда открыть *)
bClose : BOOL; (* Команда закрыть *)
bOpenedFB : BOOL; (* Концевик "открыто" *)
bClosedFB : BOOL; (* Концевик "закрыто" *)
tTimeout : TIME := T#30S; (* Таймаут хода *)
END_VAR
VAR_OUTPUT
bOpenCmd : BOOL; (* Выход на привод - открыть *)
bCloseCmd : BOOL; (* Выход на привод - закрыть *)
bFault : BOOL; (* Авария *)
eState : E_ValveState; (* Текущее состояние *)
END_VAR

Создаёшь 30 экземпляров этого блока. Логику отладил один раз. Нашёл ошибку в поведении при таймауте - исправил в одном месте, перекомпилировал - работают все 30. Это и есть промышленная архитектура.

Подводные камни: что реально ломает новичков

Переход на ST не бывает совсем безоблачным. Несколько вещей, которые стабильно удивляют тех, кто пришёл с Ladder.

Таймеры надо вызывать каждый цикл. Это не очевидно. В FBD блок таймера нарисован на схеме и выполняется всегда. В ST его надо вызывать явно. Если написать вызов таймера внутри ветки IF, он будет работать только когда условие истинно - и вести себя непредсказуемо.

(* Так НЕЛЬЗЯ - таймер застывает, когда bCondition = FALSE *)
IF bCondition THEN
tMyTimer(IN := TRUE, PT := T#5S);
END_IF

(* Правильно - таймер вызывается каждый скан *)
tMyTimer(IN := bCondition, PT := T#5S);

Деление на ноль не даёт исключения. REAL в ПЛК работает по IEEE 754. При делении на ноль переменная получает значение +Inf или NaN. Потом это число уходит в Modbus, в SCADA, на мнемосхему - и диспетчер видит какую-то бесконечность вместо нормального значения. Всегда проверяйте делитель перед операцией.

IF ABS(rDivisor) > 1.0E-6 THEN
rResult := rNumerator / rDivisor;
ELSE
rResult := 0.0;
bDivisionError := TRUE;
END_IF

Modbus и масштабирование. Данные из Modbus-регистров приходят как целые числа. Температура 23.5°C обычно передаётся как 235 или 2350, в зависимости от соглашения производителя датчика. Забыть поделить на 10 или на 100 - классика пусконаладки.

rTemperature := INT_TO_REAL(nModbusReg) / 10.0;

Переменные RETAIN - с осторожностью. Они выживают при перезапуске питания. Если переменная состояния вашего автомата помечена RETAIN, после аварийного отключения контроллер стартует не из STATE_IDLE, а из того состояния, в котором завис. Оборудование может включиться само. Это неприятно.

ST в реальном проекте: как это собирается

Средний промышленный объект на CODESYS 3.5 - это несколько программных единиц (POU), каждая со своей ответственностью. Главная программа PRG_Main на языке SFC вызывает остальные по очереди. PRG_IO на ST обрабатывает сырые сигналы - масштабирование, фильтрацию, проверку достоверности. PRG_Control реализует основную логику через конечные автоматы и вызовы FB. PRG_Comms занимается коммуникациями - Modbus RTU/TCP, OPC UA, MQTT-публикации. Библиотека типовых блоков - FB_Pump, FB_Valve, FB_HeatExchanger - написана на ST один раз и кочует между проектами.

Простые блокировки безопасности и сигнализацию при этом никто не запрещает оставить на Ladder - особенно если проект потом будет проверять электрик, которому Ladder привычнее.

Про коммуникации отдельно. Работа с Modbus RTU/TCP в ST - это комфортно. Особенно когда нужно опрашивать устройства условно: читать расширенные регистры только при определённом состоянии системы или только при активной аварии. В FBD такую условную логику опроса надо как-то обходить. В ST - обычный IF вокруг вызова блока Modbus-запроса.

Отладка и журналирование: бонус, о котором редко говорят

CODESYS даёт нормальный отладчик для ST: точки останова, пошаговое выполнение, просмотр переменных в реальном времени. Это принципиально другой уровень по сравнению с тем, как ищут баги в Ladder - смотреть на состояние контактов на экране и гадать, что происходит.

Для редких ошибок, которые воспроизводятся раз в несколько часов или только в определённых режимах, в ST удобно сделать внутренний журнал событий прямо в памяти контроллера:

IF bFaultCondition AND (nLogIndex < MAX_LOG_SIZE) THEN
arEventLog[nLogIndex].dtTimestamp := NOW();
arEventLog[nLogIndex].eState := eCurrentState;
arEventLog[nLogIndex].rTemp := rTemperature;
nLogIndex := nLogIndex + 1;
END_IF

Массив событий затем читается через OPC UA или выгружается по MQTT в SCADA. Картина того, что происходило перед аварией, - без внешних регистраторов, без осциллографа, прямо из памяти ПЛК. На современных контроллерах с Linux RT, вроде тех, что строятся на базе процессоров ARM Cortex-A - а именно такая архитектура используется в российских ПЛК СТАБУР - ресурсов для такого журнала более чем достаточно.

Вопрос производительности: закрываем тему

"ST медленнее Ladder" - это легенда из 90-х, когда контроллеры были 8-битными, а компиляторы примитивными. Сейчас компилятор CODESYS транслирует ST в нативный машинный код так же, как и Ladder. Разницы в быстродействии на уровне задачи нет. Время цикла определяется архитектурой задач, объёмом коммуникаций и железом - но не выбором языка внутри POU.

Если кто-то говорит "у нас жёсткие требования по времени цикла, поэтому только Ladder" - это не технический аргумент. Это привычка, оформленная под технический аргумент.

Коротко о главном

Что такое Structured Text? ST - один из пяти языков стандарта IEC 61131-3 для программирования ПЛК. Текстовый, типизированный, похожий на Pascal. Компилируется в тот же машинный код, что Ladder и FBD.

Зачем переходить на ST? Для сложных алгоритмов - вычислений, обработки массивов, конечных автоматов, библиотечных блоков - ST заметно удобнее. Код читается, поддерживается и отлаживается быстрее.

Трудно ли освоить ST с нуля? Базовый синтаксис - за 1-2 дня. Через неделю практики уже пишешь рабочие программы. Главный порог - психологический, не технический.

ST заменяет Ladder полностью? Нет. Для простых битовых блокировок и там, где программу должен читать электрик или технолог - Ladder на своём месте. Хороший проект использует оба языка там, где каждый из них сильнее.

Язык - это инструмент. Хороший инженер не привязан к одному молотку. ST давно перестал быть чем-то экзотическим в мире АСУ ТП - это рабочая часть профессионального набора. И чем раньше начать с ним работать, тем быстрее становится понятно: писать читаемый код для ПЛК - это не привилегия программистов, а обычная инженерная практика.

Автор: Дмитрий Стабур, инженер АСУ ТП