Машина состояний — один из моих любимых вопросов, когда я собеседую кандидата. Из интересных наблюдений, что даже если кандидат не знает определения, то скорее всего он уже пару разе ее реализовывал.
В 2021 году я уже выпускал статью про машину состояний Конечный автомат в АСУТП для ПЛК. Избавляемся от множества IF-ов и CASE-ов в Codesys 3.5, но это было давно, и там было очень много абстракций, большой сегмент инициализации данных, да и сейчас я назову этот подход далеким от реальности. Сейчас я хочу разобраться в машине состояние получше, понять как можно построить событийно-ориентированные системы и насколько они могут быть большими.
Здесь будет ST, Codesys и никакого ООП.
Глоссарий
Первым делом стоит актуализировать понятийную базу
Машина состояний(Конечный автомат) — это математическая модель последовательности событий и действий, описывающих определенный (логический) процесс.
Сама машина состояний состоит из трех частей.
Состояния — это разделенный на различные элементы, наборы условий, в которых может существовать система.
Для нас состояния — это различные режимы работы или этапы технологического процесса. Состояния всегда детерминированы.
События — это триггеры, вызывающие переходы от состояния к состоянию. События — единственный способ переходить между состояниями, они гарантируют, что все изменения являются предусмотренными и отслеживаемыми.
Здесь можно добавить, что события бывают внешними, которые приходят к нам и внутренние, которые порождаются самой системой. Внешние сигналы-это все что может прийти с панели оператора, какие-то сработки датчиков или выходы других ФБ, внутренние события — это внутренние таймеры, какие-то внутренние условия или проверки, все то, что нам необходимо для функционирования внутри системы.
Переходы — правила, определяющие, как система реагирует на события. В процессе этой реакции модуль получает возможность выполнить обработку полученного события.
Это набор действий, которые необходимо совершить при событии, чтобы перейти из одной состояния в другое.
Можно поискать и другие определения, но я считаю этот набор базовым минимум.
Также я не буду сильно уходить в математику и абстрактные математические вычисления, так что про Автомат Мили, если будет желание, можно прочитать на википедии.
Также я опущу про концепции исторических состояний или составную иерархию машины состояний, сосредоточившись на простых примерах.
Варианты использования
Для основы я покажу базовое использование машины состояний, на основе которых можно будет дальше развивать эту тему.
Изменение поведения
Машину состояний можно применять для изменения поведения функционального блока в зависимости от его состояния. Один из таких примеров — это одна кнопка на «Старт/Стоп»
Сама машина будет выглядеть следующим образом:

Система может находится или в поведении Start или в поведении Stop. При нажатии кнопки система получить событие EVENT и в зависимости от текущего поведения, будет выполнен соответствующий переход и произойдет смена поведения.
FUNCTION_BLOCK fbSimpleState
VAR_INPUT
xButton:BOOL;
END_VAR
VAR_OUTPUT
iOUT: INT;
END_VAR
VAR
State: enSimpleState;
EventDetection: R_TRIG;
END_VAR
EventDetection(CLK:=xButton); //Детекция события
CASE State OF
START: // Состояние
IF EventDetection.Q THEN //Событие
(*Переход*)
iOUT:=1;
State:= STOP;
END_IF
STOP:
IF EventDetection.Q THEN
iOUT:=2;
State:= START;
END_IF
END_CASE
Таких поведений может быть много, а условием смены поведения вполне может быть любое значение, однако не стоит сразу создавать универсальный блок с несколькими десятками поведений, чтобы красиво конфигурировать все на HMI.
Стандартное управление
Чаще всего машину состояний используют для пошагового выполнения каких-либо операций или технологических процессов, а также для управления исполнительными механизмами.
Управление клапаном можно уместить в такую машину.
Рассматриваемый клапан является упрощенным и тут не будут рассматриваться все граничные условия его работы. Важен сам принцип построения машины состояний и переходов.

При старте ПЛК наш ФБ будет в состояние IDLE, которое можно интерпретировать как ожидание, но оно помимо ожидания делает проверку состояния клапана. Если приходит один из концевиков, то система переходит в следующее состояние, если же система не получает сигнал от какого-нибудь концевика, то в состоянии IDLE система отрабатывает как команды открытия, так и команды закрытия.
Если пришла команда открытия OPEN COMMAND, то система перейдет в состояние OPENING — открывается, где будет ожидать или сигнала от концевика, или срабатывание аварийного таймера.
Если концевик пришел раньше таймера, то система переходит в состояние OPENED. Единственным способом перейти в другое состояние — подать CLOSE COMMAND.
Переход в состояние CLOSING -> CLOSED идентичен OPENING -> OPENED.
Если во время любого перехода концевик не придет за указанное время, то система переходит в состояние ALARM, переход из которой в состояние IDLE идет только через команду оператора RESET.
FUNCTION_BLOCK fbSimpleStateMachine
VAR_INPUT
OperationCommand: enSimpleStateMachineEvents;
Valve: enValveStatus;
END_VAR
VAR_OUTPUT
xAlarm: BOOL;
END_VAR
VAR
tWait: TON:=(PT:=T#5S);
State : enSimpleStateMachineState;
END_VAR
tWait();
xAlarm:=FALSE;
CASE State OF
IDLE:
IF Valve = VALVE_OPENED THEN
State:=OPENED;
RETURN;
END_IF
IF Valve = VALVE_CLOSED THEN
State:=CLOSED;
RETURN;
END_IF
IF OperationCommand = OPEN_COMMAND THEN
tWait.IN:=TRUE;
State:=OPENING;
RETURN;
END_IF
IF OperationCommand = CLOSE_COMMAND THEN
tWait.IN:=TRUE;
State:=CLOSING;
RETURN;
END_IF
OPENING:
IF VALVE=VALVE_OPENED THEN
tWait.IN:=FALSE;
State:=OPENED;
RETURN;
END_IF
IF tWait.Q THEN
State:=ALARM;
RETURN;
END_IF
OPENED:
IF OperationCommand=CLOSE_COMMAND THEN
tWait.IN:=TRUE;
State:=CLOSING;
END_IF
CLOSING:
IF VALVE=VALVE_CLOSED THEN
tWait.IN:=FALSE;
State:=CLOSED;
RETURN;
END_IF
IF tWait.Q THEN
State:=ALARM;
RETURN;
END_IF
CLOSED:
IF OperationCommand=OPEN_COMMAND THEN
State:=OPENING;
END_IF
ALARM:
xAlarm:=TRUE;
IF OperationCommand = RESET_COMMAND THEN
tWait.IN:=FALSE;
State:=IDLE;
END_IF
END_CASE
Управление с обратной связью
Управление с обратной связью актуально, когда какой-то исполнительный механизм, участвующий в процессе, управляется по цифровым протоколам и нам надо получить от него обратную связь о выполнении команды или об успешном получении.
Наш технологический процесс начинает работать с команды оператора, он делает одну операцию, потом проверяет продолжать дальше или нет, если продолжать, то происходит какая-то смена и процесс начинается заного.
Команда старт процесса и смена — это команды, которые происходят путем обмена данными с устройством.
Пример такого обмена

FMS — это рассматриваемая машина состояний, Driver — это еще один ФБ, который принадлежит к другому логическому слою ППО и само устройство.
FMS отправляет событие Send Command на драйвер устройства. И в этот момент внутренним событием переходит в состояние ожидание ответа от драйвера. Драйвер отправляет команду на устройство, получает ответ и, обработав его, возвращает результат в виде какого-то события FMS.
Если событие APPLY, то мы можем применить все действия для перехода, если ERROR, то срабатывает ветка обработки ошибки.

Начинается все также с IDLE STATE. Событием перехода является Timer, но запускается он с команды оператора.
Cостояния START PROCESS и CHANGE, которые инициируют отправку команды на устройство совершают переход в состояние APPLY.
Состояние APPLY, в случае события ERROR, совершает переход в прошлое состояние, чтобы повторить отправку команды, если спустя N попыток события Apply не было, то система считает, что у нее необъявленное поведение(UB) и переходит в аварийное состояние EMERGENCY, где будет ожидать сброса от оператора.
Если вам кажется логически неправильным делать возврат по событию ERROR в прошлое состояние ради отправки команды, то вы правы. На самом деле ответственность за обработку ошибок и ретрансляция команд должна лежать на драйвере.
Мной сознательно было принято решение организовать обработку ошибки таким образом
FUNCTION_BLOCK fbFMSFeedback
VAR_IN_OUT
xApply: BOOL;
xError: BOOL;
xStart: BOOL;
xReset: BOOL;
END_VAR
VAR_OUTPUT
END_VAR
VAR
NextState: enFMSFeedbackState;
LastState: enFMSFeedbackState;
State : enFMSFeedbackState;
TryCount: UINT;
tWait: TON;
xContinue: BOOL:=TRUE;
END_VAR
VAR CONSTANT
uiTryCountLimit:UINT:=3;
END_VAR
tWait(IN:=xStart, PT:=T#10S);
CASE State OF
IDLE_STATE:
IF tWait.Q THEN
State:=START_PROCESS;
xStart:=FALSE;
END_IF
START_PROCESS:
(*Событие на старт на старт*)
LastState:=State;
State:=APPLY;
NextState:=END_PROCESS;
END_PROCESS:
(*Обработка сигнала о завершении процесса*)
State:=CHECK;
CHECK:
(*Проверка на продолжение*)
IF xContinue THEN
State:=CHANGE;
ELSE
State:=IDLE_STATE;
END_IF
CHANGE:
(*Событие на смену*)
LastState:=State;
State:=APPLY;
NextState:=START_PROCESS;
APPLY:
IF xApply THEN
TryCount:=0;
xApply:=FALSE;
State:=NextState;
END_IF
IF xError THEN
CASE TryCount OF
3:
State:=EMERGENCY;
ELSE
xError:=FALSE;
State:=LastState;
TryCount:=TryCount+1;
END_CASE
END_IF
EMERGENCY:
IF xReset THEN
xError:=FALSE;
xReset:=FALSE;
TryCount:=0;
State:=IDLE;
END_IF
END_CASE;
Все работы с таймерами я предпочитаю выносить из машины состояний, чтобы в какой-то момент не выключить их работу, так как состояние может изменится, а вызов таймер перенести забуду.
Также для всех состояние хорошо использовать перечисления, так как алиасы дают больше понимания в коде.
Итог
На этом моменте для первой порции думаю остановиться. Разобрали основные понятия машины состояний, реализовали паттерн программирования State, рассмотрели управление обычным механизмом и устройством, управление которым осуществляется через передачу данных.
Следующий этап будет заключаться в изменении машины состояний извне при помощи механизма обратного вызова.
Почта для ваших предложений и сотрудничества: info@engcore.ru
Мой ТГ канал: «Я вам че — Автоматизатор?»
