Le sucede a los controladores de programa (PLC) en un entorno CODESYS. Todos los que se han ocupado de este sistema saben que en cualquier proyecto hay una biblioteca Standard.lib, en la que se implementan temporizadores básicos, disparadores, contadores y otras funciones y bloques de funciones. Muchas de estas unidades se usan constantemente en programas PLC. Y la biblioteca en sí, como los lenguajes de programación CODESYS, es una realización del estándar IEC 61131-3, es decir. diseñado para ayudar con la programación de tareas clásicas de PLC.
Una de las características de los programas de PLC es que el ciclo del programa principal debe ejecutarse sin demoras significativas, no debe tener ciclos internos con un tiempo de salida indefinido o llamadas síncronas de funciones "pensadas", especialmente con respecto a la comunicación a través de canales lentos. Las imágenes de entrada y salida del proceso se actualizan solo en el límite del bucle principal, y cuanto más tiempo "nos sentemos" dentro de una iteración del bucle, menos sabremos sobre el estado real del objeto de control y, finalmente, el perro guardián del desbordamiento del bucle funcionará. Muchos pueden objetarme, diciendo que los PLC modernos son ambiguos, hay soporte para interrupciones de hardware. Estoy de acuerdo, pero hablar sobre tales sistemas no está incluido en mis planes, pero quiero hablar sobre (cuasi, pseudoelección) un PLC de una implementación de tarea única (sin interrupciones) basada en la plataforma de microprocesador Arduino, en la que solo hay un bucle principal. Por cierto, no estará fuera de lugar decir que el artículo
PLC CONTROLLINO compatible con Arduino, parte 1 sobre el intento de implementar Arduino en hardware, me promovió a escribir esta nota. PLC
Algunas palabras sobre Arduino. Desde el punto de vista del programador PLC, Arduino es un controlador típico con uno muy rápido o, por el contrario, un bucle muy lento (). No hay restricciones en el tiempo de ejecución del ciclo, y puede funcionar tanto una vez como un número infinito de veces, de acuerdo con el plan del programador. Cuando el programa es simple y se reduce a realizar operaciones secuenciales, controladores, sin eventos paralelos, es suficiente alternar operaciones con ciclos de verificación de condición anidadas interminables y retrasos síncronos del tipo delay (). Los pasos secuenciales de dicho programa se ejecutarán literalmente línea por línea, simple y lógicamente. Pero, tan pronto como surja la necesidad de programar operaciones paralelas, es necesario cambiar el paradigma del programa.
En un sistema de tarea única, el paralelismo visible solo puede lograrse mediante un escaneo secuencial muy rápido de estados paralelos, sin demorarse por mucho tiempo en cada llamada de función o verificación de condición. No hay problemas con las entradas y salidas físicas, las funciones funcionan lo suficientemente rápido, pero el retraso () se convierte en un freno injustificado. Y aquí entran los temporizadores sin bloqueo, los mismos que son clásicos en la programación de PLC. La conclusión es que usan un contador de tiempo de milisegundos, y todas las acciones están vinculadas a los valores de este contador global.
Y ahora recordemos el mismo Standard.lib de CODESYS. Simplemente implementó los temporizadores sin bloqueo de IEC-s. Lo tomé como base y porté las funciones de temporizadores y disparadores al código de la biblioteca Arduino (C ++). Es decir Intenté acercar a Arduino al clásico PLC.
A continuación daré una breve descripción de los bloques de funciones portadas (FB) de CODESYS y sus análogos en mi biblioteca
plcStandardLib , todos los diagramas de temporización son correctos para la nueva biblioteca Arduino. Se puede encontrar una descripción más detallada de los bloques fuente, por ejemplo, en la ayuda en ruso de CODESYS.
TON - bloque de funciones "temporizador con retraso activado"
TON(IN, PT, Q, ET)
Entradas IN y PT de los tipos BOOL y TIME respectivamente. Las salidas Q y ET son similares a los tipos BOOL y TIME. Mientras IN es FALSO, la salida Q = FALSO, la salida ET = 0. Tan pronto como IN se vuelve VERDADERO, la cuenta regresiva (en milisegundos) en la salida ET comienza a un valor igual a PT. Además, el contador no aumenta. Q es VERDADERO cuando IN es VERDADERO y ET es PT, de lo contrario es FALSO. Entonces
así, la salida Q se establece con un retraso de PT desde el borde de la entrada IN.
En el IDE de Arduino:
Opciones de anuncios:
TON TON1(); TON TON1(unsigned long PT);
Casos de uso:
Q = TON1.Run(boolean IN); // " " TON1.IN = IN; TON1.Run(); Q = TON1.Q;
Diagrama de tiempo de operación de TON:
TOF - bloque de funciones "temporizador con apagado retardado"
TOF(IN, PT, Q, ET)
Entradas IN y PT de los tipos BOOL y TIME respectivamente. Las salidas Q y ET son similares a los tipos BOOL y TIME. Si IN es VERDADERO, entonces la salida Q = VERDADERO y la salida ET = 0. Tan pronto como IN pasa a FALSO, comienza la cuenta regresiva (en milisegundos) en la salida ET. Cuando se alcanza la duración establecida, la cuenta regresiva se detiene. La salida Q es FALSA si IN es FALSA y ET es PT, de lo contrario es VERDADERO. Por lo tanto, la salida Q se restablece con un retraso PT desde la disminución de la entrada IN.
En el IDE de Arduino:
Muy similar a TON, para abreviar:
TOF TOF1(unsigned long PT);
Diagrama de tiempo de TOF:
TP - bloque de función de temporizador de impulso
TP(IN, PT, Q, ET)
Entradas IN y PT de los tipos BOOL y TIME respectivamente. Las salidas Q y ET son similares a los tipos BOOL y TIME. Mientras IN es FALSO, salida Q = FALSO, salida ET = 0. Cuando IN se cambia a VERDADERO, la salida Q se establece en VERDADERO y el temporizador comienza a contar (en milisegundos) en la salida ET hasta que se alcanza la duración especificada por PT. Además, el contador no aumenta. Por lo tanto, la salida Q genera un pulso de duración PT a lo largo del borde de la entrada IN.
En el IDE de Arduino:
Muy similar a TON, para abreviar:
TP TP1(unsigned long PT);
Cuadro de tiempo de operación TP:
R_TRIG - bloque de funciones "detector frontal"
El bloque de funciones R_TRIG genera un pulso en el borde ascendente de la señal de entrada. La salida Q es FALSA siempre que la entrada CLK sea FALSA. Tan pronto como el CLK se convierte en VERDADERO, Q se establece en VERDADERO. La próxima vez que se llama al bloque de funciones, la salida se restablece a FALSE. Por lo tanto, el bloque emite un impulso único en cada transición CLK de FALSO a VERDADERO.
Ejemplo CODEDESYS en ST:
RTRIGInst : R_TRIG ; RTRIGInst(CLK:= VarBOOL1); VarBOOL2 := RTRIGInst.Q;
En el IDE de Arduino:
Anuncio:
R_TRIG R_TRIG1;
Casos de uso:
Q = R_TRIG1.Run(boolean CLK);
F_TRIG - unidad de función "detector de disminución"
El bloque de funciones F_TRIG genera un pulso en el borde posterior de la señal de entrada.
La salida Q es FALSA hasta que la entrada CLK sea VERDADERA. Tan pronto como CLK se establece en FALSE, Q se establece en TRUE. La próxima vez que se llama al bloque de funciones, la salida se restablece a FALSE. Por lo tanto, el bloque emite un solo impulso en cada transición CLK de VERDADERO a FALSO.
En el IDE de Arduino:
F_TRIG F_TRIG1; Q = F_TRIG1.Run(boolean CLK);
RS_TRIG - bloque de funciones RS-trigger / SR_TRIG - bloque de funciones SR-trigger
Cambie con el interruptor dominante apagado, RS-trigger:
Q1 = RS (SET, RESET1)
Cambiar con dominante en:
Q1 = SR (SET1, RESET)
Las variables de entrada SET y RESET1 son las mismas que la variable de salida Q1 de tipo BOOL.
En el IDE de Arduino:
RS_TRIG RS_TRIG1; Q = RS_TRIG1.Run(boolean SET, boolean RESET); // " "
SR_TRIG SR_TRIG1; Q = SR_TRIG1.Run(boolean SET, boolean RESET); // " "
Código fuente y ejemplo
plcStandardLib_1.h #ifndef PLCSTANDARDLIB_1_H_ #define PLCSTANDARDLIB_1_H_ #if ARDUINO >= 100 #include <Arduino.h> #else #include <WProgram.h> #endif 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; }; 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; }; 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; }; class R_TRIG // { public: R_TRIG(); boolean Run(boolean CLK); boolean CLK; // boolean Q; // private: boolean _M; // }; class F_TRIG // { public: F_TRIG(); boolean Run(boolean CLK); boolean CLK; // boolean Q; // private: boolean _M; // }; class RS_TRIG // { public: RS_TRIG(); boolean Run(); boolean Run(boolean SET, boolean RESET); boolean SET; // boolean RESET; // boolean Q; // //private: }; class SR_TRIG // { public: SR_TRIG(); boolean Run(); boolean Run(boolean SET, boolean RESET); boolean SET; // boolean RESET; // boolean Q; // //private: }; #endif
plcStandardLib_1.cpp #include "plcStandardLib_1.h" 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() { 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() { 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() { 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() { 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() { 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);
Por ejemplo, para filtrar la charla de los contactos del botón (cuando se abre también), este código es suficiente:
FiltredButtonIn = TON1.Run(digitalRead(ButtonIn))
En conclusión: así es como CODESYS se parece a un generador de impulsos basado en una cadena de temporizadores TON y TP. Al principio, TON está cubierto por retroalimentación con inversión, y a partir de él se genera un solo generador de pulso, que inicia la operación del generador de pulso TP. En mi ejemplo de Arduino, un análogo de esto se ve así:
digitalWrite(LED, TP2.Run(TON2.Run(!TON2.Q)))