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 소스를 반복적으로 변경할 때 메모리를 재활용/재사용하는 권장 방법은 무엇인가요?
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 연속 블록을 찾을 수 없게 된 것입니다.
결론:
240×240 트루컬러 GIF를 100 kB 수준 SRAM에서 '네이티브 디코딩’하는 것은 Mission Impossible입니다.
조각화는 낙타의 등을 부러뜨린 마지막 낙엽일 뿐, 근본 원인은 '단일 프레임 메모리 요구량 > 총 사용 가능 메모리’입니다.
구제할 수 있는 몇 가지 방법(‘수술 난이도’ 순)
직접 ‘절단’ — 해상도를 반으로 줄이기
120×120은 단일 프레임당 56 kB만 필요해 겨우 살아남을 수 있습니다. UI 레벨에서 lv_img_set_zoom(gif, 256*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색으로 미리 변환해야 합니다.
직접 ‘스트리밍’ 디코딩 — 한 번에 malloc으로 전체 프레임 할당하지 않기
LVGL 기본 제공 lv_gif는 gifdec 기반으로 수정하기 번거롭므로干脆 그냥 사용하지 않는 것도 방법입니다:
외부에서 Tiny-GIF 또는 LZ4 프레임 차분 라이브러리를 사용해 GIF를 256색 인덱스 프레임으로 분리 후 압축합니다.
메인 루프에서 주기적으로 lv_img_set_src(img, &frame_n)을 호출해 현재 프레임만 디코딩하고, 사용 후 즉시 lv_mem_free()합니다.
프레임 간 lv_mem_defrag()(LVGL 8.3에 기본 내장)로 강제로 구멍을 병합하면 frag를 10% 이내로 억제할 수 있습니다.
코드량은 많지 않지만 프레임레이트와 캐시 정책을 직접 관리해야 합니다.
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이므로 칩이 감당할 수 있는지 확인해야 합니다.
궁극적 해결책 — 외장 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” 경고로 이어집니다.
단편화를 줄이기 위한 해결책
GIF 객체를 위한 고정 메모리 풀을 사전 할당
LV_MEM_CUSTOM = 1을 사용하고 GIF 데이터를 위해 사전 할당된 블록을 사용하는 자체 할당기를 제공합니다.
이는 일반 힙을 피하고 GIF 버퍼가 항상 동일한 영역에서 할당되도록 보장합니다.
동일한 GIF 위젯 재사용
GIF 위젯을 파괴하고 다시 생성하는 대신, 하나의 위젯을 유지하고 소스만 변경합니다:
lv_gif_set_src(existing_gif_obj, new_gif_data);
이는 위젯 구조체 자체의 반복적인 할당/해제를 방지합니다.
GIF 해상도 또는 색상 심도 감소
240×240 ARGB8888 프레임은 240×240×4 ≈ 230 kB가 필요하며, 이는 이미 100 kB SRAM을 초과합니다.
GIF를 인덱스 색상(LV_COLOR_FORMAT_I1/I8)으로 변환하고 해상도를 낮춰서 각 프레임이 메모리 예산 내에 맞도록 하세요.
LVGL의 메모리 조각 모음 활성화(가능한 경우)
최신 LVGL 버전에서는 주기적으로 lv_mem_defrag()를 호출하여 빈 블록을 통합할 수 있습니다. v8.3에서는 내장 기능이 없는 경우 직접 간단한 조각 모음 전략을 구현해야 할 수 있습니다.
이미지를 위한 별도 힙 사용
SRAM을 분할하세요: 하나는 LVGL의 일반 객체용, 다른 하나는 이미지/GIF 버퍼용입니다. 이는 단편화를 이미지 힙으로 격리시키고 다른 UI 작업이 중단되는 것을 방지합니다.
메모리가 부족한(100 kB) 상황에서 전체 240×240 GIF와 여러 프레임을 표시하는 것은 본질적으로 어렵습니다. 위의 단계들은 단편화를 완화하는 데 도움이 되지만, 안정적으로 실행하려면 GIF의 리소스 요구사항을 낮춰야 할 수도 있습니다.