Librerías PID y herramientas de identificación para Arduino
Introducción
A lo largo de los tres artículos anteriores he implementado el controlador, identificado el proceso y calculado una sintonía. Llegados a este punto es razonable preguntarse por qué escribir todo ese código si existen librerías PID para Arduino. La respuesta es sencilla. Una librería puede ahorrarme trabajo, pero no elimina la necesidad de entender qué ecuación ejecuta, cómo interpreta sus parámetros, cómo trata la saturación o qué prueba realiza cuando ofrece autotuning.
En este último artículo no voy a buscar una librería que me permita olvidarme del PID. Haré justo lo contrario y utilizaré lo aprendido para saber qué debo comprobar antes de confiar en una implementación ajena. También separaré claramente dos funciones diferentes, controlar el proceso e identificarlo o sintonizarlo automáticamente.
¿Qué debe ofrecer una librería PID?
La llamada más visible de una librería suele ser Compute(), update() o una función equivalente, pero para comparar implementaciones necesito mirar bastante más. En el primer artículo mi ejemplo terminó incorporando una estructura seleccionable PID/PI-D/I-PD, tiempo de muestreo conocido, límites de OP, integración condicional como anti-windup, acción de control, modo manual/automático, inicialización para reducir los saltos en la transferencia y PV Tracking. Esa implementación me sirve ahora como lista de requisitos con la que comparar las librerías.
| Aspecto | Qué debemos comprobar |
| Parámetros | ¿Espera Kc/Ki/Kd o Kc/Ti/Td? ¿Cómo escala Ki y Kd con Ts? |
| Estructura | ¿PID clásico, PI-D, I-PD o ponderación configurable de P y D? |
| Muestreo | ¿Usa un Ts interno fijo, millis(), temporizador o tiempo real entre muestras? |
| Saturación | ¿Permite configurar límites físicos de OP? |
| Anti-windup | ¿Clamping, integración condicional, back-calculation u otra estrategia? |
| Acción / dirección | ¿Qué significan DIRECT y REVERSE exactamente en esa API? |
| Modos | ¿Gestiona manual y automático? |
| Bumpless transfer | ¿Inicializa sus estados internos para evitar un salto de OP? |
| PV Tracking | ¿Puede hacer que SP siga a PV en manual o debemos implementarlo fuera? |
| Diagnóstico | ¿Podemos consultar P, I y D por separado y conocer la configuración activa? |
Una librería que no documenta estos puntos puede servir para una demostración sencilla, pero me dificulta trasladar una sintonía obtenida con otro método o diagnosticar por qué el lazo no se comporta como esperaba. Además, no doy por hecho que nombres como Kc, Ki o Kd representen exactamente la misma parametrización que he utilizado en la serie, compruebo siempre cómo los define cada librería.
Arduino PID Library de Brett Beauregard
La Arduino PID Library de Brett Beauregard es la referencia clásica dentro del ecosistema Arduino. Su fichero library.properties declara actualmente la versión 1.2.1. La librería permite configurar las ganancias, los límites de salida, el tiempo de muestreo y la acción del controlador. Desde la versión 1.2 también permite elegir entre Proportional on Error y Proportional on Measurement. En su API la ganancia proporcional del controlador se denomina Kp, aunque en esta serie seguiré llamándola Kc para no confundirla con Kp, que he utilizado para representar la ganancia del proceso.
Aquí aparece una relación directa con las estructuras que estudié en el primer artículo. PID_v1 calcula siempre la acción derivativa sobre la medida. Cuando selecciono P_ON_E, las acciones P e I trabajan sobre el error mientras D actúa sobre PV. Con la nomenclatura utilizada en esta serie, esto corresponde a una estructura PI-D. Si selecciono P_ON_M, la acción proporcional también pasa a trabajar sobre la medida mientras I continúa actuando sobre el error. Desde el punto de vista de las estructuras que hemos estudiado, el comportamiento corresponde entonces a una estructura I-PD.
Me parece importante señalar esta diferencia porque Proportional on Measurement no significa PI-D. En realidad ocurre justo lo contrario, ya que esta opción desplaza la acción proporcional desde el error hacia la medida. La estructura PI-D es la que obtenemos con P_ON_E, puesto que la librería ya calcula por defecto la acción derivativa sobre PV.
//P_ON_E + D sobre PV -> PI-D
//P_ON_M + D sobre PV -> I-PD
#include <PID_v1.h>
double SP, PV, OP;
double Kc = 2.0, Ki = 0.2, Kd = 0.0;
// Para nuestro proceso de calentamiento: PI-D y proceso PV(+) OP(-)
PID pid(&PV, &OP, &SP, Kc, Ki, Kd, P_ON_E, REVERSE);
void setup() {
pid.SetOutputLimits(0, 255);
pid.SetSampleTime(100);
pid.SetMode(AUTOMATIC);
}
void loop() {
PV = leerProceso();
pid.Compute();
aplicarSalida(OP);
}
En cuanto a los cambios entre manual y automático, PID_v1 incorpora un mecanismo básico de inicialización destinado a reducir los saltos durante la transferencia. Al pasar a automático, la librería inicializa su suma interna con el valor actual de OP y almacena la medida actual como referencia para el término derivativo. También limita esa suma interna a los límites configurados para la salida, proporcionando una protección básica frente al windup.
Sin embargo, en mis implementaciones he comprobado que esta inicialización no garantiza por sí sola una transferencia completamente sin salto. Con la configuración habitual, en la que la acción proporcional trabaja sobre el error, en el primer cálculo automático se añade a la salida inicializada el término proporcional . Si existe una diferencia apreciable entre SP y PV en ese momento, puede producirse un cambio brusco de OP.
Por este motivo, en mis implementaciones sigo realizando explícitamente la inicialización antes de pasar a automático. Mientras el controlador está en manual aplico PV Tracking, haciendo , y mantengo la variable de salida del PID igualada a la OP que realmente estoy aplicando al proceso. De esta forma, cuando paso a automático, el controlador parte tanto de un error inicial nulo como de la salida real que estaba utilizando en manual, reduciendo considerablemente la posibilidad de que aparezca un salto.
PID_v1 gestiona, por tanto, los modos manual y automático y proporciona una inicialización interna útil, pero no incorpora PV Tracking ni una lógica de inicialización tan completa como la que utilizo habitualmente en mis implementaciones. Tampoco permite consultar por separado las contribuciones instantáneas P, I y D, aunque sí permite consultar los valores configurados de Kc, Ki y Kd.
QuickPID
QuickPID parte de la Arduino PID Library y amplía considerablemente sus posibilidades. En la versión principal mantenida actualmente por Dlloydev puedo seleccionar la proporcional sobre error, medida o una combinación 50/50, elegir la derivada sobre error o medida, seleccionar distintos modos de anti-windup, utilizar modo TIMER y consultar individualmente los términos P, I y D. También dispone de modos manual y automático y de un mecanismo interno de inicialización al cambiar de modo.
Con pOnError y dOnError obtengo el PID clásico. Con pOnError y dOnMeas obtengo PI-D. Con pOnMeas y dOnMeas obtengo I-PD. La opción pOnErrorMeas reparte la acción proporcional entre error y medida y representa una estructura intermedia. De esta forma puedo seleccionar explícitamente dónde actúan P y D sin reescribir el algoritmo.
QuickPID pid(&PV, &OP, &SP); pid.SetTunings(Kc, Ki, Kd); pid.SetProportionalMode(QuickPID::pMode::pOnError); pid.SetDerivativeMode(QuickPID::dMode::dOnMeas); // PI-D pid.SetAntiWindupMode(QuickPID::iAwMode::iAwCondition); pid.SetControllerDirection(QuickPID::Action::reverse);
QuickPID no incorpora PV Tracking como función específica, por lo que en mis implementaciones mantendría fuera de la librería la misma estrategia que he utilizado hasta ahora para inicializar el paso de manual a automático. En cuanto al autotuning, su documentación remite a sTune, una librería independiente que comentaré al hablar de identificación en lazo abierto.
Controlar no es lo mismo que identificar
Me parece importante separar dos tipos de librerías. Una librería PID ejecuta el controlador durante el funcionamiento normal. Una librería de autotuning sustituye temporalmente ese control o modifica deliberadamente la actuación para obtener información del proceso. Cuando termina la prueba devuelve parámetros que después puedo utilizar en el controlador.
Esta distinción me parece especialmente importante por seguridad. Durante el autotuning la salida puede oscilar deliberadamente y PV se apartará de su valor estacionario. Antes de activar cualquier autotuner necesito conocer qué ensayo realiza y comprobar que el proceso puede soportarlo.
Arduino PID Autotuner
Arduino-pid-autotuner, de jackw01, implementa un autotuning basado en el método de relé y reglas de Ziegler-Nichols. La propia documentación indica que debe ejecutarse con el mismo intervalo que utilizará posteriormente el lazo PID, permite configurar el rango de salida y ofrece tres variantes de ajuste, Basic PID, Less Overshoot y No Overshoot. Es, por tanto, un ejemplo claro de identificación en lazo cerrado mediante oscilación inducida.
Durante la prueba la librería sustituye temporalmente al controlador y genera la actuación necesaria para inducir la oscilación. Al terminar devuelve Kc, Ki y Kd para transferirlos al controlador que vaya a utilizar. Esto enlaza directamente con el método de relé de Åström-Hägglund que expliqué en el segundo artículo, el autotuner automatiza el ensayo y el cálculo de la sintonía, pero no cambia el principio físico del procedimiento ni elimina la necesidad de que compruebe que la oscilación es segura.
PIDAutotuner tuner; tuner.setTargetInputValue(SP); tuner.setLoopInterval(loopInterval); tuner.setOutputRange(0, 255); tuner.startTuningLoop(micros()); // Durante el ensayo: OP = tuner.tunePID(PV, micros()); // Al finalizar: double Kc = tuner.getKp(); double Ki = tuner.getKi(); double Kd = tuner.getKd();
Presto especial atención a la nomenclatura. Esta API llama Kp a la ganancia proporcional del controlador. En esta serie he reservado Kp para la ganancia del proceso y Kc para la ganancia del controlador. Por tanto, tuner.getKp() debe interpretarse conceptualmente como el Kc que he utilizado hasta ahora. La librería devuelve directamente las ganancias del controlador, no confundir ese resultado con haber obtenido un modelo FOPDT Kp, T0 y Tp del proceso.
¿Y la identificación en lazo abierto?
La identificación en lazo abierto del segundo artículo también se puede automatizar. Una opción especialmente coherente con el recorrido de esta serie es sTune, de Dlloydev. La librería coloca el PID en manual, aplica un escalón de salida y analiza la curva de reacción. Su modo de punto de inflexión puede terminar la prueba antes de alcanzar completamente el nuevo estado estacionario y dispone también de un ensayo más largo. Entre los resultados accesibles están la ganancia del proceso, el tiempo muerto y la constante de tiempo.
Con la nomenclatura utilizada en esta serie, esos tres resultados corresponden conceptualmente a Kp, T0 y Tp. Esto enlaza de forma directa con los artículos 2 y 3 en que a partir de la respuesta escalón obtengo un modelo aproximado del proceso y después puedo aplicar reglas como Ziegler-Nichols en lazo abierto o Cohen-Coon. sTune ofrece varias reglas de sintonía y devuelve también parámetros del controlador. Aun así, sigue siendo un ensayo en lazo abierto donde antes de utilizarlo necesito poder fijar la OP manualmente y dejar evolucionar la PV dentro de límites seguros.
Comparativa práctica
Control
| Aspecto | Mi implementación (art. 1) | PID_v1 | QuickPID |
| Parámetros / ecuación | Kc, Ki, Kd; Ki = Kc/Ti; Kd = Kc·Td | API: Kp = Kc de esta serie; Ki y Kd se escalan internamente con Ts | API: Kp = Kc de esta serie; Ki y Kd se escalan internamente con Ts |
| Estructura | PID / PI-D / I-PD | P_ON_E = PI-D; P_ON_M = I-PD | PID clásico / PI-D / I-PD + P 50/50 |
| D sobre medida | Seleccionable | Sí, siempre | Seleccionable |
| Ts | Configurable; cálculo con tiempo transcurrido | SetSampleTime() | SetSampleTimeUs() / TIMER |
| Límites OP | Sí | Sí | Sí |
| Anti-windup | Integración condicional | Clamping de la suma interna | Condicional, clamping u off |
| Acción | Seleccionable: directa / inversa | DIRECT / REVERSE | direct / reverse |
| Manual / automático | Sí | Sí | Sí + TIMER |
| Inicialización MAN→AUTO | Explícita: PV Tracking + OP manual sincronizada | Básica: inicializa suma interna y medida anterior | Inicialización interna; la complemento si necesito PV Tracking |
| PV Tracking | Sí, lo implemento en manual | No; lo implemento externamente | No específico; lo implemento externamente |
| P, I y D separados | Sí internamente | No | Sí: GetPterm() GetIterm() GetDterm() |
Identificación / autotuning
| Aspecto | Mi ensayo | arduino-pid-autotuner | sTune |
| Tipo de ensayo | Escalón en lazo abierto | Relé / oscilación en lazo cerrado | Escalón en lazo abierto |
| Qué obtenemos | Kp, T0, Tp | Ganancias Kp, Ki, Kd de su API | Ganancia de proceso, tiempo muerto, Tau + sintonía |
| Equivalencia | Kp, T0, Tp | Kp devuelto = Kc de esta serie | Process gain = Kp; dead time = T0; Tau = Tp |
| Reglas | Las calculamos después | Variantes Ziegler-Nichols | ZN, Cohen-Coon y otras |
| Modelo del proceso | Sí, FOPDT aproximado | No directamente | Sí, parámetros equivalentes al FOPDT |
| Condición | Manual seguro | Oscilaciones seguras | Manual seguro |
He separado las tablas de control e identificación. Utilizo el código que desarrollé en el artículo 1 como referencia porque sé exactamente qué ecuación ejecuta y qué funciones he implementado. PID_v1 me obliga a traducir P_ON_E/P_ON_M a las estructuras que ya he explicado, mientras que QuickPID añade más flexibilidad de estructura y diagnóstico. Para identificar, arduino-pid-autotuner automatiza el ensayo de relé en lazo cerrado y sTune automatiza el escalón en lazo abierto que conduce a Kp, T0 y Tp.
Qué comprobar antes de utilizar una librería
Antes de integrar una librería traduzco primero su nomenclatura a la utilizada en esta serie. Qué llama Kp, cómo selecciona la acción del controlador y sobre qué señal calcula P y D. Después compruebo parametrización, Ts, límites de OP y anti-windup, y pruebo manual/automático, la inicialización al cambiar de modo y, si lo necesito, PV Tracking. Solo entonces traslado una sintonía o ejecuto una herramienta de identificación.
Conclusiones
Cuando encontramos por internet implementaciones de control PID en Arduino, a menudo aparecen en un contexto académico o como maquetas caseras destinadas a realizar una prueba de concepto. Normalmente no son sistemas diseñados para trabajar 24 horas al día y 7 días a la semana, por lo que en esas situaciones podemos permitirnos ciertas simplificaciones. Puede que no nos preocupe demasiado que el controlador no se inicialice correctamente o que, ante un cambio en el punto de consigna, la OP oscile bruscamente entre el 0 y el 100 % durante unos segundos.
Sin embargo, Arduino permite desarrollar sistemas de control mucho más complejos y existen incluso soluciones orientadas al entorno industrial, como los Arduino Opta. Cuando pasamos de una maqueta de laboratorio a un sistema que debe funcionar de forma continua y fiable, una implementación correcta deja de ser algo deseable y pasa a ser una necesidad. Aspectos como la inicialización, la transferencia entre manual y automático, el tratamiento de la saturación, el anti-windup o la forma en que se aplican los cambios de consigna pueden marcar una diferencia importante en el comportamiento real del proceso.
Una librería me permite escribir menos código, pero entender el PID me permite saber si realmente hace lo que necesito. Después de esta serie ya no me interesa preguntar únicamente «¿qué valores pongo en Kc, Ki y Kd?». Me interesa también saber qué significa cada parámetro, qué estructura utiliza la librería, sobre qué señales actúan la parte Proporcional y la Derivativa, cómo gestiona la saturación, qué ocurre al cambiar entre manual y automático y de dónde proceden realmente los parámetros que devuelve un autotuner.
Ese es, para mí, el principal valor de haber implementado primero el controlador, haber identificado después el proceso y haber calculado finalmente la sintonía antes de delegar esas funciones en una librería. Una vez entendido todo lo anterior, utilizar una librería deja de ser una forma de evitar conocer cómo funciona el PID y pasa a ser simplemente una forma más cómoda de implementar aquello que ya sabemos que necesitamos.
Serie Introducción al PID y su implementación en Arduino
- Artículo 1: Entender e implementar antes de sintonizar
- Artículo 2: Identificación de procesos para la sintonía de un PID
- Artículo 3: Métodos de sintonía PID a partir de la identificación del proceso
- Artículo 4: Librerías PID y herramientas de identificación para Arduino
Lectura recomendada
[1] Tore Hägglund. Process Control in Practice. De Gruyter, 2023. eBook ISBN: 978-3-11-110495-9.
[2] Karl J. Åström, Tore Hägglund. Control PID avanzado. Pearson Educación, 2009. ISBN: 978-84-8322-511-0.
[3] Daniel Chuk. Los sistemas de primer orden y los controladores PID. 2012. [Link] [Link2]
[4] Fernando Morilla García. ¿Qué quieres controlar? ¿Has probado con controladores PID? Dpto. de Informática y Automática, UNED, 2015. [Link]
[5] Brett Beauregard. Arduino PID Library (PID_v1). Biblioteca PID para Arduino, versión 1.2.1. [GitHub]
[6] Dlloydev. QuickPID. Implementación PID para Arduino basada en Arduino PID Library, con opciones configurables para las acciones proporcional y derivativa, anti-windup y diagnóstico de los términos P, I y D. [GitHub]
[7] Dlloydev. sTune. Librería de autotuning para Arduino basada en identificación en lazo abierto mediante respuesta al escalón y análisis del punto de inflexión. [GitHub]
[8] Jack W. arduino-pid-autotuner. Librería de autotuning PID basada en el método de relé y reglas de Ziegler-Nichols. [GitHub]