¿Alguna vez has jugado a escribir tu nombre en el aire con una bengala o una linterna en la oscuridad? Aunque la luz está en un solo punto a la vez, tu cerebro ve una línea completa.
Este fenómeno se llama Persistencia de la Visión (POV), y consiste en fusionar imágenes rápidas en una sensación continua. La retina humana retiene una imagen durante aproximadamente 1/10 a 1/25 de segundo. Si algo se mueve más rápido que eso, nuestro cerebro une las imágenes discretas.
En este proyecto vamos a construir un Display POV. Usaremos una sola fila de LEDs (una dimensión) y, al moverla rápidamente, crearemos una pantalla virtual de dos dimensiones en el aire.
Este proyecto es una clase magistral sobre tiempos de ejecución. Aquí delay(100) es una eternidad. Trabajaremos en el orden de los microsegundos.
Cambiar espacio por tiempo
En una pantalla normal (como la de tu móvil), tienes una matriz de píxeles que se encienden todos a la vez. En un display POV, no tenemos el eje X. Solo tenemos el eje Y (nuestra fila de LEDs).
Para generar el eje X, aprovechamos el movimiento.
- En el instante
t = 0, encendemos la primera columna de la imagen. - Movemos la mano (o el motor gira).
- En el instante
t = Δt, encendemos la segunda columna. - …
Si lo hacemos lo suficientemente rápido, el cerebro reconstruye la matriz completa.
Esto significa que debemos “refrescar” la imagen completa al menos 25 veces por segundo para que no parpadee demasiado.
Componentes
- [1x|blue] Arduino Nano | O Uno (Nano mejor por tamaño)
- [8x|green] LEDs | Rojos o azules de alta luminosidad
- [8x|gray] Resistencia 220Ω o 330Ω | Limitan corriente de LEDs
- [1x|red] Powerbank de 5V o batería con regulador | Alimentación portátil
- [Varios|gray] Placa de prototipos pequeña | O soldada directamente
Para este montaje buscaremos la compacidad. Un Arduino Uno es un poco grande para agitarlo con la mano, así que si tenéis un Arduino Nano, es el momento de usarlo. Si no, el Uno servirá igual.
Esquema de conexión
Vamos a conectar los 8 LEDs a 8 pines digitales consecutivos. Esto es crucial para simplificar la programación (y optimizarla si quisiéramos usar manipulación directa de puertos).
Usaremos los pines del D2 al D9.
Fila de LEDs
Conectamos los ánodos a los pines mediante resistencias y unimos todos los cátodos a GND.
[ { “from”: “LED 1 (Arriba) + resistencia”, “to”: “Pin 9”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 2 + resistencia”, “to”: “Pin 8”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 3 + resistencia”, “to”: “Pin 7”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 4 + resistencia”, “to”: “Pin 6”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 5 + resistencia”, “to”: “Pin 5”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 6 + resistencia”, “to”: “Pin 4”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 7 + resistencia”, “to”: “Pin 3”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “LED 8 (Abajo) + resistencia”, “to”: “Pin 2”, “color”: “red”, “note”: “Cátodo a GND” }, { “from”: “GND común (todos LEDs)”, “to”: “GND Arduino”, “color”: “black” } ]
El orden importa. Si los conectas al revés, tu texto saldrá boca abajo o en espejo.
Dibujar con unos y ceros
Aquí es donde la programación se vuelve visual. Para definir qué LEDs se encienden en cada columna, lo más cómodo es usar sistema binario.
Imagina que quieres dibujar una letra “I”.
- Columna 1: Base (ej:
0b10000001) - Columna 2: Base (ej:
0b10000001) - Columna 3: Palo vertical completo (ej:
0b11111111) - Columna 4: Base (ej:
0b10000001) - Columna 5: Base (ej:
0b10000001)
Al usar 0b, podemos “ver” el dibujo directamente en el código. Un 1 es LED encendido, un 0 es apagado.
Definición de la imagen
Vamos a dibujar un Corazón, que es más fácil de reconocer que el texto si la velocidad no es constante.
// Pines de los LEDs (del D2 al D9)
// Nota: El orden en el array define cuál es el bit más significativo
const int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9};
// --- EL DIBUJO ---
// Cada byte representa una COLUMNA vertical de la imagen.
// El array se lee de izquierda a derecha.
// 1 = LED Encendido, 0 = LED Apagado.
byte imagen[] = {
0b00000000, // Espacio vacío
0b00000000,
0b00011000, // **
0b00111100, // ****
0b01111110, // ******
0b11111111, // ******** (Centro del corazón)
0b11111111, // ********
0b01111110, // ******
0b00111100, // ****
0b00011000, // **
0b00000000,
0b00000000
};
int largoImagen = sizeof(imagen);
int tiempoPorColumna = 2; // Milisegundos que dura cada columna encendida
setup
Configuramos los pines como salida.
void setup() {
for (int i = 0; i < 8; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW); // Empezamos apagados
}
}
Función de pintado
Necesitamos una función que tome un byte (ej: 10010001) y encienda los LEDs correspondientes.
void mostrarColumna(byte datos) {
for (int i = 0; i < 8; i++) {
// Leemos el bit 'i' de nuestro byte de datos
// bitRead es una función muy útil de Arduino para esto
int estado = bitRead(datos, i);
digitalWrite(ledPins[i], estado);
}
}
Bucle principal
El bucle simplemente recorre el array de la imagen una y otra vez.
void loop() {
// Recorremos cada columna de nuestra imagen
for (int i = 0; i < largoImagen; i++) {
mostrarColumna(imagen[i]);
// Esta pausa define el ANCHO del píxel.
// Si te mueves rápido, necesitas un delay pequeño.
// Si te mueves lento, necesitas un delay mayor.
delay(tiempoPorColumna);
}
// Apagamos todo entre repeticiones para que no se mezcle el final con el inicio
mostrarColumna(0);
delay(10); // Espacio negro entre "frames"
}
Probar el montaje
- Carga el código.
- Desconecta el USB y conecta una alimentación portátil de 5 V con el montaje ya aislado y sujeto.
- Mueve el conjunto de lado a lado sin acercarlo a personas, objetos ni cables.
- Deberías ver un corazón rojo flotando en el aire.
Si ves el corazón “estirado”, reduce el tiempoPorColumna.
Si lo ves “comprimido” o muy estrecho, aumenta el tiempoPorColumna o agita la mano más despacio.
Manipulación directa de puertos
Si intentas escribir textos largos o imágenes complejas, notarás que ocho llamadas a digitalWrite() introducen una separación temporal entre LED. A velocidades altas, esto puede deformar ligeramente cada columna.
Para solucionarlo, podemos usar los Registros de Puerto. En el Arduino Uno/Nano, los pines D0 a D7 pertenecen al puerto PORTD.
Una escritura de puerto reduce mucho esa latencia, aunque depende del microcontrolador y obliga a tratar todos los bits del registro con cuidado:
void mostrarColumnaRapido(byte datos) {
// Escribimos directamente en el registro del Puerto D
// OJO: Los pines 0 y 1 son RX/TX. Si escribimos ahí podemos cortar la comunicación Serial.
// Lo ideal es usar PORTB (Pines 8-13) o tener cuidado con la máscara de bits.
PORTD = datos;
}
Este fragmento no sirve con el cableado D2-D9 del ejemplo: habría que rediseñar las conexiones y preservar los bits de los pines que no pertenezcan al display. En un Uno, escribir PORTD completo también afecta a D0 y D1, usados por el puerto serie.
Evolución del proyecto
La varita POV que agitas con la mano es el nivel 1. ¿Cómo lo convertimos en un proyecto de ingeniería?
- POV rotatorio: Solo debe construirse con un rotor equilibrado, una carcasa resistente y una fijación calculada para la velocidad de giro. Una placa o una batería desprendida puede causar lesiones graves.
- Sincronización (Sensor Hall): Al girar con un motor, la imagen rotará y se moverá sola. Necesitas un punto de referencia fijo. Coloca un imán en la base y un sensor de efecto Hall en la parte giratoria. Cada vez que el sensor detecta el imán, el Arduino sabe que ha completado una vuelta y reinicia el dibujo en el mismo punto exacto. Así la imagen se queda “quieta” en el aire.
- Texto Dinámico: Añade un módulo Bluetooth. Podrías enviar desde tu móvil “HOLA” y que el Arduino convierta esas letras en el mapa de bits correspondiente para mostrarlo.