SINTONIZAR PID CON ARDUINO – Laboratorio de Control de Temperatura (TCLab)
Introducción
El Laboratorio de Control de Temperatura (TCLab) es una pequeña placa compatible con Arduino creada por John D. Hedengren y la comunidad APMonitor. Esta placa está diseñada para fines didácticos y después de probarla os puedo asegurar que es una virguería para los amantes del control. La placa en sí es muy sencilla, consta de dos sensores de temperatura y dos transistores usados como calentadores, lo realmente interesante es el software que lo acompaña ya que tenemos a nuestra disposición diferentes desarrollos en Matlab, Simulink y Python que van desde un simple PID hasta un controlador MPC no lineal, en definitiva, una maravilla con un potencial didáctico enorme.
En esta primera entrada vamos a obviar el software que acompaña a la placa usando mis propios desarrollos tanto en el firmware de Arduino como en el software de recolección de datos.
Características
La placa está compuesta por dos sensores de temperatura TMP36 con un rango de -40 a 150ºC y dos transistores TIP31C usados como calentadores. La transferencia entre sensor y calentador se da mediante conducción, convección y radiación, y como más adelante comprobaremos en la identificación, al estar tan juntas se afectan mucho entre sí.
Sensor y calentador están unidos por una pintura térmica de color gris que se vuelve rosa a medida que aumenta la temperatura. En la imagen que se puede ver a continuación T2 estaba aproximadamente a 50ºC.
Identificación
Con el objetivo de implementar dos PIDs vamos a realizar un StepTest a cada temperatura para empezar a sacar conclusiones. En fucsia vemos la temperatura T1 a la que se está realizando el test (ºC), en amarillo la salida PWM del calentador 1 y en azul la temperatura T2 (ºC).
La primera conclusión a la que he llegado es que hay que limitar de alguna manera que los calentadores se acerquen a los 100ºC. Para ello en Manual limitaremos la salida PWM a 190 y en Automático el máximo punto de consigna será de75ºC.
La segunda conclusión es que hay mucha interrelación entre temperaturas de modo que si queremos controlar las dos temperaturas en determinadas situaciones no vamos a poder cumplir con los puntos de consigna.
En cuanto a la identificación, los resultados son similares tanto en saltos positivos como negativos, quedándonos con las constantes halladas de un salto positivo intermedio.
T1 | T2 | |
K(%/%) | 0.380 | 0.405 |
T0(s) | 29.5 | 20 |
Tp(s) | 100.5 | 117 |
Control PID con una temperatura
El control de una sola temperatura es muy sencillo. A continuación se muestra la respuesta de tres sintonías aunque todas las probadas respondieron adecuadamente.
CC-25 (PI)
Sintonía Cohen-Coon con una razón de amortiguamiento del 25%.
La simulación se corresponde con el primer cambio de Set Point.
ZN-10 (PI)
Sintonía Ziegler-Nichols con una razón de amortiguamiento del 10%. La razón de amortiguamiento es muy pequeña, casi imperceptible (0.1ºC).
La simulación se corresponde con el primer cambio de Set Point.
LAMBDA (PI)
La simulación se corresponde con el primer cambio de Set Point.
Control PID con dos temperaturas
Para la prueba se ha sintonizado T1 con ZN-10 (PI) (Ziegler-Nichols con una razón de amortiguamiento del 10%) que está diseñada para comportarse mejor ante perturbaciones que ante cambios de Set Point. T2 se ha sintonizado con IAE-SP (PI), diseñada para minimizar la integral del valor absoluto del error y comportarse de forma equilibrada ante cambios de Set Point aunque con cierto sobre impulso. Al inicio de la prueba se sitúan las temperaturas T1 y T2 en 40 y 42ºC respectivamente y a partir de ahí se realizan una serie de cambios de Set Point. Como se aprecia en la imagen, las sintonías se comportan según lo esperado provocándose perturbaciones entre sí.
Código Arduino
He aprovechado para actualizar Arduino COM Plotter para poder visualizar ambas temperaturas de forma separada pero simultáneamente. También se ha implementado la posibilidad de enviar comandos vía serial para poder hacer cambios de Set Point entre otras cosas. A continuación un resumen de los comandos implementados:
- T1M – Pone el PID de la temperatura 1 en Manual. Al hacer esto sólo podemos cambiar la OP (salida PWM). Al arrancar Arduino arranca en éste modo. En Manual el Set Point y la PV marcan lo mismo (PV Tracking) para evitar problemas al pasar a Automático.
- T1A – Pone el PID de la temperatura 1 a Automático. En éste modo sólo podemos alterar el Set Point limitado a un máximo de 75ºC.
- T1OP X – Permite cambiar la salida PWM de la temperatura 1 siendo X un valor entre 0 y 190. Sólo si el controlador está en Manual.
- T1SP X – Permite cambiar el Set Point de la temperatura 1 siendo X un valor entre 0 y 75ºC. Sólo si el controlador está en automático.
- T1KC X – Permite cambiar la constante proporcional del controlador PID de la temperatura 1.
- T1KI X – Permite cambiar la constante integral del controlador PID de la temperatura 1.
- T1KD X – Permite cambiar la constante derivativa del controlador PID de la temperatura 1.
- SST1 – Realiza un Step Test de la temperatura 1. Para ello pone ambos controladores en Manual, su salida a 0, espera a que la temperatura a la que se va a realizar el test esté por debajo de 30ºC y a continuación comienza el test. El test dura unos 30 minutos.
/*************************************************************** * TCLAB - Control PID v0.2 * Desarrollado por Garikoitz Martínez [garikoitz.info] [01/2023] * https://garikoitz.info/blog/?p=1923 ***************************************************************/ /*************************************************************** * Librerías ***************************************************************/ #include <PID_v1.h> /*************************************************************** * Variables ***************************************************************/ int T1pin = 0; int T2pin = 2; int H1pin = 3; int H2pin = 5; float T1_PV = 0.0; float T2_PV = 0.0; float T1_SP = 0.0; float T2_SP = 0.0; int T1_OP = 0; int T2_OP = 0; int T1SPMAX = 75; int T2SPMAX = 75; int T1OPMAX = 190; int T2OPMAX = 190; const long baud = 9600; // serial baud rate unsigned long previousMillis = 0; int Ts = 50; //Sample time in ms int contador = 0; int n = 15; //Promedio lecturas boolean newData = false; const char sp = ' '; const char nl = '\n'; // global variables char Buffer[64]; int buffer_index = 0; String cmd; float val; //PID double SetpointT1, InputT1, OutputT1, SetpointT2, InputT2, OutputT2; double KcT1=13.70, KiT1=0.14, KdT1=0.0; //ZN10 double KcT2=21.82, KiT2=0.18, KdT2=0.0; //IAE-SP PID PIDT1(&InputT1, &OutputT1, &SetpointT1, KcT1, KiT1, KdT1, P_ON_E, DIRECT); //PI-D //PID PIDT1(&InputT1, &OutputT1, &SetpointT1, KcT1, KiT1, KdT1, P_ON_M, DIRECT); //I-PD PID PIDT2(&InputT2, &OutputT2, &SetpointT2, KcT2, KiT2, KdT2, P_ON_E, DIRECT); //PI-D //PID PIDT2(&InputT2, &OutputT2, &SetpointT2, Kc, Ki, Kd, P_ON_M, DIRECT); //I-PD /*************************************************************** * SINTONÍAS * T1 -> K:0.380 T0:29.5 TP:100.5 * CC25 PI KcT1=08.29, KiT1=0.14, KdT1=0.0 * CC10 PI KcT1=04.97, KiT1=0.07, KdT1=0.0 * ZN25 PI KcT1=08.07, KiT1=0.08, KdT1=0.0 * ZN10 PI KcT1=05.37, KiT1=0.05, KdT1=0.0 * ITAE-C PI KcT1=07.49, KiT1=0.12, KdT1=0.0 * ITAE-SP PI KcT1=04.74, KiT1=0.05, KdT1=0.0 * IAE-C PI KcT1=08.67, KiT1=0.12, KdT1=0.0 * IAE-SP PI KcT1=05.73, KiT1=0.05, KdT1=0.0 * LAMBDA PI KcT1=03.81, KiT1=0.04, KdT1=0.0 TF:40 * IMC PI KcT1=04.73, KiT1=0.04, KdT1=0.0 TF:60 * SIMC PI KcT1=04.44, KiT1=0.04, KdT1=0.0 TF:30 * IMP.SIMC PI KcT1=04.18, KiT1=0.04, KdT1=0.0 TF:40 * ------------------------------------------------------ * T2 -> K:0.405 T0:20 TP:117 * CC25 PI KcT1=12.45, KiT1=0.08, KdT1=0.0 * CC10 PI KcT1=07.92, KiT1=0.13, KdT1=0.0 * ZN25 PI KcT1=13.00, KiT1=0.19, KdT1=0.0 * ZN10 PI KcT1=08.66, KiT1=0.13, KdT1=0.0 * ITAE-C PI KcT1=11.91, KiT1=0.23, KdT1=0.0 * ITAE-SP PI KcT1=07.30, KiT1=0.06, KdT1=0.0 * IAE-C PI KcT1=13.87, KiT1=0.25, KdT1=0.0 * IAE-SP PI KcT1=08.57, KiT1=0.07, KdT1=0.0 * LAMBDA PI KcT1=04.13, KiT1=0.04, KdT1=0.0 TF:50 * IMC PI KcT1=06.02, KiT1=0.05, KdT1=0.0 TF:50 * SIMC PI KcT1=05.78, KiT1=0.05, KdT1=0.0 TF:30 * IMP.SIMC PI KcT1=06.11, KiT1=0.05, KdT1=0.0 TF:30 ***************************************************************/ /*************************************************************** * SETUP ***************************************************************/ void setup() { analogReference(EXTERNAL); while (!Serial) { ; // wait for serial port to connect. } Serial.begin(baud); Serial.flush(); // PIDT1.SetOutputLimits(0, 255); PIDT1.SetMode(MANUAL); PIDT2.SetOutputLimits(0, 255); PIDT2.SetMode(MANUAL); if (Ts < 100){ PIDT1.SetSampleTime(Ts); PIDT2.SetSampleTime(Ts); } } /*************************************************************** * BUCLE PRINCIPAL ***************************************************************/ void loop() { if (millis() - previousMillis > Ts) { previousMillis = millis(); //Lectura Temperaturas LecturaTTs(); LeoCMD(); ProcesoCMD(); EjecutoCMD(); //PID (Parámetros vía serial cmds) if (PIDT1.GetMode() == 1){//AUTO InputT1 = T1_PV; SetpointT1 = T1_SP; PIDT1.Compute(); T1_OP = map(OutputT1, 0, 255, 0, 100); //PWM -> % analogWrite(H1pin,OutputT1); }else if (PIDT1.GetMode() == 0) {//MANUAL T1_SP = T1_PV; } if (PIDT2.GetMode() == 1){//AUTO InputT2 = T2_PV; SetpointT2 = T2_SP; PIDT2.Compute(); T2_OP = map(OutputT2, 0, 255, 0, 100); //PWM -> % analogWrite(H2pin,OutputT2); }else if (PIDT2.GetMode() == 0) {//MANUAL T2_SP = T2_PV; } //Para Arduino COM Plotter Serial.print("#"); //Char inicio Serial.print(T1_SP,1); // Serial.write(" "); //separador Serial.print(T1_PV,1); // Serial.write(" "); //separador Serial.print(T1_OP); // Serial.write(" "); //separador Serial.print(T2_SP,1); // Serial.write(" "); //separador Serial.print(T2_PV,1); // Serial.write(" "); //separador Serial.print(T2_OP); // Serial.println(); // }//millis }//loop /*************************************************************** * FUNCIONES ***************************************************************/ double mapf(double val, double in_min, double in_max, double out_min, double out_max) { return (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } void LeoCMD() { while (Serial && (Serial.available() > 0) && (newData == false)) { int byte = Serial.read(); if ((byte != '\r') && (byte != nl) && (buffer_index < 64)) { Buffer[buffer_index] = byte; buffer_index++; } else { newData = true; } } } void ProcesoCMD(void) { if (newData) { String read_ = String(Buffer); // separate command from associated data int idx = read_.indexOf(sp); cmd = read_.substring(0, idx); cmd.trim(); cmd.toUpperCase(); // extract data. toFloat() returns 0 on error String data = read_.substring(idx + 1); data.trim(); val = data.toFloat(); // reset parameter for next command memset(Buffer, 0, sizeof(Buffer)); buffer_index = 0; newData = false; } } void EjecutoCMD(void) { if (cmd == "T1A") { PIDT1.SetMode(AUTOMATIC); } else if (cmd == "T1M") { PIDT1.SetMode(MANUAL); } else if (cmd == "T2A") { PIDT2.SetMode(AUTOMATIC); } else if (cmd == "T2M") { PIDT2.SetMode(MANUAL); } else if (cmd == "T1SP") { if (PIDT1.GetMode() == 1){ if (val > T1SPMAX){ T1_SP = T1SPMAX; }else{ T1_SP = val; } } } else if (cmd == "T1OP") { if (PIDT1.GetMode() == 0){ if (val > T1OPMAX){ val = T1OPMAX; } T1_OP = val; val = map(val, 0, 100, 0, 255); analogWrite(H1pin,T1_OP); } } else if (cmd == "T2SP") { if (PIDT2.GetMode() == 1){ if (val > T2SPMAX){ T2_SP = T2SPMAX; }else{ T2_SP = val; } } } else if (cmd == "T2OP") { if (PIDT2.GetMode() == 0){ if (val > T2OPMAX){ val = T2OPMAX; } T2_OP = val; val = map(val, 0, 100, 0, 255); analogWrite(H2pin,T2_OP); } } else if (cmd == "T1KC") { PIDT1.SetTunings(val,KiT1,KdT1); } else if (cmd == "T1KI") { PIDT1.SetTunings(KcT1,val,KdT1); } else if (cmd == "T1KD") { PIDT1.SetTunings(KcT1,KiT1,val); } else if (cmd == "T2KC") { PIDT2.SetTunings(val,KiT2,KdT2); } else if (cmd == "T2KI") { PIDT2.SetTunings(KcT2,val,KdT2); } else if (cmd == "T2KD") { PIDT2.SetTunings(KcT2,KiT2,val); } else if (cmd == "STT2") { StepTestT2(); } else if (cmd == "STT1") { StepTestT1(); } Serial.flush(); cmd = ""; } void LecturaTTs(void) { //Lectura Temperaturas for (int i = 0; i < n; i++) { T1_PV += (analogRead(T1pin)* 0.322265625 - 50.0); // 3.3v AREF T2_PV += (analogRead(T2pin)* 0.322265625 - 50.0); // 3.3v AREF //T1_PV += (analogRead(T1pin)*4.88 - 500) / 10; //5v //T2_PV += (analogRead(T2pin)*4.88 - 500) / 10; //5v } T1_PV = T1_PV / float(n); T2_PV = T2_PV / float(n); } void StepTestT1(void) { PIDT2.SetMode(MANUAL); analogWrite(H2pin,0); PIDT1.SetMode(MANUAL); analogWrite(H1pin,0); while (T1_PV > 30){ LecturaTTs(); Serial.print(T1_PV,1); Serial.println(); Serial.write("Esperando a T1_PV <= 30ºC"); Serial.println(); } for (int contador = 0; contador < 1850; contador++) { LecturaTTs(); if (contador <=50){ analogWrite(H1pin,0); T1_OP = 0; }else if (contador >=50 && contador <350){ analogWrite(H1pin,63); T1_OP = 63; //25% }else if (contador >=350 && contador <650){ analogWrite(H1pin,127); T1_OP = 127; //50% }else if (contador >=650 && contador <950){ analogWrite(H1pin,190); T1_OP = 190; //75% }else if (contador >=950 && contador <1250){ analogWrite(H1pin,127); T1_OP = 127; }else if (contador >=1250 && contador <1550){ analogWrite(H1pin,63); T1_OP = 63; }else if (contador >=1550 && contador <1850){ analogWrite(H1pin,0); T1_OP = 0; } if (1){ //Debug serial Arduino COM Plotter Serial.print("#"); //Char inicio Serial.print(T2_PV,1); // Serial.write(" "); //separador Serial.print(T1_PV,1); // Serial.write(" "); //separador Serial.print(T1_OP); // Serial.println(); } delay(1000); } } void StepTestT2(void) { PIDT2.SetMode(MANUAL); analogWrite(H2pin,0); PIDT1.SetMode(MANUAL); analogWrite(H1pin,0); while (T2_PV > 30){ LecturaTTs(); Serial.print(T2_PV,1); Serial.println(); Serial.write("Esperando a T2_PV <= 30ºC"); Serial.println(); } for (int contador = 0; contador < 1850; contador++) { LecturaTTs(); if (contador <=50){ analogWrite(H2pin,0); T2_OP = 0; }else if (contador >=50 && contador <350){ analogWrite(H2pin,63); T2_OP = 63; //25% }else if (contador >=350 && contador <650){ analogWrite(H2pin,127); T2_OP = 127; //50% }else if (contador >=650 && contador <950){ analogWrite(H2pin,190); T2_OP = 190; //75% }else if (contador >=950 && contador <1250){ analogWrite(H2pin,127); T2_OP = 127; }else if (contador >=1250 && contador <1550){ analogWrite(H2pin,63); T2_OP = 63; }else if (contador >=1550 && contador <1850){ analogWrite(H2pin,0); T2_OP = 0; } if (1){ //Debug serial Arduino COM Plotter Serial.print("#"); //Char inicio Serial.print(T2_PV,1); // Serial.write(" "); //separador Serial.print(T2_PV,1); // Serial.write(" "); //separador Serial.print(T2_OP); // Serial.println(); } delay(1000); } }
Conclusiones
La placa viene acompañada con un Arduino Leonardo que no he sido capaz de hacer comunicar vía puerto serie con Arduino COM Plotter. En su lugar he utilizado un Arduino UNO y éste comunica sin problema.
Me ha sorprendido la sencillez del concepto de la placa con todas las posibilidades que tiene. Desde el punto de vista del control podemos desarrollar un control ON/OFF, un PID y un MPC (Control Predictivo basado en Modelo). Dependiendo del control elegido podemos conformarnos con realizar una estimación, obtener un modelo o plantear las ecuaciones de balance para crear simulaciones, es decir, las posibilidades son amplias.
La implementación del PID es muy sencilla y creo que la interacción con la placa mediante comandos serial ha sido un acierto. Los resultados hablan por sí mismos, las sintonías calculadas coinciden plenamente con la simulación en lazo cerrado sin necesidad de realizar ningún ajuste adicional. Los disipadores están muy pegados tocándose ligeramente lo que hace que por conducción suframos perturbaciones entre temperaturas y podamos comprobar el comportamiento de nuestras sintonías tanto ante perturbaciones como ante cambios de Setpoint.
En definitiva, TCLab es sencillo de implementar, testear y sintonizar. Sin duda es un buen punto de partida para quien quiera iniciarse en el mundo del control de procesos.
Descargas
Enlaces
- Temperature Control Lab [APMonitor] [Github]
- Comprar Temperature Control Lab [APMonitor]
- Teaching Dynamics and Control with Arduino-based TCLab [Youtube]
- Arduino COM Plotter [garikoitz.info] [Github]
- PIDLab – Cálculo de Sintonías online [garikoitz.info]
- Librería PID by Brett Beauregard [Github]
Libros y publicaciones
[1] Karl J. Astrom, Tore Hagglund. Control PID avanzado, Pearson, ISBN: 978-84-8322-511-0
[2] Daniel Chuck. Los sistemas de primer orden y los controladores PID, edición 2012. [Link] [Link2]
[3] J.G. Ziegler, N.B. Nichols. Optimum Settings For Automatic Controllers, 1942 edition, American Society of Mechanical Engineers. [Link]
[4] G.H. Cohen, G.A. Coon. Theoretical Consideration of Retarded Control, 1953 edition, American Society of Mechanical Engineers. [Link] [Link2]
[5] Daniel E. Rivera. Internal Model Control: A Comprehensive View, 1999 edition, College of Engineering and Applied Sciences. [Link]
[6] R. Vilanova, A. Visioli. PID Control in the Third Millennium. Chapter 5, The SIMC Method for smooth PID Controller Tuning, Springer. ISBN: 978-1-4471-2424-5.
[7] Chriss Grimholt, Sigurd Skogestad. The improved SIMC method for PI controller tuning, IFAC-conference PID’12, Brescia, Italy, March 2012. [Link]