Proyecto B.O.M.B.A
Introducción al Sistema
El proyecto B.O.M.B.A. es un sistema interactivo basado en hardware y software que adapta a la realidad física la mecánica central del videojuego cooperativo Keep Talking and Nobody Explodes. El objetivo fundamental es la creación de una experiencia de simulación de alta presión donde la resolución de problemas y la comunicación técnica son los pilares de la victoria.
1.1. Concepto y Dinámica Operativa
El sistema se fundamenta en un modelo de comunicación asimétrica entre dos figuras clave:
- El Desactivador: El operario que interactúa directamente con el artefacto físico. Este usuario posee la capacidad de manipulación de los módulos, pero carece del conocimiento sobre la lógica interna necesaria para resolverlos.
- El Experto: Un analista que posee el manual técnico de desactivación. El experto dispone de toda la lógica condicional y las tablas de referencia, pero tiene prohibido el contacto visual con el dispositivo.
La resolución del sistema solo es posible mediante el intercambio constante de información precisa, lo que introduce un factor humano de error que debe gestionarse bajo condiciones de estrés temporal.
1.2. Arquitectura del Juego y Condiciones de Fallo
La bomba está gobernada por un sistema de control central que monitoriza dos parámetros críticos de seguridad:
- Gestión Temporal: Un temporizador de cuenta atrás que define el margen de maniobra del equipo. El agotamiento del tiempo resulta en la detonación inmediata del artefacto.
- Sistema de Penalizaciones (Strikes): El dispositivo cuenta con un registro de errores para el seguimiento de fallos operativos. El sistema admite un máximo de dos errores; al registrarse el tercer strike, el módulo central activa la secuencia de detonación como medida de seguridad ante una manipulación negligente.
1.3. Descripción de los Módulos de Desactivación
Para lograr la desactivación completa del sistema, el equipo debe neutralizar cinco subsistemas o módulos independientes, cada uno diseñado para poner a prueba diferentes capacidades cognitivas y técnicas:
- Código Morse: Un módulo de decodificación que requiere la interpretación visual de señales lumínicas para identificar palabras clave y sintonizar una frecuencia de radio específica mediante un potenciómetro.
- Simón Dice Condicional: Un juego de memoria visual cuya lógica de respuesta es dinámica. La correspondencia entre la luz mostrada y el botón a pulsar varía en función del número de strikes activos en la bomba en ese instante.
- Cortar Cables: Un desafío de lógica pura basado en el análisis del estado inicial del cableado. El desactivador debe identificar qué cables específicos interrumpir siguiendo una jerarquía de reglas condicionales de alta complejidad.
- Módulo de Memoria: Un sistema de cinco fases que exige al jugador recordar posiciones y etiquetas de botones anteriores en función de la información numérica mostrada en un display de siete segmentos.
- Descarga del Capacitor (Módulo «Needy»): A diferencia de los anteriores, este es un módulo asíncrono de mantenimiento persistente. No se desactiva permanentemente, sino que requiere ser monitorizado y descargado cada pocos segundos para evitar una sobrecarga que sumaría un strike al contador global, interrumpiendo así el flujo de trabajo en los demás módulos.
Proyecto B.O.M.B.A
Módulo 1: Código Morse
2.1. Casos de Uso y Funcionamiento
Se trata de un módulo de decodificación donde el jugador debe interpretar una palabra transmitida en código Morse mediante el parpadeo de un LED. La palabra se emite en bucle de forma continua. El jugador dispone de un potenciómetro que controla una frecuencia mostrada en un display de 7 segmentos; debe sintonizar la frecuencia que corresponde a la palabra que está siendo transmitida. La complejidad y el factor diferencial (el caso de uso principal) radica en que el jugador necesita conocer tanto el código Morse como la tabla de correspondencias palabra–frecuencia: primero debe decodificar visualmente la secuencia de puntos y rayas, identificar la palabra, y luego girar el potenciómetro hasta seleccionar la frecuencia correcta antes de pulsar el botón para confirmar su respuesta.
2.2. Implementación Hardware
Implementado sobre la placa Arduino Mega, el módulo requiere:
- Entradas: 1 potenciómetro conectado al pin analógico A0 para la selección de frecuencia, y 1 pulsador con resistencia Pull-Up interna habilitada por software (pin 7).
- Salidas: 1 LED (pin 13) para la emisión de la señal Morse, y 1 display de 4 dígitos de 7 segmentos controlado mediante un registro de desplazamiento 74HC595 (pines Latch=8, Clock=12, Data=11) con 4 líneas de selección de dígito (pines 2, 3, 4, 5) para la multiplexación.

2.3. Implementación Software
El módulo Morse está implementado como una clase que encapsula toda la lógica de juego, desde la emisión no bloqueante de señales Morse hasta la lectura analógica del potenciómetro y la validación de la frecuencia seleccionada.
A. Inicialización y Configuración (begin)
- Gestión de Pines: Configura los pines del registro de desplazamiento (latch, clock, data) y el LED Morse como salidas, los pines de selección de dígito como salidas, y el botón como entrada utilizando INPUT_PULLUP para aprovechar las resistencias internas del microcontrolador.
- Aleatoriedad: Utiliza randomSeed(analogRead(A5)) para asegurar que la palabra seleccionada sea diferente en cada partida, leyendo el ruido eléctrico de un pin analógico flotante. Tras ello, random(0, TOTAL_PALABRAS) elige un índice aleatorio de la tabla de 10 palabras posibles.
B. Tabla de Palabras y Alfabeto Morse
El módulo almacena dos estructuras de datos estáticas fundamentales:
- table[10]: Un array de structs MorseWord que asocia cada palabra posible con su frecuencia correspondiente (ej. «brillo» → 1505, «bomba» → 1552). Esta tabla es la referencia tanto para la emisión como para la validación.
- alphabet[26]: Un array que codifica cada letra del abecedario (a–z) en su representación Morse de puntos (‘.’) y rayas (‘-‘), permitiendo traducir cualquier palabra carácter a carácter.
C. Máquina de Estados en actualizarMorse
Este método gestiona la emisión de la señal Morse de forma completamente no bloqueante, utilizando millis() y una variable nextEventTime para controlar las transiciones temporales. La unidad de tiempo base (U) es de 200ms. Su flujo se divide en:
- ENVIANDO_SIMBOLO: Enciende el LED y programa el tiempo de encendido: 1×U (200ms) para un punto, 3×U (600ms) para una raya. Transiciona al estado ESPACIO_SIMBOLO.
- ESPACIO_SIMBOLO: Apaga el LED tras el punto o la raya y evalúa el siguiente paso:
- Si quedan más símbolos en la letra actual, espera 1×U (espacio intra-carácter) y vuelve a ENVIANDO_SIMBOLO.
- Si la letra ha terminado, pero quedan más letras en la palabra, espera 3×U (espacio entre letras) y avanza al siguiente carácter.
- Si la palabra ha terminado por completo, espera 10×U (2 segundos de espacio entre repeticiones) y reinicia la emisión desde el primer carácter.
D. Multiplexación del Display (mostrarFrecuencia)
Este método implementa la técnica de multiplexación por barrido rápido para mostrar la frecuencia de 4 dígitos en el display de 7 segmentos utilizando un único registro de desplazamiento 74HC595. Para cada dígito:
- Apaga todos los dígitos para eliminar el efecto de «sombras» (ghosting).
- Carga el patrón de segmentos correspondiente al dígito actual en el 74HC595 mediante shiftOut.
- Enciende únicamente el dígito activo antes de pasar al siguiente.
E. Lectura del Potenciómetro y Selección de Frecuencia (update)
El método update se ejecuta en cada iteración del loop principal y orquesta las cuatro tareas del módulo:
- Lectura analógica: Lee el valor crudo del potenciómetro con analogRead(potPin).
- Calibración y mapeo: Aplica constrain(val, 15, 940) para recortar los extremos no alcanzables del rango ADC, y luego map() para convertir el valor calibrado a un índice de la tabla de palabras (0–9), obteniendo así la frecuencia seleccionada.
- Emisión Morse: Invoca actualizarMorse() con la palabra asociada al índice aleatorio elegido al inicio.
- Comprobación del botón: Si txButtonJustPressed() detecta una pulsación confirmada, invoca validarRespuesta().
F. Debounce del Botón (txButtonJustPressed)
Implementa un algoritmo de debounce por software que detecta flancos de bajada (HIGH – LOW) estables. Utiliza tres variables de estado (_lastStableBtnState, _candidateBtnState, _debounceTimestamp) para filtrar el ruido mecánico del pulsador:
- Si la lectura cambia respecto al candidato actual, se reinicia el temporizador.
- Solo cuando la lectura se mantiene estable durante más de 50ms (DEBOUNCE_MS) y difiere del último estado confirmado, se acepta como pulsación real.
- Retorna true únicamente en el instante de la transición confirmada a LOW, garantizando una única detección por pulsación.
G. Validación de Respuesta (validarRespuesta)
Compara la frecuencia actualmente seleccionada por el potenciómetro (selectedFreq) con la frecuencia correcta de la palabra que se está emitiendo:
- Acierto (return 1): Apaga todos los componentes del módulo (LED, display) dejándolo en estado resuelto.
- Fallo (return -1): El valor de retorno se procesa en el loop principal, que incrementa el contador global de strikes.
Módulo 2: Simón Dice Condicional
3.1. Casos de Uso y Funcionamiento
Se trata de un juego de memoria visual y sonora de hasta 5 rondas. El jugador debe replicar la secuencia de luces generada por el Arduino. La complejidad y el factor diferencial (el caso de uso principal) radica en que la correspondencia de botones cambia dinámicamente según el estado global de la bomba. Si el jugador no tiene errores (0 strikes), el botón rojo puede corresponder a la luz azul; pero si tiene 1 strike, el mapeo cambia.
3.2. Implementación Hardware
Implementado sobre la placa Arduino Mega, el módulo requiere:
- Entradas: 4 pulsadores con resistencias Pull-Up internas habilitadas por software.
- Salidas: 4 LEDs (Rojo, Azul, Verde, Amarillo) con resistencias limitadoras de 220Ω y un zumbador pasivo para el feedback sonoro.

3.3 Implementación Software
El módulo Simón Dice está implementado como una clase que encapsula toda la lógica de juego, desde la generación de secuencias hasta la validación de entradas con reglas condicionales.
A. Inicialización y Configuración (begin)
- Gestión de Pines: Configura los pines de los LEDs como salidas y los botones como entradas utilizando INPUT_PULLUP para aprovechar las resistencias internas del microcontrolador.
- Entropía: Utiliza randomSeed(analogRead(A0)) para asegurar que las secuencias generadas sean diferentes en cada partida, leyendo el ruido eléctrico de un pin analógico flotante.
B. Lógica de Mapeo Condicional (getMappedColor)
Este método es el corazón de la «lógica condicional» del proyecto. No realiza una comparación directa, sino que traduce el color mostrado a un botón esperado basándose en el número de strikes actuales:
- 0 strikes: Mapea mediante el array map0 (ej. Rojo se convierte en Azul).
- 1 strike: Mapea mediante map1 (ej. Rojo se convierte en Verde).
- ≥2 strikes: Mapea mediante map2 (ej. Rojo se convierte en Amarillo).
C. Máquina de Estados en update
El método update se ejecuta en cada iteración del loop principal sin detener el procesador. Su flujo se divide en:
- GENERATE: Añade un nuevo paso aleatorio a la secuencia y reinicia el contador para mostrarla.
- PAUSING y SHOWING: Controlan el tiempo de encendido (500ms) y el intervalo entre LEDs (200ms). Utiliza una variable _timer con millis() para determinar cuándo cambiar de estado sin usar delay().
- WAITING: Llama a readButtons() para detectar la entrada del jugador.
- Si el botón coincide con el resultado de getMappedColor, avanza al estado FEEDBACK.
- Si falla, envía una señal de error al GameState (strike) y reinicia la visualización de la secuencia.
- FEEDBACK: Mantiene el LED del botón pulsado encendido por 150ms para dar respuesta visual al usuario antes de procesar el siguiente paso.
- ROUND_FLASHING y FLASHING: Son estados de transición estética que ejecutan parpadeos rápidos al completar una ronda o el módulo completo, respectivamente.
Módulo 3: Cortar Cables
4.1. Casos de Uso y Funcionamiento
Este módulo presenta una serie de 10 cables de distintos colores. El objetivo del desactivador es inutilizar el módulo cortando únicamente los cables correctos basándose en un conjunto de reglas lógicas. Si el jugador corta un cable que no corresponde, el módulo registrará un error y enviará un STRIKE.
Nota Crítica de Funcionamiento: Todas las reglas lógicas se evalúan única y exclusivamente basándose en el estado inicial de la bomba. Los cables que el jugador vaya cortando siguen contando para todas las reglas (por ejemplo, si inicialmente había 2 cables rojos y corta uno, la lógica que exige «exactamente 2 rojos» sigue siendo la válida). El desactivador debe llevar un registro mental o físico de la configuración original.
Instrucciones de Desconexión: En el momento en que una condición se cumpla, se debe ejecutar la acción:
- Si hay exactamente 2 cables Rojos: Corta todos los cables Rojos.
- Si hay 1 cable Marrón y al menos 2 Azules: Corta el cable Marrón.
- Si hay al menos 1 cable Rojo y uno Negro: Corta el cable Negro.
- Si hay 1 cable Verde y un Azul: Corta el cable Azul.
- Si hay 1 cable Rojo y un Verde: Corta el cable Rojo.
Condición importante:
- Si hay un cable de color BLANCO: Se ignora cualquier otra regla o cable mencionado anteriormente. El desactivador debe cortar únicamente el cable Blanco, ignorando por completo el resto de colores.
4.2. Implementación Hardware
Implementado sobre la placa Arduino Mega, este módulo se basa en la interrupción de un circuito cerrado:
- Entradas: 10 cables físicos puenteados. Un extremo de cada cable va conectado a un pin común de Tierra (GND) y el otro extremo va conectado directamente a pines digitales específicos del Arduino Mega (pines 45, 46, 47, 48, 49, 50, 51, 52, 53 y 6).
- Se utilizan las resistencias Pull-Up internas de la placa habilitadas por software, por lo que un cable conectado leerá un estado lógico LOW (conectado a tierra) y un cable cortado leerá un estado HIGH (circuito abierto).
4.3 Implementación Software
El módulo está implementado mediante la clase Cables, la cual encapsula la configuración de los pines, la evaluación del estado inicial y el ciclo de validación de cortes.
A. Inicialización y Configuración (begin)
- Gestión de Pines: Itera sobre el array de pines configurándolos como INPUT_PULLUP.
- Captura del Estado Inicial: Evalúa cada pin usando digitalRead() == LOW para determinar qué cables están físicamente presentes al arrancar la bomba, guardando esta información en el array _estadoInicial.
- A continuación, invoca el método inicializarReglas() para precalcular la solución.
B. Evaluación Lógica de Reglas (inicializarReglas)
Este método actúa como el cerebro del módulo, traduciendo las reglas del manual a lógica de código basándose en el estado inicial capturado:
- Condición de Prioridad Máxima (Blanco): Revisa primero si el pin asociado al cable blanco (índice 7) está presente. Si lo está, marca exclusivamente este cable en _cableADesconectar y finaliza la evaluación mediante un return.
- Conteo de Colores: Si no hay cable blanco, recorre el estado inicial y clasifica los cables presentes en variables contadoras (verdes, rojos, azules, marrones) según su índice de hardware.
- Cascada Condicional: Evalúa los contadores a través de una serie de sentencias if que representan fielmente las instrucciones de desconexión. Si una condición es verdadera, marca el índice (o índices) correspondientes en el array booleano _cableADesconectar.
C. Monitoreo y Validación de Cortes (update)
El método update se ejecuta en cada iteración del ciclo principal de la bomba de forma no bloqueante:
- Detección de Cambios (Cortes): Lee el estado actual de los 10 pines. Si detecta que un pin pasa de estado LOW (Anterior) a HIGH (Actual), significa que el jugador acaba de cortar ese cable.
- Validación de STRIKE: Al detectar un corte, comprueba si ese índice específico estaba marcado como true en _cableADesconectar. Si no lo estaba, incrementa el contador de fallos (_fallos) y devuelve un evento de error (-1) al sistema central.
- Condición de Victoria: Simultáneamente, verifica que todos los cables que debían ser desconectados se encuentren ahora en estado HIGH (cortados). Si es así, marca el flag _resolved como true y devuelve 1, indicando que el módulo ha sido desactivado con éxito.
Módulo 4: Memoria
5.1. Casos de Uso y Funcionamiento
Este módulo pone a prueba la memoria del usuario, dado que el objetivo es ir presionando los botones correctos en función del número que se muestra en un display. Hay varias fases y en cada una de ellas cambia el botón que se debe presionar con cada número, si se comete un error se vuelve a la primera fase. Para completar este módulo es necesario superar las 5 fases que tiene.
5.2. Implementación Hardware
Implementado sobre la placa Arduino Mega, el módulo requiere:
- Entradas: 4 pulsadores con resistencias Pull-Up internas habilitadas por software.
- Salidas: 1 display de 7 segmentos.

5.3 Implementación Software
El módulo Memoria está implementado como una clase la cual contiene la lógica necesaria para configurar y usar el módulo.
A. Inicialización y Configuración (begin)
- Botones (setUpButtons): Se recorre la lista con los pines de los botones, configurando cada pin como entrada con resistencia interna (INPUT_PULLUP).
- Display (setUpSegments): Recorriendo los pines asignados a cada segmento del display se establecen como pines de salida.
- Entropía: Utiliza randomSeed(analogRead(A0)) para asegurar que los números mostrados en el display sean diferentes cada vez que se inicia el módulo, leyendo el ruido de un pin analógico.
- Inicialización de debounce: Se inicializan las estructuras necesarias para el control de rebotes de los botones, almacenando el estado estable, el estado candidato y el tiempo de cambio de cada botón.
B. Mostrar un numero (showNumber)
Se obtiene de la lista que contiene las configuraciones para cada número la configuración deseada. Después se visita los pines de cada segmento. Al mismo tiempo se obtiene el valor de salida del pin de la configuración, activando y desactivando de esta forma los segmentos para formar el número.
C. Apagar display(shutDownNumber)
Un simple método que desactiva todos los segmentos, visitando el pin de cada segmento y estableciéndolo a 0.
D. Leer botón activo (readButtons)
Se recorren los pines de los botones leyendo su estado actual. Para evitar lecturas erróneas producidas por el rebote mecánico, se compara cada lectura con un estado candidato y se utiliza un temporizador para verificar que el cambio se mantiene estable durante un periodo mínimo (DEBOUNCE_MS).
Cuando se detecta un cambio estable de HIGH a LOW (flanco de bajada), se considera que el botón ha sido pulsado y se devuelve su índice. En caso de no detectarse ninguna pulsación válida, se devuelve –1.
E. Comprobar si el botón pulsado es correcto (checkAnswer)
Recibiendo la fase en la que se encuentra el módulo, el número que se ha mostrado y el botón pulsado, se comprueba si el botón pulsado es el correcto. Esta comprobación se realiza obteniendo el índice del botón que debería haber sido pulsado con la fase y el número, y comparándolo con el índice del botón pulsado.
F. Desarrollo del juego y cambios de fase (Update)
Para empezar, se comprueba si el módulo ya ha sido completado previamente. En caso contrario, el funcionamiento del juego se controla mediante una máquina de estados no bloqueante.
En el estado inicial se genera un número aleatorio y se muestra en el display. A continuación, el sistema pasa a un estado de espera de entrada, donde se comprueba continuamente si se ha pulsado algún botón mediante el método con debounce.
Cuando se detecta una pulsación, se comprueba si el botón es correcto. En caso de ser incorrecto, el display se apaga y se devuelve un error. Si es correcto, el display se apaga y se inicia una pausa de 1 segundo, antes de continuar.
Cada fase está compuesta por 6 subrondas. Tras cada acierto se avanza a la siguiente subronda. Una vez completadas las 6 subrondas, se realiza una pausa de 2 segundos que indica el cambio de fase.
El proceso continúa hasta completar las 5 fases. Una vez finalizadas todas, el módulo pasa al estado de completado e informa de que el juego ha sido superado.
Módulo 5: Módulo «Needy» (Descargar Capacitor)
6.1. Casos de Uso y Funcionamiento
A diferencia del resto de módulos que son estáticos, el «Needy» es un módulo asíncrono y de interrupción cognitiva. De forma aleatoria (entre 3 y 10 segundos), el módulo exige atención encendiendo un LED y emitiendo un sonido rápido y agudo. El caso de uso exige que el jugador mantenga pulsado un botón de descarga durante exactamente 2 segundos continuos antes de que pasen 10 segundos. Si suelta el botón antes, debe volver a empezar. Si se acaba el tiempo, se suma un strike.
6.2. Implementación Hardware
- Entradas: 1 pulsador físico (INPUT_PULLUP).
- Salidas: 1 LED indicador de atención y un zumbador dedicado.

6.3 Implementación Software
El módulo Needy actúa como una tarea asíncrona de mantenimiento que requiere atención periódica del jugador.
A. Control de Activación Aleatoria (refreshInterval)
El módulo no está activo constantemente. En el estado IDLE, espera un tiempo aleatorio entre 3 y 10 segundos (IDLE_MIN_MS y IDLE_MAX_MS) antes de entrar en fase de alerta.
B. Gestión de Entradas con Debouncing (btnJustPressed y btnHeld)
A diferencia de una lectura simple, el código implementa dos tipos de detección para el mismo botón:
- btnJustPressed: Detecta el momento exacto en que el botón es presionado (flanco de bajada) tras 50ms de estabilidad.
- btnHeld: Verifica continuamente si el botón permanece en estado bajo (LOW) durante el proceso de descarga.
C. Flujo de la Máquina de Estados
- IDLE: El módulo está «dormido». Solo monitorea el tiempo para su próxima activación.
- ATTENTION: Es el estado de urgencia.
- Activa el parpadeo del LED y sonidos de alerta mediante un cálculo de módulo (elapsed % 400 < 200) para crear una onda cuadrada de parpadeo sin bloqueos.
- Si pasan 10 segundos (ATTENTION_TIMEOUT_MS) sin que el jugador actúe, el método retorna -1 para sumar un strike.
- HOLDING: Se activa cuando el jugador presiona el botón.
- Debe mantenerse por 2 segundos (HOLD_REQUIRED_MS).
- Si el jugador suelta el botón (!btnHeld()), el estado regresa a ATTENTION, obligando a reiniciar la descarga.
- Si se completa el tiempo, retorna 1 (éxito), apaga los indicadores y vuelve a IDLE.
Problemas y Soluciones
El desarrollo e implementación del proyecto B.O.M.B.A ha supuesto un reto significativo, no solo desde la perspectiva de la lógica de programación, sino especialmente en la fase de integración física. A lo largo del ciclo de vida del proyecto, se identificaron y resolvieron diversas problemáticas, siendo las más críticas aquellas relacionadas con las limitaciones de hardware y la concurrencia del sistema.
7.1. Limitación de Pines y Escalabilidad del Hardware
Durante las fases iniciales del proyecto, se adoptó una metodología de desarrollo modular y en paralelo. Cada integrante del equipo fue responsable de diseñar, programar y testear su respectivo módulo de manera independiente. En esta etapa, las pruebas se realizaron utilizando la Arduino UNO proporcionada en el kit de la asignatura (con un número limitado de entradas y salidas), y todos los subsistemas funcionaron a la perfección en entornos aislados.
Sin embargo, el primer obstáculo de gran magnitud surgió en la fase de integración. Al intentar unificar los cinco módulos simultáneos (Simón Dice, Needy, Memoria, Morse y Cables) en una única placa, la demanda total de pines GPIO (General Purpose Input/Output) superó con creces la capacidad de un microcontrolador estándar. La suma de pantallas de 7 segmentos, registros de desplazamiento, LEDs individuales, potenciómetros, zumbadores y la extensa matriz de botones, interruptores y cables hizo inviable la conexión conjunta.
Solución: Para resolver esta limitación de escalabilidad, fue imperativo adquirir y migrar todo el sistema a la placa Arduino Mega. Esta plataforma proporciona un número significativamente mayor de pines digitales y analógicos, lo que nos permitió alojar todos los componentes de los cinco módulos sin necesidad de recurrir a arquitecturas excesivamente complejas de multiplexación que habrían comprometido la velocidad de respuesta del sistema.
7.2. Complejidad del Ensamblaje Físico y Falsos Contactos
El principal cuello de botella del proyecto radicó en el hardware durante la fase de montaje final. La convivencia de cinco módulos operando simultáneamente requirió un volumen masivo de cableado. A nivel físico, esto generó un entramado de conexiones cruzadas de alta densidad que resultó ser extremadamente sensible.
Este «laberinto» de cables derivó en numerosos problemas de conductividad y falsos contactos. En un sistema tan interconectado, el más mínimo movimiento en la estructura provocaba que algunos pines quedaran «flotantes» durante milisegundos- Una simple vibración o un cable ligeramente suelto en la protoboard era interpretado por el microcontrolador como un corte intencionado, lo que impedía el desarrollo normal del sistema.
Solución: Se tuvo que llevar a cabo un proceso de saneamiento exhaustivo del cableado. Se reestructuraron las conexiones, agrupando las líneas lógicas, asegurando firmemente los puentes en las placas de pruebas y revisando meticulosamente las conexiones a tierra (GND) comunes para evitar ruidos parásitos. Además, se reforzó la robustez mecánica del montaje para garantizar que la manipulación del jugador durante el juego no afectara a la estabilidad eléctrica de los componentes internos.
7.3. Concurrencia de Software y Depuración Cruzada
A los problemas físicos se sumó el reto de la integración del software. Ejecutar cinco módulos de alta exigencia temporal (como el Needy, que requiere atención asíncrona, o el Morse, que emite señales continuas sin bloquear el sistema) en un único hilo de ejecución requirió una orquestación sumamente precisa.
La conjunción de los fallos de hardware mencionados anteriormente con la concurrencia del software dificultó enormemente la localización de los errores (depuración cruzada). En ocasiones, un comportamiento anómalo del software parecía ser un fallo de programación, cuando en realidad era un falso contacto en un pin de lectura.
Solución: Para garantizar la estabilidad, fue necesario realizar una revisión exhaustiva del código fuente. Se revisó línea por línea el bucle principal (loop) y la máquina de estados de cada módulo para asegurar una ejecución estrictamente no bloqueante (eliminando cualquier uso de funciones como delay()). Esta estricta organización estructural y metodológica permitió aislar el comportamiento lógico del físico, lo que facilitó identificar si un error provenía de una caída de tensión, un pin desconectado o una transición de estado incorrecta en el código. Tras este intenso trabajo de depuración conjunta, se logró estabilizar el sistema obteniendo una bomba completamente funcional y fiable.
Lista de Materiales y Costes
| Material | Coste |
| Arduino Mega | 25 € |
| Pantalla LCD | 6 € |
| Cables de distintos tipos y protoboards extra | 15 € |
| Kit de botones con tapas de colores | 8,50 € |
| Pintura negra | 4 € |
| Caja de madera | Prestada |
| Display de 7 segmentos de 1 y 4 dígitos | Kit de clase |
| LEDs de distintos colores | Kit de clase |
| Potenciómetro | Kit de clase |
| 74HC595 | Kit de clase |
| Resistencias de distintos valores | Kit de clase |
| Buzzer | Kit de clase |
| Total | 58,5 € |
Anexo: Código fuente
#include "SimonDice.h"
#include "Cables.h"
#include "Morse.h"
#include "Memoria.h"
#include "Needy.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// --- CONSTANTES DE CONFIGURACIÓN GLOBAL ---
const unsigned long TIME_LIMIT_MS = 300000;
const uint8_t MAX_STRIKES = 3;
// ESTADO DEL JUEGO (Anteriormente en GameState)
unsigned long startTime = 0;
bool gameStarted = false;
int numStrikes = 0;
bool isModuleSolved[] = {false, false, false, false};
uint8_t simonLeds[] = {22, 23, 24, 25};
uint8_t simonBtns[] = {26, 28, 27, 29};
uint8_t simonBuzzer = 30;
SimonDice simonModule(simonLeds, simonBtns, simonBuzzer);
// 1 - CORTAR CABLES
uint8_t cableWires[] = {45, 46, 47, 48, 49, 50, 51, 52, 53, 6};
bool cableInitialState[] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};
Cables cablesModule(cableWires, cableInitialState);
// 2 - CÓDIGO MORSE
Morse morseModule;
// 3 - MEMORIA
Memoria memoriaModule;
// 4 - NEEDY
uint8_t needyBtn = 42;
uint8_t needyLed = 39;
uint8_t needyBuzzer = 30;
Needy needyModule(needyBtn, needyLed, needyBuzzer);
// LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
int lastLcdSeconds = -1;
int lastLcdStrikes = -1;
unsigned long getTimeRemaining() {
if (!gameStarted) return TIME_LIMIT_MS;
unsigned long elapsed = millis() - startTime;
if (elapsed >= TIME_LIMIT_MS) return 0;
return TIME_LIMIT_MS - elapsed;
}
bool isGameOver() {
return (getTimeRemaining() == 0 || numStrikes >= MAX_STRIKES);
}
bool isGameWon() {
// Needy no cuenta para la victoria, es un modulo de mantenimiento
for(int i=0; i<4; i++){
if(isModuleSolved[i] == false) return false;
}
return true;
}
void updateLCD() {
unsigned long timeRemaining = getTimeRemaining();
int secondsTotal = timeRemaining / 1000;
if (secondsTotal != lastLcdSeconds || numStrikes != lastLcdStrikes) {
lastLcdSeconds = secondsTotal;
lastLcdStrikes = numStrikes;
int minutes = secondsTotal / 60;
int seconds = secondsTotal % 60;
lcd.setCursor(0, 0);
lcd.print("TIEMPO: ");
if (minutes < 10) lcd.print("0");
lcd.print(minutes);
lcd.print(":");
if (seconds < 10) lcd.print("0");
lcd.print(seconds);
lcd.setCursor(0, 1);
lcd.print("STRIKES: ");
lcd.print(numStrikes);
lcd.print("/");
lcd.print(MAX_STRIKES);
}
}
void solveModuleOutput(int mOutput, uint8_t moduleIndex) {
switch(mOutput){
case 1:
// Needy (moduleIndex 4) no se marca como resuelto permanentemente
if (moduleIndex < 4) {
isModuleSolved[moduleIndex] = true;
}
break;
case -1:
numStrikes++;
break;
}
}
void setup() {
Wire.begin();
lcd.init();
lcd.backlight();
simonModule.begin();
cablesModule.begin();
morseModule.begin();
memoriaModule.begin();
needyModule.begin();
startTime = millis();
gameStarted = true;
}
void loop() {
if (isGameOver()) {
updateLCD();
lcd.setCursor(0, 0);
lcd.print(" GAME OVER ");
return;
}
if (isGameWon()) {
lcd.setCursor(0, 0);
lcd.print(" YOU WIN! ");
return;
}
updateLCD();
solveModuleOutput(simonModule.update(numStrikes), 0);
solveModuleOutput(cablesModule.update(), 1);
solveModuleOutput(morseModule.update(isModuleSolved[2]), 2);
solveModuleOutput(memoriaModule.update(isModuleSolved[3]), 3);
solveModuleOutput(needyModule.update(isGameOver(), isGameWon()), 4);
}
El resto del código se encuentra disponible en nuestro repositorio de github: github.com/jae9104/SistemasEmpotrados-TiempoReal