Tetris en Arduino y Matriz de LEDs

Desde su creación en 1984, Tetris no es solo un videojuego; es un fenómeno cultural, un desafío a la percepción espacial y un ejercicio de toma de decisiones bajo presión. Llevar este clásico a la vida real, utilizando componentes electrónicos discretos, es el sueño de todo aficionado a la electrónica y la programación. Este proyecto no solo te enseñará sobre la interacción entre software y hardware con la plataforma Arduino, sino que también te sumergirá en el fascinante control de matrices de LEDs y la gestión de entradas de usuario mediante botones.



🔩Funcionamiento

El Cerebro del Sistema: Arduino Uno

El Arduino Uno es la plataforma de microcontrolador de código abierto elegida. Actúa como la Unidad Central de Procesamiento (CPU) de nuestra consola, ejecutando la lógica de juego, gestionando la física de las piezas (caída, rotación y colisión) y orquestando la comunicación con la pantalla y los controles.

  • Microcontrolador: ATmega328P. Este chip maneja el bucle principal (loop()) que constantemente verifica el tiempo de caída, las entradas de los botones y actualiza el display.
  • Pines Digitales: Los pines digitales son cruciales para dos funciones:
    1. Comunicación SPI (Pines 10, 11, 13): Se utilizan para enviar datos a la matriz de LEDs.
    2. Entradas de Botón (Pines 2, 3, 4, 5): Permiten al jugador interactuar con el juego.
  • Memoria (RAM): Aproximadamente 2 KB de SRAM. Esto es un factor limitante, ya que aquí se almacena la matriz de juego interna (board[8][32]), las variables de posición de la pieza y las variables de control del juego.

Importancia en Tetris: El Arduino debe realizar la detección de colisiones y la limpieza de líneas de forma rápida y eficiente para evitar el lag, lo que requiere algoritmos optimizados.

El Chip Controlador MAX7219

El MAX7219 es un controlador de display LED de cátodo común. Su función principal es multiplexar las 64 LEDs de cada matriz 8×8, controlando individualmente cada LED mientras minimiza el número de cables y el consumo de energía del microcontrolador.

  • Comunicación: Utiliza el protocolo SPI (Serial Peripheral Interface). Esto permite controlar hasta 8 dispositivos encadenados (como nuestras 4 matrices) usando solo tres pines de Arduino (DIN, CLK, CS), un ahorro masivo comparado con intentar controlar los 4 x 64 = 256 LEDs directamente.
  • Almacenamiento: Cada MAX7219 tiene un registro de memoria interna que almacena el estado de los 64 píxeles, liberando a la pequeña RAM del Arduino de esta tarea.
  • Encadenamiento (Daisy-Chain): La salida de datos (DOUT) de un MAX7219 se conecta a la entrada (DIN) del siguiente. Esto permite que el Arduino envíe un flujo continuo de 32 bytes de datos (8 bytes por módulo) para actualizar toda la pantalla 32×8.

Los Módulos de Matriz de LEDs 8×8

Cada módulo contiene 64 LEDs y el chip MAX7219. Al encadenar cuatro módulos, obtenemos una gran superficie para visualizar el juego.

Importancia en Tetris: La velocidad de actualización de la matriz debe ser alta para evitar el parpadeo perceptible. El MAX7219 maneja la alta frecuencia de refresco de manera eficiente, asegurando que las piezas en movimiento se vean sólidas y no intermitentes.

PINES del módulo Matriz 8×8 (MAX7219)

Pin módulo Matriz 8×8 (MAX7219)ProtocoloNotas Importantes
VCCAlimentaciónUso de 5V.
GNDTierraConexión de referencia.
CS (Chip Select)SS para SPISelecciona el módulo
DIN (Data In)MOSI para SPIEntrada de datos.
CLK (Clock)SCK para SPSeñal de reloj

🔨Componentes

ComponenteCantidadEspecificaciónFunción
Placa Arduino1Arduino Uno, Nano, etc.Control de componentes(cerebro)
Matriz LED48×8LEDs para mostrar los símbolos
Módulo multiplexor4MAX7219Controlador de matriz LED con interfaz SPI
Botones4PushPara mover/rotar los símbolos
Resistencias410kΩPull-down para los botones
Cables de conexiónnUnir los componentes


🔌Conexiones

Conexión módulo Display 8×8(MAX7219)

MAX7219 PinArduino Pin
VCC5v
GNDGND
DIND11
CSD10
CLKD13

Conexión de los botones(Configuración Pull-Down):

Arduino PinArduino Pin
5vPin 2 (con resistencia de 10kΩ a GND)
5vPin 3 (con resistencia de 10kΩ a GND)
5vPin 4 (con resistencia de 10kΩ a GND)
5vPin 5 (con resistencia de 10kΩ a GND)

Para asegurar una lectura digital estable, los pulsadores se conectan en configuración Pull-Down: un extremo a 5V, el otro extremo al Pin Digital (2, 3,4 o 5) de Arduino y, crucialmente, una resistencia de 10kΩ entre ese mismo pin digital y GND. Esto garantiza que el pin digital lea un estado LOW cuando el botón está abierto y HIGH (5V) solo cuando es presionado.


0️⃣Código

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#include <Arduino.h>

#include <LedControl.h> 

// **********************************************
// 1. CONFIGURACIÓN DE HARDWARE Y CONSTANTES
// **********************************************

// Pines de comunicación LedControl (SPI)
#define DIN_PIN   11 
#define CLK_PIN   13 
#define CS_PIN    10 
#define MAX_DEVICES 4 // Número de módulos 8x8 en cascada

// Pines de Botones (Usando INPUT_PULLUP)
#define ACCELERATE_PIN 2 
#define ROTATE_PIN     3 
#define LEFT_PIN       4 
#define RIGHT_PIN      5 

// Dimensiones de Juego (Lógica de juego: 8 columnas, 32 filas)
#define GRID_W 8
#define GRID_H 32      
#define NUM_PIECE_TYPES 7
#define NUM_ROTATIONS 4

// Constantes de Tiempo y Dirección
#define DROP_NORMAL_MS 750 
#define DROP_ACCEL_MS  50  
#define DEBOUNCE_MS    150 
#define DOWN 0
#define LEFT 1
#define RIGHT 2

// Objeto LedControl
LedControl lc = LedControl(DIN_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);

// Tablero de juego y Variables Globales
int grid[GRID_W][GRID_H]; // Tablero lógico 8x32
int currentPiece, currentRotation, currentX, currentY;
unsigned long lastDropTime = 0;
int currentDropInterval = DROP_NORMAL_MS;
unsigned long lastButtonActionTime = 0;
int score = 0;

// --- Definición de los 7 Tetrominos (I, J, L, O, S, T, Z) ---
// [Tipo][Rotación][Fila][Columna]
const byte PIECES[NUM_PIECE_TYPES][NUM_ROTATIONS][4][4] = {
  // 0. I
  {{{0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,0,1,0}, {0,0,1,0}, {0,0,1,0}}, {{0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,0,1,0}, {0,0,1,0}, {0,0,1,0}}},
  // 1. J 
  {{{1,0,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,1,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0}}, {{0,0,0,0}, {1,1,1,0}, {0,0,1,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,0,0}, {1,1,0,0}, {0,0,0,0}}},
  // 2. L 
  {{{0,0,1,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,0,0}, {0,1,1,0}, {0,0,0,0}}, {{0,0,0,0}, {1,1,1,0}, {1,0,0,0}, {0,0,0,0}}, {{1,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0}}},
  // 3. O 
  {{{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}, {{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}, {{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}, {{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}},
  // 4. S 
  {{{0,1,1,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0}}, {{0,1,1,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0}}},
  // 5. T 
  {{{0,1,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0}}, {{0,0,0,0}, {1,1,1,0}, {0,1,0,0}, {0,0,0,0}}, {{0,1,0,0}, {1,1,0,0}, {0,1,0,0}, {0,0,0,0}}},
  // 6. Z 
  {{{1,1,0,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0}}, {{1,1,0,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0}}}
};


// --- Funciones Prototipo (Declaración Adelantada) ---
void setLedVirtual(int x, int y, bool state);
void initGrid();
void newPiece();
bool checkCollision(int x, int y, int rotation);
void movePiece(int direction);
void rotatePiece();
void lockPiece();
void checkLines();
void updateDisplay();
void handleInput();
void gameOver();

// ================================= SETUP =================================

void setup() {
  randomSeed(analogRead(A0)); 

  // Inicialización de los 4 módulos de la matriz
  for (int i = 0; i < MAX_DEVICES; i++) {
    lc.shutdown(i, false); 
    lc.setIntensity(i, 2); 
    lc.clearDisplay(i);    
  }

  pinMode(ROTATE_PIN, INPUT_PULLUP);
  pinMode(ACCELERATE_PIN, INPUT_PULLUP);
  pinMode(LEFT_PIN, INPUT_PULLUP);
  pinMode(RIGHT_PIN, INPUT_PULLUP);

  initGrid();
  newPiece();
}

// ================================= LOOP =================================

void loop() {
  handleInput();

  unsigned long currentTime = millis();

  if (currentTime - lastDropTime >= currentDropInterval) {
    movePiece(DOWN);
    lastDropTime = currentTime;
  }

  updateDisplay();
}

// ======================= 2. LÓGICA DE JUEGO =======================

void initGrid() {
  for (int x = 0; x < GRID_W; x++) {
    for (int y = 0; y < GRID_H; y++) {
      grid[x][y] = 0;
    }
  }
}

void newPiece() {
  currentPiece = random(0, NUM_PIECE_TYPES);
  currentRotation = 0;
  currentX = GRID_W / 2 - 2; 
  currentY = 0; // Se spawnea en la fila superior (Y=0)

  if (checkCollision(currentX, currentY, currentRotation)) {
    gameOver(); 
  }
}

bool checkCollision(int newX, int newY, int newRotation) {
  const byte (*pieceShape)[4] = PIECES[currentPiece][newRotation];
  
  for (int dy = 0; dy < 4; dy++) {
    for (int dx = 0; dx < 4; dx++) {
      if (pieceShape[dy][dx] == 1) { 
        int gx = newX + dx;
        int gy = newY + dy;

        // 1. Verificar límites
        if (gx < 0 || gx >= GRID_W || gy >= GRID_H) return true;

        // 2. Verificar colisión con piezas fijadas
        if (gy >= 0 && grid[gx][gy] != 0) return true;
      }
    }
  }
  return false;
}

void movePiece(int direction) {
  int newX = currentX;
  int newY = currentY;
  
  if (direction == LEFT) newX--;
  else if (direction == RIGHT) newX++;
  else if (direction == DOWN) newY++;

  if (!checkCollision(newX, newY, currentRotation)) {
    currentX = newX;
    currentY = newY;
  } else if (direction == DOWN) {
    lockPiece();
    checkLines();
    newPiece();
  }
}

void rotatePiece() {
  int newRotation = (currentRotation + 1) % NUM_ROTATIONS;
  if (!checkCollision(currentX, currentY, newRotation)) {
    currentRotation = newRotation;
  }
}

void lockPiece() {
  const byte (*pieceShape)[4] = PIECES[currentPiece][currentRotation];

  for (int dy = 0; dy < 4; dy++) {
    for (int dx = 0; dx < 4; dx++) {
      if (pieceShape[dy][dx] == 1) {
        int gx = currentX + dx;
        int gy = currentY + dy;
        
        if (gy >= 0 && gy < GRID_H) {
          grid[gx][gy] = currentPiece + 1; 
        }
      }
    }
  }
}

void checkLines() {
  int linesCleared = 0;
  
  for (int y = GRID_H - 1; y >= 0; y--) {
    bool isLineFull = true;
    for (int x = 0; x < GRID_W; x++) {
      if (grid[x][y] == 0) {
        isLineFull = false;
        break;
      }
    }

    if (isLineFull) {
      linesCleared++;
      // Mover filas hacia abajo (compactación de memoria)
      for (int moveY = y; moveY > 0; moveY--) {
        for (int x = 0; x < GRID_W; x++) {
          grid[x][moveY] = grid[x][moveY - 1];
        }
      }
      for (int x = 0; x < GRID_W; x++) {
        grid[x][0] = 0;
      }
      y++; 
    }
  }

  if (linesCleared > 0) {
    score += linesCleared * 100;
  }
}

// ======================= 3. CONTROL DE DISPLAY LedControl.h (REAJUSTADO FINAL) =======================

/**
 * @brief Mapea la coordenada virtual (x, y) a la coordenada física (device, row, col).
 * Este mapeo asume:
 * 1. Cableado produce una rotación de 90 grados (X juego -> R física, Y juego -> C física).
 * 2. La caída va de Y=0 (arriba) al Dispositivo 3, y Y=31 (abajo) al Dispositivo 0.
 */
void setLedVirtual(int x, int y, bool state) {
  if (x >= 0 && x < GRID_W && y >= 0 && y < GRID_H) {
    
    // --- 1. Mapeo del Dispositivo (INVERTIDO) ---
    
    // Y=0 (Lógica: Arriba) está en el primer bloque (0-7) de Y.
    // Si el Módulo 3 está físicamente arriba, entonces:
    // device_map = 0 para Y=0..7; device_map = 1 para Y=8..15, etc.
    int device_map = y / 8;     
    
    // Invertimos la asignación: 0 -> 3, 1 -> 2, 2 -> 1, 3 -> 0.
    // Esto fuerza que el Dispositivo 3 (arriba) se use para Y=0.
    int device = MAX_DEVICES - 1 - device_map; 
    
    // --- 2. Mapeo de la Columna y Fila (CORRECCIÓN DE ROTACIÓN) ---

    // La columna X del juego (0-7) se mapea a la Fila física R (0-7) del chip, invertida.
    int row = 7 - x; 
    
    // La fila Y del juego se mapea a la Columna física C del chip (0-7).
    // Usamos el resto de Y para la posición local dentro del módulo.
    int col = y % 8;        

    // LedControl.h: setLed(dispositivo, fila, columna, estado)
    lc.setLed(device, row, col, state);
  }
}

void updateDisplay() {
  // Limpiar toda la pantalla antes de dibujar el nuevo frame
  for(int i = 0; i < MAX_DEVICES; i++) {
    lc.clearDisplay(i);
  }
  
  // 1. Dibujar piezas fijas (grid)
  for (int x = 0; x < GRID_W; x++) {
    for (int y = 0; y < GRID_H; y++) {
      if (grid[x][y] > 0) {
        setLedVirtual(x, y, true);
      }
    }
  }
  
  // 2. Dibujar la pieza cayendo
  const byte (*pieceShape)[4] = PIECES[currentPiece][currentRotation];

  for (int dy = 0; dy < 4; dy++) {
    for (int dx = 0; dx < 4; dx++) {
      if (pieceShape[dy][dx] == 1) {
        setLedVirtual(currentX + dx, currentY + dy, true); 
      }
    }
  }
}

void handleInput() {
  unsigned long currentTime = millis();

  // Control de Caída Rápida (Soft Drop)
  if (digitalRead(ACCELERATE_PIN) == HIGH) {
      currentDropInterval = DROP_ACCEL_MS;
  } else {
      currentDropInterval = DROP_NORMAL_MS;
  }

  // Control de Movimiento y Rotación con Debounce
  if (currentTime - lastButtonActionTime > DEBOUNCE_MS) {
    
    if (digitalRead(ROTATE_PIN) == HIGH) {
      rotatePiece();
      lastButtonActionTime = currentTime;
    }
    else if (digitalRead(LEFT_PIN) == HIGH) {
      movePiece(LEFT);
      lastButtonActionTime = currentTime;
    }
    else if (digitalRead(RIGHT_PIN) == HIGH) {
      movePiece(RIGHT);
      lastButtonActionTime = currentTime;
    }
  }
}

void gameOver() {
  // Animación de Game Over: Llenado de la pantalla
  for (int y = GRID_H - 1; y >= 0; y--) {
    for (int x = 0; x < GRID_W; x++) {
      setLedVirtual(x, y, true);
      delay(20);
    }
  }
  
  // Parpadeo final
  while(true) {
    for (int i = 0; i < MAX_DEVICES; i++) lc.setIntensity(i, 0); 
    delay(300);
    for (int i = 0; i < MAX_DEVICES; i++) lc.setIntensity(i, 15); 
    delay(300);
  }
}


🖌️Diseños


🎬Videos


📑Conclusión

La creación del juego Tetris utilizando la plataforma Arduino y la matriz de LEDs 32×8 MAX7219 es una demostración excepcional de cómo la programación de bajo nivel se fusiona con la manipulación directa del hardware para recrear un clásico atemporal.

Puntos Clave de la Ingeniería del Proyecto

  • Optimización de Recursos: Este proyecto ilustra la necesidad de una programación eficiente. La memoria limitada del Arduino obliga a utilizar estructuras de datos concisas (la matriz de juego 8×32) y algoritmos rápidos para la detección de colisiones y la rotación de figuras.
  • Dominio de la Comunicación Serial: El uso del chip MAX7219 y el protocolo SPI es fundamental. Permite al microcontrolador gestionar 256 LEDs (la matriz 32×8) utilizando solo tres pines, demostrando la eficacia de la multiplexación en la electrónica moderna.
  • Física de Juego: La lógica de Tetris se reduce a ecuaciones de movimiento (X ± 1, Y + 1) y geometría matricial. El sistema debe coordinar de manera precisa la temporización de la caída con la respuesta instantánea a las entradas del usuario (botones), un desafío crucial resuelto mediante el control del intervaloCaida y la implementación de debounce.

Este proyecto trasciende el mero pasatiempo; es una prueba de concepto que valida la capacidad de las plataformas open source como Arduino para actuar como controladores de sistemas complejos. Al construir un juego, el desarrollador aprende no solo a encender LEDs, sino a gestionar el tiempo, el espacio y la interacción en un bucle continuo de procesamiento. El Tetris en 32×8 es, en esencia, una CPU, una GPU y un sistema de entrada reducidos a sus componentes más esenciales.


Deja un comentario

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

Carrito de compra