Una necesidad habitual en automatización y control es la realización de una determinada acción cuando un valor de entrada atraviesa un valor de referencia, que denominaremos consigna o umbral (threshold).
El control mediante un umbral es una de las formas más sencillas de regulación que podemos implementar. Aparece frecuentemente cuando disponemos de un actuador de tipo todo/nada por lo que, al no disponer la opción de modular, no es posible otro tipo de controlador más sofisticado.
Cuando la variable a controlar es independiente de la variable registrada podemos emplear un único umbral como condición de activación y desactivación. Por ejemplo, encender una luz o una alarma cuando la medición de un sensor sea inferior (o superior) a una determinada consigna.
Sin embargo cuando la variable a controlar tiene realimentación en la variable registrada generalmente necesitaremos emplear un doble umbral, es decir, emplear consignas diferentes para la condición de activación y desactivación.
Por ejemplo, un termostato que enciende o apaga un emisor de calor en función de la temperatura registrada, o un equipo de bombeo que se activa si el nivel de un depósito sobrepasa una determinada cota.
El interés del doble umbral, o histéresis, es evitar múltiples encendidos en el actuador que supondrían un desgaste prematuro. De hecho, en un mundo ideal donde la acción realizada tuviera un efecto inmediato y en ausencia de ruido, en el instante en que se atravesara el umbral se activaría el actuador, corrigiendo la variable en una magnitud diferencial, suficiente para provocar su desconexión. Es decir, tendríamos infinitos encendidos una vez alcanzada la consigna, y el sistema no sería capaz de evolucionar a otro estado.
Por supuesto, un sistema real tiene inercias, retrasos, inhomogenidades, etc. Por tanto, no tendremos un número infinito de encendidos, aun que sí puede original un gran número de encendidos.
Esto nos lleva al otro motivo del doble umbral, el ruido de la variable registrada. Si, por ejemplo, tomamos la medición de temperatura, cuando el sensor alcance el umbral la temperatura de la estancia no es perfectamente homogénea. Cualquier pequeña variación (ruido) de la señal provocará, nuevamente, múltiples disparos en torno al umbral.
El ruido en la variable registrada hace que el doble umbral sea recomendable incluso para variables independientes. Por ejemplo, si queremos encender una luz si el agua de un depósito sobre pasa un nivel, igualmente puede ser recomendable que esta no se apague hasta que baje de una consigna inferior al valor de encendido. De lo contrario las propias ondas y turbulencias de las superficie del liquido podrían provocar un efecto de parpadeo en el sensor al atravesar el umbral.
En esta entrada vamos a ver dos implementaciones sencillas para un control todo o nada con umbral y doble umbral en un autómata o procesador como Arduino..
Umbral simple en Arduino
Implementar un umbral simple es realmente sencillo. Emplearemos una función GetMeasure() que simula la obtención de una medición desde un sensor.
Almacenamos el estado anterior, y en función del estado previo y del estado actual determinamos si es necesario realizar la acción de activación o desactivación.
// Simula la captura de un sensor o la lectura de un sensor
int values[] = { 7729, 7330, 10075, 10998, 11502, 11781, 12413, 12530, 14070, 13789, 18186, 14401, 16691, 16654, 17424, 21104, 17230, 20656, 21584, 21297, 19986, 20808, 19455, 24029, 21455, 21350, 19854, 23476, 19349, 16996, 20546, 17187, 15548, 9179, 8586, 7095, 9718, 5148, 4047, 3873, 4398, 2989, 3848, 2916, 1142, 2427, 250, 2995, 1918, 4297, 617, 2715, 1662, 1621, 960, 500, 2114, 2354, 2900, 4878, 8972, 9460, 11283, 16147, 16617, 16778, 18711, 22036, 28432, 29756, 24944, 27199, 27760, 30706, 31671, 32185, 32290, 30470, 32616, 32075, 32210, 28822, 30823, 29632, 29157, 31585, 24133, 23245, 22516, 18513, 18330, 15450, 12685, 11451, 11280, 9116, 7975, 8263, 8203, 4641, 5232, 5724, 4347, 4319, 3045, 1099, 2035, 2411, 1727, 852, 1134, 966, 2838, 6033, 2319, 3294, 3587, 9076, 5194, 6725, 6032, 6444, 10293, 9507, 10881, 11036, 12789, 12813, 14893, 16465, 16336, 16854, 19249, 23126, 21461, 18657, 20474, 24871, 20046, 22832, 21681, 21978, 23053, 20569, 24801, 19045, 20092, 19470, 18446, 18851, 18210, 15078, 16309, 15055, 14427, 15074, 10776, 14319, 14183, 7984, 8344, 7071, 9675, 5985, 3679, 2321, 6757, 3291, 5003, 1401, 1724, 1857, 2605, 803, 2742, 2971, 2306, 3722, 3332, 4427, 5762, 5383, 7692, 8436, 13660, 8018, 9303, 10626, 16171, 14163, 17161, 19214, 21171, 17274, 20616, 18281, 21171, 18220, 19315, 22558, 21393, 22431, 20186, 24619, 21997, 23938, 20029, 20694, 20648, 21173, 20377, 19147, 18578, 16839, 15735, 15907, 18059, 12111, 12178, 11201, 10577, 11160, 8485, 7065, 7852, 5865, 4856, 3955, 6803, 3444, 1616, 717, 3105, 704, 1473, 1948, 4534, 5800, 1757, 1038, 2435, 4677, 8155, 6870, 4611, 5372, 6304, 7868, 10336, 9091 };
//int values[] = { 14401, 16691, 16654, 17424, 21104, 17230, 20656, 21584, 21297, 19986, 20808, 19455, };
size_t valuesLength = sizeof(values) / sizeof(values[0]);
int getMeasure()
{
size_t static index = 0;
index++;
return values[index - 1];
}
bool state;
int threshold = 15000;
void calculateThreshold(int value)
{
if (state == false && value > threshold)
{
state = true;
Rising();
}
else if (state == true && value < threshold)
{
state = false;
Falling();
}
}
void Rising()
{
Serial.println("Rising");
}
void Falling()
{
Serial.println("Falling");
}
void setup()
{
Serial.begin(9600);
state = true;
for (size_t index = 0; index < valuesLength; index++)
{
// Obtener medicion simulada
int rawMeasure = getMeasure();
calculateThreshold(rawMeasure);
}
}
void loop()
{
}
La respuesta del sistema es la siguiente. Como vemos, la señal conmuta cada vez que la señal atraviesa la consigna en cualquiera de los dos sentidos. También vemos que el ruido puede original múltiples disparos en torno a la consigna, como habíamos mencionado con anterioridad.
Por este motivo, salvo en sistemas muy sencillos, en general siempre emplearemos un doble umbral en nuestros proyectos.
Umbral doble (histeresis) en Arduino
Implementar un doble umbral no es mucho más difícil. En esta ocasión tendremos dos consignas diferentes, y el cambio de estado dependerá del estado previo, del valor de la señal, y de ambas consignas.
// Simula la captura de un sensor o la lectura de un sensor
int values[] = { 7729, 7330, 10075, 10998, 11502, 11781, 12413, 12530, 14070, 13789, 18186, 14401, 16691, 16654, 17424, 21104, 17230, 20656, 21584, 21297, 19986, 20808, 19455, 24029, 21455, 21350, 19854, 23476, 19349, 16996, 20546, 17187, 15548, 9179, 8586, 7095, 9718, 5148, 4047, 3873, 4398, 2989, 3848, 2916, 1142, 2427, 250, 2995, 1918, 4297, 617, 2715, 1662, 1621, 960, 500, 2114, 2354, 2900, 4878, 8972, 9460, 11283, 16147, 16617, 16778, 18711, 22036, 28432, 29756, 24944, 27199, 27760, 30706, 31671, 32185, 32290, 30470, 32616, 32075, 32210, 28822, 30823, 29632, 29157, 31585, 24133, 23245, 22516, 18513, 18330, 15450, 12685, 11451, 11280, 9116, 7975, 8263, 8203, 4641, 5232, 5724, 4347, 4319, 3045, 1099, 2035, 2411, 1727, 852, 1134, 966, 2838, 6033, 2319, 3294, 3587, 9076, 5194, 6725, 6032, 6444, 10293, 9507, 10881, 11036, 12789, 12813, 14893, 16465, 16336, 16854, 19249, 23126, 21461, 18657, 20474, 24871, 20046, 22832, 21681, 21978, 23053, 20569, 24801, 19045, 20092, 19470, 18446, 18851, 18210, 15078, 16309, 15055, 14427, 15074, 10776, 14319, 14183, 7984, 8344, 7071, 9675, 5985, 3679, 2321, 6757, 3291, 5003, 1401, 1724, 1857, 2605, 803, 2742, 2971, 2306, 3722, 3332, 4427, 5762, 5383, 7692, 8436, 13660, 8018, 9303, 10626, 16171, 14163, 17161, 19214, 21171, 17274, 20616, 18281, 21171, 18220, 19315, 22558, 21393, 22431, 20186, 24619, 21997, 23938, 20029, 20694, 20648, 21173, 20377, 19147, 18578, 16839, 15735, 15907, 18059, 12111, 12178, 11201, 10577, 11160, 8485, 7065, 7852, 5865, 4856, 3955, 6803, 3444, 1616, 717, 3105, 704, 1473, 1948, 4534, 5800, 1757, 1038, 2435, 4677, 8155, 6870, 4611, 5372, 6304, 7868, 10336, 9091 };
//int values[] = { 14401, 16691, 16654, 17424, 21104, 17230, 20656, 21584, 21297, 19986, 20808, 19455, };
size_t valuesLength = sizeof(values) / sizeof(values[0]);
int getMeasure()
{
size_t static index = 0;
index++;
return values[index - 1];
}
bool state;
int riseThreshold = 20000;
int fallThreshold = 15000;
void calculateThreshold(int value)
{
if (state == false && value > riseThreshold)
{
state = true;
Rising();
}
else if (state == true && value < fallThreshold)
{
state = false;
Falling();
}
}
void Rising()
{
Serial.println("Rising");
}
void Falling()
{
Serial.println("Falling");
}
void setup()
{
Serial.begin(9600);
state = true;
for (size_t index = 0; index < valuesLength; index++)
{
// Obtener medicion simulada
int rawMeasure = getMeasure();
calculateThreshold(rawMeasure);
}
}
void loop()
{
}
La respuesta del sistema es la siguiente.
La salida del sistema se activa cuando la entrada supera el umbral de subida, pero no se desactiva hasta que atraviesa el umbral inferior. De esta forma, eliminamos los disparos que se producían por el ruido en el caso de umbral simple.
Umbral y doble umbral en una librería
¿Y si limpiamos y mejoramos el código, y lo metemos en una librería para que sea más cómodo de usar? Por supuesto que sí, aquí una librería Threshold para Arduino. ¡A disfrutarlo!
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.