Случается программировать контроллеры (ПЛК) в среде 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)));

Поделиться с друзьями
-->

Комментарии (6)


  1. alexhott
    06.04.2017 20:44
    +1

    в ардуино ид изначально есть примеры не тормозящего счетчика для мигания светодиодом.
    реализуется легко в одной строке с условием и суммирование в переменную
    делал так во всех проектах, никогда делай не использовал.
    У меня даже была индикация на 7-и сегментник и конечный автомат в одном цикле и прекрасно работало
    задержку дребезга делал примерно так
    if ((PINC&(1<<PC5))==0x00){butt1++; if(butt1>200){butt1=201;}} else {butt1=0;}
    if(butt1==200 ){тут чего-то делаем; butt1=201;}
    и тут сразу отработка действия четко один раз
    примерно таким же макаром удалось работать с энкодером без левых срабатываний


    если кнопок много то библиотека конечно размер кода сократит.


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


  1. totuin
    07.04.2017 10:39

    Счётчик миллисекунд в ограничен разрядностью, и через какое то время сбросится в 0. Тут то эти таймеры и сглючат. Это реальная ситуация. Я уже четыре года занимаюсь проектом по действительному переводу ардуино на языки ПЛК. В первый год у меня были таймеры по такому же принципу. Было много реальных ошибок на устройствах собранных пользователями. Решается введением нескольких строчек.

    bool _isTimer(unsigned long startTime, unsigned long period )
    {
      unsigned long currentTime;
      currentTime = millis();
      if (currentTime >= startTime) {
        return (currentTime >= (startTime + period));
      }else {
        return (currentTime >= (4294967295 - startTime + period));
      }
    }
    


    1. Reegool
      12.04.2017 12:38

      От сброса счетчика миллисекунд существует простой, известный большинству ардуинщиков рецепт:

      unsigned long tm = millis();
      unsigned long elapsed = (tm >= previousMillis) ? tm - previousMillis : 0xFFFFFFFF - previousMillis + tm + 1;
      if( elapsed > interval ) {
        .......
      }
      

      Простой контроль переполнения. И ничего не сглючит.


      1. totuin
        12.04.2017 13:20

        Это Ваш вариант (в стиле — убейте мозг), у меня свой (который делает то же самое), но более читаемый. Каждый программист — как художник — видит по своему))))


    1. ajiekceu2691
      12.04.2017 12:38
      +1

      Не проще писать сразу так if (millis()-startTime>period)?


    1. Chupakabra303
      12.04.2017 15:10

      Ваш код защищает не от переполнения счетчика блока таймера как такового (он начинает тикать от startTime), а от смены знака при переходе счетчика millis() через 0, но ведь используемые беззнаковые типы данных unsigned long как раз позволяют производить вычитание на границе 0xFF… 0x0 без разрыва, мой код позволяет это делать:
      ET = millis() — _StartTime; // вычисляем время
      if (ET >= PT)
      А от 50-дневного переполнения таймер конечно не защищен, но от него это и не требуется. Когда такие интервалы программируются лучше (надежнее и точнее) использовать RTC.