SINTONIZAR PID CON ARDUINO – Laboratorio de Control de Temperatura (TCLab)

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.

T2 con temperatura

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.

Identificación de T1
T1T2
K(%/%)0.3800.405
T0(s)29.520
Tp(s)100.5117
Resultado de la identificación

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%.

Respuesta real

La simulación se corresponde con el primer cambio de Set Point.

Simulación en lazo cerrado de un cambio de SP de 7ºC

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).

Respuesta real

La simulación se corresponde con el primer cambio de Set Point.

Simulación en lazo cerrado de un cambio de SP de 9ºC

LAMBDA (PI)

Respuesta real

La simulación se corresponde con el primer cambio de Set Point.

Simulación en lazo cerrado de un cambio de SP de 8ºC
Control PID de T1. T2 permanece en manual

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í.

Respuesta real del control de las dos temperaturas
Vídeo: Respuesta real del control de las dos temperaturas

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

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]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Información básica sobre protección de datos
Responsable Garikoitz Martínez Moreno +info...
Finalidad Gestionar y moderar tus comentarios. +info...
Legitimación Consentimiento del interesado. +info...
Destinatarios Automattic Inc., EEUU para filtrar el spam. +info...
Derechos Acceder, rectificar y cancelar los datos, así como otros derechos. +info...
Información adicional Puedes consultar la información adicional y detallada sobre protección de datos en nuestra página de política de privacidad.