2024年7月16日火曜日

ESP32とAtomDisplayで10.1インチの抵抗膜式モニタを扱う


背景

ゴミや水が散る可能性のある場所にマイコンで制御するタッチパネルを置きたかったので、自分が知っている最も大きい抵抗膜式のモニタ(10.1インチ LCD、画像HDMI、タッチ SPI)をESP32とAtomDisplayを使って制御しました。
備忘録として取り組んだ内容を記事に残します。

使ったもの

  • 10.1インチ抵抗膜式モニタ(LCD) 1024×600, HDMI, IPS
    画像はHDMI、タッチ情報はSPIで通信するモニタです。
  • AtomDisplay
    ESP32からのSPI通信でHDMIの信号を出す装置です。
    M5Stackディスプレイモジュールでも同じことができそうです。
    どなたか試して成功したら教えていただけると嬉しいです。
  • ESP32 devkitC
    AtomDisplayにはESP32が内蔵されているAtomLite付属していますが、必要最低限のピンしか出ていないためタッチ情報を扱うためのCSピンが足りません。
    そのためESP32 devkitCを別途使います。
  • HDMIケーブル
    AtomDisplayとモニタを繋ぎます。
  • microB USBケーブル 2本
    devkitCへのプログラムを書き込みとモニタへの給電に利用します。
    devkitCへの給電だけでも動きますがdevkitC上のダイオードがとても熱くなるので、モニタへの給電ケーブルは別途行うのが良いです。
  • プログラムを書き込むPC
    ESP32にプログラムを書き込みます。
    この記事ではVSCode + PlatformIOの環境を利用しました。
  • ブレッドボード x2
    ESP32の信号線を引き出してAtomDisplayとタッチのSPI通信線を繋ぎます。
  • ジャンパワイヤ
    ブレッドボード上の信号線の接続に利用します。
  • ケーブル色々
    AtomDisplayとブレッドボードの接続には耐熱電子ワイヤを利用しました。
    モニタのタッチ情報用SPIとブレッドボード間HDMIケーブルと同じ長さのは6芯のケーブルを使うのが良いですが、手元に無かったので3芯ケーブルを2本並列して繋ぎました。
  • ピンヘッダ
    ケーブルをはんだ付して、ブレッドボードやモニタに接続して信号線を引き出します。
  • テープ + 油性ペン
    ピンヘッダに巻きつけて絶縁し、ペンで役割を記載します。
  • ユニバーサル基板
    AtomDisplayからの信号線引き出しに利用します。
    ユニバーサル基板が無かったので幅が同じだったSOP28変換基板を利用しました。
  • 低頭ピンソケット
    ユニバーサル基板を介してケーブルをはんだ付けしてAtomDisplayの信号線を引き出します。
  • はんだ + はんだごて
    ケーブルのはんだ付けに利用します。
  • ワイヤストリッパー、ニッパー
    ケーブルの切断と被覆剥きに利用します。

配線

注意点と共に配線内容を説明します。


機器間の接続は内容はこうです。
モニタ画像 <- HDMIケーブル -> AtomDisplay <- 自作9芯ケーブル -> ESP32 devkitC
モニタタッチ <- 自作6芯ケーブル -> ESP32 devkitC

自作ケーブルの箇所を解説します。

AtomDisplay <- 自作9芯ケーブル -> ESP32 devkitC

AtomDisplayから出ている端子9本全てを使います。
5Vと3.3V両方の給電が必要です。
この記事では使わないAtomLiteのピンと同じESP32のピンに配線します。


モニタタッチ <- 自作6芯ケーブル -> ESP32 devkitC

モニタの背面にあるRaspberry Pi向けの端子から下記6本を引き出します。
モニタの詳細情報は製造業者のWikiで確認できます。
AtomDisplayのSPIと共存させると双方の通信内容が壊れて安定稼働が難しかったのでSPIバスを分けました。
  • 5V -> ESP32の5V
  • GND -> ESP32の5V
  • MOSI ->18番ピンに接続
  • MISO ->17番ピンに接続
  • SCK ->16番ピンに接続
  • CS -> 5番ピンに接続

今回は引き出したケーブルに都合が良い順序に割当ました。
割当はプログラムで変更可能です。





プログラム

ESP32用の画像描画ライブラリLovyanGFXはAtomDisplay(M5HDMI)とXPT2046に対応しているので、AtomDisplay用の初期化コードをコピーしてXPT2046の設定を追加しました。

作成したプログラムを含むPlatformIOのプロジェクトをgithubで公開しています。

pio-esp32-atom-display-and-xpt2046

上記のプロジェクトのmain.cppは200行以上ありますが、記事を書いている時点でのコードをここでも共有して要所を解説します。
#include <Arduino.h>

#include <LovyanGFX.hpp>
#include <lgfx/v1/LGFXBase.hpp>

#if __has_include(<sdkconfig.h>)
#include <sdkconfig.h>
#include <soc/efuse_reg.h>

#if __has_include(<esp_idf_version.h>)
#include <esp_idf_version.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0)
#define M5ATOMDISPLAY_SPI_DMA_CH SPI_DMA_CH_AUTO
#endif
#endif

#else
#include <lgfx/v1/platforms/sdl/Panel_sdl.hpp>
#endif
// #include <M5GFX.h>
#include <lgfx/v1/panel/Panel_M5HDMI.hpp>
#include <lgfx/v1/touch/Touch_XPT2046.hpp>

#ifndef M5ATOMDISPLAY_LOGICAL_WIDTH
#define M5ATOMDISPLAY_LOGICAL_WIDTH 1280
#endif
#ifndef M5ATOMDISPLAY_LOGICAL_HEIGHT
#define M5ATOMDISPLAY_LOGICAL_HEIGHT 720
#endif
#ifndef M5ATOMDISPLAY_REFRESH_RATE
#define M5ATOMDISPLAY_REFRESH_RATE 0.0f
#endif
#ifndef M5ATOMDISPLAY_OUTPUT_WIDTH
#define M5ATOMDISPLAY_OUTPUT_WIDTH 0
#endif
#ifndef M5ATOMDISPLAY_OUTPUT_HEIGHT
#define M5ATOMDISPLAY_OUTPUT_HEIGHT 0
#endif
#ifndef M5ATOMDISPLAY_SCALE_W
#define M5ATOMDISPLAY_SCALE_W 0
#endif
#ifndef M5ATOMDISPLAY_SCALE_H
#define M5ATOMDISPLAY_SCALE_H 0
#endif
#ifndef M5ATOMDISPLAY_PIXELCLOCK
#define M5ATOMDISPLAY_PIXELCLOCK 74250000
#endif

class M5AtomDisplayWithTouch : public lgfx::v1::LGFX_Device {
// class M5AtomDisplayWithTouch : public M5GFX {
public:
struct config_t {
uint16_t logical_width = M5ATOMDISPLAY_LOGICAL_WIDTH;
uint16_t logical_height = M5ATOMDISPLAY_LOGICAL_HEIGHT;
float refresh_rate = M5ATOMDISPLAY_REFRESH_RATE;
uint16_t output_width = M5ATOMDISPLAY_OUTPUT_WIDTH;
uint16_t output_height = M5ATOMDISPLAY_OUTPUT_HEIGHT;
uint_fast8_t scale_w = M5ATOMDISPLAY_SCALE_W;
uint_fast8_t scale_h = M5ATOMDISPLAY_SCALE_H;
uint32_t pixel_clock = M5ATOMDISPLAY_PIXELCLOCK;
};

config_t config(void) const { return config_t(); }

M5AtomDisplayWithTouch(const config_t& cfg) {
_board = lgfx::board_t::board_M5AtomDisplay;
_config = cfg;
}

M5AtomDisplayWithTouch(uint16_t logical_width = M5ATOMDISPLAY_LOGICAL_WIDTH,
uint16_t logical_height = M5ATOMDISPLAY_LOGICAL_HEIGHT,
float refresh_rate = M5ATOMDISPLAY_REFRESH_RATE,
uint16_t output_width = M5ATOMDISPLAY_OUTPUT_WIDTH,
uint16_t output_height = M5ATOMDISPLAY_OUTPUT_HEIGHT,
uint_fast8_t scale_w = M5ATOMDISPLAY_SCALE_W,
uint_fast8_t scale_h = M5ATOMDISPLAY_SCALE_H,
uint32_t pixel_clock = M5ATOMDISPLAY_PIXELCLOCK) {
_board = lgfx::board_t::board_M5AtomDisplay;
_config.logical_width = logical_width;
_config.logical_height = logical_height;
_config.refresh_rate = refresh_rate;
_config.output_width = output_width;
_config.output_height = output_height;
_config.scale_w = scale_w;
_config.scale_h = scale_h;
_config.pixel_clock = pixel_clock;
}

bool init_impl(bool use_reset, bool use_clear) {
// if (_panel_last.get() != nullptr) {
// return true;
// }

const int i2c_port = 1;
const int i2c_sda = GPIO_NUM_25;
const int i2c_scl = GPIO_NUM_21;
const int spi_cs = GPIO_NUM_33;
const int spi_mosi = GPIO_NUM_19;
const int spi_miso = GPIO_NUM_22;
const int spi_sclk = GPIO_NUM_23;
const auto spi_host = SPI2_HOST;

const int spi_touch_cs = GPIO_NUM_5;
// const int spi_touch_mosi = spi_mosi;
// const int spi_touch_miso = spi_miso;
// const int spi_touch_sclk = spi_sclk;
// const auto spi_touch_host = spi_host;
const int spi_touch_mosi = GPIO_NUM_18;
const int spi_touch_miso = GPIO_NUM_17;
const int spi_touch_sclk = GPIO_NUM_16;
const auto spi_touch_host = SPI3_HOST;

auto p = new lgfx::Panel_M5HDMI();
if (!p) {
return false;
}

auto bus_spi = new lgfx::Bus_SPI();
{
auto cfg = bus_spi->config();
cfg.freq_write = 80000000;
cfg.freq_read = 16000000;
cfg.spi_mode = 3;
cfg.spi_host = spi_host;
cfg.dma_channel = M5ATOMDISPLAY_SPI_DMA_CH;
cfg.use_lock = true;
cfg.pin_mosi = spi_mosi;
cfg.pin_miso = spi_miso;
cfg.pin_sclk = spi_sclk;
cfg.spi_3wire = false;

bus_spi->config(cfg);
p->setBus(bus_spi);
// _bus_last.reset(bus_spi);
}

{
auto cfg = p->config_transmitter();
cfg.freq_read = 400000;
cfg.freq_write = 400000;
cfg.pin_scl = i2c_scl;
cfg.pin_sda = i2c_sda;
cfg.i2c_port = i2c_port;
cfg.i2c_addr = 0x39;
cfg.prefix_cmd = 0x00;
cfg.prefix_data = 0x00;
cfg.prefix_len = 0;
p->config_transmitter(cfg);
}

{
auto cfg = p->config();
cfg.offset_rotation = 3;
cfg.pin_cs = spi_cs;
cfg.readable = false;
cfg.bus_shared = false;
p->config(cfg);
p->setRotation(1);

lgfx::Panel_M5HDMI::config_resolution_t cfg_reso;
cfg_reso.logical_width = _config.logical_width;
cfg_reso.logical_height = _config.logical_height;
cfg_reso.refresh_rate = _config.refresh_rate;
cfg_reso.output_width = _config.output_width;
cfg_reso.output_height = _config.output_height;
cfg_reso.scale_w = _config.scale_w;
cfg_reso.scale_h = _config.scale_h;
cfg_reso.pixel_clock = _config.pixel_clock;
p->config_resolution(cfg_reso);
}
{
auto t = new lgfx::Touch_XPT2046();
auto cfg = t->config();
cfg.bus_shared = false;
cfg.spi_host = spi_touch_host;
cfg.pin_cs = spi_touch_cs;
cfg.pin_mosi = spi_touch_mosi;
cfg.pin_miso = spi_touch_miso;
cfg.pin_sclk = spi_touch_sclk;
cfg.offset_rotation = 1;
cfg.freq = 125000;
t->config(cfg);
p->touch(t);
}
setPanel(p);
// _panel_last.reset(p);

if (lgfx::LGFX_Device::init_impl(use_reset, use_clear)) {
return true;
}
setPanel(nullptr);
// _panel_last.reset();

return false;
}

protected:
config_t _config;
};

#define DISPLAY_WIDTH 1024
#define DISPLAY_HEIGHT 600

M5AtomDisplayWithTouch display(DISPLAY_WIDTH, DISPLAY_HEIGHT);

void setup() {
Serial.begin(115200);
display.begin();
// uint16_t calib[] = {215, 173, 205, 3916, 3733, 145, 3803, 3938};
// 左下, 右下 ,左上, 右上
// (215, 173) x方向接触点より外側に描画される -> (400, 0)
// 右下横方向 3800外 4200内
uint16_t calib[] = {215, 50, 0, 4000, 3733, 60, 3950, 4000};
display.setTouchCalibrate(calib);
// uint16_t calib[8];
// display.calibrateTouch(calib, TFT_WHITE, TFT_BLACK);
// Serial.print("uint16_t calib[] = {");
// for (int i = 0; i < 8; ++i) {
// if (i != 0) {
// Serial.print(",");
// }
// Serial.print(calib[i]);
// }
// Serial.println("};");
}

void loop() {
display.setCursor(0, 0);
display.println("hi " + String(millis()));
lgfx::v1::touch_point_t pointTouch;
if (display.getTouch(&pointTouch) > 0) {
Serial.println("x: " + String(pointTouch.x) +
" y: " + String(pointTouch.y));
display.drawCircle(pointTouch.x, pointTouch.y, 1, TFT_WHITE);
Serial.println(millis());
}
}

setPanelの前にタッチIC XPT2046の設定を追加

AtomDisplayをコピーして作成したAtomDisplayWithTouchのinit_impl関数内のsetPanelを呼ぶ前にXPT2046の設定を追加すると今回利用するモニタのタッチ機能を使えます。
class M5AtomDisplayWithTouch : public lgfx::v1::LGFX_Device {
bool init_impl(bool use_reset, bool use_clear) {
// 他の設定

const int spi_touch_cs = GPIO_NUM_5;
const int spi_touch_mosi = GPIO_NUM_18;
const int spi_touch_miso = GPIO_NUM_17;
const int spi_touch_sclk = GPIO_NUM_16;
const auto spi_touch_host = SPI3_HOST;

// 他の設定

{
auto t = new lgfx::Touch_XPT2046();
auto cfg = t->config();
cfg.bus_shared = false;
cfg.spi_host = spi_host;
cfg.pin_cs = spi_cs_touch;
cfg.pin_mosi = spi_mosi;
cfg.pin_miso = spi_miso;
cfg.pin_sclk = spi_sclk;
cfg.offset_rotation = 1;
cfg.freq = 125000;
t->config(cfg);
p->touch(t);
}
setPanel(p); // 元からある記述
}
}

AtomDisplayでSPI2_HOSTを使っているのでXPT2046にはSPI3_HOSTを割り当てました。
    const auto spi_touch_host = SPI3_HOST;

XPT2046の仕様としては1MHzで通信可能なはずですが、ケーブルが長いためかそれだと通信内容が意図しない値になるので、リフレッシュレートに合わせて125kHzにしました。
      cfg.freq = 125000;

タッチ位置を補正

LovyanGFXには補正機能があるので、それを利用してタッチ位置を補正します。

下記のコードを有効にすると補正が始まり、処理後に補正値がシリアルモニタに表示されます。
void setup() {
// 他の初期化処理
uint16_t calib[] = {215, 50, 0, 4000, 3733, 60, 4000, 4000};
display.setTouchCalibrate(calib);
uint16_t calib[8];
display.calibrateTouch(calib, TFT_WHITE, TFT_BLACK);
Serial.print("uint16_t calib[] = {");
for (int i = 0; i < 8; ++i) {
if (i != 0) {
Serial.print(", ");
}
Serial.print(calib[i]);
}
Serial.println("};");
}

取得した補正値はsetTouchCalibrateに渡すと再利用できます。
void setup() {
// cfg.offset_rotation = 1 のとき 左下, 右下 ,左上, 右上
uint16_t calib[] = {215, 50, 0, 4000, 3733, 60, 4000, 4000};
display.setTouchCalibrate(calib);
}

補正機能は便利なのですが自分が試したモニタは角のタッチが効かない個体だったので、角付近の補正値を生成後に手動で調整しました。
角から5%内側をタッチなど若干内側での補正になって欲しいです。

動作確認

配線してプログラムを書き込むとタッチ機能付きのモニタの処理が始まります。
画面の角に稼働状況把握のために経過ミリ秒を表示しています。


補正無しだと1cmほどずれる個体でした。


この個体は角のタッチが効かなかったので、補正が難儀でした。


ずれが許容範囲になるよう補正値を調整しました。


ゆっくり押すと測定値が飛ぶので、正確性を上げたい場合は複数測定して中央値を扱うのが良さそうです。


おわり

ESP32、AtomDisplay、LovyanGFXを利用して抵抗膜タッチ式の10.1インチモニタを扱えました。
プログラムや回路に改善可能な箇所があれば、コメントやtwitter(今はX)などで教えていただけると嬉しいです。

参考

10.1インチ抵抗膜式モニタ(LCD) 1024×600, HDMI, IPS
AtomDisplay - switch science
AtomDisplay lovyanGFX

変更履歴

2024.07.18
安定稼働できない注意書きを記事冒頭に追加しました。
2024.07.22
XPT2046のSPIバスを分けることで安定稼働できたので、フォトカプラと抵抗を使う回路の紹介は止めて、SPIバスを2つ使う内容に記事を更新しました。

0 件のコメント :