Очередь на ПЛК. Codesys v3.5

Здравствуйте, коллеги. Сегодня мы с вами познакомимся с организацией работы при ограниченных ресурсах. Рассмотрим что такое «Очередь» как она работает, реализуем пару вариантов очередей, затронем динамическое выделение памяти в ПЛК.

Я пишу эту статью и программирую в режиме реального времени, т.е. у меня есть задача и я ее решаю. Все свои мысли и попытки(за исключением очень глупых) я записываю в эту статью так что приятного чтения.

ТЕОРИЯ

Очередь — это абстрактный тип данных, в котором доступ организуется по принципу 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. Пытаемся уместить все в одном месте.

Нам потребуется на вход:

  1. Указатель на Глобальный UDT для чтения
  2. Указатель на UDT куда будем писать.
  3. Команды управления

На выход у нас получается:

  1. Сигнал об успешной записи в очередь
  2. Сигнал об успешном чтении из очереди
  3. Очередь пустая
  4. Очередь полная
  5. В работе

Пункты 3 и 4 можно объединить в ошибку и сделать какой-нибудь код для нее, но я не буду.

Под капотом:

  1. Массив элементов( для теста 10)
  2. Индекс старта
  3. Индекс окончания
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

Ответить