El proyecto
Vamos a realizar un contador de vueltas para un circuito de slot de 4 carriles.
Se tratará de un contador de vueltas digital construido partiendo de la base de una placa Arduino Mega 2560
Slot Counter 1.0 |
Al final del artículos podéis encontrar el archivo sketch de Arduino , las librerías modificadas y el fichero Fritzing.
Características
- Pantalla de test en la que se comprobará el funcionamiento del sistema.
- Posibilidad de elegir entre Modo Carrera y Modo Entrenamiento
- El número de vueltas de carrerá será configurable. Por defecto 20.
- Antes de la salida se mostrará la vuelta más rápida y la temperatura.
- Se realizará una cuenta atrás antes de la salida señalizándola con led rojos y verdes junto con el display de siete segmentos.
- Se tendrá en cuenta la salida adelantada (o nula) antes de tiempo.
- La pantalla mostrará el número de vueltas restantes (o realizadas) y el tiempo de la última vuelta de cada jugador.
- Cada vez que un jugador pase por línea de meta se indicará con un sonido y el encendido de un led azul. Además el display de 7 segmentos mostrará el número del último jugador que ha pasado por meta.
- Cuando algún jugador se encuentre en su última vuelta se encenderá un led amarillo.
- Si algún jugador hace un tiempo mejor que el récord de vuelta se almacenará en memoria
- El sistema es controlable desde los botones y desde un mando a distancia.
Seleccione número de vueltas |
Lista de Componentes
- 1 Placa Arduino Mega 2560 (Rev3)
- 2 Displays de 7 segmentos(para representar unidades, decenas y también letras)
- 4 Pulsadores
- 4 Pulsadores D3C Omrom
- 1 Piezo (para reproducir sonidos)
- 1 Pantalla LCD de 4x20 con interfaz de conexión
- 3 Leds rojos
- 3 Leds verdes
- 1 Led azul
- 1 Led amarillo
- 5 Resistencias de 220Ω
- 1 Sensor de Temperatura LM35
- 1 Receptor de infrarrojos
- 1 Mando a distancia
- 1 Cable USB de alimentación
- 1 Transformador de móvil
- 1 Montón de cables de colores
- 1 Placa de prueba
Esquema Cuentavueltas |
El diseño del programa
El programa implementa una máquina de estados en el que cada uno representa una información en el display. Cada estado se corresponde con una determinada función:
- init() --> Inicia los valores de los pines y los objetos que serán manejador por la aplicación
- test() --> Realiza una prueba de los diferentes elementos del sistema.
- menu() --> Menú principal de la aplicación
- seleccionNumVueltas() --> Pantalla para elegir el número de vueltas
- carrera() --> Muestra el número de vueltas y tiempo en el display.
- ganador()--> Calcula el ganador de una carrera (o ganadores en caso de empate)
Es posible moverse entre los estados test, menu, seleccionNumVueltas y carrera pulsando los botones [IZQUIERDA] y [DERECHA]
El estado al arrancar la placa es init y al terminar se pasa al estado menu. Para acceder al estado test se pulsa izquierda y para ir al estado seleccionNumVueltas se pulsa derecha.
Menú principal |
Notas sobre el código
El programa está pensado para funcionar en una placa Arduino Mega 2560 dado que hace uso de las siguientes peculiaridades de la placa:
- Tamaño de memoria RAM y de Memoria de Programa
- Utiliza 4 interrupciones externas
- Utiliza la interrupción interna 6 para el control del tono.
Si se desea utilizar en otras placas habrá que adaptar tanto el programa como las librerías ya que el tema del sonido es dependiente de los temporizadores.
- Se utiliza CONST siempre que sea posible. El compilador sustituye el texto de la variable por su valor númerico antes de compilar, ahorrando memoria RAM.
- Se utiliza la macro F() para el manejo de cadenas constantes. Esto hace que el programa busque las cadenas en la Memoria de Programa (Flash) con lo que se ahorra memoria RAM.
- Se utilizan los tipos de memoria más pequeños (p.ej: byte) de nuevo con el fin de ahorrar memoria RAM.
- NO SE UTILIZA la clase String para el manejo de cadenas dinámicas. La clase String de Arduino es una fuente de problemas de memoria (fragmentación y liberación). En su lugar se utilizan una matriz como cadena vacía, otra matriz representando una línea del display y las funciones de copia de C (strcpy... etc)
- Existen dos objetos que chocan por usar los mismos temporizadores de Arduino: Tone y RMTControl. El problema es que utilizan el TIMER 2. Se ha utilizado una librería Tone que no es la que viene por defecto en el entorno y se ha modificado para que no use el TIMER 2 si hay otros displonibles.
- Para evitar el rebote (bounce) en los pulsadores se ha introducido el control por tiempo en el momento de comprobar la pulsación de un botón. No se ha utilizado la librería debounce para no aumentar el tamaño del programa. La solución por hardware de usar contensadores y resistencias creando un filtro atenúa el problema pero no lo solucionan con lo cual no es una solución válida. La única solución hardware posible es incluir un biestable Flip-Flop RS. Es algo muy barato y que Arduino debería incorporar ya de seríe activándolo con algo así:
pinMode (14, flipflop) - Aunque se deshabiliten las funciones de interrupción las interrupciones se siguen quedando en cola y llamarán a las funciones de interrupción en cuanto se habiliten de nuevo. En vez de usar la solución de llamar a una función de interrupción vacía el programa accede a bajo nivel para borrar la cola de peticiones de interrupción.
- El control del paso por meta de los coches se usa a través de las 4 líneas de interrupción externas. Ésto es así porque no podemos perder el paso por meta bajo ningún concepto. Cuando un coche pasa por la línea de salida se debe aumentar su número de vueltas, poner a cero su temporizador, comprobar si ha ganado la carrera, encender un led azul y apagarlo, emitir un sonido, escribir su número en el Display de 7 segmentos y encender un led amarillo si es la última vuelta. Todo esto lleva tiempo de cálculo y durante el proceso puede suceder que otro coche pase por la línea de meta. Como las interrupciones se encolan el paso del otro coche también será procesado después de acabar el proceso del primero.
Notas sobre el hardware
Ruido electromágnetico
El principal problema y más importante fue debido al ruido electromagnético que provocan los motores de los coches. Las interferencias eran tales que se marcaban falsos pasos por linea de meta constantemente.
Se probó a colocar condensadores entre las patillas de los motores de los coches sin obtener mejora alguna.
Se colocaron condensadores y resistencias creando un filtro en las entradas de los pines utilizados para las interrupciones externas con poca o nula mejora.
Finalmente se utilizó un cable paralelo de impresora blindado. Tanto el blindaje como los cables no utilizados se unieron todos a tierra.
Consumo eléctrico
Debido a la cantidad de componentes no es posible, por ejemplo, encender todos los leds a la vez
aunque en la ejecución normal del programa nunca se da ese caso.
Código Fuente
/* Slot Counter v 1.0 Contador de vueltas de 4 carriles. Escrito para RetroPasado */ //Librerías: //#include#include //Manejo de IR (infrarrojos - mando a distancia) #include //Tone #include //Para poder manejar dispositivos I2C (el Display) #include //Librería para el Display #include //Para poder manejar dispositivos OneWire (el Sensor) #include //Librería para el Sensor #include //Para manejar la EEPROM #include /* HARDWARE - PINES Se asignan con define para ahorrar memoria y porque su asignación no es conflictiva. Para el resto de variables estáticas se utilizará const */ #define LEDROJO1 10 #define LEDROJO2 9 #define LEDROJO3 8 #define LEDAZUL 13 #define LEDSVERDES 12 #define LEDAMARILLO 46 #define BTN_IZQUIERDA 7 #define BTN_DERECHA 4 #define BTN_ARRIBA 6 #define BTN_ABAJO 5 #define INT_CARRIL0 2 #define INT_CARRIL1 3 #define INT_CARRIL2 19 #define INT_CARRIL3 18 #define ZUMBADOR 11 #define SENSOR_TEMPERATURA 31 #define IRPIN 48 // pin al que conectamos el receptor de IR #define IR_ARRIBA 3994140416 #define IR_ABAJO 3860446976 #define IR_IZQUIERDA 3944005376 #define IR_DERECHA 3910582016 #define IR_0 4060987136 #define IR_1 4278238976 #define IR_2 4261527296 #define IR_3 4244815616 #define IR_4 4211392256 #define IR_5 4194680576 #define IR_6 4177968896 #define IR_7 4144545536 #define IR_8 4127833856 #define IR_9 4111122176 #define IR_OK 3927293696 #define IR_ASTERISCO 4077698816 #define IR ALMOHADILLA 4044275456 #define DECENAS 1 #define UNIDADES 0 #define J 10 #define E 11 #define OFF 12 #define C 13 #define A 14 #define E 15 #define N 16 #define P 17 #define R 18 OneWire oneWire(SENSOR_TEMPERATURA); DallasTemperature sensors(&oneWire); LiquidCrystal_I2C lcd(0x27, 20, 4); NECIRrcv ir(IRPIN) ; //decode_results results; /* SOFTWARE */ unsigned long codigo_ir = 0; //Código leido por el sensor IR Tone tone1; const unsigned long TIEMPO_ENTRE_PULSACIONES_LARGO = 1000; //0.50s const unsigned long TIEMPO_ENTRE_PULSACIONES_CORTO = 50; //0.050s const long unsigned TIEMPO_ENTRE_PULSACIONES = TIEMPO_ENTRE_PULSACIONES_LARGO; //Dado que el procesador es de 8 bits se utilizan tipos //de 8 bits para ahorrar memoria y mejorar rendimiento //Definición de los estados (pantallas) posibles const byte ESTADO_PRUEBA_SISTEMA = 0; const byte ESTADO_SELECCION_NUM_VUELTAS = 1; const byte ESTADO_PREPARANDO_SALIDA = 2; const byte ESTADO_SALIDA_NULA = 3; const byte ESTADO_CARRERA = 4; const byte ESTADO_GANADOR = 5; const byte ESTADO_MENU = 6; //Estado actual en que se encuentra el sistema byte estadoActual = ESTADO_MENU; //Flag de salida nula volatile boolean salidaNula = false; //Flag indicando modo Carrera o modo Entrenamiento boolean modoCarrera = true; //Opción seleccionada del menú int posMenu = 0; //Número de vueltas por defecto const int NUM_VUELTAS_DEFECTO = 20; //Número de vueltas seleccionadas desde el menú int numVueltas = NUM_VUELTAS_DEFECTO; //Número de vueltas que lleva cada jugador volatile int numVueltasJugador[4] = { -1, -1, -1, -1}; //Tiempo que lleva en la vuelta actual de cada jugador volatile unsigned long tiempoVueltaJugador[4] = {0, 0, 0, 0}; //Instante en el que se inició la carrera unsigned long tiempoInicioCarrera = 0; //Tiempo de la vuelta más rápida (modo Carrera) const unsigned long HORA = 3600000; //Es necesario declarar como volatile las variables //que se modifican desde una función de interrupción volatile unsigned long mejorTiempoVuelta = HORA; //Tiempo de la vuelta más rápida (modo Entrenamiento) volatile unsigned long mejorTiempoVueltaEntrenamientoJugador[4] = {HORA, HORA, HORA, HORA}; //Tiempo que hace que se pulsó cada botón //para evitar dobles pulsaciones y falsas lecturas volatile unsigned long tiempoBtnIzquierda = 0; volatile unsigned long tiempoBtnDerecha = 0; volatile unsigned long tiempoBtnArriba = 0; volatile unsigned long tiempoBtnAbajo = 0; //Redefinición de caracteres para //números grandes en el LCD //Cada caracter es una matriz de 8x5 pixels byte arribaIzq[8] = { B00000, B00000, B00000, B00000, B00011, B01111, B01111, B11111 }; byte arribaCentro[8] = { B00000, B00000, B00000, B00000, B11111, B11111, B11111, B11111 }; byte arribaDer[8] = { B00000, B00000, B00000, B00000, B11000, B11110, B11110, B11111 }; byte abajoIzq[8] = { B11111, B01111, B01111, B00011, B00000, B00000, B00000, B00000 }; byte abajoCentro[8] = { B11111, B11111, B11111, B11111, B00000, B00000, B00000, B00000 }; byte abajoDer[8] = { B11111, B11110, B11110, B11000, B00000, B00000, B00000, B00000 }; byte uTilde[8] = { B00010, B00100, B01000, B00000, B10001, B10001, B10001, B01110 }; byte interrogacion[8] = { B00100, B00100, B00000, B00100, B01000, B10001, B10001, B01110 }; //Usando los caracteres anteriores //se definen los número del 0 al 9 //Cada uno ocupa 4x3 caracteres. byte conjuntoCaracteres[10][4][3] = { { // Número 0 {0, 1, 2}, {255, 16, 255}, {255, 16, 255}, {3, 4, 5} }, { // Número 1 {16, 16, 2}, {16, 16, 255}, {16, 16, 255}, {16, 16, 5} }, { // Número 2 {0, 1, 2}, {0, 1, 255}, {255, 16, 16}, {3, 4, 5} }, { // Número 3 {0, 1, 2}, {0, 1, 255}, {3, 4, 255}, {3, 4, 5} }, { // Número 4 {0, 16, 2}, {255, 16, 255}, {3, 4, 255}, {16, 16, 5} }, { // Número 5 {0, 1, 2}, {255, 1, 2}, {16, 16, 255}, {3, 4, 5} }, { // Número 6 {0, 1, 2}, {255, 1, 2}, {255, 16, 255}, {3, 4, 5} }, { // Número 7 {0, 1, 2}, {16, 1, 255}, {16, 4, 255}, {16, 16, 5} }, { // Número 8 {0, 1, 2}, {255, 1, 255}, {255, 4, 255}, {3, 4, 5} }, { // Número 9 {0, 1, 2}, {255, 16, 255}, {3, 4, 255}, {16, 16, 5} } }; //Para pintar en pantalla char strLineaBuff[21]; //Representa una línea del display (20 caracteres)
// + el caracter null de fin de línea \n const char strLimpia[1] = {""}; // 2 posiciones: cadena vacía
//+ caracter null de fin de línea \n // Se definen los segmentos que se encienden y se apagan // para cada uno de los números además de las letras J y E y otro // con todos los segmentos apagados // Cada número se compone de los segmentos: A B C D E F G // Arduino pin: 32,33,34,35,36,37,38 // 39,40,41,42,43,44,45 const byte conjuntoDigitos[19][7] = { { 0, 0, 0, 0, 0, 0, 1 }, // 0 { 1, 0, 0, 1, 1, 1, 1 }, // 1 { 0, 0, 1, 0, 0, 1, 0 }, // 2 { 0, 0, 0, 0, 1, 1, 0 }, // 3 { 1, 0, 0, 1, 1, 0, 0 }, // 4 { 0, 1, 0, 0, 1, 0, 0 }, // 5 { 0, 1, 0, 0, 0, 0, 0 }, // 6 { 0, 0, 0, 1, 1, 1, 1 }, // 7 { 0, 0, 0, 0, 0, 0, 0 }, // 8 { 0, 0, 0, 1, 1, 0, 0 }, // 9 { 1, 0, 0, 0, 0, 1, 1 }, // J --> 10 { 0, 1, 1, 0, 0, 0, 0 }, // E --> 11 { 1, 1, 1, 1, 1, 1, 1 }, // OFF --> 12 { 0, 1, 1, 0, 0, 0, 1 }, // C --> 13 { 0, 0, 0, 1, 0, 0, 0 }, // A --> 14 { 0, 1, 1, 0, 0, 0, 0 }, // E --> 15 { 1, 1, 0, 1, 0, 1, 0 }, // n --> 16 { 0, 0, 1, 1, 0, 0, 0 }, // P --> 17 { 1, 1, 1, 1, 0, 1, 0 } // r --> 18 }; /* FIN DE SOFTWARE - FIN DE DEFINICIÓN DE VARIABLES*/ /* Se ejecuta al encender Arduino Labores de comprobación: Muestra mensajes en el LCD Lee desde la EEPROM Encidence los leds Hace sonar el zumbador */ void setup() { //Serial.begin(9600); lcd.init (); //lcd.begin(20,4); lcd.backlight(); //La macro F() permite que una cadena se guarde en la memoria Flash (PROGMEM) //y no consuma RAM. Sólo determinados métodos pueden leer el tipo de la cadena devuelta. lcd.print(F(" Slot Counter v1.0")); lcd.setCursor ( 0, 2 ); lcd.print (F("Circuito 4 carriles")); sensors.begin(); ir.begin(); tone1.begin(ZUMBADOR); //irrecv.enableIRIn(); // Start the receiver //lcd.setCursor ( 0, 3 ); //lcd.print ("(C) 2014 David Diaz"); //Los pins de entrada se configuran como PULL UP (resistencia con +5v) ya que //si se configuran como PULL DOWN (resistencia con tierra) al pasar los coches //cerca de los interruptores se producen falsan lecturas debido a las interferencias //eléctricas que provocan. //Iniciación de los pins que manejan las interrupciones //Iniciación de los pins de entrada pinMode(INT_CARRIL0, INPUT_PULLUP);//2 pinMode(INT_CARRIL1, INPUT_PULLUP);//3 pinMode(INT_CARRIL2, INPUT_PULLUP);//19 pinMode(INT_CARRIL3, INPUT_PULLUP);//18 //Iniciación de los pins de entrada pinMode(BTN_IZQUIERDA, c); pinMode(BTN_DERECHA, INPUT_PULLUP); pinMode(BTN_ARRIBA, INPUT_PULLUP); pinMode(BTN_ABAJO, INPUT_PULLUP); //Iniciación de los pins de salida pinMode(LEDROJO1, OUTPUT); pinMode(LEDROJO2, OUTPUT); pinMode(LEDROJO3, OUTPUT); pinMode(LEDAZUL , OUTPUT); pinMode(LEDSVERDES, OUTPUT); pinMode(LEDAMARILLO, OUTPUT); pinMode(ZUMBADOR, OUTPUT); //Display de 7 segmentos 2 Digitos pinMode(32, OUTPUT); pinMode(33, OUTPUT); pinMode(34, OUTPUT); pinMode(35, OUTPUT); pinMode(36, OUTPUT); pinMode(37, OUTPUT); pinMode(38, OUTPUT); pinMode(39, OUTPUT); pinMode(40, OUTPUT); pinMode(41, OUTPUT); pinMode(42, OUTPUT); pinMode(43, OUTPUT); pinMode(44, OUTPUT); pinMode(45, OUTPUT); //Se introducen en la memoria del display LCD //los nuevos caracteres para números grandes lcd.createChar(0, arribaIzq); lcd.createChar(1, arribaCentro); lcd.createChar(2, arribaDer); lcd.createChar(3, abajoIzq); lcd.createChar(4, abajoCentro); lcd.createChar(5, abajoDer); lcd.createChar(6, uTilde); lcd.createChar(7, interrogacion); delay(5000); } /* Bucle de ejecución de Arduino alterna entre los diferentes estados (pantallas) de la aplicación */ void loop() { switch (estadoActual) { case ESTADO_PRUEBA_SISTEMA: estadoPruebaSistema(); break; case ESTADO_MENU: estadoMenu(); break; case ESTADO_SELECCION_NUM_VUELTAS : estadoSeleccionNumVueltas(); break; case ESTADO_PREPARANDO_SALIDA: estadoPreparandoSalida(); break; case ESTADO_CARRERA: estadoCarrera(); break; case ESTADO_GANADOR: estadoGanador(); break; } } void estadoPruebaSistema() { lcd.clear(); lcd.print(F(" PRUEBA DE SISTEMA")); lcd.setCursor ( 0, 3 ); lcd.print(F("Temp: ")); sensors.requestTemperatures(); lcd.print((char)223); lcd.print(sensors.getTempCByIndex(0)); delay (3000); lcd.setCursor ( 0, 3 ); lcd.print(F("Caracteres: ")); lcd.write((uint8_t)0); lcd.write(1); lcd.write(2); lcd.write(3); lcd.write(4); lcd.write(5); lcd.write(6); lcd.write(7); delay (3000); lcd.setCursor ( 0, 3 ); lcd.print (F("Encendido Leds ")); digitalWrite(LEDSVERDES, HIGH); delay (2000); digitalWrite(LEDROJO1, HIGH); delay (2000); digitalWrite(LEDROJO2, HIGH); delay (2000); digitalWrite(LEDROJO3, HIGH); delay (2000); //digitalWrite(LEDSVERDES, HIGH); //delay (2000); digitalWrite(LEDAZUL, HIGH); delay (2000); digitalWrite(LEDAMARILLO, HIGH); delay (2000); lcd.setCursor ( 0, 3 ); lcd.print (F("Sonido Zumbador ")); tone1.play(NOTE_B3, 100); tone1.play(NOTE_G4, 200); delay (3000); //Si se pulsa el botón izquierda se borra la EEPROM if (digitalRead(BTN_IZQUIERDA) == LOW) { EEPROM.write(0, 0); lcd.print (F(" Record Borrado ")); delay (3000); mejorTiempoVuelta = EEPROM.read(0); strFormatoTiempo(mejorTiempoVuelta); delay (3000); } lcd.print (F("Lectura EEPROM ")); lcd.print (EEPROM.read(0)); lcd.setCursor ( 0, 3 ); lcd.print (F("Display 7 seg ")); for (byte contador = 0; contador < 100; contador++) { delay(100); escribirSieteSegmentos(contador / 10, DECENAS); escribirSieteSegmentos(contador % 10, UNIDADES); } delay(4000); estadoActual = ESTADO_MENU; } /* Muestra en pantalla el menú del sistema. Interfaz: Botón arriba -> Subir opción menú Botón abajo -> Bajar opción menú Botón derecha-> Aceptar */ void estadoMenu() { //Reinicio de variables numVueltasJugador[0] = -1; numVueltasJugador[1] = -1; numVueltasJugador[2] = -1; numVueltasJugador[3] = -1; //Tiempo que lleva en la vuelta actual de cada jugador tiempoVueltaJugador[0] = 0; tiempoVueltaJugador[1] = 0; tiempoVueltaJugador[2] = 0; tiempoVueltaJugador[3] = 0; //Mejores tiempos de vuelta para test mejorTiempoVueltaEntrenamientoJugador[0] = HORA; mejorTiempoVueltaEntrenamientoJugador[1] = HORA; mejorTiempoVueltaEntrenamientoJugador[2] = HORA; mejorTiempoVueltaEntrenamientoJugador[3] = HORA; //Apagado de LEDS y 7 Segmentos escribirSieteSegmentos(OFF, DECENAS); escribirSieteSegmentos(OFF, UNIDADES); digitalWrite(LEDROJO1, LOW); digitalWrite(LEDROJO2, LOW); digitalWrite(LEDROJO3, LOW); digitalWrite(LEDAZUL, LOW); digitalWrite(LEDSVERDES, LOW); digitalWrite(LEDAMARILLO, LOW); lcd.clear(); lcd.setCursor ( 3, 0 ); lcd.print(F(" CARRERA")); lcd.setCursor ( 3, 1 ); lcd.print(F("ENTRENAMIENTO")); lcd.setCursor ( 3, 2 ); lcd.print(F(" PRUEBA")); lcd.setCursor ( 0, posMenu ); lcd.print(F("*")); lcd.setCursor ( 19, posMenu ); lcd.print(F("*")); escribirSieteSegmentosPosMenu(); while (true) { leerIr(); if ( (tiempo(tiempoBtnAbajo) && digitalRead(BTN_ABAJO) == LOW)
|| codigo_ir == IR_ABAJO){ //Serial.println(F("ABAJO")); //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_LARGO; tiempoBtnAbajo = millis(); lcd.setCursor ( 0, posMenu ); lcd.print(F(" ")); lcd.setCursor ( 19, posMenu ); lcd.print(F(" ")); posMenu++; if (posMenu == 3) { posMenu = 0; } lcd.setCursor ( 0, posMenu ); lcd.print(F("*")); lcd.setCursor ( 19, posMenu ); lcd.print(F("*")); escribirSieteSegmentosPosMenu(); tone1.play(NOTE_G3, 100); } if ( (tiempo(tiempoBtnArriba) && digitalRead(BTN_ARRIBA) == LOW )
|| codigo_ir == IR_ARRIBA ){ //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_LARGO; //Serial.println(F("ARRIBA")); tiempoBtnArriba = millis(); lcd.setCursor ( 0, posMenu ); lcd.print(F(" ")); lcd.setCursor ( 19, posMenu ); lcd.print(F(" ")); escribirSieteSegmentosPosMenu(); posMenu--; if (posMenu == -1) { posMenu = 2; } lcd.setCursor ( 0, posMenu ); lcd.print(F("*")); lcd.setCursor ( 19, posMenu ); lcd.print(F("*")); escribirSieteSegmentosPosMenu(); tone1.play(NOTE_G3, 100); } if ( (tiempo(tiempoBtnDerecha) && digitalRead(BTN_DERECHA) == LOW)
|| codigo_ir == IR_DERECHA || codigo_ir == IR_OK ) { // Serial.println(F("DERECHA")); tiempoBtnDerecha = millis(); switch (posMenu) { case 0: modoCarrera = true; numVueltas = NUM_VUELTAS_DEFECTO; estadoActual = ESTADO_SELECCION_NUM_VUELTAS; break; case 1: modoCarrera = false; numVueltas = 99; estadoActual = ESTADO_PREPARANDO_SALIDA; break; case 2: estadoActual = ESTADO_PRUEBA_SISTEMA; break; } tone1.play(NOTE_G3, 100); //Scroll de la pantalla hacia la izquierda //lcd.autoscroll(); // for (int positionCounter = 0; positionCounter < 20; positionCounter++) { //lcd.scrollDisplayLeft(); // lcd.print(F(" ")); // delay(100); //} //lcd.noAutoscroll(); break; } //Sólo para controlar el rebote y pulsación rápida de los botones if (tiempoCorto(tiempoBtnAbajo) && digitalRead(BTN_ABAJO) == HIGH) { tiempoBtnAbajo = 0; //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_CORTO; } if (tiempoCorto(tiempoBtnArriba) && digitalRead(BTN_ARRIBA) == HIGH) { tiempoBtnArriba = 0; //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_CORTO; } } //Fin de bucle } /* Muestra en pantalla el número de vueltas de la carrera. Interfaz: Botón arriba -> Aumentar número de vueltas Botón abajo -> Disminuir número de vueltas Botón derecha-> Aceptar */ void estadoSeleccionNumVueltas() { escribirSieteSegmentos(OFF, DECENAS); escribirSieteSegmentos(OFF, UNIDADES); digitalWrite(LEDROJO1, LOW); digitalWrite(LEDROJO2, LOW); digitalWrite(LEDROJO3, LOW); digitalWrite(LEDAZUL, LOW); digitalWrite(LEDSVERDES, LOW); lcd.clear(); lcd.print(F("Seleccione")); lcd.setCursor ( 0, 1 ); lcd.print(F("N")); lcd.write(6); lcd.print(F("mero")); lcd.setCursor ( 0, 2 ); lcd.print(F("De")); lcd.setCursor ( 0, 3 ); lcd.print(F("Vueltas")); pintarNumero(numVueltas); int numeroPulsado = -1; while (true) { leerIr(); //Si se ha pulsado un número del mando a distancia: switch (codigo_ir){ case IR_0:numeroPulsado=0;break; case IR_1:numeroPulsado=1;break; case IR_2:numeroPulsado=2;break; case IR_3:numeroPulsado=3;break; case IR_4:numeroPulsado=4;break; case IR_5:numeroPulsado=5;break; case IR_6:numeroPulsado=6;break; case IR_7:numeroPulsado=7;break; case IR_8:numeroPulsado=8;break; case IR_9:numeroPulsado=9;break; default:numeroPulsado=-1;break; } if (numeroPulsado!=-1){ //Serial.println(F("PULSADO NUMERO")); //Desplazamos el número de la derecha una posición a la izquierda y el //número pulsado en el mando a distancia se queda en la derecha. numVueltas = (numVueltas % 10) * 10 + numeroPulsado; pintarNumero(numVueltas); } if ( (tiempo(tiempoBtnAbajo) && digitalRead(BTN_ABAJO) == LOW)
|| codigo_ir==IR_ABAJO ) { //Serial.println(F("ABAJOOOOOOOOOOOOO")); //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_LARGO; tiempoBtnAbajo = millis(); numVueltas--; if (numVueltas == 0) { numVueltas = 99; } pintarNumero(numVueltas); } if ( (tiempo(tiempoBtnArriba) && digitalRead(BTN_ARRIBA) == LOW)
|| codigo_ir==IR_ARRIBA ) { //Serial.println(F("ARRIBAAAAAAAAAAAAA")); //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_LARGO; tiempoBtnArriba = millis(); numVueltas++; if (numVueltas == 100) { numVueltas = 1; } pintarNumero(numVueltas); } if ( (tiempo(tiempoBtnDerecha) && digitalRead(BTN_DERECHA) == LOW)
|| codigo_ir==IR_DERECHA || codigo_ir == IR_OK) { //Serial.println(F("DERECHAAAAAAAAAAA")); tiempoBtnDerecha = millis(); estadoActual = ESTADO_PREPARANDO_SALIDA; tone1.play(NOTE_G3, 100); //Scroll de la pantalla hacia la izquierda //for (int positionCounter = 0; positionCounter < 20; positionCounter++) { // lcd.scrollDisplayLeft(); // delay(150); // } break; } //Si se pulsa el botón izquierda se sale if ( (tiempo(tiempoBtnIzquierda) && digitalRead(BTN_IZQUIERDA) == LOW)
|| codigo_ir==IR_IZQUIERDA || codigo_ir == IR_ASTERISCO ) { //Serial.println(F("IZQUIERDAAAAAAAAAAAAAA")); tiempoBtnIzquierda = millis(); estadoActual = ESTADO_MENU; tone1.play(NOTE_G3, 100); //Scroll de la pantalla hacia la derecha //for (int positionCounter = 0; positionCounter < 20; positionCounter++) { // lcd.scrollDisplayRight(); // delay(150); //} break; } //Sólo para botones if (tiempoCorto(tiempoBtnAbajo) && digitalRead(BTN_ABAJO) == HIGH) { tiempoBtnAbajo = 0; //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_CORTO; } if (tiempoCorto(tiempoBtnArriba) && digitalRead(BTN_ARRIBA) == HIGH) { tiempoBtnArriba = 0; //TIEMPO_ENTRE_PULSACIONES=TIEMPO_ENTRE_PULSACIONES_CORTO; } } //Fin de bucle } /* Muestra en pantalla la temperatura y el menor tiempo de vuelta. Se ejcuta una cuenta atrás de semáforo encendiento los leds rojos uno por uno y finalmente los tres verdes para indicar el comienzo de la carrera. Si un jugador sale antes de estar los leds verdes encendidos entonces se aborta la carrera (Salida Nula) */ void estadoPreparandoSalida() { //Serial.println("ANTES a"); lcd.clear(); //Serial.println("ANTES b"); salidaNula = false; //Serial.println("ANTES c"); //interrupts(); //Se borra el buffer de interrupciones (ya que las interrupciones //siempre se encolan aunque no haya función que las escuche) //Así se evita que si existen interrupciones pendientes (por ejemplo cuando //un coche gana una carrera y los otros coches pasan se generan interrupciones //que aunque ya no se tienen en cuenta para contar el número de vueltas
//se quedan encoladas). //Entonces al llegar a este punto las interrupciones encoladas saltaban
//y llamaban a fnSalidaNula //cuando no se había producido ninguna interrupción en este momento. //Es necesario por tanto borrar la bandera que indica que se
//ha producido una interrupción EIFR = 0xFF; attachInterrupt(0, fnSalidaNula, FALLING ); attachInterrupt(1, fnSalidaNula, FALLING ); attachInterrupt(4, fnSalidaNula, FALLING ); attachInterrupt(5, fnSalidaNula, FALLING ); //Serial.println("ANTES d"); sensors.requestTemperatures(); // Serial.println("ANTES f"); lcd.print(F("TEMPERATURA:")); lcd.setCursor(13, 0); lcd.print(sensors.getTempCByIndex(0)); lcd.setCursor(23, 0); lcd.print((char)223); mejorTiempoVuelta = EEPROM.read(0); lcd.setCursor ( 0, 1 ); lcd.print(F("MEJOR TIEMPO: ")); limpiarBufferLinea(); strFormatoTiempo(mejorTiempoVuelta); lcd.setCursor ( 0, 2 ); lcd.print(strLineaBuff); if (modoCarrera) { escribirSieteSegmentos(C, DECENAS); } else { escribirSieteSegmentos(E, DECENAS); } while (!salidaNula) { delay(3000); if (!salidaNula) { escribirSieteSegmentos(3, UNIDADES); lcd.setCursor ( 0, 3 ); lcd.print(F(" ! EN SUS PUESTOS ! ")); digitalWrite(LEDROJO1, HIGH); tone1.play(NOTE_B3, 100); delay(1000); } if (!salidaNula) { escribirSieteSegmentos(2, UNIDADES); lcd.setCursor ( 0, 3 ); lcd.print(F(" !! PREPARADOS !! ")); digitalWrite(LEDROJO2, HIGH); tone1.play(NOTE_B3, 100); delay(1000); } if (!salidaNula) { escribirSieteSegmentos(1, UNIDADES); lcd.setCursor ( 0, 3); lcd.print(F(" !! ATENTOS !! ")); digitalWrite(LEDROJO3, HIGH); tone1.play(NOTE_B3, 100); delay(1000); } if (!salidaNula) { escribirSieteSegmentos(0, UNIDADES); lcd.setCursor ( 0 , 3); lcd.print(F("COMIENZO DE CARRERA ")); digitalWrite(LEDROJO1, LOW); digitalWrite(LEDROJO2, LOW); digitalWrite(LEDROJO3, LOW); digitalWrite(LEDSVERDES, HIGH); tone1.play(NOTE_B3, 1000); attachInterrupt(0, aumentarVueltaJ0, FALLING ); attachInterrupt(1, aumentarVueltaJ1, FALLING ); attachInterrupt(4, aumentarVueltaJ2, FALLING ); attachInterrupt(5, aumentarVueltaJ3, FALLING ); tiempoInicioCarrera = millis(); tiempoVueltaJugador[0] = tiempoInicioCarrera; tiempoVueltaJugador[1] = tiempoInicioCarrera; tiempoVueltaJugador[2] = tiempoInicioCarrera; tiempoVueltaJugador[3] = tiempoInicioCarrera; estadoActual = ESTADO_CARRERA; break; } } //Fin de while if (salidaNula) { lcd.clear(); lcd.print(F(" !! SALIDA NULA !!")); digitalWrite(LEDAZUL, HIGH); tone1.play(NOTE_B3, 1000); delay(3000); digitalWrite(LEDAZUL, LOW); digitalWrite(LEDROJO1, LOW); digitalWrite(LEDROJO2, LOW); digitalWrite(LEDROJO3, LOW); } } /* Muestra el texto salida nula y hace sonar el zumbador */ void fnSalidaNula() { salidaNula = true; } /* Muestra en pantalla el número de vueltas de cada jugador y el tiempo transcurrido de la vuelta actual. Interfaz: Botón Izquierda --> Salir (regresa a la pantalla de selección de vueltas) */ void estadoCarrera() { lcd.clear(); while (true) { leerIr(); //Si se pulsa el botón izquierda se sale if ( (tiempo(tiempoBtnIzquierda) && digitalRead(BTN_IZQUIERDA) == LOW)
|| codigo_ir==IR_IZQUIERDA || codigo_ir == IR_ASTERISCO ) { tiempoBtnIzquierda = millis(); estadoActual = ESTADO_MENU; tone1.play(NOTE_B3, 100); break; } //Si algún coche ya tiene el número de vueltas se sale if (numVueltasJugador[0] >= numVueltas || numVueltasJugador[1] >= numVueltas || numVueltasJugador[2] >= numVueltas || numVueltasJugador[3] >= numVueltas ) { estadoActual = ESTADO_GANADOR; break; } //Pintar el tiempo que lleva en esta vuelta cada jugador lcd.setCursor ( 0, 0); limpiarBufferLinea(); strObtenerNumVueltas(0); lcd.print(strLineaBuff); lcd.setCursor ( 0, 1 ); limpiarBufferLinea(); strObtenerNumVueltas(1); lcd.print(strLineaBuff); lcd.setCursor ( 0, 2 ); limpiarBufferLinea(); strObtenerNumVueltas(2); lcd.print(strLineaBuff); lcd.setCursor ( 0, 3 ); limpiarBufferLinea(); strObtenerNumVueltas(3); lcd.print(strLineaBuff); } } /* Muestra en el lcd al ganador/es de la carrera Interfaz: Botón Derecha --> Salir (regresa a la pantalla de selección de vueltas) */ void estadoGanador() { digitalWrite(LEDSVERDES, LOW); digitalWrite(LEDAZUL, HIGH); digitalWrite(LEDAMARILLO, LOW); tone1.play(NOTE_B3, 100); tone1.play(NOTE_DS4, 100); tone1.play(NOTE_G3, 100); //Si se deshabilitan todas las interrupciones //el display deja de funcionar //noInterrupts(); //Se deshabilitan las interrupciones que se utilizan detachInterrupt(0); detachInterrupt(1); detachInterrupt(4); detachInterrupt(5); //Tiempo en completar la carrera tiempoInicioCarrera = millis(); //Calcular orden de fin de carrera byte idJugadores[] = {0, 1, 2, 3}; //Pruebas // numVueltasJugador[0] = 4; //numVueltasJugador[3] = 4; //Fin de Pruebas //Serial.println("ANTES"); //Serial.println(numVueltasJugador[0]); // Serial.println(numVueltasJugador[1]); //Serial.println(numVueltasJugador[2]); // Serial.println(numVueltasJugador[3]); // Serial.println(idJugadores[0]); // Serial.println(idJugadores[1]); // Serial.println(idJugadores[2]); // Serial.println(idJugadores[3]); // Serial.println("FIN DE ANTES"); bubbleSort(numVueltasJugador, idJugadores); //Serial.println("ORDENADO"); //Serial.println(numVueltasJugador[0]); //Serial.println(numVueltasJugador[1]); //Serial.println(numVueltasJugador[2]); //Serial.println(numVueltasJugador[3]); //Serial.println(idJugadores[0]); //Serial.println(idJugadores[1]); //Serial.println(idJugadores[2]); //Serial.println(idJugadores[3]); //Serial.println("FIN DE ORDENADO"); //Presentar los resultados lcd.clear(); limpiarBufferLinea(); strObtenerGanador(0, idJugadores); lcd.print(strLineaBuff); lcd.setCursor ( 0, 1 ); limpiarBufferLinea(); strObtenerGanador(1, idJugadores); lcd.print(strLineaBuff); lcd.setCursor ( 0, 2 ); limpiarBufferLinea(); strObtenerGanador(2, idJugadores); lcd.print(strLineaBuff); lcd.setCursor ( 0, 3 ); limpiarBufferLinea(); strObtenerGanador(3, idJugadores); lcd.print(strLineaBuff); while (true) { leerIr(); if ( (tiempo(tiempoBtnDerecha) && digitalRead(BTN_DERECHA) == LOW)
|| codigo_ir==IR_DERECHA || codigo_ir == IR_OK ) { tiempoBtnDerecha = millis(); estadoActual = ESTADO_MENU; tone1.play(NOTE_B3, 100); break; } } } /* Devuelve una cadena con el formato ganador de un jugador */ void strObtenerGanador(byte numJugador, byte idJugadores[]) { char strAux[1]; //2 posiciones. El id del jugador + caracter null de fin de línea \n //Ya hemos ordenado los resultados, en la posición 0 //se encuentra el ganador if (numVueltasJugador[0] == numVueltasJugador[numJugador]) { strcat(strLineaBuff, "GANADOR: JUGADOR: "); strcat(strLineaBuff, itoa(idJugadores[numJugador] + 1, strAux, 10)); strcat(strLineaBuff, "!"); } } /* Devuelve una cadena con el formato del número de vueltas de un jugador */ void strObtenerNumVueltas(byte numJugador) { char strAux[2]; //3 caracteres- 2 números + caracter null de fin de cadena \0 if (numVueltasJugador[numJugador] == -1) { //strcat(strLineaBuff,"J"); strcat(strLineaBuff, itoa(numJugador + 1, strAux, 10)); strcat(strLineaBuff, ": NO HA SALIDO"); } else { unsigned long tiempoActual = millis(); strcat(strLineaBuff, itoa(numJugador + 1, strAux, 10)); strcat(strLineaBuff, ": "); //MODO CARRERA: Se imprime el número de vueltas que quedan para terminar la carrera //MODO ENTRENAMIENTO: Se imprime el número de vueltas que se llevan recorridas if (modoCarrera) { int numVueltasRestantes = numVueltas - numVueltasJugador[numJugador]; if (numVueltasRestantes < 10) { strcat(strLineaBuff, "0"); } strcat(strLineaBuff, itoa(numVueltasRestantes, strAux, 10)); strcat(strLineaBuff, " - "); strFormatoTiempo(tiempoActual - tiempoVueltaJugador[numJugador]); } else { if (numVueltasJugador[numJugador] < 10) { strcat(strLineaBuff, "0"); } strcat(strLineaBuff, itoa(numVueltasJugador[numJugador], strAux, 10)); strcat(strLineaBuff, " = "); strFormatoTiempo(mejorTiempoVueltaEntrenamientoJugador[numJugador]); } //str = str + numJugador + ": " + numVueltasJugador[numJugador]
+ " - " + strFormatoTiempo(tiempoActual - tiempoVueltaJugador[numJugador]); } } /* Muestra en el LCD un número de dos cifras en tamaño grande (ocupando cuatro filas del display) */ void pintarNumero(byte numero) { tone1.play(NOTE_B3, 100); for (byte contadorCifras = 0; contadorCifras < 2; contadorCifras++) { //Ej: numero = 16 byte cifra; if (contadorCifras == 0) { cifra = (numero / 10); // 16/10 = 1 } else { cifra = numero % 10; // 16/10 = 1 y sobran 6. Cojo el resto 6. } //Los números se pintan en la columna 15 del display byte columnaInicial = 11 + (contadorCifras * 4); for (byte contadorFilas = 0; contadorFilas < 4; contadorFilas++) { for (byte contadorColumnas = 0; contadorColumnas < 3; contadorColumnas++) { lcd.setCursor(columnaInicial + contadorColumnas, contadorFilas); lcd.write((uint8_t)conjuntoCaracteres[cifra][contadorFilas][contadorColumnas]); } //Fin de for columnas } //Fin de for filas }//Fin de for cifras } void aumentarVuelta(byte numJugador) { digitalWrite(LEDAZUL, HIGH); escribirSieteSegmentos(numJugador + 1, UNIDADES); if (tiempo(tiempoVueltaJugador[numJugador])) { unsigned long time = millis(); unsigned long tiempoVuelta = time - tiempoVueltaJugador[numJugador]; if (modoCarrera) { almacenarSiEsRecorVuelta(tiempoVuelta); } else { if (tiempoVuelta < mejorTiempoVueltaEntrenamientoJugador[numJugador]) { mejorTiempoVueltaEntrenamientoJugador[numJugador] = tiempoVuelta; } } tiempoVueltaJugador[numJugador] = time; numVueltasJugador[numJugador]++; tone1.play(NOTE_G3 + (numJugador * 50), 100); } //Indicador de última vuelta (led Amarillo) if ((numVueltasJugador[numJugador] + 1) >= numVueltas) { digitalWrite(LEDAMARILLO, HIGH); } digitalWrite(LEDAZUL, LOW); } //Funciones de Interrupción /* Aumenta la cantidad de vueltas del jugador 1 y graba en la EEPROM si es un record de vuelta */ void aumentarVueltaJ0() { aumentarVuelta(0); } /* Aumenta la cantidad de vueltas del jugador 2 y graba en la EEPROM si es un record de vuelta */ void aumentarVueltaJ1() { aumentarVuelta(1); } /* Aumenta la cantidad de vueltas del jugador 3 y graba en la EEPROM si es un record de vuelta */ void aumentarVueltaJ2() { aumentarVuelta(2); } /* Aumenta la cantidad de vueltas del jugador 4 y graba en la EEPROM si es un record de vuelta */ void aumentarVueltaJ3() { aumentarVuelta(3); } //Fin funciones de interrupción //Graba en la EEPROM el record de vuelta void almacenarSiEsRecorVuelta(unsigned long tiempoVuelta) { if (tiempoVuelta < mejorTiempoVuelta) { tone1.play(NOTE_B2, 100); mejorTiempoVuelta = tiempoVuelta; EEPROM.write(0, tiempoVuelta); } } //Ordenación por burbuja inverso (de mayor a menor) void bubbleSort(volatile int numVueltasJugador[4], byte idJugadores[4]) { int temp; byte tempidjugador; for (int i = 0; i < 4; i++) { for (int y = 1; y < 4 - i; y++) { if (numVueltasJugador[y - 1] < numVueltasJugador[y]) { temp = numVueltasJugador[y - 1]; numVueltasJugador[y - 1] = numVueltasJugador[y]; numVueltasJugador[y] = temp; tempidjugador = idJugadores[y - 1]; idJugadores[y - 1] = idJugadores[y]; idJugadores[y] = tempidjugador; } } } } //Comprueba si ha pasado ya el tiempo mínimo //desde que se pulso un botón para evitar //dobles pulsaciones y falsas lecturas bool tiempo(unsigned long tiempo) { return (millis() - tiempo) > TIEMPO_ENTRE_PULSACIONES; } bool tiempoCorto(unsigned long tiempo) { return (millis() - tiempo) > TIEMPO_ENTRE_PULSACIONES_CORTO; } //Formatea los milisegundos en 00:00.0 void strFormatoTiempo(unsigned long milisegundos) { char strAux[2]; //3 caracteres. El número como máximo tendrá 2 caracteres
//+ el caracter nulo \0 long restohoras = milisegundos % 3600000; long minutos = restohoras / 60000; long restominutos = restohoras % 60000; long segundos = restominutos / 1000; long restosegundos = restominutos % 10; if (minutos < 10) { strcat(strLineaBuff, "0"); } strcat(strLineaBuff, itoa(minutos, strAux, 10)); strcat(strLineaBuff, ":"); if (segundos < 10) { strcat(strLineaBuff, "0"); } strcat(strLineaBuff, itoa(segundos, strAux, 10)); strcat(strLineaBuff, "."); strcat(strLineaBuff, itoa(restosegundos, strAux, 10)); } /* Limpia el buffer de una linea del display */ void limpiarBufferLinea() { strcpy(strLineaBuff, strLimpia); } //Escribe un número 0-9 en el Display 7 segmentos //posición es 0 ó 1 void escribirSieteSegmentos(byte digito, byte posicion) { byte pin = 32 + (7 * posicion); for (byte contador = 0; contador < 7; contador++) { //Recorremos cada segmento digitalWrite(pin + contador, conjuntoDigitos[digito][contador]); } } /* Lee el código IR */ void leerIr(){ if (ir.available()) { codigo_ir = ir.read(); //irrecv.resume(); //Serial.println(codigo_ir); //Serial.println(getFreeMemory()); }else{ codigo_ir = 0; } } /* Escribe en el display de 7 segmentos una abreviación de la opción de menú seleccionada */ void escribirSieteSegmentosPosMenu(){ switch (posMenu){ case 0: escribirSieteSegmentos(C, DECENAS); escribirSieteSegmentos(A, UNIDADES); break; case 1: escribirSieteSegmentos(E, DECENAS); escribirSieteSegmentos(N, UNIDADES); break; case 2: escribirSieteSegmentos(P, DECENAS); escribirSieteSegmentos(R, UNIDADES); break; } }
Receptor de infrarrojos |
Buenas noches. Me parece un curro tremendo el que te has pegado para hacer el programa y modificar las librerías.
ResponderEliminarTe he encontrado googleando como hacer un cuentavueltas con Arduino. Te escribo porque no sé si me podrías ayudar con el programa para el arduino MEGA.
Me he bajado los ficheros del Sketch y de las librerías pero, al intentar incorporarlas desde el IDEde Arduino, (el 1.8.5), me da un error:
Un subdirectorio de su 'cuaderno de bocetos' no es una librería válida
No sé que es lo que puede pasar pero me fastidia mucho pq tu proyecto está genial.
Las librerías las intento incorporar con el Gestionador de librerías que lleva el IDE incorporado.
Si me puedes dar una indicación, te estaría muy agradecido.
Saludos.
Hola.
ResponderEliminarSiento no haber podido podido contestarte antes.
¿Pudiste solucionarlo?
Cuando terminé el proyecto del cuentavueltas dejé abandonado por falta de tiempo el tema de arduino y la verdad es que ahora mismo no recuerdo los problemas que podía dar el entorno (ya no siquiera lo tengo instalado)
Siempre puedes coger un proyecto súper sencillo de prueba, ver que funciona y luego añadirle sólo una librería y ver si continua funcionando. Poquito a poco para detectar donde está el fallo.
hola, tengo un par de preguntas, que yo para esto soy un poco negado, te dejo mi correo fantasy.adicted@gmail.com
ResponderEliminar