Случается программировать контроллеры (ПЛК) в среде 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_TRIG1Run(boolean SET, boolean RESET); // вызов "все в одном"
SR_TRIG SR_TRIG1; Q = SR_TRIG1Run(boolean SET, boolean RESET); // вызов "все в одном"
Исходный код и пример
/* * 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.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; }
#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)));
ссылка на оригинал статьи https://geektimes.ru/post/286870/
Добавить комментарий