はじめに

CYD で現在作成中の MP3 プレイヤーで、SD カードに保存したアルバムのカバー写真を表示したく、悪戦苦闘してました。

LVGL には ESP32 標準の SD ライブラリ.bmp.jpg.png などの 画像ファイルをデコードする機能 がありますが、残念ながらファイル名が英数字だけとか 8 文字以内(拡張子を除く)とかの制限付きです(また何より遅い!)。そこで SdFat 用のドライバを作成することにしました。

ドライバの作成自体は LVGL が標準装備する SD ライブラリ用ドライバ を元にすればそう難しくないのですが、MP3 プレイヤー向けにはもう1つクリアすべき課題があります。

ストリーミング再生とレンダリングの両立

ESP32 2432S028R (CYD)でLVGL - ダブルバッファとマルチコアで応答性を高める(1)」で分析した通り、LVGL ではレンダリング用のバッファを画面全体の 1/10 のサイズに留める事で省メモリを実現しています。

分割されたレンダリング領域
分割されたレンダリング領域

このことは、例えば1枚の画像が割り当てられたウィジェットをレンダリングするのに、複数回そのリソースを読み込む必要があることを意味します。画像データが Flash 上にある場合は十分早い速度で読み込むことが可能ですが、SD カード上にある場合は、その読み込み速度が問題となります。

さらに MP3 プレイヤーの場合、曲のストリーミング再生中に別セクタの画像ファイルを時分割で読み込むといった芸当ができないため、新たな課題が生じます。

この課題を LVGL のデモ music で説明すると次の様になります。

  • 曲の再生中に、スワイプでプレイリストを表示する GUI の実装を想定します。
  • 再生開始前に SD カードから画像(下図の 赤枠)をウィジェットに読み込みます。
  • 画面遷移中も同ウィジェットを更新するため、SD カードへのアクセスが発生します。
  • するとストリーミング再生が中断し、エラー割り込みが発生、プログラムが落ちます。
再生中にプレイリストに遷移
再生中にウィジェットを更新するとストリーミングが中断する!

この課題をやっつけるには、一度読み込んだ画像データを何らかの方法で内部にキャッシュすることが必要で、LVGL では以下の2つの方法が考えられます。

  1. Canvas ウィジェットを使う
    TFT_eSPILovyanGFX で言う “スプライト” に相当するウィジェットです。読み込んだ画像データを静的に確保したバッファに保存し、lv_canvas_set_buffer() でキャンバスのウィジェットに割り当てます。ただし RGB565 の画像でも、縦×横×2バイトと、それなりにメモリを消費します。

  2. RAM 上にメモリファイルシステムを作る
    適度に圧縮した jpeg のファイルイメージをメモリ上に作成し On The Fly でデコードします。LVGL ではデコードに TJpgDec が使えるので、メモリのフットプリントも小さく、1. より省メモリを実現できる可能性があります。

今回は、LVGL や MP3 を再生する ESP32 I2S audio library が結構メモリを消費するので、2. の方法で実現することにしました。

SdFat を使ったファイルシステムの作成

LVGL では ESP32 用に LittleFSSD ライブラリ を抽象化する ファイルシステム が標準で備わっています。そこでまずは 同ライブラリ用ドライバ lv_fs_arduino_sd.cpp を SdFat 用に移植し動作を確認します。移植自体は、次のメソッドを SdFat 用に改修すれば OK です。

  • fs_open()
  • fs_close()
  • fs_read()
  • fs_seek()
  • fs_tell()

ただし移植に際し一番困ったのは、fs_open() の次のコードです。

static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) {
  ...
  File file = SD.open(path, flags); // ローカル変数に指定ファイルのディスクリプタを設定する
  if(!file) {
    return NULL;
  }

  SdFile * lf = new SdFile{file}; // ローカル変数をグローバルに生成した構造体内に閉じ込める

  return (void *)lf; // 生成した構造体を呼び出し元に返す
}

自動変数の file をグローバルに割り当てた 構造体 SdFile 内に閉じ込めることで、スコープが外れても保持されるようにしていますが、SdFat では次のコンパイルエラーが発生します(実際にはもっと大量のエラーが出力されますが、省略しています)。

error: use of deleted function 'FsFile::FsFile(const FsFile&)'
   80 |     SdFile * lf = new SdFile{file};
...
.../Arduino/libraries/SdFat/src/FsLib/FsFile.h:905:7: note: 'FsFile::FsFile(const FsFile&)' is implicitly deleted because the default definition would be ill-formed:
  905 | class FsFile : public StreamFile<FsBaseFile, uint64_t> {
      |       ^~~~~~
...
.../Arduino/libraries/SdFat/src/ExFatLib/../common/ArduinoFiles.h:76:7: note: 'StreamFile<FsBaseFile, long long unsigned int>::StreamFile(const StreamFile<FsBaseFile, long long unsigned int>&)' is implicitly deleted because the default definition would be ill-formed:
   76 | class StreamFile : public stream_t, public BaseFile {
      |       ^~~~~~~~~~

おそらく FsLibFatLib の階層でコンストラクタの定義が不十分なためと思われますが、SdFat に手を入れることなくエラーを解消するには、ローカル変数の file をスタティックな変数にせざるを得ませんでした。ただし単純に単一の変数としたので、同時に複数のファイルは扱えず、当然スレッドセーフにもなりませんが、今回の使い方では OK としています 😛

ドライバコードの全文と使い方

ということで、移植したコードの全体は次の様になります。

SdFat 用ドライバ
#include "lvgl.h"

// 標準のファイルシステムとの競合を回避
#if LV_USE_FS_ARDUINO_SD == 0

#include "SdFat.h"

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p);
static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br);
static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw);
static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p);

// ファイルディスクリプタを静的変数化
static File my_file;

// 互換性のための追加定義
enum SeekMode {
  SeekSet = 0,
  SeekCur = 1,
  SeekEnd = 2
};

// ドライブレターの設定
#define LV_FS_ARDUINO_SD_LETTER 'S'

/**
 * Register a driver for the SD File System interface
 */
void lv_fs_arduino_sd_init(void) {
  static lv_fs_drv_t fs_drv;
  lv_fs_drv_init(&fs_drv);

  fs_drv.letter = LV_FS_ARDUINO_SD_LETTER;
  fs_drv.open_cb  = fs_open;
  fs_drv.close_cb = fs_close;
  fs_drv.read_cb  = fs_read;
  fs_drv.write_cb = fs_write;
  fs_drv.seek_cb  = fs_seek;
  fs_drv.tell_cb  = fs_tell;

  fs_drv.dir_close_cb = NULL;
  fs_drv.dir_open_cb  = NULL;
  fs_drv.dir_read_cb  = NULL;

  lv_fs_drv_register(&fs_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/**
 * Open a file
 * @param drv       pointer to a driver where this function belongs
 * @param path      path to the file beginning with the driver letter (e.g. S:/folder/file.txt)
 * @param mode      read: FS_MODE_RD, write: FS_MODE_WR, both: FS_MODE_RD | FS_MODE_WR
 * @return          a file descriptor or NULL on error
 */
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
  LV_UNUSED(drv);

  int flags;
  if (mode == LV_FS_MODE_WR)
    flags = FILE_WRITE;
  else if (mode == LV_FS_MODE_RD)
    flags = FILE_READ;
  else if (mode == (LV_FS_MODE_WR | LV_FS_MODE_RD))
    flags = FILE_WRITE;

  my_file = SD.open(path, flags);
  if (!my_file) {
    return NULL;
  }

  return (void *)&my_file;
}

/**
 * Close an opened file
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable. (opened with fs_open)
 * @return          LV_FS_RES_OK: no error or  any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p) {
  LV_UNUSED(drv);
  my_file.close();
  return LV_FS_RES_OK;
}

/**
 * Read data from an opened file
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable.
 * @param buf       pointer to a memory block where to store the read data
 * @param btr       number of Bytes To Read
 * @param br        the real number of read bytes (Byte Read)
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br) {
  LV_UNUSED(drv);
  *br = my_file.read((uint8_t *)buf, btr);
  return (int32_t)(*br) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}

/**
 * Write into a file
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable
 * @param buf       pointer to a buffer with the bytes to write
 * @param btw       Bytes To Write
 * @param bw        the number of real written bytes (Bytes Written)
 * @return          LV_FS_RES_OK: no error or  any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw) {
  LV_UNUSED(drv);
  *bw = my_file.write((uint8_t *)buf, btw);
  return (int32_t)(*bw) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}

/**
 * Set the read write pointer. Also expand the file size if necessary.
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable. (opened with fs_open )
 * @param pos       the new position of read write pointer
 * @param whence    tells from where to interpret the `pos`. See @lv_fs_whence_t
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence) {
  LV_UNUSED(drv);

  SeekMode mode = SeekSet;
  if (whence == LV_FS_SEEK_SET)
    mode = SeekSet;
  else if (whence == LV_FS_SEEK_CUR)
    mode = SeekCur;
  else if (whence == LV_FS_SEEK_END)
    mode = SeekEnd;

  int rc;
  switch (mode) {
    case SeekSet:
      rc = my_file.seekSet(pos);
      break;
    case SeekCur:
      rc = my_file.seekCur(pos);
      break;
    case SeekEnd:
      rc = my_file.seekEnd(pos);
      break;
  }

  return rc < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}

/**
 * Give the position of the read write pointer
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_p variable
 * @param pos_p     pointer to store the result
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p) {
  LV_UNUSED(drv);
  *pos_p = my_file.curPosition();
  return (int32_t)(*pos_p) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}

#endif // LV_USE_FS_ARDUINO_SD

上記コードを適当な名前で(例えば my_fs_arduino_sd.cpp)保存します。またデコードする画像ファイルのタイプに合わせ lv_conf.h を設定します。以下は Tiny JPEG Decompressor を使う場合の 設定 です。

/** JPG + split JPG decoder library.
 *  Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_TJPGD 1

また次は、BMP Decoder を使う場合の 設定 です。

/** BMP decoder library */
#define LV_USE_BMP 1

Arduino 用の .ino ファイル には、先頭で SdFat.h#include とインスタンス SD を定義し、setup() の最後を次のようにすれば良いでしょう。

LVGL_Arduino.ino(抜粋)
#include "lvgl.h"

// 標準のファイルシステムとの競合を回避
#if LV_USE_FS_ARDUINO_SD == 0
  #include "SdFat.h"
  SdFat SD;
  extern void lv_fs_arduino_sd_init(void);

// 標準のファイルシステムを試す場合
#else
  #include "FS.h"
  #inlucde "SD.h"
#endif
...

void setup() {
  ...

  // SD カード I/F を初期化
  if (!SD.begin()) {
    Serial.println("SD Card Mount Failed");
    return;
  }

  lv_init();

#if LV_USE_FS_ARDUINO_SD == 0
  lv_fs_arduino_sd_init();
#endif

  ...

  // ウィジェットに画像を設定
  lv_obj_t *image = lv_image_create(lv_screen_active());
  lv_image_set_src(image, "S:/sample.jpg");
  lv_obj_center(image);

  Serial.println("Setup done");
}

Arduino 用 .ino ファイルについては「LVGL のデモを動かす」を参考にしてください。

ちなみに lv_conf.h で次を設定 すると標準の SD ライブラリ用コードの動作確認ができます。この場合 lv_init() で実行される lv_fs_arduino_sd_init() 以前に SD カードの初期化を行う必要があることに注意してください。

/** API for Arduino Sd. */
#define LV_USE_FS_ARDUINO_SD 1
#if LV_USE_FS_ARDUINO_SD
    #define LV_FS_ARDUINO_SD_LETTER 'S'   /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */
    #define LV_FS_ARDUINO_SD_PATH ""      /**< Set the working directory. File/directory paths will be appended to it. */
#endif

RAM 上にメモリファイルシステムを作る

さて本題の、読み込んだ画像をキャッシュ化する メモリファイルシステム の作成です。

元々 LVGL にはメモリファイルシステム用の設定 LV_USE_FS_MEMFS とそのソースコード lv_fs_memfs.c があり、これと前章のコードを混ぜ合わせた様なドライバを作成します。

その仕組みは、fs_open() 時にマルっとファイルイメージを RAM に保存し、fs_seek()fs_tell() 用のアドレスを管理するという簡単なものです。

SdFat 用メモリファイルシステムのドライバ
#include "lvgl.h"
#include <string>
#include <functional>

// 標準のファイルシステムとの競合を回避
#if LV_USE_FS_ARDUINO_SD == 0

#include "SdFat.h"

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p);
static lv_fs_res_t fs_read (lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br);
static lv_fs_res_t fs_seek (lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_tell (lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p);

// キャッシュ用バッファとファイルポインタを管理する構造体と変数を定義
typedef struct {
  size_t    id;
  size_t    size;
  char *    buffer;
  uint32_t  position;
} FsCache_t;

static FsCache_t fs_cache = {};

// ドライブレターの設定
#define LV_FS_ARDUINO_SD_LETTER 'S'

// キャッシュ用バッファの解放
void lv_fs_clear_cache(void) {
  fs_cache.id = 0;
  fs_cache.size = 0;
  fs_cache.position = 0;

  if (fs_cache.buffer) {
    free(fs_cache.buffer);
    fs_cache.buffer = 0;
  }
}

/**
 * Register a driver for the SD File System interface
 */
void lv_fs_arduino_sd_init(void) {
  static lv_fs_drv_t fs_drv;
  lv_fs_drv_init(&fs_drv);

  fs_drv.letter   = MY_FS_ARDUINO_SD_LETTER;
  fs_drv.open_cb  = fs_open;
  fs_drv.close_cb = fs_close;
  fs_drv.read_cb  = fs_read;
  fs_drv.write_cb = NULL; // 書き込み用メソッドは不要
  fs_drv.seek_cb  = fs_seek;
  fs_drv.tell_cb  = fs_tell;

  fs_drv.dir_close_cb = NULL;
  fs_drv.dir_open_cb  = NULL;
  fs_drv.dir_read_cb  = NULL;

  lv_fs_drv_register(&fs_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/
/**
 * Open a file
 * @param drv       pointer to a driver where this function belongs
 * @param path      path to the file beginning with the driver letter (e.g. S:/folder/file.txt)
 * @param mode      read: FS_MODE_RD, write: FS_MODE_WR, both: FS_MODE_RD | FS_MODE_WR
 * @return          a file descriptor or NULL on error
 */
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
  LV_UNUSED(drv);
  LV_UNUSED(mode);

  // ファイル名のハッシュを生成
  std::hash<std::string> makeHash;
  size_t id = makeHash(path);

  // 新規または別ファイルが指定された場合
  if (id != fs_cache.id) {
    // キャッシュを解放
    lv_fs_clear_cache();
    fs_cache.id = id;

    // ファイルをマルっと保持できるサイズのバッファを確保
    File file = SD.open(path, FILE_READ);
    size_t size = file.fileSize();
    fs_cache.buffer = (char *)malloc(size);
    assert(fs_cache.buffer);

    // キャッシュ用バッファにファイルを読み込む
    fs_cache.size = file.read((uint8_t *)fs_cache.buffer, size);
    assert(fs_cache.size == size);

    file.close();
  }

  // ファイルポインタを初期化
  fs_cache.position = 0;
  return (void *)drv;
}

/**
 * Close an opened file
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable. (opened with fs_open)
 * @return          LV_FS_RES_OK: no error or  any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p) {
  LV_UNUSED(drv);
  LV_UNUSED(file_p);

  return LV_FS_RES_OK; // Keep the cache
}

/**
 * Read data from an opened file
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable.
 * @param buf       pointer to a memory block where to store the read data
 * @param btr       number of Bytes To Read
 * @param br        the real number of read bytes (Byte Read)
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br) {
  LV_UNUSED(drv);
  LV_UNUSED(file_p);

  if (0 <= fs_cache.position && fs_cache.position <= fs_cache.size) {
    /* Do not allow reading beyond the actual memory block (it can be happend with 'LV_USE_TJPGD' */
    uint32_t remaining = fs_cache.size - fs_cache.position;
    if (btr > remaining) {
      btr = remaining;
    }

    memcpy(buf, fs_cache.buffer + fs_cache.position, btr);
    fs_cache.position += (*br = btr);
    return LV_FS_RES_OK;
  }

  else {
    assert(false);
    *br = 0;
    return LV_FS_RES_INV_PARAM;
  }
}

/**
 * Set the read write pointer. Also expand the file size if necessary.
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_t variable. (opened with fs_open )
 * @param pos       the new position of read write pointer
 * @param whence    tells from where to interpret the `pos`. See @lv_fs_whence_t
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence) {
  LV_UNUSED(drv);
  LV_UNUSED(file_p);

  switch (whence) {
    case LV_FS_SEEK_SET:
      fs_cache.position = pos;
      break;
    case LV_FS_SEEK_CUR:
      fs_cache.position += pos;
      break;
    case LV_FS_SEEK_END:
      fs_cache.position = (fs_cache.size - 1) - pos;
      break;
  }

  assert(fs_cache.position < fs_cache.size);
  return LV_FS_RES_OK;
}

/**
 * Give the position of the read write pointer
 * @param drv       pointer to a driver where this function belongs
 * @param file_p    pointer to a file_p variable
 * @param pos_p     pointer to store the result
 * @return          LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
 */
static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p) {
  LV_UNUSED(drv);
  LV_UNUSED(file_p);

  *pos_p = fs_cache.position;
  return LV_FS_RES_OK;
}

#endif // LV_USE_FS_ARDUINO_SD

使い方は前章と同じです 👍

LVGL のキャッシュシステムについて

lv_fs_memfs.c もファイルをメモリ上にマッピングするドライバで、LVGL のキャッシュシステム を使用しています。これをそのまま利用しない理由は、最初からファイル全体をキャッシュせずに途中で不足分を追加で読み込んだりするので、再生中の SD 読み込みと競合する可能性があるからです。

サンプルコード

LovyanGFX と LVGL 9.2.2 および 9.3.0 で動作確認を行なった検証用サンプルを GitHub に保存しています。SD ライブラリの差し替えやキャッシュ機能の検証用で、また何より作成中の MP3 プレイヤーへの組み込みを前提としているため、本記事で提示したコードよりかなり複雑ですが、よかったら参考にしてください。

サンプルコードの実行結果
サンプルコードの実行結果

実は…

冒頭に「悪戦苦闘してました」と書きましたが、TJpgDec 用デコーダの設定 LV_USE_TJPGD を有効化して MP3 プレイヤーと組み合わせると、プレイリストのスクロール中に次の割り込みエラーが発生してプログラムが落ちる事象と戦ってました 🥵

Guru Meditation Error: Core  1 panic'ed (Double exception). 

マルチタスク化された描画タスクへの割り込み中に、さらに割り込みエラーが発生する二重のエラーの様で、2つ目のエラーが1つ目のエラー要因を隠しちゃってます。

試しに再起動後に esp_reset_reason()esp_rom_get_reset_reason()リセット理由を表示 させてみましたが、やはり役に立ちません。

Reset reason (overall): exception and/or kernel panic
Reset reason (core 0) : Software resets CPU
Reset reason (core 1) : Software resets CPU

またスタックトレースでは、LVGL 9.2.2 と 9.3.0 でかなり様子が異なり、お手上げ状態です。

9.2.2 のスタックトレース
0x400e7fc5: lv_image_decoder_get_info at /Users/🤔/Documents/Arduino/libraries/lvgl/src/draw/lv_image_decoder.c:86
0x400e6f91: lv_draw_image at /Users/🤔/Documents/Arduino/libraries/lvgl/src/draw/lv_draw_image.c:107
0x401048a9: draw_image at /Users/🤔/Documents/Arduino/libraries/lvgl/src/widgets/image/lv_image.c:794
0x401048a9: lv_image_event at /Users/🤔/Documents/Arduino/libraries/lvgl/src/widgets/image/lv_image.c:684
0x401e1b2d: lv_obj_event_base at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_obj_event.c:89
0x400dfeb2: event_send_core at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_obj_event.c:364
0x400dff42: lv_obj_send_event at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_obj_event.c:67
0x400e4195: lv_obj_redraw at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:116
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1032
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1015
0x400e424e: lv_obj_redraw at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:167
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1032
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1015
0x400e424e: lv_obj_redraw at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:167
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1032
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1015
0x400e424e: lv_obj_redraw at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:167
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1032
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1015
0x400e424e: lv_obj_redraw at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:167
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1032
0x400e447d: refr_obj at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1015
0x400e47ef: refr_obj_and_children at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:820
0x400e492d: refr_area_part at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:753
0x400e515c: refr_area at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:679
0x400e515c: refr_invalid_areas at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:586
0x400e515c: lv_display_refr_timer at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:398
0x400fb982: lv_timer_exec at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:326
0x400fb982: lv_timer_handler at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:107
0x400fb982: lv_timer_handler at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:63
0x400dab10: loop() at /Users/🤔/Documents/Arduino/Arduino-CYD-2432S028R/LVGL/LVGL_Arduino_MP3Player/LVGL_Arduino_MP3Player.ino:273
0x4013e1c0: loopTask(void*) at /Users/🤔/Library/Arduino15/packages/esp32/hardware/esp32/3.2.0/cores/esp32/main.cpp:74
0x4008d996: vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139
9.3.0 のスタックトレース
0x4008ad2c: xt_utils_compare_and_set at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/xtensa/include/xt_utils.h:216
0x4008ad2c: esp_cpu_compare_and_set at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_hw_support/cpu.c:232
0x4008daf3: spinlock_acquire at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_hw_support/include/spinlock.h:123
0x4008daf3: xPortEnterCriticalTimeout at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:479
0x40091c39: xPortEnterCriticalTimeoutSafe at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/include/freertos/portmacro.h:581
0x40091c39: vPortEnterCriticalSafe at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/include/freertos/portmacro.h:588
0x40091c39: multi_heap_internal_lock at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap.c:164
0x40091ddd: multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:253
0x40091ddd: multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:243
0x400837cb: aligned_or_unaligned_alloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:82
0x400837cb: heap_caps_aligned_alloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:156
0x400837e5: heap_caps_malloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:176
0x40083969: heap_caps_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:84
0x401192e9: heap_alloc_dma at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/platforms/esp32/../esp32/common.hpp:107
0x401192e9: lgfx::v1::FlipBuffer::getBuffer(unsigned int) at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/platforms/esp32/../common.hpp:146
0x4011942f: lgfx::v1::Bus_SPI::writePixels(lgfx::v1::pixelcopy_t*, unsigned long) at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp:456
0x401eda6d: lgfx::v1::Panel_LCD::writePixels(lgfx::v1::pixelcopy_t*, unsigned long, bool) at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/panel/Panel_LCD.cpp:278
0x400d8a51: void lgfx::v1::LGFXBase::writePixelsDMA(lgfx::v1::rgb565_t const*, long) at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/LGFXBase.hpp:338
0x400d8a51: void lgfx::v1::LGFXBase::pushPixelsDMA(lgfx::v1::rgb565_t*, long) at /Users/🤔/Documents/Arduino/libraries/LovyanGFX/src/lgfx/v1/LGFXBase.hpp:346
0x400d8a51: my_disp_flush at /Users/🤔/Documents/Arduino/Arduino-CYD-2432S028R/LVGL/LVGL_Arduino_MP3Player/LVGL_Arduino_MP3Player.ino:152
0x400e4348: call_flush_cb at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1409
0x400e4348: draw_buf_flush at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:1373
0x400e54fd: refr_invalid_areas at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:635
0x400e54fd: lv_display_refr_timer at /Users/🤔/Documents/Arduino/libraries/lvgl/src/core/lv_refr.c:403
0x401021c2: lv_timer_exec at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:327
0x401021c2: lv_timer_handler at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:107
0x401021c2: lv_timer_handler at /Users/🤔/Documents/Arduino/libraries/lvgl/src/misc/lv_timer.c:63
0x400da960: loop() at /Users/🤔/Documents/Arduino/Arduino-CYD-2432S028R/LVGL/LVGL_Arduino_MP3Player/LVGL_Arduino_MP3Player.ino:273
0x401454d0: loopTask(void*) at /Users/🤔/Library/Arduino15/packages/esp32/hardware/esp32/3.2.0/cores/esp32/main.cpp:74
0x4008d996: vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139

9.3.0 の更新履歴 にはデコーダ関係の改修が 17 項目も挙がってますが、今回作成のコードを組み込まずとも LV_USE_TJPGD を有効にするだけでエラーとなります。従って本記事で提示したコードには問題がないとは思います… 多分ですが 🙄

事象の発生源であるプレイリストは “music” からの流用で、スクロールの度にヒープメモリを弄ってるのが原因と睨んでますが、LV_USE_TJPGD の代わりに LV_USE_BMP を使えばエラーは起きないので、取り敢えずは先に進もうと思います 😅