ESP32 Arduino2 LVGL9 HMIディスプレイボード駆動

AliExpressの「Sunton Store」「GUITION Official Store」で販売されている、ESP32とLCDが付いたHMI(Human Machine Interface)向けの開発ボードが存在します。

シリーズ名は無く、黄色の基板で安価なため、海外では「ESP32 Cheap Yellow Display Board / CYD Board」と言われているようです。

最近発売されたと思われる「JC~」型番のボードは「Guition」と表記がありますね。シリーズ名でしょうか。

私は最初、技適マークが無いと思っていたのであまり興味がなかったのですが、Twitter上で技適マーク付きとの報告があり購入した次第です。

所有しているものは今のところ全て技適マークが付いていますが、上記URL等を参考にすると技適マークが付いていないロットがあるようで要注意です。

今回はこのボードを使用してLVGLを動作させてみます。

 

ディスプレイボード型番一覧

型番 ESP型番 画面サイズ 接続 タッチ 概要
ESP32-1732S019N ESP32-S3-WROOM-1 1.9 (170x320) SPI (ST7789) 無し GPIO 20ピン有り
ESP32-2432S022N ESP-WROOM-32 2.2 (240x320) 8bit (ST7789) 無し GPIO無し
ESP32-2432S022C ESP-WROOM-32 2.2 (240x320) 8bit (ST7789) 静電容量 (CST820 I2C) GPIO無し
ESP32-2432S024N ESP-WROOM-32 2.4 (240x320) SPI (ILI9341) 無し PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-2432S024R ESP-WROOM-32 2.4 (240x320) SPI (ILI9341) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-2432S024C ESP-WROOM-32 2.4 (240x320) SPI (ILI9341) 静電容量 (CST820 I2C) PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-2432S028R ESP-WROOM-32 2.8 (240x320) SPI (ILI9341) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 3(電源他)+ SP USBがTypeCではない終売?
JC2432W328N ESP-WROOM-32 2.8 (240x320) SPI (ST7789) 無し PicoBlade 4P * 4(電源他)+ SP + BAT
JC2432W328R ESP-WROOM-32 2.8 (240x320) SPI (ST7789) 抵抗膜 (?) PicoBlade 4P * 4(電源他)+ SP + BAT
JC2432W328C ESP-WROOM-32 2.8 (240x320) SPI (ST7789) 静電容量 (?) PicoBlade 4P * 4(電源他)+ SP + BAT
ESP32-2432S032N ESP-WROOM-32 3.2 (240x320) IPS SPI (ST7789) 無し PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-2432S032R ESP-WROOM-32 3.2 (240x320) IPS SPI (ST7789) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-2432S032C ESP-WROOM-32 3.2 (240x320) IPS SPI (ST7789) 静電容量 (? I2C) PicoBlade 4P * 3(電源他)+ SP + BAT
ESP32-3248S035R ESP-WROOM-32 3.5 (320x480) SPI (ST7796) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 3(電源他)+ SP USBがTypeCではない終売?
ESP32-3248S035C ESP-WROOM-32 3.5 (320x480) SPI (ST7796) 静電容量 (? I2C) PicoBlade 4P * 3(電源他)+ SP USBがTypeCではない終売?
ESP32-4827S043N ESP32-S3-WROOM-1 4.3 (480x272) RGB565 (ILI9485) 無し PicoBlade 4P * 3(電源他)+ SH 4P * 1
ESP32-4827S043R ESP32-S3-WROOM-1 4.3 (480x272) RGB565 (ILI9485) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 3(電源他)+ SH 4P * 1
ESP32-4827S043C ESP32-S3-WROOM-1 4.3 (480x272) RGB565 (ILI9485) 静電容量 (? I2C) PicoBlade 4P * 3(電源他)+ SH 4P * 1
ESP32-8048S043C ESP32-S3-WROOM-1 4.3 (800x480) IPS RGB565 (ILI9485) 静電容量 (? I2C) PicoBlade 4P * 3(電源他)+ SH 4P * 1
JC4827W543N ESP32-S3-WROOM-1 4.3 (480x272) IPS SPI (ST3401A) 無し PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP + BAT
JC4827W543R ESP32-S3-WROOM-1 4.3 (480x272) IPS SPI (ST3401A) 抵抗膜 (XPT2046 SPI) PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP + BAT
JC4827W543C ESP32-S3-WROOM-1 4.3 (480x272) IPS SPI (ST3401A) 静電容量 (? I2C) PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP + BAT
ESP32-8048S050N ESP32-S3-WROOM-1 5.0 (800x480) IPS RGB565 (ST7262) 無し PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP
ESP32-8048S050C ESP32-S3-WROOM-1 5.0 (800x480) IPS RGB565 (ST7262) 静電容量 (? I2C) PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP
JC8048W550N ESP32-S3-WROOM-1 5.0 (800x480) IPS RGB565 (ST7262) 無し PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP + BAT
JC8048W550C ESP32-S3-WROOM-1 5.0 (800x480) IPS RGB565 (ST7262) 静電容量 (? I2C) PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP + BAT
ESP32-8048S070N ESP32-S3-WROOM-1 7.0 (800x480) RGB565 (EK9716) 無し PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP
ESP32-8048S070C ESP32-S3-WROOM-1 7.0 (800x480) RGB565 (EK9716) 静電容量 (? I2C) PicoBlade 4P * 4(電源他)+ SH 4P * 1 + SP

RGB565接続でも大丈夫なのか?と思いましたが、ESP32-S3のドキュメントを見るとパラレルRGB接続のLCDでも扱えるみたいですね。

大型LCDを備えたものではESP32-S3が必須のようです。

 

技適メモ一覧

日本国内で電波を出す機器には技適マークが必須です。

搭載されているESP32マイコンモジュール自体にWi-Fi/Bluetoothの電波を出す機能があるため、技適マークが無いものを使用するのは違法です。

型番 技適マーク 概要
ESP32-1732S019N
ESP32-2432S022N
ESP32-2432S022C
ESP32-2432S024N
ESP32-2432S024R
ESP32-2432S024C
ESP32-2432S028R
JC2432W328N
JC2432W328R
JC2432W328C あり(211-161007 2024/08購入分:Espressif?(メーカー刻印無し) ESP-WROOM-32 搭載確認
ESP32-2432S032N
ESP32-2432S032R
ESP32-2432S032C
ESP32-3248S035R
ESP32-3248S035C
ESP32-4827S043N あり(201-220052 2024/01購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認
ESP32-4827S043R あり(201-220052 2024/09購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認         
ESP32-4827S043C
ESP32-8048S043C
JC4827W543N
JC4827W543R
JC4827W543C 無し 2024/08購入分:Guitionロゴ表記カスタム品?ESP32-S3-WROOM N4R8
ESP32-8048S050N あり(201-220052 2024/01購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認
ESP32-8048S050C あり(201-220052 2024/09購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認
JC8048W550N
JC8048W550C 無し 2024/08購入分:Siinst ESP32-S3-WROOM N16R8
ESP32-8048S070N あり(201-220052 2024/01購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認
ESP32-8048S070C あり(201-220052 2024/09購入分:Espressif ESP32-S3-WROOM-1-N16R8 搭載確認

情報をお待ちしております。

技適マークが無いものに関しては、技適マークがあるものにリワークして貼り替え必須かと思います。

 

技適に対応するためのESP32リワーク

ホットエアーリワークステーション(Quick 861DW)を使用してリワークを行いました。

周りの部品に影響が出ないように手元にあった銅テープで熱を遮断しています。

液晶画面がすぐ裏にあるのであまり長く行うとダメージが多くなると思われます。

 

JC4827W543C

ESP32 Arduino2 LVGL9 HMI Display Board Rework JC4827W543C 001 ESP32 Arduino2 LVGL9 HMI Display Board Rework JC4827W543C 002

真ん中のGNDパッドは元々ハンダが乗っていませんでした。楽にリワークできました。

 

JC8048W550C

ESP32 Arduino2 LVGL9 HMI Display Board Rework JC8048W550C 001 ESP32 Arduino2 LVGL9 HMI Display Board Rework JC8048W550C 002

真ん中のGNDパッドは元々ハンダが乗っていませんでした。楽にリワークできました。

 

ボード上の接続コネクタ

Molex PicoBlade 4P 1.25mm

説明中には「JS-1.25」などと表記があるが、これはPicoBladeの互換品と思われる。

(中国では小さいコネクタを総称してJSTと言っているのではないか?)

種類 型番
コネクタハウジング 51021-0400
コンタクトピン(連鎖・AWG 26~28) 50079-8000
コンタクトピン(バラ・AWG 26~28) 50079-8100
コンタクトピン(連鎖・AWG 28~32) 50058-8000
コンタクトピン(バラ・AWG 28~32) 50058-8100
基板用スルーホールコネクタヘッダ 53047-0410
基板用スルーホールコネクタヘッダL字 53048-0410
基板用面実装コネクタヘッダ 53398-0471
基板用面実装コネクタヘッダL字 53261-0471
両側コネクタ付きハーネス (50mm) 15134-0400
両側コネクタ付きハーネス (100mm) 15134-0401
両側コネクタ付きハーネス (150mm) 15134-0402
両側コネクタ付きハーネス (300mm) 15134-0403
両側コネクタ付きハーネス (450mm) 15134-0405
両側コネクタ付きハーネス (600mm) 15134-0406
片側コネクタ付きハーネス (75mm) 218112-0400
片側コネクタ付きハーネス (150mm) 218112-0401
片側コネクタ付きハーネス (225mm) 218112-0402
片側コネクタ付きハーネス (300mm) 218112-0403
片側コネクタ付きハーネス (425mm) 218112-0404
延長コネクタ付きハーネス (75mm) 218113-0400
延長コネクタ付きハーネス (150mm) 218113-0401
延長コネクタ付きハーネス (225mm) 218113-0402
延長コネクタ付きハーネス (300mm) 218113-0403
延長コネクタ付きハーネス (425mm) 218113-0404

Molex型番は、ハイフンを入れない番号のみで検索すると出やすい場合があります。

なお、一部ボードに搭載されているスピーカー・バッテリーコネクタはPicoBladeの2Pです。

 

互換品ハウジング

互換品コンタクトピン

互換品が多くて探しきれません。

互換品コネクタヘッダ

互換品ケーブルハーネス

 

JST SH 4P 1.0mm

種類 型番
コネクタハウジング SHR-04V-S-B
コンタクトピン(バラ・AWG 28~32) SSH-003T-P0.2-H
基板用面実装コネクタヘッダ BM04B-SRSS-TB
基板用面実装コネクタヘッダL字 SM04B-SRSS-TB
圧着済みケーブル(150mm) ※1本 SH3-SH3-28150
圧着済みケーブル(300mm) ※1本 SH3-SH3-28300

秋月・千石に取り扱いあり!

SparkFun社のQwiicシステムと同等ですね。

 

互換品ハウジング

互換品コンタクトピン

互換品が多くて探しきれません。

互換品コネクタヘッダ

互換品ケーブルハーネス

 

USBシリアル

使用しているUSBシリアルチップは CH340C

最近のWindowsであれば接続するだけでドライバが当たるはずです。

なお、JC4827W543Cだけは例外でUSBシリアルチップは使用せずに、ESP32 S3のUSBポートに直接つながっています。

ArduinoのUSB Mode設定はUSB-OTGに変更し、初回書き込み時にはBOOTボタンを押しながらUSBを接続してDownload Bootモードにする必要があります。

初期状態では何も書き込まれていないためリセットを繰り返します。

 

搭載されているGPIO詳細

手元にあって確認したものを書いておきます。

 

2.8インチ 240 * 320(JC2432W328N/R/C)

 

4.3インチ 480 * 272(ESP32-4827S043N/R/C)

LCDとはパラレル接続のためIOがかなりカツカツ。

末尾Cのタッチパネル対応機種であるとIO 19/20使用のI2C接続のため、外部コネクタも使用方法が限られる。

抵抗膜方式の場合、コントローラーがSPI接続のため、TP_表記のピンも使用する。

TP_IRQとIO 18が接続され、なおかつプルアップされているために、末尾Rモデルの場合はIO 18は基本使用できないものと考えたほうが良さそう。

TF_表記のピンはTransFlash(microSD)カードスロットに接続されている。

自由に使えそうなIOは IO 17 か、IO 11 / 12 / 13(TFカード・抵抗膜タッチパネル使用時を除く)と思われる。

IO 19/ 20 はタッチパネルとの通信に使用するため、末尾Cモデルは使用不可。

ただし、I2C接続のため、アドレスが被らないデバイスを並列に接続することは出来ると思われる。

TXD / RXDポートはUSBシリアルICと並列に接続されてしまっていて切り離せないため、使用すると信号がぶつかってしまうのではないか?

CH340Cのデータシートを見るとUARTがアイドルの時、TXDピンはHIGHになるとの表記があります。

並列にデバイスをつなぐと出力がぶつかってしまうため、基本的にはP1は電源入力専用でTXD/RXDは使用しないほうが良さそうです。

CH340Kであれば弱プルアップになるというデータシート表記があるので、こちらを採用してほしかったなとは思います。

コネクタ 種類 ピン 接続線 概要
P1 PicoBlade 4P 1 5V 3.3V生成のDCDCコンバータに接続。USB側の5Vにはダイオードが入ってるので安心。
P1 PicoBlade 4P 2 TXD 100Ωを経由してCH340Cの3ピン・ESP32の37ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 3 RXD 100Ωを経由してCH340Cの2ピン・ESP32の36ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 4 GND GND
P2 PicoBlade 4P 1 IO 19 SPI表記あり。タッチパネルSDA接続。プルアップ抵抗実装済み。
P2 PicoBlade 4P 2 IO 11 SPI表記あり。TP_DIN / TF_CMD
P2 PicoBlade 4P 3 IO 12 SPI表記あり。TP_CLK / TF_CLK
P2 PicoBlade 4P 4 IO 13 SPI表記あり。TP_OUT / TF_DAT0
P3 PicoBlade 4P 1 IO 17 UART1表記あり。
P3 PicoBlade 4P 2 IO 18 UART1表記あり。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P3 PicoBlade 4P 3 IO 19 USB表記あり。タッチパネルSDA接続。4.7kプルアップ抵抗実装済み。
P3 PicoBlade 4P 4 IO 20 USB表記あり。タッチパネルSCL接続。4.7kプルアップ抵抗実装済み。
P4 SH 4P 1 GND GND
P4 SH 4P 2 3.3V AMS1117の出力。バイパスダイオードが無いので3.3V出力として扱ったほうが良い。
P4 SH 4P 3 IO 17
P4 SH 4P 4 IO 18 TP_IRQ / ジャンパ実装するとタッチパネルINT接続。

 

4.3インチ 480 * 272(JC4827W543N/R/C)

 

4.3インチ 800 * 480(ESP32-8048S043C)

 

5.0インチ 800 * 480(ESP32-8048S050N/C)

LCDとはパラレル接続のためIOがかなりカツカツ。

末尾Cのタッチパネル対応機種であるとIO 19/20使用のI2C接続のため、外部コネクタも使用方法が限られる。

抵抗膜方式タッチパネルは回路図にはあるが、販売はされていない?

抵抗膜方式の場合、コントローラーがSPI接続のため、TP_表記のピンも使用する。

TF_表記のピンはTransFlash(microSD)カードスロットに接続されている。

自由に使えそうなIOは IO 17 / 18(I2Sアンプ使用時を除く)か、IO 11 / 12 / 13(TFカード使用時を除く)と思われる。

IO 19/ 20 はタッチパネルとの通信に使用するため、末尾Cモデルは使用不可。

ただし、I2C接続のため、アドレスが被らないデバイスを並列に接続することは出来ると思われる。

TXD / RXDポートはUSBシリアルICと並列に接続されてしまっていて切り離せないため、使用すると信号がぶつかってしまうのではないか?

CH340Cのデータシートを見るとUARTがアイドルの時、TXDピンはHIGHになるとの表記があります。

並列にデバイスをつなぐと出力がぶつかってしまうため、基本的にはP1は電源入力専用でTXD/RXDは使用しないほうが良さそうです。

CH340Kであれば弱プルアップになるというデータシート表記があるので、こちらを採用してほしかったなとは思います。

現在出回ってる基板のバージョンが1.1で、初期の1.0の基板だとP2の1番ピンがIO 19ではなくIO 18の場合があるようです。

手元には両方のバージョンがあるため、P2コネクタ違いになっています。

コネクタ 種類 ピン 接続線 概要
P1 PicoBlade 4P 1 5V 3.3V生成のDCDCコンバータに接続。USB側の5Vにはダイオードが入ってるので安心。
P1 PicoBlade 4P 2 TXD 100Ωを経由してCH340Cの3ピン・ESP32の37ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 3 RXD 100Ωを経由してCH340Cの2ピン・ESP32の36ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 4 GND GND
P2 PicoBlade 4P 1 IO 19 SPI表記あり。タッチパネルSDA接続。プルアップ抵抗実装済み。
P2 PicoBlade 4P 2 IO 11 SPI表記あり。TP_DIN / TF_CMD
P2 PicoBlade 4P 3 IO 12 SPI表記あり。TP_CLK / TF_CLK
P2 PicoBlade 4P 4 IO 13 SPI表記あり。TP_OUT / TF_DAT0
P3 PicoBlade 4P 1 IO 17 UART1表記あり。I2SアンプDIN接続。
P3 PicoBlade 4P 2 IO 18 UART1表記あり。I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P3 PicoBlade 4P 3 IO 19 USB表記あり。タッチパネルSDA接続。4.7kプルアップ抵抗実装済み。
P3 PicoBlade 4P 4 IO 20 USB表記あり。タッチパネルSCL接続。4.7kプルアップ抵抗実装済み。
P4 PicoBlade 4P 1 GND GND
P4 PicoBlade 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P4 PicoBlade 4P 3 IO 17 I2SアンプDIN接続。
P4 PicoBlade 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P5 SH 4P 1 GND GND
P5 SH 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P5 SH 4P 3 IO 17 I2SアンプDIN接続。
P5 SH 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P6 PicoBlade 2P 1 SP- I2SアンプMAX98357出力
P6 PicoBlade 2P 2 SP+ I2SアンプMAX98357出力

 

5.0インチ 800 * 480(JC8048W550N/C)

LCDとはパラレル接続のためIOがかなりカツカツ。

末尾Cのタッチパネル対応機種であるとIO 19/20使用のI2C接続のため、外部コネクタも使用方法が限られる。

抵抗膜方式タッチパネルは回路図にはあるが、販売はされていない?

抵抗膜方式の場合、コントローラーがSPI接続のため、TP_表記のピンも使用する。

TF_表記のピンはTransFlash(microSD)カードスロットに接続されている。

自由に使えそうなIOは IO 17 / 18(I2Sアンプ使用時を除く)か、IO 11 / 12 / 13(TFカード使用時を除く)と思われる。

IO 19/ 20 はタッチパネルとの通信に使用するため、末尾Cモデルは使用不可。

ただし、I2C接続のため、アドレスが被らないデバイスを並列に接続することは出来ると思われる。

TXD / RXDポートはUSBシリアルICと並列に接続されてしまっていて切り離せないため、使用すると信号がぶつかってしまうのではないか?

CH340Cのデータシートを見るとUARTがアイドルの時、TXDピンはHIGHになるとの表記があります。

並列にデバイスをつなぐと出力がぶつかってしまうため、基本的にはP1は電源入力専用でTXD/RXDは使用しないほうが良さそうです。

CH340Kであれば弱プルアップになるというデータシート表記があるので、こちらを採用してほしかったなとは思います。

コネクタ 種類 ピン 接続線 概要
P1 PicoBlade 4P 1 5V 3.3V生成のDCDCコンバータに接続。USB側の5Vにはダイオードが入ってるので安心。
P1 PicoBlade 4P 2 TXD 100Ωを経由してCH340Cの3ピン・ESP32の37ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 3 RXD 100Ωを経由してCH340Cの2ピン・ESP32の36ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 4 GND GND
P2 PicoBlade 4P 1 IO 19 SPI表記あり。タッチパネルSDA接続。プルアップ抵抗実装済み。
P2 PicoBlade 4P 2 IO 11 SPI表記あり。TP_DIN / TF_CMD
P2 PicoBlade 4P 3 IO 12 SPI表記あり。TP_CLK / TF_CLK
P2 PicoBlade 4P 4 IO 13 SPI表記あり。TP_OUT / TF_DAT0
P3 PicoBlade 4P 1 IO 17 UART1表記あり。I2SアンプDIN接続。
P3 PicoBlade 4P 2 IO 18 UART1表記あり。I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P3 PicoBlade 4P 3 IO 19 USB表記あり。タッチパネルSDA接続。4.7kプルアップ抵抗実装済み。
P3 PicoBlade 4P 4 IO 20 USB表記あり。タッチパネルSCL接続。4.7kプルアップ抵抗実装済み。
P4 PicoBlade 4P 1 GND GND
P4 PicoBlade 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P4 PicoBlade 4P 3 IO 17 I2SアンプDIN接続。
P4 PicoBlade 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P5 SH 4P 1 GND GND
P5 SH 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P5 SH 4P 3 IO 17 I2SアンプDIN接続。
P5 SH 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P6 PicoBlade 2P 1 SP- I2SアンプNS4168出力
P6 PicoBlade 2P 2 SP+ I2SアンプNS4168出力
P7 PicoBlade 2P 1 BAT+ 充電管理IP5306バッテリー接続
P7 PicoBlade 2P 2 BAT- 充電管理IP5306バッテリー接続

 

7.0インチ 800 * 480(ESP32-8048S070N/C)

LCDとはパラレル接続のためIOがかなりカツカツ。

末尾Cのタッチパネル対応機種であるとIO 19/20使用のI2C接続のため、外部コネクタも使用方法が限られる。

抵抗膜方式タッチパネルは回路図にはあるが、販売はされていない?

抵抗膜方式の場合、コントローラーがSPI接続のため、TP_表記のピンも使用する。

TF_表記のピンはTransFlash(microSD)カードスロットに接続されている。

自由に使えそうなIOは IO 17 / 18(I2Sアンプ使用時を除く)か、IO 11 / 12 / 13(TFカード使用時を除く)と思われる。

IO 19/ 20 はタッチパネルとの通信に使用するため、末尾Cモデルは使用不可。

ただし、I2C接続のため、アドレスが被らないデバイスを並列に接続することは出来ると思われる。

TXD / RXDポートはUSBシリアルICと並列に接続されてしまっていて切り離せないため、使用すると信号がぶつかってしまうのではないか?

CH340Cのデータシートを見るとUARTがアイドルの時、TXDピンはHIGHになるとの表記があります。

並列にデバイスをつなぐと出力がぶつかってしまうため、基本的にはP1は電源入力専用でTXD/RXDは使用しないほうが良さそうです。

CH340Kであれば弱プルアップになるというデータシート表記があるので、こちらを採用してほしかったなとは思います。

コネクタ 種類 ピン 接続線 概要
P1 PicoBlade 4P 1 5V 3.3V生成のDCDCコンバータに接続。USB側の5Vにはダイオードが入ってるので安心。
P1 PicoBlade 4P 2 TXD 100Ωを経由してCH340Cの3ピン・ESP32の37ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 3 RXD 100Ωを経由してCH340Cの2ピン・ESP32の36ピンに。CH340Cが繋がってるが使えるのか?
P1 PicoBlade 4P 4 GND GND
P2 PicoBlade 4P 1 IO 19 SPI表記あり。タッチパネルSDA接続。プルアップ抵抗実装済み。
P2 PicoBlade 4P 2 IO 11 SPI表記あり。TP_DIN / TF_CMD
P2 PicoBlade 4P 3 IO 12 SPI表記あり。TP_CLK / TF_CLK
P2 PicoBlade 4P 4 IO 13 SPI表記あり。TP_OUT / TF_DAT0
P3 PicoBlade 4P 1 IO 17 UART1表記あり。I2SアンプDIN接続。
P3 PicoBlade 4P 2 IO 18 UART1表記あり。I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P3 PicoBlade 4P 3 IO 19 USB表記あり。タッチパネルSDA接続。4.7kプルアップ抵抗実装済み。
P3 PicoBlade 4P 4 IO 20 USB表記あり。タッチパネルSCL接続。4.7kプルアップ抵抗実装済み。
P4 PicoBlade 4P 1 GND GND
P4 PicoBlade 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P4 PicoBlade 4P 3 IO 17 I2SアンプDIN接続。
P4 PicoBlade 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P5 SH 4P 1 GND GND
P5 SH 4P 2 3.3V DCDCのインダクタすぐ後に接続。逆流防止が無いので3.3V出力として扱ったほうが良い。
P5 SH 4P 3 IO 17 I2SアンプDIN接続。
P5 SH 4P 4 IO 18 I2SアンプLRCLK接続。TP_IRQ / ジャンパ実装するとタッチパネルINT接続。
P6 PicoBlade 2P 1 SP- I2SアンプMAX98357出力
P6 PicoBlade 2P 2 SP+ I2SアンプMAX98357出力

 

搭載されているLCDについて

深圳市晶彩智能有限公司(Shenzhen Jingcai Intelligent Co., Ltd)製のよう。

 

4.3インチ 480 * 272(深圳市晶彩智能有限公司 JC4827B043N)

データシート上、ドットクロックは最小8MHz, 通常9MHz, 最大12MHz。

 

4.3インチ 800 * 480(深圳市晶彩智能有限公司?)

データシート見つからず。

 

5.0インチ 800 * 480(深圳市晶彩智能有限公司 JC8048B050N)

データシート上、ドットクロックは最小23MHz, 通常25MHz, 最大27MHz。

発色は比較的良いが、ドットクロックをデータシート通りに設定すると表示が盛大にズレる。

ESP32側の問題の可能性もあるが、ドットクロックを落とすと正常な位置に表示される。

同封サンプルでは12MHzやら16MHzの指定が見られる。私はデータシートの半分の値、12.5MHzを使用した。

クロックを落としている関係かわからないが、色表示がうねっている。特に端のほうが顕著。

 

7.0インチ 800 * 480(深圳市晶彩智能有限公司 JC8048B070N)

データシート上、ドットクロックは最小29MHz, 通常33MHz, 最大38MHz。

全体的に白く飛んでいる。

 

LVGL GUIデザイン

LVGLを使用する上で、GUIデザイン用のソフトを探しました。

LVGLは画面上に配置するウィジェットをコード中で座標指定して配置していく形なので、デザイン用のソフトが無いと効率が悪いです。

色々探したところ、最近になってLVGLに対応し始めた「EEZ Studio」が良さそうでした。

 

EEZ Studio 0.15.1 を使用したLVGL GUIデザイン

こちらからダウンロードすることができます。

今回使用したのは「EEZ-Studio-Setup-0.15.1.exe」です。

 

ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 001 ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 002

起動したら「LVGL」を選択してプロジェクトを作成します。

LVGLのバージョンは9.0を選択しておきます。

保存場所については、Arduinoのスケッチと同じ位置にすると良いです。

EEZ Studioで書き出しを行うと、プロジェクトファイルと同じ位置から「src/ui」というフォルダで書き出されます。

Arduinoはsrcという名前のフォルダのみ、別途ソースコードを含むことができますので、スケッチのあるフォルダにプロジェクトを保存すると都合が良いと思います。

デフォルトでは「Main」という名前のページが作成され、800*480の解像度設定になっています。

 

解像度設定

ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 003 ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 004

使用するボードの液晶解像度に合わせて設定してみましょう。480*272で設定する例です。

左ペインから「Main」ページを選択し、右ペインに表示される「Properties」から「Width」と「Height」を設定します。

左下の「Check」に警告が出ていますね。

これは、設定されているディスプレイの解像度とページの解像度が違うためです。

ディスプレイの解像度も合わせるために、メニューバー中の歯車マークをクリックし、「Settings」を表示させます。

「General」中の「Display width」と「Display height」がディスプレイ解像度になります。これも同様に変更します。

 

インクルードパス設定

ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 005

生成されるファイルのインクルードパスも設定しておきましょう。

「Settings」中の「Build」を選択すると「LVGL include」が表示されます。

デフォルトでは「lvgl/lvgl.h」ですが、Arduino環境から使う場合には「lvgl.h」のほうが都合が良いので変更しておきます。

これで生成されるファイル内のインクルードパスが変更されます。

 

GUIデザインのビルド(LVGLコード書き出し)

ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 006 ESP32 Arduino2 LVGL9 HMI Display Board EEZ Studio 007

GUIのデザインが終わったら「Save」で保存しておきましょう。

チェックマークのボタン「Check」で設定をチェックし、レンチマークのボタン「Build」で現在のGUIを書き出しします。

このようにファイル群が書き出しされます。

他の環境からはこれらをインクルードして使用します。

 

Arduino IDEでの動作テスト

各種ディスプレイで動作確認したコードです。

 

ESP32-4827S043N 動作テストコード

ESP32-S3向け、LCDがRGB565パラレルで接続されているものは流用が効くと思います。

Arduino core for the ESP32(ESP-IDF)のドライバをそのまま使用しているので、別途TFT_eSPIやLovyanGFX等のLCDライブラリは必要ありません。

しかし、パラレルLCDはESP32-S3特有のペリフェラルなので、無印ESP32では動作しません。

//#include "esp_log.h"
//static const char *TAG = "main";

#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "driver/ledc.h"
#include "lv_conf.h"
extern "C" {
#include "src/ui/ui.h"
}

#define LCD_H_RES              480
#define LCD_V_RES              272
#define LCD_PIXEL_CLOCK_HZ     (9 * 1000 * 1000)
#define LCD_HSYNC_PULSE_WIDTH  4
#define LCD_HSYNC_BACK_PORCH   43
#define LCD_HSYNC_FRONT_PORCH  8
#define LCD_VSYNC_PULSE_WIDTH  4
#define LCD_VSYNC_BACK_PORCH   12
#define LCD_VSYNC_FRONT_PORCH  8
#define LCD_PIN_NUM_HSYNC      GPIO_NUM_39
#define LCD_PIN_NUM_VSYNC      GPIO_NUM_41
#define LCD_PIN_NUM_DE         GPIO_NUM_40
#define LCD_PIN_NUM_PCLK       GPIO_NUM_42
#define LCD_PIN_NUM_DISP_EN    GPIO_NUM_NC
#define LCD_PIN_NUM_DATA0      GPIO_NUM_8   // B0
#define LCD_PIN_NUM_DATA1      GPIO_NUM_3   // B1
#define LCD_PIN_NUM_DATA2      GPIO_NUM_46  // B2
#define LCD_PIN_NUM_DATA3      GPIO_NUM_9   // B3
#define LCD_PIN_NUM_DATA4      GPIO_NUM_1   // B4
#define LCD_PIN_NUM_DATA5      GPIO_NUM_5   // G0
#define LCD_PIN_NUM_DATA6      GPIO_NUM_6   // G1
#define LCD_PIN_NUM_DATA7      GPIO_NUM_7   // G2
#define LCD_PIN_NUM_DATA8      GPIO_NUM_15  // G3
#define LCD_PIN_NUM_DATA9      GPIO_NUM_16  // G4
#define LCD_PIN_NUM_DATA10     GPIO_NUM_4   // G5
#define LCD_PIN_NUM_DATA11     GPIO_NUM_45  // R0
#define LCD_PIN_NUM_DATA12     GPIO_NUM_48  // R1
#define LCD_PIN_NUM_DATA13     GPIO_NUM_47  // R2
#define LCD_PIN_NUM_DATA14     GPIO_NUM_21  // R3
#define LCD_PIN_NUM_DATA15     GPIO_NUM_14  // R4

#define LCD_BL_PWM_FREQ        1000 
#define LCD_BL_PWM_RESOLUTION  LEDC_TIMER_8_BIT 
#define LCD_BL_LEDC_CHANNEL    LEDC_CHANNEL_0  
#define LCD_BL_LEDC_TIMER      LEDC_TIMER_0 
#define LCD_BL_LEDC_PIN_NUM    GPIO_NUM_2
#define LCD_BL_LEDC_INVERT     0

#define LVGL_TASK_MAX_DELAY_MS 500
#define LVGL_TASK_MIN_DELAY_MS 1
#define LVGL_TASK_STACK_SIZE   (8 * 1024)
#define LVGL_TASK_PRIORITY     2


SemaphoreHandle_t sem_vsync_end;
SemaphoreHandle_t sem_gui_ready;
SemaphoreHandle_t lvgl_mux;


void init_backlight(void) {
  ledc_channel_config_t ledc_channel = {
    .gpio_num = LCD_BL_LEDC_PIN_NUM,
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .channel = LCD_BL_LEDC_CHANNEL,
    .intr_type = LEDC_INTR_DISABLE,
    .timer_sel = LCD_BL_LEDC_TIMER,
    .duty = 0,
    .hpoint = 0,
    .flags = {
      .output_invert = LCD_BL_LEDC_INVERT,
    },
  };
  ledc_timer_config_t ledc_timer = {
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .duty_resolution = LCD_BL_PWM_RESOLUTION,
    .timer_num = LCD_BL_LEDC_TIMER,
    .freq_hz = LCD_BL_PWM_FREQ,
    .clk_cfg = LEDC_AUTO_CLK,
  };
  ESP_LOGI(TAG, "Initializing LCD backlight");
  ledc_channel_config(&ledc_channel);
  ledc_timer_config(&ledc_timer);
}


void set_backlight_brightness(uint8_t brightness) {
  ESP_LOGI(TAG, "Setting LCD backlight brightness to %d", ((0b1 << LCD_BL_PWM_RESOLUTION) - 1) * brightness / 100);
  ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_BL_LEDC_CHANNEL, ((0b1 << LCD_BL_PWM_RESOLUTION) - 1) * brightness / 100);
  ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_BL_LEDC_CHANNEL);
}


IRAM_ATTR static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) {
  esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) lv_display_get_user_data(disp);
  xSemaphoreGive(sem_gui_ready);
  xSemaphoreTake(sem_vsync_end, portMAX_DELAY);
  esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map);
  lv_disp_flush_ready(disp);
}


IRAM_ATTR static bool on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data) {
  BaseType_t high_task_awoken = pdFALSE;
  if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
    xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
  }
  return high_task_awoken == pdTRUE;
}


IRAM_ATTR static void lvgl_port_task(void *arg) {
  ESP_LOGI(TAG, "Starting LVGL task");
  uint32_t task_delay_ms = LVGL_TASK_MAX_DELAY_MS;
  while (1) {
    if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
      task_delay_ms = lv_task_handler();
      ui_tick();
      xSemaphoreGiveRecursive(lvgl_mux);
    }
    if (task_delay_ms > LVGL_TASK_MAX_DELAY_MS) {
      task_delay_ms = LVGL_TASK_MAX_DELAY_MS;
    } else if (task_delay_ms < LVGL_TASK_MIN_DELAY_MS) {
      task_delay_ms = LVGL_TASK_MIN_DELAY_MS;
    }
    vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
  }
}


void init_lcd(esp_lcd_panel_handle_t *panel_handle) {
  ESP_LOGI(TAG, "Create LCD semaphores");
  sem_vsync_end = xSemaphoreCreateBinary();
  assert(sem_vsync_end);
  sem_gui_ready = xSemaphoreCreateBinary();
  assert(sem_gui_ready);
  esp_lcd_rgb_panel_config_t panel_config = {
    .clk_src = LCD_CLK_SRC_DEFAULT,
    .timings = {
      .pclk_hz = LCD_PIXEL_CLOCK_HZ,
      .h_res = LCD_H_RES,
      .v_res = LCD_V_RES,
      .hsync_pulse_width = LCD_HSYNC_PULSE_WIDTH,
      .hsync_back_porch = LCD_HSYNC_BACK_PORCH,
      .hsync_front_porch = LCD_HSYNC_FRONT_PORCH,
      .vsync_pulse_width = LCD_VSYNC_PULSE_WIDTH,
      .vsync_back_porch = LCD_VSYNC_BACK_PORCH,
      .vsync_front_porch = LCD_VSYNC_FRONT_PORCH,
      .flags = {
        .hsync_idle_low = false,
        .vsync_idle_low = false,
        .de_idle_high = false,
        .pclk_active_neg = true,
        .pclk_idle_high = false,
      },
    },
    .data_width = 16,
    .bits_per_pixel = 0,
    .num_fbs = 2,
    .bounce_buffer_size_px = 0,
    .sram_trans_align = 0,
    .psram_trans_align = 64,
    .hsync_gpio_num = LCD_PIN_NUM_HSYNC,
    .vsync_gpio_num = LCD_PIN_NUM_VSYNC,
    .de_gpio_num = LCD_PIN_NUM_DE,
    .pclk_gpio_num = LCD_PIN_NUM_PCLK,
    .disp_gpio_num = LCD_PIN_NUM_DISP_EN,
    .data_gpio_nums = {
      LCD_PIN_NUM_DATA0,
      LCD_PIN_NUM_DATA1,
      LCD_PIN_NUM_DATA2,
      LCD_PIN_NUM_DATA3,
      LCD_PIN_NUM_DATA4,
      LCD_PIN_NUM_DATA5,
      LCD_PIN_NUM_DATA6,
      LCD_PIN_NUM_DATA7,
      LCD_PIN_NUM_DATA8,
      LCD_PIN_NUM_DATA9,
      LCD_PIN_NUM_DATA10,
      LCD_PIN_NUM_DATA11,
      LCD_PIN_NUM_DATA12,
      LCD_PIN_NUM_DATA13,
      LCD_PIN_NUM_DATA14,
      LCD_PIN_NUM_DATA15,
    },
    .flags = {
      .disp_active_low = 0,
      .refresh_on_demand = 0,
      .fb_in_psram = true,
      .double_fb = true,
      .no_fb = 0,
      .bb_invalidate_cache = 0,
    },
  };
  ESP_LOGI(TAG, "Create RGB LCD panel");
  ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, panel_handle));
  ESP_LOGI(TAG, "Register event callbacks");
  esp_lcd_rgb_panel_event_callbacks_t cbs = {
    .on_vsync = on_vsync_event,
  };
  esp_lcd_rgb_panel_register_event_callbacks(*panel_handle, &cbs, NULL);
  ESP_LOGI(TAG, "Initialize RGB LCD panel");
  ESP_ERROR_CHECK(esp_lcd_panel_reset(*panel_handle));
  ESP_ERROR_CHECK(esp_lcd_panel_init(*panel_handle));
}


void init_lvgl(esp_lcd_panel_handle_t panel_handle) {
  ESP_LOGI(TAG, "Create LVGL semaphores");
  lvgl_mux = xSemaphoreCreateRecursiveMutex();
  assert(lvgl_mux);
  ESP_LOGI(TAG, "Initialize LVGL library");
  lv_init();
  ESP_LOGI(TAG, "Set system tick callback");
  lv_tick_set_cb(millis);
  ESP_LOGI(TAG, "Allocating LVGL buffers");
  void *buf1 = NULL;
  void *buf2 = NULL;
  ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2));
  static lv_display_t *disp;
  disp = lv_display_create(LCD_H_RES, LCD_V_RES);
  lv_display_set_buffers(disp, buf1, buf2, LCD_H_RES * LCD_V_RES * (LV_COLOR_DEPTH / 8), LV_DISPLAY_RENDER_MODE_DIRECT);
  lv_display_set_user_data(disp, panel_handle);
  lv_display_set_flush_cb(disp, lvgl_flush_cb);
}


void init_display(void) {
  esp_lcd_panel_handle_t lcd = NULL;
  init_backlight();
  init_lcd(&lcd);
  init_lvgl(lcd);
  if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
    ui_init();
    xSemaphoreGiveRecursive(lvgl_mux);
  }
  ESP_LOGI(TAG, "Create LVGL task");
  xTaskCreate(lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL);
}


void setup() {
  Serial.begin(115200);
  ESP_LOGI(TAG, "Start");
  init_display();
  set_backlight_brightness(100);

}


void loop() {

  delay(1);
}

 

JC8048W550C(※ESP32-S3貼り替え済み) 動作テストコード

ESP32-S3向け、LCDがRGB565パラレルで接続されているものは流用が効くと思います。

上記、ESP32-4827S043N向けのコードに加えて静電容量式のタッチパネルの処理が入っています。

また画面解像度も異なります。

//#include "esp_log.h"
//static const char *TAG = "main";

#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "driver/ledc.h"
#include "driver/i2c.h"
#include "lv_conf.h"
extern "C" {
#include "src/lcd_touch/esp_lcd_touch_gt911.h"
#include "src/ui/ui.h"
}

#define LCD_H_RES              800
#define LCD_V_RES              480
#define LCD_PIXEL_CLOCK_HZ     (12.5 * 1000 * 1000)
#define LCD_HSYNC_PULSE_WIDTH  4
#define LCD_HSYNC_BACK_PORCH   8
#define LCD_HSYNC_FRONT_PORCH  8
#define LCD_VSYNC_PULSE_WIDTH  4
#define LCD_VSYNC_BACK_PORCH   8
#define LCD_VSYNC_FRONT_PORCH  8
#define LCD_PIN_NUM_HSYNC      GPIO_NUM_39
#define LCD_PIN_NUM_VSYNC      GPIO_NUM_41
#define LCD_PIN_NUM_DE         GPIO_NUM_40
#define LCD_PIN_NUM_PCLK       GPIO_NUM_42
#define LCD_PIN_NUM_DISP_EN    GPIO_NUM_NC
#define LCD_PIN_NUM_DATA0      GPIO_NUM_8   // B0
#define LCD_PIN_NUM_DATA1      GPIO_NUM_3   // B1
#define LCD_PIN_NUM_DATA2      GPIO_NUM_46  // B2
#define LCD_PIN_NUM_DATA3      GPIO_NUM_9   // B3
#define LCD_PIN_NUM_DATA4      GPIO_NUM_1   // B4
#define LCD_PIN_NUM_DATA5      GPIO_NUM_5   // G0
#define LCD_PIN_NUM_DATA6      GPIO_NUM_6   // G1
#define LCD_PIN_NUM_DATA7      GPIO_NUM_7   // G2
#define LCD_PIN_NUM_DATA8      GPIO_NUM_15  // G3
#define LCD_PIN_NUM_DATA9      GPIO_NUM_16  // G4
#define LCD_PIN_NUM_DATA10     GPIO_NUM_4   // G5
#define LCD_PIN_NUM_DATA11     GPIO_NUM_45  // R0
#define LCD_PIN_NUM_DATA12     GPIO_NUM_48  // R1
#define LCD_PIN_NUM_DATA13     GPIO_NUM_47  // R2
#define LCD_PIN_NUM_DATA14     GPIO_NUM_21  // R3
#define LCD_PIN_NUM_DATA15     GPIO_NUM_14  // R4

#define LCD_BL_PWM_FREQ        1000 
#define LCD_BL_PWM_RESOLUTION  LEDC_TIMER_8_BIT 
#define LCD_BL_LEDC_CHANNEL    LEDC_CHANNEL_0  
#define LCD_BL_LEDC_TIMER      LEDC_TIMER_0 
#define LCD_BL_LEDC_PIN_NUM    GPIO_NUM_2
#define LCD_BL_LEDC_INVERT     0

#define TOUCH_I2C_SCL          GPIO_NUM_20
#define TOUCH_I2C_SDA          GPIO_NUM_19
#define TOUCH_I2C_RST          GPIO_NUM_38
#define TOUCH_I2C_CLK_SPEED    400000
#define TOUCH_I2C_NUM          I2C_NUM_0

#define LVGL_TASK_MAX_DELAY_MS 500
#define LVGL_TASK_MIN_DELAY_MS 1
#define LVGL_TASK_STACK_SIZE   (8 * 1024)
#define LVGL_TASK_PRIORITY     2


SemaphoreHandle_t sem_vsync_end;
SemaphoreHandle_t sem_gui_ready;
SemaphoreHandle_t lvgl_mux;


void init_backlight(void) {
  ledc_channel_config_t ledc_channel = {
    .gpio_num = LCD_BL_LEDC_PIN_NUM,
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .channel = LCD_BL_LEDC_CHANNEL,
    .intr_type = LEDC_INTR_DISABLE,
    .timer_sel = LCD_BL_LEDC_TIMER,
    .duty = 0,
    .hpoint = 0,
    .flags = {
      .output_invert = LCD_BL_LEDC_INVERT,
    },
  };
  ledc_timer_config_t ledc_timer = {
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .duty_resolution = LCD_BL_PWM_RESOLUTION,
    .timer_num = LCD_BL_LEDC_TIMER,
    .freq_hz = LCD_BL_PWM_FREQ,
    .clk_cfg = LEDC_AUTO_CLK,
  };
  ESP_LOGI(TAG, "Initializing LCD backlight");
  ledc_channel_config(&ledc_channel);
  ledc_timer_config(&ledc_timer);
}


void set_backlight_brightness(uint8_t brightness) {
  ESP_LOGI(TAG, "Setting LCD backlight brightness to %d", ((0b1 << LCD_BL_PWM_RESOLUTION) - 1) * brightness / 100);
  ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_BL_LEDC_CHANNEL, ((0b1 << LCD_BL_PWM_RESOLUTION) - 1) * brightness / 100);
  ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_BL_LEDC_CHANNEL);
}


IRAM_ATTR static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) {
  esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) lv_display_get_user_data(disp);
  xSemaphoreGive(sem_gui_ready);
  xSemaphoreTake(sem_vsync_end, portMAX_DELAY);
  esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map);
  lv_disp_flush_ready(disp);
}


IRAM_ATTR static bool on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data) {
  BaseType_t high_task_awoken = pdFALSE;
  if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
    xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
  }
  return high_task_awoken == pdTRUE;
}


IRAM_ATTR static void lvgl_port_task(void *arg) {
  ESP_LOGI(TAG, "Starting LVGL task");
  uint32_t task_delay_ms = LVGL_TASK_MAX_DELAY_MS;
  while (1) {
    if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
      task_delay_ms = lv_task_handler();
      ui_tick();
      xSemaphoreGiveRecursive(lvgl_mux);
    }
    if (task_delay_ms > LVGL_TASK_MAX_DELAY_MS) {
      task_delay_ms = LVGL_TASK_MAX_DELAY_MS;
    } else if (task_delay_ms < LVGL_TASK_MIN_DELAY_MS) {
      task_delay_ms = LVGL_TASK_MIN_DELAY_MS;
    }
    vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
  }
}


void init_lcd(esp_lcd_panel_handle_t *panel_handle) {
  ESP_LOGI(TAG, "Create LCD semaphores");
  sem_vsync_end = xSemaphoreCreateBinary();
  assert(sem_vsync_end);
  sem_gui_ready = xSemaphoreCreateBinary();
  assert(sem_gui_ready);
  esp_lcd_rgb_panel_config_t panel_config = {
    .clk_src = LCD_CLK_SRC_DEFAULT,
    .timings = {
      .pclk_hz = LCD_PIXEL_CLOCK_HZ,
      .h_res = LCD_H_RES,
      .v_res = LCD_V_RES,
      .hsync_pulse_width = LCD_HSYNC_PULSE_WIDTH,
      .hsync_back_porch = LCD_HSYNC_BACK_PORCH,
      .hsync_front_porch = LCD_HSYNC_FRONT_PORCH,
      .vsync_pulse_width = LCD_VSYNC_PULSE_WIDTH,
      .vsync_back_porch = LCD_VSYNC_BACK_PORCH,
      .vsync_front_porch = LCD_VSYNC_FRONT_PORCH,
      .flags = {
        .hsync_idle_low = false,
        .vsync_idle_low = false,
        .de_idle_high = false,
        .pclk_active_neg = true,
        .pclk_idle_high = false,
      },
    },
    .data_width = 16,
    .bits_per_pixel = 0,
    .num_fbs = 2,
    .bounce_buffer_size_px = 0,
    .sram_trans_align = 0,
    .psram_trans_align = 64,
    .hsync_gpio_num = LCD_PIN_NUM_HSYNC,
    .vsync_gpio_num = LCD_PIN_NUM_VSYNC,
    .de_gpio_num = LCD_PIN_NUM_DE,
    .pclk_gpio_num = LCD_PIN_NUM_PCLK,
    .disp_gpio_num = LCD_PIN_NUM_DISP_EN,
    .data_gpio_nums = {
      LCD_PIN_NUM_DATA0,
      LCD_PIN_NUM_DATA1,
      LCD_PIN_NUM_DATA2,
      LCD_PIN_NUM_DATA3,
      LCD_PIN_NUM_DATA4,
      LCD_PIN_NUM_DATA5,
      LCD_PIN_NUM_DATA6,
      LCD_PIN_NUM_DATA7,
      LCD_PIN_NUM_DATA8,
      LCD_PIN_NUM_DATA9,
      LCD_PIN_NUM_DATA10,
      LCD_PIN_NUM_DATA11,
      LCD_PIN_NUM_DATA12,
      LCD_PIN_NUM_DATA13,
      LCD_PIN_NUM_DATA14,
      LCD_PIN_NUM_DATA15,
    },
    .flags = {
      .disp_active_low = 0,
      .refresh_on_demand = 0,
      .fb_in_psram = true,
      .double_fb = true,
      .no_fb = 0,
      .bb_invalidate_cache = 0,
    },
  };
  ESP_LOGI(TAG, "Create RGB LCD panel");
  ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, panel_handle));
  ESP_LOGI(TAG, "Register event callbacks");
  esp_lcd_rgb_panel_event_callbacks_t cbs = {
    .on_vsync = on_vsync_event,
  };
  esp_lcd_rgb_panel_register_event_callbacks(*panel_handle, &cbs, NULL);
  ESP_LOGI(TAG, "Initialize RGB LCD panel");
  ESP_ERROR_CHECK(esp_lcd_panel_reset(*panel_handle));
  ESP_ERROR_CHECK(esp_lcd_panel_init(*panel_handle));
}


void init_lvgl(esp_lcd_panel_handle_t panel_handle, esp_lcd_touch_handle_t touch_handle) {
  ESP_LOGI(TAG, "Create LVGL semaphores");
  lvgl_mux = xSemaphoreCreateRecursiveMutex();
  assert(lvgl_mux);
  ESP_LOGI(TAG, "Initialize LVGL library");
  lv_init();
  ESP_LOGI(TAG, "Set system tick callback");
  lv_tick_set_cb(millis);
  ESP_LOGI(TAG, "Allocating LVGL buffers");
  void *buf1 = NULL;
  void *buf2 = NULL;
  ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2));
  static lv_display_t *disp;
  disp = lv_display_create(LCD_H_RES, LCD_V_RES);
  lv_display_set_buffers(disp, buf1, buf2, LCD_H_RES * LCD_V_RES * (LV_COLOR_DEPTH / 8), LV_DISPLAY_RENDER_MODE_DIRECT);
  lv_display_set_user_data(disp, panel_handle);
  lv_display_set_flush_cb(disp, lvgl_flush_cb);
  ESP_LOGI(TAG, "Register input device driver to LVGL");
  lv_indev_t *indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  lv_indev_set_user_data(indev, touch_handle);
  lv_indev_set_read_cb(indev, touchpad_read);
}


IRAM_ATTR static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data) {
  esp_lcd_touch_handle_t touch_handle = (esp_lcd_touch_handle_t)lv_indev_get_user_data(indev);
  uint16_t touchpad_x;
  uint16_t touchpad_y;
  uint16_t touch_strength;
  uint8_t touch_cnt = 0;
  data->state = LV_INDEV_STATE_REL;
  esp_lcd_touch_read_data(touch_handle);
  bool touchpad_pressed = esp_lcd_touch_get_coordinates(touch_handle, &touchpad_x, &touchpad_y, &touch_strength, &touch_cnt, 1);
  if (touchpad_pressed) {
    ESP_LOGI(TAG, "Touchpad_read %d %d", touchpad_x, touchpad_y);
    data->state = LV_INDEV_STATE_PR;
    data->point.x = touchpad_x;
    data->point.y = touchpad_y;
  }
}


void init_display(void) {
  esp_lcd_panel_handle_t lcd = NULL;
  esp_lcd_touch_handle_t tp = NULL;
  init_backlight();
  init_touch(&tp);
  init_lcd(&lcd);
  init_lvgl(lcd, tp);
  if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
    ui_init();
    xSemaphoreGiveRecursive(lvgl_mux);
  }
  ESP_LOGI(TAG, "Create LVGL task");
  xTaskCreate(lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL);
}


void init_touch(esp_lcd_touch_handle_t *touch_handle) {
  ESP_LOGI(TAG, "Install Touch driver");
  const i2c_config_t i2c_conf = {
    .mode = I2C_MODE_MASTER,
    .sda_io_num = TOUCH_I2C_SDA,
    .scl_io_num = TOUCH_I2C_SCL,
    .sda_pullup_en = GPIO_PULLUP_DISABLE,
    .scl_pullup_en = GPIO_PULLUP_DISABLE,
    .master = {
      .clk_speed = TOUCH_I2C_CLK_SPEED,
    },
  };
  ESP_LOGI(TAG, "i2c_param_config");
  i2c_param_config(TOUCH_I2C_NUM, &i2c_conf);
  ESP_LOGI(TAG, "i2c_driver_install");
  i2c_driver_install(TOUCH_I2C_NUM, i2c_conf.mode, 0, 0, 0);
  const esp_lcd_touch_config_t tp_cfg = {
    .x_max = LCD_H_RES,
    .y_max = LCD_V_RES,
    .rst_gpio_num = TOUCH_I2C_RST, 
    .int_gpio_num = GPIO_NUM_NC,
    .levels = {
      .reset = 0,
      .interrupt = 0,
    },
    .flags = {
      .swap_xy = 0,
      .mirror_x = 0,
      .mirror_y = 0,
    },
  };
  esp_lcd_panel_io_handle_t tp_io_handle = NULL;
  const esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
  ESP_LOGI(TAG, "Create LCD panel IO handle");
  esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)TOUCH_I2C_NUM, &tp_io_config, &tp_io_handle);
  ESP_LOGI(TAG, "Create a new GT911 touch driver");
  esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, touch_handle);
}



void setup() {
  Serial.begin(115200);
  ESP_LOGI(TAG, "Start");
  init_display();
  set_backlight_brightness(100);


}


void loop() {

  delay(1);
}

 

スタックサイズについて

タスクを作成する際のスタックメモリ量指定が難しいです。

マイコンという限られたリソースのなか、タスクに割り当てるスタックのサイズは少ないとエラーが発生して停止しますし、多く割り当てるとその分無駄が大きくなります。

クラッシュして再起動を繰り返し「Debug exception reason: Stack canary watchpoint triggered (LVGL)」のようなメッセージがシリアルモニタに出ている場合は、サイズを増やしてみると良いと思われます。

Arduino core for the ESP32でのloop関数の設定値は8192なので、そのあたりの数値を基準にするとよいかと思われます。(1024単位が良さそう?)

 

ESP32上でのセマフォ・ミューテックスの必要性

LVGLはスレッドセーフではありません。

つまり、FreeRTOSが動作しているESP32上や、複数のコアが存在するマイコンで動作させるような場合、セマフォ・ミューテックスが必要になってきます。

例えばスレッドが2つ存在して、それぞれからLVGLで表示されている内容を書き換えるような処理があったとします。

それぞれのスレッドは並列に動きますから、片方のスレッドで書き換え処理をしている”最中”にもう片方のスレッドから書き換え処理を初めてしまうということが起こり得ます。

同時に複数箇所から書き換え処理が行われたLVGL側ではどっちの値を取ればいいのか混乱してしまい、動作がおかしくなってしまうなどといった現象が起こります。

そのため、セマフォやミューテックスを活用し、同時に複数から書き換えできないようにする必要があるのです。

ESP32(FreeRTOS)では、xSemaphoreTakeやxSemaphoreGiveといった関数があり、これを実行して排他制御を行います。

基本的にはTakeでセマフォを得て、処理を行い、Giveでセマフォを開放するという流れになります。

Takeでセマフォを得ると、続けて他からTakeが行われた場合にセマフォを得ることができません。

このような流れで複数箇所からの同時処理を防ぐのです。

 

LCDドライバ

Arduino core for the ESP32に収録されています。

そのままインクルードして使用できます。

LCDドライバ周りの記述は以下のパスに入っているはずです。

C:\Users\(ユーザー名)\AppData\Local\Arduino15\packages\esp32\tools\esp32-arduino-libs\idf-release_v5.1-b6b4727c58\esp32s3\include\esp_lcd\include\esp_lcd_panel_rgb.h

上記パラレルLCDドライバはESP32-S3でないと使用できません。(無印のESP32には機能が搭載されていません)

SPI液晶など他のバスを使用するLCDはどのESP32でも使えると思います。

 

LVGLの設定ファイル(lv_conf.h)の準備

必ず現在のバージョンのライブラリからコピーしてきます。

例えばLVGL v8の設定ファイルを流用すると、LVGL v9ではビルドが通らないなど不具合があるので、必ず現在のバージョンのライブラリからコピーして持ってくるようにしましょう。

C:\Users\(ユーザー名)\Documents\Arduino\libraries\lvgl\lv_conf_template.h

にあるファイルをスケッチと同じディレクトリにコピーし、lv_conf.hに名前変更しておきます。

#if 1 /*Set it to "1" to enable content*/

ファイル中15行目にある値を1に変更して、設定を有効にしましょう。

設定内容に関しては他にいじらなくて大丈夫です。

スケッチと同じフォルダに入れ、「#include "lv_conf.h"」でインクルードするのが一番良いとは思います。

万が一、うまく動作しない場合は、「#define LV_CONF_INCLUDE_SIMPLE」を入れるなどなにかしら対応する必要がありそうです。

※実際の設定読み込み処理に関しては以下のファイル中、40行目~あたりに書かれていますので参考まで。

C:\Users\(ユーザー名)\Documents\Arduino\libraries\lvgl\src\lv_conf_internal.h

 

ESP-BSPの準備

様々なところで例に上げられているタッチデバイスについては、ESP-BSPというボードサポート用のパッケージに入っています。

ESP-IDFをインストールした際には収録されているようですが、今回使用するArduino core for the ESP32には収録されていません。

タッチパネル周りのものだけ esp-bsp/components/lcd_touch/ 以下から必要なファイルをダウンロードして以下のように配置して使用しています。

  • src/lcd_touch/esp_lcd_touch.c
  • src/lcd_touch/esp_lcd_touch.h
  • src/lcd_touch/esp_lcd_touch_gt911.c
  • src/lcd_touch/esp_lcd_touch_gt911.h

必要なものだけを抽出した関係で、一部の定数(CONFIG_ESP_LCD_TOUCH_MAX_POINTS)が宣言されずにビルドが通りませんでした。

対処は以下のように行いました。

 

build_opt.hの準備

-DCONFIG_ESP_LCD_TOUCH_MAX_POINTS=5

build_opt.hというテキストファイルを用意し、上記の内容を書き込みます。

Arduino core for the ESP32では、スケッチと同じ位置にあるbuild_opt.hファイルを読み込むようです。

そのため、コンパイラの-DオプションでCONFIG_ESP_LCD_TOUCH_MAX_POINTS=5を宣言するようにしました。

#define CONFIG_ESP_LCD_TOUCH_MAX_POINTS 5

と同じ効果があります。

 

Arduino IDE上での設定

8MB以上のPSRAMを備えたものでは

  • PSRAM: OPI PSRAM

設定にすること。

でないと上記スケッチはフレームバッファ分のRAMが確保できないので動きません。

具体的には

  • ESP32-S3-WROOM-1-N4R8
  • ESP32-S3-WROOM-1-N8R8
  • ESP32-S3-WROOM-1-N16R8

のものは Octal SPI 接続のPSRAMになります。

 

EEZ Studioで生成されるLVGLコードと変数のやり取りをする方法

数値を表示したりするのに基本となる変数での値のやり取りの方法についてメモしておきます。

 

※注意:変数名に関して内部ではスネークケースで扱うようです。

キャメルケースで入力しても、生成されるゲッター・セッター関数名に関してはスネークケースになるようです。

例:変数名 lblText と入力したが、生成される関数名は get_var_lbl_text() 等。

そのため変数名はスネークケース表記(小文字でアンダースコアで区切る:lbl_text_1 等)をオススメしておきます。

 

EEZ Studio上での変数追加

これはウィジェットから参照する値をいれるための変数の宣言です。

私はVisual Baisc 6.0時代の癖で、3文字プレフィックスを付けることが多いです。

(Button→btn, Label→lbl 等。気になる人は「Visual Basic Prefixes」等で検索してみてください)

 

EEZ Studio上の左ペインから「Variables」を探します。

「Global」と「Enums」とあり、「Global」が変数の一覧になっています。

プラスマークの「Add Item」ボタンをクリックし、変数を追加してみましょう。

「New Global Variable」というウィンドウが表示されます。

「Name」に変数名を指定(スネークケース推奨)、「Type」で変数のデータ型を指定します。

integer float double boolean string ENUMS enum:$LVGLDir enum:$LVGLKey

 

EEZ Studioで生成されるコードの仕組み

src/ui/vars.h ファイル内には変数のゲッター・セッター関数のプロトタイプ宣言が追加されます。

  • get_var_[変数名](スネークケース)
  • set_var_[変数名](スネークケース)

これらはLVGL側からのゲッター・セッターとして動きます。

表示だけのウィジェットを使用している場合は、外部からの入力を受け付けないため、セッター(set_var_)関数の実体は無くても良いと思われます。

extern で宣言されているため、関数の実体はCリンケージされたどのファイルに記載されていても大丈夫です。

src/ui/screens.c ファイル内の tick_screen_[ページ名] 関数内に実際の処理が書かれています。

いま表示されている値と、get_var_[変数名] 関数で得られた値を比べて、違う場合に表示更新を行うようです。

これらの処理は、src/ui/ui.c ファイル内の ui_tick 関数から呼ばれています。

なお、入力可能なウィジェットを配置した場合には、src/ui/screens.c ファイル内上部のイベントハンドラ関数によって、セッター(set_var_)が呼ばれるようです。

 

実際の値のやり取り方法(ラベル・文字列)

ラベルに設定した変数「lbl_text」に対応するゲッター関数の実体。

ui_tick 関数が呼ばれるたびに以下の関数も呼ばれます。

(.cpp/.ino 中に書く場合にCリンケージにするためにextern "C"を記述しています。.c 中に書く場合は除外してください。)

extern "C" const char *get_var_lbl_text() {
  return "test";
}

固定値を返す方法は、EEZ Studio側でも設定できるので使うことは無いでしょう。

動的に変更するには変数を用意し、以下のようになります。

String lbl_text = "test";

void loop() {
  
  if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
    lbl_text = "test 1";
    xSemaphoreGiveRecursive(lvgl_mux);
  }
  delay(1000);

  if (xSemaphoreTakeRecursive(lvgl_mux, portMAX_DELAY) == pdTRUE) {
    lbl_text = "test 2";
    xSemaphoreGiveRecursive(lvgl_mux);
  }
  delay(1000);

}


extern "C" const char *get_var_lbl_text() {
  return lbl_text.c_str();
}

表示内容の変数「lbl_text」を用意し、初期状態では文字列「test」を代入。

1秒おきに、内容が「test 1」、「test 2」と切り替わります。

ゲッターが呼ばれている最中に書き換えされるとマズいので、セマフォで同時には動かないように対策しています。

そのため、「lbl_text = 」の代入文をセマフォのTakeとGiveで囲っています。

 

EEZ Studioで生成されるLVGLコードからアクションを受け取る方法

タッチパネル入力等を受け取って任意の処理を実行する方法についてメモしておきます。

 

環境について

- 環境 バージョン 概要
開発環境 Arduino 2.3.2 Arduino IDE使用
ボード定義 esp32 3.0.4 ESP32S3 Dev Module設定
ライブラリ LVGL 9.2.0 GUI用
GUIデザイン EEZ Studio 0.15.1 LVGL v9 GUIデザイン

 

参考リンク

こちらの記事も参考にどうぞ

 

各種情報