sábado, 19 de noviembre de 2016

Jugando con un sensor PIR.


Los sensores PIR permiten captar el movimiento, casi siempre se utilizan para detectar si un ser humano se ha movido dentro del rango del sensor. 

Son pequeños, de bajo costo, bajo consumo de energía, fácil de usar y totalmente de estado sólido. Por tal motivo, se encuentran comúnmente en electrodomésticos y aparatos utilizados en los hogares o negocios. Se refieren a ellos normalmente como "PIR", "Passive Infrared", "piroeléctrico", o "sensores de movimiento IR".

Un  PIR básicamente es un sensor que puede detectar los niveles de radiación infrarroja. Todo cuerpo emite cierto nivel de radiación infrarroja, y cuanto más caliente, más radiación emite. El sensor en un detector de movimiento y esta dividido en dos mitades. La razón de ello es que estamos tratando de detectar movimiento (cambio) y no los niveles promedio de radiación infrarroja. Las dos mitades están cableados de manera que se anulan entre sí. Si uno ve más o menos radiación infrarroja que el otro, la salida se cambiará alta o baja.

El propio sensor PIR se puede adquirir por separado:
Pero naturalmente siempre es más cómodo de manejar con un pequeño circuito de estabilización y control, que permita usarlo como un sensor digital directo.

La mayoría de los PIR utilizan el circuito integrado BISS0001 (“Micro Poder Detector de movimiento PIR IC”), sin duda un chip muy barato. Este chip toma la salida del sensor y realiza diversos procesamientos en el mismo, para emitir un impulso de salida digital desde el sensor analógico.

Para muchos proyectos donde es necesario detectar cuando una persona ha salido o entrado en una determinada zona, los sensores PIR son geniales. Pero hay que tener en cuenta que los PIR no dirán cuántas personas están alrededor, o qué tan cerca están del sensor, la lente con frecuencia se fija a un cierto barrido y distancia, también hay que considerar que se puede activar con animales domésticos.

El otro elemento restante para que todo funcione es la óptica del sensor. Básicamente es una cúpula de plástico formada por lentes de fresnell, que divide el espacio en zonas, y enfoca la radiación infrarroja a cada uno de los campos del PIR.

De esta manera, cada uno de los sensores capta un promedio de la radiación infrarroja del entorno. Cuando un objeto entra en el rango del sensor, alguna de las zonas marcadas por la óptica recibirá una cantidad distinta de radiación, que será captado por uno de los campos del sensor PIR, disparando la alarma.


Y acá es donde deberíamos de enterarnos un poco sobre de las lentes cóncavas, fresnell y un montón de detalles varios que hacen al conocimiento general de los principios ópticos, pero no útil al momento de hacer funcionar un sensor PIR, pero si recomiendo buscar información al respecto en google, y particularmente dejo el siguiente link:


Donde encontrarán información tanto del sensor y de la lente, formulas matemáticas, así como algunos proyectos muy interesantes y simples (en inglés).

Otros links interesantes:


Y ya entrando en algo un poco más práctico, vamos a familiarizarnos con los pines, los preset´s y el jumper del sensor PIR:

Los pines:
  • GND: masa o 0v
  • VCC +5v
  • OUT es la señal de salida, este lo conectamos al arduino o a un led.
Nota: La salida es del tipo ON/OFF, con lo que puede alimentar a un led o pequeño relé inclusive, si nuestro proyecto es simplemente encender una luz de escalera por ejemplo, no necesitamos ningún arduino.
El pin de salida del modulo PIR es open-colector, esto quiere decir que tenemos que poner una resistencia de 10K a Vcc si queremos accionar un relé o un led. El arduino puede configurarse con una resistencia de pull-up en la entrada digital con lo que no hace falta ponerla físicamente. Cuando el PIR detecte movimiento la salida irá a +3V, y todos los arduinos leen señales de 3V.

Una vez alimentado el PIR, le toma aproximadamente 30 segundos en estabilizar al sensor propiamente dicho.


En varios sitios encontré que se puede alimentar desde 3V hasta 12V, tiene un regulador lo que me da cierta seguridad de decir que es correcto, pero me encuentro en la obligación de no recomendarlo. Con 5V está más que bien, 9V por si usa una batería de 9V y ya 12V puede hacer que se caliente demasiado el regulador y algo se queme (más vale prevenir...)


El jumper:

Tiene dos posiciones, H o L, conectando el jumper entre el pin central y la selección deseada o bien dejarlo sin conectar. He visto en Internet que recomiendan poner el jumper en la posición H para las primeras pruebas que arriba está rotulado como "Disparo Único", pero en algunos casos es más fiable sacar el jumper y dejarlo al aire (es cuestión de probar)

Cuando colocamos el sensor en la posición L, al detectar algo la salida se activará, y al poco se apagará (un segundo aproximadamente) y hará una cadencia tipo blinking LED dependiendo de lo que detecte. A este modo se le llama no retriggering y no suele ser demasiado interesante.

Si lo ponemos en H o lo dejamos libre, cuando detecte movimiento la salida se activará y se mantendrá así durante un tiempo (Llamado retrigger mode) y suele ser más conveniente en la mayoría de los circuitos prácticos.

Los preset´s:

Para ajustar la sensibilidad podemos usar el preset correspondiente, girando en sentido horario aumentamos la sensibilidad

El segundo preset ajusta el tiempo que estará activa la señal de detección después de que ésta haya desaparecido. Pero parece que también afecta al retraso con que inicia la detección, así que es cuestión de jugar para encontrar un punto adecuado según corresponda a las desarrollo decidido.


Algunos ejemplos con arduino:

En Internet hay muchísimos ejemplos, yo he recopilado tres, que me parecen son más que representativos del uso de un sensor PIR asociado a un arduino.

Para probar los dos primeros códigos, se requiere conectar el sensor al arduino de la siguiente forma:

Y los mencionados códigos son los siguientes:

  • El código necesario para realizar la lectura del sensor es realmente muy simple. Se trata de leer la salida del PIR, y hacer parpadear el LED mientras la señal esté activa.
const int LEDPin = 13;

const int PIRPin = 2;
void setup() {
  pinMode(LEDPin, OUTPUT);
  pinMode(PIRPin, INPUT);
}
void loop() {
  if (digitalRead(PIRPin) == HIGH) {
    digitalWrite(LEDPin, HIGH);
    delay(50);
    digitalWrite(LEDPin, LOW);
    delay(50);
  }
  else digitalWrite(LEDPin, LOW);
}

  • Si quisiéramos ejecutar una acción una única vez al detectar movimiento, en lugar de todo el tiempo que la señal este activa, usaríamos el siguiente código.
const int LEDPin = 13;       //pin para el LED

const int PIRPin = 2;        //pin de entrada
int pirState = LOW;          //de inicio no hay movimiento
int val = 0;                 //estado del pin
void setup() {
  pinMode(LEDPin, OUTPUT); 
  pinMode(PIRPin, INPUT);
  Serial.begin(9600);
}
void loop() {
  
  val = digitalRead(PIRPin);
  
  if (val == HIGH) {            //si está activado
    digitalWrite(LEDPin, HIGH); //LED ON
    if(pirState == LOW) {   //si previamente estaba apagado
      Serial.println("Sensor activado");
      pirState = HIGH;
    }
  } else {                     //si esta desactivado
    digitalWrite(LEDPin, LOW); //LED OFF
    if(pirState == HIGH) {//si previamente estaba encendido
      Serial.println("Sensor parado");
      pirState = LOW;
    }
  }
}

  • Y el tercer ejemplo, que más que ejemplo ya de por si solo es una aplicación, lamento que mis conocimientos en música sean muy limitados, el proyecto original se encuentra en:

https://diyhacking.com/arduino-motion-sensor-tutorial/
Requiere de la siguiente configuración de conexionado:
O sea, sumamos un buzzer. 
El código a cargar en el arduino es el siguiente:
/*
Arduino Motion Sensor Burglar Alarm Project
by Arvind Sanjeev
Please check out  http://diyhacking.com for the tutorial of this project.
DIY Hacking
*/

int speakerOut=9;//Piezo buzzer's positive terminal is connected to digital pin 9               
byte names[] = {'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C'};  
int tones[] = {1915, 1700, 1519, 1432, 1275, 1136, 1014, 956};
byte melody[] = "2d2a1f2c2d2a2d2c2f2d2a2c2d2a1f2c2d2a2a2g2p8p8p8p";
// count length: 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
//                                10                  20                  30
int count = 0;
int count2 = 0;
int count3 = 0;
int MAX_COUNT = 24;
int statePin = LOW;
void siren();

 volatile byte intruder;
 void setup() {
   Serial.begin(115200);
   //Initialize the intterrupt pin for the motion sensor (Arduino digital pin 2)
   attachInterrupt(0, intruder_detect, RISING);
   intruder = 0;
 }
 void loop() { }

 //This function is called whenever an intruder is detected by the arduino
 void intruder_detect() {
   intruder++;
   Serial.println("Intruder detected");
   for(int i=0; i<3; i++) siren();//Play the alarm three times
 }

 //This function will make the alarm sound using the piezo buzzer
 void siren() {
  for (count = 0; count < MAX_COUNT; count++) {
    for (count3 = 0; count3 <= (melody[count*2] - 48) * 30; count3++) {
      for (count2=0;count2<8;count2++) {
        if (names[count2] == melody[count*2 + 1]) {       
          analogWrite(speakerOut,1023);
          delayMicroseconds(tones[count2]);
          analogWrite(speakerOut, 0);
          delayMicroseconds(tones[count2]);
        } 
        if (melody[count*2 + 1] == 'p') {
          // make a pause of a certain size
          analogWrite(speakerOut, 0);
          delayMicroseconds(100);
        }
      }
    }
  }
}

En el siguiente link se lo puede observar en funcionamiento (enciendan los parlantes):
https://www.youtube.com/watch?v=TB5vpSCyiSA



miércoles, 9 de noviembre de 2016

Termostato de dos cortes con Arduino

Mientras que los termómetros solo nos dicen cuál es la temperatura del ambiente, los termostatos también son capaces de regularla, entonces, voy a tratar de desarrollar un termostato.

La idea es sencilla, se trata de desarrollar un dispositivo capaz de medir la temperatura e informarla en una pantalla LCD, así cómo setear dos temperaturas desde donde encender/apagar un sistema calefactor o un sistema refrigerador.

Supongamos que necesitamos de un artefacto donde conservar una sustancia dentro de un determinado rango de temperaturas, podría tratarse de un medicamento, el que se debe mantener entre 25 °C y 45 °C. 

O quisiéramos controlar un Aire acondicionado y una estufa eléctrica de modo de encender el aire al alcanzar determinada temperatura y apagarlo cuando la temperatura descienda cierta cantidad de grados, así cómo encender la estufa cuando la temperatura desciende por debajo de un límite inferior y apagarla al alcanzar una temperatura más confortable. 

En ambos casos podemos usar un Arduino para activar/desactivar dos relés de comando de los aparatos refrigerador/calefactor.

En la práctica, no voy a construir ningún aparato ni dispositivo conservador, no lo necesito y dudo de que en algún momento lo necesite, pero nada me impide teorizar al respecto, e implementarlo en Proteus, donde simular el comportamiento de la electrónica y del programa es muy fácil, bondades del CAD (Desarrollo Asistido por Computadora) que permite "armar todo en la compu" sin soldar un solo cable.

Entonces... manos a la obra (o dedos al teclado).

EL HARDWARE:

No hay mucho que decir de la electrónica necesaria, ya que existen shields (módulos comerciales) que cubren todas las necesidades de este proyecto. La construcción simplemente se limitaría a adquirirlos e interconectarlos adecuadamente, entonces, necesitaríamos:

  • Arduino uno R3 (o cualquier otro)
  • Módulo control de dos relés de 5V de alimentación.
  • Módulo keypad con con display.
  • Sensor de temperatura LM35.

Esta lista es solo orientadora, se puede usar electrónica discreta, de echo en Proteus no existen dichos módulos y al momento de simularlos hay que "construirlos".

Algunas páginas para conocer un poco de estos shield´s, así cómo la hoja de datos del LM35:
http://saber.patagoniatec.com/control-modulo-de-reles-arduino-argentina-ptec/
http://www.dx.com/es/p/diy-lm35-linear-temperature-sensor-module-black-166653#.WB8xIdXhCM8
http://www.ti.com/lit/ds/symlink/lm35.pdf

Sin más preámbulos, el circuito desarrollado:


Por supuesto que se puede mejorar esta electrónica, si estuviera usando un módulo control de dos relés tendría un acople óptico (optoacoplador) entre el Arduino y los transistores de conmutación de los relés, lo que aportaría una aislarción de por lo menos 2KV, protegiendo así al componente principal, o sea al Arduino.

En fin, queda a criterio del lector, a efectos de experimentar y verificar el programa, es suficiente este circuito.

En cuanto a Proteus y su simulación de Arduino, hay varios titulares disponibles en la red, solo diré que hay que tener abierto el entorno de programación de Arduino y configurado de modo tal que nos informe donde almacena el archivo .hex; que será en un directorio temporal, al cerrar el entorno, se borra el temporal y deja de funcionar la simulación. claro que siempre se puede copiar al .hex a un directorio seguro, pero como por lo general estamos modificando el programa y verificando en la simulación, no sería práctico recuperar el archivo temporal .hex hasta no acabar definitivamente con la programación (cosa que raras veces terminamos).

Mis preferencias de configuración del entorno (Versión 1.6.9):

Esta configuración me permite ver en el área de notificaciones del entorno el path donde se almacena el archivo .hex:

 La que hay que copiar al porta papeles seleccionándolo con el mouse y luego usando la combinación de teclas <Control + C> (no el botón derecho del mouse), para pegarlo en el editor del componente "SIMULINO" en proteus, opción "Program File":


Un proceso un tanto engorroso, por lo menos la primera vez.

Como dije, hay muchísimos tutoriales en Internet que explican muy bien cómo hacer esto, a título de ejemplo, dejo el siguiente link:
https://www.youtube.com/watch?v=5FDFVUKLVX4

Y otro más, que no conocía y me parece excelente:
http://huborarduino.com/simulacion/curso-simulacion.html

Pasemos al desarrollo de la lógica de control.

EL SOFTWARE:

El programa completo es el siguiente:

#include <EEPROM.h>
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

#define btnRIGHT  0
#define btnUP     1
#define btnDOWN   2
#define btnLEFT   3
#define btnSELECT 4
#define btnNONE   5

const byte PinRefr = 2;
const byte PinCalf = 3;
const byte PinIlum = 10;

float Centigr;
float Kelvin;
float Fahrenh;
float Rankine;
float Reaumur;

int ValorAnalog = 0;
int SelecTemp   = 0;
int TMin        = 20;
int TMax        = 40;
byte RangoTem   = 5;
int SelecMenu   = 0;
int EstBtnAnt   = btnNONE; 
int EstBtnAct   = btnNONE; 
unsigned long tiempo;
char temp[16] = " ";
boolean AuxTMin = false;
boolean AuxTMax = false;

void setup() {
  lcd.begin(16,2);
  pinMode(PinRefr, OUTPUT);
  pinMode(PinCalf, OUTPUT);
  pinMode(PinIlum, OUTPUT);
  digitalWrite(PinRefr, LOW);
  digitalWrite(PinCalf, LOW);
  digitalWrite(PinIlum, HIGH);
  lcd.setCursor(0,0);
  lcd.print(" E. E. S. T. #6 ");
  lcd.setCursor(0,1);
  lcd.print("Prof. VIEGAS B. ");
  delay(2500);
  if (!cargarConfiguracion()) {
    lcd.setCursor(0,0);
    lcd.print("ERROR LECURA MEM");
    lcd.setCursor(0,1);
    lcd.print("Carga conf. def.");
    delay(1500);
  }
  digitalWrite(PinIlum, LOW);
  lcd.clear();
  tiempo = millis();
}

void loop() {
  Centigr = centigr ();
  Kelvin  = kelvin  (Centigr);
  Fahrenh = fahrenh (Centigr);
  Rankine = rankine (Centigr);
  Reaumur = reaumur (Centigr);

  if (Centigr <= TMin & !AuxTMin) {
    digitalWrite(PinCalf, HIGH);
    AuxTMin = true;
  }
  if (Centigr >= TMin + RangoTem & AuxTMin) {
    digitalWrite(PinCalf, LOW);
    AuxTMin = false;
  }

  if (Centigr >= TMax & !AuxTMax) {
    digitalWrite(PinRefr, HIGH);
    AuxTMax = true;
  }
  if (Centigr <= TMax - RangoTem & AuxTMax) {
    digitalWrite(PinRefr, LOW);
    AuxTMax = false;
  }
  
  EstBtnAct = Leer_Botones(); 
  switch(EstBtnAct) {
    case btnRIGHT : if(SelecMenu==0) SelecTemp++; break;
    case btnLEFT  : if(SelecMenu==0) SelecTemp--; break;
    case btnUP    : if(SelecMenu==1) TMin++; else if(SelecMenu==2) TMax++; break;
    case btnDOWN  : if(SelecMenu==1) TMin--; else if(SelecMenu==2) TMax--; break;
    case btnSELECT: SelecMenu++; break;
  }

  if (EstBtnAnt != EstBtnAct) {
    digitalWrite(PinIlum, HIGH);
    delay(100);
    tiempo = millis();
  }
  
  if (SelecMenu > 2) SelecMenu = 0; 
  if (SelecTemp > 3) SelecTemp = 0; 
  if (SelecTemp < 0) SelecTemp = 3;
  
  if (TMin > 135) TMin = 135; 
  if (TMax > 150) TMax = 150; 
  if (TMin > TMax - (RangoTem*2) & SelecMenu == 1) TMin = TMax - (RangoTem*2);   
  if (TMax < TMin + (RangoTem*2) & SelecMenu == 2) TMax = TMin + (RangoTem*2);
  
  lcd.setCursor(0,0);
  switch(SelecMenu) {
    case 0: 
      lcd.print("Temperatura:    ");
      Escribir(Centigr, "C  ", 0);
      switch (SelecTemp) {
        case 0: Escribir(Kelvin , "K", 9); break
        case 1: Escribir(Fahrenh, "F", 9); break;  
        case 2: Escribir(Rankine, "R", 9); break;
        case 3: Escribir(Reaumur, "Re",8); break;
      } 
      break
    case 1: SetearLimites("Setear Lim. Inf."); break;
    case 2: SetearLimites("Setear Lim. Sup."); break
   }
  
  if (millis() - tiempo > 5000) {
    digitalWrite(PinIlum, LOW);
    SelecMenu = 0;
    guardarConfiguracion();
    tiempo    = millis();
  }
  EstBtnAnt = EstBtnAct;
}

int Leer_Botones() {
  ValorAnalog = analogRead(A0);
  if (ValorAnalog > 900)  return btnNONE;
  if (ValorAnalog < 50 )  return btnRIGHT; 
  if (ValorAnalog < 250)  return btnUP;
  if (ValorAnalog < 450)  return btnDOWN;
  if (ValorAnalog < 650)  return btnLEFT;
  if (ValorAnalog < 850)  return btnSELECT; 
  return btnNONE;                      
}

float centigr() { return (500.0 * analogRead(A1))/1023; }
float kelvin (float& cent){ return (cent + 273.15); }
float fahrenh(float& cent) { return (cent * 1.8 + 32); }
float rankine(float& cent) { return ((cent + 273.15)*1.8); }
float reaumur(float& cent) { return (cent / 1.25); }

void Escribir(float& r, String t, byte c) {
  char buffn[8] = " ";   
  dtostrf(r,6,2,buffn);  
  String cadena = buffn;
  cadena.concat(t);
  lcd.setCursor(c,1);
  lcd.print(cadena);
}

void SetearLimites(char s[16]) {
  lcd.print(s); 
  lcd.setCursor(0,1);
  switch(SelecMenu) {
    case 1: sprintf(temp, "%3d Ini. %3d Fin", TMin, TMin + RangoTem); break;
    case 2: sprintf(temp, "%3d Ini. %3d Fin", TMax, TMax - RangoTem); break;
  }  
  lcd.print(temp);
}

boolean cargarConfiguracion(){
  if ((EEPROM.read(0) == 'V') && 
      (EEPROM.read(1) == 'B') &&  
      (EEPROM.read(2) == 'V')) {
    SelecTemp = EEPROM.read(3); 
    TMin      = EEPROM.read(4);  
    TMax      = EEPROM.read(5);
    RangoTem  = EEPROM.read(6);
    return true;
  }
  return false;
}

void guardarConfiguracion(){
  EEPROM.write(0, 'V');
  EEPROM.write(1, 'B');
  EEPROM.write(2, 'V');
  EEPROM.write(3, SelecTemp);
  EEPROM.write(4, TMin);
  EEPROM.write(5, TMax);
  EEPROM.write(6, RangoTem);
}

Voy a tratar de explicarlo.

#include <EEPROM.h>
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

Estas líneas se encargan de incorporar dos librerías externas, la librería EPROM (lo que quiere decir que usaré la memoria eprom del arduino, para almacenar/recuperar la configuración o seteo principalmente de las temperaturas mínima y máxima de corte) y la librería LiquidCrystal, que me soluciona la vida a la hora de usar el display. A continuación la declaración del objeto lcd con sus respectivos pines de conexión.

#define btnRIGHT 0
#define btnUP 1
#define btnDOWN 2
#define btnLEFT 3
#define btnSELECT 4
#define btnNONE 5

Definición de los literales y sus respectivos valores que devolverá la función encargada de leer los botones del Módulo keypad.

const byte PinRefr = 2;
const byte PinCalf = 3;
const byte PinIlum = 10;

Constantes referentes a otros pines usados del arduino, siendo PinRefr el pin encargado de controlar al relé de accionamiento del sistema Refrigerador, PinCalf el encargado del sistema Calefactor y PinIlum el de encender/apagar el led de iluminación del display.

float Centigr;
float Kelvin;
float Fahrenh;
float Rankine;
float Reaumur;

Estas variables de coma flotante almacenan los valores de temperatura leída del LM35, se lee el valor analógico, se convierte a grados centígrados, y luego de centígrados a las demás escalas, a saber:
  • Kelvin (K) 
  • Fahrenheit (F) 
  • Rankine (R) 
  • Réaumur (Re) 
La última de estas escalas no está vigente hoy día. Recomiendo visitar la página de wikipedia:
https://es.wikipedia.org/wiki/Temperatura

int ValorAnalog = 0;
int SelecTemp = 0;
int TMin = 20;
int TMax = 40;
byte RangoTem = 5;
int SelecMenu = 0;
int EstBtnAnt = btnNONE; 
int EstBtnAct = btnNONE; 
unsigned long tiempo;
char temp[16] = " ";
boolean AuxTMin = false;
boolean AuxTMax = false;

Y las variables globales de trabajo necesarias (según mi criterio) en el programa:
  • ValorAnalog: Se usa para determinar el botón pulsado 
  • SelecTemp : Selector de escalas de temperatura a mostrar 
  • TMin : Temperatura mínima o de accionamiento del sistema calefactor 
  • TMax : Temperatura máxima o de accionamiento del sistema refrigerador 
  • RangoTem : Rango entre accionamiento/detención de cualquiera de los sistemas 
  • SelecMenu : Selector del menú a presentar en display 
  • EstBtnAnt : Estado del botón anterior 
  • EstBtnAct : Estado del botón actual (para determinar cambios) 
  • tiempo : Medir tiempos, visualización de un menú, apagado de iluminación, etc 
  • temp[16] : Cadena auxiliar para formateo de datos a presentar en display 
  • AuxTMin : Auxiliar booleana para mantener activo el relé de calefacción 
  • AuxTMax : Auxiliar booleana para mantener activo el relé de refrigeración 

Función de inicialización del dispositivo, necesaria en la estructura de un programa arduino:

void setup() {
  lcd.begin(16,2); //Columnas y filas del objeto lcd

//Configurar pines del arduino
  pinMode(PinRefr, OUTPUT);
  pinMode(PinCalf, OUTPUT);
  pinMode(PinIlum, OUTPUT);

// Setear estado inicial de los pines
  digitalWrite(PinRefr, LOW);
  digitalWrite(PinCalf, LOW);
  digitalWrite(PinIlum, HIGH); // led display encendido

//presentar propaganda por dos segundos y medio 
  lcd.setCursor(0,0);
  lcd.print(" E. E. S. T. #6 ");
  lcd.setCursor(0,1);
  lcd.print("Prof. VIEGAS B. ");
  delay(2500);

//Cargar configuración, de fallar la carga, se informa durante
//un segundo y medio de la situación 
  if (!cargarConfiguracion()) {
    lcd.setCursor(0,0);
    lcd.print("ERROR LECURA MEM");
    lcd.setCursor(0,1);
    lcd.print("Carga conf. def.");
    delay(1500);
  }

//Apagar led, limpiar display y almacenar el tiempo de inicialización 
  digitalWrite(PinIlum, LOW);
  lcd.clear();
  tiempo = millis();
}

A continuación, la función loop o bucle de programa principal:

void loop() {
  Centigr = centigr ();
  Kelvin = kelvin (Centigr);
  Fahrenh = fahrenh (Centigr);
  Rankine = rankine (Centigr);
  Reaumur = reaumur (Centigr);

Lo primero, leer la temperatura desde el sensor LM35 en grados centígrados y aplicar las funciones de conversión a las demás escalas.

  if (Centigr <= TMin & !AuxTMin) {
    digitalWrite(PinCalf, HIGH);
    AuxTMin = true;
  }
  if (Centigr >= TMin + RangoTem & AuxTMin) {
    digitalWrite(PinCalf, LOW);
    AuxTMin = false;
  }

Estos dos condicionales se encargan de encender o apagar la salida digital conectada al relé de calefacción.

SI la temperatura medida es menor o igual a la temperatura mínima Y el relé se encuentra desactivado ENTONCES se enciende el relé y se actualiza el estado de la variable auxiliar que representa su estado.

SI la temperatura medida es mayor o igual a la temperatura mínima más un rango de actuación Y el relé se encuentra activado ENTONCES se apaga el relé y se actualiza el estado de la variable auxiliar que representa su estado.

Dicho de otra manera, lo que hace es encender el relé al alcanzar la mínima, y lo apaga al superar la mínima teniendo en cuenta un rango de 5 grados entre encendido y apagado.

  if (Centigr >= TMax & !AuxTMax) {
    digitalWrite(PinRefr, HIGH);
    AuxTMax = true;
  }
  if (Centigr <= TMax - RangoTem & AuxTMax) {
    digitalWrite(PinRefr, LOW);
    AuxTMax = false;
  }

Los siguientes dos condicionales se comportan de la misma manera, pero actuando en función de la temperatura máxima y de su correspondiente relé. 

Establecido entonces el funcionamiento principal del programa, o sea leer la temperatura y activar el relé que corresponda según se encuentre la temperatura en determinado rango o valor, pasamos a leer el estado de los botones y actuar en consecuencia.

  EstBtnAct = Leer_Botones(); 
  switch(EstBtnAct) {
    case btnRIGHT : if (SelecMenu == 0) SelecTemp++; break;
    case btnLEFT  : if (SelecMenu == 0) SelecTemp--; break;
    case btnUP    : if (SelecMenu == 1) TMin++; else if (SelecMenu == 2) TMax++; break;
    case btnDOWN  : if (SelecMenu == 1) TMin--; else if (SelecMenu == 2) TMax--; break;
    case btnSELECT: SelecMenu++; break;
  }

Lo que hago, básicamente, es: según el botón pulsado y la pantalla visualizada, actuar incrementando o decrementando diversas variables.

 Hay tres posibles pantallas:

La primera es de información de la temperatura medida, la segunda permite setear la mínima temperatura de encendido del sistema calefactor y su desactivación, y la tercer pantalla permite setear la temperatura de encendido del sistema refrigerador y su desactivación.

Entonces, la variable SelecMenu solo podrá tomar los valores 0, 1 y 2 según sea cualquiera de estas tres pantallas de visualización.

Por otro lado, estando en la primer pantalla, se puede informar la temperatura en cualquiera de las posibles escalas previstas:


En las cuatro pantallas, se informa la temperatura en C y en otra escala (K, F, R y Re). Según el valor de la variable SelecTemp se visualiza cualquiera de las cuatro pantallas, o sea dicha variable podrá tomar los valores 0, 1, 2 y 3 correspondiéndose cada valor con una pantalla de información.

Volviendo al código y según el botón pulsado:
  • Sea btnRIGHT y estando en la pantalla de visualización de escalas (SelecMenu == 0), entonces incrementamos SelecTemp para pasar a la siguiente pantalla de visualización.
  •  Sea btnLEFT y estamos en la pantalla de visualización de escalas (SelecMenu == 0), entonces decrementamos SelecTemp para volver a la pantalla de visualización anterior. 
  • Sea btnUP  y estando en la pantalla de seteo de la mínima (SelecMenu == 1) entonces incrementamos la temperatura mínima TMin, si no, estando en la pantalla de seteo de la máxima (SelecMenu == 2) entonces incrementamos la temperatura máxima TMax.
  • Sea  btnDOWN  y estando en la pantalla de seteo de mínima (SelecMenu == 1) entonces decrementamos la temperatura mínima TMin, si no, estando en la pantalla de seteo de la máxima (SelecMenu == 2) entonces decrementamos la temperatura máxima TMax.
  • Sea btnSELECT entonces cambiamos de pantalla de menú incrementando SelecMenu.
Espero se entienda, ya que ese es el "corazón" del sistema de visualización y seteo.

El siguiente condicional se encarga de encender el led de iluminación del display, al pulsar cualquier botón:

  if (EstBtnAnt != EstBtnAct) {
    digitalWrite(PinIlum, HIGH);
    delay(100);
    tiempo = millis();
  }

Si detectamos que se pulsó un botón (EstBtnAnt != EstBtnAct) entonces encendemos el pin de iluminación, damos un mínimo retardo de 100 ms para evitar rebotes falsos y actualizamos la variable tiempo para iniciar un nuevo conteo, el display permanecerá encendido durante cinco segundos a partir de este momento.

  if (SelecMenu > 2) SelecMenu = 0; 
  if (SelecTemp > 3) SelecTemp = 0; 
  if (SelecTemp < 0) SelecTemp = 3;

Estas líneas aseguran los rangos posibles de las variables SelecMenu y SelecTemp.

Si SelecMenu supera el valor 2, entonces se la pone a cero, o lo que es lo mismo, al llegar a la última pantalla, se vuelve a la primera.

Si SelecTemp es mayor a 3, se vuelve a la primer pantalla, si estamos pasando las pantallas de visualización de escalas en forma ascendente, al llegar a la última se vuelve a la primera.

Si SelecTemp es menor a 0, se pasa a la última pantalla, si estamos pasando las pantallas de visualización de escalas en forma descendente, al llegar a la primera se vuelve a la última.

Los siguientes condicionales hacen lo propio con los valores máximo y mínimo de temperatura:

  if (TMin > 135) TMin = 135; 
  if (TMax > 150) TMax = 150; 
  if (TMin > TMax - (RangoTem * 2) & SelecMenu == 1) TMin = TMax - (RangoTem * 2);    
  if (TMax < TMin + (RangoTem * 2) & SelecMenu == 2) TMax = TMin + (RangoTem * 2);

La temperatura minina no puede ser mayor de 135 °C. La máxima está limitada al máximo que permite el LM35.

Por otro lado, se impide que temperatura mínima sea mayor que la temperatura máxima - 10 grados,  así como la máxima no sea menor que la mínima + 10 grados.

Al momento de setear las temperaturas mínima y máxima, teniendo en cuenta un ciclo de histéresis de 5 grados en el funcionamiento de los diversos actuadores, se impide que se pudieran solapar  los límites de apagado de cualquier actuador (no sea cosa que encendamos la estufa y el aire acondicionado al mismo tiempo...)

A continuación, la visualización en el display:

  lcd.setCursor(0,0);
  switch(SelecMenu) {
    case 0:
      lcd.print("Temperatura:    ");
      Escribir(Centigr, "C  ", 0);
      switch (SelecTemp) {
        case 0: Escribir(Kelvin , "K", 9); break;
        case 1: Escribir(Fahrenh, "F", 9); break;
        case 2: Escribir(Rankine, "R", 9); break;
        case 3: Escribir(Reaumur, "Re",8); break;
      }
      break;
    case 1: SetearLimites("Setear Lim. Inf."); break;
    case 2: SetearLimites("Setear Lim. Sup."); break;
   }

No hay mucho que decir de este código ya que solo se trata de presentar el correspondiente menú según sea el estado de las variables SelecMenu y SelecTemp, y la escritura en el display se soluciona a través de una función de formateo: Escribir() que se encuentra un poco más abajo en el programa, o la función -también de formateo- SetearLimites(), por supuesto desarrollada más abajo.

Según sea SelecMenu, se presentan las posibles escalas de visualización de temperatura, o se pasa a las correspondientes opciones de seteo de mínima y máxima.

  if (millis() - tiempo > 5000) {
    digitalWrite(PinIlum, LOW);
    SelecMenu = 0;
    guardarConfiguracion();
    tiempo    = millis();
  }

Este código se encarga de apagar la iluminación del display si pasan 5 segundos sin tocar ningún botón, fija la presentación en la pantalla informativa de temperatura y guarda la configuración.

  EstBtnAnt = EstBtnAct;
}

Por último, se iguala la variable estado de botón anterior (EstBtnAnt) al estado de la variable estado del botón actual (EstBtnAct) quedando a la espera de una nueva pulsación. Y termina aquí el bucle principal del programa.

int Leer_Botones() {
  ValorAnalog = analogRead(A0);
  if (ValorAnalog > 900)  return btnNONE;
  if (ValorAnalog < 50 )  return btnRIGHT; 
  if (ValorAnalog < 250)  return btnUP;
  if (ValorAnalog < 450)  return btnDOWN;
  if (ValorAnalog < 650)  return btnLEFT;
  if (ValorAnalog < 850)  return btnSELECT; 
  return btnNONE;                      

Esta función se encarga de leer el botón pulsado (entrada analógica A0). Como en el modulo display ingeniosamente los botones están en serie para solo usar una entrada analógica del arduino, cada botón dará un valor de tensión distinto y exclusivo a cada botón. Entonces según sea el botón pulsado, la función devuelve un literal acorde.

Las siguientes funciones se encargan de leer la temperatura desde la entrada analógica A1, y convertirla a las otras escalas.

float centigr() { return (500.0 * analogRead(A1))/1023; }

float kelvin (float& cent){ return (cent + 273.15); }

float fahrenh(float& cent) { return (cent * 1.8 + 32); }

float rankine(float& cent) { return ((cent + 273.15)*1.8); }

float reaumur(float& cent) { return (cent / 1.25); }

Siendo la función centigr() la que se encarga de leer la temperatura en °C. Esta fórmula sale de la relación del sensor con los grados. Es fácilmente rastreable por la web pero vamos a intentar explicarla un poco: El sensor de temperatura LM35 responde a variaciones de 10 mV por cada grado centígrado. Si el sensor detecta 1 grado centígrado a la salida del sensor obtenemos 10 mV. Ejemplo: 26,4ºC = 264 mV = 0.264 V.

Tenemos que el convertidor analógico digital del arduino que es de 10 bits de resolución, los valores variarán entre 0 y 1023, entonces Vout = (5V * Dato) / 1023 siendo  (0<Dato<1023) y para ajustar la escala a grados centígrados: Vout = ((5V * Dato) * 100) / 1023, o lo que es lo mismo:
(500.0 * analogRead (A1)) / 1023.

Las otras funciones solo hacen la conversión de centígrado a las otras escalas.

void Escribir(float& r, String t, byte c) {
  char buffn[8] = " ";   //Cadena donde almacenaremos el número convertido
  dtostrf(r,6,2,buffn);  //Llamada a la función
  String cadena = buffn;
  cadena.concat(t);
  lcd.setCursor(c,1);
  lcd.print(cadena);
}

En el caso de presentar con formato un valor de coma flotante, se me presentó el problema que la función sprintf() en arduino no funciona, esto se debe a que el entorno trata de obtener el .hex más chico posible, y esta función no trabaja de la misma manera que en C convencional, ya que es una versión resumida del original. Pero por fortuna existe dtostrf() que si soluciona este problema. Convierte el número real (r) a una cadena de 6 caracteres total con 2 decimales (6,2) devolviendo la cadena formateada en un buffer de tipo char (buffn).

A partir de tener el número ya convertido en cadena, simplemente se lo copia a la variable auxiliar Cadena de tipo String para concatenarla con la letra que representa la escala (parámetro t), por último se sitúa el cursor ya sea en la primer mitad de la segunda fila o la segunda mitad, según corresponda.

void SetearLimites(char s[16]) {
  lcd.print(s); 
  lcd.setCursor(0,1);
  switch(SelecMenu) {
    case 1: sprintf(temp, "%3d Ini. %3d Fin", TMin, TMin + RangoTem); break;
    case 2: sprintf(temp, "%3d Ini. %3d Fin", TMax, TMax - RangoTem); break;
  }  
  lcd.print(temp);
}

La función SetearLimites() hace algo similar, solo que en este caso al tratarse de números enteros si se puede usar la función sprintf() que facilita el formateo, presentando según sea el caso de SelecMenu las temperaturas mínima o máxima.

boolean cargarConfiguracion(){
  if ((EEPROM.read(0) == 'V') && (EEPROM.read(1) == 'B') &&  (EEPROM.read(2) == 'V')) {
    SelecTemp = EEPROM.read(3); 
    TMin      = EEPROM.read(4);  
    TMax      = EEPROM.read(5);
    RangoTem  = EEPROM.read(6);
    return true;
  }
  return false;
}

La función que se encarga de cargar la configuración primero verifica de que en la EPROM se encuentre grababa la "contraseña" VBV (nada muy original), a efectos de no cargar valores aleatorios, de no encontrar dicha cadena devuelve false, y se dejan los valores por defecto para las variables SelecTemp, TMin, TMax y RangoTem.

La primera vez que se energice el dispositivo no encontrará la susodicha contraseña, por ende informará del error de lectura y mantendrá los valores por defecto para esas variables, pero en cuanto el dispositivo funcione el tiempo necesario para guardar la configuración, ya cargará los valores seteados para estas variables.

por último, la función que se encarga de guardar la configuración:

void guardarConfiguracion(){
  EEPROM.write(0, 'V');
  EEPROM.write(1, 'B');
  EEPROM.write(2, 'V');
  EEPROM.write(3, SelecTemp);
  EEPROM.write(4, TMin);
  EEPROM.write(5, TMax);
  EEPROM.write(6, RangoTem);
}

Simplemente escribe en cada posición de la memoria EPROM la contraseña y los valores de las variables. Demás está que diga que la única condición a tener en cuenta es la de leer de las mismas posiciones donde se escribió.

Y hasta aquí, este proyecto "teórico", notese que la variable RangoTem se almacena, pero no es un valor que cambie, esto es porque bien podría ser este un valor modificable dándole un poco más de versatilidad al termostato, dejo como tarea entonces a quien pudiera estar interesado en esto la modificación de dicha variable.