martes, 25 de octubre de 2016

Arduino + LCD Keypad Shield (2)


Jugando con mi keypad con display LCD.

Se me ocurrió un proyecto para probar "a fondo" mi LCD keypad Shield, pensé en los viejos video game´s de los '80, creí que este proyecto me llevaría un tiempo considerable, pero grande fue mi sorpresa cuando encontré en http://www.instructables.com/id/Arduino-LCD-Game/ un proyecto que se ajustaba en un 99% a mi idea, las diferencias radicaban en que "yo pensé en un autito esquivando obstáculos" y en el juego dibuja a una "personita"... 

La otra diferencia radica en que el original usa un display LCD de 16x2 y un pulsador conectado a la interrupción 0 (pin digital 2). Esto me motivó a modificar el original con intención de usar uno de los pulsadores del propio Shield.

Pero mayor aún fue mi sorpresa cuando -entre los comentarios del sitio- alguien ya lo hizo (apodo DADECOZA), dejando el link al código fuente modificado: http://goo.gl/uF8z24 que redirecciona hacia: https://gist.github.com/dadecoza/c1532f48ccc4a0e276e7f34a129b959c

Con lo que solo me quedó a mi cambiar la leyenda "Press Start" por "Pulse un boton". Y nada más.

El código completo, ya modificado y listo para usar, es el siguiente:

/*
 *   Modified slightly to work with the LCD Keypad Shield.
 *  Original - http://www.instructables.com/id/Arduino-LCD-Game/
 */

#include <LiquidCrystal.h>

#define SPRITE_RUN1 1
#define SPRITE_RUN2 2
#define SPRITE_JUMP 3
#define SPRITE_JUMP_UPPER '.'         // Use the '.' character for the head
#define SPRITE_JUMP_LOWER 4
#define SPRITE_TERRAIN_EMPTY ' '      // User the ' ' character
#define SPRITE_TERRAIN_SOLID 5
#define SPRITE_TERRAIN_SOLID_RIGHT 6
#define SPRITE_TERRAIN_SOLID_LEFT 7

#define HERO_HORIZONTAL_POSITION 1    // Horizontal position of hero on screen

#define TERRAIN_WIDTH 16
#define TERRAIN_EMPTY 0
#define TERRAIN_LOWER_BLOCK 1
#define TERRAIN_UPPER_BLOCK 2

#define HERO_POSITION_OFF 0          // Hero is invisible
#define HERO_POSITION_RUN_LOWER_1 1  // Hero is running on lower row (pose 1)
#define HERO_POSITION_RUN_LOWER_2 2  //                              (pose 2)

#define HERO_POSITION_JUMP_1 3       // Starting a jump
#define HERO_POSITION_JUMP_2 4       // Half-way up
#define HERO_POSITION_JUMP_3 5       // Jump is on upper row
#define HERO_POSITION_JUMP_4 6       // Jump is on upper row
#define HERO_POSITION_JUMP_5 7       // Jump is on upper row
#define HERO_POSITION_JUMP_6 8       // Jump is on upper row
#define HERO_POSITION_JUMP_7 9       // Half-way down
#define HERO_POSITION_JUMP_8 10      // About to land

#define HERO_POSITION_RUN_UPPER_1 11 // Hero is running on upper row (pose 1)
#define HERO_POSITION_RUN_UPPER_2 12 //                              (pose 2)

//LiquidCrystal lcd(11, 9, 6, 5, 4, 3);
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

static char terrainUpper[TERRAIN_WIDTH + 1];
static char terrainLower[TERRAIN_WIDTH + 1];
static bool buttonPushed = false;

void initializeGraphics() {
  static byte graphics[] = {
    // Run position 1
    B01100,
    B01100,
    B00000,
    B01110,
    B11100,
    B01100,
    B11010,
    B10011,
    // Run position 2
    B01100,
    B01100,
    B00000,
    B01100,
    B01100,
    B01100,
    B01100,
    B01110,
    // Jump
    B01100,
    B01100,
    B00000,
    B11110,
    B01101,
    B11111,
    B10000,
    B00000,
    // Jump lower
    B11110,
    B01101,
    B11111,
    B10000,
    B00000,
    B00000,
    B00000,
    B00000,
    // Ground
    B11111,
    B11111,
    B11111,
    B11111,
    B11111,
    B11111,
    B11111,
    B11111,
    // Ground right
    B00011,
    B00011,
    B00011,
    B00011,
    B00011,
    B00011,
    B00011,
    B00011,
    // Ground left
    B11000,
    B11000,
    B11000,
    B11000,
    B11000,
    B11000,
    B11000,
    B11000,
  };
  int i;
  // Skip using character 0, this allows lcd.print() to be used to
  // quickly draw multiple characters
  for (i = 0; i < 7; ++i) {
    lcd.createChar(i + 1, &graphics[i * 8]);
  }
  for (i = 0; i < TERRAIN_WIDTH; ++i) {
    terrainUpper[i] = SPRITE_TERRAIN_EMPTY;
    terrainLower[i] = SPRITE_TERRAIN_EMPTY;
  }
}

// Slide the terrain to the left in half-character increments
//
void advanceTerrain(char* terrain, byte newTerrain) {
  for (int i = 0; i < TERRAIN_WIDTH; ++i) {
    char current = terrain[i];
    char next = (i == TERRAIN_WIDTH - 1) ? newTerrain : terrain[i + 1];
    switch (current) {
      case SPRITE_TERRAIN_EMPTY:
        terrain[i] = (next == SPRITE_TERRAIN_SOLID) ? SPRITE_TERRAIN_SOLID_RIGHT : SPRITE_TERRAIN_EMPTY;
        break;
      case SPRITE_TERRAIN_SOLID:
        terrain[i] = (next == SPRITE_TERRAIN_EMPTY) ? SPRITE_TERRAIN_SOLID_LEFT : SPRITE_TERRAIN_SOLID;
        break;
      case SPRITE_TERRAIN_SOLID_RIGHT:
        terrain[i] = SPRITE_TERRAIN_SOLID;
        break;
      case SPRITE_TERRAIN_SOLID_LEFT:
        terrain[i] = SPRITE_TERRAIN_EMPTY;
        break;
    }
  }
}

bool drawHero(byte position, char* terrainUpper, char* terrainLower, unsigned int score) {
  bool collide = false;
  char upperSave = terrainUpper[HERO_HORIZONTAL_POSITION];
  char lowerSave = terrainLower[HERO_HORIZONTAL_POSITION];
  byte upper, lower;
  switch (position) {
    case HERO_POSITION_OFF:
      upper = lower = SPRITE_TERRAIN_EMPTY;
      break;
    case HERO_POSITION_RUN_LOWER_1:
      upper = SPRITE_TERRAIN_EMPTY;
      lower = SPRITE_RUN1;
      break;
    case HERO_POSITION_RUN_LOWER_2:
      upper = SPRITE_TERRAIN_EMPTY;
      lower = SPRITE_RUN2;
      break;
    case HERO_POSITION_JUMP_1:
    case HERO_POSITION_JUMP_8:
      upper = SPRITE_TERRAIN_EMPTY;
      lower = SPRITE_JUMP;
      break;
    case HERO_POSITION_JUMP_2:
    case HERO_POSITION_JUMP_7:
      upper = SPRITE_JUMP_UPPER;
      lower = SPRITE_JUMP_LOWER;
      break;
    case HERO_POSITION_JUMP_3:
    case HERO_POSITION_JUMP_4:
    case HERO_POSITION_JUMP_5:
    case HERO_POSITION_JUMP_6:
      upper = SPRITE_JUMP;
      lower = SPRITE_TERRAIN_EMPTY;
      break;
    case HERO_POSITION_RUN_UPPER_1:
      upper = SPRITE_RUN1;
      lower = SPRITE_TERRAIN_EMPTY;
      break;
    case HERO_POSITION_RUN_UPPER_2:
      upper = SPRITE_RUN2;
      lower = SPRITE_TERRAIN_EMPTY;
      break;
  }
  if (upper != ' ') {
    terrainUpper[HERO_HORIZONTAL_POSITION] = upper;
    collide = (upperSave == SPRITE_TERRAIN_EMPTY) ? false : true;
  }
  if (lower != ' ') {
    terrainLower[HERO_HORIZONTAL_POSITION] = lower;
    collide |= (lowerSave == SPRITE_TERRAIN_EMPTY) ? false : true;
  }

  byte digits = (score > 9999) ? 5 : (score > 999) ? 4 : (score > 99) ? 3 : (score > 9) ? 2 : 1;

  // Draw the scene
  terrainUpper[TERRAIN_WIDTH] = '\0';
  terrainLower[TERRAIN_WIDTH] = '\0';
  char temp = terrainUpper[16 - digits];
  terrainUpper[16 - digits] = '\0';
  lcd.setCursor(0, 0);
  lcd.print(terrainUpper);
  terrainUpper[16 - digits] = temp;
  lcd.setCursor(0, 1);
  lcd.print(terrainLower);

  lcd.setCursor(16 - digits, 0);
  lcd.print(score);

  terrainUpper[HERO_HORIZONTAL_POSITION] = upperSave;
  terrainLower[HERO_HORIZONTAL_POSITION] = lowerSave;
  return collide;
}

// Handle the button push as an interrupt
void buttonPush() {
  buttonPushed = true;
}

void setup() {
  initializeGraphics();
  lcd.begin(16, 2);
}

void loop() {
  buttonCheck();
  static byte heroPos = HERO_POSITION_RUN_LOWER_1;
  static byte newTerrainType = TERRAIN_EMPTY;
  static byte newTerrainDuration = 1;
  static bool playing = false;
  static bool blink = false;
  static unsigned int distance = 0;

  if (!playing) {
    drawHero((blink) ? HERO_POSITION_OFF : heroPos, terrainUpper, terrainLower, distance >> 3);
    if (blink) {
      lcd.setCursor(0, 0);
      lcd.print("Pulse un boton");
    }
    delay(250);
    blink = !blink;
    if (buttonPushed) {
      initializeGraphics();
      heroPos = HERO_POSITION_RUN_LOWER_1;
      playing = true;
      buttonPushed = false;
      distance = 0;
    }
    return;
  }

  // Shift the terrain to the left
  advanceTerrain(terrainLower, newTerrainType == TERRAIN_LOWER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);
  advanceTerrain(terrainUpper, newTerrainType == TERRAIN_UPPER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);

  // Make new terrain to enter on the right
  if (--newTerrainDuration == 0) {
    if (newTerrainType == TERRAIN_EMPTY) {
      newTerrainType = (random(3) == 0) ? TERRAIN_UPPER_BLOCK : TERRAIN_LOWER_BLOCK;
      newTerrainDuration = 2 + random(10);
    } else {
      newTerrainType = TERRAIN_EMPTY;
      newTerrainDuration = 10 + random(10);
    }
  }

  if (buttonPushed) {
    if (heroPos <= HERO_POSITION_RUN_LOWER_2) heroPos = HERO_POSITION_JUMP_1;
    buttonPushed = false;
  }

  if (drawHero(heroPos, terrainUpper, terrainLower, distance >> 3)) {
    playing = false; // The hero collided with something. Too bad.
  } else {
    if (heroPos == HERO_POSITION_RUN_LOWER_2 || heroPos == HERO_POSITION_JUMP_8) {
      heroPos = HERO_POSITION_RUN_LOWER_1;
    } else if ((heroPos >= HERO_POSITION_JUMP_3 && heroPos <= HERO_POSITION_JUMP_5) && terrainLower[HERO_HORIZONTAL_POSITION] != SPRITE_TERRAIN_EMPTY) {
      heroPos = HERO_POSITION_RUN_UPPER_1;
    } else if (heroPos >= HERO_POSITION_RUN_UPPER_1 && terrainLower[HERO_HORIZONTAL_POSITION] == SPRITE_TERRAIN_EMPTY) {
      heroPos = HERO_POSITION_JUMP_5;
    } else if (heroPos == HERO_POSITION_RUN_UPPER_2) {
      heroPos = HERO_POSITION_RUN_UPPER_1;
    } else {
      ++heroPos;
    }
    ++distance;
  }
  delay(100);
}

void buttonCheck() {
  int b = analogRead(A0);
  if (b < 850) {
    buttonPushed = true;
  }
}

lunes, 24 de octubre de 2016

Arduino + LCD Keypad Shield (1)

Mi primer aproximación a un keypad con display LCD.


En ocasiones, cuando estamos desarrollando algún circuito, es necesario un display para ver cómo van las cosas, y en muchas situaciones la consola del entorno Arduino es una opción incomoda o simplemente no está disponible. Demás esta que mencione que en determinados proyectos es necesario informar con mensajes de texto sobre diversas acciones o estados al usuario de nuestro desarrollo. En estos casos disponer de un display local es la mejor solución.

Para estos casos basta conectar un display del tipo que sea al Arduino.  Lo más fácil suele ser un display LCD de 16 caracteres por 2 renglones (16x2) del tipo I2C, para no tener que conectar muchos pines, que suelen tener la mala costumbre de soltarse en el peor momento, lo que suele provocar las típicas "rabietas" por las que a los electrónicos y técnicos en general se nos acusa de mal genio y de hablar solos.

Una posible y práctica solución es un pequeño Shield comercial, cuya finalidad principal es mejorar nuestras relaciones con quienes nos rodean, dado que no necesitan de cables de conexión, sus pines encastran perfectamente con Arduino, permitiendo montar con seguridad y robustez un display con botones, lo que es ideal para muchas pruebas de campo, o como dispositivo final en cualquier proyecto.

El Shield aquí comentado dispone de un LCD de 16x2 más 5 botones, listos para usar en nuestros proyectos, y un botón de reset. Ocupa o consume varios pines del Arduino para la gestión del display, dejando libres los pines digitales del 0 al 7, y las puertas analógicas de A1 a A5, también deja libre el ICSP.

La ventaja del LCD keypad Shield es que se coloca rápidamente en su sitio asegurando una robusta sujeción a prueba de cables bailarines, evitando el uso del protoboard y así garantiza la estabilidad del conjunto.

Vista general del Shield:

Identificación de componentes y pines:

Máscara de componentes:
Diagrama eléctrico:
El LCD se conecta con Arduino de la siguiente forma:

Para no usar 5 pines en el reconocimiento de los 5 botones, éstos están conectados en serie con resistencias a una única entrada analógica, de modo que se reconoce el botón pulsado mediante el valor de tensión leído en A0 (esto tiene el inconveniente de no poder discriminar si se pulsan dos botones al mismo tiempo)

No entraré en los detalles del control de un display LCD, a los fines prácticos de esta entrada no es relevante, pero la librería proporcionada por Arduino que se encarga de gestionar un display LCD es la LiquidCrystal.h (el entorno Arduino dispone de varios ejemplos realmente muy interesantes). 

Las funciones de la librería son las siguientes:
  • begin
  • clear
  • home
  • print
  • setCursor
  • cursor
  • noCursor
  • blink
  • noBlink
  • display
  • noDisplay
  • autoscroll
  • noAutoscroll
  • leftToRight
  • rightToLeft
  • scrollDisplayLeft
  • scrollDisplayRight
  • createChar
  • setRowOffsets
Existen ejemplos de como usar estas funciones en Internet, recomiendo visitar el sitio oficial para profundizar en el uso de cada función: https://www.arduino.cc/en/Tutorial/HelloWorld

He desarrollado una pequeña aplicación usando las funciones más comunes de la librería, basándome en el material disponible en http://www.prometec.net/lcd-keypad-shield/  para experimentar el funcionamiento del Shield.

El código completo es el siguiente:

#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7); //Definición del objeto lcd

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

int ValorAnalog = 0;

int Leer_Botones() {          // Función para leer los pulsadores 
  ValorAnalog = analogRead(A0); // Leemos la entrada analógica A0
  
  // Mis botones: 0=Right, 131=Up, 305=Down, 478=Left, 721=Select
  // Y los comparamos con un margen cómodo
    
  if (ValorAnalog > 900)  return btnNONE;  // Ningún botón pulsado 
  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;                         // Por si todo falla
}
  
void setup() {  
  lcd.begin(16, 2);             //Inicializar el LCD
  lcd.print(" E.E.S.T. Nro 6 ");//Imprime un mensaje simple
  lcd.setCursor(0,1);           //Cursor a linea 2, posición 1
  lcd.print("VIEGAS BARROS V.");//Imprime un mensaje simple
  delay(5000);             //Tiempo para que se lea la "propaganda"
  lcd.clear();                  //Limpiamos el display
  lcd.print("  TECLA   VALOR ");//Imprime encabezado
}

void loop() {  
  lcd.setCursor(0,1);             // Cursor a linea 2, posición 1
  switch(Leer_Botones()) {        // Según sea el botón pulsado...
    case btnRIGHT : lcd.print(" DERECHA "); break;
    case btnLEFT  : lcd.print("IZQUIERDA"); break;
    case btnUP    : lcd.print("  ARRIBA "); break;
    case btnDOWN  : lcd.print("  ABAJO  "); break;
    case btnSELECT: lcd.print(" SELECTOR"); break;
    case btnNONE  : lcd.print(" NINGUNA "); break;
  }
    
  //Pequeña rutina para posicionar el valor analógico leído
  lcd.setCursor(10, 1);             // Cursor a linea 2, posición 10
  if (ValorAnalog<10) { lcd.print("   "); lcd.setCursor(13, 1); }
  else
    if (ValorAnalog<100) { lcd.print("  "); lcd.setCursor(12, 1); }
    else
      if(ValorAnalog<1000) { lcd.print(" "); lcd.setCursor(11, 1); }
  
  lcd.print(ValorAnalog);       // Imprime el valor de la entrada A0
}

Este código se basa en la función Int Leer_Botones()que por lo que pude observar se encuentra en la mayoría de los ejemplos de Internet. Su funcionamiento es muy sencillo, solo lee el valor de tensión presente en la entrada analógica A0 del Arduino, para luego -según se encuentre dicha tensión en determinado rango- entregar cómo resultado una constante literal fácilmente reconocible. 

Las tensiones previstas para cada pulsador son: 0, 175, 350, 525, 700 y 1023 para el caso de reposo. 

Pero los valores entregados por cada pulsador pueden diferir debido a las tolerancias de los componentes resistivos usados en el divisor de tensiones, por lo que es recomendable chequearlos.

El programa es sencillo, está bastante documentado y es fácilmente adaptable a cualquier proyecto.

Ventajas del Shield: 
Es muy práctica cuando se quiere experimentar con un prototipo sin enredarse demasiado con cableríos.

Desventajas del Shield:
Para conectar sensores y/o actuadores hay que soldarlos a la placa, o al menos soldar pines -hembras o machos, según criterio del desarrollador- o idear otra forma de conexión segura sobre los pines libres del Arduino.