ESP32 2432S028R (CYD)でLVGL - 2つのSPIバスでLCD、タッチ、SDを共存させる方法
ESP32-DevKit 系の開発ボードでは、LCD、タッチパネル、SD カードの3つの SPI デバイスを(ユーザーに解放された)2つの SPI バスで動かす場合、LCD とタッチパネルを1つのバスに、SD カードをもう1つのバスに接続する構成が一般的かと思います(図1)。
例えば TFT_eSPI では、LCD とタッチパネルの SCK
、MOSI
、MISO
は共通が前提です。また XIAO や SuperMini など小型のボードでは SD カードも共通にせざるを得ません(図2)。
本記事では旧来の「マスター/スレーブ」ではなく「メイン/サブ」と表記しています。



一方 LCD、タッチパネル、SD カードが1つのボードに載った ESP32 2432S028R(以下 CYD)のピン配は次の通りで、SCK
、MOSI
、MISO
がそれぞれ 別々の 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() で切り替えて通信を行います。
一方タッチパネルの SCK
、MOSI
、MISO
は他と被らず独立しているので、一度トランザクションを設定すれば切り替える必要がありません。
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); // タッチスクリーンをパネルにセットします。
}

このモードでは、CS
、MOSI
、MISO
に割り当てられた GPIO を直接叩く、いわゆる ビットバン によってタッチパネルの情報をソフトウェアで読み取ります(図4)。
また 前回 調べた通り、LGFX_AUTODETECT
を設定すれば自動でこのモードにしてくれます。とっても便利ですネ 🤗
TFT_eSPI の場合は?
LVGL には、予め TFT_eSPI 用のサンプルコードが組み込まれていて、例えば Random Nerd のチュートリアル を参考すれば簡単に CYD で LVGL を走らせることができます。
TFT_eSPI もタッチ機能を持っていますが、図1が前提で図3の接続ができず、CYD では機能しません。そこで同チュートリアルでは XPT2046_Touchscreen を 図3の形で繋いでいます。ただし SD カードを組み込むことが出来ません。

このため CYD のまとめリポジトリ ESP32-Cheap-Yellow-Display では、LovyanGFX 同様にビットバン方式をとる XPT2046_Bitbang_Slim を 推奨しています。
このライブラリは、グラフィカルなキャリブレーション機能が無い(数値を直接いじる)上に、座標系の回転が未実装だし、何より精度の低い抵抗膜方式では外れ値を除外するフィルターが必要だったりと、何かと面倒です。
以前紹介した サーモグラフィカメラ では、これらの問題に対処するため新たに ラッパークラス を作成しましたが、LVGL で SD カードを使う場合、そーまでして TFT_eSPI を使う理由が見当たりません 😑
条件付きで動作させる方法
どーしても TFT_eSPI を使いたい場合は、保証は出来ませんが条件付きで SD カードの読み書きが可能になる方法があります。

SPI 例題スケッチ SPI_Multiple_Buses の変則版で(図5)、SPI トランザクション を想定外な方法で使うのですが、スクリーンキャプチャ(=LCD から画素値を読み込みながら SD カードに書き出す処理)がうまく動かない(=画素値の読み込みが出来ない)だけで、なぜか SD カードへの読み書きだけなら出来ちゃいます。
実際には、User_Setup.h
で TFT_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 でデモを動かす手順などを報告予定です。