domingo, 9 de diciembre de 2018

Veleta con Sensores de efecto Hall 49E

Hasta ahora hemos dedicado tiempo a la preparación, y ha llegado la hora de tratar los elementos con los que construiremos nuestra estación meteorológica.
Esta publicación esta dedicada a la veleta, y debo decir que ha sido lo mas complicado de implementar, tanto para encontrar el sensor adecuado como por su programación.
Después de probar con infinidad de elementos, y tras algunas semanas de trabajo sin mayores complicaciones con el sensor de efecto Hall 49E, he decidido unánimemente que este es el más idóneo.
El sensor de efecto Hall 49E es un sensor analógico, esto significa que lanza una rango de valores que pueden ir de 0 a 1023.
Si lo conectamos a nuestra Arduino como se muestra en la imagen de más abajo, y le acercamos un imán veremos que por su patilla de salida podemos leer el rango de valores descrito anteriormente, en función de la proximidad del campo Norte magnético.


En AliExpress  compré treinta unidades de estos elementos electrónicos por unos 2 Euros.

Por otro lado un día de muchos que dediqué a buscar ideas por la RED para hacer mi veleta, me tope con un post de un Arduinero que había desmontado una veleta comercial para averiguar su sistema, Resultaba que tenia dispuestos dos sensores de efecto hall (un modelo realmente caro) en forma de L y en el centro un imán sujeto al eje de la veleta. Este Arduinero teorizaba acerca de su funcionamiento sin que ofreciera ni solución práctica ni Sketch alguno, pero eso si, me dió suficiente información como para llevarlo a cabo.

La Idea es la siguiente: el sensor dispuesto verticalmente ofrecería los valores de las coordenadas X y el sensor horizontal ofrecería las coordenadas Y. Como es de conocimiento universal en un plano cartesiano, con un valor X y un valor Y obtenemos un vector, ese vector forma un angulo respecto a la vertical y ese angulo es igual al "componente" que necesitamos para determinar la dirección del viento. ¿Es genial verdad?, pues implementarlo me ha llevado un rato.

Espero que la siguiente ilustración aclare algo un poco más lo anteriormente expuesto:



Aviso, como podéis ir imaginando esta entrada será larga de narices...

Ahora esto tenemos que convertirlo en valores útiles, así que vamos a meterle mano al map()
en el lugar apropiado (ya se verá más adelante) de nuestro Sketch; ponemos algo así:

  vectorX=map(sensorX,0,1023,-100,100);
  vectorY=map(sensorY,0,1023,-100,100);


y el map() nos arroja valores tal como se muestran en la siguiente tabla:


Cuadro 1 = 90º cuadro 2 = 180º cuadro 3 = 270º cuadro 4 = 0º
Lectura
Sensor
Valor
Mapeado
Lectura
Sensor
Valor
Mapeado
Lectura
Sensor
Valor
Mapeado
Lectura
Sensor
Valor
Mapeado
X 1023 100 512 0 0 -100 512 0
Y 512 0 0 -100 512 0 1023 100

A continuación una ilustración creada con una hoja de cálculo para las comprobaciones oportunas:
AQUI MISMO el enlace para descargarlo, si alguien tiene problemas que lo diga.



Hasta aquí muy bien, pero hay que darle las fórmulas de triangulación oportunas a nuestra Arduino para la conversión a grados, además la cosa se complica en este punto, puesto que cada cuadrante requiere una fórmula distinta (no es del todo así), para un matemático esto no será complicado, pero este campo no es mi fuerte, además la capacidad de nuestra Arduino esta bastante limitada, con todo esto no me queda otra que aplicar el siguiente principio: " Toda resolución matemática se puede adaptar a una resolución informática" esto ya lo había comentado anteriormente, un programador con una buena base matemática tiene una gran ventaja, y si no la tienes o lo aprendes o adaptas tu programa con las herramientas que te ofrece la programación.

Mi adaptación es la siguiente, creo cuatro cuadrantes en función del valor arrojado, ya mapeado,
 de 0 a 90º Cuadrante 1(C1) En este caso tanto las X como las Y son positivas
de 91 a 180º Cuadrante 2 (C2) Aquí todas las X son positivas y las Y negativas
de 181 a 270º Cuadrante 3(C3) Ahora las X son negativas y las Y también
de 271 a 360º Cuadrante 4 (C4) y por último las X son negativas y las Y positivas

por lo que C1,C2,C3 y C4 las declaro como booleanas y las condiciono según sigue:

if (y >= 0 && x >= 0){c1=true;} // y+ x+
if (y < 0 && x >= 0){c2=true;} // y- x+
if (y < 0 && x < 0){c3=true;} // y- x-
if (y >= 0 && x < 0){c4=true;} // y+ x-


Declaro la formula para el primer cuadrante:

double formula = 180*(double)(atan((double)(x/y))/PI);


y adapto la formula para cada cuadrante:

if (c1){Serial.println(formula);}
if (c2 || c3){Serial.println(180+formula);}
if (c4){Serial.println(360+formula);}


Antes de continuar con el código debemos configurar la veleta físicamente o al menos hacer un prototipo que nos sirva de pruebas.
Los imanes deben ser cilíndricos y perforados en su interior, por donde pasaremos un eje que irá unido al eje de la veleta, deberán girar solidarios, Además los imanes deben tener sus campos magnéticos dispuestos diametralmente NO en sus caras planas.

Yo compré los que muestro en la siguiente imagen en Aliexpress del vendedor que enlazo AQUI MISMO, me costaron 100 unidades unos 13€


¡Ojo que son muy pequeños y su agujero interior es de sólo 2mm.!

Aquí debajo os muestro un prototipo para hacer las mediciones iniciales montado sobre una miniboard:


Como podéis observar la varilla que empleo como eje es de un diámetro inferior, para acabarla de ajustar le puse una funda termo-retráctil.
La distancia que existe entre el imán y los sensores es escasamente de 1 mm.


Y para las pruebas oportunas os adjunto a continuación un Sketch de pruebas, éste nos va a resultar muy útil para determinar varias cosas.



#include<math.h>
boolean c1,c2,c3,c4;
//double YValue,XValue;//si no disponemos de los sensores, descomentar esta linea y sustituir las lineas 13 y 14 por lo comentado
void setup(){
  Serial.begin(9600);
}  
 
void loop(){ 
  c1=false;
  c2=false;
  c3=false;
  c4=false;
  double YValue =analogRead(0);//YValue = random(219, 866);
  double XValue =analogRead(1);//XValue = random(231, 837);
  double y = map(YValue, 219, 866, -100, 100);
  double x = map(XValue, 231, 837, -100, 100);
  if (y >= 0 && x >= 0){c1=true;} // y+ x+
  if (y < 0 && x >= 0 ){c2=true;} // y- x+
  if (y < 0 && x < 0){c3=true;} // y- x-
  if (y >= 0 && x < 0){c4=true;} // y+ x-
   Serial.print("YValue: ");Serial.print(YValue);Serial.print("  Y: ");Serial.println(y);//descomentar estas dos lineas para ver los valores 
   Serial.print("XValue: ");Serial.print(XValue);Serial.print("  X: ");Serial.println(x);//arrojados por el sensor Hall 49E y comentar la siguiente
// descomentar la siguiente linea para ver los valores ya mapeados y comentar las dos anteriores
// Serial.print("X: ");Serial.print(x);Serial.print("  Y: ");Serial.println(y); 
   double formula = 180*(double)(atan((double)(x/y))/PI);
   if (c1){Serial.println(formula);} // imprime grados igual al componente del viento
   if (c2 || c3){Serial.println(180+formula);}// imprime grados igual al componente del viento
   if (c4){Serial.println(360+formula);}// imprime grados igual al componente del viento
   Serial.println();
  delay(200);
}

Observareis que el rango de los valores indicados en el map() no son los ideales (0,1023) que había propuesto en la idea, esto se debe a dos motivos: los valores arrojados por los sensores dependen de la intensidad del imán y de la distancia en que los coloquemos del mismo, además cada sensor arrojara valores similares pero no exactamente los mismos.
De manera que lo primero será leer el rango de valores que nos arroja cada sensor e indicar su máximo y mínimo en el map (y que no tienen por que coincidir con los que yo indico en mi Sketch)

Para ello podemos comentar de la linea 15 a la 30 (desde los map) y descomentar las líneas 21 y 22.
Serial.print("Yvalue: ");.....
Serial.print("Xvalue: ");.....

Con el programa corriendo, el sensorY conectado en A0 y el sensorX en A1, hacer girar el imán sobre su eje y anotar los valores mínimos y máximos de cada uno de ellos; casi es mejor hacerlo uno después del otro comentando la linea de un sensor, e indicar en el map de cada uno de ellos el rango obtenido.

Una vez concluida la fase de pruebas sólo nos queda montar la veleta (físicamente), ¡que rápido se dice, verdad! Bueno os mostraré una imagen de como la tengo yo montada para que os hagáis una idea:


...bueno si, al final le puse un condensador, algún datasheet muestra en el diagrama básico la puesta de un condensador pero no indica característica alguna de éste así que le puse uno de 1uF 50v (el más pequeño que tenía) lo cierto es que funciona bien con o sin él.

Y ya por último quizás nos quede crear el código para trabajar con él de manera definitiva, el broche final de este blog será un código que recoge los valores de todos los sensores, los imprime por consola, por donde Processing los captura muestra y almacena. Pero mientras tanto os dejo uno para el manejo exclusivo de la veleta.
El siguiente código incluye cosas de las que todavía no he hablado en este momento, como el control del tiempo, la impresión de datos a una LCD y el respaldo de datos a una SD, estos temas se tratarán de manera individualizada para su mayor comprensión.
Por otro lado se incluye un sistema visual del componente por cuadrantes a través de leds(¡Ojo! no confundir con los cuadrantes para los sensores).
Para la gestión de la EEPROM y de los datos desde hace bastante tiempo empleo un sistema un tanto particular (que también explicaré) y que requiere cargar un Sketch antes del programa principal, este código la incluiré después del programa principal para el que lo entienda y sepa manejar,

Ahí va el programa principal:



/*  Version con sensores Hall 49E captura la posicion cada 5 minutos y mapping para guardar en EEprom
ESQUEMA DE CONEXIONES
                                          +-----+
              ____[PWR]___________________| USB |__
             |                            +-----+  |
             |         GND/RST2  [ ][ ]            |
             |       MOSI2/SCK2  [ ][ ]  A5/SCL[ ] |<---- SCL del LCD    
             |          5V/MISO2 [ ][ ]  A4/SDA[ ] |<---- SDA del LCD       
             |                             AREF[ ] |
             |                              GND[ ] |
             | [ ]N/C                    SCK/13[ ] |<---- SCK tarjeta SD    
             | [ ]IOREF                 MISO/12[ ] |<---- MISO tarjeta SD   
             | [ ]RST                   MOSI/11[ ]~|<---- MOSI tarjeta SD   
             | [ ]3V3    +---+               10[ ]~|<----- Led Verde, cuadrante PONIENTE (P) de 226º a 315º
             | [ ]5v    -| A |-               9[ ]~|<----- Led Azul, cuadrante NORTE (N) de 316º a 45º
             | [ ]GND   -| R |-               8[ ] |   
             | [ ]GND   -| D |-                    |
             | [ ]Vin   -| U |-               7[ ] |<----- CS tarjeta SD
             |          -| I |-               6[ ]~|<----- Led Naranja, cuadrante LEVANTE (E) de 46º a 135º   
Sensor 0-->  | [ ]A0    -| N |-               5[ ]~|<----- Led rojo, cuadrante SUR (S) de 136º a 225º 
Sensor 1-->  | [ ]A1    -| O |-               4[ ] |   
             | [ ]A2     +---+           INT1/3[ ]~|  
             | [ ]A3                     INT0/2[ ] |   
             | [ ]A4/SDA  RST SCK MISO     TX>1[ ] |   
             | [ ]A5/SCL  [ ] [ ] [ ]      RX<0[ ] |   
             |            [ ] [ ] [ ]              |
             |  UNO_R3    GND MOSI 5V  ____________/
              \_______________________/
     */

#include<math.h>
#include <EEPROM.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,16,2);//0x3F
boolean c1,c2,c3,c4;
const byte ledRojSur=5;
const byte ledNarLev=6;
const byte ledAzuNor=9;
const byte ledVerPon=10;
boolean traspasado;//ErrorTarjeta, 
int componente;//Veleta
File DatComp,CSComp;
//byte errorTarjeta=EEPROM.read(8);
byte CambioDia=EEPROM.read(7);
int PosVeleta=EEPROM.read(9);
byte PosCtrl_V=EEPROM.read(10);
byte dia=EEPROM.read(2);
byte mes=EEPROM.read(3);
byte hora=EEPROM.read(4);
byte minuto=EEPROM.read(5);
byte segundo=EEPROM.read(6);
byte resto=0;
byte multiplo=5;
unsigned long newMilli;
byte Mes[12]={31,28,31,30,31,30,31,31,30,31,30,31};

void setup(){   
  Serial.begin(9600);//Inicializamos el puerto serie. Serial.begin(115200);
//  Serial.print(EEPROM.read(4));Serial.print(":");Serial.print(EEPROM.read(5));
//  Serial.print(" del " );Serial.print(EEPROM.read(2));Serial.print("/");Serial.println(EEPROM.read(3));
  if(PosCtrl_V==0){PosVeleta=EEPROM.read(9);} // recupera la 
  if(PosCtrl_V==1){PosVeleta=(EEPROM.read(9)+256);} // posicion de registros en funcion del 
  //Serial.print("PosVeleta: ");Serial.print(PosVeleta); Serial.println("...Debe ser 11");
  pinMode(ledRojSur, OUTPUT); // Pin 5 Veleta
  pinMode(ledNarLev, OUTPUT); // Pin 6 Veleta
  pinMode(ledAzuNor, OUTPUT); // Pin 9 Veleta
  pinMode(ledVerPon, OUTPUT); // Pin 10 Veleta
  //------------------------------------------------------------------------------------LCD
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0,0);lcd.print("Iniciando...");delay(300);lcd.clear();
  //------------------------------------------------------------------------------------
  InitSD();
}
void loop(){ 

  newMilli=millis()/996.5; //  con 996.2 se adelanta
  while(newMilli+1>millis()/996.5){};// se inicia  seg atrasado
  segundo++;
  
  componente=calculaGrados();
  
  if((componente >= 0 && componente <= 45) || (componente > 315 && componente <= 360)){digitalWrite(ledAzuNor,HIGH);digitalWrite(ledNarLev,LOW);digitalWrite(ledRojSur,LOW);digitalWrite(ledVerPon,LOW);}
  if(componente > 45 && componente <= 135){digitalWrite(ledAzuNor,LOW);digitalWrite(ledNarLev,HIGH);digitalWrite(ledRojSur,LOW);digitalWrite(ledVerPon,LOW);}
  if(componente > 135 && componente <= 225){digitalWrite(ledAzuNor,LOW);digitalWrite(ledNarLev,LOW);digitalWrite(ledRojSur,HIGH);digitalWrite(ledVerPon,LOW);}
  if(componente > 225 && componente <= 315){digitalWrite(ledAzuNor,LOW);digitalWrite(ledNarLev,LOW);digitalWrite(ledRojSur,LOW);digitalWrite(ledVerPon,HIGH);}
  if(componente > 360){digitalWrite(ledAzuNor,HIGH);digitalWrite(ledNarLev,HIGH);digitalWrite(ledRojSur,HIGH);digitalWrite(ledVerPon,HIGH);}//digitalWrite(alarma,HIGH); 
  
  if (segundo == 1 && resto==0){
         lcd.clear();lcd.setCursor(0,0);lcd.print("Grabando: ");lcd.print(componente);
         lcd.setCursor(0,1);lcd.print("en Posicion: ");lcd.print(PosVeleta);delay(500);lcd.clear(); 
         if (PosVeleta <= 255){EEPROM.write(10,0);EEPROM.write(9,PosVeleta);}// guarda en PosCtrl_V; EEPROM(10) el numero
         if (PosVeleta > 255 && PosVeleta <= 511){EEPROM.write(10,1);EEPROM.write(9,PosVeleta);}// en giro de bytes que dara       
         byte x = map(componente, 1, 360, 1, 255);// convierte el valor(componente)a un byte para poder guardarlo en la EEprom
         EEPROM.write(PosVeleta,x);PosVeleta++;
  }
  
  if (segundo > 59){segundo=0;EEPROM.write(6,segundo);minuto++;EEPROM.write(5,minuto);}
  if (minuto > 59){minuto=0;EEPROM.write(5,minuto);
      if (hora < 23){hora++;EEPROM.write(4,hora);
        }else{CambioDia=2;EEPROM.write(7,2);hora=0;EEPROM.write(4,hora);dia++;EEPROM.write(2,dia); 
            for(int i=11;i<=299;i++){EEPROM.write(i+289,EEPROM.read(i));delay(2);} // hace CS de los registros del dia en la misma EEprom    
            InitSD();
            if (CambioDia==2){ 
               traspasado=Traspas_SD();
               EEPROM.write(1023,traspasado);                 
               CSComp=SD.open("CSComp", FILE_WRITE);
               for(int i=300;i<=588;i++){CSComp.println(EEPROM.read(i));delay(2);}
               CSComp.close(); 
               PosVeleta=11;EEPROM.write(9,PosVeleta);
               PosCtrl_V=0;EEPROM.write(10,PosCtrl_V);   
               CambioDia=1;EEPROM.write(7,1);
           }         
      }
  }
 resto=minuto%multiplo;
//---------------------------------------IMPRESION LCD    
 lcd.setCursor(0,0);lcd.clear();if (hora<10){lcd.print("0");}lcd.print(hora);lcd.print(":");if(minuto<10){lcd.print("0");}lcd.print(minuto);lcd.print(":");
 if (segundo<10){lcd.print("0");}lcd.print(segundo);lcd.print(" ");
 lcd.setCursor(11,0);if(dia<10){lcd.print("0");}lcd.print(dia);lcd.print("/");if(mes<10){lcd.print("0");}lcd.print(mes);
 lcd.setCursor(0,1);lcd.print("Componente: ");lcd.print(componente);
//---------------------------------------------

Serial.println(componente);

 if (mes!=EEPROM.read(3)){EEPROM.write(3,mes);}
 if (dia > Mes[mes-1]){dia=1;mes++;EEPROM.write(2,dia);EEPROM.write(3,mes);}
 if (mes > 12 ){mes=1;EEPROM.write(3, mes);}   
}//------------------------------------------------------------------------------------------------------------ cierra LOOP

void InitSD(){
   if (!SD.begin(7)){
      lcd.setCursor(0,0);lcd.print("SD NO conectada");delay(300);lcd.clear();
   }else{lcd.setCursor(0,0);lcd.print("SD en Conexion");delay(300);lcd.clear();} 
  if(!SD.exists("DtComp")){ 
      lcd.setCursor(0,0);lcd.print("Error Lectura SD");delay(400);lcd.clear();
  }else{lcd.setCursor(0,0);lcd.print("Componente existe ");delay(700);lcd.clear();} //   Serial.println("Componente existe ");
}//cierra funcion InitSD

boolean Traspas_SD(){
   InitSD();
   DatComp = SD.open("DtComp",FILE_WRITE);
   for(int i=11;i<=299;i++){
      int x = map(EEPROM.read(i), 1, 255, 1, 360);// convierte el valor(componente) guardado en la EEprom(como Byte) a Int
      DatComp.println(x);delay(2);
   }
   boolean z=SD.exists("DtComp");
   DatComp.close();
   return z;
}//cierra funcion Traspas_SD

int calculaGrados (){ // funcion denominada calculaGrados(), esta funcion determina que potenciometro debe emplear y devuelve su valor.
   c1=false;
   c2=false;
   c3=false;
   c4=false;
   
   double YValue =analogRead(0);
   double XValue =analogRead(1);

   double x = map(XValue, 231, 837, -100, 100);//XValue, 250, 766, -100, 100
   double y = map(YValue, 219, 866, -100, 100);//YValue, 197, 866, -100, 100
  
   if (y >= 0 &&  x >= 0){c1=true;} // y+ x+
   if (y < 0  &&  x >= 0){c2=true;} // y- x+
   if (y < 0  &&  x < 0 ){c3=true;} // y- x-
   if (y >= 0 &&  x < 0) {c4=true;} // y+ x-
  
   double formula = 180*(double)(atan((double)(x/y))/PI);
   
   if (c1){return formula;}
   if (c2 || c3){return (180+formula);
   } else { return (360+formula);}
} // Cierra funcion 

y a continuación os dejo el código para que el programa principal gestione correctamente los datos y la EEPROM.
Recuerdo que el siguiente código debe cargarse antes que el programa principal, en definitiva hay que indicarle hora, fecha y el día juliano, que es 0 para el 1 de enero y 365 para el 31 de diciembre.
PosVeleta, es la variable que almacena la posición de la EEPROM donde debe guardarse el siguiente registro.
PosCtrl_V, es una variable auxiliar para PosVeleta, ésta última se guarda en la EEPROM, cuando el valor de PosVeleta supera el byte,  PosCtrl_V suma uno, y el programa principal determina la posición donde guardar el siguiente registro sumando 255 al valor de PosVeleta.
Si la Arduino se reinicia por algún motivo la posición queda guardada en la EEPROM, pero si el tiempo de parada es elevado habrá que actualizar la hora o minuto y en consecuencia los valores de PosVeleta y PosCtrl_V en su caso, lógicamente cada minuto del día esta asociado a una posición concreta de PosVeleta, este dato será necesario conocerlo para indicárselo.

Sketch previo al programa principal:



// PROGRAMA PREVIO AL PRINCIPAL PARA LA VELETA CON SENSOR HALL 49E
#include <EEPROM.h> // sentecia para EEPROM
int PosVeleta=0;
int PosAnem=0;
void setup() {
Serial.begin(9600);
// *********************************************** BORRADO/ESCRITURA DE DATOS
//  EEPROM.write(0,220);// Jul0
//  EEPROM.write(1,0);// Jul1
  EEPROM.write(2,29);// dia
  EEPROM.write(3,7);// mes
  EEPROM.write(4,10);// hora
  EEPROM.write(5,38);// minuto
  EEPROM.write(6,0);// seg
  EEPROM.write(7,1);// cambioDia
//  EEPROM.write(9,138);// PosVeleta
//  EEPROM.write(10,0);// PosCtrl_V
/*
 for (int i=11;i<=299;i++){ // registro veleta
   EEPROM.write(i,0);
 }
  for (int i=300;i<=588;i++){ // CS registros
   EEPROM.write(i,0);
 }
*/
// *****************************************************************
// ----------------------------------------------------------------- LECTURA
Serial.print("Ultimo Registro a las " );Serial.print(EEPROM.read(4));Serial.print(":");Serial.print(EEPROM.read(5));
Serial.print(" del " );Serial.print(EEPROM.read(2));Serial.print("/");Serial.println(EEPROM.read(3));
//Serial.print("Juliano: " );Serial.println(EEPROM.read(0)+EEPROM.read(1));
Serial.print("CambioDia: "  );Serial.println(EEPROM.read(7));
Serial.print("Error Tarjeta: " );Serial.println(EEPROM.read(8));
Serial.print("Pos Eprom: " );Serial.println(EEPROM.read(9));
Serial.print("PosCtrl: " );Serial.println(EEPROM.read(10));
if(!EEPROM.read(10)){PosVeleta=EEPROM.read(9);} // recupera la posicion del ultimo 
if(EEPROM.read(10)){PosVeleta=(EEPROM.read(9)+255);} //  registro en funcion del valor de PosCtrl (EEPROM (10))
Serial.print("Pos_Veleta: " );Serial.println(EEPROM.read(9));
Serial.print("Valor CambioDia: " );Serial.println(EEPROM.read(1024));
Serial.print("Valor Traspasado: " );Serial.println(EEPROM.read(1023));
/*
Serial.println("Lectura registros Veleta:");
for(int i=11;i<=299;i++){
  Serial.println(EEPROM.read(i));
}
Serial.println("Lectura registros CopiaSeguridad:");
for(int i=300;i<=588;i++){
  Serial.println(EEPROM.read(i));
}
Serial.println("Lectura Indiscriminada:");
for (int i=0;i<=1024;i++){
  Serial.println (EEPROM.read(i));
}
*/
// -------------------------------------------------------

}// CIERRA VOID

void loop() {
}


y con esto y un bizcocho ayer fue día veintiocho.