Arduino 環境で LVGL を試したくて、ディスプレイや SD カードの面倒な配線が不要な “黄色い基板”(以下 CYD)を購入しました。ただ最初に入手した品がハズレだったため、結果的に2種類の2.8インチ版を手にする事に…。

どうせならと言うことで、LVGL に深入りする前に CYD を使いこなす上での次の2つのお題を片付けておく事にしました。

  • 2種類の LCD ドライバ IC(ILI9341、ST7789)の SPI クロック周波数と描画速度の比較
  • 2本しか無い SPI バスで3つのデバイス(LCD、タッチパネル、SD カード)を扱う方法

今回はまず、前者に関する調査結果を報告したいと思います。

ILI9341 と ST7789 の SPI クロック周波数

CYD のように LCD を搭載したボード限定ですが、僕がお気に入りの LovyanGFX には、自動でボードの種類を識別し、パラメータ を設定してくれるとても便利な機能があります。

#define LGFX_AUTODETECT
#include <LovyanGFX.h>

この仕組みを利用し、設定されたパラメータをダンプするよう コード に手を加えて得られたのが次の2つです。1つ目は Micro-USB が1つのタイプ(ILI9341)、2つ目は Micro-USB と USB-C の2つを備えたタイプ(ST7789)です。

SPI のピン番号は、ボードタイプに ESP32-2432S028R CYD を設定すると読み込まれる pins_arduino.h の定義済みシンボルに置き換えています。

LGFX_ESP32_2432S028R_CYD_1USB.hpp (ILI9341)
#pragma once

#include <LovyanGFX.hpp>

// ESP32-2432S028R で LovyanGFX を独自設定で利用する場合の設定
// ファイル名例:LGFX_ESP32_2432S028R_CYD_1USB.hpp
//
// ピン設定は、以下を元にしています
// ボードタイプ:ESP32_2432S028R CYD
// https://github.com/espressif/arduino-esp32/blob/master/variants/jczn_2432s028r/pins_arduino.h

/// 独自の設定を行うクラスを、LGFX_Deviceから派生して作成します。
class LGFX : public lgfx::LGFX_Device
{
  lgfx::Panel_ILI9341   _panel_instance;  // 接続するパネルの型にあったインスタンスを用意します。
  lgfx::Bus_SPI         _bus_instance;    // SPIバスのインスタンス
  lgfx::Light_PWM       _light_instance;  // バックライト制御が可能な場合はインスタンスを用意します。
  lgfx::Touch_XPT2046   _touch_instance;  // タッチスクリーンの型にあったインスタンスを用意します。

public:
  // コンストラクタを作成し、ここで各種設定を行います。
  LGFX(void)
  {
    { // バス制御の設定を行います。
      auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。

      cfg.spi_host = HSPI_HOST;           // 使用するSPIを選択  ESP32-S2,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
      cfg.spi_mode = 0;                   // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000;          // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      cfg.freq_read  = 16000000;          // 受信時のSPIクロック
      cfg.spi_3wire  = false;             // 受信をMOSIピンで行う場合はtrueを設定
      cfg.use_lock   = true;              // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO;  // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)
      cfg.pin_sclk = CYD_TFT_SCK;         // SPIのSCLKピン番号を設定 (14)
      cfg.pin_mosi = CYD_TFT_MOSI;        // SPIのMOSIピン番号を設定 (13)
      cfg.pin_miso = CYD_TFT_MISO;        // SPIのMISOピン番号を設定 (12)
      cfg.pin_dc   = CYD_TFT_DC;          // SPIのD/Cピン番号を設定  (2)

      _bus_instance.config(cfg);          // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。
    }

    { // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs           = CYD_TFT_CS;    // CSが接続されているピン番号   (15)
      cfg.pin_rst          = -1;            // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy         = -1;            // BUSYが接続されているピン番号 (-1 = disable)

      cfg.panel_width      =   240;  // 実際に表示可能な幅
      cfg.panel_height     =   320;  // 実際に表示可能な高さ
      cfg.offset_x         =     0;  // パネルのX方向オフセット量
      cfg.offset_y         =     0;  // パネルのY方向オフセット量
      cfg.offset_rotation  =     2;  // 回転方向の値のオフセット 0~7 (4~7は上下反転)
      cfg.dummy_read_pixel =     8;  // ピクセル読出し前のダミーリードのビット数
      cfg.dummy_read_bits  =     1;  // ピクセル以外のデータ読出し前のダミーリードのビット数
      cfg.readable         =  true;  // データ読出しが可能な場合 trueに設定
      cfg.invert           = false;  // パネルの明暗が反転してしまう場合 trueに設定
      cfg.rgb_order        = false;  // パネルの赤と青が入れ替わってしまう場合 trueに設定
      cfg.dlen_16bit       = false;  // 16bitパラレルやSPIでデータ長を16bit単位で送信するパネルの場合 trueに設定
      cfg.bus_shared       = false;  // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います)

      cfg.memory_width     =   240;  // ドライバICがサポートしている最大の幅
      cfg.memory_height    =   320;  // ドライバICがサポートしている最大の高さ

      _panel_instance.config(cfg);
    }

    { // バックライト制御の設定を行います。
      auto cfg = _light_instance.config();  // バックライト設定用の構造体を取得します。

      cfg.pin_bl = CYD_TFT_BL;      // バックライトが接続されているピン番号 (21)
      cfg.invert = false;           // バックライトの輝度を反転させる場合 true
      cfg.freq   = 12000;           // バックライトのPWM周波数
      cfg.pwm_channel = 7;          // 使用するPWMのチャンネル番号

      _light_instance.config(cfg);
      _panel_instance.setLight(&_light_instance);  // バックライトをパネルにセットします。
    }

    { // タッチスクリーン制御の設定を行います。
      auto cfg = _touch_instance.config();

      cfg.x_min      =  240;        // タッチスクリーンから得られる最小のX値(生の値)
      cfg.x_max      = 3800;        // タッチスクリーンから得られる最大のX値(生の値)
      cfg.y_min      = 3700;        // タッチスクリーンから得られる最小のY値(生の値)
      cfg.y_max      =  200;        // タッチスクリーンから得られる最大のY値(生の値)
      cfg.pin_int    = CYD_TP_IRQ;  // INTが接続されているピン番号 (36)
      cfg.bus_shared = false;       // 画面と共通のバスを使用している場合 trueを設定
      cfg.offset_rotation = 0;      // 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定

      cfg.spi_host = -1;            // 使用するSPIを選択 (HSPI_HOST or VSPI_HOST)
      cfg.freq = 1000000;           // SPIクロックを設定
      cfg.pin_sclk = CYD_TP_CLK;    // SCLKが接続されているピン番号 (25)
      cfg.pin_mosi = CYD_TP_MOSI;   // MOSIが接続されているピン番号 (32)
      cfg.pin_miso = CYD_TP_MISO;   // MISOが接続されているピン番号 (39)
      cfg.pin_cs   = CYD_TP_CS;     //   CSが接続されているピン番号 (33)

      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);  // タッチスクリーンをパネルにセットします。
    }

    setPanel(&_panel_instance); // 使用するパネルをセットします。
  }
};
LGFX_ESP32_2432S028R_CYD_2USB.hpp (ST7789)
#pragma once

#include <LovyanGFX.hpp>

// ESP32-2432S028R で LovyanGFX を独自設定で利用する場合の設定
// ファイル名例:LGFX_ESP32_2432S028R_CYD_1USB.hpp
//
// ピン設定は、以下を元にしています
// ボードタイプ:ESP32_2432S028R CYD
// https://github.com/espressif/arduino-esp32/blob/master/variants/jczn_2432s028r/pins_arduino.h

/// 独自の設定を行うクラスを、LGFX_Deviceから派生して作成します。
class LGFX : public lgfx::LGFX_Device
{
  lgfx::Panel_ST7789    _panel_instance;  // 接続するパネルの型にあったインスタンスを用意します。
  lgfx::Bus_SPI         _bus_instance;    // SPIバスのインスタンス
  lgfx::Light_PWM       _light_instance;  // バックライト制御が可能な場合はインスタンスを用意します。
  lgfx::Touch_XPT2046   _touch_instance;  // タッチスクリーンの型にあったインスタンスを用意します。

public:
  // コンストラクタを作成し、ここで各種設定を行います。
  LGFX(void)
  {
    { // バス制御の設定を行います。
      auto cfg = _bus_instance.config();  // バス設定用の構造体を取得します。

      cfg.spi_host = HSPI_HOST;           // 使用するSPIを選択  ESP32-S2,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
      cfg.spi_mode = 0;                   // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 80000000;          // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      cfg.freq_read  = 16000000;          // 受信時のSPIクロック
      cfg.spi_3wire  = false;             // 受信をMOSIピンで行う場合はtrueを設定
      cfg.use_lock   = true;              // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO;  // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)
      cfg.pin_sclk = CYD_TFT_SCK;         // SPIのSCLKピン番号を設定 (14)
      cfg.pin_mosi = CYD_TFT_MOSI;        // SPIのMOSIピン番号を設定 (13)
      cfg.pin_miso = CYD_TFT_MISO;        // SPIのMISOピン番号を設定 (12)
      cfg.pin_dc   = CYD_TFT_DC;          // SPIのD/Cピン番号を設定  (2)

      _bus_instance.config(cfg);          // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。
    }

    { // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs           = CYD_TFT_CS;    // CSが接続されているピン番号   (15)
      cfg.pin_rst          = -1;            // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy         = -1;            // BUSYが接続されているピン番号 (-1 = disable)

      cfg.panel_width      =   240;  // 実際に表示可能な幅
      cfg.panel_height     =   320;  // 実際に表示可能な高さ
      cfg.offset_x         =     0;  // パネルのX方向オフセット量
      cfg.offset_y         =     0;  // パネルのY方向オフセット量
      cfg.offset_rotation  =     0;  // 回転方向の値のオフセット 0~7 (4~7は上下反転)
      cfg.dummy_read_pixel =    16;  // ピクセル読出し前のダミーリードのビット数
      cfg.dummy_read_bits  =     1;  // ピクセル以外のデータ読出し前のダミーリードのビット数
      cfg.readable         =  true;  // データ読出しが可能な場合 trueに設定
      cfg.invert           = false;  // パネルの明暗が反転してしまう場合 trueに設定
      cfg.rgb_order        = false;  // パネルの赤と青が入れ替わってしまう場合 trueに設定
      cfg.dlen_16bit       = false;  // 16bitパラレルやSPIでデータ長を16bit単位で送信するパネルの場合 trueに設定
      cfg.bus_shared       = false;  // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います)

      cfg.memory_width     =   240;  // ドライバICがサポートしている最大の幅
      cfg.memory_height    =   320;  // ドライバICがサポートしている最大の高さ

      _panel_instance.config(cfg);
    }

    { // バックライト制御の設定を行います。
      auto cfg = _light_instance.config();  // バックライト設定用の構造体を取得します。

      cfg.pin_bl = CYD_TFT_BL;      // バックライトが接続されているピン番号 (21)
      cfg.invert = false;           // バックライトの輝度を反転させる場合 true
      cfg.freq   = 12000;           // バックライトのPWM周波数
      cfg.pwm_channel = 7;          // 使用するPWMのチャンネル番号

      _light_instance.config(cfg);
      _panel_instance.setLight(&_light_instance);  // バックライトをパネルにセットします。
    }

    { // タッチスクリーン制御の設定を行います。
      auto cfg = _touch_instance.config();

      cfg.x_min      =  240;        // タッチスクリーンから得られる最小のX値(生の値)
      cfg.x_max      = 3800;        // タッチスクリーンから得られる最大のX値(生の値)
      cfg.y_min      = 3700;        // タッチスクリーンから得られる最小のY値(生の値)
      cfg.y_max      =  200;        // タッチスクリーンから得られる最大のY値(生の値)
      cfg.pin_int    = CYD_TP_IRQ;  // INTが接続されているピン番号 (36)
      cfg.bus_shared = false;       // 画面と共通のバスを使用している場合 trueを設定
      cfg.offset_rotation = 2;      // 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定

      cfg.spi_host = -1;            // 使用するSPIを選択 (HSPI_HOST or VSPI_HOST)
      cfg.freq = 1000000;           // SPIクロックを設定
      cfg.pin_sclk = CYD_TP_CLK;    // SCLKが接続されているピン番号 (25)
      cfg.pin_mosi = CYD_TP_MOSI;   // MOSIが接続されているピン番号 (32)
      cfg.pin_miso = CYD_TP_MISO;   // MISOが接続されているピン番号 (39)
      cfg.pin_cs   = CYD_TP_CS;     //   CSが接続されているピン番号 (33)

      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);  // タッチスクリーンをパネルにセットします。
    }

    setPanel(&_panel_instance); // 使用するパネルをセットします。
  }
};

diff コマンドで両者の差分を取ると、次のようになります(僕はこれらを1つのファイルにまとめ、ココ に掲載されたシンボル定義 DISPLAY_CYD_2USB切り替えてます)。

% diff LGFX_ESP32_2432S028R_CYD_1USB.hpp LGFX_ESP32_2432S028R_CYD_2USB.hpp 
17c17
<   lgfx::Panel_ILI9341   _panel_instance;  // 接続するパネルの型にあったインスタンスを用意します。
---
>   lgfx::Panel_ST7789    _panel_instance;  // 接続するパネルの型にあったインスタンスを用意します。
31c31
<       cfg.freq_write = 40000000;          // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
---
>       cfg.freq_write = 80000000;          // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
56,57c56,57
<       cfg.offset_rotation  =     2;  // 回転方向の値のオフセット 0~7 (4~7は上下反転)
<       cfg.dummy_read_pixel =     8;  // ピクセル読出し前のダミーリードのビット数
---
>       cfg.offset_rotation  =     0;  // 回転方向の値のオフセット 0~7 (4~7は上下反転)
>       cfg.dummy_read_pixel =    16;  // ピクセル読出し前のダミーリードのビット数
92c92
<       cfg.offset_rotation = 0;      // 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定
---
>       cfg.offset_rotation = 2;      // 表示とタッチの向きのが一致しない場合の調整 0~7の値で設定

これらの設定が他の ILI9341 や ST7789 に適用可能とは限りません。僕の経験では、単体の ST7789 ディプレイで dummy_read_pixel をデフォルトの 8 から 16 に変えたところ、スクリーンキャプチャ(=画素値の読み出し)時に色がシフトしてしまいました。

上記から分かる通り、LovyanGFX の自動設定では ILI9341 の SPI クロック周波数(40MHz)を ST7789(80MHz)の 1/2 に設定してます。各パネルのデータシートを見ると、書き込み時のクロック周期として ILI9341 が 100ns(10MHz)、ST7789 は 66ns(約 15MHz)が「最小値」として示されているだけで、最大値の規定がありません。

ILI9341 クロック仕様
ILI9341 クロック仕様
ST7789 クロック仕様
ST7789 クロック仕様

おそらく 40MHz とか 80MHz とかの値は、らびやんさん をはじめ多くの先人達の経験に基づく値と思われます。例えば witnessmenow/ESP32-Cheap-Yellow-Display では、TFT_eSPI 用の値として ILI9341 タイプST7789 タイプ ともに 55MHz が設定されています。

手元の CYD で TFT_eSPI のベンチマーク用スケッチ を 55MHz に設定しても、40MHz と有意なさは見られませんでした。どこかで(調べろよ、オィ😅)40MHz の倍数で丸められているようです。

描画性能の実験

先人達の経験値を信じないわけではありませんが、鵜呑みにして「ILI9341 より ST7789 の方が速い!」と結論づけるのもナニなので、TFT_eSPI のベンチマーク用スケッチ を LovyanGFX でも動くようにして、SPI クロック周波数を変えてテストしてみました。

結果はご覧の通り、ILI9341 も 80MHz で動作し、ST7789 と同等のスコアを示しました。

ILI9341 SPI クロック 40MHz
ILI9341 SPI クロック 40MHz
ILI9341 SPI クロック 80MHz
ILI9341 SPI クロック 80MHz
ST7789 SPI クロック 80MHz
ST7789 SPI クロック 80MHz

念の為、各テスト結果を 24 ビットのビットマップ画像に保存し、40MHz と 80MHz で比較したところ、完全に一致しました(ただし ILI9341 と ST7789 とでは、幾つかの画像で目視ではほぼ区別がつかない程度の差(RGB 値で 1 ビット)が見られた)。

一方 LovyanGFX の例題スケッチ RotatedZoomSample では、ILI9341 の 80MHz で同期が乱れたような画像になっちゃいました。

RotatedZoom ILI9341 40MHz
RotatedZoom ILI9341 40MHz
RotatedZoom ILI9341 80MHz
RotatedZoom ILI9341 80MHz
RotatedZoom ST7789 80MHz
RotatedZoom ST7789 80MHz

この例題スケッチではスプライトが使われているので、メモリからパネルへのデータ転送時にタイミング的な不整合が生じたのでしょう。DMA は明示的に有効化してませんが、デフォルトで働いていたのかも知れません(調べろよ、オィ 😠

これらの結果から、使用する関数次第で 40MHz が上限の場合もあれば、80MHz が可能な場合もある事が分かります。逆に言えば、ILI9341 であっても 40MHz に固執する必要はなく、条件次第で 80MHz で高速化を狙えるという事です。

ILI9341、オーバークロックして大丈夫?

どこからがオーバークロックなのかハッキリしませんが、TFT_graphicstest_PDQ の全ての待ち時間をゼロにして3時間連続稼働した、なんちゃって耐久試験(Alexa に聞いたら室温は22度でした)を行いました。

80MHz 連続描画の耐久試験
80MHz 連続描画の耐久試験
ボードのオモテ面
ボードのオモテ面
ボードの基板面
ボードの基板面

パネルの中のドライバ IC を直接測温することは出来ませんが、表も裏も特段に熱くなることもなく正常に機能しています。まぁ、ILI9341 も ST7789 もチップの作りとしては似たり寄ったりなのでしょう(よく知りもせず、いい加減なヤツ 🙄

次回は…

もしかしたら LVGL+ILI9341 でも 80MHz で動かせるかも?という期待を持ちつつ、今回はココまでとします。

次回は、冒頭に挙げた2番目の気になる項目「2本しか無い SPI バスで3つの SPI デバイスを扱う方法」を掘り下げたいと思います。3つのうち2つしか使えないなんて、有り得ないですからネ 🤡