LVGL 8.3 – Memory fragmentation when cycling through GIF images

,

안녕하세요,

LVGL 8.3, LV_USE_MEM_MONITOR 1 활성화, 콘솔 로그 켜짐.
lv_gif 객체에서 여러 GIF(컴파일 타임 C 배열로 저장됨)를 전환하고 있습니다. 몇 분 후에 UI가 멈춥니다.

멈추기 직전 메모리 모니터는 다음을 표시합니다:

시작: 62.2 kB 사용 (63 %), 30 % 단편화
잠시 후: 62.2 kB 사용 (63 %), 54 % 단편화

마지막 경고는 항상 다음과 같습니다:

[Warn] (10.886, +10886) lv_gif_set_src: Could't load the source (in lv_gif.c line #77)
[Warn] (10.894, +8) _lv_img_cache_open: Image draw cannot open the image resource
(in lv_img_cache.c line #125)
[Warn] (10.904, +10) lv_draw_img: Image draw error (in lv_draw_img.c line #84)
[Warn] (10.917, +13) _lv_img_cache_open: Image draw cannot open the image resource
(in lv_img_cache.c line #125)
[Warn] (10.927, +10) lv_draw_img: Image draw error (in lv_draw_img.c line #84)

힙이 너무 단편화되어 LVGL이 다음 프레임을 위한 디코더 버퍼를 더 이상 할당할 수 없는 것 같습니다.
다른 분들도 이 문제를 겪으신 적이 있나요? 단편화를 피하기 위해 GIF 소스를 반복적으로 변경할 때 메모리를 재활용/재사용하는 권장 방법은 무엇인가요?

감사합니다!

가끔 GIF를 전환하는 동안 화면에 “데이터 없음”이 여전히 나타납니다.

스택 크기를 늘리거나 동적 할당을 사용하세요

SRAM이 100kB 조금 넘게 남았을 뿐인데, 240×240 GIF를 디코딩할 공간이 전혀 없어—그 적은 메모리로는 불가능해 보인다.

100 kB 조금 넘는 메모리로 240×240 GIF를 구동하는 것은 기본적으로 ‘칼끝에서 춤추는’ 것과 같습니다. 지금 보이는 54% frag + 멈춤 현상은 사실상 칼끝이 부러진 것입니다. 메모리 풀의 조각화가 너무 심해 완전히 디코딩된 한 프레임조차 확보할 수 없고, lv_mem_alloc이 실패하면서 이어지는 'Couldn’t load the source / Image draw error’는 단지 연쇄 반응일 뿐입니다.

LVGL의 GIF 디코더는 속도를 위해 전체 디코딩 버퍼를 한 번에 malloc(240×240×4≈225 kB)로 할당하는데, 이는 MCU 전체 SRAM보다 큽니다. 실패는 시간 문제였습니다.
62.2 kB used가 증가하지 않은 것처럼 보이는 이유는 할당 실패 시 NULL을 바로 반환해서 메모리 통계에 전혀 반영되지 않았기 때문입니다. 그래서 숫자는 계속 멈춰 있었지만, 조각화율은 치솟았습니다. gif 객체를 반복적으로 생성/삭제할 때 디코딩 실패로 남은 '작은 구멍’들이 메모리 풀을 산산조각 내어 결국 225 kB 연속 블록을 찾을 수 없게 된 것입니다.

결론:

  1. 240×240 트루컬러 GIF를 100 kB 수준 SRAM에서 '네이티브 디코딩’하는 것은 Mission Impossible입니다.
  2. 조각화는 낙타의 등을 부러뜨린 마지막 낙엽일 뿐, 근본 원인은 '단일 프레임 메모리 요구량 > 총 사용 가능 메모리’입니다.

구제할 수 있는 몇 가지 방법(‘수술 난이도’ 순)

  1. 직접 ‘절단’ — 해상도를 반으로 줄이기
    120×120은 단일 프레임당 56 kB만 필요해 겨우 살아남을 수 있습니다. 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 kB(8비트 팔레트)로 줄어듭니다. 여기에 LV_COLOR_DEPTH=16 표시를 결합하면 메모리가 절반으로 직접 감소합니다.
    대가: 컬러 밴딩이 발생하므로 디자이너가 GIF를 256색으로 미리 변환해야 합니다.

  3. 직접 ‘스트리밍’ 디코딩 — 한 번에 malloc으로 전체 프레임 할당하지 않기
    LVGL 기본 제공 lv_gif는 gifdec 기반으로 수정하기 번거롭므로干脆 그냥 사용하지 않는 것도 방법입니다:

    • 외부에서 Tiny-GIF 또는 LZ4 프레임 차분 라이브러리를 사용해 GIF를 256색 인덱스 프레임으로 분리 후 압축합니다.
    • 메인 루프에서 주기적으로 lv_img_set_src(img, &frame_n)을 호출해 현재 프레임만 디코딩하고, 사용 후 즉시 lv_mem_free()합니다.
    • 프레임당 버퍼는 240×240×1 B + 팔레트 1 kB만 유지하면 피크 < 60 kB입니다.
    • 프레임 간 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을 전혀 차지하지 않습니다.
    전환 시 포인터만 변경하면 할당/해제가 없어 조각화가 0입니다.
    대가: Flash가 충분히 커야 하며, 240×240×256색 50프레임 ≈ 2.8 MB이므로 칩이 감당할 수 있는지 확인해야 합니다.

  5. 궁극적 해결책 — 외장 PSRAM 사용
    MCU에 SPI/PSRAM 인터페이스가 있는 경우(예: ESP32-S3, F1C200S), LV_MEM_CUSTOM를 활성화하고 lv_mem_alloc을 PSRAM로 리다이렉트하여 LVGL이 ‘무한’ 메모리가 있는 것처럼 인식하게 하면 240×240 TRUE_COLOR도 구동할 수 있습니다.
    대가: 리프레시가 한 프레임당 약 60 ms 정도 느려지며, 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 kB 이내로 압축할 수 있으며, 이 상태로 한 밤중 스트레스 테스트를 실행하면 frag가 15% 이하로 안정되어 기본적으로 더 이상 멈추지 않습니다.


한 문장 요약
100 kB SRAM으로는 '트루컬러 풀 사이즈’를 생각하지 마세요. 해상도/색상 깊이를 낮추거나 프레임을 분리해 스트리밍 재생하거나 외장 RAM을 사용해야 합니다. 조각화는 단지 증상일 뿐, 진짜 병변은 '단일 프레임이 전체 메모리보다 큼’이며, 병변을 제거하면 조각화는 자연스럽게 사라집니다.

설명과 로그 출력을 기반으로, 이 문제는 실제로 LVGL의 동적 메모리 할당기에서 발생한 메모리 단편화 때문입니다.

단편화가 발생하는 이유

  • SRAM이 약 100 kB밖에 없고, LVGL의 내장 메모리 관리자(LV_USE_MEM_MONITOR가 활성화된 경우)는 GIF를 반복적으로 전환한 후 단편화가 30%에서 54%로 증가하는 것을 보여줍니다.
  • 각 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 kB가 필요하며, 이는 이미 100 kB SRAM을 초과합니다.
    • GIF를 인덱스 색상(LV_COLOR_FORMAT_I1/I8)으로 변환하고 해상도를 낮춰서 각 프레임이 메모리 예산 내에 맞도록 하세요.
  4. LVGL의 메모리 조각 모음 활성화(가능한 경우)
    최신 LVGL 버전에서는 주기적으로 lv_mem_defrag()를 호출하여 빈 블록을 통합할 수 있습니다. v8.3에서는 내장 기능이 없는 경우 직접 간단한 조각 모음 전략을 구현해야 할 수 있습니다.

  5. 이미지를 위한 별도 힙 사용
    SRAM을 분할하세요: 하나는 LVGL의 일반 객체용, 다른 하나는 이미지/GIF 버퍼용입니다. 이는 단편화를 이미지 힙으로 격리시키고 다른 UI 작업이 중단되는 것을 방지합니다.

메모리가 부족한(100 kB) 상황에서 전체 240×240 GIF와 여러 프레임을 표시하는 것은 본질적으로 어렵습니다. 위의 단계들은 단편화를 완화하는 데 도움이 되지만, 안정적으로 실행하려면 GIF의 리소스 요구사항을 낮춰야 할 수도 있습니다.

1개의 좋아요