Archivos de la categoría ‘–Programación–’

“Los viejos hábitos tardan en morir” dice Mick Jagger en la canción de nombre homónimo compuesta para la película “Alfie”, y si eres lector asiduo de este blog sabrás cuán cierto es ya que como makers siempre estamos trasteando con diferentes artilugios electrónicos de diferentes épocas apoyándonos en tecnologías actuales como microcontroladores o mini computadoras, desde tubos nixie y tubos geiger hasta proyectos de robótica avanzada, pasando por seguridad electrónica y seguridad en sistemas.

En esta ocasión tenemos sobre la mesa un proyecto de consolas retro para devolverles la vida a nuestros antiguos (pero infalibles) mandos de nuestras excelsas consolas como Sega Mega Drive o Super Nintendo, en este punto se me “pianta” un lagrimón como diría Carlos Gardel.

Recordando aquellos tiempos cuando todo era mejor (difícil es mirar al pasado con los ojos del presente), la Mega Drive, conocida en diversos territorios de América como Genesis, fue la tercera consola de Sega cuya primera versión fue lanzada en Japón en 1988, sucedida por el lanzamiento en Norteamérica bajo el renombramiento de Genesis en 1989.

Por otra parte, la Super Nintendo Entertainment System, abreviada habitualmente como Super Nintendo, Super NES o SNES, también llamada Super Famicom es la tercera videoconsola de sobremesa de Nintendo, se lanzó en Japón el miércoles 21 de noviembre de 1990. Fue la más exitosa y vendida de la era de los 16 bits. Gracias al chip Super FX, la SNES tuvo los primeros videojuegos totalmente tridimensionales en la consola, siendo Star Fox el primer videojuego para consola de videojuegos con gráficos completamente tridimensionales.

La rivalidad entre Nintendo y Sega dio lugar a lo que se ha descrito como una de las guerras de consolas más notables en la historia de los videojuegos. Mientras que en Japón Sega se mantuvo distante en el mercado de videojuegos, en Norteamérica se mantendría dominante con su consola Sega Genesis, llamada Sega Mega Drive en Europa, desarrollando importantes campañas publicitarias agresivas en contra de Nintendo a partir del año 1989, una de las más recordadas por el público americano fue la campaña publicitaria llamada “Genesis does what Nintendon’t” (en español: “Genesis hace lo que Nintendo no puede“) que demostraba las capacidades técnicas superiores de la consola de Sega en 16 bits y un extenso cátalogo de videojuegos deportivos frente a las capacidades en 8 bits de la consola NES con sus videojuegos licenciados de títulos arcade. Sin embargo, uno de los factores claves del éxito de la Genesis en Estados Unidos fue la incorporación del videojuego Sonic the Hedgehog en el paquete de la consola en 1991, el título logró tener críticas favorables y ayudó a que varios jugadores compraran la consola Genesis en lugar de la SNES. Para 1992, Nintendo tendría que enfrentarse ahora con un rival ya establecido en los mercados americanos y europeos durante algunos años, solo en Norteamérica lograría consagrarse hasta el año 1994.

Con estos titanes y pioneros de las consolas de videojuegos, probablemente en algún lugar de casa tenemos guardada y cubierta de polvo alguna de estas consolas o ambas, ¿por qué no? en su caja con los respectivos mandos. En caso de no ser funcional la consola o simplemente deseamos conectar los mandos a nuestra PC para reutilizarlos en nuestros emuladores favoritos deberemos seguir los pasos de esta humilde publicación.

Previamente a realizar desarme, soldado y programación investigué acerca de los mandos mencionados anteriormente, sus pines, conectores y señales utilizadas por los mismos. Sega Genesis utilizó un enfoque ligeramente diferente en la configuración de sus pines con respecto al mando de Atari. La conexión del mando de Sega parece ser la configuración estándar de un mando de 2 botones donde los pines 6 y 9 se usan para los botones que conectan a tierra el pin cuando se presionan. Lo único extraño es que el mando también requiere 5V en el pin 5. Sega ha agregado un modelo de 6 botones además de su configuración original de 3 botones. Los mandos de 6 botones usan esencialmente la misma interfaz, pero agregan más botones mediante un circuito integrado adicional (CMOS 74HC157) que multiplexa las nuevas señales con las señales de joystick existentes, usando el bit de control en el pin 7 del conector. Este es un multiplexor CMOS cuádruple de 2 líneas a 1 línea de alta velocidad. La consola puede, con ayuda del pin “Select”, elegir entre dos funciones en cada entrada. Si el pin 7 se deja flotando, la línea asume por defecto un valor lógico alto.

Lista de Componentes
1 Mando de Consola Sega Genesis o SNES.
1 Sparkfun Arduino Pro Micro 5V (o compatible).
35mm Tubo Termocontraíble (diám. 20mm)
Librería Joystick de Matthew Heironimus

En primer lugar tomaremos el mando por el extremo del conector y cortaremos aproximadamente 10cm de cable como observamos en la siguiente imagen:

(click para ampliar)

Ahora debemos identificar y establecer una relación entre los pines del conector y los colores de los cables para así conectarlos correctamente a nuestro Arduino Pro Micro. Como vemos en la imagen he utilizado un pequeño alfiler (que también podría ser un pequeño trozo de cable o un clip) para establecer la correspondencia. Desde luego el multímetro debe encontrarse seteado para medición de continuidad, prueba de diodos o un valor óhmico lo más bajo posible.

(click para ampliar)

Esta prueba la llevaremos a cabo con la ayuda de la siguiente imagen y tabla.

(Vista frontal del conector hembra D-SUB9 bajo prueba)

Pin D-SUB9
Descripción
Pin Arduino Pro Micro
Color (*)
1 Arriba / Z 2 Amarillo
2 Abajo / Y 3 Naranja
3 Izquierda / X 4 Rojo
4 Derecha / Mode 5 Marrón
5 5V VCC Negro
6 A / B 6 Verde
7 SELECT 8 Azul
8 GND GND Gris
9 START / C 7 Blanco

(*) Los colores pueden variar.

Habiendo establecido exitosamente la relación entre los pines del conector D-SUB9 y los colores de los cables, procederemos a programar nuestro Arduino Pro Micro dejando como último paso el soldado de los cables del mando a los pines de nuestro Arduino.

#include <Joystick.h>

// Create Joystick
Joystick_ Joystick;

const int PLAYERS = 2;
 
// Controller Button Flags
const int ON = 1;
const int UP = 2;
const int DOWN = 4;
const int LEFT = 8;
const int RIGHT = 16;
const int START = 32;
const int A = 64;
const int B = 128;
const int C = 256;
const int X = 512;
const int Y = 1024;
const int Z = 2048;
const int MODE = 4096;
 
// Controller DB9 Pin 7 Mappings. Controller 0,1
const int SELECT[] = { 8, 9 };
 
typedef struct
{
  int player;
  int pin;
  int lowFlag;
  int highFlag;
  int pulse3Flag;
} input;
 
// Controller DB9 Pin to Button Flag Mappings
// First column is the controller index, second column
// is the Arduino pin that the controller's DB9 pin is
// attached to
input inputMap[] = {
  { 0,  2,  UP,    UP,     Z    }, // P0 DB9 Pin 1
  { 0,  3,  DOWN,  DOWN,   Y    }, // P0 DB9 Pin 2
  { 0,  4,  ON,    LEFT,   X    }, // P0 DB9 Pin 3
  { 0,  5,  ON,    RIGHT,  MODE }, // P0 DB9 Pin 4
  { 0,  6,  A,     B,      0    }, // P0 DB9 Pin 6
  { 0,  7,  START, C,      0    }, // P0 DB9 Pin 9
  { 1,  A0, UP,    UP,     Z    }, // P1 DB9 Pin 1
  { 1,  A1, DOWN,  DOWN,   Y    }, // P1 DB9 Pin 2
  { 1,  A2, ON,    LEFT,   X    }, // P1 DB9 Pin 3
  { 1,  A3, ON,    RIGHT,  MODE }, // P1 DB9 Pin 4
  { 1,  A4, A,     B,      0    }, // P1 DB9 Pin 6
  { 1,  A5, START, C,      0    }  // P1 DB9 Pin 9
};
 
typedef struct
{
  int player;
  int flag;
  char key;
} output;
 
// Controller Button Flag to Keyboard Mappings
// First column is the controller index, second column
// is the button flag, third is joystick button
output outputMap[] = {
  { 0, UP,    0 },
  { 0, DOWN,  1 },
  { 0, LEFT,  2 },
  { 0, RIGHT, 3 },
  { 0, START, 4 },
  { 0, A,     5 },
  { 0, B,     6 },
  { 0, C,     7 },
  { 0, X,     8 },
  { 0, Y,     9 },
  { 0, Z,     10 },
  { 0, MODE,  11 },
  { 1, UP,    12 },
  { 1, DOWN,  13 },
  { 1, LEFT,  14 },
  { 1, RIGHT, 15 },
  { 1, START, 16 },
  { 1, A,     17 },
  { 1, B,     18 },
  { 1, C,     19 },
  { 1, X,     20 },
  { 1, Y,     21 },
  { 1, Z,     22 },
  { 1, MODE,  23 }
};

 
// Controller State
int currentState[] = { 0, 0 };
int lastState[] = { -1, -1 };
 
// Default to three-button mode until six-button connects
boolean sixButtonMode[] = { false, false };
 
void setup()
{
  // Setup input pins
  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    pinMode(inputMap[i].pin, INPUT);
    digitalWrite(inputMap[i].pin, HIGH);
  }
  
  // Setup select pins
  for (int i = 0; i < PLAYERS; i++)
  {
    pinMode(SELECT[i], OUTPUT);
    digitalWrite(SELECT[i], HIGH);
  }

  // Initialize Joystick Library
  Joystick.begin(false);  
}
 
void loop()
{
  readButtons();
  sendStates();
}
 
void readButtons()
{
  for (int i = 0; i < PLAYERS; i++)
  {
    resetState(i);
    if (sixButtonMode[i])
    {
      read6buttons(i);
    }
    else
    {
      read3buttons(i);
    }
  }
}
 
void resetState(int player)
{
  currentState[player] = 0;
}
 
void read3buttons(int player)
{
  // Set SELECT LOW and read lowFlag
  digitalWrite(SELECT[player], LOW);
     
  delayMicroseconds(20);
     
  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].lowFlag;
    }
  }
 
  // Set SELECT HIGH and read highFlag
  digitalWrite(SELECT[player], HIGH);
     
  delayMicroseconds(20);
     
  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].highFlag;
    }
  }
    
  // When a six-button first connects, it'll spam UP and DOWN,
  // which signals the game to switch to 6-button polling
  if (currentState[player] == (ON | UP | DOWN))
  {
    sixButtonMode[player] = true;
  }
  // When a controller disconnects, revert to three-button polling
  else if ((currentState[player] & ON) == 0)
  {
    sixButtonMode[player] = false;
  }
  
  delayMicroseconds(20);
}
 
void read6buttons(int player)
{
  // Poll for three-button states twice
  read3buttons(player);
  read3buttons(player);
  
  // After two three-button polls, pulse the SELECT line
  // so the six-button reports the higher button states
  digitalWrite(SELECT[player], LOW);
  delayMicroseconds(20);
  digitalWrite(SELECT[player], HIGH);
  
  for(int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].pulse3Flag;
    }
  }
  
  delayMicroseconds(1360); // Increased from 1000 for Sega Mega Drive original Arcade 6 Button Pad Controller MK-1653-50
}
 
void sendStates()
{
  for (int i = 0; i < sizeof(outputMap) / sizeof(output); i++)
  {
    int last = (lastState[outputMap[i].player] & outputMap[i].flag);
    int current = (currentState[outputMap[i].player] & outputMap[i].flag);
     
    if (last != current)
    {
      if (current == outputMap[i].flag)
      {
        Joystick.pressButton(outputMap[i].key);
      }
      else
      {
        Joystick.releaseButton(outputMap[i].key);
      }
    }
  }
  
  for (int i = 0; i < PLAYERS; i++)
  {
    lastState[i] = currentState[i];
  }
  Joystick.sendState();
}

Con la programación exitosa de nuestro Arduino Pro Micro, ahora solo debemos soldar los cables del mando a los pines del mismo y colocar el tubo termocontraíble. Al final de la publicación se encuentra el vídeo donde podemos verificar el correcto funcionamiento de nuestro mando modificado con conexión USB.

Para realizar la misma modificación en un mando de SNES el procedimiento es similar al descripto anteriormente.

(click para ampliar)

En primer lugar tomaremos el mando por el extremo del conector y cortaremos aproximadamente 10cm de cable como observamos en la siguiente imagen:

(click para ampliar)

Ahora debemos identificar y establecer una relación entre los pines del conector y los colores de los cables para así conectarlos correctamente a nuestro Arduino Pro Micro. Como vemos en la imagen he utilizado un pequeño alfiler (que también podría ser un pequeño trozo de cable o un clip) para establecer la correspondencia. Desde luego el multímetro debe encontrarse seteado para medición de continuidad, prueba de diodos o un valor óhmico lo más bajo posible.

(click para ampliar)

Esta prueba la llevaremos a cabo con la ayuda de la siguiente imagen y tabla.

(Vista frontal del conector SNES 7 pines bajo prueba)

Pin SNES 7
Descripción
Pin Arduino Pro Micro
Color (*)
1 5V VCC Blanco
2 Data Clock 6 Azul
3 Data Latch 7 Amarillo
4 Serial Data 5 Rojo
5 NC
6 NC
7 GND GND Marrón

(*) Los colores pueden variar.

Habiendo establecido exitosamente la relación entre los pines del conector SNES 7 y los colores de los cables, procederemos a programar nuestro Arduino Pro Micro dejando como último paso el soldado de los cables del mando a los pines de nuestro Arduino.

#include <Joystick.h>

// Create Joystick
Joystick_ Joystick;

// Controller Buttons
#define SNES_B       1      //000000000001
#define SNES_Y       2      //000000000010
#define SNES_SELECT  4      //000000000100
#define SNES_START   8      //000000001000
#define SNES_UP      16     //000000010000
#define SNES_DOWN    32     //000000100000
#define SNES_LEFT    64     //000001000000
#define SNES_RIGHT   128    //000010000000
#define SNES_A       256    //000100000000
#define SNES_X       512    //001000000000
#define SNES_L       1024   //010000000000
#define SNES_R       2048   //100000000000

const int PIN_CLOCK = 6;
const int PIN_LATCH = 7;
const int PIN_DATA = 5; //12;

void setup(){
  Joystick.begin(false);
  
  pinMode(PIN_CLOCK, OUTPUT);
  digitalWrite(PIN_CLOCK, HIGH);
  pinMode(PIN_LATCH, OUTPUT);
  digitalWrite(PIN_LATCH, LOW);
  pinMode(PIN_DATA, INPUT_PULLUP);
}

void loop(){
  uint16_t state = 0;
    // 12us latch
    digitalWrite(PIN_LATCH, HIGH);
    delayMicroseconds(12);
    digitalWrite(PIN_LATCH, LOW);
    delayMicroseconds(6);
    // Retrieve 4021s sixteen bits of data
    for(int i = 0; i < 16; i++){
        digitalWrite(PIN_CLOCK, LOW);
        delayMicroseconds(6);
        state |= digitalRead(PIN_DATA) << i;
        digitalWrite(PIN_CLOCK, HIGH);
        delayMicroseconds(6);
    }

  // buttons
  SNES_B & ~state ? Joystick.pressButton(0) : Joystick.releaseButton(0);
  SNES_Y & ~state ? Joystick.pressButton(1) : Joystick.releaseButton(1);
  SNES_SELECT & ~state ? Joystick.pressButton(2) : Joystick.releaseButton(2);
  SNES_START & ~state ? Joystick.pressButton(3) : Joystick.releaseButton(3);
  SNES_A & ~state ? Joystick.pressButton(4) : Joystick.releaseButton(4);
  SNES_X & ~state ? Joystick.pressButton(5) : Joystick.releaseButton(5);
  SNES_L & ~state ? Joystick.pressButton(6) : Joystick.releaseButton(6);
  SNES_R & ~state ? Joystick.pressButton(7) : Joystick.releaseButton(7);

  // 360° Hat Switch 0
  Joystick.setHatSwitch(0, -1); // release
  if (SNES_UP & ~state) Joystick.setHatSwitch(0, 0);
  if (SNES_RIGHT & ~state) Joystick.setHatSwitch(0, 90);
  if (SNES_DOWN & ~state) Joystick.setHatSwitch(0, 180);
  if (SNES_LEFT & ~state) Joystick.setHatSwitch(0, 270);
  if (SNES_UP & ~state && SNES_RIGHT & ~state) Joystick.setHatSwitch(0, 45);
  if (SNES_DOWN & ~state && SNES_RIGHT & ~state) Joystick.setHatSwitch(0, 135);
  if (SNES_DOWN & ~state && SNES_LEFT & ~state) Joystick.setHatSwitch(0, 225);
  if (SNES_UP & ~state && SNES_LEFT & ~state) Joystick.setHatSwitch(0, 315);
  
  Joystick.sendState();
  delay(25);
}

Con la programación exitosa de nuestro Arduino Pro Micro, ahora procederemos a fijar con adhesivo de contacto o silicona caliente el cable de nuestro mando a la parte trasera de nuestro Arduino como observamos en la siguiente imagen:

(click para ampliar)

Ahora soldaremos los cables del mando a los pines de nuestro Arduino Pro Micro.

(click para ampliar)

(click para ampliar)

Como último paso solo nos resta colocar el tubo termocontraíble.

(click para ampliar)

(click para ampliar)

A continuación se encuentra el vídeo donde podemos verificar el correcto funcionamiento de nuestro mando modificado con conexión USB.

Anuncios

El vúmetro es un dispositivo indicador en equipos de audio para mostrar el nivel de señal en unidades de volumen, también es llamado “indicador del volumen”.

Consta de un instrumento de bobina móvil o galvanómetro con una balística (amortiguamiento) determinada, alimentado por medio de un rectificador de onda completa que se conecta a la línea de audio mediante una resistencia en serie. No necesita más fuente de energía para su funcionamiento que la señal de entrada. Esencialmente permite visualizar las variaciones de la tensión en la señal de audio, rectificándola y obteniendo el valor medio. Este se obtiene por la balística del instrumento usado, no por una integración capacitiva. Lo que exige que el galvanómetro de un vúmetro no sea fabricado igual que otros tipos de medidores eléctricos (como los amperímetros, voltímetros, etc.) para conformar el estándar SVI (Standard Volume Indicator).

El vúmetro no fue diseñado para medir explícitamente la tensión de la señal, sino para que los usuarios tuvieran una referencia u objetivo de 0 VU, identificado como el 100% o 0 decibelios, en telefonía y en la modulación de los transmisores de la época, por lo que no era muy importante que el dispositivo no fuera extremadamente lineal o preciso para bajos niveles. En efecto, la escala de -20 VU a +3 VU, con 0 VU al 70% de la escala, fue limitado por la tecnología de la época. La mitad superior de la escala solo cubre 6 dB, permitiendo ajustar con precisión solo los niveles alrededor de 0 VU.

Se ha reemplazado en muchos equipos el tradicional vúmetro de aguja por indicadores luminosos con LEDs. Además del nivel ponderado, algunos vúmetros digitales también muestran los picos o máximos. Como regla general, los niveles de grabación deben ser tales que no superen el área roja más allá de 0 VU, o solo en raras ocasiones. Si el volumen de grabación es demasiado alto, la calidad del sonido y respuesta en frecuencia es generalmente más pobre y los efectos de saturación y recorte pueden ser especialmente problemáticos para un sistema de grabación digital. Por el contrario si el nivel es demasiado bajo, los niveles de ruido serán altos en relación a la señal principal que se está grabando.

Actualmente, la mayoría de las computadoras utilizan pantallas LED o LCD para indicar el nivel de sonido, por otro lado, dan un aspecto colorido y dramático.
Este proyecto consta de una visualización con memoria de pico que muestra gráficamente la señal de audio estéreo que ingresa a través de dos entradas analógicas de nuestra placa Arduino.
Tiene múltiples aplicaciones, ya que podría ser un indicador de nivel de audio, control de nivel de líquido, voltímetro o amperímetro en una fuente de alimentación, indicador de temperatura, medición de una señal de RF, etc.

El diagrama esquemático del vúmetro estéreo se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Display LCD 16×2 con controlador Hitachi HD44780 o compatible
2 Capacitores Electrolíticos 1uF 63V
2 Diodos Rectificadores 1N4001
1 Resistencia 3K3Ω 0.25W
1 Resistencia 220Ω 0.25W
2 Resistencias 330KΩ 0.25W

#include <LiquidCrystal.h>

#define IN_LEFT    A4  // analog input for left channel
#define IN_RIGHT   A5  // analog input for right channel

#define T_REFRESH    100
#define T_PEAKHOLD   (3 * T_REFRESH)

LiquidCrystal lcd(7,  // RS
                  8,  // E
                  3,  // DB4
                  4,  // DB5
                  5,  // DB6
                  6   // DB7
                  );

byte  fill[6] = {0x20, 0x00, 0x01, 0x02, 0x03, 0xFF};
byte  peak[7] = {0x20, 0x00, 0x04, 0x05, 0x06, 0x07, 0x20};
byte block[8][8]=
{
  {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10},
  {0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18},
  {0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C},
  {0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E},

  {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08},
  {0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04},
  {0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02},
  {0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
};

int lmax[2];
int dly[2];

long lastT = 0;

void  bar(int row, int lev) {

    lcd.setCursor(0, row);
    lcd.write(row ? 'R' : 'L');

    for(int i = 1; i < 16; i++) {
        int f = constrain(lev-i*5, 0, 5);
        int p = constrain(lmax[row]-i*5, 0, 6);
        if(f)
            lcd.write(fill[f]);
        else
            lcd.write(peak[p]);
    }

    if(lev > lmax[row]) {
        lmax[row] = lev;
        dly[row]  = -(T_PEAKHOLD) / T_REFRESH;
    }
    else {
        if(dly[row] > 0)
            lmax[row] -= dly[row];

        if(lmax[row] < 0)
            lmax[row] = 0;
        else
            dly[row]++;
    }
}

void  setup() {

    byte fillbar[8] = {
    B00000,
    B01000,
    B01100,
    B01010,
    B01001,
    B01010,
    B01100,
    B01000
    };

    byte mark[8] = {
    B00000,
    B01010,
    B10001,
    B10101,
    B10001,
    B01010,
    B00000,
    B00000
    };

    lcd.begin(16, 2);

    lcd.createChar(0, fillbar);
    lcd.createChar(1, mark);

    lcd.setCursor(3, 0);
    lcd.print("JLM Sound");
    lcd.setCursor(12, 0);
    lcd.write(1);

    for(int i = 0; i < 16; i++) {
        lcd.setCursor(i, 1);
        lcd.write((byte)0);

        delay(125);
    }

    lcd.clear();

    for(int j = 0; j < 8; j++)
        lcd.createChar(j, block[j]);
}

void  loop() {

    if(millis() < lastT)
        return;

    lastT += T_REFRESH;

    int anL = map(sqrt(analogRead(IN_LEFT)*16), 0, 128, 0, 80);
    int anR = map(sqrt(analogRead(IN_RIGHT)*16), 0, 128, 0, 80);
    
    bar(0, anL);
    bar(1, anR);
}

Arduino es una plataforma prácticamente infinita para todo tipo de aplicaciones como lo hemos visto en este humilde blog de entre millones que existen en la web. En este espacio comenzamos con aplicaciones de Hacking, luego incursionamos en la fascinante disciplina de la Robótica y últimamente avanzamos sobre el increíble universo de los Videojuegos.

Anteriormente publicamos juegos (basados en la librería VGAx desarrollada por Sandro Maffiodo) como, por ejemplo:

También construimos juegos más sencillos basados simplemente en LEDs y pulsadores como

Y no hace mucho tiempo atrás comenzamos con la construcción de videojuegos utilizando matrices de LEDs

En esta ocasión construiremos un juego muy sencillo orientado a quienes recién se inician en este maravilloso mundo de los microcontroladores, particularmente en Arduino, y desean obtener resultados satisfactorios a corto plazo ya que no creo que haya alguna emoción más intensa para un inventor, que ver alguna de sus creaciones funcionando. Utilizaremos solamente un display LCD de 16 columnas 2 filas, y un pulsador.

El juego que crearemos pertenece al género “Endless Runner“, del inglés corredor infinito cuya definición es la siguiente:

Género donde el jugador debe avanzar de manera irremediable en una misma dirección, generalmente escapando de algún enemigo o peligro, y cuyo objetivo es avanzar lo máximo posible antes de morir.

Generalmente las acciones principales son saltar y esquivar obstáculos, pero muchos juegos de este tipo incluyen también algún tipo de ataque, tanto cuerpo a cuerpo como con armas.

Desde la decáda de los 80’s con juegos como “Jump Bug” o “Moon Patrol” hasta el día de hoy se encuentran de manera casi omnipresente en nuestras vidas. Como ejemplo inmediato tenemos el mundialmente conocido navegador web del gigante Google, “Chrome“, que a partir de “Chrome Canary” incluye un divertido juego “endless runner” en el cual controlamos a un simpático T-Rex. Aunque parezca increíble existen en la web competencias de mayor puntaje obtenido en el juego. Meses atrás un gigante del entretenimiento, Netflix, ha incluido un juego “endless runner” en el cual podemos seleccionar personajes como Pablo Escobar o Marco Polo.

El diagrama esquemático de nuestro divertido y sencillo juego se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Display LCD 16×2 con controlador Hitachi HD44780 o compatible
1 Resistencia 3K3Ω 0.25W
1 Resistencia 220Ω 0.25W
1 Pulsador N.A. o Push-Button N.A.

#include <LiquidCrystal.h>

#define PIN_BUTTON 2

#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(7,  //RS
                  8,  //E
                  3,  //DB4
                  4,  //DB5
                  5,  //DB6
                  6   //DB7
                  );

char terrainUpper[TERRAIN_WIDTH + 1];
char terrainLower[TERRAIN_WIDTH + 1];
volatile boolean buttonPushed = false;

void initializeGraphics() {
	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;
        }
    }
}

boolean drawHero(byte position, char* terrainUpper, char* terrainLower, unsigned int score) {
    boolean 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() {
    pinMode(PIN_BUTTON, INPUT_PULLUP);

    // Digital pin 2 maps to interrupt 0
    attachInterrupt(0, buttonPush, FALLING);

    initializeGraphics();

    lcd.begin(16, 2);
}

void loop(){
    static byte heroPos = HERO_POSITION_RUN_LOWER_1;
    static byte newTerrainType = TERRAIN_EMPTY;
    static byte newTerrainDuration = 1;
    static boolean playing = false;
    static boolean 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("Press Start");
        }

        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);
}

El vúmetro es un dispositivo indicador en equipos de audio para mostrar el nivel de señal en unidades de volumen, también es llamado “indicador del volumen”.

Consta de un instrumento de bobina móvil o galvanómetro con una balística (amortiguamiento) determinada, alimentado por medio de un rectificador de onda completa que se conecta a la línea de audio mediante una resistencia en serie. No necesita más fuente de energía para su funcionamiento que la señal de entrada. Esencialmente permite visualizar las variaciones de la tensión en la señal de audio, rectificándola y obteniendo el valor medio. Este se obtiene por la balística del instrumento usado, no por una integración capacitiva. Lo que exige que el galvanómetro de un vúmetro no sea fabricado igual que otros tipos de medidores eléctricos (como los amperímetros, voltímetros, etc.) para conformar el estándar SVI (Standard Volume Indicator).

El vúmetro no fue diseñado para medir explícitamente la tensión de la señal, sino para que los usuarios tuvieran una referencia u objetivo de 0 VU, identificado como el 100% o 0 decibelios, en telefonía y en la modulación de los transmisores de la época, por lo que no era muy importante que el dispositivo no fuera extremadamente lineal o preciso para bajos niveles. En efecto, la escala de -20 VU a +3 VU, con 0 VU al 70% de la escala, fue limitado por la tecnología de la época. La mitad superior de la escala solo cubre 6 dB, permitiendo ajustar con precisión solo los niveles alrededor de 0 VU.

Se ha reemplazado en muchos equipos el tradicional vúmetro de aguja por indicadores luminosos con LEDs. Además del nivel ponderado, algunos vúmetros digitales también muestran los picos o máximos. Como regla general, los niveles de grabación deben ser tales que no superen el área roja más allá de 0 VU, o solo en raras ocasiones. Si el volumen de grabación es demasiado alto, la calidad del sonido y respuesta en frecuencia es generalmente más pobre y los efectos de saturación y recorte pueden ser especialmente problemáticos para un sistema de grabación digital. Por el contrario si el nivel es demasiado bajo, los niveles de ruido serán altos en relación a la señal principal que se está grabando.

Actualmente, la mayoría de las computadoras utilizan pantallas LED o LCD para indicar el nivel de sonido, por otro lado, dan un aspecto colorido y dramático.
Este proyecto consta de 6 visualizaciones diferentes que muestran gráficamente la señal de audio estéreo que ingresa a través de dos entradas analógicas del PIC.
Tiene múltiples aplicaciones, ya que podría ser un indicador de nivel de audio, control de nivel de líquido, voltímetro o amperímetro en una fuente de alimentación, indicador de temperatura, medición de una señal de RF, etc.

Datasheet PIC16F877A

El diagrama esquemático del vúmetro estéreo se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Microcontrolador PIC16F877A
1 Display LCD 16×2 con controlador Hitachi HD44780 o compatible
1 Cristal de Cuarzo 20MHz
2 Capacitores Cerámicos 27pF 50V
2 Capacitores Electrolíticos 1uF 63V
2 Diodos Rectificadores 1N4001
1 Trimpot horizontal multivuelta 10K
1 Resistencia 10KΩ 0.25W
1 Resistencia 4K7Ω 0.25W
2 Resistencias 330KΩ 0.25W
1 Pulsador N.A. o Push-Button N.A.

El código fuente del firmware del microcontrolador está escrito en el lenguaje PICBasic. Puede ser compilado sin ningún inconveniente utilizando la última versión del compilador PICBasic Pro Trial Version 3.0.7.1 y cargar el firmware en el PIC16F877A utilizando el programador PICKit 3 v3.01.

#CONFIG
        __config _HS_OSC & _WDT_OFF & _CP_OFF & _BOREN_OFF & _LVP_OFF

#ENDCONFIG

' *******************************************************************************
DEFINE OSC 20     ' Oscillator 20 MHz
' *******************************************************************************
ADCON1 = 00000000      ' Set all PORTA and PORTE as analog
PAUSE 500              ' Time to initiation of the LCD
DEFINE LCD_DREG PORTB  ' LCD data PORT B ships
DEFINE LCD_DBIT 4      ' LCD uses less than 0 or greater Bit 4
DEFINE LCD_RSREG PORTB ' LCD RS in the PORTB
DEFINE LCD_RSBIT 0     ' LCD RS PORTB.0
DEFINE LCD_EREG PORTB  ' LCD E in the PORTB
DEFINE LCD_EBIT 1      ' LCD E PORTB.1
DEFINE LCD_BITS 4      ' LCD using 4 or 8 bits for data
DEFINE LCD_LINES 2     ' Number of lines lcd
' *******************************************************************************
DEFINE ADC_BITS 8      ' Result of 8 or 10-bit ADC
DEFINE ADC_CLOCK 3     ' Set clock
DEFINE ADC_SAMPLEUS 50 ' Set sampling time in microseconds
' *******************************************************************************
CHANNEL var BYTE             ' Variable name
LINE    VAR BYTE             ' Variable name
COUNTER VAR BYTE             ' Variable name
REST    VAR BYTE             ' Variable name
Mark1   VAR BIT              ' Variable name
Mark2  var BYTE              ' Variable name
pushbutton    Var PORTB.2    ' button in the PORTB.2
TRISA =  %11111111
TRISB =  %00000100
TRISC =  %00000000

' *******************************************************************************
LCDOUT 254,64,1,2,5,5,5,5,2,1                      ' position CGRAM 0
LCDOUT 254,72,24,4,26,2,2,26,4,24                  ' 1 position CGRAM
LCDOUT $FE,1                                       ' Clear screen
' *******************************************************************************
GRAF1 :
Mark2 = 1
LCDOUT 254,64 , 0, 16, 16 , 16, 16 , 16, 16 , 0 ' 0 position CGRAM
LCDOUT 254,72 , 0, 24, 24 , 24, 24, 24 , 24, 0  ' 1 position CGRAM
LCDOUT 254,80 , 0, 28, 28 , 28, 28, 28 , 28, 0  ' CGRAM Position 2
LCDOUT 254,88 , 0, 30, 30 , 30, 30, 30 , 30, 0  ' 3 position CGRAM
LCDOUT 254,96 , 0, 31, 31 , 31, 31, 31 , 31, 0  ' 4 position CGRAM
PAUSE 300
GOTO BAR
' *******************************************************************************
GRAF2 :
Mark2 = 2
LCDOUT 254,64 , 0, 24, 24 , 24, 24, 24 , 24, 0 ' 0 position CGRAM
LCDOUT 254,72 , 0, 24, 24 , 24, 24, 24 , 24, 0 ' 1 position CGRAM
LCDOUT 254,80 , 0, 27, 27 , 27, 27, 27 , 27, 0 ' CGRAM Position 2
LCDOUT 254,88 , 0, 27, 27 , 27, 27, 27 , 27, 0 ' 3 position CGRAM
LCDOUT 254,96 , 0, 27, 27 , 27, 27, 27 , 27, 0 ' 4 position CGRAM
PAUSE 300
GOTO BAR
' *******************************************************************************
GRAF3 :
Mark2 = 3
LCDOUT 254,64 , 0, 0 , 0, 16, 16 , 0, 0 , 0 ' 0 position CGRAM
LCDOUT 254,72 , 0, 0 , 0, 24, 24 , 0, 0 , 0 ' 1 position CGRAM
LCDOUT 254,80 , 0, 0 , 0, 28, 28 , 0, 0 , 0 ' 2 Position CGRAM 
LCDOUT 254,88 , 0, 0 , 0, 30, 30 , 0, 0 , 0 ' 3 position CGRAM
LCDOUT 254,96 , 0, 0 , 0, 31, 31 , 0, 0 , 0 ' 4 position CGRAM
PAUSE 300
GOTO BAR
' *******************************************************************************
GRAF4 :
Mark2 = 4
LCDOUT 254,64 , 0, 16, 16 , 16, 16 , 16, 16 , 0 ' 0 position CGRAM
LCDOUT 254,72 , 0, 16, 16 , 16, 16 , 16, 16 , 0 ' 1 position CGRAM
LCDOUT 254,80 , 0, 20, 20, 20 , 20, 20 , 20, 0  ' 2 Position CGRAM  
LCDOUT 254,88 , 0, 20, 20, 20 , 20, 20 , 20, 0  ' 3 position CGRAM
LCDOUT 254,96 , 0, 21, 21 , 21, 21, 21 , 21, 0  ' 4 position CGRAM
PAUSE 300
GOTO BAR
' *******************************************************************************
GRAF5 :
Mark2 = 5
LCDOUT 254,64 , 0, 0 , 0, 4 , 0, 0 , 0, 0    ' 0 position CGRAM
LCDOUT 254,72 , 0, 0 , 4, 14 , 4, 0 , 0, 0   ' 1 Position CGRAM
LCDOUT 254,80 , 0, 0 , 14, 14 , 14, 0, 0, 0  ' 2 Position CGRAM 
LCDOUT 254,88 , 0, 4 , 14, 31 , 14, 4, 0, 0  ' 3 position CGRAM
LCDOUT 254,96 , 0, 14, 31, 31, 31 , 14, 0, 0 ' 4 position CGRAM
PAUSE 300
GOTO BAR
' *******************************************************************************
GRAF6 :
Mark2 = 6
LCDOUT 254,64,0,14,31,31,31,14,0,0           ' 0 position CGRAM
LCDOUT 254,72,0,14,31,31,31,14,0,0           ' 1 position CGRAM
LCDOUT 254,80,0,14,31,31,31,14,0,0           ' 2 Position CGRAM
LCDOUT 254,88,0,14,31,31,31,14,0,0           ' 3 position CGRAM
LCDOUT 254,96,0,0,0,31,0,0,0,0               ' 4 position CGRAM
PAUSE 300
' *******************************************************************************
BAR :
IF Mark1 = 1 THEN                             ' If true
LINE = $2: ADCIN 7, CHANNEL: Mark1 = 0        ' 1st line LCD shows L channel
ELSE                                          ' If it is not
LINE = $c0: ADCIN 6, CHANNEL: Mark1 = 1       ' 2nd Line LCD shows R channel
ENDIF 'End of the comparison
' *******************************************************************************
REST = ( CHANNEL // 5 )    ' Operation subtracted from the division
LCDOUT $FE,LINE            ' Start writing in the LCD
FOR COUNTER = 1 TO(CHANNEL/5) 'Count forward
LCDOUT 4                   ' 4 position CGRAM Print
NEXT COUNTER               ' Returns to COUNTER
IF REST = 1 THEN  LCDOUT 0 ' Print CGRAM position 0
IF REST = 2 THEN  LCDOUT 1 ' Print CGRAM Position 1
IF REST = 3 THEN  LCDOUT 2 ' Prints 2 position CGRAM
IF REST = 4 THEN  LCDOUT 3 ' 3 position CGRAM Print
LCDOUT 20, 20 , 20         ' Clearing 3 characters
' *******************************************************************************
IF (pushbutton = 1) and (Mark2 = 6) THEN GRAF1   '1 ° Viewing 
IF (pushbutton = 1) and (Mark2 = 1) THEN GRAF2   '2 ° Viewing
IF (pushbutton = 1) and (Mark2 = 2) THEN GRAF3   '3 ° Viewing 
IF (pushbutton = 1) and (Mark2 = 3) THEN GRAF4   '4 ° Viewing 
IF (pushbutton = 1) and (Mark2 = 4) THEN GRAF5   '5 ° Viewing 
IF (pushbutton = 1) and (Mark2 = 5) THEN GRAF6   '6 ° Viewing 
' *******************************************************************************
GOTO BAR
END

PICBasic Pro Trial Version 3.0.7.1 [incluye MPASM v8.90, Microcode Studio 5 (MCSX) para PBP v3, PBP 3.0.7.1]
Tamaño: 123.1MB
SHA1 Hash: C449786CFE50B26EB6818F9C35E10C718C36F6AD
Descarga

PICKit 3 v3.01
Tamaño: 8.4MB
SHA1 Hash: 34600B01B759F65DF993E1FA6408B4B3B3BA4AC9
Descarga

Además también se encuentra disponible para descargar el firmware ya compilado, listo para cargar en el microcontrolador.

VuMeter v1.0 HEX
Tamaño: 2KB
SHA1 Hash: 9D1F393C4EA3B561377988757898DAE7
Descarga

Arduino es una plataforma prácticamente infinita para todo tipo de aplicaciones como lo hemos visto en este humilde blog de entre millones que existen en la web. En este espacio comenzamos con aplicaciones de Hacking, luego incursionamos en la fascinante disciplina de la Robótica y últimamente avanzamos sobre increíble universo de los Videojuegos.

Anteriormente publicamos juegos (basados en la librería VGAx desarrollada por Sandro Maffiodo) como, por ejemplo:

Y juegos más sencillos basados simplemente en LEDs y pulsadores como

En esta ocasión continuaremos con la construcción de juegos sencillos, pero algo más elaborados utilizando matrices de LEDs controladas por el circuito integrado MAX7219.
Pong (o Tele-Pong) fue un videojuego de la primera generación de videoconsolas publicado por Atari, creado por Nolan Bushnell y lanzado el 29 de noviembre de 1972. Pong está basado en el deporte de tenis de mesa (o ping pong). La palabra Pong es una marca registrada por Atari Interactive, mientras que la palabra genérica «pong» es usada para describir el género de videojuegos «bate y bola». La popularidad de Pong dio lugar a una demanda de infracción de patentes y ganada por parte de los fabricantes de Magnavox Odyssey, que poseía un juego similar.
Pong es un juego de deportes en dos dimensiones que simula un tenis de mesa. El jugador controla en el juego una paleta moviéndola verticalmente en la parte izquierda de la pantalla, y puede competir tanto contra un oponente controlado por computadora, como con otro jugador humano que controla una segunda paleta en la parte opuesta. Los jugadores pueden usar las paletas para pegarle a la pelota hacia un lado u otro. El objetivo consiste en que uno de los jugadores consiga más puntos que el oponente al finalizar el juego. Estos puntos se obtienen cuando el jugador adversario falla al devolver la pelota.

Este videojuego es un verdadero clásico y es sumamente adictivo para jugar durante breves intervalos de tiempo. Ya sea esperando un micro o taxi, alguna hora o minutos libres en la Universidad o Colegio, o simplemente de noche hasta que logramos conciliar el sueño.
Esta variante contiene tres niveles de dificultad aumentado la velocidad de desplazamiento de la pelota a medida que avanzamos de nivel. En el último nivel la pelota se desplaza muy rápidamente, no apto para cardíacos, y si logramos superar este nivel ocurre una secuencia que podrán observar en el gameplay que se encuentra al final de la publicación y solo apto para verdaderos fanáticos.

La Matriz de LEDs

Una matriz LEDs es un display formado por múltiples LEDs en distribución rectangular. Existen distintos tamaños, siendo el más habitual los cuadrados de 8×8 LEDs.
Podemos combinar varios módulos para formar un display mucho mayor. En estos display podemos mostrar textos, dibujos o animaciones, como desplazar un texto (scroll).
Por lo demás, son diodos LEDs totalmente normales, organizados en forma de matriz, que tendremos que multiplexar para poder iluminar uno u otro punto.
Si los diodos se unen por el positivo, se dice que son matrices de Ánodo común, y si se une por el negativo decimos que son de Cátodo común. Dependiendo del fabricante podemos encontrar de ambos tipos.

Encender una matriz de LEDs directamente con Arduino requiere emplear una gran cantidad de pines, lo cual supondría un gran desperdicio de recursos. Por este motivo, lo normal es siempre utilizar un controlador específicamente diseñado para esta función. Un controlador habitualmente empleado por ser barato y sencillo es el circuito integrado MAX7219.

Circuito Integrado MAX7219

Encender una matriz de 8×8 LED requeriría 16 señales digitales y un trabajo constante del procesador para refrescar la imagen. Eso es una cantidad enorme de recursos para cualquier autómata, que estaríamos desperdiciando para simplemente encender un display.
Por este motivo, utilizamos el circuito integrado MAX7219 que está especialmente diseñado para encender displays de 7 segmentos y matrices de LEDs, liberando al procesador para hacer tareas mucho más valiosas y productivas.
La comunicación con el circuito integrado MAX7219 se realiza mediante el bus SPI por lo que sólo se requieren 3 pines de Arduino (SS, MOSI y SCK). Además, ni siquiera “ocupamos” en su totalidad estos pines, ya que con el mismo bus podemos controlar múltiples dispositivos.
Por último, las placas MAX7219 generalmente incorporan un puerto de entrada y salida, de forma que podemos combinar múltiples controladores sin ninguna dificultad.
MAX7219 Datasheet

El diagrama esquemático de esta variante de Arduino Pong se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Resistencia 10KΩ 0.25W
1 Potenciómetro Lineal 10KΩ
1 Transistor NPN 2SC1815
2 Módulos con Matrices de LEDs 8×8 con Circuito Integrado MAX7219 c/u. Cátodo Común [Color Rojo en caso de aplicar Red Mod]
1 Buzzer Pasivo 5V
1 Placa Arduino Nanino
Librería LedControl desarrollada por wayoda

#include <LedControl.h>

#define DATA_PIN    12
#define CLOCK_PIN   11
#define CS_PIN      10
#define NUM_DEVICES 2
#define KNOB_PIN    A0
#define RANDOM_PIN  A1
#define SPEAKER_PIN 7

#define N_a   440
#define N_f   349
#define N_cH  523
#define N_eH  659
#define N_fH  698
#define N_gS  415

int step_col, step_row, subidas, OLD_X, sensor, row, col, sc, sr, dly;
int row_bar = 0;
int X = 4;
int cnt = 0;
int desarma = 0;

boolean showBars_enabled = false;

LedControl LC = LedControl(DATA_PIN, CLOCK_PIN, CS_PIN, NUM_DEVICES);

byte MSG[8 * 20];
byte SPACE[8] = {
    B00000000,
    B00000000,
    B00000000,
    B00000000,
    B00000000,
    B00000000,
    B00000000,
    B00000000
};
byte W[8] = {
    B00000000,
    B01000010,
    B01000010,
    B01000010,
    B01000010,
    B01011010,
    B01100110,
    B00000000
};
byte I[8] = {
    B00000000,
    B00111100,
    B00011000,
    B00011000,
    B00011000,
    B00011000,
    B00111100,
    B00000000
};
byte N[8] = {
    B00000000,
    B01000010,
    B01100010,
    B01010010,
    B01001010,
    B01000110,
    B01000010,
    B00000000
};
byte E[8] = {
    B00000000,
    B01111110,
    B01000000,
    B01111100,
    B01000000,
    B01000000,
    B01111110,
    B00000000
};
byte R[8] = {
    B00000000,
    B01111100,
    B01000010,
    B01111100,
    B01001000,
    B01000100,
    B01000010,
    B00000000
};

void loose()
{
    delay(80);

    for(row = 0; (row < 16); row++) {
        setROW(row, 0xFF);
        tone(SPEAKER_PIN, (row * 500), 10);
        delay(20);
    }

    delay(130);

    for(row = 15; (row >= 0); row--) {
        setROW(row, 0x00);
        tone(SPEAKER_PIN, (row * 500), 10);
        delay(20);
    }
}

void showBars(boolean show)
{
    showBars_enabled = show;

    if(show) {
        row_bar = (2 + random(5));
    }

    setROW(row_bar, (show == true ? 0xFF : 0x00));
}

void setLED(int l, int c, boolean state)
{
    int disp = 0;

    if(l > 7) {
        l = (l - 8);
        disp = 1;
    }

    LC.setLed(disp, l, c, state);
}

void setROW(int r, char ch)
{
    int disp = 0;

    if(r > 7) {
        r = (r - 8);
        disp = 1;
    }

    LC.setRow(disp, r, ch);
}

void copyLetter(byte *ch,int pos)
{
    int i;

    for(i = 0; (i < 8); i++) {
        *(MSG + i + (pos * 8)) = *( ch + i);
    }
}

void showMSG(int qtde, int tempo)
{
    int idx;

    LC.clearDisplay(0);
    LC.clearDisplay(1);

    for(idx = 0; (idx < (8 * qtde)); idx++) {
        for(row = 0; (row < 16); row++) {
            setROW(row, *(MSG + row + idx));
        }
		
        delay(tempo);
    }

    LC.clearDisplay(0);
    LC.clearDisplay(1);
}

void beep(int frequencyInHertz, long timeInMilliseconds)
{
    int x;
    long delayAmount = (long)(1000000 / frequencyInHertz);
    long loopTime = (long)((timeInMilliseconds * 1000) / (delayAmount * 2));

    for(row = 0; (row < 16); row++) {
        setROW(row,0xFF);
    }

    for (x = 0; (x < loopTime); x++) {
        digitalWrite(SPEAKER_PIN, HIGH);
        delayMicroseconds(delayAmount);
        digitalWrite(SPEAKER_PIN, LOW);
        delayMicroseconds(delayAmount);
    }

    LC.clearDisplay(0);
    LC.clearDisplay(1);

    delay(20);
}

void march()
{
    beep(N_a, 500);
    beep(N_a, 500);
    beep(N_a, 500);
    beep(N_f, 350);
    beep(N_cH, 150);

    beep(N_a, 500);
    beep(N_f, 350);
    beep(N_cH, 150);
    beep(N_a, 1000);

    beep(N_eH, 500);
    beep(N_eH, 500);
    beep(N_eH, 500);
    beep(N_fH, 350);
    beep(N_cH, 150);

    beep(N_gS, 500);
    beep(N_f, 350);
    beep(N_cH, 150);
    beep(N_a, 1000);
}

void winner()
{
    copyLetter(SPACE, 0);
    copyLetter(SPACE, 1);

    copyLetter(W, 2);
    copyLetter(I, 3);
    copyLetter(N, 4);
    copyLetter(N, 5);
    copyLetter(E, 6);
    copyLetter(R, 7);

    copyLetter(SPACE, 8);
    copyLetter(SPACE, 9);

    showMSG(9, 80);

    march();

    setup();
}

void setup() {
    pinMode(KNOB_PIN, INPUT);      // Potentiometer 10K Linear (GND, KNOB_PIN, +5v)
	pinMode(RANDOM_PIN, INPUT);    // Random Seed
    pinMode(SPEAKER_PIN, OUTPUT);  // Passive Buzzer

    LC.shutdown(0, false);
    LC.setIntensity(0, 8);
    LC.clearDisplay(0);

    LC.shutdown(1, false);
    LC.setIntensity(1, 8);
    LC.clearDisplay(1);

    randomSeed(analogRead(RANDOM_PIN) * millis());

    loose();

    dly = 500;

    setLED(15, X, true);
    setLED(15, (X + 1), true);
    setLED(15, (X + 2) ,true);
    OLD_X = (-1);
    subidas = 0;

    step_col = 1;
    step_row = 1;

    sc = step_col;
    sr = step_row;

    row = ((-1) + random(3));
    col = random(8);

    showBars(false);
}

void loop()
{
    sensor = analogRead(KNOB_PIN);

    X = map(sensor, 0, 980, 1, 6);

    if(X != OLD_X) {
        OLD_X = X;
        setROW(15, 0x00);
        setLED(15, (X - 1),true);
        setLED(15, X, true);
        setLED(15, (X + 1), true);
    }

    if(cnt == 0) {
        setLED(row, col, false);

        if((subidas == 1 || random(2) == 1) && showBars_enabled == false && row == 0) {
            showBars(true);
            subidas = 0;
            desarma = (2 + random(5));
        }

        if((subidas == desarma) && (showBars_enabled == true)) {
            showBars(false);
            subidas = 0;
        }

        if(col == 7) {
            sc = -step_col;
            tone(SPEAKER_PIN, 1000, 20);
        }

        if(col == 0) {
            sc = step_col;
            tone(SPEAKER_PIN, 1000, 20);
        }

        if(row == 0) {
            sr = step_row;
            subidas++;
            dly -= 5;
            tone(SPEAKER_PIN, 1000, 20);
        }

        if(dly <= 190) {
            winner();
        }

        if(showBars_enabled == true && sr > 0 && row < row_bar) {
            if(row == (row_bar - 1)) {
                sr = -step_row;
                tone(SPEAKER_PIN, 1000, 20);
            }
        }

        if(row == 14)
        {
            if(col >= (X - 1) && col <= (X + 1)) {
                sr = -step_row;
				tone(SPEAKER_PIN, 1500, 20);
			}
            else if(col == (X - 2) && (sc > 0)) {
                sr = -step_row;
                sc = -step_col;
                tone(SPEAKER_PIN, 1500, 20);
            }
            else if(col == (X + 2) && (sc < 0)) {
                sr = -step_row;
                sc = step_col;
                tone(SPEAKER_PIN, 1500, 20);
            }
        }

        row += sr;
        col += sc;

        if(col == 8) {
            col = 7;
        }

        if(col == (-1)) {
            col = 0;
        }

        setLED(row, col, true);

        if(row == 15) {
            setup();
        }
    }

    cnt++;

    if(cnt == dly) {
        cnt = 0;
    }
}

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

(click para ampliar)

Red Mod
La cinta Kapton es resistente al calor y eléctricamente aislante que se usa mucho en electrónica e impresión 3D. Si has estado en esta afición por un tiempo hay una buena probabilidad de que tengas un rollo a mano.

Una o dos capas de cinta Kapton aplicada a las matrices las hacen menos propensas a lavarse bajo luz brillante. El color de la cinta es lo suficientemente similar a los LEDs rojos que brillan a través con poca dificultad, mientras que la mayoría de la luz ambiente está bloqueada.

La película de enmascarar “Rubylith” (empleada en diseño gráfico) probablemente funcionaría igual de bien, o quizás mejor. Este film puede conseguirse en tiendas de arte decentes de la vieja escuela.

En publicaciones anteriores hemos visto diferentes mecanismos de control para nuestro robot, tanto manuales (Construyendo Nuestro Propio Robot Desde Cero – Parte 3) como autónomos (Arduino Robot Seguidor de Líneas y Laberintos). Ahora veremos una alternativa más, también inalámbrica, al igual que en la publicación anterior (Arduino Robot Controlado por Bluetooth y Android App) y que en estos últimos años ha crecido de manera exponencial en el mercado. El lector perspicaz ya advirtió que se trata de la tecnología Wi-Fi (Wireless Fidelity), omnipresente en el mercado y en nuestros hogares.

En cuanto empieces a buscar algo de Wi-Fi económico, te encontrarás con el módulo Wi-Fi ESP8266, fabricado por ESPRESSIF Systems, el cual es muy parecido a los módulos Bluetooth de la publicación anterior, y que al igual que ellos incluye toda la electrónica necesaria para la comunicación RF en la banda Wi-Fi, así como la pila TCP/IP y se comunica con nosotros a través de un puerto serie.

De hecho, exactamente al igual que los módulos Bluetooth HC-06 y HC-05, se gobierna mediante comandos AT, algo que ya no tiene secretos para los seguidores de este blog, y todo por un precio similar al de los módulos Bluetooth.
Lo primero, es decir que es un módulo muy sencillo y diseñado desde el principio con la “Internet of Things” en mente (IoT), y por eso incluye todo lo necesario para conectarse a un punto de acceso Wi-Fi o actuar por sí mismo como punto de acceso Wi-Fi mediante comandos AT, vía un puerto serie, que puede ser configurado a diferentes velocidades.

Este circuito integrado es una pequeña maravilla tecnológica. Está diseñado y pensado para ser la solución ideal para todo el que necesite un “Todo en uno Wi-Fi” para proyectos de hobby, IoT o producto comercial sin complicaciones. Y lo más importante, lo consigue.

  • Voltaje de alimentación: 3.3V
  • Procesador interno de 32 bits a 80 MHz (se puede incrementar hasta 160MHz).
  • 80KB de DRAM.
  • 35KB de IRAM, memoria rápida para el procesador.
  • 512KB (hasta 1MB, dependiendo de la versión del módulo) de memoria flash para nuestros programas.
  • Pila TCP/IP Wi-Fi a 2.4 GHz (b/g/n).
  • 30 metros de alcance teórico.

Nota del Autor: de hecho, el módulo ESP8266 incluye un pequeño procesador interno que podríamos programar para funcionar de modo autónomo y dispone de un par de puertos GPIO (General Purpose Input Output) para su uso como activadores. Es posible programarlo desde el IDE Arduino, y más interesante aún, con MicroPython.

Lista de Componentes
1 Plataforma Turtle2WD con placa Arduino UNO R3 y Shield de Control de Motores ensamblados anteriormente.
1 Adaptador Sparkfun FTDI USB – UART TTL 3.3V
1 Módulo Wi-Fi ESP8266 (ESP-01)
1 Convertidor Bidireccional de Niveles Lógicos Sparkfun.
1 Capacitor Electrolítico 1000uF 6.3V
2 Baterías AA 1.5V de Alto Rendimiento
1 Portapilas para 2 Baterías AA

Antes de instalar el módulo en nuestro robot, modificaremos algunos parámetros de su configuración mediante comandos AT. Para llevar a cabo esta tarea sólo necesitamos descargar el documento que contiene el set de instrucciones AT ESP8266 y un emulador de terminal RS232.

Set de Instrucciones AT ESP8266

Termite 3.3
Tamaño: 294KB
SHA1 Hash: 5A7DEBD9E1F245EB87FAEF397187752D5CFF1841
Descarga     Mirror

A continuación conectaremos nuestro módulo al Adaptador FTDI USB – UART TTL 3.3V como se muestra en la siguiente imagen:

(click para ampliar)

Ahora, el emulador de terminal (en nuestro caso, Termite) debe estar configurado de la siguiente manera, que son los parámetros por defecto de nuestro módulo Wi-Fi.

Habiendo logrado una conexión exitosa en el paso anterior, ahora debemos enviar los comandos AT necesarios para configurar nuestro módulo. En la siguiente imagen se observa la secuencia de comandos enviados:

AT
AT+CWSAP_DEF="TURTLE2WD","",1,0,4
AT+UART_DEF=19200,8,1,0,0

Nuestros comandos enviados se encuentran en color azul, en color verde se encuentran las respuestas de nuestro módulo y el eco local de la consola. Con la ejecución exitosa de estos comandos modificamos el SSID de nuestro módulo por “TURTLE2WD” para identificarlo fácilmente y redujimos la velocidad del puerto serie a 19200bps lo cual nos permite utilizar cables de mayor longitud para el montaje en caso de ser necesario.

Habiendo finalizado exitosamente los pasos anteriores, compilaremos y cargaremos en nuestro Arduino Uno R3 el sketch correspondiente, cuyo código fuente se encuentra a continuación:

/*
Caracteristicas:
- Control por Wi-Fi utilizando el modulo ESP8266
- Interfaz web con jquey para envio de peticiones GET
- Id de botones para envio de datos
W: Adelante
S: Atras
A: Izquierda
D: Derecha
L: Inicio/Parada Modo Vehiculo

Q: Incrementar Velocidad
T: Disminuir Velocidad
X: Encendido/Apagado Luz Frontal

- Detencion automatica en caso de detectar bordes.
*/

#define     PWM_RIGHT   3     // (Control Velocidad Motor Derecho) Timer2 8 bits D3
#define     PWM_LEFT    11    // (Control Velocidad Motor Izquierdo) Timer2 8 bits D11
#define     DIR_A       12    // (Control de Direccion de Motores) D12
#define     DIR_B       13    // (Control de Direccion de Motores) D13
#define     MAX_SPEED   200   // Velocidad Maxima
#define     MIN_SPEED   100   // Velocidad Minima
// -----

// Iluminacion
#define     LED_LIGHT   7
// -----

// Sensores Reflectivos Detectores de Bordes
#define     BORDER_L    A2    // Sensor Izquierdo Externo
#define     BORDER_R    A5    // Sensor Derecho Externo

// Serial commands
#define     DIR_FORWARD     'W'
#define     DIR_BACKWARD    'S'
#define     DIR_LEFT        'A'
#define     DIR_RIGHT       'D'
#define     START_STOP_V    'L'
#define     INC_SPEED       'Q'
#define     DEC_SPEED       'T'
#define     FRONT_LIGHT     'X'
// -----

#define     DEBUG   false

boolean stopped;

byte speed, connectionid;

char data;

void forward() {
    digitalWrite(DIR_A, LOW);
    digitalWrite(DIR_B, LOW);
}

void backward() {
    digitalWrite(DIR_A, HIGH);
    digitalWrite(DIR_B, HIGH);
}

void rotateleft() {
    digitalWrite(DIR_A, LOW);
    digitalWrite(DIR_B, HIGH);
}

void rotateright() {
    digitalWrite(DIR_A, HIGH);
    digitalWrite(DIR_B, LOW);
}

void stop() {
    analogWrite(PWM_LEFT, 0);
    analogWrite(PWM_RIGHT, 0);
}

void setspeed(boolean sp) {
    if (sp) {
        if (speed < MAX_SPEED) {
            speed += 50;
            analogWrite(PWM_LEFT, speed);
            analogWrite(PWM_RIGHT, speed);
        }
    }
    else
    {
        if (speed > MIN_SPEED) {
            speed -= 50;
            analogWrite(PWM_LEFT, speed);
            analogWrite(PWM_RIGHT, speed);
        }
    }
}

void setlight() {
    digitalWrite(LED_LIGHT, !(digitalRead(LED_LIGHT)));
}

boolean isborder() {
    if ((digitalRead(BORDER_L) == HIGH) || (digitalRead(BORDER_R) == HIGH))
        return true;
    else
        return false;
}

String sendData(String command, const word timeout, boolean debug) {
    String response = "";

    Serial.print(command); // send the read character to the esp8266

    unsigned long time = millis();

    while ((time + timeout) > millis()) {
        while(Serial.available()) {
            // The esp has data so display its output to the serial window
            char c = Serial.read(); // read the next character.
            response += c;
        }
    }

    if (debug)
        Serial.print(response);

    return response;
}

void setup() {
    pinMode(PWM_LEFT, OUTPUT);
    pinMode(PWM_RIGHT, OUTPUT);
    pinMode(DIR_A, OUTPUT);
    pinMode(DIR_B, OUTPUT);
    pinMode(LED_LIGHT, OUTPUT);

    pinMode(BORDER_L, INPUT);
    pinMode(BORDER_R, INPUT);

    digitalWrite(LED_LIGHT, LOW);

    stopped = true;

    speed = MIN_SPEED;

    Serial.begin(19200);

    sendData("AT+CIPMUX=1\r\n", 1000, DEBUG); // configure for multiple connections
    sendData("AT+CIPSERVER=1,80\r\n", 1000, DEBUG); // turn on server on port 80
}

void loop() {
    if (Serial.available()) {
        if (Serial.find("+IPD,")) {
            delay(100);
            connectionid = (Serial.read() - 48);

            Serial.find("cmd=");
            data = Serial.read();

            if (data == START_STOP_V) {
                if (stopped) {
                    forward();

                    analogWrite(PWM_LEFT, speed);
                    analogWrite(PWM_RIGHT, speed);

                    stopped = false;
                }
                else
                {
                    stop();
                    stopped = true;
                }
            }

            switch (data) {
                case DIR_FORWARD:
                    if (!stopped)
                        forward();
                    break;

                case DIR_BACKWARD:
                    if (!stopped)
                        backward();
                    break;

                case DIR_LEFT:
                    if (!stopped)
                        rotateleft();
                    break;

                case DIR_RIGHT:
                    if (!stopped)
                        rotateright();
                    break;

                case INC_SPEED:
                    if (!stopped)
                        setspeed(true);
                    break;

                case DEC_SPEED:
                    if (!stopped)
                        setspeed(false);
                    break;

                case FRONT_LIGHT:
                    setlight();
                    break;

                default:
                    break;
            }

            String closecommand = "AT+CIPCLOSE=" + String(connectionid) + "\r\n";
            sendData(closecommand, 100, DEBUG);
        }
    }

    if (isborder())
        stop();
}

En este punto nos encontramos en la etapa final. A continuación conectaremos nuestro módulo Wi-Fi a la placa Arduino UNO R3, como se observa en la siguiente imagen:

(click para ampliar)

Por último necesitamos la interfaz web desde la cual controlaremos nuestro robot. Consta de un documento HTML y jQuery, una biblioteca multiplataforma de JavaScript, que permite simplificar la manera de interactuar con los documentos HTML, manipular el árbol DOM, manejar eventos, desarrollar animaciones y agregar interacción con la técnica AJAX a páginas web. jQuery es la biblioteca de JavaScript más utilizada.

Interfaz Web
Tamaño: 27.5KB
SHA1 Hash: 0C9AE81B92C8243C362C2DFA94C79065D8D3447E
Descarga


En publicaciones anteriores hemos visto diferentes mecanismos de control para nuestro robot, tanto manuales (Construyendo Nuestro Propio Robot Desde Cero – Parte 3) como autónomos (Arduino Robot Seguidor de Líneas y Laberintos). Ahora veremos una alternativa más, también inalámbrica, mucho más eficiente que los rayos infrarrojos (IR) y muy popular entre los teléfonos móviles (sí, controlaremos nuestro robot a través de nuestro teléfono móvil/tablet). De entre todos los módulos de control inalámbricos existentes para Arduino, el Bluetooth es, en mi humilde opinión, el más interesante. Es un módulo de muy bajo costo, muy sencillo de conectar (solo posee dos pines omitiendo VCC y GND), funciona como un puerto serie estándar, es compatible con cualquier dispositivo existente el mercado actual, como laptops, tablets y teléfonos móviles, lo que le otorga una ventaja más a las mencionadas anteriormente: versatilidad.

Básicamente existen 3 tipos de módulos Bluetooth en el mercado actual:

  • HC-05: Maestro/Esclavo. Puede conectarse a otros módulos (Modo Maestro) o aceptar conexiones de otros módulos comportándose como puente inalámbrico serie (Modo Esclavo).
  • HC-06: Esclavo. Sólo puede aceptar conexiones de otros módulos, comportándose como puente inalámbrico serie (Modo Esclavo).
  • HC-07: Esclavo (de muy bajo costo, menor a HC-06 y escasa documentación. Set de comandos AT limitado).

Para controlar nuestro robot utilizaremos el último módulo de la lista, HC-07, ya que sólo necesitamos establecer una conexión desde nuestro teléfono móvil/tablet hacia nuestro robot para enviar los comandos necesarios.

Lista de Componentes
1 Plataforma Turtle2WD con placa Arduino UNO R3 y Shield de Control de Motores ensamblados anteriormente.
1 Módulo Bluetooth HC-07.
Teléfono móvil o Tablet con Sistema Operativo Android 2.3.3 o superior.
Aplicación “Arduino Bluetooth Controller” (disponible en Google Play Store).

La aplicación “Arduino Bluetooth Controller” nos ofrece diferentes modos para controlar nuestro robot, incluyendo una terminal para enviar y/o recibir datos mediante el enlace bluetooth. Para nuestro caso particular, utilizaremos los modos “Vehicle Mode” y “Controller Mode“. El primero nos permite controlar nuestro robot utilizando el giroscopio y acelerómetro de nuestro teléfono móvil/tablet y el segundo nos permite controlar nuestro robot mediante un joystick táctil.

En primer lugar, debemos descargar e instalar en nuestro teléfono móvil/tablet la aplicación mencionada anteriormente disponible en Google Play Store.
Habiendo finalizado exitosamente el paso anterior, compilaremos y cargaremos en nuestro Arduino Uno R3 el sketch correspondiente, cuyo código fuente se encuentra a continuación:

/*
Caracteristicas:
- Aplicacion "Arduino Bluetooth Controller" (FREE - Play Store)
- Manejo por enlace bluetooth mediante las teclas:
W: Adelante
S: Atras
A: Izquierda
D: Derecha
L: Inicio/Parada Modo Vehiculo
V: Inicio/Parada Modo Joystick

Q: Incrementar Velocidad
T: Disminuir Velocidad
x: Encendido/Apagado Luz Frontal

- Detencion automatica en caso de detectar bordes.
*/

#define     PWM_RIGHT   3     // (Control Velocidad Motor Derecho) Timer2 8 bits D3
#define     PWM_LEFT    11    // (Control Velocidad Motor Izquierdo) Timer2 8 bits D11
#define     DIR_A       12    // (Control de Direccion de Motores) D12
#define     DIR_B       13    // (Control de Direccion de Motores) D13
#define		MAX_SPEED	200   // Velocidad Maxima
#define		MIN_SPEED	100   // Velocidad Minima
// -----

// Iluminacion
#define     LED_LIGHT   7
// -----

// Sensores Reflectivos Detectores de Bordes
#define		BORDER_L	A2    // Sensor Izquierdo Externo
#define		BORDER_R	A5    // Sensor Derecho Externo

// Serial commands
#define     DIR_FORWARD		'W'
#define     DIR_BACKWARD	'S'
#define     DIR_LEFT		'A'
#define     DIR_RIGHT		'D'
#define     START_STOP_V	'L'
#define		START_STOP_J	'V'
#define		INC_SPEED		'Q'
#define		DEC_SPEED		'T'
#define		FRONT_LIGHT		'X'
// -----

boolean stopped;

byte speed;

char data;

void forward() {
	digitalWrite(DIR_A, LOW);
	digitalWrite(DIR_B, LOW);
}

void backward() {
	digitalWrite(DIR_A, HIGH);
	digitalWrite(DIR_B, HIGH);
}

void rotateleft() {
	digitalWrite(DIR_A, LOW);
	digitalWrite(DIR_B, HIGH);
}

void rotateright() {
	digitalWrite(DIR_A, HIGH);
	digitalWrite(DIR_B, LOW);
}

void stop() {
	analogWrite(PWM_LEFT, 0);
	analogWrite(PWM_RIGHT, 0);
}

void setspeed(boolean sp) {
	if (sp) {
		if (speed < MAX_SPEED) {
			speed += 50;
			analogWrite(PWM_LEFT, speed);
			analogWrite(PWM_RIGHT, speed);
		}
	}
	else
	{
		if (speed > MIN_SPEED) {
			speed -= 50;
			analogWrite(PWM_LEFT, speed);
			analogWrite(PWM_RIGHT, speed);
		}
	}
}

void setlight() {
	digitalWrite(LED_LIGHT, !(digitalRead(LED_LIGHT)));
}

boolean isborder() {
	if (!((digitalRead(BORDER_L) == LOW) && (digitalRead(BORDER_R) == LOW)))
		return true;
	else
		return false;
}

void setup() {
	pinMode(PWM_LEFT, OUTPUT);
	pinMode(PWM_RIGHT, OUTPUT);
	pinMode(DIR_A, OUTPUT);
	pinMode(DIR_B, OUTPUT);
	pinMode(LED_LIGHT, OUTPUT);

	pinMode(BORDER_L, INPUT);
	pinMode(BORDER_R, INPUT);

	digitalWrite(LED_LIGHT, LOW);

	stopped = true;

	speed = MIN_SPEED;

	Serial.begin(9600);
}

void loop() {
	if (Serial.available()) {

		data = Serial.read();

		if ((data == START_STOP_J) || (data == START_STOP_V)) {
			if (stopped) {
				forward();

				analogWrite(PWM_LEFT, speed);
				analogWrite(PWM_RIGHT, speed);

				stopped = false;
			}
			else
			{
				stop();
				stopped = true;
			}
		}

		switch (data) {
			case DIR_FORWARD:
				if (!stopped)
					forward();
				break;

			case DIR_BACKWARD:
				if (!stopped)
					backward();
				break;

			case DIR_LEFT:
				if (!stopped)
					rotateleft();
				break;

			case DIR_RIGHT:
				if (!stopped)
					rotateright();
				break;

			case INC_SPEED:
				if (!stopped)
					setspeed(true);
				break;

			case DEC_SPEED:
				if (!stopped)
					setspeed(false);
				break;

			case FRONT_LIGHT:
				setlight();
				break;

			default:
				break;
		}
	}

	if (isborder())
		stop();
}

Ahora procedemos a conectar nuestro módulo Módulo Bluetooth HC-07 de la siguiente manera:

Módulo Bluetooth HC-07 –> Arduino Uno R3
TX      –>   D0 (RX)
RX     –>   D1 (TX)
5V     –>   5V
GND  –>  GND

Ahora encenderemos nuestro robot y lo buscamos en nuestro teléfono móvil/tablet (normalmente con el nombre “HC-07“, aunque esto puede variar) y lo emparejaremos con nuestro dispositivo, la contraseña por defecto es “1234” (sin comillas) o “0000” (sin comillas). En la siguiente imagen observamos nuestro dispositivo emparejado con nuestro robot:

Habiendo finalizado exitosamente el paso anterior, el cual es fundamental para continuar, ahora lanzaremos la aplicación “Arduino Bluetooth Controller“, que aparecerá en nuestro teléfono móvil/tablet con el nombre “ArduinoRC“, observaremos lo que se muestra en la siguiente imagen:

Presionamos el botón “Proceed“.
Luego nos aparecerá una lista de los dispositivos emparejados a nuestro teléfono móvil/tablet. Seleccionamos nuestro robot:

Si la conexión se estableció correctamente, nos aparecerá lo que observamos en la siguiente imagen y presionamos el botón “Vehicle Mode“:

Ahora debemos personalizar la configuración de la aplicación, para esto debemos presionar el menú desplegable y seleccionar la opción “Set Commands“, como se muestra en la siguiente imagen:

Ahora procedemos a personalizar la configuración según las siguientes imágenes:

Finalizada exitosamente la configuración, podremos controlar los movimientos de nuestro robot utilizando el giroscopio y acelerómetro de nuestro teléfono móvil/tablet. El botón de Android controla “Inicio/Parada” de nuestro robot.

Habiendo configurado y verificado el correcto funcionamiento de este modo de control, a continuación regresaremos al inicio de nuestra aplicación y presionaremos el botón “Controller Mode“.

Ahora debemos personalizar la configuración de la aplicación, para esto debemos presionar el menú desplegable y seleccionar la opción “Set Commands“, como se muestra en la siguiente imagen:

Ahora procedemos a personalizar la configuración según las siguientes imágenes:

Finalizada exitosamente la configuración, podremos controlar los movimientos de nuestro robot utilizando el joystick táctil de la aplicación. En la siguiente imagen observamos la función de cada botón del joystick: