はじめに

バグ Fix はもちろん機能・性能が向上するのはありがたいことですが、プラットフォームのコアやライブラリがバージョンアップすると、大抵の場合はそれらが占有するメモリが増えます。

一方アプリケーションが使えるメモリは減り続ける事になり、バージョンアップに追従するにはそれなりの苦労が伴います。

本稿では、LVGL の習熟用に作成した CYD_MP3Player のメモリマネジメントで苦労した経験と得られた知見を元に、主に LVGL のメモリ消費削減策をまとめました。対象とする LVGL のバージョンは v9.2.2 から v9.5.0 までです。

まずはそれぞれのメモリ事情、次に LVGL のメモリ消費削減策の具体例、最後にまとめの順で書き連ねたいと思います。

ESP32 のメモリ事情

スペック上 520KB の SRAM が載っている ESP32 ですが、DRAM (Data RAM) によれば、ユーザーに開放されているのは 高々 320KB です。

There is 520 KB of available SRAM (320 KB of DRAM and 200 KB of IRAM) on the ESP32. However, due to a technical limitation, the maximum statically allocated DRAM usage is 160 KB. The remaining 160 KB (for a total of 320 KB of DRAM) can only be allocated at runtime as heap.

ESP32には520KBのSRAM(DRAM 320KBとIRAM 200KB)が利用可能です。ただし、技術的な制限により、静的に割り当てられるDRAMの最大使用量は160KBです。残りの160KB(合計320KBのDRAM)は、実行時にヒープ領域としてのみ割り当てられます。

ESP32 のメモリマップ
ESP32 のメモリマップ

また ESP32 Programmers’ Memory Model および ESP32’s family Memory Map 101 には、もう少し詳しいメモリマップが載っています。

それらによると、IRAM(Instruction RAM、CPU が命令を高速実行するために使われる RAM)は通常、ユーザーがヒープ領域として使う事ができません。

ただし次のドキュメントを参考に、ESP32 のコアを ESP-IDF で再構築できれば、LVGL の描画用バッファを IRAM に追いやれそうです(ムズイので、今回は対象外です… 🙄)。

次の表は、ESP32 コアのバージョンとヒープサイズを調べた結果です(PSRASM 無しの場合)。

バージョン/ヒープメモリのサイズ 2.0.17 3.3.5 3.3.6 3.3.7
ヒープとして使えるメモリサイズの合計 310496 311096 272736 309824
ユーザーが使えるヒープメモリのサイズ 278380 268188 231580 266992

3.3.6 が前バージョンに比べてユーザーが使えるヒープが約 36KB も少なくなったのは問題外 としても、3.3.7 は 2.0.17 に比べて 11KB 程度少なくなっています。

ヒープ残量の観測に ESP.getFreeHeap() を使うべきでない理由

ESP.getFreeHeap() は、名前からするとヒープ残量を返してくれそうですが、指定されている MALLOC_CAP_INTERNAL は内部 RAM である IRAM + DRAM(PSRAM を除く)の残量が返ります。通常 IRAM はユーザーに解放されていないので、知らずに頼ると痛い目に遭います(「why does small malloc fail when freeheap =76k+? #5346」に、このような紛らわしいネーミングになった経緯が書かれています)。

一方、heap_caps_get_free_size()MALLOC_CAP_DEFAULT を指定すれば、malloc()calloc() で確保できる DRAM(PSRAM を含む)残量が得られます。

また multi_heap_get_info() を使えば、それまでの最小値やブロックとして確保可能な最大値など、より詳しい情報が得られるのでオススメです。

#include <Arduino.h>
#include <esp_arduino_version.h>

void print_heap_size(void) {
  printf("\nESP32 core version  : %d.%d.%d\n", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH);

  // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/heap_debug.html
  const uint32_t caps = MALLOC_CAP_DEFAULT;

  multi_heap_info_t info;
  heap_caps_get_info(&info, caps);

  printf("Heap total size     :%7d\n", heap_caps_get_total_size(caps));
  printf("Heap free  size     :%7d\n", info.total_free_bytes);
  printf("Heap allocated size :%7d\n", info.total_allocated_bytes);
  printf("Heap minimum free   :%7d\n", info.minimum_free_bytes);
  printf("Heap largest free   :%7d\n", info.largest_free_block);
}

LVGL のメモリ事情

LVGL が使うメモリは「描画用バッファ」と「ウィジェット用ヒープ」の2つに大別され、これらはデフォルトで 静的領域の bss セグメント に配置されます。

描画用バッファは スケッチで宣言され、色深度が16ビットの場合 15KB を占有します。

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

またウィジェット用ヒープは lv_conf.h で設定され、lv_mem_init() で初期化されます。

バージョン/サイズ v8.1 v8.2 v9.0
LV_MEM_SIZE (32U * 1024U) (48U * 1024U) (64 * 1024U)

v9.x は v8.1 の倍ですね 😓。LVGL のフォーラムを見ていると、より軽量な v8 のユーザー も依然として相当数いるのも頷けます。

ウィジェット用ヒープメモリの観測方法

lv_mem_monitor() で得られる情報から、私は次のようなコードで観測しています。

#if __has_include(<lvgl.h>)
void print_lvgl_heap(void) {
  // LVGL memory usage
  lv_mem_monitor_t mon;
  lv_mem_monitor(&mon);

  size_t used = mon.total_size - mon.free_size;
  printf("LVGL heap total size   :%7lu (fragmentation: %d%%)\n", mon.total_size, mon.frag_pct);
  printf("LVGL heap free  size   :%7lu (largest block: %lu)\n", mon.free_size, mon.free_biggest_size);
  printf("LVGL heap allocated    :%7lu (%d%%, maximum: %lu)\n", used, mon.used_pct, mon.max_used);
}
#endif // lv_mem.h

LVGL のメモリ消費量削減策

ESP32 コアから要らない機能を外して再構築できれば良いのですが…、私にはムズ過ぎて手には負えないので、ここからは LVGL 側の工夫用により、アプリで使用可能なヒープ領域を増やすために実施した削減策を共有します。

描画用バッファの削減

以前の記事(コレアレ)で解析したように、LVGL の描画用バッファは Arduino 用サンプルで 次のように宣言 されています。

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

ここでのマジックナンバー 10 は、次の画像のように1つの画面を区切る矩形領域の数です。

このマジックナンバーを増やせば矩形領域1つあたりのサイズ DRAW_BUF_SIZE が小さくなりますが、LCD パネルへの転送回数が増えるのでフレームレートが下がることになります。

今回このマジックナンバーを「コレぐらいなら我慢できるよネ!?」という程度の 15 に設定し、15KB から 10KB に減量しました(LV_COLOR_DEPTH が16ビットの CYD 場合)。

ウィジェット用ヒープの削減策

LVGL のウィジェット用ヒープ管理には TLSF (Two-Level Segregated Fit) アロケータ が採用されているようで、次のように lv_mem_init() で静的に確保されます。

/*Allocate a large array to store the dynamically allocated data*/
static MEM_UNIT work_mem_int[LV_MEM_SIZE / sizeof(MEM_UNIT)] LV_ATTRIBUTE_LARGE_RAM_ARRAY;
state.tlsf = lv_tlsf_create_with_pool((void *)work_mem_int, LV_MEM_SIZE);

LV_MEM_SIZE は、LVGL の設定ファイル lv_conf.h で定義されるシンボルです。

/** Size of memory available for `lv_malloc()` in bytes (>= 2kB) */
#define LV_MEM_SIZE (64 * 1024U)          /**< [bytes] */

この 64KB をダイエットするためにとった戦略は次の通りです。

  1. 使用しないウィジェットは、LV_USE_* を 0 に設定する
  2. スタイル設定は最小限、かつ LV_STYLE_CONST_* を使う
  3. 表示されていないウィジェットは、メモリ上から削除する
  4. メモリの断片化を減らすため、ウィジェットを再利用する
  5. 見た目を変えることなく、より軽量なウィジェットを使う

これらにより、CYD_MP3Player では 40KB に削減することができました 🥳

1. 使用しないウィジェットは、LV_USE_* を 0 に設定する

実はコレ、作るアプリ毎に lv_conf.h を再編集する事になるので、個人的にはあまりお勧めしません。特に GitHub などで公開する場合は、ユーザーに面倒を強いる事になりかねません(とは言え、CYD_MP3Player では、デフォルトから2項目の変更をお願いしています)。

ただしウィジェット用ヒープの残りマージンを最大で数 KB 程度増やす効果は確かにあります。

また THEMES セクションの以下の2つのシンボルを 0 に変更すると、bss セグメントのサイズが 87172 → 83068 に、約 4KB 増える結果となりました(LVGL v9.5.0)。

/** A very simple theme that is a good starting point for a custom theme */
#define LV_USE_THEME_SIMPLE 0

/** A theme designed for monochrome displays */
#define LV_USE_THEME_MONO 0
変更前のコンパイル結果
Sketch uses 2972317 bytes (94%) of program storage space. Maximum is 3145728 bytes.
Global variables use 87172 bytes (26%) of dynamic memory, leaving 240508 bytes for local variables. Maximum is 327680 bytes.
変更後のコンパイル結果
Sketch uses 2972317 bytes (94%) of program storage space. Maximum is 3145728 bytes.
Global variables use 83068 bytes (25%) of dynamic memory, leaving 244612 bytes for local variables. Maximum is 327680 bytes.

2. スタイル設定は最小限、かつ LV_STYLE_CONST_* を使う

LVGL のウィジェットはデフォルトのスタイルを持っています(LV_USE_THEME_DEFAULT1 の場合)。このデフォルトの変更を出来るだけ少なくする事が省メモリにつながりますが、まぁ、そうもいかないでしょう 😉

具体例は公式ドキュメントの Initializing Styles に譲りますが、次の3つの変更方法をうまく使い分ける事が省メモリのカギとなります。

  • ローカルスタイル型
    個々のウィジェットのスタイルを lv_obj_set_style_*() で変更します。設定値を保持するためにメモリを消費するので、出来るだけ少ない変更に留めるのが吉です。

基本的には、少数のスタイルを特定のウィジェットに適用する場合は「ローカルスタイル型」が、適用するプロパティが多数の場合や複数のウィジェットに共通のスタイルを適用する場合は「Flash 配置型」が良いでしょう。4画面構成の CYD_MP3Player では、「スタイルシート型」に比べて約 3KB ほどの削減効果が得られました。

3. 表示されていないウィジェットは、メモリ上から削除する

複数のタブやページを構成する場合、非表示のタブやスクリーンをメモリ上に残しておくのは無駄です。よほど複雑なことをしない限り、タブやページの切り替え時に高速な ESP32 がアッと言う間にレンダリングしてくれます。

タブやページの削除は、lv_obj_delete() で親ウィジェットを削除すれば子ウィジェットも自動的に削除されます。ウィジェットのツリー構造 のおかげです。

特にスクリーンの切り替えを lv_screen_load() で行う際は、最後の引数に true を設定すれば、新しいスクリーンの表示後に古いスクリーンを自動で削除してくれので便利です。

アニメーションによる画面遷移
アニメーションによる画面遷移

ただし lv_screen_load_anim() でアニメーションを適用する場合は、遷移が完了するまでは2つのスクリーンがメモリ上に存在しなければなりません。

この場合、lv_event_add() で遷移完了を通知するイベント LV_EVENT_SCREEN_UNLOADED とコールバック関数を登録して後始末します。

また lv_obj_delete_async()lv_async_call() など、「次の lv_timer_handler() まで実行を遅延する」系の関数を活用するのもテクニックの1つです。

4. メモリの断片化を減らすため、ウィジェットを再利用する

ウィジェットの生成や削除を繰り返すとメモリの断片化が起き、空きメモリが小さなブロックに分断され、結果として連続した大きな領域が確保できなくなります。

断片化の防止には、各タブやスクリーンに共通なウィジェットを再利用することが有効です。ここでは2つの例を紹介します。

例1
lv_example_tabview_1
lv_example_tabview_1

シンプルなタブの例題 では、3つの各タブそれぞれに個別のラベルを割り当てていますが、ラベルを1つだけ生成し、lv_obj_set_parent() を使ってアクティブなタブにコンテンツに差し替えれば、ヒープメモリを節約できます。

具体的なコードは以下の「代替例」を参照してください。

lv_example_tabview_1.c の代替例
// lv_example_tabview_1.c の代替例
//
// オリジナルのコード
// https://github.com/lvgl/lvgl/blob/master/examples/widgets/tabview/lv_example_tabview_1.c
static lv_obj_t *label;
static const char *text[] = {
  // タブ1のコンテンツ
  "This the first tab\n\n"
  "If the content\n"
  "of a tab\n"
  "becomes too\n"
  "longer\n"
  "than the\n"
  "container\n"
  "then it\n"
  "automatically\n"
  "becomes\n"
  "scrollable.\n"
  "\n"
  "\n"
  "\n"
  "Can you see it?",

  // タブ2のコンテンツ
  "Second tab",

  // タブ3のコンテンツ
  "Third tab"
};

// タブ切り替え時のコールバック関数
static void event_handler(lv_event_t *e) {
  lv_obj_t *tabview = lv_event_get_target_obj(e);
  uint32_t index = lv_tabview_get_tab_active(tabview);

  lv_obj_t *obj = lv_tabview_get_content(tabview);
  lv_obj_t *tab = lv_obj_get_child(obj, index);

  lv_label_set_text_static(label, text[index]);
  lv_obj_set_parent(label, tab);
}

/**
 * シンプルなタブの例題
 */
void lv_example_tabview_1(void) {
  // lv_tabview オブジェクトを作成する
  lv_obj_t *tabview;
  tabview = lv_tabview_create(lv_screen_active());
  
  // タブ切り替え時のコールバック関数を登録する
  lv_obj_add_event_cb(tabview, event_handler, LV_EVENT_VALUE_CHANGED, NULL);

  // 3つのタブを追加(各タブはスクロール可能)
  lv_obj_t *tab[3];
  for (int i = 0; i < 3; i++) {
    tab[i] = lv_tabview_add_tab(tabview, ("Tab " + String(i + 1)).c_str());
  }

  // 最初に表示するタブにコンテンツを設定し、アクティベートする
  label = lv_label_create(tab[2]);
  lv_label_set_text_static(label, text[2]);
  lv_tabview_set_active(tabview, 2, LV_ANIM_OFF);
}
📌 Tips:ラベル用文字列のヒープ節約策

lv_label_set_text() は、指定された文字列をヒープメモリにコピーします。長い文字列を で切り詰める LV_LABEL_LONG_MODE_DOTS などの効果を必要としない場合には、直接アドレスを参照する lv_label_set_text_static() を使うべきです。

同種の関数(lv_<widget>_set_text_static())は他のウィジェットでも使用可能です。また幾つかの使用上の注意事項があるので、Set text の章を参照して下さい。

例2

例題:無限スクロール では、スクロールの度に lv_obj_create()(ボックスセルが可視領域に入った時)と lv_obj_delete()(可視領域から出た時)を繰り返すため、ヒープメモリの断片化が起きがちです。

lv_example_scroll_7
lv_example_scroll_7

この断片化を無くすには、コンテナ内に予め必要個数のセルを作成、スクロールの上下に合わせてトコロテン式にセルの順番をシフトすれば OK です 👍

ウィジェットの順番は、lv_obj_move_to_index() 一発でシフトできるので、可視領域に新たなセルが入る時と出る時のスクロールバー位置を判定するコードを追加するだけです。

lv_example_scroll_7.c の代替例
// lv_example_scroll_7.c の代替例
//
// オリジナルのコード
// https://github.com/lvgl/lvgl/blob/master/examples/scroll/lv_example_scroll_7.c
static lv_obj_t *high_label;
static lv_obj_t *low_label;
static int32_t top_num;
static int32_t bottom_num;
static bool update_scroll_running = false;

#define TOTAL_COUNT_CELLS 60  // セルの最大数(0 〜 59)
#define MAX_VISIBLE_CELLS 5   // 可視領域内のセルの最大数

// 空のセルを作成する
static void create_cells(lv_obj_t *parent, int32_t num) {
  for (int i = 0; i < num; i++) {
    lv_obj_t *obj = (lv_obj_t *)lv_obj_create(parent);
    lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
    lv_obj_t *label = lv_label_create(obj);
  }
}

// 指定セルのコンテンツを変更する
static void update_cell(lv_obj_t *obj, int32_t num) {
  lv_obj_t *label = lv_obj_get_child(obj, 0);
  lv_label_set_text_fmt(label, "%" PRId32, num);
}

// スクロール時の処理
static void update_scroll(lv_obj_t *obj) {
  /* do not re-enter this function when `lv_obj_scroll_by`
   * triggers this callback again.
   */
  if (update_scroll_running) return;
  update_scroll_running = true;

  // スクロールバーを更新する際の相対移動量
  //     ┌───────────────────┐
  //     │ ┌───────────────┐ │ ──┬── y0
  //     │ │     cell 0    │ │   │
  // gap │ │               │ │   │ dy = y1 - y0
  // ─↓─ │ └───────────────┘ │   │
  // ─↑─ │ ┌───────────────┐ │ ──┴── y1
  //     │ │     cell 1    │ │
  //     │ │               │ │
  //     │ └───────────────┘ │
  //     └───────────────────┘
  const lv_obj_t *i = lv_obj_get_child(obj, 0); // cell 0
  const lv_obj_t *j = lv_obj_get_child(obj, 1); // cell 1
  const int32_t  dy = lv_obj_get_y(j) - lv_obj_get_y(i);  // dy = 54
  const int32_t gap = dy - lv_obj_get_height(i);          // gap = 8

  // 下にスクロール(上にスワイプ)した場合の処理
  while (bottom_num < TOTAL_COUNT_CELLS - 1 /* or 30 */ && lv_obj_get_scroll_bottom(obj) < gap) {
    ++bottom_num;
    ++top_num;
    lv_obj_t *top = lv_obj_get_child(obj, 0);
    lv_obj_move_to_index(top, -1);
    update_cell(top, bottom_num);
    lv_obj_scroll_by(obj, 0, dy, LV_ANIM_OFF);
  }

  // 上にスクロール(下にスワイプ)した場合の処理
  while (top_num > 0 /* or -30 */ && lv_obj_get_scroll_top(obj) < gap) {
    --top_num;
    --bottom_num;
    lv_obj_t *end = lv_obj_get_child(obj, -1);
    lv_obj_move_to_index(end, 0);
    update_cell(end, top_num);
    lv_obj_scroll_by(obj, 0, -dy, LV_ANIM_OFF);
  }

  lv_label_set_text_fmt(high_label, "current top\nloaded value:\n%" PRId32, top_num);
  lv_label_set_text_fmt(low_label, "current bottom\nloaded value:\n%" PRId32, bottom_num);

  update_scroll_running = false;
}

static void scroll_cb(lv_event_t *e) {
  lv_obj_t *obj = lv_event_get_target_obj(e);
  update_scroll(obj);
}

static void checkbox_cb(lv_event_t *e) {
  lv_obj_t *checkbox = lv_event_get_target_obj(e);
  lv_obj_t *obj = (lv_obj_t *)lv_event_get_user_data(e);
  bool checked = lv_obj_has_state(checkbox, LV_STATE_CHECKED);
  lv_obj_set_style_opa(obj, checked ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_SCROLLBAR);
}

/**
 * スクロールしながらウィジェットを動的に更新する例題
 */
void lv_example_scroll_7(void) {
  lv_obj_t *scr = lv_screen_active(); // スクリーン
  lv_obj_t *obj = lv_obj_create(scr); // セルを格納するコンテナ
  lv_obj_set_size(obj, 160, 220);
  lv_obj_align(obj, LV_ALIGN_RIGHT_MID, -10, 0);
  lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_style_opa(obj, LV_OPA_TRANSP, LV_PART_SCROLLBAR);

  high_label = lv_label_create(scr);
  lv_obj_align(high_label, LV_ALIGN_TOP_LEFT, 10, 10);

  lv_obj_t *checkbox = lv_checkbox_create(scr);
  lv_checkbox_set_text_static(checkbox, "show\nscrollbar");
  lv_obj_align(checkbox, LV_ALIGN_LEFT_MID, 10, 0);
  lv_obj_add_event_cb(checkbox, checkbox_cb, LV_EVENT_VALUE_CHANGED, obj);

  low_label = lv_label_create(scr);
  lv_obj_align(low_label, LV_ALIGN_BOTTOM_LEFT, 10, -10);

  // 事前に空のセルを作成する
  create_cells(obj, MAX_VISIBLE_CELLS);

  // 可視領域内のセルIDを設定する
  top_num = 3;
  bottom_num = top_num + (MAX_VISIBLE_CELLS - 1);

  // 可視領域内の空のセルにコンテンツを設定する
  for (int i = 0; i < MAX_VISIBLE_CELLS; i++) {
    lv_obj_t *cell = lv_obj_get_child(obj, i);
    update_cell(cell, top_num + i);
  }

  lv_obj_update_layout(obj);
  update_scroll(obj);
  lv_obj_add_event_cb(obj, scroll_cb, LV_EVENT_SCROLL, NULL);
}

オリジナルのコードに比べ update_scroll() が軽くなり(4つの while() 文が2つに減る)、より滑らかにスクロールするようになりました 💮

5. 見た目を変えることなく、より軽量なウィジェットを使う

例3

lv_button は大抵の場合、ラベルウィジェットを追加します。つまり複数のボタンが整列した UI では、その数分だけヒープメモリを消費します。

これに対し lv_buttonmatrix は、複数の lv_button を生成する代わりに、単にボタンとテキストを描画するコンテナとして実装されていて、かなりの軽量化が可能です。

以下はそのドキュメントの日本語訳です。

ボタンマトリックスウィジェットは、複数のボタンを行と列に表示するための軽量な方法です。 ボタンは実際には作成されず、仮想的に描画されるだけなので、軽量です。ボタンマトリックスでは、 ボタン1つあたり8バイトの追加メモリしか使用しません。通常のボタンウィジェットでは約100~150バイト、 ラベルウィジェットでは約100バイトのメモリが使用されますが、ボタンマトリックスではわずか8バイトの 追加メモリしか使用しません。

コードの実例は、例題:単純なボタンマトリックス を参照してください。

例4

ボタンマトリックスの 例題:カスタムなボタン では、次のコードによりデフォルトのボタン描画ルーチンをユーザー定義の関数に差し替えています。

/**
 * Add custom drawer to the button matrix to customize buttons one by one
 */
void lv_example_buttonmatrix_2(void)
{
  lv_obj_t * btnm = lv_buttonmatrix_create(lv_screen_active());
  lv_obj_add_event_cb(btnm, event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
  lv_obj_add_flag(btnm, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS);
  lv_obj_center(btnm);
}
カスタムなボタン画像の例
カスタムなボタン画像の例

この仕組みは他の lv_obj_t オブジェクトにも適用が可能で、CYD_MP3Player の アルバムリスト では、例題2 の各セルにこれを設定し、チェックボックスの画像とテキストを描画する実装としています。

これにより、各セルに lv_checkbox を持たせる事なく省メモリを実現しました。

📌 Tips:ウィジェットのデフォルト描画ルーチンを差し替える例題

ウィジェットの Draw API とデータ構造 Draw Descriptors を習得すると、多彩な表現が可能になります。

LV_EVENT_DRAW_TASK_ADDEDLV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS を使った例題をピックアップしたので、既存の UI に満足できない方はチャレンジしてみて下さい。

おまけ:こりゃ無いだろ 😳 な削減例

境界線で立体感を出す例
境界線で立体感を出す例

CYD_MP3Player のプレイリストのデザインは music デモ のパクリなのですが、何と! 境界線で立体感を出す画像 用に無駄に lv_image オブジェクトを挿入しています。

角マルを画像で実現していた CSS 2.x の時代じゃあるまいし、今時こりゃ無いでしょ!ってことで、CYD_MP3Player では、境界線の上下の色を変えて同じ効果を出しています。

まぁ、大声で「削減した」とは言えませんネ 🥱

LVGL メモリ消費量の削減戦略まとめ

さて最後は、日本語英語 による検索語をベースに、Google AI との対話を通じて得られた回答をまとめ、締めたいと思います。


LVGL でのメモリ消費を最小限に抑えるには、ディスプレイバッファサイズの最適化、オブジェクトに割り当てられるメモリ量の削減、そしてグラフィック要素を新規作成するのではなく再利用することに重点を置きます。スタイルを効率的に管理し、LVGL の組み込みデータ構造を活用することも役立ちます。

戦略の詳細は以下の通りです。

  1. lv_conf.h で不要な機能を無効化する
    LVGLは多機能なため、使用しない機能を無効にするだけで大きなメモリ削減になります。
    • LV_USE_* の無効化
      使わないオブジェクト(例えば、チャート、ゲージ、テーブル、アニメーションなど)を 0 に設定します。
    • フォントの制限
      LV_FONT_* を制限し、必要なフォントのみを有効にします。デフォルトのフォントを小さいものに変更することも検討してください。
    • ファイルシステム (LV_USE_FS_*)
      不要なら無効化します。
    • テーマ
      テーマを無効化、または軽量なもの(LV_USE_THEME_MONOなど)に変更します。
  2. ディスプレイバッファの最適化
    • ディスプレイバッファサイズの縮小
      ディスプレイバッファは、画面に表示されるピクセルデータを保存するために使用されます。特に大画面を使用している場合は、ディスプレイバッファのサイズを縮小することで、メモリ使用量を大幅に削減できます。
    • 色深度を低くする
      アプリケーションで高い色深度を必要としない場合は、ピクセルあたりのビット数が低い形式(例:24ビットではなく16ビット)を使用して、ディスプレイバッファに必要なメモリを削減することを検討してください。
  3. メモリ割り当ての最適化
    • LV_MEM_SIZE の縮小
      この設定は、LVGL オブジェクトの作成に使用できるメモリの総量を定義します。これを減らすことは有効ですが、オブジェクトの作成と削除の戦略を調整する必要があるかもしれません。
    • LVGLのメモリ管理
      LVGLの組み込みメモリ管理関数とデータ構造(リンクリストやツリーなど)は、ライブラリ用に最適化されているため、活用してください。
    • 非表示スクリーンの削除
      複数のスクリーンを構成する場合、表示されているスクリーンに必要なオブジェクトのみを残し、非表示のオブジェクトやスタイルを削除します。この場合、親のオブジェクトを削除すれば、その子であるオブジェクトは自動的に削除されます。
    • オブジェクトの再利用
      ボタンやラベルなどの新しいオブジェクトを必要なたびに作成するのではなく、既存のオブジェクトを再利用するようにします。例えば、タブを切り替える際に、非アクティブなタブではオブジェクトを非表示にし、タブがアクティブになったときに再表示するようにすることで、オブジェクトを削除して再作成する手間と断片化を省くことができます。
    • オブジェクトプーリング
      頻繁に使用するオブジェクトについては、一定数のオブジェクトを事前に割り当て、必要に応じて再利用するオブジェクトプーリング手法の使用を検討してください。
  4. スタイルの最適化
    • スタイルの最小化
      使用する個々のスタイルの数を減らし、複数のオブジェクトに適用できる、より汎用的なスタイルを作成するようにしてください。
    • ローカル スタイルを使用する
      オブジェクト固有のスタイルの場合、LVGL フォーラムで提案されているように、メモリを節約するために、オブジェクトごとに新しい lv_style_t を作成する代わりに、ローカル スタイルでオーバーライドしてください。
    • スタイルのリセット
      スタイルを頻繁に変更する場合は、新しいスタイルを適用する前に、lv_style_reset() を使用して古いスタイルで使用されていたメモリを解放することを検討してください。ただし、スタイルの変更を再適用するか、LVGLに変更を通知するようにしてください。
    • 静的スタイル変数の利用
      スタイル用変数に lv_style_init() を適用する代わりに、lv_style_const_prop_tLV_STYLE_CONST_INIT でスタイル用変数を Flash 上に割り当ててください。
  5. その他の最適化手法
    • 画像の最適化
      より小さな画像を使用するか、画像形式を最適化ます(例:透過性のためにフルRGBAではなくインデックス付きPNGを使用する)。また生データ全体をRAMにロードすることは避け、Image Converterで非圧縮データとしてフラッシュメモリに保存してください。SVGやGIFなどの圧縮形式は、表示前にRAMへの完全な解凍が必要になる場合があることにご注意ください。
    • 画像キャッシュの無効化
      パフォーマンスは低下しますが、毎回再描画する設定にすることでRAMを節約できます。
    • コンポーネントの可視性
      画面に表示されていないオブジェクトは非表示にするか、親子関係を解除してレンダリングの負荷を軽減します。
    • オブジェクト作成の制限
      不要なオブジェクト、特に大量のオブジェクトを作成しないようにしてください。
    • 複雑さの軽減
      ユーザーインターフェースを簡素化して、グラフィカル要素の数と複雑さを軽減します。
    • イベント処理
      イベント処理に注意し、不要な処理は避けてください。変更を常にポーリングするのではなく、イベント駆動型の更新を使用することを検討してください。
    • コードの最適化
      コードがターゲットプラットフォームに適した最適化設定でコンパイルされていることを確認してください。たとえば、最適化レベルを「最小コード」に設定すると、コードサイズを削減できます。
    • スタックの使用
      小さなデータはヒープではなくスタックに置くようにします。

これらの手法を適用することで、特にリソースが限られた組み込みシステムで作業する場合、LVGLプロジェクトのメモリ消費量を大幅に削減できます。