● 実験テーマ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へ