Mostrando entradas con la etiqueta LCD. Mostrar todas las entradas
Mostrando entradas con la etiqueta LCD. Mostrar todas las entradas

viernes, 1 de enero de 2016

Tres en raya

Después de más de un año sin publicar vuelvo a la carga con un montaje para jugar al tres en raya con arduino. Para ello usaré una pantalla LCD y un joystick. El esquema es el siguiente:




En él hemos usado una pantalla Nokia LCD 5110, un módulo joystick, 6 resistencias de 560 Ω y 6 de 1KΩ. Las resistencias bajan el voltaje de 5V a 3.3V, que es el que usa el módulo LCD. Las conexiones del módulo LCD, que no se aprecian en el esquema son las siguientes:

  • Cables rojos. Pines VCC y Led
  • Cable amarillo. Pin RST
  • Cable verde. Pin D/C
  • Cable azul. Pin DN(MOSI)
  • Cable naranja. Pin SCLK
  • Cables negros. Pines GND y SCE

El joystick tiene los siguientes pines:

  • GND. Conectado a tierra
  • SW. Nombrado como SEL en el esquema. Es un pin digital. Detecta cuándo se ha pulsado el botón del joystick. En el que he usado devuelve LOW cuando se pulsa y HIGH en posición normal.
  • VRx. Nombrado como Xout en el esquema. Es un pin analógico que devuelve un valor entre 0 y 1023 dependiendo de la posición del eje X del joystick.
  • VRy. Nombrado Yout en el esquema. Es un pin analógico que devuelve un valor entre 0 y 1023 dependiendo de la posición del eje Y del joystick.
  • +5V. Nombrado Vcc en el esquema. Es la alimentación del joystick.
El funcionamiento es muy sencillo. Con el joystick moveremos un cursor por la pantalla LCD y al pulsar el botón del joystick pondremos nuestra ficha en la casilla seleccionada. Inmediatamente el microcontrolador hará su jugada y nos devolverá el control para la siguiente.
Aquí está montado en la placa de prototipo:

Y el montaje en funcionamiento:





El código fuente del sketch se divide en dos ficheros:

  • types.h. Este fichero es necesario porque el compilador de arduino obliga a que los tipos de datos definidos por el usuario y las funciones que tienen como valor de retorno alguno de estos tipos estén en un fichero separado del principal.
enum cellState{empty,black,white}; //Estados de cada casilla del juego

struct boardGame{
  byte xCursor; //Coordenada x del cursor  byte yCursor; //Coordenada y del cursor
  cellState board[3][3]{{empty,empty,empty},{empty,empty,empty},{empty,empty,empty}}; //Tablero de juego
  byte emptyCells=9; //Numero de casillas sin fichas
  bool stopPlay=false; //Parar el juego
};


/*****************************************************************************/
/* Funcion winner. Devuelve el color de la jugada ganadora en el  */
/* estado actual del juego. Empty si nadie gana                             */
/*                                                                                                    */
/* Parametros:                                                                                */
/* boardGame b-->Tablero de juego                                                */
/*****************************************************************************/


cellState winner(boardGame b){
 
  for(byte i=0;i<3;++i){
    for(byte j=0;j<3;++j){
      if(b.board[i][j]!=empty){
       
        //horizontal
        bool tmpWin=true;
        for(byte x=1;x<3;++x){
          tmpWin=tmpWin && (b.board[x][j]==b.board[x-1][j]);
        }
        if(tmpWin){
          return b.board[i][j];
        }
        //vertical
       
        if(!tmpWin){
          tmpWin=true;
          for(byte y=1;y<3;++y){
            tmpWin=tmpWin && (b.board[i][y]==b.board[i][y-1]);
          }
        }
       
        if(tmpWin){
          return b.board[i][j];
        }
        //diagonalVID_20151226_131816
       
        if(!tmpWin && i==0 && j==0){
          tmpWin=true;
           for(byte x=1,y=1;x<3 && y<3;++x,++y){
             tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y-1]);
          }
        }
        if(tmpWin){
          return b.board[i][j];
        }
       
        if(!tmpWin && i==0 && j==2){
          tmpWin=true;
            for(byte x=1,y=1;x>0 && y<3;++x,--y){
              tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y+1]);
             
            }
        }
        if(tmpWin){
          return b.board[i][j];
        }
       }
     }
    }
    return empty;
}

     
  • 3EnRaya.ino
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include "types.h"


//LCD

const byte SCLKpin = 13;
const byte DNpin = 11;
const byte DCpin = 12;
const byte RESETpin = 10;


//Joystick

const int xAxis=A1;
const int yAxis=A0;
const int button=2;

const char contrast=0;
int fontWidth=6;
int fontHeigth=10;
int textSize=2;

boardGame board;

Adafruit_PCD8544 lcd(SCLKpin,DNpin,DCpin,RESETpin);

/******************************************************************/
/* Funcion evaluatePlay. Devuelve el numero de jugadas ganadoras  */
/* si se juega una ficha en una casilla determinada               */
/*                                                                */
/* Parametros:                                                    */
/* boardGame b-->Tablero de juego                                 */
/* short x-->Coordenada x de la casilla que vamos a evaluar       */
/* short y-->Coordenada y de la casilla que vamos a evaluar       */
/* cellState c-->Color de la ficha con la que se pretende jugar   */
/******************************************************************/


short evaluatePlay(boardGame b,short x,short y,cellState c){
 
  short winCount=0;
  short cellCount=0;
  //Horizontal
  for(byte j=0;j<3;++j){
    if(b.board[x][j]==c || b.board[x][j]==empty){
      ++cellCount;
    }
  }
  if(cellCount==3){
    ++winCount;
  }
  cellCount=0;
  //Vertical
  for(byte i=0;i<3;++i){
    if(b.board[i][y]==c || b.board[i][y]==empty){
      ++cellCount;
    }
  }
  if(cellCount==3){
    ++winCount;
  }
  cellCount=0;
  // diagonal
  if(x==y){
    for(byte i=0,j=0;i<3 && j<3;++i,++j){
       if(b.board[i][j]==c || b.board[i][j]==empty){
         ++cellCount;
        }
     }
     if(cellCount==3){
       ++winCount;
     }
  }
  cellCount=0;
  if(x==y || abs(x-y)==2){
    for(byte i=0,j=0;i>0 && j<3;++i,--j){
       if(b.board[i][j]==c || b.board[i][j]==empty){
         ++cellCount;
       }
    }
    if(cellCount==3){
       ++winCount;
     }
  }
  return winCount;
}

/******************************************************************/
/* Funcion drawWinnerPlay. Dibuja una linea sobre la jugada       */
/* ganadora                                                       */
/*                                                                */
/* Parametros:                                                    */
/* boardGame b-->Tablero de juego                                 */
/******************************************************************/


void drawWinnerPlay(boardGame b){
 
  for(byte i=0;i<3;++i){
    for(byte j=0;j<3;++j){
      if(b.board[i][j]!=empty){
       
        //horizontal
        bool tmpWin=true;
        for(byte x=1;x<3;++x){
          tmpWin=tmpWin && (b.board[x][j]==b.board[x-1][j]);
        }
        if(tmpWin){
         
          lcd.drawLine(0, ((j+1)*16)-8, 84, ((j+1)*16)-8, BLACK);
        }
        //vertical
       
        if(!tmpWin){
          tmpWin=true;
          for(byte y=1;y<3;++y){
            tmpWin=tmpWin && (b.board[i][y]==b.board[i][y-1]);
          }
          if(tmpWin){
           
            lcd.drawLine(((i+1)*28)-14, 0, ((i+1)*28)-14, 84, BLACK);
          }
        }
       
       
        //diagonal
       
        if(!tmpWin && i==0 && j==0){
          tmpWin=true;
           for(byte x=1,y=1;x<3 && y<3;++x,++y){
             tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y-1]);
          }
          if(tmpWin){
           
            lcd.drawLine(0, 0, 84, 84, BLACK);
          }
        }
       
       
        if(!tmpWin && i==0 && j==2){
          tmpWin=true;
            for(byte x=1,y=1;x>0 && y<3;++x,--y){
              tmpWin=tmpWin && (b.board[x][y]==b.board[x-1][y+1]);
             
            }
           if(tmpWin){
            
              lcd.drawLine(0, 47, 84, 0, BLACK);
          }
        }
       
       }
     }
    }
}

/******************************************************************/
/* Funcion play. Ejecuta la jugada de la maquina                  */
/*                                                                */
/******************************************************************/


void play(){
  byte x;
  byte y;
  short maxEvaluation=-32768;
  short currentEvaluation=0;
  bool stopIteration=false;
  //Compruebo si alguna jugada me da la victoria
  for(byte i=1;i<4 && !stopIteration;++i){
    cellState *statesArray=board.board[i-1];
    for(byte j=1;j<4 && !stopIteration;++j){
     
      if(board.board[i-1][j-1]==empty){
       
        statesArray[j-1]=white;
        if(winner(board)==white){
         
          stopIteration=true;
          board.stopPlay=true;
        }
        else{
          statesArray[j-1]=empty;
        }
       }
      }
    }
    //Compruebo si alguna jugada da la victoria al oponente
    for(byte i=1;i<4 && !stopIteration;++i){
      cellState *statesArray=board.board[i-1];
      for(byte j=1;j<4 && !stopIteration;++j){
     
        if(board.board[i-1][j-1]==empty){
         
          statesArray[j-1]=black;
          if(winner(board)==black){
           
            statesArray[j-1]=white;
            stopIteration=true;
          }
          else{
            statesArray[j-1]=empty;
          }
         }
        }
      }
   
    if(!stopIteration){
      for(byte i=1;i<4;++i){
        for(byte j=1;j<4;++j){
          if(board.board[i-1][j-1]==empty){
           
            currentEvaluation=evaluatePlay(board,i-1,j-1,white);
           
            if(currentEvaluation>maxEvaluation){
              maxEvaluation=currentEvaluation;
              x=i;
              y=j;
            }
           }
          }
        }
       
        cellState *statesArray=board.board[x-1];
        statesArray[y-1]=white;
      }
     
  }

/******************************************************************/
/* Funcion drawTile. Dibuja una ficha en una posicion             */
/*                                                                */
/* Parametros:                                                    */
/* byte x-->Coordenada x de la casilla donde vamos a jugar        */
/* byte y-->Coordenada y de la casilla donde vamos a jugar        */
/* int color-->Color de la ficha con la que se pretende jugar     */
/******************************************************************/


void drawTile(byte x,byte y,int color){
  lcd.drawCircle((x*28)-14, (y*16)-8, 5,BLACK);
  if(color==BLACK){
    lcd.fillCircle((x*28)-14, (y*16)-8,5,color);
  }
}

/******************************************************************/
/* Funcion drawCross. Dibuja el cursor en una posicion            */
/*                                                                */
/* Parametros:                                                    */
/* byte x-->Coordenada x de la casilla donde vamos a jugar        */
/* byte y-->Coordenada y de la casilla donde vamos a jugar        */
/* int color-->Color de la ficha con la que se pretende jugar     */
/******************************************************************/


void drawCross(byte x,byte y,int color){
  lcd.drawFastVLine((x*28)-14,(y*16)-13, 10, color);
  lcd.drawFastHLine((x*28)-19, (y*16)-8, 10, color);
}

/******************************************************************/
/* Funcion drawGameScreen. Dibuja el tablero de juego             */
/*                                                                */
/******************************************************************/


void drawGameScreen(){
 
  lcd.drawRoundRect(0, 0, 84, 48,10,BLACK);
  lcd.drawLine(28, 0, 28, 84, BLACK);
  lcd.drawLine(56, 0, 56, 84, BLACK);
  lcd.drawLine(0, 16, 84, 16, BLACK);
  lcd.drawLine(0, 32, 84, 32, BLACK);
  lcd.drawLine(0, 32, 84, 32, BLACK);
 
  for(byte i=1;i<4;++i){
    cellState *statesArray=board.board[i-1];
    for(byte j=1;j<4;++j){
      if(statesArray[j-1]!=empty){
        drawTile(i,j,(statesArray[j-1]==black)?BLACK:WHITE);
      }
    }
   }
  
   cellState *statesArray=board.board[board.xCursor-1];
   drawCross(board.xCursor,board.yCursor,(statesArray[board.yCursor-1]==empty || statesArray[board.yCursor-1]==white)?BLACK:WHITE);
   drawWinnerPlay(board);
}



void setup(){            
  Serial.begin(115200) ;
 
  lcd.begin();
  lcd.clearDisplay();
  lcd.setTextColor(BLACK);
  lcd.setTextSize(textSize);
  board.xCursor=2;
  board.yCursor=2;
  drawGameScreen();
  lcd.display();
  pinMode(button,INPUT_PULLUP);
}

void loop(){ 
 
    if(!board.stopPlay){
      bool redrawScreen=false;
     
      if(analogRead(xAxis)==1023 && board.xCursor>1){
        //Se ha movido el joystick a la izquierda 
        board.xCursor--;
        redrawScreen=true;
      }
     else if(analogRead(xAxis)==0 && board.xCursor<3){
       //Se ha movido el joystick a la derecha
       board.xCursor++;
       redrawScreen=true;
     
     }
     else if(analogRead(yAxis)==1023 && board.yCursor<3){
       //Se ha movido el joystick hacia arriba
      board.yCursor++;
      redrawScreen=true;
     }
     else if(analogRead(yAxis)==0 && board.yCursor>1){
       //Se ha movido el joystick hacia abajo
      board.yCursor--;
      redrawScreen=true;
     }
     else if(!digitalRead(button)){
       //Se ha pulsado el boton
       cellState *statesArray=board.board[board.xCursor-1];
       if(statesArray[board.yCursor-1]==empty){
         statesArray[board.yCursor-1]=black;
         board.emptyCells--;
         if(board.emptyCells>0){
           play();
         }
         else{
           board.stopPlay=true;
         }
         redrawScreen=true;
       }
     }
  
     if(redrawScreen){
       lcd.clearDisplay();
        drawGameScreen();
        lcd.display();
        delay(500);
     
     }
    }
   
  }

     

lunes, 30 de diciembre de 2013

Nivelando superficies con un acelerómetro

Cómo funciona un acelerómetro

Un acelerómetro, como su nombre indica, mide la aceleración a la que está sometido. Si lo movemos, nos medirá no sólo la aceleración, sino hacia dónde acelera. Para ello nuestro acelerómetro tiene una entrada para cada dimensión del espacio y nos devolverá los tres componentes del vector aceleración. Pero no solo es capaz de medir la aceleración, sino que en reposo mide la componente de la gravedad que soporta cada uno de sus ejes. Si lo conectamos y lo ponemos sobre una mesa en su posición normal veremos cómo las entradas de los ejes x e y tienen más o menos el mismo valor, pero la del eje z no. Esto es porque está perpendicular al centro de la tierra y por lo tanto la atracción gravitatoria recae enteramente sobre el eje z.



Si lo inclinamos un poco z disminuirá al repartirse la gravedad entre los tres ejes.



Así pues, la superficie sobre la que se apoye el acelerómetro estará nivelada cuando el eje perpendicular a ella soporte toda la fuerza de la gravedad si estamos nivelando en horizontal y ninguna fuerza si estamos nivelando en vertical. Un detalle importante es que el acelerómetro puede detectar la gravedad pero no la orientación, es decir, que si lo rotamos alrededor de uno de los ejes no se enterará. Para detectar el giro necesitaríamos un giróscopo . Este detalle es importante a la hora de mostrar en la pantalla LCD la inclinación que tiene el acelerómetro, ya que nos obliga a orientar correctamente los ejes para realizar el nivelado de superficies verticales.

El acelerómetro

Para el circuito vamos a usar un acelerómetro GY-61. Como podemos ver en la imagen tiene dibujados los ejes para que podamos orientarlo. Para que el sketch funcione correctamente lo tenemos que orientar de forma que el eje Z quede perpendicular a la placa de prototipo. El acelerómetro funciona a 3.3V, por lo que tenemos que ir con cuidado de no conectarlo al pin de 5V de Arduino sino al 3.3V ya que lo podemos quemar.



La pantalla LCD

Para mostrar la orientación del acelerómetro vamos a usar una pantalla Nokia LCD 5110. Supuestamente es la que llevan varios modelos de móviles Nokia, así que, al menos en teoría, los podríamos desmontar y reutilizar la pantalla. En mi caso la he comprado, ya que tampoco tienen un precio excesivo. Para programarla usaremos la biblioteca de arduino desarrollada por Adafruit. La podemos descargar aquí. También tenemos que descargar la biblioteca GFX también desarrollada por Adafruit.
La pantalla LCD también funciona a 3.3V y por lo tanto también tendremos que conectarla al pin 3.3V de arduino. Además tendremos que usar resistencias para bajar el voltaje que le llega desde los pines digitales, ya que su voltaje de salida es 5V. En la conexión del acelerómtro no las necesitamos porque únicamente vamos a leer, pero en los pines del LCD vamos a escribir y por lo tanto les tenemos que enviar 3.3V en lugar de 5V, que es lo que sale de los pines del arduino. El esquema de conexión es el siguiente:

  
El pin LED del LCD es el que se encarga de iluminar la pantalla. Lo hemos conectado a corriente para que esté siempre iluminada. Si quisiéramos encenderla mediante programación tendríamos que conectarla a uno de los pines digitales de arduino y encenderla o apagarla con la función analogWrite poniendo el pin a HIGH o LOW. El resto de pines digitales son los estándares del protocolo SPI que es el que usa la biblioteca de Adafruit. Un detalle importante es que conectamos el pin VCC y LED directamente a corrienta porque todo el circuito está funcionando a 3.3V. Si conectamos el LCD a un circuito cuyos componentes funcionen a 5V deberemos ponerles una resistencia de 10kΏ o correremos el riesgo de quemar el LCD. Aquí lo tenemos montado en la placa de prototipo



El sketch

El sketch va a dibujar en el LCD dos círculos, uno señalará el punto en el que la superficie está nivelada y el otro se moverá conforme vayamos inclinando el acelerómetro. La superficie estará nivelada cuando el segundo círculo esté dentro del primero.
Antes de poder usar el acelerómetro tenemos que calibrarlo para averiguar los valores para 1g, 0g y -1g. Para ello lo colocaremos sobre una superficie horizontal y anotaremos los valores para el eje Z, que será 1g, y el valor para X o Y que serán 0g. Para averiguar el -1g pondremos el acelerómetro al revés y lo rotamos hacia ambos lados. El menor valor que tome será -1g. Las constantes que guardan estos valores son:

const int negativegValue=270;
const int zerogValue=350;
const int onegValue=424;

El código fuente completo:

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

const int xPin = 0;
const int yPin = 1;
const int zPin = 2;

const byte SCLKpin = 13;
const byte DNpin = 11;
const byte DCpin = 12; 
const byte RESETpin = 10;

const byte ballRadius=5;
const byte errorMargin=1;
int xReadOld=0;
int yReadOld=0;
int zReadOld=0;

const int negativegValue=270;
const int zerogValue=350;
const int onegValue=424;
char contrast=57;
char buffer[10];

Adafruit_PCD8544 lcd(SCLKpin,DNpin,DCpin,RESETpin);

void setup(){
  Serial.begin(115200); 
  lcd.begin();
  lcd.setContrast(contrast);
}


void loop(){

  int xRead = analogRead(xPin);
  int yRead = analogRead(yPin);
  int zRead = analogRead(zPin);
  int currentX;
  int currentY;
  /* Elegimos la orientacion que estamos nivelando por el valor del eje z que siempre sera perpendicular a la superficie
  Cambiaremos segun su valor este mas cerca de cero o de 1g*/
  if(zRead<(zerogValue+((onegValue-zerogValue)/2))){
    //Orientacion vertical
    //currentX=lcd.width()/2;
    currentX=map(zerogValue,negativegValue,onegValue,0,lcd.width());
    currentY=map(zRead,negativegValue,onegValue,0,lcd.height());
  }
  else{
    currentX=map(xRead,negativegValue,onegValue,0,lcd.width());
    currentY=map(yRead,negativegValue,onegValue,0,lcd.height());
  }
  
  if(abs(zRead-zReadOld)>=errorMargin){
  
    lcd.clearDisplay();
    
    drawBall(currentX,currentY,5);
    
    xReadOld=xRead;
    yReadOld=yRead;
    zReadOld=zRead;
  }
  
  
  Serial.print("xRead: ");
  Serial.print(xRead);
  Serial.print(" | yRead: ");
  Serial.print(yRead);
  Serial.print(" | zRead: ");
  Serial.println(zRead);

  

  delay(100);
}

void drawBall(int x,int y,int radius){
  
  for(int i=0;i<radius;i++){
    lcd.drawCircle(x,y,i,BLACK);
  }
lcd.drawCircle((int)map(zerogValue,negativegValue,onegValue,0,lcd.width()),(int)map(zerogValue,negativegValue,onegValue,0,lcd.height()),radius+2,BLACK);
  lcd.display();
}

Básicamente lo que hace el sketch es leer los valores de los tres ejes del acelerómetro y redibujar la pantalla cuando el eje Z se ha movido más de un margen de error indicado en la variable errorMargin. La orientación de la superficie a nivelar lo decidimos en función del valor del eje Z, si está más cerca de 1g asumiremos que estamos nivelando una superficie horizontal y si está más cerca de 0g asumiremos que nivelamos una superficie vertical. Para las superficies horizontales la bola se moverá en cualquier dirección del plano XY y para las verticales sólo se moverá arriba y abajo. Para que funcione correctamente con las superficies verticales tenemos que situar el acelerómetro con la flecha del eje Y apuntando hacia el centro de la Tierra. Aquí tenemos un vídeo mostrando su funcionamiento