Animación de Arranque en Linux: Principios e Implementación con Archivos IO

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).
      Selección de formato JPEG
    • 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).
  • 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).
    Nomenclatura de 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: tftp no transfiere carpetas (la carpeta gif contiene 82 JPEG). Se usa scp (basado en SSH).

  • Pasos (ejemplo: host Linux → placa ARM):

    • Transferir carpeta gif: (-r recursivo, root usuario, 192.168.1.100 IP 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/
    

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

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/profile antes de exit 0): /root/proyecto/animacion_arranque & (el & lo pone en segundo plano).

Etiquetas: linux Framebuffer LCD animación de arranque JPEG

Publicado el 7-13 02:07