Найти в Дзене

Использование объектно-ориентированного подхода для ППО ПЛК.

Очень долгое время при написании прикладного программного обеспечения(программ) для плк использовался структурный подход, да и сейчас он максимально лидирует на рынке АСУТП. Вторая вещь — ООП. Руководство от PLC Open, по ООП, первой версии было выпущено 18 ноября 2021 года. В большинстве своем, к такому подходу относятся скептически и не очень его принимают, по тем причинам, что большой процент программистов ПЛК имеют мизерный опыт использования языков высокого уровня, а также из-за того, что для решения большинства рутинных задач автоматизации удобным и быстрым способом являются графические языки стандарта МЭК. Ниже я расскажу о структурном и объектно-ориентированном подходах на примере функций и функциональных блоков, затронем отличия в написание кода и разберем пару примеров, где ООП максимально оправдано. Структурный подход Под структурным подходом я буду понимать представление программы блоками(функции, функциональный блоки, программы), имеет ветвление и цикл в качестве управляющи
Оглавление

Очень долгое время при написании прикладного программного обеспечения(программ) для плк использовался структурный подход, да и сейчас он максимально лидирует на рынке АСУТП. Вторая вещь — ООП. Руководство от PLC Open, по ООП, первой версии было выпущено 18 ноября 2021 года. В большинстве своем, к такому подходу относятся скептически и не очень его принимают, по тем причинам, что большой процент программистов ПЛК имеют мизерный опыт использования языков высокого уровня, а также из-за того, что для решения большинства рутинных задач автоматизации удобным и быстрым способом являются графические языки стандарта МЭК.

Ниже я расскажу о структурном и объектно-ориентированном подходах на примере функций и функциональных блоков, затронем отличия в написание кода и разберем пару примеров, где ООП максимально оправдано.

Структурный подход

Под структурным подходом я буду понимать представление программы блоками(функции, функциональный блоки, программы), имеет ветвление и цикл в качестве управляющих конструкций, и выполняется сверху вниз.

Теперь стоит определится с основными программными блоками, а они же организационные единицы программы, которые используются при написании программы.

Функция

Функция — это единица организации программы, которая предоставляет сигнатуру вызова, и при выполнении возвращает один элемент программы(не путать с OUTPUT параметрами).

Output параметры — это способ передачи результатов вычислений функции. Аналогичной записью могут быть указатели в Input параметрах.

Возвращаемое значение — это значение возвращаемое в точку вызова функции, после её выполнения, которая может быть присвоена переменной или использоваться как часть других выражений.

Для примера я буду использовать функцию, которая принимает на вход целочисленное значение. У данной функции будут два результата вычисления — сумма входного значения с какой-то константой и разность с какой-то константой. Если разность будет положительной, то функция вернет значение успеха равная 200, если же разность будет отрицательной, то вернется 500, что будет означать неудачу работу функции.

fSubAdd — функция которая вычисляет сумму и разность входного значения, с заранее заданными константами.
fSubAdd — функция которая вычисляет сумму и разность входного значения, с заранее заданными константами.

Тип возвращаемого значения
Тип возвращаемого значения

В первой строчке, в части объявления функции, обозначаются имя функции и тип возвращаемого значения.

Результаты вычисления
Результаты вычисления

В строчках 6 и 7 описываются выходные переменные, в которые будут записаны результаты вычислений.

-4

С 3 по 7 строчку в области реализации происходит обработка результатов и функции присваивается значение которое она вернет. В дальнейшем, возвращаемое значение можно использовать а логике программы.

Результат выполнения функции
Результат выполнения функции

Функция не имеет внутренних состояний и не сохраняет значение своих внутренних переменных. Это значит, что каждый раз подавая на вход одинаковые значения мы будет получать одинаковый результат(не всегда такое возможно, так как существуют побочные эффекты).

Функции не должны использовать жестко прописанные глобальные переменные и адреса

Исходя из определения, теперь отпадает вопрос, когда использовать функцию, а когда функциональный блок

Функциональный блок

Функциональный блок также является организационной единицей. При вызове может передавать одно или несколько значений. Вызов функционального блока всегда осуществляется с помощью экземпляра этого блока. Каждый экземпляр — это копия функционального блока, которая занимает свою область памяти. Из-за того, что экземпляр имеет свою область памяти, то он сохраняет свои внутренние состояния, а значит на одинаковое воздействие может выдавать разный результат.

Программа

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

Программы, как сущность, существует одна на всё ваше приложение и в этом отличие от функционального блока. Так как при вызове программы изменяется состояние одной и той же сущности, и не важно откуда мы вызвали ее, а у функционального блока изменяется состояние экземпляра объекта.

Объектно-ориентированный подход

Данный подход основан на том, что все взаимодействие в программе осуществляется благодаря «объектам», каждый из которых является экземпляром определенного класса.

Основной единицей организации программы в объектно-ориентированном подходе для ПЛК служит функциональный блок, в случае Codesys для этого есть целый ряд дополнительного инструментария.

Интерфейс

Интерфейс — это средство объектно-ориентированного программирования. Объект интерфейса описывает набор прототипов методов и свойств. В данном контексте прототип означает, что методы и свойства содержат только декларации и не имеют реализации.

По факту — это договор, в котором описаны все методы и свойства, которые требуется реализовать программисту с одной стороны и которые будут поддерживаться другой частью программного обеспечения.

Метод

Метод содержит объявление и реализацию. Однако, в отличие от функции, метод не является независимой организационной единицей, а подчиняется функциональному блоку или программе. Метод может получить доступ ко всем допустимым переменным вышестоящей организационной единицы.

Свойство

Свойства используются для инкапсуляции данных, поскольку они обеспечивают внешний доступ к данным и одновременно выполняют роль фильтров. Для этого свойство предоставляет методы доступа Get и Set, которые позволяют осуществлять чтение и запись данных экземпляра, находящегося под свойством.

Организация ППО

Подход к организации данных, управление выполнением команды и логикой работы блоков будут отличаться в зависимости от подхода.

Рассмотрим теоретическую задачу.

Требуется вызывать два разных функциональных блока, один из которых будет использовать данные второго.

Структурная программа для ПЛК.

Как я обычно выстраиваю взаимодействие внутри программы.

Организация программы при структурном подходе
Организация программы при структурном подходе

Есть какая-то общая программа, которая содержит в себе экземпляры двух функциональных блоков.

Область объявления программы
Область объявления программы

Эта же самая композиция в коде.

Далее обратим внимание на две структуры данных, которые находятся в листе глобальных переменных.

-8

Теперь каждую из экземпляров этих структур требуется соотнести с соответствующим экземпляром функционального блока. Так как я предполагаю, что буду не только читать из структуры, но и писать в нее, то я пользуюсь ссылками на данные структуры, во входящих переменных области объявления программы.

Связь со структурой данных в FB1
Связь со структурой данных в FB1
Связь со структурой данных в FB2
Связь со структурой данных в FB2
Конструкция REFERENCE TO тождественна объявлению в разделе VAR_IN_OUT

Далее все это связываем в области реализации программы.

-11

Теперь про передачу значений из одной структуры в другую. Лично я, просто завожу еще одну ссылку на нужную мне структуру.

Добавление новой входной переменной
Добавление новой входной переменной

Результат работы выглядит следующим образом

-13

Тут сразу отвечу почему я использую в такие моменты ссылку(не указатели, а именно ссылки). Мне так удобнее по следующим причинам

  • Меньше входных переменных
  • Я так привык

Из минусов, который получают все — это иммутабельность входных переменных.

Также мы можем передать значения, через входные переменные, путем копирования данных значений.

-14

Конечно нам пришлось немного поменять и код в части реализации и вызова.

-15

Но он до сих пор работает, а так как мы передаем данные через копирования, а не через адресную адресацию, то и исходные значения мы не сможем поменять.

Также можно передавать копию структуры, чтобы избежать случайных изменений в исходных данных.

Если вас интересует, а можно ли структуры указать внутри функционального блока и избавиться от списка глобальных переменных, то тоже можно.

После изменений получаем следующее

FB1 c инкапсулированной структурой
FB1 c инкапсулированной структурой
FB2 c инкапсулированной структурой данных
FB2 c инкапсулированной структурой данных
Вызов функциональных блоков в программе
Вызов функциональных блоков в программе

Тут прошу обратить внимание на входные переменные x и y у FB2. Туда мы передаем прям из FB1, те данные которые инкапсулированы. Так делать можно, но не стоит. В идеале должно произойти следующее изменения.

-19

И вот после таких изменений, на входа FB2 мы отправляем выхода FB1

-20
-21

Однако записать хоть что-то внутрь VAR, не прокидывая через входные переменные — нельзя.

Так что если мы добавим в FB2 какой-нибудь условный коэффициент.

Условный коэффициент k
Условный коэффициент k

То при прямом обращении к нему, получим следующее сообщение.

[ERROR] SPvsOOP: StructPRG [Device: Plc Logic: Application](Строка 1, Столбец 1 (Реализ.)): C0037: 'k'не является входом 'fbInternalData2'

ОО программа для ПЛК.

Тут схема чуть сложнее Для такой задачи она неоправданно сложная.

-23

Тут подход начинается с того, что нам требуется обозначить два разных интерфейса.

Первый интерфейс Updater с методом Update. Он отвечает за обновление состояний экземпляра функционального блока. Это именно тот метод, который будет вызываться циклически.

Интерфейс Updater
Интерфейс Updater

Второй интерфейс это Getter, который будет иметь метод Get, который будет принимать значения из FB1 .

Интерфейс Getter
Интерфейс Getter

И также нам стоит указать в области объявления интерфейса входные переменные.

Входные переменные интерфейса.
Входные переменные интерфейса.

Теперь создаем два функциональных блока. FB1 реализует только интерфейс Updater, а FB2 реализует оба интерфейса.

После создания у функционального блока уже есть метод от интерфейса.

-27

В области объявления прописываем структуру, которая будет там инкапсулирована

-28

А в методе Update прописываем логику работы.

-29

Далее займемся вторым функциональным блоком. Он реализует сразу два интерфейса Updater и Getter

-30

После того как была нажата кнопка «Добавить» появился функциональный блок, в котором предопределенно сразу два метода

-31

В области определения указываем нашу структуру.

-32

Суть fbInternalData2OOP в том, чтобы просто записывать внутрь своей структуры данные, которые равны сумме двух переменных. Для этого нам потребуется лишь метод Get.

-33

Метод Update мы не трогаем. Теперь надо как-то пробросить значения из одного функционального блока в другой.

Для этого в fbInternalDataOOP во входных переменных указывает интерфейс Getter

-34

А в методе Update организуем вызов метода данного интерфейса.

-35

Он на строчке 10.

Теперь требуется создать экземпляры данных функциональных блоков. Для этого я использую лист глобальных переменных.

-36

Далее в основной программе прописываем инициализацию и вызов метода Update у FB1.

-37

С 9 по 12 строчку происходит инициализация значений. Если быть точным, то я на вход, который ожидает реализацию интерфейса Getter отправляю FB2. Тем самым я показываю FB1 по какому адресу находится реализация.

После на строчке 14, происходит вызов метода Update. И результат следующий.

-38

Теперь можем немного усложнить FB2 и написать реализацию метода Update используя переменную k, как какой-нибудь коэффициент.

Добавляем данную переменную в области определения функционального блока.

-39

Теперь нам надо записать нужное значение в k. Из примера в структурном стиле можно сделать вывод, что напрямую писать не получится, для этого в ООП есть свойства, которые позволяют писать данные в инкапсулированные переменные.

Окно создания «Свойства»
Окно создания «Свойства»

В результате у функционального блока fbInternalData2OOP появилось свойство.

-41

Get — позволяет получить инкапсулированные свойства, Set — позволяет установить.

Так как мы не особо хотим получать значение коэффициента, то Get можно удалить, а в Set написать реализацию.

-42

Также мы можем в свойствах писать простую обработку данных при Get и Set.

А теперь в методе Update из поля Result будет еще вычитать наш коэффициент.

-43

Дополняем нашу секцию инициализации значением k и вызываем метод Update.

Изменения в программе
Изменения в программе
Результат выполнения
Результат выполнения

По факту это две программы, которые действуют одинаково, если я ничего не напутал в UML нотации, но выглядят по разному.

Если в случае со структурным подходом, я надеюсь, все понятно, то в случае с ОО подходом появляются дополнительные слоя абстракции.

Такие слои абстракций позволяют сделать программу максимально гибкой и применять очень интересные архитектурные решения по построение ППО, однако такой подход увеличивает время разработки, но уменьшает трудозатраты на модификацию и сопровождения(при наличие хорошей документации и если другой программист тоже знает за ООП).

Примеры использования

А теперь настало время субъективных ситуаций, когда стоит начать применять объектно-ориентированный подход.

Использование устройств с разной внутренней логикой, но одинаковым интерфейсом.

Сокрытие реализации и общение через интерфейсы позволяет спокойно менять логику в одном месте, не переживая, что она как-то заденет другой кусок программы.

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

Например у нас появится fbInternalData3OOP, который также реализует интерфейс Getter

Вот его реализация

-46

Вот создаем экземпляр

-47

Поменял в блоке инициализации одно значение на другое

-48

Запускаю программу и все работает

-49

А теперь поместим FB2 и FB3 в массив интерфейсов.

-50

И немного перепишем основную логику.

-51

Теперь после прогрузки ПЛК можно менять логику выполнения программы на ходу

-52

Поменяли индекс в массиве.

-53

Основная логика в методе Update у FB2 как шла так и идет. Начал работать FB3. И все это просто по изменению одной переменной.

Выполнение методов стороннего функционального блока.

Такая же слабая связанность и возможность развязаться через интерфейсы позволяет улучшить опыт повторного использования кода.

Теперь не надо прописывать различные флаги или команды, а можно просто вызвать метод и потом через свойства получить нужные данные для продолжения работы.

Абстрактные классы

Если вы хотите еще немного сложностей, то давайте поработаем с абстрактными классами, которые позволяют улучшить повторное использование кода. Давайте немного по другому реализуем fbInternal2OOP и fbInternal3OOP. Допустим у них у обоих есть коэффициент k, он везде используется в методе Update, но вот логика метода Get будет разная.

Создаю абстрактный класс

-54

Добавляю свойство InitK.

-55

Далее в области объявления прописываю структуру и коэффициент K

-56

Пишу реализацию свойства и метода Update

Дальше я буду создавать новый функциональный блок, который буду расширять этим абстрактным классом

-57

В итоге получился функциональный блок со следующими методами.

И метод Update я просто удалю.

В метод Get перенесу логику из fbInternalData2OOP путем копирования.

-58

И точно также создам fbInternalData5OOP, удалю метод Update,а в метод Get скопирую логику fbInternalData3OOP.

-59

Дальше создаем экземпляры, инициализируем, прописываем в логику программы и смотрим.

-60

Можно заметить что у FB4 и FB5 что являются экземплярами fbInternalData4OOP и fbInternalData5OOP есть свойство InitK, который не реализован, метод Update, который был удален.

Однако они работают

-61
-62

И это очень удобный способ повторного использования кода, без его копирования, с возможностью расширения функционала.

Что-то еще?

На самом деле сюда можно спокойно отнести хорошую такую долю паттернов проектирования, плюс всевозможные приемы переиспользования кода и различного наследования, но всегда есть одно но…

Не используй ООП если…

Не стоит использовать парадигму ООП на коммерческих проектах, если еще не набита рука и нет опыта. Некоторые, любят использовать боевые проекты, как поле для изучения новых концепций, что делать не рекомендую. Учеба и коммерция идут рядом, но не стоит практиковаться там, где надо зарабатывать деньги.

Где себя точно не окупит такой подход — объекты малой автоматизации. Если это какая-то небольшая установка, на условные 50 сигналов, без сложной логики, которая не будет изменяться, то зачем тратить лишнее время, если его нет в запасе.

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

Хочу сказать большое спасибо Кислову Евгению, который смог дать подробный разбор всей статьи и указать на ошибки.

Также выражаю благодарность всей тестовой группе, которая дала обратную связь.

Телеграм канал: https://t.me/wtfcontrolsengineer

Почта для связи: info@engcore.ru