LVGL 8.3 – Fragmentación de memoria al cambiar entre imágenes GIF.

, , ,

Hola a todos,

LVGL 8.3, LV_USE_MEM_MONITOR 1 habilitado, registro de consola activado.
Estoy cambiando entre varios GIFs (almacenados como arreglos C en tiempo de compilación) en un objeto lv_gif. Después de unos minutos la interfaz de usuario se congela.
Justo antes del bloqueo, el monitor de memoria muestra:

Inicio: 62,2 kB usados (63 %), 30 % frag
Después de un tiempo: 62,2 kB usados (63 %), 54 % frag

Las últimas advertencias son siempre:

[Warn] (10.886, +10886) lv_gif_set_src: No se pudo cargar el origen (en lv_gif.c línea #77)
[Warn] (10.894, +8) _lv_img_cache_open: No se puede abrir el recurso de imagen
(en lv_img_cache.c línea #125)
[Warn] (10.904, +10) lv_draw_img: Error al dibujar la imagen (en lv_draw_img.c línea #84)
[Warn] (10.917, +13) _lv_img_cache_open: No se puede abrir el recurso de imagen
(en lv_img_cache.c línea #125)
[Warn] (10.927, +10) lv_draw_img: Error al dibujar la imagen (en lv_draw_img.c línea #84)

Parece que el heap está tan fragmentado que LVGL ya no puede asignar los búferes del decodificador para el siguiente fotograma.
¿Alguien más ha visto esto? ¿Cuál es la forma recomendada de reciclar/reutilizar la memoria al cambiar repetidamente el origen del GIF para evitar la fragmentación?

¡Gracias!

Ocasionalmente, “Sin datos” todavía aparece en la pantalla al cambiar GIFs.

Aumenta el tamaño de la pila o usa asignación dinámica

Con solo un poco más de 100 kB de SRAM disponible, simplemente no hay espacio para decodificar un GIF de 240×240—parece imposible con tan poca memoria.

Ejecutar un GIF de 240×240 con poco más de 100 kB es básicamente “bailar sobre la punta de un cuchillo”. El 54 % de fragmentación + bloqueo que ves ahora es, en realidad, que al final la punta del cuchillo se rompió: hay demasiados fragmentos en el pool de memoria, no se puede liberar espacio ni para un solo fotograma decodificado completo, lv_mem_alloc falla, y la cadena de “Couldn´t load the source / Image draw error” que sigue no es más que una reacción en cadena.

El decodificador de GIF de LVGL, para ser rápido, asigna de una sola vez el búfer de decodificación completo con malloc(240×240×4≈225 kB), que es más grande que toda la SRAM de tu MCU; el fallo es solo cuestión de tiempo.
El “62.2 kB used” parece que no aumenta porque cuando la solicitud falla devuelve NULL directamente y las estadísticas de memoria ni siquiera se actualizan, por lo que el número se queda atascado ahí, pero la tasa de fragmentación se dispara: al crear/eliminar objetos gif repetidamente, los “agujeritos” que dejan los fallos de decodificación van cortando el pool en pedazos, hasta que no se encuentra ni un bloque contiguo de 225 kB.

Conclusión:

  1. La “decodificación nativa” de un GIF a color real de 240×240 en SRAM de nivel de 100 kB es Misión Imposible;
  2. La fragmentación es solo la gota que colma el vaso, la causa raíz es “demanda de memoria por fotograma > memoria total disponible”.

Algunas soluciones que pueden salvarlo (ordenadas por “nivel de cirugía”)

  1. “Amputación” directa: reduce la resolución a la mitad
    Un fotograma de 120×120 solo necesita 56 kB, apenas puede sobrevivir; a nivel UI usa lv_img_set_zoom(gif, 256*2) para escalarlo de nuevo, la pixelación es mejor que el bloqueo.

  2. Cambia la profundidad de color: no uses TRUE_COLOR
    En lv_conf.h baja LV_COLOR_DEPTH a 16 o incluso 8, y activa LV_IMG_CF_INDEXED_8, establece el formato después de la decodificación del GIF a LV_IMG_CF_INDEXED_8, un fotograma de 240×240 se queda en solo 57 kB (paleta de 8 bits), combinado con LV_COLOR_DEPTH=16 para mostrar, la memoria se reduce a la mitad directamente.
    Costo: bandas de color, necesitas que el diseñador convierta el GIF a 256 colores primero.

  3. Decodificación “streaming” por tu cuenta: no permitas que haga malloc de todo el fotograma de una vez
    El lv_gif incluido en LVGL está basado en gifdec, es complicado de modificar, puedes simplemente no usarlo:

    • Usa externamente una librería Tiny-GIF o LZ4 de diferencia entre fotogramas, primero descompón el GIF en índices de 256 colores por fotograma, luego comprime;
    • En el bucle principal usa lv_img_set_src(img, &frame_n) periódicamente, solo decodifica el fotograma actual, usa lv_mem_free() inmediatamente después;
    • Deja solo 240×240×1 B + paleta de 1 kB como búfer por fotograma, pico < 60 kB;
    • Entre fotogramas usa lv_mem_defrag() (LVGL 8.3 ya lo incluye) para forzar la fusión de huecos, puede bajar el frag a menos del 10 %.
      No es mucho código, pero tienes que gestionar el framerate y la estrategia de caché tú mismo.
  4. Mueve el GIF a Flash, úsalo como “fotogramas de secuencia sin procesar”
    Usa GIMP o FFmpeg para dividir el GIF en BMP de 256 colores, después conviértelos a arrays y ponlos en const uint8_t frame_xx[] LV_ATTRIBUTE_MEM_ALIGN LV_IMG_CF_INDEXED_8;
    Así solo necesitas una estructura lv_img_dsc_t (decenas de bytes) en RAM apuntando a Flash, no ocupa SRAM en absoluto;
    Al cambiar solo modificas el puntero, no hay solicitudes/liberaciones, fragmentación 0.
    Costo: Flash debe ser lo suficientemente grande, 50 fotogramas de 240×240×256 colores ≈ 2.8 MB, depende de si el chip puede soportarlo.

  5. Solución definitiva: usa PSRAM externo
    Si tu MCU tiene interfaz SPI/PSRAM (por ejemplo ESP32-S3, F1C200S), activa LV_MEM_CUSTOM, redirige lv_mem_alloc a PSRAM, haz que LVGL piense que tiene memoria “infinita”, incluso 240×240 TRUE_COLOR puede funcionar.
    Costo: el refresco es lento aproximadamente 60 ms por fotograma, y debes configurar bien el DMA o la pantalla parpadeará.


Tres líneas de código para salir del apuro (profundidad de color + desfragmentación)

// lv_conf.h
#define LV_COLOR_DEPTH      16
#define LV_IMG_CF_INDEXED_8 1
#define LV_USE_MEM_MONITOR  1
#define LV_MEM_CUSTOM       0      // no lo toques a menos que tengas PSRAM

// Forzar desfragmentación después de cada cambio de GIF
lv_obj_del(gif);          // primero elimina el objeto antiguo
lv_mem_defrag();          // combina fragmentos
gif = lv_gif_create(parent);
lv_gif_set_src(gif, &my_indexed_gif);

Cuando conviertas el archivo fuente GIF con LVGLImageConverter, selecciona “Indexed 8” + “RLE”, generalmente puede comprimir 240×240 a menos de 20 kB/fotograma, luego ejecuta una prueba de estrés durante toda la noche, frag se estabiliza por debajo del 15 %, básicamente no se bloqueará más.


Resumen en una frase:
Con 100 kB de SRAM no pienses en “color real a tamaño completo”, o reduces la resolución/profundidad de color, o divides los fotogramas para reproducción streaming, o añades RAM externa; la fragmentación es solo un síntoma, la verdadera enfermedad es “un fotograma es más grande que toda la memoria disponible”, elimina la enfermedad y la fragmentación desaparecerá naturalmente.

Basado en tu descripción y la salida del registro, el problema está causado por fragmentación de memoria en el asignador de memoria dinámica de LVGL.

Por qué ocurre la fragmentación

  • Solo tienes alrededor de 100 kB de SRAM, y el gestor de memoria integrado de LVGL (cuando LV_USE_MEM_MONITOR está habilitado) muestra que la fragmentación aumenta del 30% al 54% después de cambiar GIFs repetidamente.
  • Cada cambio de GIF implica:
    • Cerrar el GIF anterior (liberando su búfer del decodificador y la caché de imágenes)
    • Abrir un nuevo GIF (asignando un nuevo búfer para el fotograma decodificado y posiblemente la paleta)
  • Estas asignaciones y liberaciones son de diferentes tamaños (debido a diferentes dimensiones/profundidades de color de los GIF), lo que con el tiempo fragmenta el heap.
  • Una vez que la fragmentación es alta, aunque exista memoria libre total, puede que no haya un bloque contiguo lo suficientemente grande para el búfer de fotogramas del siguiente GIF, lo que provoca las advertencias “Could’t load the source” y “Image draw cannot open the image resource”.

Soluciones para reducir la fragmentación

  1. Preasignar un pool de memoria fijo para objetos GIF

    • Usa LV_MEM_CUSTOM = 1 y proporciona tu propio asignador que utilice un bloque preasignado para los datos GIF.
    • Esto evita el heap general y garantiza que los búferes GIF siempre se asignen desde la misma región.
  2. Reutilizar el mismo widget GIF
    En lugar de destruir y recrear el widget GIF, mantén un widget y solo cambia su fuente:

    lv_gif_set_src(existing_gif_obj, new_gif_data);
    

    Esto evita la asignación/liberación repetida de la estructura del widget en sí.

  3. Reducir la resolución o profundidad de color de los GIF

    • Un fotograma ARGB8888 de 240×240 requiere 240×240×4 ≈ 230 kB, lo que ya excede tus 100 kB de SRAM.
    • Convierte los GIF a color indexado (LV_COLOR_FORMAT_I1/I8) y resolución más baja para que cada fotograma quepa en tu presupuesto de memoria.
  4. Habilitar la desfragmentación de memoria de LVGL (si está disponible)
    En versiones posteriores de LVGL puedes llamar a lv_mem_defrag() periódicamente para consolidar bloques libres. En v8.3 puede que necesites implementar tú mismo una estrategia simple de desfragmentación si la integrada no está presente.

  5. Usar un heap separado para imágenes
    Particiona tu SRAM: una región para los objetos generales de LVGL, otra para los búferes de imagen/GIF. Esto aisla la fragmentación al heap de imágenes y evita que interrumpa otras operaciones de la UI.

Dada tu memoria limitada (100 kB), mostrar un GIF completo de 240×240 con múltiples fotogramas es inherentemente difícil. Los pasos anteriores ayudarán a mitigar la fragmentación, pero puede que aún necesites reducir las demandas de recursos del GIF para funcionar de forma confiable.