Una más de Conway (¿la última?, no lo sé Rick...)
En los dos posteos previos, ya entramos de lleno en el Juego de la Vida de Conway:
Primero arrancamos desde cero con cuadernos interactivos y una versión para la consola en El Juego de la Vida de Conway en Python: una guía paso a paso (Writeups + CLI).
Después dimos el salto visual a la interfaz gráfica con PyGame, usando controles y selector de patrones en De la terminal a una ventana: el Juego de la Vida de Conway con Pygame.
En este caso, ver cobrar vida al Juego de la Vida de Conway en una matriz LED 8×8 sobre la mesa de trabajo combina dos satisfacciones: la de ver la teoría convertida en pequeños puntos luminosos reales (aunque ojalá hubiera tenido a mano una matriz más grande) y la excusa perfecta para aprender a usar PlatformIO, un entorno con el que nunca antes había trabajado.
PlatformIO es un ecosistema de desarrollo de código abierto diseñado para simplificar la programación de sistemas embebidos y proporcionar un entorno de trabajo común para múltiples arquitecturas de hardware. Integrado habitualmente en Visual Studio Code o CLion, ofrece una alternativa más flexible a los IDE específicos de cada fabricante. -- Fuente: kensocircuits.com
Así nace la tercera parte y cierre (?) de esta trilogía: el Juego de la Vida implementado en un microcontrolador Arduino Nano con una matriz LED de 8×8 controlada por el chip MAX7219, sumándole además un pulsador físico con detección de pulsación corta y larga para interactuar con la simulación en tiempo real.
El Juego de la Vida
"Se trata de un juego de cero jugadores, lo que quiere decir que su evolución está determinada por el estado inicial y no necesita ninguna entrada de datos posterior. El 'tablero de juego' es una malla plana formada por cuadrados (las 'células') que se extiende por el infinito en todas las direcciones..."
— Fuente: Wikipedia
Más información en las dos entradas anteriores arriba citadas.
En nuestro caso, el tablero es una cuadrícula finita de 8×8 celdas (64 células en total), donde cada LED encendido representa una célula viva y cada LED apagado una célula muerta. Cada célula observa a sus 8 vecinas (arriba, abajo, lados y diagonales) y su destino en la siguiente generación se rige por las tres reglas que definen al juego: Supervivencia, nacimiento y muerte.
El Hardware: Componentes necesarios
Para montar este proyecto necesitamos muy pocas cosas y todas son súper accesibles:
- 1x Arduino Nano (con microcontrolador ATmega328P) o el que tengan a mano...
- 1x Módulo de matriz LED 8×8 con MAX7219 (el clásico módulo rojo o azul que viene con 5 pines de entrada).
- 1x Pulsador tipo botón (push button)
- 1x Protoboard y cables de conexión.
¿Qué hace el MAX7219?
El MAX7219 es un controlador serie integrado que se encarga de multiplexar y controlar los 64 LEDs de la matriz utilizando apenas 3 líneas digitales del microcontrolador (Datos, Reloj y Selección de chip). Esto nos ahorra tener que manejar individualmente decenas de pines o lidiar con resistencias limitadoras por cada LED.
Conexiones y diagrama de cableado
El circuito es sumamente sencillo. Lo mejor de todo es que no necesitamos resistencias pull-up externas para el pulsador, ya que aprovechamos las resistencias pull-up internas que incluye el ATmega328P del Arduino.
| Pin Arduino | Módulo / Componente | Función / Descripción |
|---|---|---|
| D11 | MAX7219: DIN | Transmisión de datos serie (Data In) |
| D13 | MAX7219: CLK | Señal de reloj de sincronía (Clock) |
| D10 | MAX7219: CS (o LOAD) | Selección de chip (Chip Select / Carga de datos) |
| 5V | MAX7219: VCC | Alimentación de 5V |
| GND | MAX7219: GND y Pulsador (un terminal) | Tierra común compartida |
| D2 | Pulsador (el otro terminal) | Entrada digital con INPUT_PULLUP interno |
Detalle técnico sobre el generador aleatorio:
En el código verán la línearandomSeed(analogRead(A0));. El pin analógicoA0se deja intencionalmente desconectado (flotante). Al leer el ruido electromagnético ambiental del ambiente a través de ese pin analógico, generamos una semilla inicial verdaderamente caótica para que cada reinicio de la placa ofrezca un tablero aleatorio distinto.
¿Por qué PlatformIO en lugar del clásico Arduino IDE?
PlatformIO nos brinda ventajas enormes:
Gestión automática de librerías: No tenés que andar descargando archivos ZIP ni buscando en menús manuales; las dependencias se declaran en el archivo
platformio.iniy se descargan solas al compilar.Compilación y subida reproducible: Cualquier persona que clone el repositorio puede compilar exactamente la misma versión sin preocuparse por diferencias de entorno.
Hace tiempo ya había publicado una entrada respecto a la modularización del código en el IDE de Arduino en: Separando archivos en Arduino... Mientras que en el entorno clásico de Arduino separar el código en varias pestañas es casi opcional, en PlatformIO esto se vuelve prácticamente indispensable y mucho más natural al estructurar el proyecto en carpetas. Pero la gran ventaja no se queda solo ahí: trabajar con VS Code te da un autocompletado que el IDE de Arduino no tiene y una compilación notablemente más rápida (al menos eso me pareció a mí)
Nuestro platformio.ini queda así de simple:
[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino
monitor_speed = 9600
lib_deps =
wayoda/LedControl@^1.0.6
lennarthennigs/Button2@^2.7.0
Estructura modular
Siguiendo la misma filosofía de diseño que aplicamos en las versiones de terminal y Pygame, dividimos las responsabilidades en módulos independientes:
.
├── include/
│ ├── display.h # Prototipos para inicializar y actualizar la matriz LED
│ └── logica.h # Estructura Matriz (8x8) y funciones de evolución celular
├── src/
│ ├── display.cpp # Control del chip MAX7219 mediante la librería LedControl
│ ├── logica.cpp # Reglas de Conway, cálculo de vecinos y preset oscilador
│ └── main.cpp # Inicialización, temporización con millis() y gestión del botón
└── platformio.ini # Configuración de hardware y librerías
1. El motor de Conway (include/logica.h y src/logica.cpp)
En microcontroladores con recursos limitados (el ATmega328P cuenta con solo 2 KB de SRAM). Definimos una estructura compacta:
struct Matriz {
byte celdas[8][8];
};
Para calcular el paso siguiente, la función siguiente_generacion(matriz) crea una nueva instancia temporal, asegurando que el cálculo de cada celda se realice sobre el estado original simultáneo sin alterar las lecturas de sus vecinas durante el barrido.
El cálculo de vecinos recorre un array de desplazamientos relativos direcciones[8] e implementa bordes finitos: si el vecino cae fuera del rango 0 a 7, simplemente se ignora.
Además, incluimos una función generar_preset() que carga un patrón oscilador diseñado a medida para 8×8 celdas, el cual cicla de forma indefinida con un período de 3 generaciones:
Matriz generar_preset() {
Matriz matriz = {{{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 1, 1},
{0, 0, 0, 1, 0, 1, 0, 1},
{0, 0, 0, 0, 0, 1, 1, 0}}};
return matriz;
}
2. El control de la matriz (src/display.cpp)
Usamos la librería LedControl para comunicarnos con el MAX7219. Al iniciar, sacamos al chip del modo de reposo (shutdown), ajustamos una intensidad lumínica adecuada y limpiamos el buffer.
La función mostrar_matriz recorre la matriz bidimensional y setea cada LED con led_control.setLed(0, fila, col, estadoLed).
3. El ciclo principal y el botón interactivo (src/main.cpp)
En lugar de usar la función bloqueante delay(), la evolución de las generaciones se controla comparando los milisegundos transcurridos con millis(). Esto permite que el procesador esté libre continuamente para responder de forma instantánea a las acciones del usuario.
Para manejar el pulsador utilizamos la excelente librería Button2, la cual resuelve de forma automática el rebote mecánico (debounce) y nos permite registrar callbacks para diferentes tipos de pulsación:
- Pulsación corta (Click simple): Genera un nuevo tablero aleatorio y reinicia el contador de generaciones a 0.
- Pulsación larga (Presión sostenida > 800 ms): Carga el preset oscilador de período 3 y reinicia el contador a 0.
Al mismo tiempo, el puerto serie informa cada generación que avanza y notifica cuándo se dispara una acción del botón:
Generación: 0
Generación: 1
Generación: 2
...
Nueva matriz generada
Generación: 0
Generación: 1
...
Preset cargado
Generación: 0
Cómo compilar y subir el código
Si querés probarlo en tu placa, el procedimiento con PlatformIO Core es sumamente directo:
Cloná el repositorio:
git clone https://github.com/mcattani/conway_arduino.git cd conway_arduinoCompilá el proyecto:
pio runConectá el Arduino Nano por USB y subí el firmware:
pio run --target upload(Si tenés varias placas conectadas o estás en Linux y querés especificar el puerto, podés usar
--upload-port /dev/ttyUSB0o/dev/ttyACM0).Abrí el monitor serie para seguir la simulación:
pio device monitor --baud 9600
Conclusiones de la trilogía
Llegar a este punto ha sido un recorrido sumamente interesante:
- Arrancamos desarmando las matemáticas del algoritmo y aprendiendo a calcular vecinos en Python puro.
- Construimos una ventana interactiva con Pygame para disfrutarlo con controles gráficos y presets históricos.
- Y finalmente lo condensamos en un microcontrolador de 8 bits para verlo correr de forma autónoma en una placa física con LEDs reales.
El código fuente completo, esquemas y archivos de configuración están disponibles en GitHub: https://github.com/mcattani/conway_arduino
Si tenés una placa Arduino y una matriz LED dando vueltas, te invito a armarlo y contarme qué te pareció en los comentarios. ¿Qué otros patrones o mejoras le sumarías a esta versión de 8×8? ¡Los leo!
Y si te gustó este proyecto y querés apoyar el contenido del blog para que sigan saliendo nuevos proyectos con código y circuitos, podés invitarme un cafecito:








