Mini Futbolín Auto-Arbitrado

Autores:
Sergio Espinosa Robles
Joel Domené Álvaro
Idea del proyecto:
Este proyecto fue inspirado gracias a que todos en algún momento de nuestra vida, hemos manejado o al menos conocido Arduino siendo jóvenes, sin embargo, siempre han sido proyectos muy clásicos, como los robots de competición de seguimiento de linea.
Para ello, pensamos, que podría motivar a alguien joven a interesarse por su propia cuenta en este tipo de tecnologías. ¿La respuesta? La diversión. Y quien no ha disfrutado nunca jugando con amigos en un bar al futbolín, al billar, dardos.
El problema es que queríamos algo que también pudiera estar orientado hacia un publico mas infantil, algo que pudiese motivar a los mas pequeños de la casa a interesarse por la tecnología de Arduino.
De este modo nació la idea, un futbolín que integrase tecnología de Arduino, ahora había que ver cual, dado que ELEGOO, ofrece una caja con múltiples componentes, quisimos diseñar algo que únicamente tuviera piezas de este pack, con el objetivo de minimizar costes y que pudieran reutilizar los componentes, para futuros proyectos. Dado que los niños son propensos a hacer trampas, pensamos, ¿y si reducimos al mínimo la posibilidad de hacerlas? Y así surgió la idea del auto-arbitraje, goles y control de la partida automáticos para que así tanto niños como adultos pueden jugar unas partidas sin miedo a mentiras.
Reparto del trabajo:
Dado que nuestro proyecto era un artefacto de un tamaño considerable, consideramos que la manera mas optima de trabajar era, al uno tener disponible el material del kit y el otro disponer de multitud de herramientas, vivimos bastante lejos el uno del otro y no disponemos de un vehículo propio para transportar el proyecto con nosotros así que tratamos en la medida de lo posible, separar el diseño del software y el del hardware, para poder minimizar el numero de viajes en los que pudiera sufrir algún componente, pero teniendo en mente un esquema de como debía quedar para que luego no hubiera problemas al integrarlos, por ello la división mayoritaria fue:
Sergio Espinosa Robles: Diseño del Software
Joel Domené Álvaro: Diseño del Hardware
Ambos: Integración del proyecto y búsqueda de soluciones a distintos problemas encontrados.
Arquitectura del Hardware:
⚙️ Unidad de Control y Configuración: En lugar de llenarlo de botones físicos, hemos optado por un Receptor Infrarrojo (IR) conectado al pin 8. Este módulo lee las frecuencias a 38kHz de un mando a distancia, permitiendo a los jugadores elegir los equipos, el tiempo y el objetivo de goles cómodamente antes del saque inicial.
🚨 Sistema de Arbitraje Óptico: La magia de la detección automática ocurre gracias a dos sensores infrarrojos de barrera instalados en las porterías (pines 7 y 10). Para evitar que una misma bola se cuente varias veces si se atasca, los hemos programado mediante lógica inversa y detección por flanco de bajada. Si el láser se corta, es gol; sin margen de error ni rebotes.
📊 Interfaz Audiovisual: Para que los jugadores tengan puedan ver el marcador con tiempo en tiempo real, el sistema cuenta con un marcador digital proyectado en una Pantalla LCD 16×2 (cableada en modo de 4 bits). Además, la experiencia se completa con un Buzzer activo (pin 6) que asume el rol del silbato del árbitro: emite tonos de confirmación en los menús, celebra cada tanto y pita el final del encuentro.
🚧 Bloqueo Mecánico: Cuando el partido termina o se llega al límite de goles, el sistema debe impedir físicamente que se siga jugando. Para ello, utilizamos un Servomotor SG90 (controlado por señal PWM en el pin 9) que abre una barrera de madera a 180º para liberar las bolas, y la bloquea en 0º al finalizar.
⚡ Gestión de Energía Aislada: Dado que el motor necesita picos de tensión para moverse, dada la gran cantidad de componentes observamos que conectado junto a todo no era capaz de moverse, mientras que si lo alimentábamos con una fuente de energía externa(una batería de 4,5V), si era capaz de realizar estos movimientos

Diseño del software y casos de uso(flujo de ejecución del proyecto):
Primera Fase: Para la implementación del software, fuimos componente a componente, pero con el sistema final ya planteado, inicialmente únicamente implementamos el menú mediante la pantalla LCD y el sensor infrarrojo. Nuestro programa se basa en una maquina de estados, en esta fase, únicamente implementamos los estados SET_MODO, SET_EQUIPO_A, SET_EQUIPO_B, SET_TIEMPO, SET_GOLES y CONFIRMAR dentro de la funcion gestionarEntradaMando, una vez dábamos a confirmar, el sistema se volvía a ejecutar situarse en el estado SET_MODO y ejecutaba de nuevo de manera indefinida.
Segunda Fase: A partir de aquí, todavía no disponíamos de los sensores, si embargo, decidimos ir implementando el resto del código utilizando señales con el mando, aquí reutilizando la señal del volumen arriba y volumen abajo, controlábamos los goles de uno y otro equipo mediante estos botones y así pudimos completar lo que era el flujo de ejecución del proyecto.
Tercera Fase: Llegaron los sensores, y llegaron los problemas, en cuenta al hardware, tuvimos pequeños problemas de cara a la detección de la pelota, que únicamente requirieron de cierta calibración y compra de pelotas de los colores adecuados. Sin embargo, en cuanto a la detección, cada vez que el sensor detectaba la pelota, como envía la señal varias veces por segundo, por lo que tuvimos que diseñar una pequeña regla lógica en el código: hacer que el programa recordara cómo estaba el sensor hace un momento. Básicamente, era hacer que solo sumara un gol si el sensor detecta la bola ahora mismo, pero en la comprobación anterior no había nada. Guardando siempre ese «estado anterior», conseguimos que el sistema solo cuente el instante exacto en el que entra la pelota, ignorando el resto del tiempo que se queda tapando el sensor.
Cuarta Fase: Por ultimo vino el servomotor, realmente era la parte mas sencilla del código pero como todavía no habíamos trabajado con el hubo que investigar primero para asegurarnos de no forzar el motor, únicamente añadimos al final de la partida y al inicio el cierre y apertura del mismo para cerrar/abrir el paso a la pelota, también, consideramos adecuado añadir en la función setup un miBarrera.write(decirle al servo que gire) para cerrarlo, por si acaso se desconectaba la batería por un tirón, se colocase en la posición por defecto antes de iniciar la partida(cerrado).
Por ultimo, dejamos el codigo dispoible para todo aquel que quiera replicar el proyecto:
#include <LiquidCrystal.h>
#include <IRremote.hpp>
#include <Servo.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
const int pinIR = 8;
const int pinBuzzer = 6;
const int pinSensorA = 7;
const int pinSensorB = 10;
const int pinServo = 9;
Servo miBarrera;
const int BOTON_VOL_MAS = 70;
const int BOTON_VOL_MENOS = 21;
const int BOTON_PLAY = 64;
enum Estado { SET_MODO, SET_EQUIPO_A, SET_EQUIPO_B, SET_TIEMPO, SET_GOLES, CONFIRMAR, JUGANDO, GOL_DE_ORO, FIN_PARTIDO };
Estado estadoActual = SET_MODO;
int modoJuego = 1;
int equipoA = 0;
int equipoB = 1;
int minutosConfig = 5;
int golesObjetivo = 3;
int golesA = 0;
int golesB = 0;
int ganador = 0;
int ultimoEstadoA = HIGH;
int ultimoEstadoB = HIGH;
unsigned long tiempoInicioPartido;
unsigned long duracionTotalMs;
const char* const siglasClubes[] = { "RMA", "FCB", "ATM", "ATH", "BET", "VCF", "SFC", "GET", "RAY" };
const char* const nombresClubes[] = { "Real Madrid", "Barcelona", "Atl. Madrid", "Ath. Bilbao", "Betis", "Valencia", "Sevilla", "Getafe", "Rayo Vallecano" };
const char* const msgClubes[] = { " Hala Madrid! ", " Visca Barsa! ", " Aupa Atleti! ", " Aupa Athletic! ", " Viva er Beti! ", " Amunt Valencia!", " Vamos Sevilla! ", " Vamos Geta! ", " A las armas! " };
const char* const siglasSelecs[] = { "ESP", "FRA", "ARG", "BRA", "ITA", "ENG", "GER", "POR", "COL" };
const char* const nombresSelecs[] = { "Espana", "Francia", "Argentina", "Brasil", "Italia", "Inglaterra", "Alemania", "Portugal", "Colombia" };
const char* const msgSelecs[] = { " Viva Espana! ", "Allez les Bleus!", " Vamo Carajo! ", " Vai Brasil! ", " Forza Azzurri! ", "Come on England!", " Auf gehts! ", " Forca Portugal!", " Si se puede! " };
const char* const* siglasActivas;
const char* const* nombresActivos;
const char* const* msgActivos;
void setup() {
lcd.begin(16, 2);
IrReceiver.begin(pinIR);
pinMode(pinBuzzer, OUTPUT);
pinMode(pinSensorA, INPUT);
pinMode(pinSensorB, INPUT);
miBarrera.attach(pinServo);
miBarrera.write(0);
actualizarPantallaConfig();
}
void loop() {
if (IrReceiver.decode()) {
int comando = IrReceiver.decodedIRData.command;
if (comando != 0) {
gestionarEntradaMando(comando);
delay(300);
}
IrReceiver.resume();
}
if (estadoActual == JUGANDO || estadoActual == GOL_DE_ORO) {
leerSensoresGoles();
gestionarLogicaPartido();
}
}
void leerSensoresGoles() {
int estadoActualA = digitalRead(pinSensorA);
int estadoActualB = digitalRead(pinSensorB);
if (estadoActualA == LOW && ultimoEstadoA == HIGH) {
golesA++;
pitidoSimple(2000, 150);
comprobarVictoria();
delay(50);
}
if (estadoActualB == LOW && ultimoEstadoB == HIGH) {
golesB++;
pitidoSimple(2000, 150);
comprobarVictoria();
delay(50);
}
ultimoEstadoA = estadoActualA;
ultimoEstadoB = estadoActualB;
}
void gestionarEntradaMando(int cmd) {
switch (estadoActual) {
case SET_MODO:
if (cmd == BOTON_VOL_MAS || cmd == BOTON_VOL_MENOS) {
modoJuego = (modoJuego == 1) ? 2 : 1;
}
if (cmd == BOTON_PLAY) {
if (modoJuego == 1) {
siglasActivas = siglasClubes;
nombresActivos = nombresClubes;
msgActivos = msgClubes;
} else {
siglasActivas = siglasSelecs;
nombresActivos = nombresSelecs;
msgActivos = msgSelecs;
}
estadoActual = SET_EQUIPO_A;
pitidoSimple(1000, 100);
}
actualizarPantallaConfig();
break;
case SET_EQUIPO_A:
if (cmd == BOTON_VOL_MENOS) { equipoA = (equipoA + 1) % 9; }
if (cmd == BOTON_VOL_MAS) { equipoA = (equipoA - 1 + 9) % 9; }
if (cmd == BOTON_PLAY) {
estadoActual = SET_EQUIPO_B;
pitidoSimple(1000, 100);
}
actualizarPantallaConfig();
break;
case SET_EQUIPO_B:
if (cmd == BOTON_VOL_MENOS) { equipoB = (equipoB + 1) % 9; }
if (cmd == BOTON_VOL_MAS) { equipoB = (equipoB - 1 + 9) % 9; }
if (cmd == BOTON_PLAY) {
estadoActual = SET_TIEMPO;
pitidoSimple(1000, 100);
}
actualizarPantallaConfig();
break;
case SET_TIEMPO:
if (cmd == BOTON_VOL_MAS && minutosConfig < 8) minutosConfig++;
if (cmd == BOTON_VOL_MENOS && minutosConfig > 2) minutosConfig--;
if (cmd == BOTON_PLAY) {
estadoActual = SET_GOLES;
pitidoSimple(1000, 100);
}
actualizarPantallaConfig();
break;
case SET_GOLES:
if (cmd == BOTON_VOL_MAS && golesObjetivo < 5) golesObjetivo++;
if (cmd == BOTON_VOL_MENOS && golesObjetivo > 1) golesObjetivo--;
if (cmd == BOTON_PLAY) {
estadoActual = CONFIRMAR;
pitidoSimple(1000, 100);
}
actualizarPantallaConfig();
break;
case CONFIRMAR:
if (cmd == BOTON_PLAY) {
duracionTotalMs = (unsigned long)minutosConfig * 60000;
golesA = 0;
golesB = 0;
tiempoInicioPartido = millis();
estadoActual = JUGANDO;
lcd.clear();
miBarrera.write(180);
pitidoSimple(1500, 500);
}
break;
case JUGANDO:
case GOL_DE_ORO:
break;
case FIN_PARTIDO:
if (cmd == BOTON_PLAY) {
estadoActual = SET_MODO;
actualizarPantallaConfig();
}
break;
}
}
void comprobarVictoria() {
if (golesA >= golesObjetivo || (estadoActual == GOL_DE_ORO && golesA > golesB)) {
miBarrera.write(0);
ganador = 1;
estadoActual = FIN_PARTIDO;
mostrarPantallaVictoria();
delay(100);
pitidoFinal();
} else if (golesB >= golesObjetivo || (estadoActual == GOL_DE_ORO && golesB > golesA)) {
miBarrera.write(0);
ganador = 2;
estadoActual = FIN_PARTIDO;
mostrarPantallaVictoria();
delay(100);
pitidoFinal();
}
}
void gestionarLogicaPartido() {
unsigned long tiempoTranscurrido = millis() - tiempoInicioPartido;
if (estadoActual == JUGANDO) {
if (tiempoTranscurrido >= duracionTotalMs) {
if (golesA == golesB) {
estadoActual = GOL_DE_ORO;
pitidoSimple(1200, 400);
} else {
miBarrera.write(0);
ganador = (golesA > golesB) ? 1 : 2;
estadoActual = FIN_PARTIDO;
mostrarPantallaVictoria();
delay(100);
pitidoFinal();
}
}
}
if (estadoActual != FIN_PARTIDO) dibujarMarcador(tiempoTranscurrido);
}
void actualizarPantallaConfig() {
lcd.clear();
if (estadoActual == SET_MODO) {
lcd.print("Elija el modo:");
lcd.setCursor(0, 1);
if (modoJuego == 1) lcd.print("1-Clubes");
else lcd.print("2-Selecciones");
} else if (estadoActual == SET_EQUIPO_A) {
if (modoJuego == 1) lcd.print("Club A: (1-9)");
else lcd.print("Slcn A: (1-9)");
lcd.setCursor(0, 1);
lcd.print(equipoA + 1);
lcd.print("-");
lcd.print(nombresActivos[equipoA]);
} else if (estadoActual == SET_EQUIPO_B) {
if (modoJuego == 1) lcd.print("Club B: (1-9)");
else lcd.print("Slcn B: (1-9)");
lcd.setCursor(0, 1);
lcd.print(equipoB + 1);
lcd.print("-");
lcd.print(nombresActivos[equipoB]);
} else if (estadoActual == SET_TIEMPO) {
lcd.print("Elegir tiempo:");
lcd.setCursor(0, 1);
lcd.print(minutosConfig);
lcd.print(" minutos");
} else if (estadoActual == SET_GOLES) {
lcd.print("Goles objetivo:");
lcd.setCursor(0, 1);
lcd.print(golesObjetivo);
lcd.print(" goles");
} else if (estadoActual == CONFIRMAR) {
lcd.print("Pulse PLAY para");
lcd.setCursor(0, 1);
lcd.print("empezar...");
}
}
void dibujarMarcador(unsigned long ms) {
lcd.setCursor(0, 0);
if (estadoActual == GOL_DE_ORO) lcd.print(" GOL DE ORO ");
else {
int m = (ms / 60000);
int s = (ms % 60000) / 1000;
lcd.print("T:");
lcd.print(m);
lcd.print(":");
if (s < 10) lcd.print("0");
lcd.print(s);
lcd.print(" Obj:");
lcd.print(golesObjetivo);
lcd.print(" ");
}
lcd.setCursor(0, 1);
lcd.print(siglasActivas[equipoA]);
lcd.print(" ");
lcd.print(golesA);
lcd.print("-");
lcd.print(golesB);
lcd.print(" ");
lcd.print(siglasActivas[equipoB]);
lcd.print(" ");
}
void mostrarPantallaVictoria() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(siglasActivas[equipoA]);
lcd.print(" ");
lcd.print(golesA);
lcd.print("-");
lcd.print(golesB);
lcd.print(" ");
lcd.print(siglasActivas[equipoB]);
lcd.setCursor(0, 1);
if (ganador == 1) lcd.print(msgActivos[equipoA]);
else lcd.print(msgActivos[equipoB]);
}
void pitidoSimple(int frecuencia, int duracion) {
for (long i = 0; i < duracion * 1000L; i += frecuencia) {
digitalWrite(pinBuzzer, HIGH);
delayMicroseconds(frecuencia / 2);
digitalWrite(pinBuzzer, LOW);
delayMicroseconds(frecuencia / 2);
}
}
void pitidoFinal() {
for (int i = 0; i < 3; i++) {
pitidoSimple(1000, 400);
delay(200);
}
}
Costes del proyecto:
A continuacion, mostramos una tabla con los costes estimados del proyecto, cabe recalcar, que este proyecto esta pensado para aquellos que dispongan de kit ELEGOO completo, con el cual poder replicar proyectos, ya que muchos de los costes que tuvimos que asumir fueron unicamente de piezas que no funcionaban, por lo tanto, invitamos a realizar este proyecto a todo aquel que disfrute de Arduino y el futbol
| Componente | Descripción | Coste real/Coste con kit completo |
| Pila 9V | Pila para la placa ELEGOO | 4,5€ |
| Pila de litio CR2032 | Pila para el mando | 3€ |
| Servo SG-90 | Servomotor de giro 180 grados | 4€ |
| Cables de conexion | Cables macho hembra extra | 4,6€ |
| Cable Arduino | Cable Arduino-USB | 0,9 |
| Sensores | Sensores infrarrojos | 4,4€ |
| Cajas de empotrar | Cajas de 10x5x4,5cm(3) | 2,61€ |
| Codos | Codos de 45 grados | 0,96€ |
| Tubo | Tubos de PVC de 40 mm | 2,54€ |
| Varillas | Varillas roscadas de metal de 1m(2) | 2,46€ |
| Tablero y jugadores | Carton piedra de 1x3m | estimado 4€ |
| Estructura | Caja de madera, tablones y patas | estimado 7€ |
| Varios | Tornillos, tuercas, arandelas, etc… | 2,35€ |
| Total | 36,32€ |
Problemas encontrados, soluciones y posibles mejoras:
Problema 1: Caída insuficiente de las porterías
Descripción: Dado que las porterías eran únicamente cajas de plástico, a veces la pelota podía rebotar fuera o quedarse atrapadas dentro sin bajar
Solución: Ya que no contábamos con la materia para hacerlo, utilizamos unas pequeñas rampas hechas con cartón piedra para dirigir la pelota hacia el tubo
Posible mejora: Lo ideal seria con un material como la plastilina, dar una pequeña capa en la pared trasera de las porterías, y hacer una pequeña caída dentro para que vayan hacia el tubo, la plastilina o similares, al ser materiales muy blandos, absorben gran cantidad del impacto de la pelota, lo que haría que no rebotase
Problema 2: Goles con conteo múltiple
Descripción: Los sensores infrarrojos de las porterías envían de manera constante miles de señales para detectar si pasa algún objeto o no, por tanto, al pasar la pelota, en múltiples ocasiones, contaba goles de manera repetida.
Solución: Como mencionamos en el apartado del código, diseñamos una función lógica que guarda el estado del sensor, de manera que únicamente cuenta el gol cuando pasa de no detectar nada a detectar algo, si conserva el estado de detectar durante varias lecturas, no añade, así solo cuneta un gol por cada vez que pasa la pelota
Problema 3: Señales confusas(no solucionado)
Descripción: Al utilizar el mando para controlar el menú y los sensores para detectar los goles, se podría hacer una pequeña trampa con el mando, ya que, aunque no afecte al estado del juego, al compartir frecuencia(ambos son infrarrojos) si pulsas el mando, los sensores pueden detectar la señal y confundirla con el paso de la pelota, contando así goles que no han ocurrido
Posible mejora: Una vez finalizado el montaje, nos dimos cuenta que una gran idea y bastante mas intuitiva podría ser utilizar el joystick para controlar el menú navegando de izquierda a derecha entre modo de clubes y selecciones, arriba a abajo entre clubes/selecciones y utilizar el clic del Joystick para aceptar, dando lugar a mayor variedad de partidos como en juegos populares tipo FIFA.
Video memoria:
Finalmente, adjuntamos el video explicativo en el que se incluye en el siguiente orden la explicación del hardware utilizado, unas demostraciones del sistema en funcionamiento y finalmente, la explicacion del software implementado para asi lograr el proyecto completo.