Как стать автором
Обновить

Таймеры и триггеры CODESYS. Еще один шаг Arduino к классическому ПЛК

Время на прочтение 8 мин
Количество просмотров 83K

Случается программировать контроллеры (ПЛК) в среде CODESYS. Все, кто имел дело с этой системой, знают, что в любом проекте присутствует библиотека Standard.lib, в которой реализованы базовые таймеры, триггеры, счетчики и некоторое кол-во других функций и функциональных блоков. Многие из этих блоков постоянно используются в программах для ПЛК. А сама библиотека, как и языки программирования CODESYS, является воплощением стандарта IEC 61131-3, т.е. призвана помочь при программировании классических ПЛК задач.

Одна из особенностей программ для ПЛК в том, что основной цикл программы должен выполняться без существенных задержек, в нем не должно быть внутренних циклов с неопределенным временем выхода или синхронных вызовов «задумчивых» функций, особенно это касается коммуникаций по медленным каналам. Обновление входных и выходным образов процесса происходит только на границе основного цикла, и чем дольше мы будем «сидеть» внутри одной итерации цикла, тем меньше мы будет знать о реальном состоянии объекта управления, в конечном итоге сработает watchdog переполнения времени выполнения цикла. Многие могут мне возразить, сказав, что современные ПЛК многозначны, есть с поддержкой аппаратных прерываний. Согласен, но разговор о таких системах не входит в мои планы, я же хочу поговорить о (квази, псевдо — выбирайте) ПЛК однозадачной реализации (без прерываний) на базе микропроцессорной платформы Arduino, в котором есть только один основной цикл. Кстати, не лишним будет сказать, что на написание данной заметки меня сподвигла статья Ардуино-совместимый ПЛК CONTROLLINO, часть 1 о попытке аппаратного воплощения Arduino в пром. ПЛК.

Несколько слов об Arduino. С точки зрения программиста ПЛК, Arduino — это типичный контроллер с одним очень быстрым или, наоборот, очень медленным циклом loop(). На время выполнения цикла не накладывается никаких ограничений, и он может отработать и один, и бесконечное кол-во раз — по замыслу программиста. Когда программа проста и сводится к выполнению последовательных операций, регуляторов, без параллельных событий, то достаточно чередовать операции бесконечными вложенными циклами проверки условий и синхронными задержками типа delay(). Последовательные шаги такой программы будут выполняться буквально построчно, просто и логично. Но, как только возникает необходимость в программировании параллельных операций, необходимо менять парадигму программы.

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

А теперь давайте вспомним ту самую Standard.lib из CODESYS. В ней как раз реализованы МЭК-овские неблокирующие таймеры. Я взял ее за основу и портировал функции таймеров и триггеров в библиотечный код Arduino (С++). Т.е. попытался приблизить Arduino к классическому ПЛК.

Ниже я приведу краткое описание портированных функциональных блоков (FB) CODESYS и их аналоги в моей библиотеке plcStandardLib, все временные диаграммы верны для новой библиотеки Arduino. Подробнее описание исходных блоков можно посмотреть, например, в русскоязычной справке по CODESYS.

TON — функциональный блок «таймер с задержкой включения»


TON(IN, PT, Q, ET)

Входы IN и PT типов BOOL и TIME соответственно. Выходы Q и ET аналогично типов BOOL и TIME. Пока IN равен FALSE, выход Q = FALSE, выход ET = 0. Как только IN становится TRUE, начинается отсчет времени (в миллисекундах) на выходе ET до значения, равного PT. Далее счетчик не увеличивается. Q равен TRUE, когда IN равен TRUE, а ET равен PT, иначе FALSE. Таким
образом, выход Q устанавливается с задержкой PT от фронта входа IN.

В Arduino IDE:


Варианты объявлений:

TON TON1();
TON TON1(unsigned long PT); // с заданием интервала времени PT

Варианты использования:

Q = TON1.Run(boolean IN); // вызов "все в одном"
TON1.IN = IN;
TON1.Run();
Q = TON1.Q;

Временная диаграмма работы TON:


TOF — функциональный блок «таймер с задержкой выключения»


TOF(IN, PT, Q, ET)

Входы IN и PT типов BOOL и TIME соответственно. Выходы Q и ET аналогично типов BOOL и TIME. Если IN равен TRUE, то выход Q = TRUE и выход ET = 0. Как только IN переходит в FALSE, начинается отсчет времени (в миллисекундах) на выходе ET. При достижении заданной длительности отсчет останавливается. Выход Q равен FALSE, если IN равен FALSE и ET равен PT, иначе — TRUE. Таким образом, выход Q сбрасывается с задержкой PT от спада входа IN.

В Arduino IDE:


Очень похоже на TON, для краткости:

TOF TOF1(unsigned long PT); // с заданием интервала времени PT
Q = TOF1.Run(boolean IN); // вызов "все в одном"

Временная диаграмма работы TOF:


TP — функциональный блок «импульс-таймер»


TP(IN, PT, Q, ET)

Входы IN и PT типов BOOL и TIME соответственно. Выходы Q и ET аналогично типов BOOL и TIME. Пока IN равен FALSE, выход Q = FALSE, выход ET = 0. При переходе IN в TRUE выход Q устанавливается в TRUE и таймер начинает отсчет времени (в миллисекундах) на выходе ET до достижения длительности, заданной PT. Далее счетчик не увеличивается. Таким образом, выход Q генерирует импульс длительностью PT по фронту входа IN.

В Arduino IDE:


Очень похоже на TON, для краткости:

TP TP1(unsigned long PT); // с заданием интервала времени PT
Q = TP1.Run(boolean IN); // вызов "все в одном"

Временная диаграмма работы TP:


R_TRIG — функциональный блок «дeтектор фронта»


Функциональный блок R_TRIG генерирует импульс по переднему фронту входного сигнала. Выход Q равен FALSE до тех пор, пока вход CLK равен FALSE. Как только CLK получает значение TRUE, Q устанавливается в TRUE. При следующем вызове функционального блока выход сбрасывается в FALSE. Таким образом, блок выдает единичный импульс при каждом переходе CLK из FALSE в TRUE.

Пример CODEDESYS на языке ST:

RTRIGInst : R_TRIG ;
RTRIGInst(CLK:= VarBOOL1);
VarBOOL2 := RTRIGInst.Q;

В Arduino IDE:


Объявление:

R_TRIG R_TRIG1;

Варианты использования:

Q = R_TRIG1.Run(boolean CLK); // вызов "все в одном"
R_TRIG1.CLK = CLK;
R_TRIG1.Run();
Q = R_TRIG1.Q;

F_TRIG — функциональный блок «дeтектор спада»


Функциональный блок F_TRIG генерирует импульс по заднему фронту входного сигнала.
Выход Q равен FALSE до тех пор, пока вход CLK равен TRUE. Как только CLK получает значение FALSE, Q устанавливается в TRUE. При следующем вызове функционального блока выход сбрасывается в FALSE. Таким образом, блок выдает единичный импульс при каждом переходе CLK из TRUE в FALSE.

В Arduino IDE:


F_TRIG F_TRIG1;
Q = F_TRIG1.Run(boolean CLK); // вызов "все в одном"

RS_TRIG — функциональный блок RS-триггер / SR_TRIG — функциональный блок SR-триггер


Переключатель с доминантой выключения, RS-триггер:

Q1 = RS (SET, RESET1)

Переключатель с доминантой включения:

Q1 = SR (SET1, RESET)

Входные переменные SET и RESET1 — как и выходная переменная Q1 типа BOOL.

В Arduino IDE:


RS_TRIG RS_TRIG1;
Q = RS_TRIG1.Run(boolean SET, boolean RESET); // вызов "все в одном"

SR_TRIG SR_TRIG1;
Q = SR_TRIG1.Run(boolean SET, boolean RESET); // вызов "все в одном"

Исходный код и пример


plcStandardLib_1.h
/*
 * plcStandardLib_1.h
 *
 * Created on: 01.01.2017
 * Author: Admin
 */

#ifndef PLCSTANDARDLIB_1_H_
#define PLCSTANDARDLIB_1_H_

#if ARDUINO >= 100
#include <Arduino.h>
#else
#include <WProgram.h>
#endif

/* ------------------- TON ------------------- */
class TON
{
public:
	TON();
	TON(unsigned long PT);
	boolean Run(boolean IN);
	boolean Q; // выходная переменная
	boolean IN; // входная переменная
	unsigned long PT; // входная переменная
	unsigned long ET; // выходная переменная - текущее значение таймера
private:
	boolean _M; // внутренний флаг
	unsigned long _StartTime;
};

/* ------------------- TOF ------------------- */
class TOF
{
public:
	TOF();
	TOF(unsigned long PT);
	boolean Run(boolean IN);
	boolean Q; // выходная переменная
	boolean IN; // входная переменная
	unsigned long PT; // входная переменная
	unsigned long ET; // выходная переменная - текущее значение таймера
private:
	boolean _M; // внутренний флаг
	unsigned long _StartTime;
};

/* ------------------- TP ------------------- */
class TP
{
public:
	TP();
	TP(unsigned long PT);
	boolean Run(boolean IN);
	boolean Q; // выходная переменная
	boolean IN; // входная переменная
	unsigned long PT; // входная переменная
	unsigned long ET; // выходная переменная - текущее значение таймера
private:
	boolean _M; // внутренний флаг
	unsigned long _StartTime;
};

/* ------------------- R_TRIG ------------------- */
class R_TRIG // детектор фронта сигнала
{
public:
	R_TRIG();
	boolean Run(boolean CLK);
	boolean CLK; // входная переменная
	boolean Q; // выходная переменная
private:
	boolean _M; // внутренний флаг
};

/* ------------------- F_TRIG ------------------- */
class F_TRIG // детектор спада сигнала
{
public:
	F_TRIG();
	boolean Run(boolean CLK);
	boolean CLK; // входная переменная
	boolean Q; // выходная переменная
private:
	boolean _M; // внутренний флаг
};

/* ------------------- RS_TRIG ------------------- */
class RS_TRIG // детектор спада сигнала
{
public:
	RS_TRIG();
	boolean Run();
	boolean Run(boolean SET, boolean RESET);
	boolean SET; // установка триггера
	boolean RESET; // сброс триггера
	boolean Q; // выходная переменная
//private:
};

/* ------------------- SR_TRIG ------------------- */
class SR_TRIG // детектор спада сигнала
{
public:
	SR_TRIG();
	boolean Run();
	boolean Run(boolean SET, boolean RESET);
	boolean SET; // установка триггера
	boolean RESET; // сброс триггера
	boolean Q; // выходная переменная
//private:
};

#endif /* PLCSTANDARDLIB_H_ */


plcStandardLib_1.cpp
/*
 * plcStandardLib_1.h
 *
 * Created on: 01.01.2017
 * Author: Admin
 */

#include "plcStandardLib_1.h"

/* ------------------- TON ------------------- */
TON::TON()
{
	IN = false;
	PT = 0;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}
TON::TON(unsigned long PT)
{
	IN = false;
	TON::PT = PT;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}

boolean TON::Run(boolean IN)
{
	TON::IN = IN;
	if (!TON::IN) {
		Q = false;
		ET = 0;
		_M = false;
	} else {
		if (!_M) {
			_M = true; // взводим флаг М
			_StartTime = millis();
			// ET = 0; // сразу = 0
		} else {
			if (!Q)
				ET = millis() - _StartTime; // вычисляем время
		}
		if (ET >= PT)
			Q = true;
	}
	return Q;
}

/* ------------------- TOF ------------------- */
TOF::TOF()
{
	IN = false;
	PT = 0;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}

TOF::TOF(unsigned long PT)
{
	IN = false;
	TOF::PT = PT;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}

boolean TOF::Run(boolean IN)
{
	TOF::IN = IN;
	if (TOF::IN) {
		Q = true;
		ET = 0;
		_M = true;
	} else {
		if (_M) {
			_M = false; // сбрасываем флаг М
			_StartTime = millis();
			// ET = 0; // сразу = 0
		} else {
			if (Q)
				ET = millis() - _StartTime; // вычисляем время
		}
		if (ET >= PT)
			Q = false;
	}
	return Q;
}

/* ------------------- TP ------------------- */
TP::TP()
{
	IN = false;
	PT = 0;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}
TP::TP(unsigned long PT)
{
	IN = false;
	TP::PT = PT;
	_M = false;
	_StartTime = 0;
	Q = false;
	ET = 0;
}
boolean TP::Run(boolean IN)
{
	TP::IN = IN;
	if (!_M) {
		if (TP::IN) {
			_M = true; // взводим флаг М
			_StartTime = millis();
			if (ET < PT)
				Q = true;
		}
	} else {
		if (Q) {
			ET = millis() - _StartTime; // вычисляем время
			if (ET >= PT)
				Q = false;
		} else {
			if (!TP::IN) {
				_M = false;
				ET = 0;
			}
		}
	}
	return Q;
}

/* ------------------- R_TRIG ------------------- */
R_TRIG::R_TRIG()
{
	CLK = false;
	_M = false;
	Q = false;
}

boolean R_TRIG::Run(boolean CLK)
{
	R_TRIG::CLK = CLK;
	Q = R_TRIG::CLK && !_M;
	_M = R_TRIG::CLK;
	return Q;
}

F_TRIG::F_TRIG()
{
	CLK = false;
	_M = true;
	Q = false;
}

boolean F_TRIG::Run(boolean CLK)
{
	F_TRIG::CLK = CLK;
	Q = !F_TRIG::CLK && !_M;
	_M = !F_TRIG::CLK;
	return Q;
}

/* ------------------- RS_TRIG ------------------- */
RS_TRIG::RS_TRIG()
{
	SET = false;
	RESET = false;
	Q = false;
}

boolean RS_TRIG::Run(boolean SET, boolean RESET)
{
	RS_TRIG::SET = SET;
	RS_TRIG::RESET = RESET;
	Q = !RESET and (SET or Q);
	return Q;
}

boolean RS_TRIG::Run()
{
	Q = !RESET and (SET or Q);
	return Q;
}

/* ------------------- SR_TRIG ------------------- */
SR_TRIG::SR_TRIG()
{
	SET = false;
	RESET = false;
	Q = false;
}

boolean SR_TRIG::Run(boolean SET, boolean RESET)
{
	SR_TRIG::SET = SET;
	SR_TRIG::RESET = RESET;
	Q = SET or (!RESET and Q);
	return Q;
}

boolean SR_TRIG::Run()
{
	Q = SET or (!RESET and Q);
	return Q;
}


plcStandardLib_1_example.ino
#include "plcStandardLib_1.h"

#define LED 13
#define ButtonIn 7

TON TON1(500); // Инициализация задержки включения, 500мс.
TON TON2(1000); // Инициализация задержки включения, 1000мс.
TOF TOF1(500); // Инициализация задержки выключения, 500мс.

TP TP1(300); // Инициализация единичного импульса, 300мс.
TP TP2(200); // Инициализация единичного импульса, 200мс.

R_TRIG R_TRIG1; // Инициализация триггера фронта для кнопки

void setup() {
  pinMode(ButtonIn, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, TP1.Run(R_TRIG1.Run(TON1.Run(digitalRead(ButtonIn)))));
  // TON1 - фильтрует дребезг контакта
  // R_TRIG1 - детектирует фронт сигнала
  // TP1 - генерирует импульс по фронту
  
  digitalWrite(LED, TP2.Run(TON2.Run(!TON2.Q))); // генератор импульса на базе TON и TP
  // TON2.Run(!TON2.Q)) - генератор единичного импульса
  // TP2 - генерирует импульс по фронту
  
  digitalWrite(LED, TOF1.Run(TON1.Run(digitalRead(ButtonIn)))); // Задержка включения и отключения
}


Например, чтобы отфильтровать дребезг контактов кнопки (при размыкании тоже!) достаточно вот такого кода:

FiltredButtonIn = TON1.Run(digitalRead(ButtonIn))

В качестве заключения: вот так в CODESYS выглядит работа генератора импульса на базе цепочки таймеров TON и TP. В начале TON охватывается обратной связью с инверсией, и из него получается генератор единичного импульса, который запускает работу импульс-генератора TP. В моем примере Arduino аналог этого выглядит так:

digitalWrite(LED, TP2.Run(TON2.Run(!TON2.Q)));

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+8
Комментарии 6
Комментарии Комментарии 6

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн