martes, 2 de abril de 2019

Localizando fuentes de luz mediante el uso de fotorresistencias (LDR)

Se denomina fotorresistencia a un componente electrónico cuya resistencia disminuye con el aumento de intensidad de luz que incide sobre el mismo.​ A veces se lo denomina fotoconductor, célula fotoeléctrica o resistencia dependiente de la luz (LDR en inglés). Este tipo de componentes se utilizan mucho en electrónica para detectar situaciones de más o menos luz (por ejemplo, para detectar cuando es de día y de noche...), pero no permiten por si mismos determinar la dirección desde donde procede la luz, lo cual es algo que nos interesará mucho en otros tipo de proyectos.
Desde principio de curso, algunos alumnos de 4º de ESO están trabajando en un proyecto de construcción de un sistema de captación solar de energía fotovoltaica auto-orientable, para posteriormente realizar un estudio de rendimiento del mismo. Para construir este sistema, dividimos el problema en dos partes perfectamente diferenciadas:
  • La primera consistía en la parte mecánica, con la que tuvimos mínimos problemas, y que consideramos más o menos funcional, con algunos pequeños detalles mejorables, los cuales consideramos de poca importancia para nuestras pretensiones. 
  • La segunda parte consistía en un sistema que permitiera localizar la ubicación de una fuente de luz, algo que conceptualmente era bastante simple, y que incluso con un software relativamente sencillo funcionaba a la perfección en el simulador. Sin embargo, en la práctica han surgido numerosos problemas que hemos tenido que ir solucionando poco a poco.

Diseño preliminar del sistema (pirámide)

Para llevar a cabo el sistema de detección de luz, nuestra idea inicial consistía en colocar 4 fotorresistencias distintas orientadas en dos pares Norte-Sur y Este-Oeste, con inclinaciones perpendiculares en cada par. Básicamente colocar una LDR en cada una de los cuatro laterales de una pirámide de base cuadrada cuya inclinación fuera de 45 grados con respecto a su base. De esta forma podrían darse las siguientes situaciones:
  • Que 4 LDR tuvieran la misma resistencia, lo que supondría que todas estarían recibiendo la misma cantidad de luz, y por tanto la pirámide apuntaría a la luz.
  • Que 1 LDR de uno de los pares (N-S o E-O) tuviera menos resistencia la otra. Entonces la luz estaría indiciendo más sobre la misma, y por tanto, habría que mover un poco la pirámide hacia esa fotorresistencia para que recibiera menos a favor de la LDR opuesta.
Sistema de orientación basado en pirámide con inclinación de 45º respecto a horizontal.

Segundo diseño del sistema (paredes perpendiculares)

Después de realizar distintas pruebas, nos dimos cuenta que la inclinación de la LDR respecto al rayo de luz no tenía tanto efecto como el esperado. Si un mismo rayo se proyectaba sobre la LDR, independientemente de su ángulo (salvo inclinaciones muy exageradas), la resistencia que adquiría era similar. Así que tuvimos que modificar el diseño creando una estructura de paredes perpendiculares que proyectaran sombra sobre las LDR cuando un rayo de luz incidiera desde el otro lado de la misma.
Sistema de orientación con "pared perpendicular aislante de luz".
En principio este nuevo diseño no debería dar problemas. Todo indicaba que esta vez si que obtendríamos resultados satisfactorios. Basándonos en el nuevo diseño y en un circuito relativamente simple, pasamos a construir un sistema físico funcional.
Diseño de sistema de orientación con botón de calibración en entorno simulado.
Alumnos de 4º de ESO creando el sistema de orientación fisicamente.
Soldando conexiones del sistema de orientación físico.
Creación sistema orientación (paso 1).
Creando la estructura y colocando las LDR.
Creación sistema orientación (paso 2).
Uniendo las patillas comunes que conectan a 5V.
Creación sistema orientación (paso 3).
Conectando patillaje central con linea de 5V.
Creación sistema orientación (paso 4).
Conectando resto de patillas a línea de datos y res. pulldown.
Creación sistema orientación (paso 5).
Aislando conexiones para crear nueva capa de contactos.
Creación sistema orientación (paso 6).
Conectando resistencias pulldown a línea de tierra.

Creación de circuito de leds para pruebas y sistema de calibración básico

Antes de probar el diseño real, diseñamos un circuito adicional con 5 leds para probar el sistema. Si se recibía más luz por la derecha, se encendería el led derecho (analogamente debería ocurrir lo mismo con el izquierdo, superior e inferior). El led central se encendería en el punto óptimo. Añadimos al circuito un sistema de calibrado semiautomático que determinara la luz máxima y mínima de un determinado entorno (añadiendo un pequeño pulsador para indicar el fin del proceso de calibrado) y procedimos a realizar pruebas en un entorno simulado. Todo funcionaba perfectamente.

Esquema de conexionado del sistema de orientación con cuadro de leds de prueba.
Sin embargo, cuando pasamos a probar el sistema de orientación físico, los resultados no fueron los esperados. En ocasiones el tablero de leds respondía medianamente, pero nunca de forma precisa. Cuando exagerábamos la situación de luz y forzabamos una luz directa hacia las LDR, el circuito tenía un comportamiento más o menos adecuado. Pero usando condiciones de luz medioambiental reales, parecía que algunas fotorresistencias eran más sensibles que otras. Montamos los circuitos una y otra vez, usando distintos componentes para evitar que alguno estuviera defectuoso. Pero siempre ocurría lo mismo: el sistema "no iba fino".

Montaje del cuadro le leds de prueba.
Cuadro de leds montado en protoboard independiente.
Versión del sistema de orientación con cuadro de leds de prueba sobre la misma protoboard.
Pruebas iniciales sobre el sistema de orientación y cuadro de leds con resultados poco satisfactorios.

Rediseñando el sistema de calibrado: tratamiendo independiente de LDR's

Así que nos pusimos a realizar pruebas y mediciones, para posteriormente estudiar los resultados. Nos dimos cuenta que cada fotorresistencia devolvía valores distintos ante condiciones de luz similares.
Por tanto tuvimos que replantear como tratar los datos y optamos por calibrar cada una de las 4 LDR de forma independiente para posteriormente mapearlas a valores discretos conocidos. Para ello usamos un código de calibrado un tanto más complejo que el original.
/*
 * En esta fragmento de código se lee información de las 4 LDR conectadas a HMin, HMax, VMin y VMax
 * buscando para cada una de ellas un valor máximo y otro mínimo, en un proceso continuo que acaba
 * cuando el usuario pulsa el botón de calibrado. Estos valores se almacenan en MinLuz[] y MaxLuz[].
 *
 * La información de todo el proceso se va mostrando por el terminal serie.
 *
 * En la función loop no se ejecuta nada, ya que se está probando el proceso de calibrado unicamente.
 */


#define HMin A2
#define HMax A1
#define VMin A0
#define VMax A3

#define LedCentro 3


#define BotonCalibrado 7


#define NivelesCalibrado 2            //Las lecturas de las LDR se mapearán de 0 a NivelesCalibrado

int MinLuz[4]={1024,1024,1024,1024};  //Valor inicial a corregir en calibrado

int MaxLuz[4]={0,0,0,0};              //Valor inicial a corregir en calibrado

int foto_res[4]={HMin,HMax,VMin,VMax};


void setup() {

  int luz;
  
  char cadena[200]; //Para uso exclusivo en función de conversión dtostrf()
  
  Serial.begin(9600);
  for (int x=0;x<4;x++){ 
      pinMode(foto_res[x], INPUT); 
      };
  pinMode(BotonCalibrado, INPUT);

  
  //Proceso de calibrado, buscando luz máxima y mínima
  while (digitalRead(BotonCalibrado)==0) {
    Serial.println("");
    Serial.print(dtostrf((float)millis()/1000, 6, 1, cadena));
    for (int x=0;x<4;x++) {  
        luz=analogRead(foto_res[x]); 
        MinLuz[x]=(luz<MinLuz[x]?luz:MinLuz[x]); 
        MaxLuz[x]=(luz>MaxLuz[x]?luz:MaxLuz[x]);

        //Impresión en formato:  ----(LDRX: val(map) m:min M:max)
        //                       ----(LDR1: 230(  9) m:  6 M:231)    
        Serial.print("----(LDR"+String(x)+":"+String(luz)+"("+
                     String(map(luz,MinLuz[x],MaxLuz[x],0,NivelesCalibrado))+
                     ")  m:"+String(MinLuz[x])+" M:"+String(MaxLuz[x])+")");
        }
    }
  }

void loop() {

//Aquí iría el código del loop. Aún no programado hasta calibrar correctamente
}
Código de calibrado con salida serie: amarillo (código de control por salida serie), azul (código de calibrado)
Con este código, en la mayoría de las ocasiones todo funcionaba correctamente y el sistema detectaba correctamente el origen de la luz. Pero aún existía algún tipo de problema en determinadas situaciones y no localizabamos ningún error en el código.

Nuevo diseño de calibrado: eliminando lecturas ruidosas

Después de estudiar los datos de control obtenidos por el sistema, nos dimos cuenta que en ocasiones se producían picos máximos (o mínimos) en los mismos. Es como si los LDR repentinamente y durante periodos muy pequeños de tiempo condujeran mejor (o peor) la electricidad (como si les diera mucha luz). ¿Quizá es un defecto de este tipo de dispositivos?¿Quizá sea debido a la histéresis de los mismos?. No lo sabemos. En cualquier caso, estos picos estropeaban el calibrado, forzando al sistema a determinar un máximo que no era real, o al menos no era práctico para la mayor parte de las situaciones. Así que volvimos a pensar en nuevas soluciones. Había que eliminar los picos, pero desconocíamos qué datos eran picos y no sabíamos como identificarlos. Después de pensar mucho optamos provisonalmente por dar un peso de un 20% a los nuevos máximos que encontrasemos. En el caso de que se tratase de picos aleatorios, no afectarían demasiado al calibrado realizado. De esta forma, adaptamos el código de calibrado según el nuevo parámetro.

/*
 * En esta fragmento de código se lee información de las 4 LDR conectadas a HMin, HMax, VMin y VMax
 * buscando para cada una de ellas un valor máximo y otro mínimo, en un proceso continuo que acaba
 * cuando el usuario pulsa el botón de calibrado. Estos valores se almacenan en MinLuz[] y MaxLuz[].
 * 
 * Cada vez que se encuentra un candidato a un nuevo máximo o mínimo, este no sustituye al antiguo
 * máximo o mínimo, sino que afecta al mismo en un portentaje dado por PesoN. De esta forma se evita
 * que lecturas ruidosas afecten al proceso de calibrado de forma significativa. 
 *
 * La información de todo el proceso se va mostrando por el terminal serie.
 *
 * En la función loop no se ejecuta nada, ya que se está probando el proceso de calibrado unicamente.
 */


#define HMin A2
#define HMax A1
#define VMin A0
#define VMax A3

#define LedCentro 3


#define BotonCalibrado 7


#define PesoN 20         //Porcentaje de peso que se asigna a un nuevo máximo o mínimo encontrado

#define PesoV 80         //Porcentaje de peso que se asigna al antiguo máximo o mínimo almacenado

#define NivelesCalibrado 2            //Las lecturas de las LDR se mapearán de 0 a NivelesCalibrado

int MinLuz[4]={1024,1024,1024,1024};  //Valor inicial a corregir en calibrado

int MaxLuz[4]={0,0,0,0};              //Valor inicial a corregir en calibrado

int foto_res[4]={HMin,HMax,VMin,VMax};

void setup() {

  long int luz;
  
  char cadena[20];  //Para uso exclusivo en función de conversión dtostrf()
  
  Serial.begin(9600);
  for (int x=0;x<4;x++){ 
      pinMode(foto_res[x], INPUT); 
      };
  pinMode(BotonCalibrado, INPUT);

  
  //Proceso de calibrado, buscando luz máxima y mínima
  while (digitalRead(BotonCalibrado)==0) {
    Serial.println("");
    Serial.print(dtostrf((float)millis()/1000, 6, 1, cadena));
    for (int x=0;x<4;x++) {  
        luz=analogRead(foto_res[x]); 
        MinLuz[x]=(luz<MinLuz[x]?(luz*PesoN+MinLuz[x]*PesoV)/100:MinLuz[x]); 
        MaxLuz[x]=(luz>MaxLuz[x]?(luz*PesoN+MaxLuz[x]*PesoV)/100:MaxLuz[x]);
        Serial.print("----(LDR"+String(x)+":"+String(luz)+"("+
                     String(map(luz,MinLuz[x],MaxLuz[x],0,NivelesCalibrado))+
                     ")  m:"+String(MinLuz[x])+" M:"+String(MaxLuz[x])+")");
        }
    }
}

void loop() {


}
Codigo de calibrado robusto a ruidos con salida serie: amarillo (control por salida serie), azul (código de calibrado)
Con esta nueva situación, el calibrado parecía funcionar correctamente. El principal problema era que ahora el proceso se hacía mucho más largo, ya que había que esperar a encontrar numerosos máximos para que entre todos se almacenase un máximo bastante cercano a los mismos en la estructura MaxLuz[]. Lo mismo ocurría con los mínimos. Aumentando el peso de los nuevos máximos (PesoN), este tiempo disminuía, pero los picos extraños afectaban más al sistema de calibrado. Es algo que tendríamos que estudiar más (y estudiar su origen), pero queda fuera de este proyecto principalmente por falta de tiempo.

Realizando pruebas con circuito de leds

Finalmente procedimos a realizar pruebas sobre el circuito de leds. Reconstruimos nuevamente el citado circuito ya que lo habíamos desarmado previamente para usar sus piezas en otros proyectos. Por simplificar, eliminamos el led central (si todos los leds restantes se apagaban, el sistema estaría correctamente orientado).
Sistema de orientación en funcionamiento: la salida por serie, y los leds iluminados
informan hacia donde hay que moverlo para obtener la iluminación óptima.
Diseño hardware del sistema de orientación, así como sistema de leds que informan hacia donde hay que girar.
Con ello, comenzamos a probar el nuevo código, y hasta el momento, a falta de tests más exhaustivos, todo parece funcionar correctamente.
/*
 * En esta fragmento de código se lee información de las 4 LDR conectadas a HMin, HMax, VMin y VMax
 * buscando para cada una de ellas un valor máximo y otro mínimo, en un proceso continuo que acaba
 * a los 15 segundos (hemos eliminado el botón provisionalmente). Estos valores se almacenan en
 * MinLuz[] y MaxLuz[].
 * 
 * CALIBRACIÓN ROBUSTA
 * Cada vez que se encuentra un candidato a un nuevo máximo o mínimo, este no sustituye al antiguo
 * máximo o mínimo, sino que afecta al mismo en un portentaje dado por PesoN. De esta forma se evita
 * que lecturas ruidosas afecten al proceso de calibrado de forma significativa. 
 *
 * La información de todo el proceso se va mostrando por el terminal serie.
 *
 * FUNCINAMIENTO
 * La información procedente de las LDR se lee 20 veces y se hace un promedio, eliminado ruido del
 * proceso. El resultado se mapea en tres valores posibles (0, 1 y 2). Comparando estos valores se
 * puede determinar de donde procede la luz y que actuación hay que realizar.
 */


#define VArr A2 //Lila
#define HIzq A0 //Blanco
#define VAba A1 //Gris
#define HDer A3 //Azul

#define HLedIzq 13 //Blanco
#define HLedDer 10 //Azul
#define VLedArr 11 //Lila
#define VLedAba 12 //Gris


#define NivelesCalibrado 2

#define PesoNuevoMinMax 90
#define PesoViejoMinMax 10

#define BotonCalibrado 7
long int MinLuz[4]={1024,1024,1024,1024};  //Valor inicial a corregir en calibrado
long int MaxLuz[4]={0,0,0,0};              //Valor inicial a corregir en calibrado


int foto_res[4]={HIzq,VAba,VArr,HDer};

void setup() {
  long int luz;
  char cadena[20];  //Para uso exclusivo en función de conversión dtostrf()
  
  Serial.begin(9600);
  for (int x=0;x<4;x++){ 
    pinMode(foto_res[x], INPUT); 
    };
  pinMode(BontonCalibrado,INPUT);
p
  
  //Proceso de calibrado, buscando luz máxima y mínima
  
  while (digitalRead(BotonCalibrado)==LOW) { Calibrar hasta pulsar botón
    Serial.println("");
    Serial.print(dtostrf((float)millis()/1000, 6, 1, cadena));
    for (int x=0;x<4;x++) {  
        luz=analogRead(foto_res[x]); 
        MinLuz[x]=(luz<MinLuz[x]?(luz*PesoNuevoMinMax+MinLuz[x]*PesoViejoMinMax)/100:MinLuz[x]); 
        MaxLuz[x]=(luz>MaxLuz[x]?(luz*PesoNuevoMinMax+MaxLuz[x]*PesoViejoMinMax)/100:MaxLuz[x]);
        Serial.print("----(LDR"+String(x)+":"+String(luz)+"("+
                     String(map(luz,(int)MinLuz[x],(int)MaxLuz[x],0,NivelesCalibrado))+
                     ")  m:"+String(MinLuz[x])+" M:"+String(MaxLuz[x])+")");
        delay(10);
        }
    }
}


void loop() {
  int v[4];
  
  //Se hacen 20 lecturas distintas para cada LDR y posteriormente se hace la media 
  //de las mismas, mapeando los resultados a valores entre 0 y NivelesCalibrado.
  for (int x=0;x<4;x++) { v[x]=0; }
  for (int cont=0;cont<20;cont++){
      for (int x=0;x<4;x++) { 
          v[x]=v[x]+analogRead(foto_res[x]);
          }
      delay(20);
      } 

   
  for (int x=0;x<4;x++) { 
      v[x]=map(v[x]/20,MinLuz[x],MaxLuz[x],0,NivelesCalibrado); 
      }
      
  Serial.print( " Izq:"+String(v[0])+" Der:"+String(v[3])+
                " Arr:"+String(v[2])+" Aba:"+String(v[1]));

  //Actuación en función de los valores obtenidos en el vector v[]
  digitalWrite(HLedIzq, (v[0]>v[3]));  
  if (v[0]>v[3]) {Serial.print(" Torcer a izquierda.");} //VIzq>VDer

  digitalWrite(HLedDer, (v[3]>v[0]));  
  if (v[3]>v[0]) {Serial.print(" Torcer a derecha.");}   //VDer>Vizq

  digitalWrite(VLedArr, (v[1]>v[2]));  
  if (v[1]>v[2]) {Serial.print(" Torcer a abajo.");}     //HAba>HArr

  digitalWrite(VLedAba, (v[2]>v[1]));  
  if (v[2]>v[1]) {Serial.print(" Torcer a arriba.");}    //HArr>HAba

  Serial.println("");

  delay(100); 
}
Codigo de orientación funcional: calibrado (azul), lectura robusta de LDRs (verde), actuación (rojo), información (amarillo)
Y con el sistema acabado y funcionando de forma totalmente satisfactoria para nuestras pretensiones, ya solo queda integrar éste en el sistema motor para realizar un control automático. Hemos dedicado para conseguir un funcionamiento óptimo, hemos tenido momentos de desesperación, de querer tirar la toalla porque parecía que todo se volvía en contra y ninguno de los resultados era el esperado. Pero al final, después de mucho trabajar, investigar, rediseñar y aprender sobre la marcha, parece que la espera ha merecido la pena.






No hay comentarios: