Использование функций и функциональных блоков в программах для ПЛК
Этот пост написан мной для новичков в программировании. Оказалось, что как только в OwenLogic ввели возможность создавать Функции (FUN) и Функциональные блоки (FB) на текстовом языке ST, проблема различия и «А чего в Функции переменные не сохраняются?» возникла снова. Так как когда-то я сам дико тупил и думал, что, например, Таймеры TON, TOF, TP, BLINK работают сами по себе аппаратно, то я решил написать про это всё пост.
Здесь я расскажу о том, чем программирование на ПЛК отличается от программирования на компьютере, чем отличаются между собой Функция (FUN) и Функциональный блок (FB), как их описывать и вызывать, а ещё поделюсь кое-какими приёмами их программирования и использования. Заодно упомяну то, почему иногда у кого-то «Не работают таймеры» и то, как их надо вызывать в коде на текстовом языке ST.
Мой рассказ будет простой, и кое-какие моменты в нём (касательно задач ПЛК) я притяну за уши. Но общие принципы сгодятся для того, чтобы понять то, как всё запрограммировать без ошибок. Да, и всё же я надеюсь, что вы умеете объявлять переменные и знаете прочие базовые вещи про программирование на ПЛК.
Содержание
- 1. Как работает программа ПЛК и в чём отличия от компьютерного программирования? Фронт и Спад сигналов.
- 2. Как устроены Функции (FUN) и Функциональные блоки (FB)? Разница и возможности.
- 3. Как объявлять и вызывать Функции (FUN) в Графических и Текстовых языках?
- 4. Как объявлять и вызывать Функциональные блоки (FB) в Графических и Текстовых языках?
- 5. Скрытие локальных переменных Функционального блока (CodeSys 3.5, hide_all_locals).
- 6. Действия и Методы Функциональных блоков (FB).
- 7. Особенности работы с FB Таймеров (TON, TOF, TP, BLINK).
- 8. Использование VAR_IN_OUT в сложных случаях (управление с Кнопки и Визуализации, HomeAssistant).
1. Как работает программа ПЛК и в чём отличия от компьютерного программирования? Фронт и Спад сигналов.
Если вы, как и я, начинаете переходить на программирование ПЛК с компьютерных языков, то вас ждёт один крайне неприятный сюрприз. Связан он с тем, что ПЛК выполняет программу в ЦИКЛЕ, а не по событиям. Больше всего работа ПЛК похожа на работу программ для микроконтроллеров, где у нас есть один Главный Цикл, который бесконечно «крутит» одни и те же действия.
А в компьютерных программах чаще всего у нас есть те самые События — нажатие кнопки, открытие окна, закрытие окна и другие. В них код вызывается ОДИН РАЗ, когда такое событие наступило. Вот в этом и есть неочевидная проблема. Покажу всё на простом примере.
Например, нам надо считать какие-нибудь нажатия кнопки. Ну, пусть у нас стоит какой-нибудь бесконтактный датчик (пост про них), который считает коробки, которые едут на конвейере. В ПЛК эта кнопка-датчик подключена на дискретный вход, который привязан к переменной hwButton (у меня есть старый пост про то, как создать проект и привязать переменные на базе CodeSys 2.3).
На компьютере эта программа (условная) выглядела бы так:
Пример простой программы на Visual Basic для подсчёта нажатий
У нас есть событие «Command1_Click» (нажатие кнопки Command1), в котором мы пишем простой код: «Count = Count + 1», и всё. Тогда при каждом нажатии кнопки к переменной Count будет прибавляться ровно единица.
Но если такой же код написать в ПЛК, то будет плохо:
Пример НЕверной обработки нажатия кнопки на ПЛК (подсчёт импульсов)
Вроде бы всё очевидно, да? Если кнопка нажата (или датчик сработал), прибавляем единицу к счётчику.
Но внутри ПЛК такая программа может выполняться каждую 1 МиллиСекунду (1000 раз в секунду) в бесконечном цикле! Поэтому, если коробка едет мимо датчика за две секунды (то есть, датчик в течение двух секунд выдаёт TRUE), то мы насчитаем не одну штуку, а 2000 штук!!!
Это одна из базовых ошибок программирования на ПЛК, кстати. Ещё бывает, когда по нажатию кнопки переключают, например, реле лампы освещения: если выключено — включить и, если включено — выключить. А потом удивляются, почему при нажатии на кнопку свет дёргается и то остаётся включенным, а то вдруг выключенным.
Исправляется это просто, если знать, как. Нужно сохранять в отдельной переменной предыдущее состояние входа (кнопки) и сравнивать его с текущим. Если текущий сигнал равен TRUE, а предыдущий — FALSE — то сигнал ИЗМЕНИЛСЯ (кнопка нажата).
Чтобы не делать такие операции руками каждый раз (и не путаться в коде), в программах для ПЛК и ПР есть специальные триггеры — детекторы Фронта и Спада сигнала:
- R_TRIG (RTRIG) — Детектор Фронта. Выдаёт на выходе импульс, если вход изменился с FALSE на TRUE («включился»);
- F_TRIG (FTRIG) — Детектор Спада. Выдаёт на выходе импульс, если вход изменился с TRUE на FALSE («выключился»).
Их особенность в том, что импульс на выходе выдаётся точно на один цикл программы ПЛК.
Вот теперь наш пример можно переписать так:
Пример правильное обработки однократного нажатия кнопки на ПЛК (подсчёт импульсов)
Значение с кнопки hwButton мы передаём на триггер фронта fbBtnPress. А уже как только fbBtnPress.Q (выход) будет равен TRUE (один раз за всё время, пока кнопку нажмут и будут держать нажатой) — прибавляем единичку к Count.
Такая особенность R_TRIG и F_TRIG позволяет решить задачу, которая кажется сложной в электрике, но легко решается в ПЛК: когда нужно управлять каким-то выходом одновременно при помощи импульсного сигнала и постоянного сигнала. Ближайший пример — когда лампа освещения должна управляться по кнопке и по реле времени. При этом, когда лампа включилась по реле времени, кнопка всё равно должна её включить и выключать.
В электрике напрашивается такое решение: на управление по кнопке поставить импульсное реле (вон базовый пост про них), а контакты реле времени подключить параллельно контактам импульсного реле. Но это не верно: если реле времени будет выключено, то лампа будет управляться по импульсному реле. Но как только реле времени включится, кнопка действовать перестанет, так как контакты импульсного реле будет зашунтированы контактами реле времени.
ОКе! Возьмём дорогое и редкое импульсное реле с дополнительными входами центрального управления ON и OFF. Реле времени (например, реле времени от НоваТек, про которые я тут писал) должно иметь переключающий контакт. Мы подключим его на входы ON и OFF импульсное реле. Тогда… Всё сгорит! Потому реле времени будет подавать сигналы на импульсное реле постоянно. А нам нужны импульсы.
Ну, тогда мы возьмём два реле времени (пост про ABB CT-ERD) и настроим их так, чтобы они, получая постоянный сигнал, выдавали короткий импульс. Тогда этими импульсами будет переключаться наше импульсное реле (и управляться по кнопке).
Только всё это будет громоздко и крайне адски дорого. Вот тут-то нам помогают ПЛК или ПР.
На ПЛК или ПР делается примерно так же, но приятнее. Берём такое же (но программно написанное) импульсное реле. Я в разных постах про Siemens Logo и OwenLogic говорил, как такое реле сделать.
Например, вот то, что нужно, на базе D-триггера в OwenLogic из этого поста:
Процесс тестирования нашего импульсного реле в симуляторе OWEN Logic
На вход «C» D-триггера подключаем кнопку. Сигнал от реле времени пропускаем через R_TRIG (включение) и F_TRIG (выключение) и подключаем на входы «S» (включение) и «R» (выключение) D-триггера соответственно. Вот и вся схема.
Я думаю, что нарисую её на OwenLogic. Пусть будет, вдруг кому сгодится (в качестве реле времени я взял таймер CLOCK):
Пример программы на OwenLogic с триггерами R_TRIG и F_TRIG (управление светом независимо по кнопке и реле времени)
Вторая важная особенность работы программы на ПЛК в том, что там используется специальный концепт «Входы — Программа — Выходы». Он был специально придуман для ПЛК для того, чтобы можно было реализовывать алгоритмы, в которых переменная меняется в разных местах одной программы ПЛК.
Если описать это простым языком, то ПЛК на уровне ядра работает так:
- В пределах одной программы (или задачи — зависит от типа ПЛК) запоминаются значения всех входных переменных;
- Выполняется один цикл программы для ПЛК с использованием этих переменных;
- Все значения из программы ПЛК в конце цикла передаются на выходные переменные.
Эта схема позволяет нам знать, что промежуточные значения наших переменных не будут выданы на выход на середине нашей программы. То есть, мы можем написать примерно такие строки кода:
-
- OutLamp := FALSE;
- (Какая-то другая часть программы)
- IF (Time > 20) THEN
OutLamp := TRUE;
END_IF
То есть, реализовать концепт «Выключить (или обнулить) всё, а потом в нужных местах включить (присвоить значения), если это надо по каким-то условиям». Это удобнее, чем писать кучу вложенных IF для разных условий.
Вот пример кода из моего блока обработки ошибок связи с устройствами по Modbus (писал про него в посте про технологии обработки данных от ПЛК):
Пример изменения значения переменной в разных местах кода
Здесь одна и та же переменная ErrCount меняется в разных местах программы по разным условиям. Каждое условие описывает только одно действие: увеличение на 1 или обнуление. Однако за счёт концепта «Входы — Программа — Выходы» на выходе программы мы получим одно, верное, значение.
Это всё хорошо, если ПЛК имеет обычную многозадачность, при которой каждой задаче выделяется фиксированное время (квант времени). Тогда в один момент времени в ПЛК будет выполняться одна задача. Примерно так работают ПЛК со средой исполнения CodeSys 2.3 и OwenLogic.
А вот в среде CodeSys 3.5 многозадачность вытесняющая. Это значит, что среда исполнения ПЛК может прервать выполнение одной задачи в каком-то её месте и начать выполнять другую, более быструю задачу, а потом вернуться к более медленной.
Если вы бездумно понапихаете задач в такой ПЛК, то можете получить интересный глюк. Мой пример несколько синтетический и не совсем верный, но на нём я покажу лишь принцип ошибки, которая (если не знать про неё) крайне сложно отлавливается.
Предположим, у нас есть длинная и медленная Задача 1 и глобальная переменная gVarValue. Мы используем правило концепта «Входы — Программа — Выходы» и делаем сложные вычисления с переменной gVarValue: назначаем какое-то базовое значение, а потом прибавляем к ней другое:
Пример Задачи ПЛК: Простая с последовательными вычислениями
Если бы эта задача была одна, то на выходе неё мы всегда бы получали значение «300».
Но у нас есть ещё одна, более быстрая, Задача 2, которая использует ту же переменную gVarValue:
Пример Задачи ПЛК: Быстрая с вычислениями на основе другой задачи
Тогда может быть крутой конфликт задач. Так как Задача 2 должна вызываться часто, то выполнение Задачи 1 может быть прервано.
В результате Задача 2 может получать значение gVarValue то равное 10 (в начале Задачи 1), а то равное 300 (в конце Задачи 1):
Пример конфликта глобальных переменных в разных задачах ПЛК
Бывает и наоборот: быстрая задача что-то считает, а медленная задача вдруг кратковременно получает промежуточные результаты. Такие глюки бывают даже в ядре CodeSys. Например, на форуме ОВЕНа по этой ссылке описан случай, когда в высоконагруженных проектах на CodeSys 3.5 системное время иногда отображается без учёта временной зоны UTCOffset. Оказывается, иногда мы успеваем быстро считать системное время из промежуточного состояния задачи, когда значение UTCOffset к нему ещё не было применено!..
Поэтому крайне важно понимать то, как работает цикл программ ПЛК!
2. Как устроены Функции (FUN) и Функциональные блоки (FB)? Разница и возможности.
А теперь я расскажу о том, как работают Функция (FUN) и Функциональный Блок (FB) и какая между ними разница. Без них не было бы никакого удобного программирования. Да и множество встроенных объектов (например, таймеры) — это тоже Функции или Функциональные Блоки.
Если описать разницу кратко, то я бы сказал так:
- Функция (FUN) НЕ сохраняет значения своих переменных после её вызова в программе и делает одно и то же одинаковое действие всегда, даже при вызове из разных мест кода;
- Функциональный блок (FB) запоминает все значения своих внутренних переменных после его вызова. Он тоже делает одни и те же действия (логику действий), но его можно «привязать» к разным переменным (создать несколько экземляров) и сделать так, чтобы в разных переменных делались одни и те же действия (логика), но с разными данными (например, управление светом по кнопке: логика одна и та же, но один экземпляр блока включен, а другой — выключен, а третий — снова включен).
Различные функциональные блоки (FB) в проекте OwenLogic
Здесь мне снова хочется сделать отсылку к компьютерным языкам программирования, так как слово «Функция» есть во всех из них. Функция — это какой-то программный код, который получает на входе какие-то аргументы (параметры), делает над ними какие-то действия и выдаёт (или нет) результат.
Обычно Функции используют тогда, когда в разных местах программы делается одно и то же действие. Например:
- Математические расчёты (Среднее, Минимум, Максимум, Пропорции);
- Работа с данными (Найти в строке, Соединить две строки, Очистить строку);
- Работа с объектами (Добавить в список, Найти в списке, Сортировать список).
Суть функций в том, что один и тот же код (вычислений, операций с данными) используется в разных местах проекта без его копирования туда. Ну, например, когда мы вызываем ConCat(«Привет », «Мир!») или ConCat(«Hello », «World!»), то исходный код функции ConCat — один и тот же. Она просто соединяет две строки в одну. Ей передаются разные строки — и всё. Внутри функции могут быть разные циклы и переменные — но они все действуют и имеют смысл только на момент работы этой функции, а потом обнуляются.
Точно так же работают Функции (FUN) в ПЛК. Вот что про них можно сказать:
- Могут возвращать значение (если функция делает расчёты) или нет (если функция делает какую-то операцию вида «Очистить строку»);
- Могут принимать аргументы (параметры) или нет. Например, функция получения системного времени не принимает аргументы, а сразу возвращает время. А функция расчёта количества литров по импульсам от расходомера должна вернуть результат;
- Функции имеют доступ к глобальным переменным проекта ПЛК и могут на них влиять, если это надо. Ну, к примеру, если определена какая-нибудь глобальная переменная типа Sys_DebugMode — то в функции можно её использовать;
- Все переменные, которые используются внутри функции, имеют смысл только на момент того, когда функция выполняет код. После того, как функция выполнила код и вернула результат, все переменные уничтожаются и НЕ сохраняются. На момент следующего вызова функции они примут те значения, которые им были заданы (обычно нули или пустые строки, если явно не указано другого).
Можно сказать, что ВСЕ переменные внутри функции — это VAR_TEMP (временные). Даже RETAIN нет!
Если хочется — можно использовать константы VAR CONSTANT для удобного написания кода без «магических чисел».
Последний пункт крайне важен. Можно сказать, что из-за него и родился этот пост, так как многие думают, что переменные внутри функции будут сохраняться. А потом удивляются тому, что какая-нибудь функция с именем CountValues ничего не считает и каждый раз начинает счёт с нуля.
Ближайший аналог Функционального Блока (FB) — это Класс (Class) в компьютерных языках программирования. Класс описывается один раз (исходный код), но можно создать множество объектов этого Класса, которые будут хранить разные данные на момент исполнения программы.
В ПЛК всё точно так же, но более упрощённо. Мне хочется объяснить Функциональный Блок (FB) на примере обычного аппаратного Реле Времени:
- Вот у реле времени есть вход для запуска отсчёта, уставка времени и выход срабатывания. Об этом мы знаем из сайта производителя такого реле: он написал об этом в документации на это реле.
Аналог: мы описали Функциональный Блок и логику его работы в исходном коде программы для ПЛК. - Дальше мы понимаем, что нам надо иметь четыре схемы включения чего-то с задержкой. Время задержек и то, когда эти схемы запускаются, должно быть разным. Что делает электрик? Он покупает четыре таких реле времени и собирает четыре одинаковые схемы управления.
Аналог: мы создаём четыре переменные, которые ссылаются на Функциональный Блок (создаём четыре экземпляра блока). - Во время работы электрощита наши реле времени имеют разные состояния и настройки: какое-то уже включилось, какое-то ещё только считает время, какое-то ещё даже не запущено.
Аналог: в момент работы программы ПЛК наши Функциональные Блоки имеют разное состояние своих входов, выходов и внутренних переменных (хоть и описываются одним и тем же исходным кодом).
Возможно, это несколько сложно. Если провести ещё одну аналогию — то Функциональный Блок позволяет создавать свои копии (экземпляры), каждая из которых будет хранить разные данные (и работать по разному).
Образно, вот есть тип переменной «WORD». Мы можем объявить пять разных переменных (но их тип будет один и тоже) и присвоить им разные значения: 0, 20, 4245, 18, 658.
Вот что я бы сказал про Функциональные Блоки (FB):
- Могут иметь или не иметь Входные переменные (VAR_INPUT). Например FB Импульсного Реле будет иметь Вход для кнопки, а FB «вечной мигалки» (если не нужны никакие настройки) — только выход мигающего сигнала.
Входные переменные могут быть не только «командами» на какие-то действия, а ещё и «настройками» работы блока. Например, у блока Реле Времени из примера выше будет два входа: сигнал на запуск и уставка времени. - Могут иметь или не иметь ВЫходные переменные (VAR_OUTPUT). Аналогия тут такая же: всё, что нам надо получать на выходе блока (например, сигнал на управление лампой света) — это его выходные переменные.
- В хитрых случаях могут иметь Входно-Выходные переменные VAR_IN_OUT, которые можно использовать как входы и выходы сразу же. Про это я расскажу в последней части поста.
- Могут иметь или не иметь внутренние переменные (VAR), включая константы VAR CONSTANT и временные VAR TEMP.
- Значения ВСЕХ переменных (кроме VAR_TEMP) сохраняются на всё время выполнения программы в ПЛК. Для разных экземпляров блоков (которые объявлены разными переменными) значения могут быть разными (если это не константы).
- Есть доступ на чтение или запись глобальных переменных в программе ПЛК, как у Функций.
- Внутри Функционального Блока можно объявлять экземпляры других Функциональных Блоков (но не самого себя). Ближайший пример — внутри своего блока объявить разные FB таймеров TON, TOF, TP, BLINK — это тоже функциональные блоки. Или внутри блока управления батареей отопления с двумя кранами отопления объявить два блока управления кранами воды на этой батарее.
- Функциональный блок можно объявить как RETAIN или PERSISTENT. Тогда его состояние (все внутренние переменные) будут восстанавливаться после включения и включения питания ПЛК. Это очень удобно для того, чтобы сохранять, к примеру, состояние работы света квартиры после передёргивания питания ПЛК: просто все FB Импульсных Реле объявляем как RETAIN.
Хах! С этим RETAIN для импульсных реле был связан прикол. Как-то звонит мне заказчик и говорит: «Твой ПЛК завис! На кнопки не реагирует, а если передёрнуть его по питанию — то свет включается так же, как и был». На деле оказалось, что монтажники замкнули цепь кнопок и сожгли предохранитель, который там стоял. Вот щит на кнопки-то и не реагировал, а за счёт RETAIN сохранял состояние всех FB импульсных реле :) - Можно использовать дополнительные Действия (в CodeSys 2.3 и CodeSys 3.5) и Методы (только в CodeSys 3.5) для удобной передачи «команд» блоку. Типа fbMyDimmer.SwitchOff(). Это будет похоже на ООП (Объектно-Ориентированное Программирование).
- При отладке программы в ПЛК нужно выбрать конкретный экземпляр FB, чтобы посмотреть, как ведёт себя именно он с именно его переменными, входами и выходами.
Особо отмечу, что обращаться в программе надо ВСЕГДА именно к экземпляру Функционального Блока. Если вы описали блок CSNasos, то прямое обращение к нему бессмысленно, как если бы вы купили реле времени, и пытались заставить его работать, даже не устанавливая в щит и не подключая его. Оно было бы «выключено».
Даже, если надо использовать FB один раз — нужно объявить его экземпляр: например, «fbNasos : CSNasos;», а потом уже обращаться к этому экземпляру, например «hwMotor := fbNasos.Motor;». По аналогии, реле времени надо установить в щит, подключить и подать питание на него. На именно это реле, а не на какое-то «вообще» реле времени.
Без FB программирование на ПЛК было бы сущим адским адом. Даже функции можно использовать не так часто, как FB. Ну, например, мы делаем автоматику котельной, где есть насосы, которые работают по одному и тому же принципу: сигнал пуск-стоп, ротация двух насосов по времени, контроль тока в цепи, контроль срабатывания контакторов. Если такой код надо будет копировать для 10 насосов — то мы охренем.
А тут мы просто объявляем FB для управления такими насосами с нужными входами и выходами и объявляем его экземпляры 10 раз с понятными именами. Тогда код будет выглядеть красиво, типа «IF (fbNasosGVS.Working = TRUE) THEN»…
3. Как объявлять и вызывать Функции (FUN) в Графических и Текстовых языках?
Итак, сначала поиграемся с Функциями. Из прошлого раздела вы поняли, что они простые: нужно объявить их, написать код для расчётов, и вернуть значение (или нет).
Добавляется функция при помощи диалогового окна (оно вызывается выбором из меню по правой кнопке мыши):
Добавление описания Функции (FUN) в CodeSys
Здесь надо задать её Имя, тип возвращаемого результата (его может не быть) и язык, на котором она будет написана.
После этого можно описать её входные переменные-аргументу (VAR_INPUT), обычные переменные (VAR) и константы (VAR CONSTANT).
Использование констант — очень хорошее правило программирования. Они позволяют избежать «магических чисел» (это когда по коду не ясно, что значит какое-то число. Например «CurrentStep := 4» в конечном автомате ничего не скажет, а «CurrentStep := STEP_WORKING» будет сразу понятно.
Ещё использование констант позволяет объявить массивы и циклы по ним так, чтобы цикл не вышел за границы массива. Ну и, конечно же, всякие коэффициенты, граничные значения — должны быть константами. Как в моём примере.
Я написал простую функцию, которая получает значение типа DWORD, переводит его в REAL (при помощи временной переменной), а потом умножает на некий константный коэффициент:
Пример Функции (FUN): Объявление переменных и код вычислений
Собственно, для этого функции и используются чаще всего: для расчётов или пересчётов чего-либо.
В графических языках функция представляет собой такой же прямоугольник-блок, на входы которого надо подать значения, а с выхода — снять:
Пример вызова Функции (FUN) на графическом языке
В текстовых языках функция вызывается так же, как и встроенные в язык. Можно задать значения (аргументы) напрямую, можно подставить какие-то переменные:
Примеры вызова Функции (FUN) в текстовом языке
Вот просто так пример функции, которая приклеивает к заданной строке число типа REAL, и в конец добавляет ещё другую строку, если она не пустая:
Пример Функции (добавление числа типа REAL к строке)
Здесь число типа REAL при помощи переменной wsTmpBuffer превращается в строку, а потом при помощи библиотеки String Utils (не путать с Owen String Utils) склеивается с входной строкой-буфером.
Все переменные в этой функции не сохраняются после её вызова. В этом, кстати, и нет смысла: строка и число каждый раз разные будут.
4. Как объявлять и вызывать Функциональные блоки (FB) в Графических и Текстовых языках?
Функциональные Блоки добавляются в проект так же, как и функции (про это было сказано в прошлом разделе). Здесь ещё можно выбрать наследование и зависимость блока от других (это я пропускаю в данном посте) и так же задать язык, на котором FB будет написан:
Добавление описания Функционального Блока (FB) в CodeSys
Вот базовое тело функционального блока с разными вариантами переменных:
Различные типы переменных Функционального Блока (FB)
Вот какие виды и особенности переменных есть (конечно же, значения этих переменных будут разными для разных экземпляров блока):
- VAR_INPUT — Входные переменные блока (параметры, команды, сигналы). Доступны на чтение и запись;
- VAR_OUTPUT — Выходные переменные блока (откуда будут выдаваться данные или сигналы). Доступны на чтение;
- VAR_IN_OUT — Переменные-ссылки (указатели). Работают как на вход, так и на выход (чтение и запись; про них расскажу в последней части этого поста);
- VAR — Обычные локальные переменные блока (если не скрыты извне — доступны на чтение и запись);
- VAR CONSTANT — Константы (доступны только на чтение);
- VAR_TEMP — Временные переменные. НЕ сохраняются при работе блока; действуют только на момент выполнения программы блока. Доступны на чтение и запись;
- VAR RETAIN (VAR PERSISTENT) — Энергонезависимые переменные. Доступны на чтение и запись. Обычно (но это не точно, пишу в 4 ночи по памяти), если такие переменные объявлены — то сохраняется весь блок целиком. Используются, чтобы прямо внутри блока хранить какие-то данные между выключением питания ПЛК. Например, если блок будет счётчиком литров воды.
- ВСЕ переменные (даже выходные), кроме VAR_TEMP, СОХРАНЯЮТ свои значения на всё время работы программы в ПЛК:
- Если значение никогда не было задано — то равны нулю или пустой строке, как и обычные переменные в CodeSys;
- Если значение задано при объявлении переменных — то равны этому значению;
- Если значение задано внутри блока — равны этому значению;
- Если значение задано извне блока — равны этому значению.
То есть, как вариант, можно даже описать простой FB с переменными-входами и использовать его как хранилку каких-то данных.
Я описал разные типы переменных (кроме VAR_IN_OUT) и задал им начальные значения:
Пример описания переменных Функционального Блока (FB) различных видов
Задавать начальные значения переменных — очень хорошая практика. Это позволяет задать блоку какие-то параметры (к примеру, время длинного нажатия на кнопку, настройку автогашения света) так, что если его вызовут без указания этих параметров — блок будет нормально работать, а не сбоить.
Для того, чтобы работать с функциональным блоком, нам надо «создать» его экземпляр. Ведь функцональный блок как раз и задуман для того, чтобы можно было создавать несколько его копий, которые будут работать отдельно друг от друга.
Если выразиться попроще, то экземпляры создаются тогда, когда мы объявляем их в переменных. Причём объявление переменных одинаково для графических и текстовых языков:
Объявления отдельных экземпляров Функциональных Блоков (FB)
Как только мы объявили переменную блока — то он сразу получит те самые начальные значения своих переменных внутри, если мы их задали при объявлении (почему я и говорю, что это хорошая практика — задавать их). К ним можно сразу обращаться в программе, если это надо.
Чтобы заставить работать блок (его встроенную логику) — надо вызвать его из программы. Ведь блок — это такой же кусочек программы, и поэтому она должна выполняться так же, как и основная программв ПЛК.
Да, это важно! Ещё раз повторю, что Функциональные Блоки НЕ работают сами по себе. Можно сказать, что это что-то типа подпрограмм со своим набором переменных. И, чтобы они работали с этими переменными, эти подпрограммы НАДО вызывать. Обычно вызов происходит там же, где и идёт обращение к блоку. Ну, это логично: в задаче управления светом вызывать блоки импульсных реле. В задаче управления насосами — блоки насосов и так далее.
Чтобы добавить блок в программу на графическом языке, надо выбрать «Элемент» из списка компонентов:
Добавление экземпляра Функционального Блока (FB) на графическом языке: Выбор Элемента
А потом добавить его на поле схемы-программы:
Добавление экземпляра Функционального Блока (FB) на графическом языке: Вставка пустого Элемента
После этого надо указать имя объекта. Мы вводим имя нашего FB — «CSTest». В этот момент на схеме автоматически появляются все его входы и выходы, описанные в переменных:
Добавление экземпляра Функционального Блока (FB) на графическом языке: Назначение Элементу типа
Теперь надо указать то, к какой из объявленных переменных относится наш блок на схеме. Для этого вписываем нужную переменную сверху:
Добавление экземпляра Функционального Блока (FB) на графическом языке: Назначение Элементу экземпляра переменной
Таким образом можно вынести и остальные блоки на схему:
Несколько разных экземпляров Функционального Блока (FB) на графическом языке
Будьте внимательны! Копипаста может наделать ошибок: для программы ПЛК НЕ обязательно вызывать один экземпляр FB только один раз за программу. Поэтому вызов блоков fbTest1 и ещё раз fbTest1 не будет ошибкой для компилятора! Код в этих блоках сработает два раза за один цикл программы, и всего делов. А вот для нас это может быть ошибкой! Где-то свет сразу включится и выключится по второму «нажатию», а где-то и данные могут не так подсчитаться!
Дальше нам остаётся привязать входные и выходные переменные к блоку, и всё будет работать.
Передача переменных Функциональному Блоку (FB) на графическом языке
Ещё раз обращу внимание на неочевидность: если блок объявлен в переменных, то его входы и выходы будут доступны программно. Но если он не «нарисован» на схеме — то он НЕ будет работать!
При этом не будет ошибкой нарисовать блок без входов или без выходов, если они не испольузются (лишние можно удалить, чтобы не мозолили глаза на схеме): он будет работать корректно, так как все его переменные сохраняются внутри.
А вот вызов Функционального Блока в текстовых языках (ST) более НЕочевиден, так как там все входы и выходы блока записываются просто через запятую. Я долго тупил про это, переходя с компьютерных языков программирования, так как думал что блок «как-то сам работает внутри», и надо как-то за одну строчку кода привязать в него все переменные…
На деле же среда CodeSys помогает правильно описать вызов FB при помощи Ассистента ввода (горячая клавиша «F2»).
Главное выбрать правильный режим. Для вызова НЕ надо выбирать «Переменные» (тогда вставится только имя переменной блока):
Добавление экземпляра Функционального Блока (FB) в текстовом языке: Неверный выбор варианта вставки в код
Надо выбирать «Вызовы интерфейса»:
Добавление экземпляра Функционального Блока (FB) в текстовом языке: Выбор варианта вставки в код как Вызов Интерфейса
После этого происходит чудо: CodeSys делает нам описание всех входов и выходов блока в текстовом формате:
Вызов Функционального Блока (FB) в текстовом языке (после добавления через Ассистент ввода)
Обратите внимание, что входы записываются через присваивание «:=», а выходы — через стрелочку «=>». Среда CodeSys не отделяет имена от присваиваний пробелами, а записывает так: «InPulse:=». Можно записать это же с пробелами или табуляцией: «InPulse :=».
Что можно делать с таким описанием?
- Список назначений входов и выходов разделяется только запятыми;
- Можно писать в одну строчку или в несколько;
- Порядок назначений НЕ важен;
- Допускаются пустые значения, как на скриншоте сверху. Так тоже всё будет работать. Поэтому какие-то входы или выходы можно описать и оставить «резервом» на будущее. Вот ведь в графическом языке могут быть «неподключенные» входы или выходы. Так и тут;
- Значения можно не передавать списком, а записать через точку, как в какой-нибудь структуре (STRUCT).
Различные способы вызова Функционального Блока (FB) в текстовом языке
Я заготовил вам три распространённых способа передачи значений в и из блока:
Различные способы вызова Функционального Блока (FB) в текстовом языке с привязкой переменных
- Способ 1: Описать сразу все входы и выходы.
Это удобно, если блок простой: к нему можно сразу привязать все входные и выходные переменные. Например, если это импульсное реле: привязали сразу кнопку и лампу — и всё; - Способ 2: Все входные значения передаём через запятую, а выходы используем дальше в программе или передаём на переменные.
Это удобно, когда блок — таймер. Его удобно вызывать с указанием флага запуска и времени работы, а выход Q использовать в каком-нибудь условии типа «IF (fbTimerDelay.Q = TRUE) THEN»; - Способ 3: Отдельно присвоить значения входов блока, вызвать его для работы, и потом забрать значения выходов блока.
Я таким почти никогда не пользуюсь: вызов самого блока может затеряться среди присвоений (удобнее Способ 2). Однако такой приём может быть удобен, если надо присвоить блоку какие-то начальные значения в начале программы, а потом вызывать его в разных местах программы с небольшими изменениями части значений.
Для примера я приведу вам мой блок для подсчёта значений. Помните, в начале поста я говорил про цикл программы и то, что для подсчёта одиночных импульсов надо использовать R_TRIG? Так вот иногда (чаще всего для отладки) мне нужно было считать импульсы. Вот для этого я сделал себе такой блок:
Пример Функционального Блока (FB): Счётчик импульсов
Его можно сделать RETAIN и хранить в нём, например, число импульсов от счётчиков воды!
А в посте про технологии отладки и программирования ПЛК я использовал огромные и мощные FB для сбора статистики с датчиков:
Блоки сбора статистики и средних значений данных с датчиков за разные периоды
В своих проектах я использую оба языка для работы с FB.
Простые задачи (например, управление отоплением по температуре) я могу отрисовать в графическом виде, так как тут наглядно видны входы и выходы:
Пример программы с Функциональными Блоками (FB) на графическом языке
А в каком-то месте, где надо рисовать много сигналов, которые сложно вводятся (обращение к другим FB и вложенным переменным), мне удобнее накопипастить и написать всё текстом:
Пример программы с Функциональными Блоками (FB) на текстовом языке
5. Скрытие локальных переменных Функционального блока (CodeSys 3.5, hide_all_locals).
Как вы уже поняли, внутри Функционального блока может быть много внутренних переменных. Это могут быть как простые переменные, которые используются для внутренних расчётов, так и объявленные внутри экземпляры других функциональных блоков.
Когда мы обращаемся к переменным экземпляра блока через точку, то по умолчанию нам покажутся ВСЕ переменные этого блока, даже внутренние. В простых FB тут нет ничего страшного. Но если у нас сложный FB, в котором эти переменные будут мешать, или эти переменные не должны видеть чужие люди (если блок, например, находится в запароленной библиотеке), то есть фишка, которая позволяет эти переменные скрыть.
Работает она в CodeSys 3.5. Это специальный атрибут «hide_all_locals», который скрывает ВСЕ локальные (внутренние) переменные Функционального Блока, кроме VAR_INPUT, VAR_OUTPUT и VAR_IN_OUT.
Записывается он в виде строки «{attribute ‘hide_all_locals’}» в самой первой строке описания блока:
Специальный атрибут hide_all_locals (скрытие локальных переменных) - включен
В этом случае все внутренние переменные будут скрыты:
Процесс выбора переменных при наборе программы на ST (локальные переменные скрыты)
Если эта фишка не нужна — удалите или закомментируйте этот атрибут:
Специальный атрибут hide_all_locals (скрытие локальных переменных) - отключен
Тогда в списке переменных блока будут ВСЕ его переменные. Даже константы:
Процесс выбора переменных при наборе программы на ST (локальные переменные открыты)
Доступ к внутренним переменным блока (если они не скрыты) через точку легко действует на текстовом языке ST. Мы просто выбираем нужную после ввода точки из списка.
На графическом языке CFC/FBD такое не прокатит. Если мы поставим на схему блок с выключенным «hide_all_locals», то он будет выглядеть так же, как и обычно:
Доступ к локальным переменным на графическом языке (штатно невозможен)
Единственный вариант доступа к внутренним переменным блока — создать Входной элемент и через точку обратиться к переменной:
Доступ к локальным переменным на графическом языке через вызов их через точку
6. Действия и Методы Функциональных блоков (FB).
Следующая хитрая и удобная фишка Функциональных Блоков — это Действия и Методы. К сожалению, в CodeSys 2.3 доступны ТОЛЬКО Действия.
Что это такое? А это возможность прямо внутри Функционального Блока создавать свои «подпрограммы», которые можно вызывать как внутри самого блока, так и извне.
Первое, для чего это можно использовать — оформлять одинаковые куски кода внутри одного FB. Например, может быть действие Clear(), которое очищает какие-то данные в блоке. И оно может вызываться из разных мест кода этого блока, чтобы не копипастить один и тот же код. Или же это может быть метод SetState для конечного автомата, который красиво переключает его в новое состояние вместо записи «CurrentState := STATE_WORK;». Вызов SetState(STATE_WORK) выглядит куда красивее.
Второе — для подачи внешних команд. Для внешних команд можно завести переменные типа Reset, SetOn, SetOff и написать код типа «IF (Reset = TRUE) THEN», а можно создать действия, к которым обращаться извне экземпляра блока по типу fbDimmer.SetOn. Или методы: fbDimmer.SetBright(30);
- Действие не имеет входных аргументов (параметров) и ничего не возвращает на выходе. Можно сказать, что это просто подпрограмма;
- Метод может иметь входные аргументы (параметры) и возвращать что-то на выходе;
- К сожалению, в CodeSys 2.3 есть только Действия;
- Все Действия и Методы имеют доступ ко всем переменным внутри Функционального Блока;
- Внутри Методов могут быть свои локальные переменные. Все они работают как VAR_TEMP (НЕ сохраняют значения после выполнения кода Метода);
- В CodeSys 3.5 Действия и Методы могут иметь разные варианты доступа: PUBLIC (доступен извне экземпляра блока) и PRIVATE (доступен только внутри блока для самого себя, снаружи — нет);
- В CodeSys 3.5 Действия и Методы могут писаться на языках, отличающихся от того, на котором написан сам FB. К примеру, если что-то удобно написать на CFC — это можно сделать в виде Действия.
Чтобы добавить Действие или Метод, надо кликнуть правой кнопкой мыши по описанию FB в структуре проекта:
Добавление Действия и Метода к Функциональному Блоку (FB)
Далее в диалоговом окне указать нужные значения: имя, возвращаемый тип (для Метода), язык реализации, вариант доступа (если не указан — то PUBLIC).
Диалог добавления Метода к Функциональному Блоку (FB)
А дальше — объявить переменные (для Метода) и написать нужный код.
В моём примере у Функционального блока есть действие Reset, которое обнуляет счётчик и метод SetVal, который устанавливает новое значение счётчика, если оно не превышает константу CountMax, описанную в основном теле Функционального блока.
Пример добавленных Действия и Метода
Вызываются Действия или Методы одинаково: через точку. Если метод имеет аргументы (параметры) — то их надо передать (это могут быть прямые значения или какие-либо переменные программы):
Вызов Действий и Методов Функционального Блока (FB)
Методы могут возвращать значения.
Доработаем наш пример так, чтобы метод SetVal возвращал TRUE, если значение задание успешно, или FALSE при ошибке:
Пример Метода с возвратом значения из кода
Теперь можно вызывать этот метод или сразу же в теле условия, или передавать результат его работы в какую-то переменную.
Пример вызова Метода с возвратом значения из кода
Действия и Методы мне очень понравились. Например, когда я написал сложный диммер управления светом на CodeSys 2.3, я разбил его типовые операции на Действия:
Пример использования Действий в программе ПЛК (Диммер)
Это позволило сильно упростить код: я написал нужные участки кода один раз в действиях и вызывал их там, где мне нужно:
Один из кодов Действия для Диммера в программе ПЛК
Обратите внимание, что из одного Действия можно вызвать другое (для Методов — тоже; главное не уйти в рекурсию).
Также Действия и Методы можно добавлять и в Программы:
Возможность добавления Действий и Методов к Программам ПЛК
Это мне тоже понравилось. Например, сложную программу управления светом я разбил на Действия по каждому помещению:
Пример Программы ПЛК с добавленными действиями (разделение кода на части)
Теперь не надо было с кодом, прокручивая его до нужного места. Открыл нужное помещение — и посмотрел, что и как там делается. Удобно!
7. Особенности работы с FB Таймеров (TON, TOF, TP, BLINK).
Вот в самом начале поста я говорил о том, что сам, когда начинал программировать на Siemens Logo (все посты про них по тэгу) и других контроллерах (например ABB CL), совершал дикую ошибку. Я почему-то думал, что всякие таймеры, которые были в этих контроллерах, работают сами по себе так же, как это происходит в микроконтроллерах.
Ну, дескать, ты настроил некий «Таймер 1» на время срабатывания в 20 минут — и всё, каждые 20 минут он срабатывает (так в микроконтроллерах и есть).
Я тогда рисовал такие вот схемы (эта — для вентилятора ванной из этого поста), и думал что «всё работает само».
Различные функциональные блоки-таймеры в проекте Siemens Logo
К сожалению, точно так же это всё выглядит на графических языках (и работает «само»). Точно так же можно нарисовать такие же таймеры в OwenLogic (все посты про программируемые реле ОВЕН), «подключить» к ним «сигналы», и всё сразу будет работать:
Вызов Таймеров на графическом языке (OwenLogic)
Но как только мы переходим на текстовый язык ST, то начинаются сложности и непонятки (я дико тупил, пока не понял, как всё делать). Вот с ними мы и разберёмся.
У таймеров есть стандартные имена входов и выходов:
- IN — Вход для запуска. Таймер реагирует на импульс (R_TRIG/F_TRIG), поэтоу сигнал с входа надо когда-нибудь снимать, чтобы повторно запустить таймер;
- PT — Уставка времени работы таймера;
- Q — Выход таймера;
- ET — Прошедшее время с момента запуска таймера.
Все Таймеры — это НЕ аппаратные, а ПРОГРАММНЫЕ компоненты. Это такие же Функциональные блоки, как и написанные вами для себя самих. Более того! Если вам хочется, то вы можете написать свой таймер со своими функциями.
Вот простой алгоритм таймера задержки выключения (TOF):
- Как только пришла команда запуска — сохранить текущее время ПЛК в переменной и ВКлючить выход;
- Посчитать разницу текущего времени ПЛК и сохранённого времени;
- Если разница времени больше или равна заданной уставке времени — ВЫключить выход.
А вы помните, что надо делать для того, чтобы FB работал? Правильно: ВЫЗЫВАТЬ ЕГО В ОСНОВНОЙ ПРОГРАММЕ ПЛК! Поэтому основные ошибки работы с таймерами связаны с тем, что кажется:
- Достаточно объявить его, выставить IN := TRUE — и он сам начнёт считать.
НЕТ: Если таймер не вызывать в теле программы ПЛК, то он не будет работать! - Раз он «сам считает», то можно вызывать его в теле условия IF. Это так обычно ошибочно пишут логику «Если что-то там, то начать отсчёт времени», выставляя IN := TRUE.
НЕТ: В итоге таймер будет вызываться только в IF, и никогда не сбросится и никогда не перезапустится. А то и не досчитает, если условие запуска в IF перестанет действовать. Потому что его никто не будет вызывать. - Таймер можно использовать в Функции (FUN). Раз среда позволяет его объявить — то почему нет?
НЕТ: Ведь в Функции (FUN) переменные НЕ сохраняются — они все временные. Да, таймер — это тоже одна из переменных. Но каждый раз при вызове функции запуск таймера будет начинаться с самого начала. А после работы функции всё будет сбрасываться. Нормально работать таймеры будут только в Функциональных Блоках (FB). Поэтому, если нам нужна «функция с таймером внутри», надо переделывать её в Функциональный Блок (FB).
Правильным является вызывать таймер в теле Программы (PRG) или Функционального Блока (FB). А условия его запуска должны именять его вход «IN». Таймер при этом постоянно должен вызываться и обрабатываться!
Кстати, есть ещё одна скрытая особенность работы таймеров. Вызываются они ведь в коде какой-то задачи, которая работает с заданной периодичностью. И, если задача будет работать медленнее, чем время PT таймера, то он никогда и не посчитает ничего точно!
Ведь для него время будет идти, например, как 100, 200, 300, 400, 500 мсек. Если уставка PT у него будет равна 20 ms, то он сработает уже на следующем цикле вызова в задаче — через 100 мсек!
Вот разные способы работы с таймером.
Главное в них то, что его экземпляр ВСЕГДА вызывается в теле программы (или Функционального блока) вне всяких условий:
Вызов Таймеров на текстовом языке с передачей им параметров
- Можно вызывать таймер, сразу передавая ему все входные параметры;
- Можно задавать разные параметры в разных местах кода, а вызывать таймер потом без параметров: так как это функциональный блок — то параметры, заданные ранее, будут сохранены в нём между вызовами.
Для того, чтобы остановить таймер немедленно, надо задать ему PT, равное нулю: это прекратит отсчёт времени. Для этого можно использовать оператор SEL:
Вызов Таймеров на текстовом языке с возможностью их сброса (остановки)
8. Использование VAR_IN_OUT в сложных случаях (управление с Кнопки и Визуализации, HomeAssistant).
И напоследок у меня есть подарок (донаты и спонсорство приветствуются!): то, как хитро можно использовать переменные VAR_IN_OUT для управления чем-либо с нескольких мест. В данном посте это прям кунг-фу высшей сложности.
Переменные, которые объявлены как VAR_IN_OUT — это ссылки (указатели). Они ОБЯЗАТЕЛЬНО должны быть привязаны к переменным в программе, иначе будет или ошибка компиляции, или программа на ПЛК будет аварийно завершаться при работе (обращение к несуществующией памяти).
При обращении к FB на графическом языке эти переменные рисуются со стороны входов и имеют стрелочку в оба направления: «↔», а при обращении к FB на текстовом языке эти переменные обозначаются как входные через «:=».
Зачем они нужны? А для того, чтобы можно было одновременно управлять FB в одном месте программы и одновременно получать значение из FB в другом месте программы.
Итак, дарю вам концепт импульсного реле для управления светом, в котором есть такая переменная — Switch (выключатель).
Переменные типа VAR_IN_OUT для сложных Функциональных Блоков (FB)
Код этого FB написан таким образом: если нет нажатия на физическую кнопку, то мы переключаем импульсное реле в состояние, которое определяется переменной Switch. Если есть нажатие на кнопку, то переключаем импульсное реле по кнопке. А в конце кода FB выдаём текущее значение выхода Импульсного реле в переменную Switch:
Пример кода с переменной VAR_IN_OUT для FB Импульсного Реле
На базе этих импульсных реле я построил большой блок сразу на 6 групп света — CSLightRoom и привязал к нему переменные на эти самые выключатели:
Пример вызова FB с переменными VAR_IN_OUT на графическом языке
Теперь «выключатели» можно привязать к переключателям на экране визуализации ПЛК:
Пример привязки переменной VAR_IN_OUT на визуализации ПЛК (управление с экрана)
И тогда мы получим следующую фишку: если группа света будет переключаться кнопкой — то на экране переключатель будет показывать её состояние. А если же изменить положение переключателя на экране, то группа света будет управляться с экрана!
Я оставляю такие переменные во всех проектах (на CodeSys 2.3 фишка VAR_IN_OUT тоже есть). Вот кусочек такого проекта:
Пример использования переменных VAR_IN_OUT в текстовом языке (HomeAssistant Ready)
Можно сказать, что это делает такие проекты HomeAssistant-Ready: если получать такую переменную по Modbus, то HA покажет состояние группы света. А если изменить такую переменную из HA, то ПЛК переключит группу света. Вуаля! Про HA я расскажу как-нибудь, когда разберусь с ним (спасибо тем, кто мне сделал подгон компьютеров с ним из списка донатов)!
Проекту исполнилось 15 лет! Поддержать проект материально, проспонсировать проекты Автора или сделать ему подарок можно на этой странице: "Донаты и Спонсорство, Список Желаний".
0 Отзыв на “Что такое Функция (FUN) и Функциональный блок (FB)? В чём разница и как с ними работать (CodeSys, OwenLogic)?”