● 実験テーマ174

「ESP32S3+SDカード+液晶+MP3デコーダIC MP3 Player製作記録 その6」
(アルバムジャケット・イメージの表示が出来る様にしました。)

■ 2026.9.29
  ・
テスト用の2G-SDカード内のイメージファイル:jpegを整理した。
   曲数:127曲/ジャケット画像:43個(曲番と紐付け→ image001.jpeg・ジャケット画像無しの曲もある)
   サイズ:100x100dot  


■ 2026.9.30
  ・今回もCopilotに聞きながら作業を進めた。

<私>

 ・PIC32MXで実績ある以下ソースをEXP32S3(ARDUINO SD.h)に移植したいと考えています。
  mp3プレーヤでsdカードからジャケットイメージ:image***.bmp(***は曲番号と紐付け)を
  読出して再生前にQVGA液晶へ表示することを考えています。

<PIC32MXでの記述>
#include <FSIO.h>
unsigned int ImageBuffer[10000]; // 16bitBMPデータバッファ:100x100= 10000ワード
char str_IMAGE[]= "IMAGExxx.BMP"; // ジャケットイメージ表示用

/***********************************************************
* 16bitBMPカラーイメージ表示関数(アルバムジャケット表示用)
* 正方形サイズ前提(100x100dot)
* 表示は、右上原点(305,34)より画素描画を繰り返す
*
* <引数の説明>
* ① Ndot:表示ドット数(例:100)
* ② Str_X:ドット描画開始X座標
* ③ Str_Y:ドット描画開始Y座標
************************************************************/
void DispJacketImage(int Ndot, int Str_X, int Str_Y)
{
  int Xpos, Ypos, ptr;

  /// BMPフォーマットのデータは、左下から右上に向かって配列されているので
  /// この順で描画すると、上下逆様の画像表示になってしまう。
  /// 右上の画素(例:ptr=9999, Xpos= 305, Ypos= 34)から下に向かって描画する必要がある。
  ptr= (Ndot * Ndot)-1;
  for(Ypos= Str_Y; Ypos < Str_Y+(Ndot-1); Ypos++) {
    for(Xpos=Str_X; Xpos > Str_X-Ndot; Xpos--) {
      Glcd_Pixel(Xpos, Ypos, ImageBuffer[ptr--]);
    }
  }
}

/***************************************
* 整数からASCII文字に変換
****************************************/
void itostring(char digit, unsigned short data, char *buffer)
{
  char i;

  buffer += digit; // 最後の数字位置
  for(i=digit; i>0; i--) { // 変換は下位から上位へ
    buffer--; // ポインター1
    *buffer = (data % 10) + '0'; // ASCIIへ
    data = data / 10; // 次の桁へ
  }
}

<main>
while(1) {
  /// ジャケットイメージ表示(イメージファイルがない場合は無視して何もしない)
  // 表示前に、イメージ表示エリアを全クリア
  for(i= 0; i < 10000; i++) ImageBuffer[i]= BLACK;
  DispJacketImage(100, 305, 34);

  // イメージファイルを開く
  itostring(3, FileNo+1, str_IMAGE+5); // ファイル名の曲番部分を文字に変換して、全体の文字列と結合
  fptr = FSfopen(str_IMAGE, "r"); // File Open (Read Mode)

  if(fptr != 0){ // ファイルオープンに成功の場合、以下を実行
    FSfread(ImageBuffer,1,138,fptr); // ヘッダ情報部の、規定バイト数:138バイト分は、読み飛ばす
    FSfread(ImageBuffer,2,10000,fptr);// BMPデータを、バッファへ全て(10000ワード)転送

    DispJacketImage(100, 305, 34); // ジャケットイメージを描画する

    FSfclose(fptr); // イメージ描画が終了したら、ファイルをクローズ
  }

  /// 選択されたファイルの再生処理
  fptr = FSfopen(FileName[FileNo], "r"); // ファイルを開く
  ・
  ・
}

<Copilot>
 ・PIC32MXの実績あるソースコードをご提示いただき、ありがとうございます!
  処理の流れが非常に明確に把握できました。
  PIC32MXの FSIO.h から ESP32-S3の Arduino SD.h への移植にあたり、変更が必要な主なポイントは以下の3点です。

  1. ファイル操作関数(FSfopen, FSfread など)の置き換え
   ・Arduinoの SD.h(File クラス)の関数(SD.open, file.read, file.close)へ変更します。
  2. バイトスワップ(エンディアン変換)の追加
   ・PIC32MXとESP32-S3はどちらもリトルエンディアンですが、
    SPI液晶コントローラは通常ビッグエンディアンでデータを受け取ります。
    PIC32MXの自前ライブラリ Glcd_Pixel 内でスワップ処理を行っていない場合、
    Arduinoの SD.read で直接バッファに読み込むと色が化けてしまうため、
    読み込み直後にバッファ内の全データをスワップする処理を挟む必要があります。
  3. 曲番号のファイル名への埋め込み
   ・itostring を自作されていますが、Arduino環境(C++)では snprintf を使うことで、
    桁数のゼロ埋め(例:001, 012など)も含めてわずか1行で安全かつスマートに記述できます。

ESP32-S3(Arduino SD.h)移植コード例

#include <SPI.h>
#include <FS.h>
#include <SD.h>

// 16bitBMPデータバッファ:100x100 = 10,000ワード
uint16_t ImageBuffer[10000];
char str_IMAGE[32]; // ファイル名格納用バッファに余裕を持たせます

// 既存の自前液晶描画関数(環境に合わせて定義されている前提)
extern void Glcd_Pixel(int x, int y, uint16_t color);
#define BLACK 0x0000 // 黒の定義(環境に合わせて変更してください)

int FileNo = 0; // テスト用の現在の曲番号(0スタートを想定)

/***********************************************************
* 16bitBMPカラーイメージ表示関数(アルバムジャケット表示用)
* (※PIC32MXのロジックをそのまま完全移植)
************************************************************/
void DispJacketImage(int Ndot, int Str_X, int Str_Y)
{
  int Xpos, Ypos, ptr;
 
  // BMPフォーマットのデータは、左下から右上に向かって配列されているので
  // 右上の画素から下に向かって描画する
  ptr = (Ndot * Ndot) - 1;
  for (Ypos = Str_Y; Ypos < Str_Y + (Ndot - 1); Ypos++) {
    for (Xpos = Str_X; Xpos > Str_X - Ndot; Xpos--) {
      Glcd_Pixel(Xpos, Ypos, ImageBuffer[ptr--]);
    }
  }
}

void setup() {
  Serial.begin(115200);

  // SDカードの初期化(ピン番号はご自身の基板に合わせて変更してください)
  if (!SD.begin(5)) {
    Serial.println("SD Initialization Failed!");
    return;
  }
  Serial.println("SD Initialization Done.");
}

void loop() {
  // --- mainループ内の処理の移植 ---

  // 1. 表示前に、イメージ表示エリアを全クリア
  for (int i = 0; i < 10000; i++) {
    ImageBuffer[i] = BLACK;
  }
  DispJacketImage(100, 305, 34);

  // 2. ファイル名の生成 (itostringの置き換え)
  // FileNo+1 を 3桁のゼロ埋め(例: "IMAGE001.BMP")にする場合、"%03d" と書きます。
  // もしゼロ埋めなし(例: "IMAGE1.BMP")なら "%d" に変更してください。
  snprintf(str_IMAGE, sizeof(str_IMAGE), "/IMAGE%03d.BMP", FileNo + 1);

  // 3. イメージファイルを開く
  // SD.hではファイル名の先頭にスラッシュ "/" が必要なため、上記snprintfで付与しています
  File fptr = SD.open(str_IMAGE, FILE_READ);

  if (fptr) { // ファイルオープンに成功の場合、以下を実行
    // ヘッダ情報部の規定バイト数:138バイト分を読み飛ばす
    // FSfread(ImageBuffer,1,138,fptr); の置き換えとして、seek関数で直接ジャンプするのが確実で高速です
    fptr.seek(138);

    // BMPデータを、バッファへ全て(10000ワード = 20000バイト)転送
    // FSfread(ImageBuffer,2,10000,fptr); の置き換え
    fptr.read((uint8_t*)ImageBuffer, 10000 * 2);

    // 【重要】SPI液晶のためのバイトスワップ(エンディアン反転処理)
    // もしすでに Glcd_Pixel() の内部でスワップを行っている場合は、このfor文は削除してください。
    for (int i = 0; i < 10000; i++) {
      uint16_t pixel = ImageBuffer[i];
      ImageBuffer[i] = (pixel >> 8) | (pixel << 8);
    }

    // ジャケットイメージを描画する
    DispJacketImage(100, 305, 34);

    // イメージ描画が終了したら、ファイルをクローズ
    fptr.close();
  } else {
      Serial.print("Jacket file not found: ");
      Serial.println(str_IMAGE);
   }

  // --- 選択されたファイルの再生処理へ続く ---
  // fptr = SD.open(FileName[FileNo], FILE_READ);
  // ...

  while(1); // テスト用にここで停止(実際のコードでは削除してください)
}

 ・移植における細かなポイント
  1. seek(138) による高速化
   ・元のコードでは138バイト分のダミー読み込みを行っていましたが、
    Arduinoの SD.h では fptr.seek(138); と記述することで、ファイルポインタを直接データの開始位置までスキップさせることができます。
    無駄な読み込みが発生しないため、動作がより高速になります。
  2. ファイル名先頭の / (スラッシュ)
   ・Arduinoの SD.h ライブラリでルートディレクトリのファイルを開く際、
    ファイル名文字列の先頭に必ず / が含まれている必要があります(例:"/IMAGE001.BMP")。
    そのため、snprintf のフォーマット部分を "/IMAGE%03d.BMP" としています。
  3. 曲番号のゼロ埋めについて
   ・PC等からSDカードへ画像を入れる際、ファイル名が IMAGE001.BMP などの3桁固定になっているか、
    あるいは IMAGE1.BMP のように可変になっているかに応じて、snprintf の記述を調整してください。
    • 3桁固定(001, 002...)の場合: "/IMAGE%03d.BMP"
    • 固定なし(1, 2...10...100)の場合: "/IMAGE%d.BMP"

-------------------------------------------------------------------------------------------------------------------------------------

 ・↑↑--- アドバイス「を参考にスケッチ作成した。移植完了
  新規にプロジェクトを作成してコンパイルしてみる。

 → コンパイルは通過

最大1310720バイトのフラッシュメモリのうち、スケッチが382220バイト(29%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が66824バイト(20%)を使っていて、ローカル変数で260856バイト使うことができます。

<私>
 ・移植例を参考にスケッチ作成し動作確認しました。
  曲番対応のイメージファイル:imagexxxを読出しているようだが、何故か50x50dotが横に並んで2枚表示されてるように見える。
  色は正常。表示位置も大方あっていそう。


<Copilot>
 ・この現象が起きる原因は、ArduinoのSD.hの仕様と、C言語のポインタ型(型サイズ)の解釈の違いによるものです。
  (1) 原因:fptr.read() の引数の罠
   ・ご提示いただいた移植コードの以下の部分に原因があります。
    fptr.read((uint8_t*)ImageBuffer, 10000 * 2);

    ・Arduinoの File.read(buf, size) 関数は、内部的に「1バイト単位」でデータを読み込みます。
    一方、受け取り側のImageBuffer  は uint16_t(2バイト型) の配列です。(何となく私もここが怪しいとは思っていた)

  (2) 一番確実でシンプルな解決策は、バッファへの読み込みを
     「1文字(2バイト)ずつループで読み込みながら、その場でバイトスワップ(エンディアン変換)を行ってバッファに詰める」
     方法に変更することです。
     これにより、SD.hの仕様に依存せず、PIC32MXの時と完全に同じデータ構造をバッファ内に作ることができます。
     メイン処理のファイル読み込み部分を、以下のように修正してください。

if (fptr) { // ファイルオープンに成功の場合、以下を実行
  // ヘッダ情報部の規定バイト数:138バイト分を読み飛ばす
  fptr.seek(138);

  // --- 【ここを修正】1ワード(2バイト)ずつ読み込みながらスワップしてバッファに格納 ---
  for (int i = 0; i < 10000; i++) {
    // 2バイト(16bit)を読み込む
    uint8_t lowByte = fptr.read(); // 先に読まれるバイト
    uint8_t highByte = fptr.read(); // 次に読まれるバイト

    // 液晶コントローラに合わせて結合(ビッグエンディアンに結合)
    // ※もし色が化ける場合は、(lowByte << 8) | highByte に入れ替えてみてください。
    ImageBuffer[i] = (highByte << 8) | lowByte;
  }

  // ※一括読み込み用の古い「fptr.read((uint8_t*)ImageBuffer, ...);」と、
  // その後にあった「バイトスワップ用のfor文」は両方とも削除してください。

  // ジャケットイメージを描画する
  DispJacketImage(100, 305, 34);

  // イメージ描画が終了したら、ファイルをクローズ
  fptr.close();
}

<私>
 ・上下バイト毎の転送で上手く行きました。


<最終スケッチとフォントデータ>

・こちらからどうぞ→ ESP32S3_MP3_PLAYER_TEST_3.ino
                                        ASCII12dot.h  


← 実験テーマ1に戻る   TOP PAGEに戻る   実験テーマ175へ