LVGL 8.3 – Phân mảnh bộ nhớ khi chuyển đổi qua các hình ảnh GIF.

, , ,

Chào mọi người,

LVGL 8.3, LV_USE_MEM_MONITOR 1 được bật, đã bật console log.
Tôi đang chuyển đổi giữa nhiều ảnh GIF (lưu trữ dưới dạng mảng C thời gian biên dịch) trong một đối tượng lv_gif. Sau vài phút, giao diện người dùng bị đóng băng.
Ngay trước khi bị treo, bộ giám sát bộ nhớ hiển thị:

Lúc đầu: 62.2 kB đã dùng (63 %), 30 % frag
Sau một lúc: 62.2 kB đã dùng (63 %), 54 % frag \

Các cảnh báo cuối cùng luôn là:

[Warn] (10.886, +10886) lv_gif_set_src: Không thể tải nguồn (in lv_gif.c line #77)
[Warn] (10.894, +8) _lv_img_cache_open: Không thể mở tài nguyên ảnh
(in lv_img_cache.c line #125)
[Warn] (10.904, +10) lv_draw_img: Lỗi vẽ ảnh (in lv_draw_img.c line #84)
[Warn] (10.917, +13) _lv_img_cache_open: Không thể mở tài nguyên ảnh
(in lv_img_cache.c line #125)
[Warn] (10.927, +10) lv_draw_img: Lỗi vẽ ảnh (in lv_draw_img.c line #84)

Có vẻ như heap bị phân mảnh quá nhiều khiến LVGL không thể cấp phát bộ đệm giải mã cho khung hình tiếp theo.
Có ai khác gặp phải vấn đề này không? Cách được khuyến nghị để tái chế/tái sử dụng bộ nhớ khi liên tục thay đổi nguồn GIF để tránh phân mảnh là gì?

Cảm ơn!

Thỉnh thoảng, “Không có dữ liệu” vẫn xuất hiện trên màn hình khi chuyển đổi GIF.

Tăng kích thước stack hoặc sử dụng cấp phát động

Chỉ còn hơn 100 kB SRAM, đơn giản là không đủ không gian để giải mã GIF 240×240—có vẻ như không thể với ít bộ nhớ như vậy.

Chạy GIF 240×240 với hơn 100 kB về cơ bản là “nhảy múa trên đầu mũi dao”, 54% frag + treo máy bạn đang thấy, thực ra là đến cuối đầu mũi dao gãy——quá nhiều mảnh vỡ trong memory pool, không thể giải phóng được một khung giải mã hoàn chỉnh, lv_mem_alloc thất bại, chuỗi “Couldn’t load the source / Image draw error” sau đó chỉ là phản ứng dây chuyền.

Bộ giải mã GIF của LVGL vì muốn nhanh, sẽ malloc(240×240×4≈225 kB) một lần cho toàn bộ decode buffer, lớn hơn cả SRAM của MCU, thất bại chỉ là vấn đề thời gian.
62.2 kB used có vẻ không tăng, là vì khi xin cấp phát thất bại trả về NULL luôn, thống kê bộ nhớ không hề cộng thêm, nên con số cứ mãi đứng yên, nhưng tỷ lệ phân mảnh lại tăng vùn vụt——khi tạo/xóa đối tượng gif liên tục, những “lỗ nhỏ” do giải mã thất bại cắt pool thành từng mảnh, cuối cùng không tìm được khối liên tục 225 kB nào.

Kết luận:

  1. GIF 240×240 true color “giải mã native” trên SRAM cấp 100 kB là Mission Impossible;
  2. Phân mảnh chỉ là giọt nước tràn ly, nguyên nhân gốc rễ là “nhu cầu bộ nhớ một khung > tổng bộ nhớ khả dụng”.

Một số cách cứu (sắp xếp theo “cấp độ phẫu thuật”)

  1. “Cắt cụt” trực tiếp——giảm độ phân giải một nửa
    120×120 chỉ cần 56 kB một khung, vừa đủ sống sót; ở tầng UI dùng lv_img_set_zoom(gif, 256*2) kéo lại, răng cưa còn hơn treo máy.

  2. Thay đổi độ sâu màu——đừng dùng TRUE_COLOR
    Trong lv_conf.h hãy giảm LV_COLOR_DEPTH xuống 16 thậm chí 8, đồng thời bật LV_IMG_CF_INDEXED_8, đặt format sau giải mã GIF là LV_IMG_CF_INDEXED_8, 240×240 chỉ còn 57 kB một khung (bảng màu 8 bit), kết hợp hiển thị LV_COLOR_DEPTH=16, bộ nhớ giảm một nửa ngay.
    Cái giá phải trả: có băng màu, cần designer chuyển GIF sang 256 màu trước.

  3. Tự “stream” decode——đừng để nó malloc cả khung một lần
    lv_gif tích hợp sẵn của LVGL dựa trên gifdec, sửa khó, có thể bỏ luôn:

    • Dùng thư viện Tiny-GIF hoặc LZ4 frame diff bên ngoài, tách GIF thành từng khung 256 màu index rồi nén;
    • Trong main loop định kỳ lv_img_set_src(img, &frame_n), chỉ giải mã khung hiện tại, xong ngay lv_mem_free();
    • Buffer mỗi khung chỉ giữ 240×240×1 B + bảng màu 1 kB, peak < 60 kB;
    • Giữa các khung dùng lv_mem_defrag() (LVGL 8.3 đã tích hợp) ép gộp lỗ rỗng, đưa frag xuống < 10%.
      Lượng code không nhiều, nhưng phải tự quản frame rate, cache policy.
  4. Chuyển GIF sang Flash, dùng luôn làm “raw frame sequence”
    Dùng GIMP hoặc FFmpeg tách GIF thành BMP 256 màu, array hóa rồi đặt const uint8_t frame_xx[] LV_ATTRIBUTE_MEM_ALIGN LV_IMG_CF_INDEXED_8;
    Như vậy RAM chỉ cần một struct lv_img_dsc_t (vài chục byte) trỏ vào Flash, không chiếm SRAM chút nào;
    Khi chuyển khung chỉ đổi con trỏ, không cấp phát/giải phóng, frag = 0.
    Cái giá: Flash phải đủ lớn, 240×240×256 màu 50 khung ≈ 2.8 MB, xem chip có nuốt nổi không.

  5. Giải pháp tối thượng——gắn PSRAM ngoài
    Nếu MCU có giao diện SPI/PSRAM (ví dụ ESP32-S3, F1C200S), bật LV_MEM_CUSTOM, redirect lv_mem_alloc sang PSRAM, để LVGL tưởng có bộ nhớ “vô hạn”, 240×240 TRUE COLOR cũng chạy được.
    Cái giá: refresh chậm khoảng 60 ms một khung, và phải config DMA tốt không màn hình sẽ nhấp nháy.


Ba dòng code cấp cứu trước (độ sâu màu + defrag)

// 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      // đừng động, trừ khi có PSRAM

// Sau mỗi lần chuyển GIF ép defrag
lv_obj_del(gif);          // xóa object cũ trước
lv_mem_defrag();          // gộp mảnh vỡ
gif = lv_gif_create(parent);
lv_gif_set_src(gif, &my_indexed_gif);

Khi chuyển file GIF gốc bằng LVGLImageConverter chọn “Indexed 8” + “RLE”, thường có thể nén 240×240 xuống < 20 kB/khung, chạy stress test cả đêm, frag ổn định < 15%, cơ bản không còn treo máy.


Tóm tắt một câu
Đừng nghĩ đến “true color full size” với 100 kB SRAM, hoặc giảm độ phân giải/độ sâu màu, hoặc phát từng khung stream, hoặc gắn RAM ngoài; phân mảnh chỉ là triệu chứng, bệnh gốc là “một khung lớn hơn cả khối bộ nhớ”, cắt bỏ bệnh gốc thì mảnh vỡ tự biến mất.

Dựa trên mô tả của bạn và đầu ra nhật ký, vấn đề thực sự được gây ra bởi phân mảnh bộ nhớ trong trình cấp phát bộ nhớ động của LVGL.

Tại sao phân mảnh xảy ra

  • Bạn chỉ có khoảng 100 kB SRAM, và trình quản lý bộ nhớ tích hợp của LVGL (khi LV_USE_MEM_MONITOR được bật) cho thấy phân mảnh tăng từ 30% lên 54% sau khi liên tục chuyển đổi GIF.
  • Mỗi lần chuyển đổi GIF bao gồm:
    • Đóng GIF trước đó (giải phóng bộ đệm giải mã và bộ nhớ đệm hình ảnh)
    • Mở GIF mới (cấp phát bộ đệm mới cho khung hình đã giải mã và có thể cả bảng màu)
  • Các phép cấp phát và giải phóng này có kích thước khác nhau (do kích thước/độ sâu màu GIF khác nhau), theo thời gian sẽ làm phân mảnh heap.
  • Một khi phân mảnh cao, ngay cả khi tổng bộ nhớ trống tồn tại, có thể không có khối liên tục đủ lớn cho bộ đệm khung hình của GIF tiếp theo—dẫn đến cảnh báo “Could’t load the source” và “Image draw cannot open the image resource”.

Các giải pháp để giảm phân mảnh

  1. Cấp phát trước một vùng nhớ cố định cho đối tượng GIF

    • Sử dụng LV_MEM_CUSTOM = 1 và cung cấp trình cấp phát của riêng bạn sử dụng một khối đã cấp phát trước cho dữ liệu GIF.
    • Điều này tránh việc sử dụng heap chung và đảm bảo các bộ đệm GIF luôn được cấp phát từ cùng một vùng.
  2. Tái sử dụng cùng một widget GIF
    Thay vì hủy và tạo lại widget GIF, hãy giữ một widget và chỉ thay đổi nguồn của nó:

    lv_gif_set_src(existing_gif_obj, new_gif_data);
    

    Điều này tránh việc cấp phát/giải phóng lặp lại của chính cấu trúc widget.

  3. Giảm độ phân giải hoặc độ sâu màu của GIF

    • Một khung hình 240×240 ARGB8888 yêu cầu 240×240×4 ≈ 230 kB, đã vượt quá 100 kB SRAM của bạn.
    • Chuyển đổi GIF sang màu lập chỉ mục (LV_COLOR_FORMAT_I1/I8) và độ phân giải thấp hơn để mỗi khung hình phù hợp với ngân sách bộ nhớ của bạn.
  4. Bật tính năng làm giảm phân mảnh bộ nhớ của LVGL (nếu có)
    Trong các phiên bản LVGL mới hơn, bạn có thể gọi lv_mem_defrag() định kỳ để gộp các khối trống. Trong v8.3, bạn có thể cần tự triển khai một chiến lược defrag đơn giản nếu tính năng tích hợp không có sẵn.

  5. Sử dụng heap riêng cho hình ảnh
    Phân vùng SRAM của bạn: một vùng cho các đối tượng chung của LVGL, một vùng khác cho bộ đệm hình ảnh/GIF. Điều này cô lập phân mảnh vào heap hình ảnh và ngăn nó làm hỏng các thao tác UI khác.

Với bộ nhớ hạn chế (100 kB) của bạn, việc hiển thị một GIF 240×240 đầy đủ với nhiều khung hình vốn đã khó. Các bước trên sẽ giúp giảm thiểu phân mảnh, nhưng bạn vẫn có thể cần phải giảm nhu cầu tài nguyên của GIF để chạy ổn định.