ESP32-DevKit 系の開発ボードでは、LCD、タッチパネル、SD カードの3つの SPI デバイスを(ユーザーに解放された)2つの SPI バスで動かす場合、LCD とタッチパネルを1つのバスに、SD カードをもう1つのバスに接続する構成が一般的かと思います(図1)。

例えば TFT_eSPI では、LCD とタッチパネルの SCKMOSIMISO は共通が前提です。また XIAO や SuperMini など小型のボードでは SD カードも共通にせざるを得ません(図2)。

本記事では旧来の「マスター/スレーブ」ではなく「メイン/サブ」と表記しています。

図1 TFT_eSPI の使用例
図1 TFT_eSPI の使用例
図2 TFT_eSPI + XIAO
図2 TFT_eSPI + XIAO
図3 TFT_eSPI + CYD
図3 TFT_eSPI + CYD

一方 LCD、タッチパネル、SD カードが1つのボードに載った ESP32 2432S028R(以下 CYD)のピン配は次の通りで、SCKMOSIMISO がそれぞれ 別々の GPIO に接続されているため共通化することができず、SPI バスが1つ不足します(図3)。

CYD_* は、ボードタイプに「ESP32-2432S028R CYD」を選択した時に読み込まれる pins_arduino.h で定義されるシンボルです。

信号名\デバイス ディスプレイ タッチパネル SDカード
SCK CYD_TFT_SCK (14) CYD_TP_CLK (25) CYD_SD_SCK (18)
MOSI CYD_TFT_MOSI (13) CYD_TP_MOSI (32) CYD_SD_MOSI (23)
MISO CYD_TFT_MISO (12) CYD_TP_MISO (39) CYD_SD_MISO (19)
CS CYD_TFT_CS (15) CYD_TP_CS (33) CYD_SD_SS (5)
DC CYD_TFT_DC (2)    
IRQ   CYD_TP_IRQ (36)  

では、どうやって3つの SPI デバイスを共存させれば良いのか? ─ その答えは LovyanGFX にありました。

LovyanGFX の解決法

図1の様に1つの SPI メインに複数の SPI サブノードが接続されている場合、各ノード仕様に合わせた設定(クロック周波数、ビット列の並び、及びクロックパルス動作モード)、いわゆるトランザクションを spiSettings() で切り替えて通信を行います。

一方タッチパネルの SCKMOSIMISO は他と被らず独立しているので、一度トランザクションを設定すれば切り替える必要がありません。

LovyanGFX では、設定ファイル 中の「タッチスクリーン制御の設定 → 使用するSPIを選択 (HSPI_HOST or VSPI_HOST)」にある cfg.spi_host-1 にすることで、このモードで XPT2046 と SPI 通信することが可能になります。

    { // タッチスクリーン制御の設定を行います。
      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);  // タッチスクリーンをパネルにセットします。
    }
図4 LovyanGFXの動作
図4 LovyanGFXの動作

このモードでは、CSMOSIMISO に割り当てられた GPIO を直接叩く、いわゆる ビットバン によってタッチパネルの情報をソフトウェアで読み取ります(図4)。

また 前回 調べた通り、LGFX_AUTODETECT を設定すれば自動でこのモードにしてくれます。とっても便利ですネ 🤗

TFT_eSPI の場合は?

LVGL には、予め TFT_eSPI 用のサンプルコードが組み込まれていて、例えば Random Nerd のチュートリアル を参考すれば簡単に CYD で LVGL を走らせることができます。

TFT_eSPI もタッチ機能を持っていますが、図1が前提で図3の接続ができず、CYD では機能しません。そこで同チュートリアルでは XPT2046_Touchscreen図3の形で繋いでいます。ただし SD カードを組み込むことが出来ません。

XPT2046_Bitbang_Slim
XPT2046_Bitbang_Slim

このため CYD のまとめリポジトリ ESP32-Cheap-Yellow-Display では、LovyanGFX 同様にビットバン方式をとる XPT2046_Bitbang_Slim推奨しています

このライブラリは、グラフィカルなキャリブレーション機能が無い(数値を直接いじる)上に、座標系の回転が未実装だし、何より精度の低い抵抗膜方式では外れ値を除外するフィルターが必要だったりと、何かと面倒です。

以前紹介した サーモグラフィカメラ では、これらの問題に対処するため新たに ラッパークラス を作成しましたが、LVGL で SD カードを使う場合、そーまでして TFT_eSPI を使う理由が見当たりません 😑

条件付きで動作させる方法

どーしても TFT_eSPI を使いたい場合は、保証は出来ませんが条件付きで SD カードの読み書きが可能になる方法があります。

図5 2つのSPIClassインスタンス
図5 2つのSPIClassインスタンス

SPI 例題スケッチ SPI_Multiple_Buses の変則版で(図5)、SPI トランザクション を想定外な方法で使うのですが、スクリーンキャプチャ(=LCD から画素値を読み込みながら SD カードに書き出す処理)がうまく動かない(=画素値の読み込みが出来ない)だけで、なぜか SD カードへの読み書きだけなら出来ちゃいます。

実際には、User_Setup.hTFT_MISO の定義をコメントアウト(トライステートのハイインピーダンス状態)するだけです。こーすると、LCD には MOSI を介して出力するだけ、タッチパネルからは MISOを介して読み込むだけになり、それぞれが競合しない?のか、機能します 🥳

この場合、Espressif 標準の SD ライブラリ はデフォルトの SPI バスである VSPI に接続されるので、ディスプレイもタッチパネルも HSPI に接続するのが良いと思います。

  • TFT_eSPI の User_Setup.h
// https://github.com/espressif/arduino-esp32/blob/master/variants/jczn_2432s028r/pins_arduino.h
// #define TFT_MISO CYD_TFT_MISO   // 12
#define TFT_MOSI CYD_TFT_MOSI   // 13
#define TFT_SCLK CYD_TFT_SCK    // 14
#define TFT_CS   CYD_TFT_CS     // 15
#define TFT_DC   CYD_TFT_DC     //  2
#define TFT_RST  -1
#define TOUCH_CS -1
...
// The ESP32 has 2 free SPI ports i.e. VSPI and HSPI, the VSPI is the default.
// If the VSPI port is in use and pins are not accessible (e.g. TTGO T-Beam)
// then uncomment the following line:
#define USE_HSPI_PORT
  • XTP2046_Touchscreen の設定
// https://github.com/espressif/arduino-esp32/blob/master/variants/jczn_2432s028r/pins_arduino.h
#define XPT2046_IRQ   CYD_TP_IRQ  // 36
#define XPT2046_MOSI  CYD_TP_MOSI // 32
#define XPT2046_MISO  CYD_TP_MISO // 39
#define XPT2046_CLK   CYD_TP_CLK  // 25
#define XPT2046_CS    CYD_TP_CS   // 33

SPIClass touchscreenSPI = SPIClass(HSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);
...
void setup() {
  ...
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  ...
}

SPI デバイスの適切な配置について

Espressif のドキュメント「Sharing the SPI Bus Among SD Cards and Other SPI Devices」には、比較的低い周波数で動作する SD カードを他のデバイスと SPI バスを共有する場合の注意事項と対策が解説されています。

事実僕も、TFT_eSPI を図2の接続で使うとスクリーンキャプチャが正しく保存できないという事象を経験し、未だに解決できていません。「混ぜるな、危険!」とまでは言いませんが、図1図4の様に、ディスプレイと SD カードは別々の SPI バスに接続するのが良いと思います。

それと余談ですが、ESP32 のチップって、無印(?)以外にも C2, C3, C6, H2, P4, S2, S3 といった派生があり、SPI バスの定義が微妙に異なるので要注意です。

#ifdef CONFIG_IDF_TARGET_ESP32S2
#define FSPI 1  //SPI 1 bus. ESP32S2: for external memory only (can use the same data lines but different SS)
#define HSPI 2  //SPI 2 bus. ESP32S2: external memory or device  - it can be matrixed to any pins
#define SPI2 2  // Another name for ESP32S2 SPI 2
#define SPI3 3  //SPI 3 bus. ESP32S2: device only - it can be matrixed to any pins
#elif CONFIG_IDF_TARGET_ESP32
#define FSPI 1  //SPI 1 bus attached to the flash (can use the same data lines but different SS)
#define HSPI 2  //SPI 2 bus normally mapped to pins 12 - 15, but can be matrixed to any pins
#define VSPI 3  //SPI 3 bus normally attached to pins 5, 18, 19 and 23, but can be matrixed to any pins
#else
#define FSPI 0
#define HSPI 1
#endif
#if CONFIG_IDF_TARGET_ESP32
SPIClass SPI(VSPI);
#else
SPIClass SPI(FSPI);
#endif

今回の結論

色々と書きましたが、要は LovyanGFX 一択で OK、かつ初期化時にボードを自動認識する LGFX_AUTODETECT でタッチ検出にビットバンを機能させましょう 👍 が今回の結論です。

と言うことで、前回 と今回で LVGL を動かす準備が整いました。次回は LVGL について分かったことや LovyanGFX でデモを動かす手順などを報告予定です。