Robertxpp
Usuario (México)
Snake con Arduino, Joystick y matriz de LEDsEn esta ocasión veremos como implementar el juego de SNAKE mediante un programa desarrollado en C++ para explicar el funcionamiento de la comunicación serial entre el Arduino y una computadora. Se presupone que el lector tiene conocimientos básicos de programación en C++ y que está familizarizado con el entorno del Arduino, en todo caso trataré de explicar cada proceso de forma detallada.El entorno de desarrollo utilizado para este proyecto fue Qt Creator corriendo en Ubuntu y con ayuda de las bibliotecas QExtSerialPort. Si no tienes instalada la plataforma Qt junto con el editor Qt Creator, ¿a qué esperas?; Qt es un conjunto de paquetes que permiten desarrollar aplicaciones en C++ de forma rápida y eficiente y Qt Creator es el editor para gestionar proyectos, compilar, depurar y administrar los archivos de tus aplicaciones Qt. En Ubuntu 11.04 no deberías tener problemas para instalar QtCreator puesto que lo obtienes como cualquier otra aplicación. Para instalarlo en Windows te recomiendo que visites la .Dado que Qt no incluye ningún componente para manejar puertos se ha recurrido a opciones de código abierto con QExtSerialPort. Recomiendo visitar el para saber cómo instalar y configurar la biblioteca. No te demores tanto en este proceso, hasta ahora lo que debes tener "a la mano" es el Qt Creator totalmente operacional (junto con los paquetes Qt) + la biblioteca QExtSerialPort descargada (puedes checar ), compilada y debidamente instalada. Si trabajas en Windows checa el tutorial de Angel Perles. Las herramientas y materiales requeridos (en orden de importancia) son: Microcontrolador Arduino UNO Computadora o laptop con S.O. Ubuntu (o Windows XP, 7) Matriz de LEDs 8x8 (GMM-23088CSB) Joystick de 2 ejes Cable USB para conectar el Arduino Placa modular de contactos de presión (protoboard) x 2 Cables y pinzas para los puentes Cautín y soldadura para las unionesArmando el circuitoPodemos entender la matriz de LEDs como un sistema de coordenadas poco convencional: si queremos que encienda un LED determinado (sin tomar en cuenta el resto de LEDs) debemos mandar 5.0 V en la columna en que se encuentra dicho LED y 0.0 V en la fila del mismo, esto puede resultarte un tanto obvio, ya que cuando la corriente fluye por el LED de forma directa, el LED se "activa". Pero hay un problema que debemos contemplar al intentar encender varios LEDs a la vez. Supon que quieres ver una diagonal, y para ello mandas 5.0 V a la primera columna y 0.0 V a la primera fila, con esto lograrás que encienda el primer LED; ahora debes encender el LED de la columna 2 y la fila 2, pero sin encender los LEDs de las coordenadas (1, 2) y (2, 1). Si ya mandaste 0.0 V a la fila 1, al mandar 5.0 V a la columna 2 para encender el LED (2, 2) conseguirás encender no únicamente el LED (2, 2) que es el que te interesa, sino también TODOS los LEDs que se encuentren en las filas a las que mandes 0.0 V.El problema se soluciona encendiendo una fila o columna a la vez manteniendo las otras apagadas. Si recorremos todas las columnas por ejemplo, con tal rapidez que no sea perceptible por el ojo humano, dará la impresión de que todas las columnas están encendidas, pero por cada columna únicamente los LEDs que nos interesan (esto lo haremos con programación).Ahora para conectar cada pin de la matriz en el Arduino debemos tener en cuenta que no se encuentran ordenados como quisiéramos. Puedes consultar el datasheet del modelo de matriz que te haz conseguido para determinar cómo conectarlas. Las columnas y las filas se numeran del 1 al 8, y en mi caso las columnas corresponden a los pines 13, 3, 4, 10, 6, 11, 15, 16 (en orden creciente) y las filas corresponden a los pines 9, 14, 8, 12, 1, 7, 2, 5. Conectaremos estos pines como mejor nos convenga en los pines del Arduino y más adelante los ubicaremos mediante código.Observa que tenemos 16 pines a conectar en el Arduino, y no podemos usar los pines 0 y 1 del Arduino porque se requieren durante la comunicación serial, así que utilizaremos como salidas digitales los pines analógicos A5, A4, A3, A2 y los pines digitales del 2 al 13. Nos quedarán libres dos pines analógicos que servirán para las lecturas del Joystick.Yo he conectado los pines, por comodidad, de esta forma:Debemos tener en cuenta qué pin de la matriz conectamos a cada salida del Arduino, para que se organicen en un arreglo (Array) en el programa del Arduino,Preparando el JoystickUn joystick de dos ejes no es más que dos potenciómetros unidos a una palanca, más los botones. El potenciómetro de uno de los ejes se posiciona en el centro y cuando movemos la palanca giramos el potenciómetro hacia uno de los lados. Generalmente los valores de los potenciómetros son de 10 KOhms y en un viejo Joystick para computadora la cablería no conecta los potenciómetros como requerimos. Es por ello que debes cerciorarte de que las patitas de los potenciómetros esten conectadas como muestra el siguiente diagrama, en caso contrario deberás soldar las patitas.Conectamos entonces los cables del Joystick en los pines analógicos que quedan del Arduino. El pin medio de los potenciómetros van a las entradas analógicas y los pines de los extremos van conectados a Gnd y 5V.De esta forma al mover los potenciómetros estaremos variando el voltaje de entrada en los pines analógicos del Arduino, con lo que conseguiremos saber en que dirección estamos moviendo el Joystick. Los valores de voltaje leído se mapean entre 0 y 1023 (10 bits), por lo tanto sólo basta conocer el valor de este entero para "mover" algo. El valor medio (512) corresponde al Joystick en estado de reposo, aunque debido a las características mecánicas del Joystick está valor fluctuará alrededor de 512 pero núnca permanecerá estable, por lo que en el programa debemos preguntar que tan cerca se encuentra de 0 para tomar una dirección o que tan cerca está de 1023 para irse por la otra dirección (en un viejo Joystick de computadora dificilmente los valores de lectura serán 0 y 1023, en realidad siempre son valores aproximados). Para un mayor performance deberemos CALIBRAR el Joystick, intentando que en estado de reposo los valores de lectura sean aproximados a 512.Programa del ArduinoEste sketch de Arduino se encarga de leer los valores del Joystick, mandar estas lecturas en forma de cadena de caracteres a través del puerto serial USB de la computadora para que sean interpretadas por el programa de C++. También recibe los valores del puerto USB para encender la matriz de LEDs con los valores indicados en la cadena leída. Todo esto funciona de forma coordinada para permitir establecer una comunicación continua entre Arduino y computadora, o de otro modo el sistema colapsaría y no podría continuar.El principio básico es el siguiente: Preparamos una bandera inicializada en FALSE al inicio del programa del Arduino, esta bandera sirve para controlar la sincronización entre lectura y escritura de datos por el puerto USB. Leemos los valores del Joystick (en realidad son dos potenciómetros conectados a los pines analógicos 0 y 1), dividimos este valor entre 4 (en realidad no es necesario, antes se intentó mandar dos bytes en vez de una cadena de caracteres y la conversión se utilizaba para convertir esos 10 bits, en 8 bits, osea un byte) e imprimimos los valores al puerto USB (Serial.print). Después de imprimir tenemos que esperar a que la computadora "conteste" con otra cadena de caracteres, indicando qué LEDs deben ser activados en la matriz; cuando se imprime se activa la bandera de espera, con el objeto de impedir mandar los datos de nuevo. Una vez que se lee la cadena completa enviada por la comptadora, activamos los LEDs que indica la cadena y ponemos la bandera de espera en FALSE para poder mandar la siguiente lectura del Joystick. Esto lo hacemos repetidamente hasta el final del programa. Si te he confundido, mejor lee el código e intenta sacar tus conclusiones, es probable que no lo entiendas por completo hasta que lo compares con el programa en C++ desarrollado en QtCreator.Ahora el programa en C para el Arduino (se ha utilizado el IDE de Arduino que esta disponible en ):int pot1; //Lectura del potenciometro 1int pot2; //Potenciometro 2int cols = 8; //Rojosint fils = 8; //Negrosboolean espera = false; //Indica si se encuentra en modo de lecturachar bytesReceived[8*8+10]; //Cadena recibida del puertoint bytePos; //Contador de posicion para la cadenaint dpins[16] = {10, 3, 2, 7, A3, 8, 12, 13, //columnas6, 11, A5, 9, 5, A4, 4, A2}; //filasvoid setup(){ Serial.begin(9600); //Iniciando la comunicacion serial //Pin0 y Pin1, son deshabilitados en modo serial for (int i=0; i<cols+fils; i++) { pinMode(dpins, OUTPUT); //Empezamos por poner en estado bajo toda la matriz digitalWrite(dpins, LOW); } //Inicializar la cadena de bytes en '0','1' for (int j=0; j<8*8+8; j++) { if (j%2 == 0) bytesReceived[j] = '0'; else bytesReceived[j] = '1'; }}void loop(){ if(espera == false) { //Leer los valores del joystick pot1 = analogRead(0) / 4; //Eje X Rojo pot2 = analogRead(1) / 4; //Eje Y Negro //Mandar los datos al puerto serial Serial.flush(); Serial.print(pot1); Serial.print(','); Serial.println(pot2); //Activar bandera de espera espera = true; } if(Serial.available() >= 8*8+8) //Checar el puerto y recibir datos { //Leer cada byte y guardarlo en el arreglo bytePos = 0; while (Serial.available() > 0) { bytesReceived[bytePos++] = Serial.read(); } espera = false; }//Fin if //Encender la matriz de LEDs for(int i=0; i<cols; i++) //Bucle principal de columnas { //Apaga todas las columnas for(int c=0; c<cols; c++) digitalWrite(dpins[c], LOW); digitalWrite(dpins, HIGH); //Prende columna indicada for(int j=0; j<fils; j++) //Bucle para las filas { //Leemos el byte que nos interesa en la posicion j+(fils+1)*i if(bytesReceived[j+i+fils*i] == '0') digitalWrite(dpins[j+8], LOW); else digitalWrite(dpins[j+8], HIGH); //Prende fila indicada } delay(2); }//Fin for}El arreglo dpins[] guarda las direcciones de los pines que son utilizados para las salidas a la matriz de LEDs de tal forma que se ordenen las columnas y las filas y podamos activarlas (o desactivarlas) a través de un ciclo (for). Esto es importante para no activar fila a fila y columna a columna, además nos permite ordenar las columnas y las filas en su inicialización en código, sin tener que preocuparnos como conectar los cables (evitando así, tener cables enredados XD).Otro aspecto importante, es que cuando se imprimen las lecturas del Joystick se hace en forma de una única línea (usamos println solo en el último valor), los valores se separan con una "," (en el programa de C++ separamos los valores con la famosa función strtok) y antes de mandar la cadena nos cercioramos de que no haya basura en el puerto, llamando a la función flush.Leemos del puerto y cuando la cadena se ha recibido en su totalidad (por ello el ">= 8*8+8", 8*8 por las filas y columnas, +8 por cada coma que separa las filas) guardamos dicha cadena byte a byte en el arreglo bytesReceived. Finalmente activamos los LEDs de la matriz y para ello, primeramente apagamos todas las columnas (recuerda que si mandamos 0.0 V a una columna no podrá activarse ningún LED en dicha columna, ya que para que un LED se active en una coordenada determinada debemos mandar 5.0 V a la columna y 0.0 V a la fila), y encendemos una columna a la vez (por ello el ciclo externo con contador 'i'). Sin embargo para esta columna sólo activamos los LEDs que nos indique la cadena que recibimos de la computadora: dado que ya hemos mandado 5.0 V a la columna indicada, al mandar 0.0 V a una fila, el LED de dicha fila (y para esta columna) se activará, pero si mandamos 5.0 V a la fila el LED permanecerá desactivado.El delay al final del bucle principal es para dar un tiempo de espera que permita percibir al ojo humano las columnas activas en cada ciclo del bucle, si no hacemos esto parecerá que todos los LEDs están activados siempre.Programa en C++, bibliotecas Qt + bilioteca QExtSerialPort en QtCreatorEn el proyecto creado en QtCreator hay siete archivos básicamente: culebra.h, culebra.cpp, mainwindow.h, mainwindow.cpp, mainwindow.ui, main.cpp y Puertos.pro. Lo primero que se debe hacer es configurar el proyecto (archivo Puertos.pro) para que pueda cargar las bilbiotecas dinámicas de QextSerialPort. En Ubuntu hay que agregar:unix { #librerias de qext... LIBS += -L./qextserialport/build LIBS += -lqextserialportd #aniadir donde localizar los archivos de cabecera de qext... INCLUDEPATH += ./qextserialport #etiquetas para saber en que tipo de sistema estamos DEFINES = _TTY_POSIX_}El formulario de la ventana principal contiene únicamente 2 QLineEdits con sus respectivas etiquetas y un QPushButton con el texto "Abrir puerto". La ventana principal mostrará la posición de los ejes del Joystick y podrá iniciar la comunicación cuando se presione el botón "Abrir puerto". No quiero abordar mucho el código de culebra.cpp puesto que no es en lo que se centra este tutorial y, además, es fácil de entender, así que voy a tratar de explicar rapidamente el funcionamiento de culebra.cpp (se puede decir que es el juego en sí) y les dejo al final gran parte del código principal (mainwindow.cpp) con comentarios. El código de main.cpp sólo se encarga de crear una instancia de la aplicación y ejecutarla. Puedes para leer detenidamente el código, probar cómo funciona y modificarlo.La lógica del juego se encuentra en culebra.cpp. Este programa contiene varias funciones que permiten mover las posiciones de los "elementos" de Snake, pero para que podamos observarlo en la matriz de LEDs lo que hacemos es guardar las posiciones en un arreglo bidimensional que posteriormente será enviado a través del puerto USB al Arduino, que se encargará de "dibujar" ese arreglo en la matriz de LEDs. La primera función de nuestro interés puede ser la función getMatriz(), que lo que hace es convertir el arreglo bidimensional en una cadena de texto (interpretada como un puntero a char) con el formato adecuado para que pueda ser entendido por el Arduino. Si ves el código podrás notar que la cadena se invierte, esto es para que el código de Arduino no tenga problemas al interpretar un 0 como apagado y un 1 como encendido cuando activa columna a columna la matriz de LEDs. La función mover() es llamada cada cierto intervalo de tiempo. Se realizará una serie de acciones para el juego (Nibble o Snake en este caso). La lógica es la siguiente: Mover cada parte de la culebra (cada parte es, en realidad una posición de un arreglo llamado gusano) a la posición de la parte anterior a ella (excepto la primera parte o "cabeza" de la culebra). Determinar la dirección que se ha establecido (se define cuando movemos el Joystick en un sentido en particular) y mover la "cabeza" de la culebra a la posición correspondiente. Si la culebra sale por los bordes, la posicionamos del lado contrario. Guardamos las posiciones de la culebra en el arreglo bidimensional, colocando un 1 en las posiciones ocupadas por la culebra y un 0 en los lugares vacíos. En la posición del alimento también colocamos un 1. Con esto estamos indicando las partes que queremos ver activadas en la matriz de LEDs. La función pintar() se encarga de esto. Determinar si la cabeza de la culebra ha hecho contacto con el alimento, si es así se incrementa la longitud de la culebra y llama a la función alimentar() que determina una posición aleatoria para colocar nuevamente el alimento (debe validarse que el alimento no caiga en una posición ocupada por la culebra). Determinar si la cabeza de la culebra ha hecho contacto con alguna parte de su cuerpo, si es asi llamar a la función gameOver() que pone en pausa el juego, reinicia las variables para un nuevo juego y dibuja en el arreglo bidimensional una figura indicando un juego perdido.Aquí está el código principal para leer y mandar datos por el puerto USB (comunicación con el Arduino):Lo que hace el painEvent es dibujar líneas y rectángulos en la posición adecuada para señalar la posición actual del joystick. Si tienes dudas de cómo funciona esto puedes revisar la documentación de Qt en QtCreador, es muy fácil de utilizar, el único "inconveniente" es que está en inglés.La función loop() se ejecuta de manera repetida cada 200 milisegundos (se puede configurar para hacer más rápido o más lento el juego en el slot de botón). Esta función debe sincronizarse con el programa de Arduino para poder transmitir datos entre la computadora y el Arduino. El sistema funciona de la siguiente manera. En el Arduino leemos los valores del Joystick (son dos valores para los ejes x e y) y mandamos estos valores en forma de cadena a la computadora, el programa de Qt está preguntando cada 200 milisegundos si hay datos disponibles. Cuando el programa de Qt recibe los datos (leyéndolos del puerto USB) separa la cadena en dos tokens y convierte los valores en números enteros, guardándolos en las variables x e y. Con los datos de las variables x e y, se determina la dirección en que debe moverse la culebra, usamos unos condicionales (if) y cambiamos el valor de la variable dir del juego para indicar el sentido del movimiento (recordar que el juego se encarga de hacer las conversiones de posición necesarias). Opcionalmente mostramos los valores del Joystick en los cuadros de texto y dibujamos la posición con el paintEvent. Hasta aquí hemos LEíDO del puerto. Ahora obtenemos el valor de la matriz del juego como una cadena de caracteres que el Arduino puede entender y lo mandamos por el puerto USB. Si el juego está pausado a causa de un gameOver() se detiene la lectura y el sistema colapsa (para reiniciar el juego, basta con resetear el Arduino). El Arduino checa si hay datos disponibles en el puerto si es así, guarda los datos en un arreglo y enciende la matriz columna a columna de acuerdo a este arreglo. Esto se repite constantemente para ver un SNAKE en la matriz de LEDs.Finalmente les dejo un vídeo del Snake funcionando: