Hay una diferencia enorme entre una bolita que se desliza suave cuando inclinas la placa y una que salta a tirones. No la produce el código del juego: la produce quien ejecuta ese código. Ese es el punto de este proyecto.

Vas a armar un laberinto de bolita sobre la matriz LED de 8×13 del Arduino UNO Q. Inclinas el board, la bolita rueda, esquivas hoyos que laten y avanzas por tres niveles hasta la meta. No hay que soldar nada, el montaje toma unos minutos y el código completo está en GitHub. Al terminar vas a tener el juego funcionando, vas a saber diseñar tus propios niveles con ocho líneas de texto, y sobre todo vas a entender por qué la lógica corre en el microcontrolador STM32U585 y no en el Linux que la misma placa lleva adentro.

Arduino UNO Q con el cable Qwiic conectado al Modulino Movement y el laberinto corriendo en la matriz LED

El hardware: dos cerebros en una placa

El UNO Q no es un Arduino clásico. Trae dos procesadores que se reparten el trabajo:

  • Un Qualcomm que corre Debian Linux, con todo lo que eso implica: sistema de archivos, red, Python, servidor web.
  • Un STM32U585 (Arm Cortex-M33, hasta 160 MHz, 2 MB de flash y 786 kB de SRAM) que corre Zephyr OS y maneja la electrónica: pines, sensores y la matriz LED.

La expresión "tiempo real" se malinterpreta seguido. No significa "muy rápido", significa "puntual garantizado". El microcontrolador repite su ciclo con un ritmo fijo y nadie se lo interrumpe: no hay un sistema operativo decidiendo que otro proceso es más importante justo ahora.

Para el juego eso se traduce en algo concreto: el ciclo corre fijo a 50 ticks por segundo. En cada tick se lee el acelerómetro, se calcula la física de la bolita y se redibuja la matriz. Siempre, con el mismo espaciado. En Linux, desde el espacio de usuario, esa garantía no existe: la mayoría de las veces funciona, pero cuando el sistema se ocupa algunas vueltas llegan tarde y la bolita pega saltos. En un juego molesta; en el control de un motor o en un lazo de regulación es un problema serio.

Lo que necesitas

  • Un Arduino UNO Q ya configurado y con Arduino App Lab funcionando en tu computador.
  • Un Modulino Movement, la IMU de 6 ejes de Arduino basada en el chip LSM6DSOX.
  • El cable Qwiic que viene en la caja del Modulino.

El Modulino se enchufa en el puerto Qwiic blanco del borde de la placa. Ese puerto cuelga del bus I²C Wire1 y trabaja a 3,3 V: los dos datos importan cuando llegues al código. Deja el módulo plano al lado del board, o pégalo encima con cinta doble contacto. Como lo que mide es la inclinación, tiene que moverse junto con la placa.

Más cableado no hay. Si no tienes el Modulino exacto, más abajo está la ruta alternativa con una IMU I²C genérica.

Crear la app y resolver el error que todos cometen

En Arduino App Lab creas una app nueva con el botón más que está al lado de "Apps".

Diálogo Create new app de Arduino App Lab

No hay que tipear el código completo: está en el repositorio de GitHub del proyecto, dentro de la carpeta labyrinth. Reemplazas el contenido de sketch/sketch.ino con el del repo.

Y acá viene el paso donde se cae casi todo el mundo: hay que agregar la librería Arduino_Modulino a mano. Si te la saltas, la compilación muere con este mensaje:

fatal error: Arduino_Modulino.h: No such file or directory

El sketch.yaml de la app no se puede editar directo desde App Lab (al menos hasta la versión 0.10.0). En vez de eso, haz clic en el ícono de librerías de la barra lateral izquierda, Add Sketch Library, busca Arduino_Modulino y agrégala. App Lab la escribe sola en el perfil de compilación, que después queda así:

YAML
profiles:
  default:
    platforms:
      - platform: arduino:zephyr
    libraries:
      - Arduino_Modulino (0.9.0)
      - STM32duino VL53L4CD (1.0.5)
      - STM32duino VL53L4ED (1.0.1)
      - Arduino_LSM6DSOX (1.1.2)
      - Arduino_LPS22HB (1.0.2)
      - Arduino_HS300x (1.0.0)
      - ArduinoGraphics (1.1.5)
      - Arduino_LTR381RGB (1.0.0)
default_profile: default

Arduino App Lab con la app del laberinto corriendo y la librería Arduino_Modulino agregada

Aprieta Run. La primera compilación descarga las librerías y se demora bastante más que las siguientes. De toda esa lista el juego usa una sola, Arduino_LSM6DSOX, que es el driver de la IMU del Modulino; el resto entra como dependencia de la familia Modulino y queda ahí sin hacer nada.

Bajo el capó: cómo está hecho el juego

Los niveles son ocho líneas de texto

Cada nivel no es más que ocho líneas de 13 caracteres, una por fila de la matriz. # es muro, O es hoyo, S el punto de partida, G la meta y . camino libre:

C++
{
  "S...#........",
  "..O.#..O.#...",
  "....#....#.O.",
  ".#..#.##.#...",
  ".#....#..#...",
  ".#.O..#..#.O.",
  ".#....#......",
  "....O.#....G.",
},

Los tres niveles del laberinto en la matriz LED del Arduino UNO Q: verde el inicio, amarillo la meta, rojo los hoyos y gris los muros

La física, tick a tick

El corazón del juego es el ciclo que corre en el STM32. En cada vuelta lee la aceleración del Modulino, la combina con el roce y mueve la bolita. La posición se guarda con decimales, entre celda y celda, para que el movimiento se vea fluido:

C++
movement.update();
float ax = movement.getX();      // inclinación en g
float ay = movement.getY();

vx = (vx + ax * ACCEL) * FRICTION;   // acelerar + roce
vy = (vy + ay * ACCEL) * FRICTION;

float nx = px + vx;                  // ¿hay muro? entonces rebota
if (isWall((int)(nx + 0.5f), (int)(py + 0.5f))) { vx = -vx * 0.3f; }
else { px = nx; }

Vale la pena detenerse en tres detalles que el código no explica pero que valen oro cuando quieras ajustarlo a tu gusto:

Por qué 50 Hz y no 100 o 25. A 50 ticks por segundo cada vuelta dura 20 ms, que es justo el punto donde el ojo deja de percibir escalones y el LSM6DSOX alcanza a entregar muestras frescas sin apurarse. Si subes la tasa, la bolita no se mueve mejor: solo consumes más muestras del sensor, y como ACCEL está calibrado por tick, la aceleración efectiva se te duplica. Si la bajas a 25, cada paso de la bolita se hace el doble de grande y empieza a atravesar muros delgados, porque la detección de colisión revisa la celda de destino, no el camino recorrido.

FRICTION no es solo roce, es un filtro. La línea vx = (vx + ax * ACCEL) * FRICTION es un filtro pasa bajos de primer orden: la velocidad vieja se multiplica por un número menor que 1 en cada tick, así que decae de forma exponencial. Con FRICTION = 0.9 la velocidad cae a la mitad en unos 7 ticks, o sea 140 ms. Eso es lo que le da al control esa sensación de peso. Si lo subes a 0.98 la bolita queda como sobre hielo, y si lo bajas a 0.7 se siente pegada al piso.

El rebote descuenta mucha más energía de lo que parece. vx = -vx * 0.3f conserva el 30% de la velocidad, pero la energía cinética va con el cuadrado: queda apenas un 9%. Por eso los choques contra los muros se sienten secos, sin rebote elástico. Si buscas un muro que devuelva la bolita, sube ese factor a 0.6 o más.

La matriz tiene ocho niveles de brillo, no dos

Un detalle que casi nadie sabe de la matriz del UNO Q: no es solo prendido y apagado, maneja ocho niveles de brillo por LED. Con matrix.setGrayscaleBits(3) se habilita ese modo y cada elemento del juego recibe su propia intensidad, para que en una pantalla de un solo color igual se distinga todo:

  • Muros: atenuados y siempre encendidos.
  • Hoyos: laten muy débil, visibles pero claramente marcados como peligro.
  • Meta: late con brillo alto.
  • Bolita: brillo máximo.

Primer plano de la matriz LED del Arduino UNO Q con el laberinto corriendo

Cuando la bolita cae en un hoyo parpadea tres veces y vuelve al inicio; cuando llega a la meta recorre la matriz una onda de victoria y se carga el siguiente nivel. Todo el juego (física, colisiones, animaciones y los tres niveles) son alrededor de 200 líneas, y el Cortex-M33 de 160 MHz se aburre: la carga real de cálculo queda muy por debajo del 1% de lo que el chip podría hacer.

Cuando la bolita no hace caso: calibración y fallas típicas

Según cómo quede el Modulino respecto del board, los ejes pueden salir cambiados o espejados. Para eso hay tres constantes al principio del sketch. En el montaje del artículo original hizo falta INVERT_X = -1 para que izquierda fuera de verdad izquierda:

C++
const bool SWAP_AXES = false;  // true: cambia izquierda/derecha por adelante/atrás
const int  INVERT_X  = -1;     // signo del eje izquierda/derecha
const int  INVERT_Y  = 1;      // signo del eje adelante/atrás

En la parte de atrás del Modulino viene impreso el diagrama de ejes, que ayuda bastante a orientarse.

Parte trasera del Modulino Movement con el diagrama de ejes impreso

Cuatro síntomas frecuentes y qué hacer con cada uno:

  • La bolita se mueve al revés de cómo inclinas. Cambia el signo de INVERT_X o de INVERT_Y, según el eje que falle.
  • Inclinas hacia adelante y se mueve hacia el lado. Pon SWAP_AXES = true: el módulo está girado 90 grados respecto del board.
  • La bolita se arranca sola hacia una esquina con la placa quieta. Eso no es un error de ejes, es el offset del acelerómetro. Todo sensor MEMS trae un pequeño sesgo de fábrica. Solución: en el setup(), promedia unas 100 lecturas con el board apoyado y plano, guarda ese promedio y réstaselo después a cada lectura de getX() y getY(). Con eso el cero queda donde tiene que estar.
  • La compilación falla con No such file or directory. Volviste a caer en la librería que falta. Revisa "Add Sketch Library".

Diseña tus propios laberintos

Lo mejor del formato de texto es que armar un nivel nuevo toma dos minutos. Copia uno de los tres bloques del sketch, dibuja tu trazado con #, O y . (ocho líneas de 13 caracteres, exactamente una S y una G) y recarga la app.

Dos consejos que salen de la experiencia: deja los pasillos de al menos una celda de ancho, y recorre cada nivel mentalmente antes de soltarle la bolita. El tercer nivel original tenía en su primera versión un camino que solo pasaba por un hoyo, y esos callejones sin salida aparecen recién cuando sigues el trazado con el dedo.

Variantes y mejoras

El proyecto queda corto a propósito, así que hay harto espacio para seguir. Tres extensiones que valen la pena:

  1. Cronómetro y ranking en la matriz. Cuenta los ticks entre el inicio y la meta y muestra el tiempo al terminar, desplazando los dígitos con ArduinoGraphics (ya viene en el perfil de compilación). Guardar el mejor tiempo en la flash del STM32 te da un récord persistente sin agregar ni un componente.
  2. Niveles con gravedad variable. En vez de leer solo la inclinación, usa también el giroscopio del LSM6DSOX para detectar sacudones y darle a la bolita un impulso extra. Un nivel donde hay que sacudir la placa para saltar un muro cambia por completo la mecánica del juego.
  3. Versión económica portada a Arduino Nano. La lógica del juego no depende del UNO Q: con un Arduino Nano, una IMU MPU-6050 y una matriz 8×8 con MAX7219 armas el mismo laberinto por una fracción del costo. Pierdes la mitad Linux y el ancho de 13 columnas (hay que rediseñar los niveles a 8×8), pero ganas algo interesante: un Nano sin sistema operativo también es determinista, así que la bolita se siente igual de suave. Es un buen experimento para comprobar que la fluidez venía del determinismo, no de la potencia bruta.

Y queda la extensión más obvia, la que el propio autor original deja planteada: hasta acá el lado Debian del UNO Q no hace absolutamente nada. Por el puente entre ambos mundos, el sketch podría avisarle a un script Python cada nivel completado y armar con eso una página web de récords: el juego en tiempo real sobre el M33 y la interfaz web sobre el Qualcomm, todo en una sola placa.

Personalización para Chile

Todo lo necesario para la ruta principal está en el catálogo de MechatronicStore:

  • Arduino UNO Q QRB2210 compatible con Linux Debian (SKU C-222). $91.990 CLP
  • Cable USB Tipo C a USB Tipo A 1mt (SKU B-101). $2.190 CLP

El Modulino Movement no se vende en Chile, y ahí hay que tomar una decisión honesta. El artículo original la deja abierta: cualquier otro acelerómetro I²C conectado a 3,3 V sirve igual, siempre que cambies en el código la consulta al sensor por la librería del chip que uses. La opción local:

  • Giroscopio y Acelerómetro IMU MPU-6050 GY-521 (SKU GO3-4). $4.490 CLP
  • Cables DuPont Hembra Hembra 4 Pines 20cm (SKU C-221). $1.190 CLP

Tres cosas cambian si tomas esa ruta, dichas sin adornos:

  1. El conector. El Modulino trae Qwiic (JST SH de 1,0 mm) y el módulo GY-521 trae pines macho de 2,54 mm. No hay adaptador Qwiic en el catálogo, así que el GY-521 se conecta con los cables DuPont hembra hembra a los pines I²C del header estándar de la placa, respetando alimentación de 3,3 V (no 5 V).
  2. El bus cambia, y el código también. El sketch original habla con Wire1, que es el bus del puerto Qwiic. Si conectas por el header, tienes que apuntar la instancia I²C al bus que corresponda a esos pines.
  3. La librería cambia. Se va Arduino_Modulino con su driver LSM6DSOX y entra una librería de MPU-6050. La dirección I²C por defecto del GY-521 es 0x68 (0x69 si levantas el pin AD0). El LSM6DSOX es un sensor bastante mejor (menos ruido, más opciones de filtrado), pero para leer inclinación en un juego el MPU-6050 cumple de sobra.

Costo aproximado de la ruta chilena completa: cerca de $99.900 CLP.

Si el precio del UNO Q te frena, la variante 3 de la sección anterior (Arduino Nano + MPU-6050 + matriz MAX7219) baja el proyecto a menos de veinte mil pesos, con el costo de reescribir el renderizado de la matriz y comprimir los niveles a 8×8.

Recursos

Versión chilena inspirada en el tutorial de raspberry.tips, con componentes en stock local en MechatronicStore.