Retro Boy — La consola perfecta para los nostálgicos

Retro Boy es una consola portátil inspirada en las Game&Watch y Game Boy Advance, creada para recuperar la esencia de las consolas antiguas y los juegos retro que generaron tan buenos recuerdos en sus momentos de gloria.

Las consolas portátiles son uno de los ejemplos más claros que se vienen a la cabeza cuando se hablan de sistemas empotrados y tienen su origen en los años 70, cuando se desarrollaron las primeras videoconsolas portátiles, las cuales sólo disponían de un único juego por aparato (característica de las Game&Watch creadas en 1980). Su diseño y complejidad fue evolucionando, añadiendo la posibilidad de cambio de juegos mediante cartuchos, característica que se mantuvo durante mucho tiempo (en forma de cartucho o disco) hasta la llegada de las descargas online.

En este contexto, Retro Boy vuelve al diseño sencillo y minimalista de las Game & Watch, donde una pequeña cantidad de botones es suficiente para poder tener una buena experiencia jugando. Al mismo tiempo, recupera el uso de los cartuchos, como todas las consolas de Nintendo, permitiendo no solo la ejecución de distintos juegos, sino también su colección en formato tangible.

De este modo, el proyecto combina elementos clave del pasado con una reinterpretación actual, dando lugar a una consola que no solo permite jugar, sino también revivir una forma de entender el entretenimiento digital más directa, sencilla y memorable.


🎯 Objetivo

El propósito central de este proyecto es el diseño y construcción de una consola portátil funcional que capture la esencia de sistemas icónicos como la Game Boy Advance o las clásicas Game & Watch. No buscamos una simple emulación por software, sino el desarrollo de un sistema integrado que gestione el hardware de forma eficiente y directa.

Metas Técnicas

Para alcanzar este propósito, nos hemos fijado los siguientes pilares fundamentales:

  • Arquitectura Basada en Estados: Implementar el núcleo del sistema como una máquina de estados finitos (FSM). Esto garantizará una transición fluida y robusta entre los menús, la lógica de juego y la gestión de periféricos.
  • Interacción Directa con el Hardware: Desarrollar el control de periféricos mediante una interfaz de usuario física, gestionando la entrada de botones en tiempo real y la salida de vídeo y audio sincronizados.
  • Identificación de Cartuchos: Crear un sistema de reconocimiento de cartuchos externos, permitiendo que la consola sea una plataforma modular capaz de cargar y ejecutar distintos títulos según el módulo conectado.
  • Diseño de Sistemas Embebidos: Integrar todos estos componentes en un formato portátil, optimizando el consumo y el espacio.

🛠️ Hardware

Para construir esta consola, hemos seleccionado componentes que equilibran potencia y bajo coste. El cerebro de todo es la ESP32-S3, que El desarrollo de la consola se basa en una arquitectura de 32 bits capaz de gestionar periféricos de entrada/salida de forma síncrona. A continuación se detalla el listado de materiales y la justificación técnica de la elección del hardware.

Listado de Componentes (BOM)

Electrónica

  • Microcontrolador: ESP32 S3 N16R8 (1 ud.) — 2,71 €
  • Interfaz Visual: Pantalla LCD TFT ST7735S (1 ud.) — 2,35 €
  • Lectura de Datos: Módulo RFID MFRC-522 (1 ud.) — 1,79 €
  • Identificadores: Llaveros de proximidad RFID (5 uds.) — 1,00 €
  • Control: Botones de interacción (7 uds.) — 1,75 €
  • Detección Mecánica: Interruptor para ranura de cartuchos (1 ud.) — 0,30 €
  • Salida de Audio: Módulo buzzer pasivo (1 ud.) — 1,40 €
  • Alimentación: Portapilas 3xAA (1 ud.) — 0,65 €
  • Soporte: Placa PCB perforada (2 uds.) — 1,02 €
  • Conectividad: Headers hembra y cableado rígido — 1,90 €

Materiales de Construcción

  • Estructura: Cartón pluma y cartulinas — 10,34 €
  • Acabado y Modelado: Arcilla, pintura acrílica y adhesivos — 20,62 €

Coste total del hardware integrado: 45,83 €


Descripción Técnica del Sistema

La elección de los componentes responde a criterios de compatibilidad y rendimiento bajo el protocolo de comunicación SPI y GPIO.

Autonomía: El sistema está diseñado para operar con una tensión nominal de 4,5V mediante pilas AA, aprovechando el regulador de voltaje interno de la placa de desarrollo para alimentar de forma estable los módulos de 3,3V.

Unidad de Procesamiento: Se ha integrado una ESP32 S3 N16R8. La selección de este modelo responde a su condición de estándar en el desarrollo de sistemas embebidos actuales, lo que asegura una amplia compatibilidad con librerías y una gran comunidad de soporte. Aunque este modelo específico cuenta con una amplia PSRAM, la eficiencia del código y la arquitectura modular de la consola han permitido ejecutar la lógica de los juegos y la gestión de buffers de imagen utilizando exclusivamente la SRAM interna, garantizando un rendimiento óptimo sin retardos en el ciclo de reloj.

Subsistema de Almacenamiento (Cartuchos): La gestión de los juegos se realiza mediante tecnología RFID. Cada cartucho artesanal contiene un tag pasivo que es interrogado por el lector MFRC-522. Para optimizar el consumo y evitar lecturas erróneas, se ha implementado un interruptor mecánico en la ranura que actúa como habilitador del proceso de escaneo.

Gestión de Periféricos: Tanto la pantalla como el lector RFID operan sobre el bus SPI. Dado que comparten las líneas de datos, el hardware requiere una gestión precisa de los pines Chip Select (CS) para evitar colisiones de datos durante la ejecución.

🔌 Diagrama de Conexiones


💻 Software: Interfaz Base

🧱 Estructura del Programa

La arquitectura de Retro Boy se basa en un enfoque modular y orientado a objetos, diseñado para maximizar la eficiencia de un sistema empotrado como el ESP32. Esta separación de responsabilidades permite que el sistema sea escalable: añadir una nueva funcionalidad o un juego no requiere reescribir el núcleo de la consola.

1. Máquina de Estados Finitos (FSM)

El flujo de la consola se gestiona mediante una Máquina de Estados que determina qué lógica debe ejecutarse en cada momento. Los estados principales definidos son:

  • STATE_OFF: Estado de reposo donde la pantalla permanece apagada.
  • STATE_MENU / STATE_SETTINGS / STATE_CALENDAR: Navegación por la interfaz base.
  • STATE_LOADING_GAME: Un estado de transición que prepara los recursos antes de lanzar un juego.
  • STATE_GAME_RUNNING: Ejecución activa del bucle de juego.

2. Polimorfismo y Gestión Dinámica de Memoria

Para gestionar las diferentes pantallas y juegos sin saturar la RAM del ESP32, hemos implementado una gestión dinámica de objetos:

  • Clase Screen e Interfaz IGame: Ambas utilizan funciones virtuales puras para definir un contrato de ejecución (init, update, draw/render).
  • Ciclo de Vida de Objetos: Utilizamos la función setScreen() para destruir la pantalla anterior (delete) y crear la nueva (new) solo cuando es necesario. Esto garantiza que la consola no consuma memoria por menús que no están visibles.

3. Gestión de Recursos y Bus SPI Compartido

Dado que el lector RFID y la pantalla TFT comparten el mismo bus de datos (SPI), el código implementa un control estricto de los Chip Select (CS):

  • Aislamiento de Periféricos: Se han creado funciones de selección (selectScreen, selectRFID) que aseguran que solo un dispositivo escuche el bus a la vez, poniendo el pin CS del otro en estado alto (HIGH) para evitar interferencias.
  • Feedback mediante NeoPixel: Se ha integrado un LED RGB (pin 48) que actúa como indicador de estado por hardware: Amarillo durante la inicialización y Azul cuando el sistema está listo.

4. Capa de Abstracción de Hardware (HAL) Refinada

El código separa la electrónica de la lógica mediante clases dedicadas:

  • Clase Button: Gestiona el debouncing para evitar lecturas erróneas por el ruido mecánico de los botones.
  • Gestión de Energía Asíncrona: Uso de interrupciones software para la implementación de apagar/ encender la consola (explicado en el próximo apartado).

5. El Bucle Principal (loop)

El loop() ha sido reorganizado para seguir una secuencia lógica de alta prioridad en cada iteración:

Bucle de Ejecución: Según el consoleState, se delega el control al método update() y draw() de la pantalla o juego activo, gestionando el refresco visual solo si es necesario para ahorrar tiempo de proceso.

Sincronización de Entradas: Lectura de botones e interruptor de cartucho.

Gestión de Eventos Críticos: Procesamiento de la petición de encendido/apagado y detección de cambios físicos en el cartucho.

Se explicará con más detalle en su apartado correspondiente.

⚡Funcionalidades Básicas: Encender/ Apagar la Consola

La base de la funcionalidad de encender/ apagar la consola se fundamenta en el uso de interrupciones hardware asociadas al botón de encendido. De esta forma se detecta de forma inmediata el cambio en el sistema, independientemente del flujo que esté siguiendo el sistema en un punto determinado.

Esta interrupción se asocia al botón de encendido y, cuando se activa, se ejecuta una rutina de servicio (ISR, Interrupt Service Routine) especificada en el código (en este caso es la función onPowerButton()). Esta rutina únicamente se encarga de modificar el valor de una variable tipo volátil (llamada powerIRQ) que actúa como flag indicando que se ha producido una solicitud de cambio.

Variable volatile

Variable que se modifica dentro de una rutina de servicio que permite leer su valor de forma correcta tanto en la interrupción como en el flujo principal del programa, evitando optimizaciones del compilador que puedan provocar incoherencias

Dentro del bucle loop se procesa el valor de powerIRQ para realizar el cambio de estado de consola (si hay cambio). Por tanto, la interrupción notifica el evento producido y la lógica principal actúa en consecuencia.

Este diseño evita ejecutar lógica pesada dentro de la interrupción (lo cual es una buena práctica en sistemas embebidos), mantiene el control centralizado en el bucle principal y garantiza una transición robusta entre estados.

💾 Funcionalidades Básicas: Reconocer Cartucho

La funcionalidad de reconocimiento de cartucho se basa en la combinación de un interruptor físico de detección y un lector RFID. Esto se ha implementado de esta forma por el problema de compartición de bus que tienen RFID y la pantalla, además de que el propio funcionamiento del RFID dificulta la lectura seguida del mismo ID. De esta forma, el interruptor detecta si hay un cartucho insertado (y cuando se saca) y el RFID sólo que encarga de identificar el juego al que corresponde un cartucho.

En cada iteración del programa principal se comprueba el estado del interruptor. Si este se encuentra presionado y anteriormente no había ningún cartucho insertado, el sistema interpreta que se ha producido una nueva inserción. En ese momento se activa el lector RFID para realizar la lectura del identificador del cartucho (el lector RFID toma control del bus SPI para leer el chip insertado).

Con el identificador obtenido, el sistema recorre la lista de juegos registrados y compara el UID leído con los identificadores almacenados. Si se encuentra una coincidencia, se asigna el juego correspondiente como juego activo, quedando disponible para ser ejecutado desde el menú de la consola.

En caso contrario, si el interruptor deja de estar presionado y había un juego activo, el sistema interpreta que el cartucho ha sido retirado. Entonces, se cancela la referencia al juego activo, se detiene cualquier ejecución asociada si fuera necesario y la consola vuelve al menú principal.

🏠 Menú Principal

El sistema operativo de Retro Boy ha sido diseñado bajo una arquitectura de menús jerárquicos que prioriza la legibilidad y la facilidad de navegación. La interfaz se divide en tres módulos principales accesibles desde el arranque:

1. Gestión Dinámica de Juegos

Esta sección actúa como el punto de entrada principal a la ejecución de software externo.

  • Estado por defecto: Mientras la ranura de expansión esté vacía, la interfaz muestra el mensaje «[SIN JUEGO]».
  • Detección activa: Al introducir un cartucho, el sistema identifica el ID mediante el lector RFID y actualiza automáticamente la etiqueta del menú con el nombre del juego correspondiente. Al pulsar el botón de confirmación, se inicia la transición al bucle de juego.

2. Calendario

Se ha integrado una herramienta de consulta temporal que presenta un calendario estático de marzo de 2026. Esta funcionalidad permite verificar la disposición de los días en pantalla, sirviendo como prueba de renderizado de tablas y fuentes personalizadas en el entorno gráfico de la consola.

3. Panel de Ajustes

El menú de ajustes permite al usuario personalizar la experiencia de uso y gestionar los recursos del hardware de forma directa. Las opciones funcionales implementadas son:

Control de Audio: Permite activar o desactivar el buzzer pasivo. Esta opción modifica una variable global que silencia los efectos de sonido y las melodías de los juegos sin detener la ejecución de la lógica del software.

Gestión de Temas: Permite alternar la interfaz entre modo claro y modo oscuro, ajustando los colores de fondo y de texto para adaptarse a las condiciones lumínicas del entorno.

Localización: El sistema es bilingüe, permitiendo cambiar toda la interfaz de la consola entre español e inglés.

⚙️ Setup y Loop

La función setup() se encarga de la inicialización completa del sistema, configurando todos los componentes hardware y software necesarios para el funcionamiento de la consola: inicialización de la pantalla, el bus de comunicación SPI, el lector RFID, los botones, el sistema de sonido, configuración de los pines de entrada/salida, establecer el estado inicial del sistema y registrar la interrupción asociada al botón de encendido.

Por otro lado, la función loop() constituye el núcleo de ejecución del sistema, y se ejecuta de forma continua durante todo el ciclo de vida de la consola. Su funcionamiento sigue una secuencia estructurada de pasos:

  • 1. Lectura de entradas: se obtienen los estados actuales de los botones y se actualizan en la estructura de datos InputState, que almacena la información de las entradas del usuario en ese instante.
  • 2. Lectura del interruptor de cartucho: se comprueba si hay un cartucho insertado o retirado, actualizando su estado interno.
  • 3. Procesamiento de interrupciones: se verifica si se ha producido una interrupción y se gestiona el cambio de estado correspondiente.
  • 4. Gestión de cambios de cartucho: se detectan inserciones o extracciones de cartuchos y se actúa en consecuencia (explicado anteriormente).
  • 5. Actualización del sistema (si está encendido): si la consola se encuentra en estado activo, se actualiza su comportamiento en función del estado actual y de las entradas del usuario.

La actualización del sistema se realiza aprovechándose de que la consola se ha implementado como una máquina finita de estados, permitiendo transicionar entre estados según los botones pulsados.

Los estados principales en los que se puede encontrar la máquina son:

enum ConsoleState {
  STATE_OFF,
  STATE_MENU,
  STATE_CALENDAR,
  STATE_SETTINGS,
  STATE_WAITING_CART,
  STATE_LOADING_GAME,
  STATE_GAME_RUNNING
};

Este enfoque permite estructurar de forma clara el flujo de ejecución, facilitando la gestión de la lógica del sistema y mejorando su mantenibilidad y escalabilidad. Cada estado encapsula un comportamiento específico, y las transiciones entre ellos garantizan una interacción coherente con el usuario.

🔀 Diagrama de Flujo


🎮 Software: Juegos

🧩 Interfaz IGame

Los juegos de la consola se han implementado siguiendo la interfaz IGame, la cual contiene las cuatro funciones principales que permiten que los juegos funcionen siguiendo la lógica de bucle de la consola.

La interfaz se declara de la siguiente forma:

class IGame {
public:
  virtual void init() = 0;
  virtual void update(const InputState& in) = 0;
  virtual void render(Adafruit_ST7735& tft, SoundManager& sound) = 0;
  virtual void exit() = 0;
  virtual ~IGame() {}
};

A continuación, se explica la funcionalidad esperada de cada una de estas funciones:

  • init(): es la función encargada de preparar el estado inicial del juego inicializando las variables o estructuras de datos necesarias para la correcta ejecución del juego.
  • update(in): es la función encargada de alterar el estado del juego en función de la entrada introducida por el jugador. Estos datos se pasan por parámetro en una estructura de datos llamada InputState que recoge la información de qué botones han sido presionados en esa iteración de la consola.
  • render(tft, sound): es la función encargada de repintar la pantalla (tft) y hacer sonar los sonidos correspondientes (sound) para mostrar la actualización del juego por el cambio de estado ocurrido en update().
  • exit(): es la función encargada de finalizar la ejecución del juego, liberando recursos y cerrando el proceso de forma controlada siempre que sea necesario.

🕹️ Juegos Programados

Color Game es un juego muy básico creado especialmente para comprobar, de forma rápida y sencilla, que toda la lógica de la consola funciona correctamente.

El juego consiste en que, dependiendo de qué botón de la cruceta se pulse, la pantalla se pinta de un color u otro.

El juego de Snake es el juego clásico de la década de los 70, el cual consiste en controlar una serpiente que se mueve por la pantalla comiendo «manzanas» (píxeles rojos). Cada vez que la serpiente come una de estas manzanas, su cuerpo aumenta de tamaño, haciendo más difícil el control de la serpiente.

Objetivo: obtener el máximo número de puntos sin chocar la cabeza con el cuerpo o sin chocarse con los bordes de la pantalla.

De la misma forma que con el Snake, el Tetris es un clásico creado en 1984 que continua siendo juego referente y popular hasta día de hoy. El juego consiste en colocar figuras geométricas en una cuadrícula intentando formar líneas horizontales completas para ganar puntos.

Objetivo: obtener la mayor cantidad de puntos sin que haya piezas que salgan de la cuadrícula.

Memory Game simula el popular juego de Simón Dice, donde el juego genera una secuencia aleatoria de colores y el usuario debe repetirla sin fallar. Esta secuancia se genera paso a paso, es decir, inicialmente muestra un único color y el usuario debe repetirlo. A continuación, se repite la secuencia anterior y se añade un color y el jugador la repite. Así infinitamente hasta que el usuario falle la secuencia.

Objetivo: llegar al máximo número de rondas (longitud de la secuencia) posible.

El juego del Dance Revolution intenta simular el famoso juego arcade Dance Dance Revolution (DDR). El DDR, realmente, es una plataforma de sensores donde la persona debe pisar (físicamente) las flechas del suelo cuando se muestran en la pantalla del arcade. Al llevarlo a una consola, se ha implementado de forma que se deban dar a los botones correspondientes (cruceta, A o B) cuando toquen la línea blanca que se encuentra en la parte inferior de la pantalla. De la misma forma que el DDR, sólo suena la música si se acierta el botón cuando toca la línea y, si se tienen muchos aciertos de seguido, se entra en el bonus combo, lo cual permite ganar más puntos por acierto (si se falla se pierde este bonus).

Objetivo: obtener el mayor número de puntos.

El último juego implementado está inspirado en una de las sagas más conocidas del mundo: Pokémon. En este juego, se simula una batalla Pokémon donde el usuario controla las acciones de un Torchic enfrentándose a un Zigzagoon. La única acción posible es atacar al Pokémon rival con un ataque de los 4 posibles que conoce Torchic (de la misma forma que en los juegos de la saga principal). Cada ataque (seleccionado de forma similar a la implementación del menú de la consola) tiene una potencia distinta y, en cada turno, Torchic realiza el ataque seleccionado, mientras que el Zigzagoon ejecuta 1 de sus 4 posibles ataques seleccionado de forma aleatoria.

Objetivo: ganar la batalla.


🔧 Prototipo

🔥 Soldaduras

Para crear la consola hemos soldado todos los componentes e interruptores en dos PCBs perforadas:

Los botones e interruptores se han soldado directamente en la PCB, pero para conectar los módulos y el microcontrolador a la placa se han soldado distintos headers (macho-hembra) para poder conectar y desconectar estos componentes con facilidad.

Una vez soldados los headers, tocaba soldar los botones y sus respectivas conexiones. Para las conexiones se utiliza un cable blanco rígido, pero también se construyeron dos buses en la PCB que simulaban el bus + y bus – de una protoboard.

Para estos buses se pelo un cable y se cubrio de estaño, de tal manera que cubrieran una serie de agujeros en la placa donde se harían las conexiones a estos buses + y -. Además, a estos dos buses se le soldaron los extremos + y – del portapilas AA, lo que permite que la consola sea portátil:

🦾 Exoesqueleto

Una vez soldado el proyecto, se procedió al diseño y fabricación de una carcasa para el prototipo utilizando los siguientes materiales:

  • Cartón Pluma
  • Cartulina Negra
  • Cola Blanca
  • Silicona

Para su construcción, se utilizaron herramientas de corte como tijeras y cúter, así como instrumentos de medición (regla y lápiz) para asegurar la precisión en las dimensiones.

La carcasa resultante permite guardar el sistema electrónico de forma ordenada y segura. Además, se realizaron aberturas específicas para los distintos componentes, permitiendo su uso una vez las placas de soldadura estén totalmente cubiertas por la infraestructura. Estas aberturas incluyen:

  • Ranura de inserción de cartuchos
  • Puerto USB-C de la placa ESP32
  • Compartimento de las pilas
  • Pantalla
  • Botones

Internamente, se añadieron pequeñas secciones haciendo uso del cartón pluma y la silicona, para fijar los componentes y evitar su movimiento dentro de la caja.

Para la ranura de los cartuchos, se creó un compartimento a base de cartulina negra y fijado en el header del sensor para evitar su movimiento. Este compartimento se diseñó de forma que ayuda a fijar el interruptor al lado del sensor, evitando que se mueva y permitiendo que, al insertar el cartucho, este active el interruptor. La construcción de este compartimento a medida también permite que los cartuchos no se muevan una vez insertados, permitiendo mover la consola libremente sin peligro a que se salga el juego.

Para los cartuchos, se ha utilizado arcilla de secado al aire para crear la forma del cartucho (la cual permite activar el interruptor) y se incrusta sobre ella el chip id del lector RFID para que, al colocarlo, el lector pueda leer el id. A estos chips se les ha quitado la anilla metálica con la que vienen haciendo uso de unos alicates. Posteriormente, la arcilla se pintó usando pintura azul acrílica y una más clara para dibujar un símbolo representativo del juego que activan.

Por último, los botones se han creado utilizando la misma arcilla y conectándolos al pulsador mediante un tubo de plástico.

Quedando el producto final de la siguiente forma:

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *