LVGL 8.3 – Фрагментация памяти при переключении GIF-изображений.

, , ,

Привет всем,

LVGL 8.3, LV_USE_MEM_MONITOR 1 включен, консольный лог включен.
Я переключаюсь между несколькими GIF (хранящимися как массивы C времени компиляции) в объекте lv_gif. Через несколько минут UI зависает.

Прямо перед зависанием монитор памяти показывает:

Старт: 62.2 кБ использовано (63 %), 30 % фрагм.
Через некоторое время: 62.2 кБ использовано (63 %), 54 % фрагм.

Последние предупреждения всегда:

[Warn] (10.886, +10886) lv_gif_set_src: Не удалось загрузить источник (in lv_gif.c line #77)
[Warn] (10.894, +8) _lv_img_cache_open: Не удалось открыть ресурс изображения
(in lv_img_cache.c line #125)
[Warn] (10.904, +10) lv_draw_img: Ошибка отрисовки изображения (in lv_draw_img.c line #84)
[Warn] (10.917, +13) _lv_img_cache_open: Не удалось открыть ресурс изображения
(in lv_img_cache.c line #125)
[Warn] (10.927, +10) lv_draw_img: Ошибка отрисовки изображения (in lv_draw_img.c line #84)

Похоже, что куча настолько фрагментирована, что LVGL больше не может выделить буферы декодера для следующего кадра.
Кто-нибудь еще сталкивался с этим? Какой рекомендуемый способ повторного использования памяти при многократной смене источника GIF, чтобы избежать фрагментации?

Спасибо!

Occasionally, “No data” still pops up on the screen while switching GIFs.

Увеличьте размер стека или используйте динамическое выделение памяти

С оставшимися чуть больше 100 кБ SRAM просто нет места для декодирования GIF размером 240×240 — похоже, это невозможно на таком малом объеме памяти.

Запуск 240×240 GIF чуть более чем на 100 кБ — это буквально «танцевать на острие ножа». То, что вы сейчас видите: 54 % frag + зависание — это и есть момент, когда к концу танца острие ломается: в памяти слишком много фрагментов, не удаётся выделить место даже для одного полного декодированного кадра, lv_mem_alloc завершается ошибкой, а вся последующая цепочка «Couldn’t load the source / Image draw error» — лишь цепная реакция.

Декодер GIF в LVGL для скорости выделяет весь буфер декодирования одним malloc(240×240×4≈225 кБ), что больше, чем вся SRAM вашего MCU, и сбой — лишь вопрос времени.
62,2 кБ used кажется не растёт, потому что при неудаче выделения возвращается NULL, и статистика памяти вообще не учитывает этот запрос, поэтому цифра застревает на месте, но уровень фрагментации растёт: при повторном создании/удалении gif-объектов «маленькие дырки», оставленные неудачными попытками декодирования, разрезают пул на куски, и в итоге не удаётся найти даже непрерывный блок в 225 кБ.

Вывод:

  1. «Нативное декодирование» 240×240 true-color GIF на SRAM уровня 100 кБ — это Mission Impossible;
  2. Фрагментация — лишь последняя соломинка, переломившая хребет верблюду; корневая причина — «память под один кадр > общая доступная память».

Несколько спасительных приёмов (в порядке «хирургической сложности)**

  1. Прямая «ампутация» — урезать разрешение вдвое
    120×120 требует всего 56 кБ на кадр, можно выжить; на уровне UI использовать lv_img_set_zoom(gif, 256*2), чтобы растянуть обратно — пикселизация лучше, чем зависание.

  2. Изменить глубину цвета — не использовать TRUE_COLOR
    В lv_conf.h установить LV_COLOR_DEPTH в 16 или даже 8, одновременно включить LV_IMG_CF_INDEXED_8, а формат декодированного GIF задать как LV_IMG_CF_INDEXED_8 — тогда 240×240 кадр займёт всего 57 кБ (8-битная палитра), при отображении с LV_COLOR_DEPTH=16 память сокращается вдвое.
    Цена: появляются полосы цвета, дизайнеру нужно предварительно конвертировать GIF в 256 цветов.

  3. Собственное «потоковое» декодирование — не давать ему malloc-ить целый кадр сразу
    Встроенный lv_gif в LVGL основан на gifdec, его непросто модифицировать, можно просто не использовать его:

    • Снаружи применить библиотеку Tiny-GIF или LZ4 для дельты кадров, предварительно разбить GIF на индексированные 256-цветные кадры и сжать;
    • В главном цикле периодически вызывать lv_img_set_src(img, &frame_n), декодировать только текущий кадр, сразу после использования lv_mem_free();
    • Оставлять буфер кадра только 240×240×1 Б + палитра 1 кБ, пик < 60 кБ;
    • Между кадрами принудительно объединять дырки с помощью lv_mem_defrag() (встроен в LVGL 8.3), это снизит frag ниже 10 %.
      Объём кода невелик, но придётся самому управлять частотой кадров и стратегией кэширования.
  4. Перенести GIF во Flash, использовать как «сырой последовательный кадр»
    GIMP или FFmpeg разбить GIF на 256-цветные BMP, преобразовать в массив и поместить в const uint8_t frame_xx[] LV_ATTRIBUTE_MEM_ALIGN LV_IMG_CF_INDEXED_8;
    Так в RAM потребуется лишь структура lv_img_dsc_t (десятки байт), указывающая на Flash, SRAM не занимается вообще;
    При переключении меняется только указатель, нет alloc/free, фрагментация 0.
    Цена: Flash должен быть достаточно большим, 240×240×256 цветов 50 кадров ≈ 2,8 МБ, нужно смотреть, сможет ли чип переварить.

  5. Ультимативное решение — подключить внешний PSRAM
    Если MCU имеет интерфейс SPI/PSRAM (например, ESP32-S3, F1C200S), включить LV_MEM_CUSTOM, перенаправить lv_mem_alloc в PSRAM, заставив LVGL думать, что памяти «бесконечно», тогда сможет работать и 240×240 TRUE_COLOR.
    Цена: замедление обновления примерно на 60 мс за кадр, и DMA нужно настроить правильно, иначе экран будет мерцать.


Три строки экстренного кода (глубина цвета + дефрагментация)

// 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      // пока не трогайте, если нет PSRAM

// После каждой смены GIF принудительно упорядочить
lv_obj_del(gif);          // сначала удалить старый объект
lv_mem_defrag();          // объединить фрагменты
gif = lv_gif_create(parent);
lv_gif_set_src(gif, &my_indexed_gif);

При конвертации исходного GIF через LVGLImageConverter выбирайте «Indexed 8» + «RLE», обычно это сжимает 240×240 до менее 20 кБ/кадр, после чего запустите ночной стресс-тест — frag стабилизируется ниже 15 %, и зависаний больше не будет.


Итог одним предложением
С 100 кБ SRAM не стоит мечтать о «true-color полном размере» — либо снижайте разрешение/глубину цвета, либо разбивайте кадры для потокового воспроизведения, либо подключайте внешнюю RAM; фрагментация — лишь симптом, источник проблемы в том, что «кадр больше всего доступного блока памяти», устраните источник — и фрагментация исчезнет сама собой.

Основываясь на вашем описании и выводе журнала, проблема действительно вызвана фрагментацией памяти в динамическом распределителе памяти LVGL.

Почему возникает фрагментация

  • У вас всего около 100 кБ SRAM, и встроенный менеджер памяти LVGL (когда включен LV_USE_MEM_MONITOR) показывает, что фрагментация возрастает с 30% до 54% после многократного переключения GIF.
  • Каждое переключение GIF включает:
    • Закрытие предыдущего GIF (освобождение буфера декодера и кэша изображения)
    • Открытие нового GIF (выделение нового буфера для декодированного кадра и возможно палитры)
  • Эти выделения и освобождения имеют разный размер (из-за различных размеров/глубины цвета GIF), что со временем фрагментирует кучу.
  • Когда фрагментация высока, даже если есть общая свободная память, может не оказаться достаточно большого смежного блока для буфера кадра следующего GIF — что приводит к предупреждениям «Could’t load the source» и «Image draw cannot open the image resource».

Решения для уменьшения фрагментации

  1. Предварительное выделение фиксированного пула памяти для объектов GIF

    • Используйте LV_MEM_CUSTOM = 1 и предоставьте собственный распределитель, который использует предварительно выделенный блок для данных GIF.
    • Это позволяет избежать использования общей кучи и гарантирует, что буферы GIF всегда выделяются из одной и той же области.
  2. Повторное использование одного и того же виджета GIF
    Вместо уничтожения и повторного создания виджета GIF сохраните один виджет и просто измените его источник:

    lv_gif_set_src(existing_gif_obj, new_gif_data);
    

    Это позволяет избежать повторного выделения/освобождения самой структуры виджета.

  3. Уменьшение разрешения или глубины цвета GIF

    • Кадр 240×240 ARGB8888 требует 240×240×4 ≈ 230 кБ, что уже превышает ваш 100 кБ SRAM.
    • Конвертируйте GIF в индексированный цвет (LV_COLOR_FORMAT_I1/I8) и снизьте разрешение, чтобы каждый кадр помещался в ваш бюджет памяти.
  4. Включение дефрагментации памяти LVGL (если доступно)
    В более поздних версиях LVGL вы можете периодически вызывать lv_mem_defrag() для объединения свободных блоков. В v8.3 вам может потребоваться реализовать простую стратегию дефрагментации самостоятельно, если встроенная отсутствует.

  5. Использование отдельной кучи для изображений
    Разделите ваш SRAM: один регион для общих объектов LVGL, другой для буферов изображений/GIF. Это изолирует фрагментацию в куче изображений и не позволит ей нарушать другие операции UI.

Учитывая ваш ограниченный объем памяти (100 кБ), отображение полноценного 240×240 GIF с несколькими кадрами изначально является сложной задачей. Вышеуказанные шаги помогут смягчить фрагментацию, но вам всё равно может потребоваться снизить требования к ресурсам GIF для надёжной работы.