Здравствуйте, коллеги. Сегодня мы с вами познакомимся с организацией работы при ограниченных ресурсах. Рассмотрим что такое «Очередь» как она работает, реализуем пару вариантов очередей, затронем динамическое выделение памяти в ПЛК.
Я пишу эту статью и программирую в режиме реального времени, т.е. у меня есть задача и я ее решаю. Все свои мысли и попытки(за исключением очень глупых) я записываю в эту статью так что приятного чтения.
ТЕОРИЯ
Очередь — это абстрактный тип данных, в котором доступ организуется по принципу FIFO(first in, first out) — первый пришел, первый ушел. Имеет два метода: добавить в очередь и достать из очереди. Данные которые мы достали из очереди — удаляются.
Согласно теории есть два метода реализации — массивом и односвязным списком. В большинстве ПЛК второй способ невозможен, но в Codesys 3 можно чуть чуть поиграть с динамическим выделением памяти.
РЕАЛИЗАЦИЯ
Реализация для одного типа
Начнем с малого. Реализуем очередь для одного типа данных. Например, мы будем вырезать отверстия.
Создание пользовательского типа данных
Наш пользовательский тип данных будет называться udtHole и будет включать в себя координаты по X и Y и диаметр отверстия.
TYPE udtHole :
STRUCT
rX:REAL;//Координата по X
rY:REAL;//Координата по Y
iD:INT;//Размер диаметра
END_STRUCT
END_TYPE
Функциональный блок
Теперь организуем список необходимых операций, которые нам понадобятся. Всего их три: Ожидание, Добавить элемент, Достать элемент. Для такого типа операций я использую перечисление и CASE в реализации. Мне просто так удобнее. Можно с помощью флагов, триггеров, IF’ов, но я делаю так.
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE eFifoOperation :
(
WAIT := 0, //Ожидание
ENQUEUE :=10, //Добавить в очередь
DEQUEUE :=20 //Достать из очереди
);
END_TYPE
Теперь работаем с FB. Пытаемся уместить все в одном месте.
Нам потребуется на вход:
- Указатель на Глобальный UDT для чтения
- Указатель на UDT куда будем писать.
- Команды управления
На выход у нас получается:
- Сигнал об успешной записи в очередь
- Сигнал об успешном чтении из очереди
- Очередь пустая
- Очередь полная
- В работе
Пункты 3 и 4 можно объединить в ошибку и сделать какой-нибудь код для нее, но я не буду.
Под капотом:
- Массив элементов( для теста 10)
- Индекс старта
- Индекс окончания
FUNCTION_BLOCK fbFifo
VAR_INPUT
udEnqueuedData: POINTER TO udtHole; //Данные которые будут добавлены в очередь
udDequeueData: POINTER TO udtHole; //Данные считанные из очереди
enCommand: POINTER TO eFifoOperation; //Команда
END_VAR
VAR_OUTPUT
xEnqueueDone: BOOL; //Добавление в очередь прошло успешно
xDequeueDone: BOOL; //Изъятье из очереди прошло успешно
xBusy: BOOL; //В Процессе
xQueueEmpty: BOOL; //Очередь пуста
xQueueFull: BOOL; //Полная очередь
xError: BOOL; //Ошибка
END_VAR
VAR
udData: ARRAY[0..9] OF udtHOLE; //Массив
iStart: INT:=0; // Индекс чтения
iEnd: INT:=0; //Индекс записи
xEmpty: BOOL:=TRUE; //Массив пуст
xFull: BOOL; //Массив полон
END_VAR
Логика работы
Логика легка и проста. читаем из элемента под индексом iStart. Считали прибавили 1. Записываем в индекс iEnd. Записали прибавили единицу.
Дошли до конца, скинули счетчик. Пошли по кругу. Всего может быть 10 элементов. Если индексы пересекаются, то это неопределенность. Либо очередь полная, либо пустая. Так что этот вопрос придется контролировать.
ДОБАВЛЕНИЕ/ИСПРАВЛЕНИЕ
При старте реализации было обнаружено, что значения пустой и полной очереди необходимо обрабатывать внутри кода. Есть два три выхода: переносим в In_Out, переносим указателями в In, создаем в области VAR и во время WAIT копируем значения.
Я выбираю третий вариант с дублированием.
Добавил в Output переменную Error.
В итоге получилось следующее
CASE enCommand^ OF
(*
Стадия ожидания команды.
Производится сбрасывание индексов для кругового обхода
И перекидыввание внутренних значений на переменныйе выхода.
*)
eFifoOperation.WAIT:
IF iStart>9 THEN
iStart:=0;
END_IF
IF iEnd>9 THEN
iEnd:=0;
END_IF
xQueueEmpty:=xEmpty;
xQueueFull:=xFull;
(*
Добавление в очередь. Если очередь не полная.
Формирование сигнала о том, что очередь заполнена
Формирование ошибки при записи в заполненную очередь
*)
eFifoOperation.ENQUEUE:
xBusy:=TRUE;
xEnqueueDone:=FALSE;
IF NOT xFull THEN
udData[iEnd] := udEnqueuedData^;
iEnd:=iEnd+1;
IF iStart = iEnd OR (iStart=0 AND iEnd=10) THEN
xFull:=TRUE;
END_IF;
IF xEmpty THEN
xEmpty:=FALSE;
xError:=FALSE;
END_IF;
xBusy:=FALSE;
xEnqueueDone:=TRUE;
ELSE
xBusy:=FALSE;
xError:=TRUE;
END_IF;
enCommand^:=eFifoOperation.WAIT;
(*
Изъятие из очереди. Если очередь не пустая.
Формирование сигнала о том, что очередь пустая
Формирование ошибки при чтении из пустой очереди очередь
*)
eFifoOperation.DEQUEUE:
xBusy:=TRUE;
xDequeueDone:=FALSE;
IF NOT xEmpty THEN
udDequeueData^:=udData[iStart];
iStart:=iStart+1;
IF iStart = iEnd OR (iStart =10 AND iEnd=0) THEN
xEmpty:=TRUE;
END_IF
IF xFull THEN
xFull:=FALSE;
xError:=FALSE;
END_IF
xBusy:=FALSE;
xDequeueDone:=TRUE;
ELSE
xBusy:=FALSE;
xError:=TRUE;
END_IF;
enCommand^:=eFifoOperation.WAIT;
END_CASE
Ну и небольшая демонстрация работы
Вопросы в комментарии или на почту info@engcore.ru
