sábado, 2 de febrero de 2019

LCD a la vista

Ya llevo algunas entradas por este canal y todavía estoy dando palos de ciego... nuestros proyectos necesitan algo de luz e independencia.
Estaremos de acuerdo que una pantallita a nuestra Arduino nos va a independizar del ordenador con su puerto serie y así poder instalar nuestro ArduinoSystem donde nos plazca.


Existen varias opciones a tener en cuenta, las pantallas de LCD Nokia 5110, están muy estandarizadas por la red, te permite diseñar tus propios gráficos y cuenta con sus propias librerías para Arduino.
A mi personalmente no me satisface demasiado para el proyecto que tenemos en marcha, en primer lugar por que se come 5 pines digitales (ni se han molestado en incorporar un driver) además según el modelo tienes que incluir elementos electrónicos como resistencias con su debida placa de circuito y todo esto para mostrar la información en una pantalla de 1.5"
El LCD 16X2 o 20X4 lo encontramos con su debido driver y nos imprime unas letras bien grandes y hermosas.



Así que vamos a emplear este modelo para nuestras prácticas, y en esta práctica nos haremos un reloj que mostrará la hora, minuto, segundo, dia, mes y año. Y ya que hoy estoy un tanto criticón, tampoco vamos a emplear los RTC que tan extendidos están para nuestra Arduino, principalmente por que en la práctica son una patata, se atrasan, cuelgan el sistema y no puedes ponerlo al día. Esto se debe a que trabajan con interrupciones, algo muy elegante aunque un tanto incompatible con el factor tiempo.
Vamos a crear un reloj propio, independiente y muy preciso.
Para ello nos aprovecharemos de la función millis(), bien es cierto que vamos a tardar un tiempo en hacerlo "muy preciso" pero cuando lo consigamos seremos plenamente felices.
A vueltas con el LCD hay que indicar que tiene algunas curiosidades, como memoria interna o la posibilidad de crear tus propios símbolos, emplean el protocolo I2C (los que incluyen el driver) así que poseerá una dirección exclusiva que debemos conocer.
Para el manejo de estas LCD's disponemos de varias librerías, no exentas de polémicas ya que te pueden dar mas de un dolor de cabeza dependiendo de la compilación que estés empleando.

Vamos a ver el conexionado, que es de lo más sencillo (para los modelos con driver)



VCC a 5 voltios y los demás en el pin del mismo nombre de la Arduino.

Bueno como decía, antes de nada tenemos que conocer que dirección está empleando nuestra LCD, por que de lo contrario no veremos nada de nada.
Para esto alguien se molestó en su momento en crear un código que te indica en que dirección se ha detectado la LCD conectada:

Fíjense que debemos incluir la librería "wire", ésta es necesaria para la gestión de direcciones del protocolo I2C

#include <Wire.h>
void setup(){
  Wire.begin();
  Serial.begin(9600);
  Serial.println("\nI2C Scanner");
}

void loop(){
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0){
      Serial.print("I2C device found at address 0x");
      if (address<16) 
        Serial.print("0");
        Serial.print(address,HEX);
        Serial.println("  !");
      nDevices++;
    } else if (error==4){
      Serial.print("Unknow error at address 0x");
      if (address<16) 
        Serial.print("0");
        Serial.println(address,HEX);
      }    
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
    else
    Serial.println("done\n");
    delay(5000);           // wait 5 seconds for next scan
  }

El código a pesar de lo viciado que está, no solo compila si no que te imprime la dirección correcta de todos los LCD que he comprado...😖

La salida por Serial es la siguiente:


Así que sin vacilaciones copiamos mentalmente o con Ctrl+C el "0x27", ya está, a partir de aquí todo es 'cuestabajo'...(no se como suena esta buena gente ahora mismo, pero un día lo comprobaré )

A continuación os dejo un código básico, comentado a rabiar por que siempre hay alguien que le viene bien:

// LCD_I2C_Basico Compilacion Arduino 1.8.1, 01/03/2019
#include <LiquidCrystal_I2C.h> // Cargamos la libreria LiquidCrystal_I2C
LiquidCrystal_I2C miLCD(0x27,16,2);//Declaramos nuestra variable "miLCD" y le pasamos los parametros (direccion I2C, nºColumnas, nºfilas)

void setup(){
  miLCD.init();// Inicializa el LCD
  miLCD.backlight();// Enciende la iluminacion del LCD
  miLCD.print("Hello world"); // Imprime (por defecto en el primer espacio disponible)
  miLCD.setCursor(0,1); // Situa el cursor en la primera posicion de la segunda linea 
  miLCD.print("funcionando");// Imprime el texto indicado en el parametro
  delay(1000);// espera un segundo
  miLCD.clear();// Limpia el LCD
}

void loop(){
  miLCD.setCursor(0,0);// Situa el cursor en la primera posicion de la primera fila
  miLCD.print("Primera Linea...");// Imprime el texto indicado en el parametro
  miLCD.setCursor(0,1);// Situa el cursor en la primera posicion de la segunda fila
  miLCD.print("Segunda Linea...");// Imprime el texto indicado en el parametro
}

El posterior código os servirá para comprobar las peculiaridades a la hora de imprimir símbolos sobre la LCD, comprobaréis que existen disparidades frente a la impresión Serial:

// LCD_I2C_Simbolos Especiales, Compilacion Arduino 1.8.1, 01/03/2019
// Para hacer uso del metodo printByte debemos declarar lo siguiente:
#if defined(ARDUINO) && ARDUINO >= 100
  #define printByte(args)  write(args);
  #else
  #define printByte(args)  print(args,BYTE);
#endif // fin printByte
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,16,2);
void setup() {
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  Serial.print(char(43));
  Serial.print(char(126));
  Serial.print(char(127));
  Serial.print(char(60));
  Serial.print(char(62));
  
  lcd.print(char(43));lcd.print(" "); lcd.printByte(43);delay(1000);// El uso de printByte es posible por la declaracion en lineas 3 a 7
  lcd.print(char(126));lcd.print(" ");lcd.printByte(126);delay(1000);// Imprime en LCD con dos metodos distintos:
  lcd.print(char(127));lcd.print(" ");lcd.printByte(127);delay(1000);// lcd.print(char()) y lcd.printByte()
  lcd.print(char(60));lcd.print(" ");lcd.printByte(60);delay(1000);// Estos resultados son identicos
  lcd.print(char(62));lcd.print(" ");lcd.printByte(62);delay(1000);// Aunque podria haber variaciones con otros valores
  lcd.clear();

  for (int i=33;i<2048;i++){// caracteres especiales LCD
     // Por serial no imprime los mismos caracteres que en el LCD, además estos últimos se pueden ver diferentes
    Serial.print(i);Serial.print(": ");Serial.printByte(i);Serial.println();
    lcd.setCursor(0,0);lcd.print(i);lcd.print(": ");lcd.printByte(i);
    delay(1000);
    lcd.clear();
  }
}

void loop() {
  // put your main code here, to run repeatedly:
}

Como os comentaba al inicio de esta entrada, es posible crear nuestros propios símbolos, os dejo a continuación en ejemplo básico que os servirá de referencia para que vosotros mismos diseñéis los vuestros:

// LCD_I2C_Crear simbolos personalizados, compilacion Arduino 1.8.1, 01/03/2019
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,16,2);
/* El LCD tiene una estructura de 16 celdas x dos filas
 cada celdas del LCD tienen una estructura de 5x8bits 
 para iluminar uno de estos bits hay que darle el valor 1 
 para crear un simbolo debemos operar sobre la celda que queremos tratar
 creando un array con una longitud igual al nº de filas de la celda(8bits)
 y cada valor del array tendra una longitud igual nº de columnas de la celda(5bits)
 para que arduino reconozca los valores indicados, estos deben formatearse con el prefijo 0b
 seguido de los valores de los bits, 0 apagado y 1 encendido.
 P.D. Tras la ventana que muestra este codigo se muestra una grafica en excel que aclara
 lo descrito*/
byte flechaAbajoL[8]={0b00100,0b00100,0b00100,0b00100,0b00100,0b10101,0b01110,0b00100};
byte flechaAbajoM[8]={0b00000,0b00000,0b00100,0b00100,0b00100,0b10101,0b01110,0b00100};
byte flechaAbajoC[8]={0b0,0b0,0b0,0b0,0b00100,0b10101,0b01110,0b00100};// si el codigo solo cotiene ceros se abrevia con 0b0
byte flechaArribaL[8]={0b00100,0b01110,0b10101,0b00100,0b00100,0b00100,0b00100,0b00100};
byte flechaArribaM[8]={0b00100,0b01110,0b10101,0b00100,0b00100,0b00100,0b00000,0b00000};
byte flechaArribaC[8]={0b00100,0b01110,0b10101,0b00100,0b00000,0b00000,0b00000,0b00000};
byte flechaDerC[8]={0b00000,0b00100,0b00010,0b01111,0b00010,0b00100,0b00000,0b00000};
byte flechaIzqC[8]={0b00000,0b00100,0b01000,0b11110,0b01000,0b00100,0b00000,0b00000};
// ! La memoria del LCD solo puede acumular 8 simbolos personalizados !
// si probaís de descomentar los dos siguientes comprobareis que el noveno(grados) pasa a tomar la posicion 0 de nuestros simbolos
// una solucion a esta limitacion es crear el char con la funcion lcd.createChar() a medida que lo necesitemos dentro del programa
//byte grados[8]={0b01100,0b10010,0b10010,0b01100,0b0,0b0,0b0,0b0}; // 
//byte centro[8]={0b11111,0b10001,0b10001,0b10101,0b10001,0b10001,0b11111,0b0};

void setup() { 
   lcd.init();
   lcd.backlight();

   lcd.createChar(0,flechaArribaC);// Carga en la memoria (posicion 0)del LCD el char flechaArribaC
   lcd.createChar(1,flechaDerC);   // Carga en la memoria (posicion 1)del LCD el char flechaDerC
   lcd.createChar(2,flechaAbajoC); // Carga en la memoria (posicion 2)del LCD el char flechaAbajoC
   lcd.createChar(3,flechaIzqC);  // y asi sucesivamente
   lcd.createChar(4,flechaAbajoL);
   lcd.createChar(5,flechaAbajoM);
   lcd.createChar(6,flechaArribaL);
   lcd.createChar(7,flechaArribaM);
// lcd.createChar(8,grados);
// lcd.createChar(9,centro);
 
   lcd.setCursor(0,0); 
   lcd.write(0); // al tratarse de un char debemos utilizar el metodo write() para imprimir el simbolo 
   lcd.print(char(1)); // aunque se puede emplear el metodo print de este modo
   lcd.write(2);
   lcd.write(3);
   lcd.write(4);
   lcd.write(5);
   lcd.write(6);
   lcd.write(7);
// lcd.write(8);
// lcd.write(9);
}

void loop() {
  for (int i=0;i<4;i++){ // con este bucle creamos movimiento
    lcd.setCursor(0,1);//situamos el cursor en la misma posicion en cada iteracion para sobreescribir la impresion
    // de lo contrario iria avanzando sobre las celdas en cada iteracion
    lcd.write(i);delay(250); 
  }
}




Para finalizar os pongo el programa reloj, como siempre fuertemente comentado, donde además se explican algunos conceptos, el código contiene un método propio para determinar si el año corriente es bisiesto y establecer los días del mes de Febrero, también se hace uso de la EEPROM para ir guardando la hora y así no perderse en caso de reinicio accidental o fallo en el suministro eléctrico.
Otro asunto importante a destacar es el uso de la función millis() en el programa, sin esta sería imposible su operatividad. La función millis() lee el tiempo transcurrido desde que se inicia la Arduino en milisegundos. Podéis conocer mejor esta función desde su  referencia al lenguaje AQUI.

Para que nuestra Arduino sea independiente vamos a hechar mano de la EEPROM, en primera instancia debemos cargar un Sketch donde se establece la fecha:

// CargarFecha, SKETCH PREVIO A LA CARGA DE RelojBasico con LCD, Compilacion Arduino 1.8.1, 01/03/2019
#include <EEPROM.h>
byte seg=0; // Asigna un valor a la variable seg
byte hora=14; // Asigna un valor a la variable hora
byte minuto=29; // Asigna un valor a la variable minuto
byte dia=1; // Asigna un valor a la variable dia
byte mes=3; // Asigna un valor a la variable mes
byte any=19; // Asigna un valor a la variable any
void setup() {
  Serial.begin(9600);
//for(int i=0;i<1024;i++){EEPROM.write(i,0);}// borrado completo de la EEPROM
  EEPROM.write(0,seg); // Establece en la posicion 0 de la EEPROM el valor de seg
  EEPROM.write(1,hora);// Establece en la posicion 1 de la EEPROM el valor de hora
  EEPROM.write(2,minuto);// Establece en la posicion 2 de la EEPROM el valor de minuto
  EEPROM.write(3,dia);// Establece en la posicion 3 de la EEPROM el valor de dia
  EEPROM.write(4,mes);// Establece en la posicion 4 de la EEPROM el valor de mes
  EEPROM.write(5,any);// Establece en la posicion 5 de la EEPROM el valor de any

  // Imprime la hora establecida
  Serial.print(EEPROM.read(1));Serial.print(":");Serial.print(EEPROM.read(2));Serial.print(":");Serial.print(EEPROM.read(0));Serial.print(" ");
  // Imprime la fecha establecida
  Serial.print(EEPROM.read(3));Serial.print("/");Serial.print(EEPROM.read(4));Serial.print("/");Serial.print(EEPROM.read(5)+2000);
  // En la EEPROM solo cabe un byte por eso solo guardamos en ella 19 para la variable any, al recuperar su valor le sumamos 2000 
}

void loop() {}

Una vez tenemos los datos cargados en la EEPROM ya podemos subir el Sketch RelojBasico el cual a partir de este momento mantendrá actualizada la EEPROM segundo a segundo siempre que no le falte la energía.

Ahí va el susodicho:


// RelojBasico con LCD, Compilacion Arduino 1.8.1 01/03/2019
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,16,2);
#include <EEPROM.h>
byte seg=EEPROM.read(0)+2;// cada vez que
byte hora=EEPROM.read(1); // se inicie nuestra 
byte minuto=EEPROM.read(2);// Arduino toma 
byte dia=EEPROM.read(3);// los datos 
byte mes=EEPROM.read(4); // de la EEPROM
int any=EEPROM.read(5)+2000; // Hay que sumarle 2000 para ver el año en su formato completo
long miMilli; // declaramos nuestra variable para el control del tiempo
byte Mes[12]={31,28,31,30,31,30,31,31,30,31,30,31}; // Tabla de los dias del mes
char tSemana[7]={'L','M','X','J','V','S','D'}; // Tabla de los dias de la semana
char dSemana; // Variable que almacena el dia de la semana
//-------------------------------------------------------------------------------------------------------------------------------- void setup
void setup(){
  diaSemana(); // LLamanos a funcion diaSemana para determinar que dia de semana es 
  lcd.init(); // Inicializamos la LCD
  lcd.backlight(); // Encedemos la iluimnacion del LCD
  esbisiesto(any); // Llamamos a la funcion esBisiesto para determinar los dias del mes de febrero
  lcd.setCursor(0,0);lcd.print("Iniciando...");lcd.clear();// Comienza la fiesta                             
}
//-------------------------------------------------------------------------------------------------------------------------------- void loop
void loop() {
  miMilli=millis()/999.22; // Asignamos a nuestra variable el momento en el que nos encontramos para calcualar un segundo
  while(miMilli+1>millis()/999.22){};// Este bucle mantiene el flujo congelado hasta que ha trancurrido un segundo aproximadamente.
  // En este caso el Sketch cotiene las instrucciones elementales para el funcionamiento del reloj, y estas instrucciones tardan 
  // un determinado tiempo en ejecutarse de ahi que se aproxime mucho a los 1000 ms. si en este Sketch añadimos instrucciones (que es la idea)
  // el tiempo de ejecucion será superior y habrá que ajustarse a ese tiempo.
  // Nosotros vamos a controlar el tiempo con la division 999.22, si se adelanta hay que subir el valor de la division y si se atrasa lo bajamos
  // Si la variacion es muy grande empezamos subiendo o bajando una unidad, si tarda muchos dias en desfasarse cambiamos las decimales, hasta que sea preciso.
  // Es muy importante que las dos divisiones sean identicas en las dos lineas
  seg++;EEPROM.write(0,seg);// sumamos un segundo y lo guardamos en la EEPROM
  if (seg > 59){seg=0;EEPROM.write(0,seg);minuto++;EEPROM.write(2,minuto);} // sumamos un minuto al superar los 59 seg. y actualizamos la EEPROM       
  if (minuto > 59){
      if (hora < 23){ 
        minuto=0;EEPROM.write(2,minuto);hora++;EEPROM.write(1,hora); // sumamos una hora si son menos de las 23h. y actualizamos la EEPROM
      }else{ // si son las 23h. la siguiente hora seran la 0h. y actualizamos la EEPROM        
          minuto=0;EEPROM.write(2,minuto);hora=0;EEPROM.write(1,hora);dia++;EEPROM.write(3,dia);
        } // cierra else
  }
  if (dia > Mes[mes-1]){dia=1;mes++;EEPROM.write(3,dia);EEPROM.write(4,mes);}// Comprobamos que el dia del mes no supere lo establecido en su tabla
  if (mes > 12 ){mes=1;EEPROM.write(4,mes);any++;EEPROM.write(5,any-2000);esbisiesto(any);} // sumamos un año si el mes es superior a 12
  // en este momento habra que comprobar si el nuevo año es bisiesto
  lcd.clear(); // Limpiamos la LCD
  if (hora<10){lcd.print("0");} // Añadimos un cero si la hora solo contiene un digito
  lcd.print(hora); // imprime la hora
  lcd.print(':'); // imprime pues eso... dos puntos
  if (minuto<10){lcd.print("0");}// Añadimos un cero si los minutos solo contienen un digito
  lcd.print(minuto); // imprime el minuto
  lcd.print(':');
  if (seg<10){lcd.print("0");}// Añadimos un cero si los segundos solo contiene un digito
  lcd.print(seg); // imprime el minuto
  lcd.setCursor(0,1);// pasamos a la segunda linea
  lcd.print(dSemana);// imprime el dia de la semana 
  lcd.print(" ");
  if (dia<10){lcd.print("0");} // imprime la fecha 
  lcd.print(dia);
  lcd.print('/');
  if (mes<10){lcd.print("0");}
  lcd.print(mes);
  lcd.print('/');
  lcd.print(any);
} // cierra loop
//------------------------------------------------------------------------------------------------------------------------------ Metodo esbisiesto
void esbisiesto(int any){ // Comprueba si el año que recibe es bisiesto 
  if (any %4==0 && any%100!=0 || any%400==0){ // en caso de serlo le asigna al mes de febrero 29 dias
    Mes[1]=29;lcd.print("Bisiesto");lcd.setCursor(0,1);lcd.print("Febrero ");lcd.print(Mes[1]);lcd.print(" dias");
  }else{ // en caso de no ser bisiesto asigna a febrero 28 dias
     Mes[1]=28;lcd.print("No Bisiesto");lcd.setCursor(0,1);lcd.print("Febrero ");lcd.print(Mes[1]);lcd.print(" dias");     
     }
  delay(1000);    
}
//------------------------------------------------------------------------------------------------------------------------------- Metodo diaSemana
void diaSemana(){// Determina el dia de la semana
  byte puntero=1; // sabemos que el 1/1/2019 fue Martes, de ahí que puntero valga 1, que es la posicion de Martes en la tabla tSemana
  byte d=1; // establecemos una variable dia para ir sumando los dias uno a uno
  byte m=1; // establecemos una variable mes para ir sumando los meses uno a uno
  String stfecha=String(dia)+"_"+String(mes);// hacemos un cast y covertimos el d y el m en una string 
  String stF=String(d)+"_"+String(m);// hacemos un cast y covertimos el dia y el mes en una string para ser comparada en el while
  while(!stF.equals(stfecha)){ // mientras la cadena que irá variando el bucle sea distinta a la fecha actual se sumaran dias
    if(puntero>5){puntero=0;}else{puntero++;} // controlamos el valor de puntero, éste nos señalará la posicion final
    d++; // vamos sumando uno a cada iteracion a la variable d
    if (d > Mes[m-1]){d=1;m++;} // comprobacion para sumar un mes
    stF=String(d)+"_"+String(m); // a cada iteracion debemos actualizar la string stF para que el bucle la compare con la fecha actual
  }// cuando las cadenas son identicas se sale del bucle while y se
 dSemana=tSemana[puntero]; // establece el dia de la semana en funcion de donde haya quedado el puntero.   
}

Os dejo una imagen con el programa corriendo en este preciso momento...



Bueno, y con esta entrada doy por concluido el apartado de utilidades, a partir de este momento ya disponemos de todos los conocimientos necesarios y contamos con todas las herramientas para realizar nuestra estación meteorológica.

No hay comentarios:

Publicar un comentario

Gracias por tu comentario, en breve lo revisaremos.