ESP32-S3 で GC9A01 円形ディスプレイに心形線を描く|極座標アニメーション 30 分で完成

ESP32-S3 で 1.28 インチ GC9A01 円形 TFT ディスプレイを駆動し、極座標心形線アニメーションを動かす。完全な配線図、ダブルバッファによるチラつきゼロのコード、そしてトラブルシューティングガイド付き。

ESP32-S3 で GC9A01 1.28 インチ円形ディスプレイを駆動する完全チュートリアル(SPI + Arduino IDE)

難易度:⭐⭐☆☆☆(初心者でも挑戦可能) 所要時間:30 分 テスト環境: Arduino IDE 2.3.8 Arduino_GFX_Library 1.6.5 ESP32 Arduino Core 3.3.8


一言でいうと:ESP32-S3 で 1.28 インチ GC9A01 円形ディスプレイを駆動し、極座標心形線アニメーションを動かす。ダブルバッファでチラつきゼロ、配線図+完全なコード+トラブルシューティング付き、30 分で完成。


はじめに

520(中国語のネットスラングで「愛してる」を意味する日)がやって来る。彼女にどんなプレゼントを贈ろうか?いくら考えても名案が浮かばない。

そこでふと思い出したのが、高校で極座標を学んだとき教科書に載っていた「心臓線(Cardioid)」という曲線のこと。極座標のデモアニメーションを作って、ハートを描き出し、自分の気持ちを伝えられないか。(理系男子が脳内であらゆるシーンを想像し、ひとり興奮中……)

本記事の目標:ゼロから始めて、30 分以内に ESP32-S3 でこの 1.28 インチ円形ディスプレイを駆動し、極座標アニメーションを動かすこと。ついでに各ステップの「なぜそうするのか」もしっかり理解する。(PS:気になる相手にプレゼントした後、正座して謝る羽目になりませんように! :P )

(このハートを見た彼女の心の中:「これ何……?!」~※急いでドリアンを用意)


実験結果

円形ディスプレイ上に回転する**心形線(Cardioid)**がリアルタイムで描画される。極座標系のグリッドと追跡点が付き、まるで小型オシロスコープが数学曲線を描いているようだ。全程チラつきゼロ、フレームレートは 16fps に固定されスムーズに動作する。



部品説明

GC9A01 1.28 インチ円形 TFT ディスプレイ

GC9A01 はドライバチップ、円形 IPS パネルがディスプレイ本体で、両者が同じ小さなモジュール上にはんだ付けされている。SPI プロトコルで画像データを「送り込む」だけで、各ピクセルを点灯してくれる。

パラメータ
解像度240 × 240 ピクセル
色深度16-bit RGB565、65536 色
インターフェース4 線式 SPI、最高 80MHz
動作電圧3.3V(ESP32-S3 に直接接続可能、レベル変換不要)
パネルタイプIPS、視野角はほぼ 180°
モジュールサイズ約 36mm 直径

選んだ理由:安い(5~15 元)、入手性が良い、円形という形状がダッシュボードや時計系プロジェクトにぴったりで、240×240 の解像度は ESP32-S3 のメモリ負荷にもちょうどよい。


BOM 表

部品数量備考
ESP32-S3 開発ボード1SPI ピンがあるバージョンなら何でも可
GC9A01 1.28” 円形ディスプレイモジュール1モジュールに BL ピンがあることを確認
ジャンパーワイヤー適量メス‑メスまたはメス‑オス、開発ボードのピン形式に合わせて

部品ピン説明

GC9A01 モジュールピン機能
VCC電源正極(3.3V)
GND電源負極
SCL / CLKSPI クロック信号
SDA / MOSISPI データ入力(マスター→スレーブ)
CSチップセレクト、Low 時にディスプレイが SPI に応答
DCデータ/コマンド選択:High=データ、Low=コマンド
RSTハードウェアリセット、Low でトリガー
BLバックライト制御、High に接続しないと画面が点灯しない

配線方法

下表の通りに 1 本ずつ接続し、接続するごとにチェックマークを付けると、トラブルシューティングの時間を 80% 削減できる。

GC9A01 ディスプレイESP32-S3
VCC3.3V
GNDGND
SCL / CLKGPIO12
SDA / MOSIGPIO11
CSGPIO9
DCGPIO10
RSTGPIO18
BLGPIO7(コード制御)または直接 3.3V に接続

⚠️ 注意:BL(バックライト)ピンは接続し忘れが多い。接続を忘れると通電しても画面が真っ暗になり、コードの問題でもディスプレイの故障でもない。まずはここを確認すること。一部のモジュールには BL ピンが引き出されていないが、その場合はモジュール内部で 3.3V に接続済みなので気にする必要はない。


必要なライブラリ

Arduino IDE → ツール → ライブラリを管理 を開き、検索してインストール:

ライブラリ名作者テスト済みバージョン
Arduino_GFX_Librarymoononournation1.6.5

TFT_eSPI はインストールしないこと:ESP32 Core 3.x では、TFT_eSPI のマクロ定義や DMA 初期化が新しい ESP32 と競合し、コンパイルエラーや起動時のフリーズが発生する。Arduino_GFX_Library は最初からモダンな C++ とメモリキャンバスをサポートしており、現在ディスプレイプロジェクトで最も手間のかからない選択肢だ。(執筆時点:2026-05-18)


完全なコード

/**
 * ESP32-S3 + GC9A01 1.28" 円形ディスプレイ — 極座標アニメーションデモ
 * ダブルバッファでチラつきゼロ、16fps に固定
 * 配線:SCL=GPIO12, SDA=GPIO11, CS=GPIO9, DC=GPIO10, RST=GPIO18, BL=GPIO7
 */

#include <Arduino_GFX_Library.h>

// ---------------------------------------------------
// ステップ1:カラーマクロを手動で定義
// 新版 Arduino_GFX では BLACK / WHITE などのグローバルエクスポートが廃止され、
// この定義がないと "BLACK was not declared in this scope" でコンパイルエラーになる
// ---------------------------------------------------
#ifndef BLACK
#define BLACK       0x0000
#endif
#ifndef WHITE
#define WHITE       0xFFFF
#endif
#ifndef RED
#define RED         0xF800
#endif
#ifndef GREEN
#define GREEN       0x07E0
#endif
#ifndef BLUE
#define BLUE        0x001F
#endif
#ifndef YELLOW
#define YELLOW      0xFFE0
#endif
#ifndef CYAN
#define CYAN        0x07FF
#endif
#ifndef MAGENTA
#define MAGENTA     0xF81F
#endif
#ifndef GRAY
#define GRAY        0x8410
#endif
#ifndef DARKGRAY
#define DARKGRAY    0x2104
#endif

// ---------------------------------------------------
// ステップ2:カラースキームを定義(濃紺背景 + オレンジ赤のメインカラー)
// ---------------------------------------------------
#define COLOR_BG        0x1123   // 濃紺黒の背景
#define COLOR_GRID      0x19E5   // グリッドの青灰色
#define COLOR_PRIMARY   0xE73C   // 曲線のオレンジ赤
#define COLOR_ACCENT    0xFDE0   // 動径の黄金色
#define COLOR_TEXT      0xF7BE   // テキストの薄い灰色

// ---------------------------------------------------
// ステップ3:物理ピンを定義
// ---------------------------------------------------
#define TFT_SCK  12
#define TFT_SDA  11
#define TFT_CS    9
#define TFT_DC   10
#define TFT_RST  18
#define TFT_BL    7

// ---------------------------------------------------
// ステップ4:SPI バスとディスプレイドライバをインスタンス化
// ---------------------------------------------------
Arduino_DataBus *bus = new Arduino_ESP32SPI(
    TFT_DC, TFT_CS, TFT_SCK, TFT_SDA, GFX_NOT_DEFINED /* MISO は不要 */
);

Arduino_GFX *gfx = new Arduino_GC9A01(
    bus, TFT_RST,
    0,    /* 回転角度 */
    true  /* IPS ディスプレイ */
);

// ---------------------------------------------------
// ステップ5:ダブルバッファキャンバスを割り当て(240×240×2 Bytes = 115.2KB SRAM)
// すべての描画はまずメモリに書き込み、完了後に一括でディスプレイに転送してチラつきを完全に解消
// ---------------------------------------------------
Arduino_Canvas *canvas = new Arduino_Canvas(240, 240, gfx);

// ---------------------------------------------------
// アニメーション変数
// ---------------------------------------------------
float angle = 0.0f;
const float  a_scale    = 50.0f;  // 心形線のスケール係数(単位:ピクセル)
const int16_t cx        = 120;    // 中心 X
const int16_t cy        = 120;    // 中心 Y

unsigned long lastFrameTime = 0;
const int frameDelay = 1000 / 16; // 16fps に固定

// 機能スイッチ(false に変更すると各レイヤーを個別にオフにできる)
const bool showGrid     = true;
const bool showCurve    = true;
const bool showRadius   = true;
const bool showTelemetry= true;

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

    // ディスプレイドライバを初期化
    gfx->begin();

    // バックライトを点灯(これを忘れると = 画面が真っ暗)
    pinMode(TFT_BL, OUTPUT);
    digitalWrite(TFT_BL, HIGH);

    // ダブルバッファキャンバスを初期化
    if (!canvas->begin()) {
        Serial.println("キャンバスのメモリ割り当てに失敗!直接描画モードに切り替え(チラつきが発生します)");
    } else {
        Serial.println("ダブルバッファ起動成功、チラつきゼロレンダリング準備完了。");
    }
}

void loop() {
    // フレームレート制限
    unsigned long now = millis();
    if (now - lastFrameTime < frameDelay) return;
    lastFrameTime = now;

    // フレームをクリア
    canvas->fillScreen(COLOR_BG);

    // --- レイヤー1:極座標グリッド ---
    if (showGrid) {
        canvas->drawCircle(cx, cy,  30, COLOR_GRID);
        canvas->drawCircle(cx, cy,  60, COLOR_GRID);
        canvas->drawCircle(cx, cy,  90, COLOR_GRID);
        canvas->drawCircle(cx, cy, 110, COLOR_GRID);
        canvas->drawFastHLine(10, cy, 220, COLOR_GRID);
        canvas->drawFastVLine(cx, 10, 220, COLOR_GRID);
    }

    // --- レイヤー2:完全な心形線の軌跡 r = a*(1 - cos θ) ---
    if (showCurve) {
        int16_t lx = 0, ly = 0;
        for (int16_t deg = 0; deg <= 360; deg += 3) {
            float rad = deg * DEG_TO_RAD;
            float r   = a_scale * (1.0f - cos(rad));
            int16_t x = cx + (int16_t)(r * cos(rad));
            int16_t y = cy - (int16_t)(r * sin(rad)); // 画面の Y 軸は下向きなので反転
            if (deg > 0) canvas->drawLine(lx, ly, x, y, COLOR_PRIMARY);
            lx = x; ly = y;
        }
    }

    // --- レイヤー3:現在の追跡点と動径 ---
    float rad_a  = angle * DEG_TO_RAD;
    float active_r = a_scale * (1.0f - cos(rad_a));
    int16_t px = cx + (int16_t)(active_r * cos(rad_a));
    int16_t py = cy - (int16_t)(active_r * sin(rad_a));

    if (showRadius) canvas->drawLine(cx, cy, px, py, COLOR_ACCENT);
    canvas->fillCircle(px, py, 5, COLOR_TEXT);

    // --- レイヤー4:数値表示 ---
    if (showTelemetry) {
        canvas->setTextColor(COLOR_TEXT);
        canvas->setTextSize(1);
        canvas->setCursor(50, 25);
        canvas->print("Polar Coordinates");
        canvas->setCursor(28, 185);
        canvas->print("r = a * (1 - cos(theta))");
        canvas->setCursor(40, 200);
        canvas->print("th:"); canvas->print((int)angle);
        canvas->print("  r:"); canvas->print((int)active_r);
        canvas->print("px");
    }

    // 角度を進める(毎フレーム +6°、1 周約 1 秒)
    angle += 6.0f;
    if (angle >= 360.0f) angle -= 360.0f;

    // メモリキャンバスの内容を物理ディスプレイに一括転送
    canvas->flush();
}

コードの解説

ダブルバッファの仕組み:すべての描画操作は canvas(メモリ)上で行われ、最後の canvas->flush() で初めて完成したフレームがディスプレイに送信される。黒板を消してから書くのではなく、下書き用紙に書いてから丸ごと貼り付けるようなもの。ディスプレイ側には「描きかけ」の状態が一切見えず、チラつきはゼロになる。

心形線の方程式 r = a * (1 - cos θ):これは極座標方程式で、r は中心からの距離、θ は角度を表す。方程式の各 θ に対して (r, θ) を計算し、画面の XY 座標に変換して線でつなぐと、あの心形曲線が描ける。

フレームレートロックframeDelay = 1000 / 16 により、各フレームの最小間隔を約 62ms に制御している。アニメーションを速くしたい場合は += 6.0f のステップ値を大きくする。より滑らかにしたい場合は targetFPS を 30 に上げてもよいが、CPU 負荷が増える。

書き込みパーティション:Arduino IDE → ツール → Partition Scheme で Huge APP (3MB No OTA) を選択すること。115KB の Canvas には十分な SRAM が必要で、デフォルトのパーティションではヒープ容量不足に陥ることがある。


よくある問題のトラブルシューティング

焦らないで。90% の問題は以下の場所にある:

通電しても画面が真っ暗、シリアルにもエラーなし まず BL ピンを確認。バックライトが High になっていないのが最も一般的な原因。GPIO7 で digitalWrite(TFT_BL, HIGH) が実行されているか確認するか、BL ジャンパーを直接 3.3V に接続してコードの問題を切り分ける。

画面は点灯するが全面が白/赤/ノイズ SPI の配線順序が間違っている。CS と DC が最も混同しやすい(どちらも制御線で見た目が同じ)。コード内のマクロ定義(CS=GPIO9, DC=GPIO10)と照合し、配線表ではなくコードを正とすること。

コンパイルエラー:BLACK was not declared in this scope 使用している Arduino_GFX のバージョンが 1.3 以上の場合、新版ではカラーマクロのグローバルエクスポートが廃止されている。コード先頭の #ifndef BLACK のセクションは必ず残すこと。削除してはいけない。

Canvas のメモリ割り当てに失敗、シリアルに直接描画のメッセージが表示される 利用可能な SRAM が 115KB に満たないということ。以下を確認:①パーティションで Huge APP を選択しているか。②他の場所で大きな配列がメモリを占有していないか。③稀に、開発ボードの PSRAM が有効になっていないケースがある(Board 設定で PSRAM を有効にする必要がある)。

アニメーションがカクつく、16fps に見えない loop() の中に delay() を入れていないか? 入れているなら削除すること。フレームレート制限はすでに millis() で実装されており、両方を重ねるとフレーム間隔が倍になってしまう。


FAQ

Q:CS、DC ピンを他の GPIO に変更できるか? A:できる。コード先頭の #define TFT_CS#define TFT_DC を変更すればよい。空いている GPIO なら何でも使える。SCL と SDA はハードウェア SPI ピン(ESP32-S3 のデフォルト SPI2:SCLK=12、MOSI=11)の使用を推奨。最高速度が出る。他のピンに変更するとソフトウェア SPI にフォールバックし、速度が明確に低下する。

Q:ディスプレイの対応リフレッシュレートは? A:GC9A01 の SPI インターフェースは理論上最高クロック 80MHz で、フルスクリーン 240×240 のリフレッシュレートは約 40fps が上限。本コードでは中低端の ESP32-S3 モジュールでも CPU に余裕を持たせるため 16fps に固定している。ボードの動作周波数が 240MHz であれば、targetFPS を 30~40 に引き上げても問題ない。

Q:2 枚のディスプレイを同時に駆動できるか? A:できる。2 枚のディスプレイで SCL/SDA を共有し、それぞれに独立した CS ピンを割り当てる。2 つの Arduino_GC9A01 オブジェクトを個別にインスタンス化し、CS を切り替えてアクティブなディスプレイを切り替える。ただしメモリに注意:2 つの Canvas で合計 230KB の SRAM が必要になるため、PSRAM の有効化が必須。

Q:電源は 3.3V と 5V のどちらを使うべきか? A:GC9A01 モジュールの動作電圧は 3.3V なので、ESP32-S3 の 3.3V ピンに直接接続する。5V を接続するとドライバチップが破損するので絶対にやめること。

Q:中国語文字を表示するにはどうすればよいか? A:Arduino_GFX_Library はデフォルトで ASCII フォントのみ内蔵している。中国語の表示には追加のフォントファイル(U8g2 フォントなど)か、LVGL フレームワークの使用が必要。フォントデータは Flash の使用量が大幅に増えるため、LVGL + SPIFFS の構成を推奨。別記事で解説する予定。

Q:GC9A01 ディスプレイには音声出力機能がなく、表示のみだが、I2S オーディオプロジェクトとどう関係があるのか? A:関係ない。GC9A01 は純粋なディスプレイであり、SPI インターフェースは画像データのみを転送する。同時にオーディオを再生したい場合は、別途 I2S DAC モジュール(MAX98357A など)が必要。両者は完全に独立して動作し、ピンも干渉しない。


応用アイデア

  • アナログ時計の文字盤に改造:目盛りと針を描画し、DS3231 RTC モジュールでリアルタイムの時刻を読み取る
  • ローズ曲線モードを追加:showTangent を false に変更し、曲線を r = a * sin(k * θ) に切り替える。パラメータ k の値を変えると花びらの数も変わる
  • ボタンでアニメーションテーマを切り替え:3 つのボタンで心形線 / ローズ曲線 / リサージュ図形を切り替え表示
  • ESP32 Wi-Fi と組み合わせる:天気 API からデータを取得し、円形ディスプレイのダッシュボードに温度・湿度を表示
  • 円形ディスプレイを 2 個購入する:

参考資料