Principio Central de la Animación de Arranque
La esencia de la animación de arranque es utilizar el efecto de persistencia retiniana del ojo humano. Al mostrar continuamente imágenes estáticas (fotogramas) se genera la ilusión de "movimiento". Se deben dominar dos conceptos clave:
Persistencia Retiniana
- Definición: Cuando la señal luminosa deja de estimular el ojo, la imagen visual en la retina no desaparece de inmediato, sino que se retiene entre 0,1 y 0,4 segundos. Este es el principio subyacente de animaciones, películas, televisión y otros medios visuales dinámicos.
- Aplicaciones históricas:
- La "linterna giratoria" de la dinastía Song China: al girar el cuerpo de la linterna, las figuras pintadas aparecían secuencialmente; fue la primera aplicación de la persistencia retiniana.
- El "taumatropo" de Paul Roget (1828): un disco con un pájaro en un lado y una jaula vacía en el otro; al girar, el pájaro parecía estar dentro de la jaula, demostrando la persistencia retiniana.
Tasa de Fotogramas (FPS): El "Interruptor de Fluidez"
- Definición: Número de fotogramas estáticos mostrados por segundo (Frames Per Second, FPS), también llamada frecuencia de refresco, medida en Hertz (Hz) (1 Hz = 1 fotograma/segundo).
- Requisitos de fluidez:
- Mínimo: 24 FPS (umbral perceptible de movimiento continuo; estándar en cine).
- Frecuentes: 30 FPS (común en video), 50 FPS (usado en animaciones de arranque para mayor suavidad, sin tirones).
- Cálculo de intervalo entre fotogramas: Intervalo = 1 segundo / FPS. Ejemplo: 50 FPS → 20 ms por fotograma, controlado con
usleep(20 * 1000)(unidad en microsegundos, 1 ms = 1000 µs).
Preparación del Material para la Animación
Primero hay que descomponer el contenido dinámico (GIF, video) en fotogramas JPEG estáticos secuenciales. Pasos:
Herramienta Clave: Divisor/Recortador de GIF
Herramientas comunes: ScreenToGif, GIFtiqu 2014, GIMP (código abierto), herramientas online (ej. ezgif.com). Aquí se usa "GIFtiqu 2014" como ejemplo.
Pasos para Dividir el Material
- Importar material dinámico: Abrir la herramienta e importar el GIF original (o el archivo convertido de video a GIF).
- Configurar parámetros de división:
- Formato de salida: elegir JPEG (alta compresión, ahorra almacenamiento en la placa de desarrollo y compatible con libjpeg).

- Resolución: debe coincidir con la pantalla LCD (por ejemplo, LCD 800×480 → fotogramas 800×480, evitando deformaciones o desbordes; si los fotogramas son más pequeños, se puede centrar en el programa).
- Rango de fotogramas: conservar todos (para animación completa).
- Formato de salida: elegir JPEG (alta compresión, ahorra almacenamiento en la placa de desarrollo y compatible con libjpeg).
- Nombrar los archivos en lote: la herramienta nombra automáticamente según el orden:
FrameX.jpg(X empieza en 0: Frame0.jpg, Frame1.jpg, …, Frame81.jpg, total 82 fotogramas).

- Exportar los fotogramas: colocar todos en una carpeta
gif(ej.proyecto/gif/).
Consideraciones al Preparar el Material
- No alterar el orden: si los nombres se desordenan (ej. Frame5.jpg antes que Frame4.jpg), la animación se verá entrecortada o invertida.
- Evitar demasiados fotogramas: si superan los 100, ocupan mucho almacenamiento; se recomienda mantener por debajo de 100 (a 50 FPS reproduce 2 segundos, suficiente para arranque).
- Calidad de imagen moderada: compresión JPEG entre 70% y 80% (equilibrio entre calidad y tamaño; cada fotograma < 100 KB).
Programa Completo para la Animación de Arranque
Lógica central: Abrir el dispositivo LCD → Mapear la memoria de vídeo LCD en espacio de usuario → Decodificar fotogramas JPEG → Escribir los datos en la memoria LCD → Controlar la tasa de fotogramas → Reproducir en bucle. Se resuelven tres grandes problemas: operación del LCD, decodificación JPEG y control de FPS. Código completo corregido (con comentarios detallados).
Dependencias y Archivos de Cabecera
Incluir cabeceras para LCD (fcntl.h, sys/mman.h), decodificación JPEG (jpeglib.h), retardo (unistd.h):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // para open()
#include <sys/mman.h> // para mmap()
#include <unistd.h> // para usleep()
#include <jpeglib.h> // decodificación JPEG (requiere libjpeg)
// Variable global: puntero a la memoria mapeada del LCD (espacio de usuario)
unsigned int *lcd_buffer = NULL;
// Configuración de la resolución LCD (ajustar según la pantalla real; aquí 800x480)
#define LCD_ANCHO 800
#define LCD_ALTO 480
#define LCD_BPP 4 // cada píxel ocupa 4 bytes (formato ARGB: A=alfa, R=rojo, G=verde, B=azul)
#define LCD_TAMANO LCD_ANCHO * LCD_ALTO * LCD_BPP
Función Clave 1: Decodificar JPEG y Mostrar en LCD
Se reutiliza la función decodificar_jpeg para obtener datos RGB, luego mostrar_jpeg_en_lcd convierte a ARGB y escribe en coordenadas (x, y).
/**
* @brief Decodifica un archivo JPEG a datos RGB.
* @param ruta Ruta al archivo JPEG (ej. "./gif/Frame0.jpg")
* @param ancho Puntero para devolver el ancho de la imagen decodificada (píxeles)
* @param alto Puntero para devolver la altura
* @return Puntero a datos RGB (3 bytes por píxel: R→G→B) o NULL en error. Debe liberarse con free().
*/
unsigned char* decodificar_jpeg(const char* ruta, int* ancho, int* alto) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
FILE* archivo = fopen(ruta, "rb");
if (!archivo) {
fprintf(stderr, "Error: no se pudo abrir %s\n", ruta);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
jpeg_stdio_src(&cinfo, archivo);
jpeg_read_header(&cinfo, TRUE);
*ancho = cinfo.image_width;
*alto = cinfo.image_height;
cinfo.out_color_space = JCS_RGB;
cinfo.output_components = 3;
jpeg_start_decompress(&cinfo);
int stride = *ancho * 3; // bytes por fila
unsigned char* linea_buffer[1] = {malloc(stride)};
unsigned char* datos_rgb = malloc(*alto * stride);
if (!linea_buffer[0] || !datos_rgb) {
fprintf(stderr, "Error: falló asignación de memoria\n");
free(linea_buffer[0]);
free(datos_rgb);
jpeg_abort_decompress(&cinfo);
fclose(archivo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
int fila = 0;
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, linea_buffer, 1);
memcpy(datos_rgb + fila * stride, linea_buffer[0], stride);
fila++;
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(archivo);
free(linea_buffer[0]);
return datos_rgb;
}
/**
* @brief Muestra un archivo JPEG en la pantalla LCD en la posición (x, y).
* @param ruta Ruta al archivo JPEG
* @param x Coordenada X superior izquierda (origen en (0,0) esquina superior izquierda)
* @param y Coordenada Y
* @return 0 si éxito, -1 en error (archivo no encontrado, decodificación fallida, desbordamiento)
*/
int mostrar_jpeg_en_lcd(const char* ruta, int x, int y) {
int img_ancho, img_alto;
unsigned char* datos_rgb = decodificar_jpeg(ruta, &img_ancho, &img_alto);
if (!datos_rgb) return -1;
// Verificar desbordamiento
if (x + img_ancho > LCD_ANCHO || y + img_alto > LCD_ALTO) {
fprintf(stderr, "Error: la imagen desborda la pantalla LCD (%dx%d), imagen (%dx%d), posición (%d,%d)\n",
LCD_ANCHO, LCD_ALTO, img_ancho, img_alto, x, y);
free(datos_rgb);
return -1;
}
unsigned char R, G, B;
int lcd_x, lcd_y;
int idx_rgb;
for (lcd_y = y; lcd_y < y + img_alto; ++lcd_y) {
for (lcd_x = x; lcd_x < x + img_ancho; ++lcd_x) {
idx_rgb = (lcd_y - y) * img_ancho * 3 + (lcd_x - x) * 3;
R = datos_rgb[idx_rgb + 0];
G = datos_rgb[idx_rgb + 1];
B = datos_rgb[idx_rgb + 2];
// Formato ARGB: alfa = 0xFF (opaco), luego R, G, B
lcd_buffer[lcd_y * LCD_ANCHO + lcd_x] = 0xFF000000 | (R << 16) | (G << 8) | B;
}
}
free(datos_rgb);
return 0;
}
Función Clave 2: Inicialización del LCD (Apertura + mmap)
El LCD se maneja a través del nodo de dispositivo framebuffer (/dev/fb0). Se abre y se mapea la memoria de vídeo física al espacio de usuario (evita múltiples read/write, mejora eficiencia):
/**
* @brief Inicializa el LCD: abre el dispositivo y mapea la memoria.
* @return Descriptor de archivo del LCD (int) en éxito, -1 en error.
*/
int inicializar_lcd() {
int fd_lcd = open("/dev/fb0", O_RDWR);
if (fd_lcd == -1) {
perror("Error: no se pudo abrir el LCD");
return -1;
}
lcd_buffer = (unsigned int*)mmap(NULL, LCD_TAMANO,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd_lcd, 0);
if (lcd_buffer == MAP_FAILED) {
perror("Error: falló mmap del LCD");
close(fd_lcd);
return -1;
}
printf("LCD inicializado: dirección de mapeo = %p, tamaño de memoria = %d bytes\n",
lcd_buffer, LCD_TAMANO);
return fd_lcd;
}
Función Principal: Lógica de la Animación de Arranque
Implementa: "Inicializar LCD → Reproducir fotogramas JPEG en bucle → Controlar FPS → (opcional) liberar recursos".
int main(int argc, char const *argv[]) {
int fd_lcd = inicializar_lcd();
if (fd_lcd == -1) {
fprintf(stderr, "Fallo al inicializar LCD. Saliendo.\n");
return -1;
}
char ruta_fotograma[128] = {0};
int total_fotogramas = 82; // Frame0.jpg … Frame81.jpg
int intervalo_ms = 20; // 20 ms → 50 FPS
printf("Reproduciendo animación de arranque (%d fotogramas, %d FPS)...\n",
total_fotogramas, 1000 / intervalo_ms);
while (1) { // Bucle infinito: se reproduce continuamente
for (int i = 0; i < total_fotogramas; ++i) {
snprintf(ruta_fotograma, sizeof(ruta_fotograma),
"./gif/Frame%d.jpg", i);
if (mostrar_jpeg_en_lcd(ruta_fotograma, 0, 0) == -1) {
fprintf(stderr, "Error al mostrar fotograma %d, se omite.\n", i);
}
usleep(intervalo_ms * 1000); // espera en microsegundos
}
}
// Código de limpieza (no se alcanza normalmente, salvo interrupción)
// munmap(lcd_buffer, LCD_TAMANO);
// close(fd_lcd);
// printf("Animación terminada.\n");
// return 0;
}
Descarga, Depuración y Solución de Problemas
Una vez escrito el programa, hay que transferir el ejecutable, los fotogramas JPEG y las bibliotecas a la placa de desarrollo.
Transferencia de Archivos a la Placa
-
Problema:
tftpno transfiere carpetas (la carpetagifcontiene 82 JPEG). Se usascp(basado en SSH). -
Pasos (ejemplo: host Linux → placa ARM):
- Transferir carpeta
gif: (-rrecursivo,rootusuario,192.168.1.100IP placa,/root/proyecto/destino)
scp -r ./proyecto/gif root@192.168.1.100:/root/proyecto/- Transferir ejecutable compilado (ej.
animacion_arranque):
scp ./proyecto/animacion_arranque root@192.168.1.100:/root/proyecto/- Transferir biblioteca dinámica
libjpeg(si el programa la necesita):
scp ./lib/libjpeg.so.9 root@192.168.1.100:/lib/ - Transferir carpeta
Compilación y Ejecución
- Comando de compilación (enlazar con libjpeg, especificar rutas de cabeceras y bibliotecas):
arm-linux-gcc main.c -o animacion_arranque -I ./include -L ./lib -ljpeg
- Ejecutar en la placa:
- Conectarse por SSH:
ssh root@192.168.1.100 - Ir al directorio:
cd /root/proyecto/ - Dar permisos:
chmod +x animacion_arranque - Ejecutar:
./animacion_arranque
- Conectarse por SSH:
Solución de Problemas Comunes
| Problema | Causa Posible (puntos a verificar) | Solución (pasos y comandos de ejemplo) |
|---|---|---|
| El programa muestra "No se pudo abrir el LCD" | Controlador LCD no cargado: no existe /dev/fb0; permisos insuficientes para /dev/fb0 |
Verificar controlador: - ls /dev/fb0 para confirmar existencia - lsmod | grep fb para ver módulos framebuffer |
| "No se pudo abrir el JPEG" | Ruta incorrecta: el directorio gif o los archivos no están donde espera el programa; o no se transfirieron |
Verificar ruta: - ls /root/proyecto/gif/Frame0.jpg - Revisar cadena ruta_fotograma en código (espacios, caracteres extraños) Transferir de nuevo: scp -r ./gif/ ip_placa:/root/proyecto/ |
| Animación negra / distorsionada | mmap falló: argumentos incorrectos (tamaño mal calculado) o devolvió MAP_FAILED; formato de píxel incorrecto (LCD espera RGB565 pero programa escribe ARGB) | Depurar mmap: - Agregar comprobación: if (lcd_buffer == MAP_FAILED) { perror("mmap"); return -1; } - Verificar tamaño: ancho * alto * 4 para ARGB Corregir formato: - Si LCD es RGB565, cambiar asignación: ((R>>3)<<11) | ((G>>2)<<5) | (B>>3) |
| Animación entrecortada | Intervalo demasiado grande (ej. 40 ms → 25 FPS, perceptible); decodificación JPEG muy lenta (>30 ms por fotograma) | Ajustar FPS: - Calcular intervalo: 1000 / 30 ≈ 33 ms (30 FPS umbral de fluidez) Optimizar decodificación: - Usar biblioteca estática: enlazar con libjpeg.a en lugar de .so - Precargar fotogramas: decodificar todos al inicio y almacenar en memoria, luego solo copiar |
| Imagen desborda la pantalla | Resolución del fotograma mayor que la del LCD (ej. 1024×768 en LCD 800×480); coordenadas (x0,y0) no limitadas | Ajustar resolución: - Redimensionar todos los fotogramas a la resolución del LCD (con GIMP, etc.) Centrado: - Calcular x0 = (LCD_ANCHO - img_ancho) / 2, y0 = ... - Recortar en el bucle: solo escribir píxeles si lcd_x < LCD_ANCHO && lcd_y < LCD_ALTO |
Consideraciones Importantes
Liberación de recursos: El bucle principal es infinito. Si se desea salir cuando el sistema haya arrancado, se debe agregar un manejador de señales (por ejemplo, capturar SIGINT para ejecutar munmap y close al presionar Ctrl+C).
#include <signal.h>
void manejador_senal(int sig) {
if (sig == SIGINT) {
munmap(lcd_buffer, LCD_TAMANO);
close(fd_lcd);
printf("\nAnimación finalizada, recursos liberados.\n");
exit(0);
}
}
// En main():
signal(SIGINT, manejador_senal);
- Sincronización de fotogramas: Si la animación tiene transiciones (fundidos, movimientos continuos), el orden debe ser estricto (Frame0 → Frame1 → …).
- Optimización de almacenamiento: Si el espacio es limitado, se pueden comprimir los JPEG a menor calidad o usar WebP, o conservar solo fotogramas clave.
- Arranque automático: Para que la animación se ejecute al encender, añadir al script de inicio (ej.
/etc/profileantes deexit 0):/root/proyecto/animacion_arranque &(el&lo pone en segundo plano).