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, чтобы избежать фрагментации?

Спасибо!

偶爾在切換 GIF 時,螢幕上仍會出現「沒有資料」。

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

С оставшимися чуть больше 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 для надёжной работы.