Sintonizar PID con Arduino – Sistema Bola-Viga

Sintonizar PID con Arduino – Sistema Bola-Viga

Introducción

Esta maqueta es un sistema muy empleado en el mundo educativo debido a que posee unas características que la hacen muy interesante de analizar y sintonizar. Una característica interesante es que su respuesta tiene carácter integrador, lo que conlleva recurrir a métodos de identificación en lazo cerrado y solamente eso, ya nos saca de nuestra zona de confort. En este caso implementaremos un control PID pero podemos encontrar por la red controladores más sofisticados como el LQR o el LQG.

Sistema empleado

La maqueta está realizada con piezas impresas en 3D para la ocasión y un par de varillas metálicas por donde se desliza la bola.

Maqueta Bola-Viga

El funcionamiento es muy sencillo, disponemos de un micro interruptor para cambiar entre modo manual y automático, y de un potenciómetro que en modo manual mueve el ángulo del servo y por ende la inclinación de la viga (OP), y en modo automático cambia el punto de consigna (SP) de posición de la bola en la viga. El objetivo es situar una bola en una posición determinada de la viga mediante un controlador PID.

Esquema de control

Componentes

  • Sensor de distancia.
  • Servomotor.
  • Micro interruptor.
  • Potenciómetro.
  • Arduino UNO o similar.

Sensor de distancia

Investigando un poco que tipo de sensores de distancia asequibles existen para Arduino, encontré unos cuantos y muchos ejemplos de uso, pero es diferente cuando se hace una prueba de un sensor a cuando le das un uso en el que se requiere cierta precisión. De todo lo que encontré me quedo con cuatro sensores de los que os dejo una tabla comparativa.

Tabla comparativa sensores de distancia
  • GP2Y0A21YK0F: Buena precisión incluso fuera de rango. Requiere calibración. Requiere objetos (en este caso bolas) de mayor dimensión para medir correctamente.
  • TOF10120: Buena precisión incluso fuera de rango. La señal necesita ser tratada para hacer control con ella.
  • VL53L0X: Buena precisión incluso fuera de rango. La señal necesita ser tratada para hacer control con ella.
  • HC-SR04: Buena precisión incluso fuera de rango. Requiere objetos (en este caso bolas) de mayor dimensión para medir correctamente.

Con cualquiera de ellos se pueden conseguir resultados aceptables pero yo finalmente elegí el sensor VL53L0X por utilizar un sensor diferente al que solemos encontrar en este tipo de maquetas.

Servomotor

En esta ocasión he utilizado un servo MG995 con un torque de entre 8,5 y 10 Kg/cm, pero valdría cualquier otro de características similares. En la parte de programación se opera con microsegundos en lugar de con ángulos para ganar precisión.

El coste de la maqueta puede oscilar entre 30 y 50€ dependiendo del servo y Arduino elegidos.

Esquema de conexiones

Conexiones del sistema

Código de Arduino

/***************************************************************
* Desarrollado por Garikoitz Martínez [garikoitz.info] [06/2022]
* https://garikoitz.info/blog/?p=1301
***************************************************************/
/***************************************************************
* Librerías
***************************************************************/
#include <Wire.h>
#include <PID_v1.h>
#include "Adafruit_VL53L0X.h"
#include <Servo.h>
/***************************************************************
* Variables
***************************************************************/
Servo myservo;
const int Interruptor = 12;
unsigned long previousMillis = 0; 
int Ts = 50, pos = 0;
unsigned long contador = 0; 
//Control PID
boolean modo = false;                       //false=auto true=manual
float tmp=0.0, tmp_ant=0.0, OP=0, angulo=0.0, angulo_ant=0.0, media=0.0;
double SetpointP, InputP, OutputP;
/***************************************************************
* Sintonías calculadas (Relé --> Ku=0.99 y Tu=4.6 segundos)
***************************************************************/
//double Kc=0.59, Ki=0.26, Kd=0.34; 	//Z-N classic PID
//double Kc=0.69, Ki=0.38, Kd=0.48; 	//Z-N Pessen Integral Rule 
//double Kc=0.33, Ki=0.14, Kd=0.49; 	//Z-N some overshoot
//double Kc=0.20, Ki=0.09, Kd=0.30; 	//Z-N no overshoot
//double Kc=0.33, Ki=0.07, Kd=0.15;		//Farrington
//double Kc=0.49, Ki=0.11, Kd=0.57;		//Farrington
//double Kc=0.53, Ki=0.12, Kd=0.49;		//McAvoy and Johnson
//double Kc=0.25, Ki=0.07, Kd=0.28;		//Atkinson and Davey (20% overshoot)
//double Kc=0.99, Ki=0.43, Kd=0.57;		//Pettit and Carr (Underdamped)
//double Kc=0.66, Ki=0.14, Kd=0.50;		//Pettit and Carr (Critically damped)
//double Kc=0.49, Ki=0.07, Kd=0.38;		//Pettit and Carr (Overdamped)
//double Kc=0.44, Ki=0.16, Kd=0.38;		//Tinham (Less than quarter decay ratio response)
//double Kc=0.74, Ki=0.26, Kd=0.34;		//Corripio (Quarter decay ratio)
//double Kc=0.62, Ki=0.27, Kd=0.24;		//ABB (P+D tuning rule)
//double Kc=0.45, Ki=0.04, Kd=0.33;		//Tyreus and Luyben
//double Kc=0.33, Ki=0.14, Kd=0.19;		//Yu (Some overshoot)
//double Kc=0.20, Ki=0.09, Kd=0.11;		//Yu (No overshoot)
//double Kc=0.74, Ki=0.26, Kd=0.34;		//Smith
//double Kc=0.99, Ki=0.34, Kd=0.45;		//Alfaro Ruiz (Quarter decay ratio)
//double Kc=1.65, Ki=0.57, Kd=0.76;		//Alfaro Ruiz (Quarter decay ratio)
//double Kc=0.39, Ki=0.09, Kd=0.29;		//Lloyd (Non self-regulating processes)
//double Kc=0.59, Ki=0.26, Kd=0.33;		//NI Labview (Quarter decay ratio)
//double Kc=0.25, Ki=0.11, Kd=0.14;		//NI Labview (Some overshoot)
//double Kc=0.15, Ki=0.06, Kd=0.08;		//NI Labview (Little overshoot)
//---------------------------------------------------------------
double Kc=0.45, Ki=0.1, Kd=0.35;    	//Tyreus and Luyben mod
//---------------------------------------------------------------
PID myPID(&InputP, &OutputP, &SetpointP, Kc, Ki, Kd,P_ON_E, REVERSE);//PI-D
//PID myPID(&InputP, &OutputP, &SetpointP, Kc, Ki, Kd,P_ON_M, REVERSE);  //I-PD
//
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
/***************************************************************
* SETUP
***************************************************************/
void setup() 
{ 
  Wire.begin();
  if (!lox.begin()) {
    Serial.println(F("Error al conectar al sensor VL53L0X"));
    while(1);
  }
  lox.startRangeContinuous();
  myservo.attach(3); // asignamos el pin al servo.
  Serial.begin(9600);
  myPID.SetOutputLimits(0, 100);
  SetpointP = 70;
}
/***************************************************************
* BUCLE PRINCIPAL
***************************************************************/
void loop() 
{
  VL53L0X_RangingMeasurementData_t measure;
  lox.rangingTest(&measure, false);
  if (measure.RangeStatus != 4) {
    //media =  measure.RangeMilliMeter;
    media = Rolling_avg(measure.RangeMilliMeter);
    media = media * 0.1; //Pasamos a cm
  }
  if (millis() - previousMillis > Ts) //Bucle de control cada Ts milisegundos
  {
    previousMillis = millis();
    contador +=1;
    if (digitalRead(12) == HIGH){
      modo=true;                              //Modo Manual
      myPID.SetMode(MANUAL);
    }else{
      modo=false;                             //Modo Automático
      myPID.SetMode(AUTOMATIC);
    }
      InputP = mapf(media, 2, 26, 0, 100);    //PV en cm a porcentaje
      //InputP = media;
    /***************************************************************
    * Modo MANUAL - Potenciómetro ---> Ángulo Servo
    ***************************************************************/
    if (modo==true){
      //----------------------
      //   Método del Relé
      //----------------------
      if (1){
         if (InputP <60){
          pos=950;                       //arriba -> bola hacia la izq
          myservo.writeMicroseconds(pos);  
         }else if (InputP >60){     
          pos=1475;                       //abajo -> bola hacia la dcha
          myservo.writeMicroseconds(pos);  
         }
         OP = mapf(pos, 850, 1575, 0, 100);
      }
      //----------------------
      //     Potenciómetro 
      //----------------------
      if (0){
        SetpointP=0;
        tmp = analogRead(A1);
        tmp = mapf(tmp, 1023.0, 0.0, 850, 1575);        // escalamos potenciometro al servo
        if(abs(tmp-tmp_ant)>1){
          angulo = tmp;
        }
        tmp_ant=angulo;
        OP = mapf(tmp,850, 1575, 0, 100);
        if (angulo>=850 || angulo<=1575) myservo.writeMicroseconds(angulo);
      }   
      /***************************************************************
      * DEBUG PUERTO SERIE (Para Arduino COM Plotter)
      ***************************************************************/
      Serial.print("#");              //Char inicio
      Serial.print(0);                //SP
      Serial.write(" ");              //Char separador
      Serial.print(InputP,0);         //PV InputP
      Serial.write(" ");              //Char separador
      Serial.print(OP,0);             //OP
      Serial.println();
      }
      /***************************************************************
      * Modo AUTOMÁTICO - PID
      ***************************************************************/
      if (modo==false){
		if (0){	//SetPoint mediante Potenciómetro
			tmp = analogRead(A1);
			tmp = mapf(tmp, 0.0, 1023.0, 0, 100);
			if(abs(tmp-tmp_ant)>4){
				SetpointP = tmp;
			}
			tmp_ant=SetpointP;
		}
		if (1){
			// SetpointP automatizado
			if (contador < 1000) SetpointP = 70;  
			if (contador >= 1000 && contador < 2000) SetpointP = 60; 
			if (contador >= 2000 && contador < 3000) SetpointP = 50; 
			if (contador >= 3000 && contador < 4000) SetpointP = 60; 
			if (contador > 4000) contador = 0;
		}
        
        angulo = mapf(OutputP, 0, 100, 850, 1575);
		
        if (abs(SetpointP-InputP)>5){   //Banda muerta del 5%
          myPID.Compute();
          myservo.writeMicroseconds(angulo);
          angulo_ant = angulo;  
        }else{
          myservo.writeMicroseconds(angulo_ant); 
        }
		
        /***************************************************************
        * DEBUG PUERTO SERIE (Para Arduino COM Plotter)
        ***************************************************************/
        Serial.print("#");                //Char inicio
        Serial.print(SetpointP,0);        //SP
        Serial.write(" ");                //Char separador
        Serial.print(InputP,0);           //PV
        Serial.write(" ");                //Char separador
        Serial.print(OutputP,0);          //OP
        Serial.println();  
      }
  }//millis            
}//Loop
/***************************************************************
* FUNCIONES
***************************************************************/
//Función MAP adaptada a punto flotante.
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;
}
//High-pass Filter
    //A low α will be very slow to rapid input changes and take many samples into account. 
    //A high α will be fast, but average over fewer samples. You can look at α as kind of a cutoff frequency in a low-pass filter.
    //OP = HPS(tmp,0.1,EMA_s_pot);
    //OP = tmp-EMA_s_pot;
double HPS(double sensorValue, float EMA_a, int EMA_S) {
    return (EMA_a*sensorValue) + ((1-EMA_a)*EMA_S);
}
//Función Promedio
float Rolling_avg(float value) {
  const byte nvalues = 10;             // Moving average window size
  static byte current = 0;            // Index for current value
  static byte cvalues = 0;            // Count of values read (<= nvalues)
  static float sum = 0;               // Rolling sum
  static float values[nvalues];
  sum += value;
  if (cvalues == nvalues)
    sum -= values[current];
  values[current] = value;          // Replace the oldest with the latest
  if (++current >= nvalues)
    current = 0;
  if (cvalues < nvalues)
    cvalues += 1;
  return sum/cvalues;
}

Adecuando la señal del sensor

La señal del sensor por defecto presenta bastante ruido por lo que para hacer control con ella he optado en esta ocasión por la implementación de una función promedio que nos devuelve una señal más limpia. En realidad la función realiza una media móvil en la que podemos especificar el número de medidas que intervienen para adecuar el resultado a nuestras necesidades.

Identificación del sistema

Como hemos comentado, la identificación es diferente a maquetas anteriores debido a que tiene un comportamiento integrador, me explico, si alteramos el ángulo de la viga la bola se moverá en una dirección y no parará. En maquetas anteriores los sistemas tenían respuesta autorregulada, lo que les hacía llegar a una situación estable con el tiempo ante cualquier alteración.

¿Qué significa esto?, pues que principalmente no podemos recurrir a realizar una identificación de saltos escalón (en lazo abierto) y obtener las constantes del sistema como en maquetas anteriores, nos vemos obligados a recurrir a otras técnicas de lazo cerrado como el de oscilación mantenida de Ziegler y Nichols o el del relé de Aström y Hägglund.

Método del relé de Aström y Hägglund

Este método consiste en programar un relé, en nuestro caso moviendo el servo de igual forma en ambos sentidos. Lo interesante es conseguir la oscilación continua sin llegar a los límites 0-100% de la PV. Del ensayo obtenemos 3 datos que nos sirven para estimar las ganancias del controlador PID, la amplitud de la PV (a), la amplitud de la OP (d) y el periodo de oscilación (Tc). Se ha usado un relé ideal sin histéresis.

Método del relé implementado
Datos obtenidos mediante el método del relé

Los datos obtenidos del ensayo son a = 93%, d = 72% y Tc = 4,6 segundos, con los cuales calcularemos la ganancia última Ku y el periodo de oscilación último Tu.

\begin{align}
& Ku = \frac{4d}{\pi a}=\frac{4\times 72}{\pi\times 93}=0.99\\
& Tu = Tc = 4.6 segundos
\end{align}

Con Ku y Tu podemos calcular la sintonía del controlador siguiendo las reglas de Ziegler y Nichols entre otras.

Sintonías resultantes

Resultados

Me gustaría aprovechar la ocasión para recordar que las sintonías calculadas ya sea en lazo abierto como en lazo cerrado son un punto de partida de las que con pequeños retoques se consigue la sintonía «final». Este es el ejemplo perfecto ya que la mayoría de las sintonías necesitan retocar alguno de los términos para que el resultado sea aceptable.

A continuación unos comentarios sobre tres sintonías.

Z-N (Pessen Integral Rule) (PI-D)

La combinación de alta ganancia integral y derivativa hace que el servo se mueva demasiado, no consiguiendo estabilizar cerca del setpoint en la mayoría de los casos.

Sintonía usada: Kc = 0,59 Ki = 0,26 Kd = 0,34 (Demasiada Ki y Kd)

Pettit and Carr (PI-D)

Se aprecia una gran mejoría comparada con la anterior aunque da la impresión de que la ganancia integral es algo pequeña.

Sintonía usada: Kc = 0,49 Ki = 0,07 Kd = 0,38

Tyreus and Luyben (PI-D)

Es la sintonía que mejores resultados arroja aunque le falta un poco de ganancia integral. Aún así, dependiendo de donde queramos estabilizar la bola, no siempre conseguimos una estabilidad total.

Sintonía usada: Kc = 0,45 Ki = 0,04 Kd = 0,33

Al probar las sintonías he llegado a la conclusión de que nos podemos mover entre:

\begin{align}
& 0.3\leq K{c} \leq 0.65\\
& 0.1\leq K{i} \leq 0.15\\
& 0.3\leq K{d} \leq 0.45\\
\end{align}
Ranking

Conclusiones

Tengo que reconocer que llevaba tiempo detrás de la realización de esta maqueta y que como esperaba, ha sido puro disfrute. Ya inicialmente, el que no sea un sistema auto regulado nos hace salir de la zona de confort e ingeniárnoslas para programar un relé y empezar a obtener sintonías. Una vez que tenemos las sintonías, nos vemos obligados a probar varias para familiarizarnos con el punto en que podemos considerar las ganancias altas o bajas y conseguir resultados aceptables.

La verdad es que es una maqueta en la que es difícil presentar unos resultados «bonitos» en cuanto a las gráficas se refiere. No es difícil de estabilizar la bola en el centro, de hecho, mediante prueba error podemos obtener una sintonía decente en pocos minutos, pero ya sabéis que lo que realmente me interesa es la obtención de sintonías mediante métodos analíticos.

Descargas

Referencias

Enlaces

Libros y Publicaciones

[1] Karl J. Astrom, Tore Hagglund. Control PID avanzado, Pearson, ISBN: 978-84-8322-511-0

[2] Myke King. «Process Control – A Practical Approach«, 2nd Edition, Chapter 2.4, Integrating Processes and Chapter 3.23, Suggested Tuning Method for Integrating Processes, Wiley, ISBN: 9781119157755

[3] O’Dwyer, Aidan. Handbook of PI and PID Controller Tuning Rules. 3rd Edition, Chapter 3.10, Non-Model Specific. Imperial College Press, ISBN: 978-1-84816-242-6

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.