ESP32-S3 + 2.8 吋 TFT LCD 觸控功能教學(LovyanGFX + XPT2046)

延續前兩篇 ESP32-S3 + TFT LCD 教學,本篇啟用 XPT2046 觸控功能。透過麵包板分接共用 SPI 腳位,搭配 LovyanGFX 內建觸控支援,實作畫板、按鈕、調色盤三個範例。

ESP32-S3 + 2.8 吋 TFT LCD 觸控功能教學(LovyanGFX + XPT2046)

前兩篇教學我們成功在 ESP32-S3 上顯示繁體中文,也可以顯示JPG圖檔了,接下來這篇要來啟用螢幕上的觸控功能。這塊 2.8 吋 TFT 模組內建 XPT2046 電阻式觸控晶片,LovyanGFX 原生支援,不用額外裝函式庫。

如果你還沒看過第一篇的入門教學,建議可以看看,因為重複的部份,這個章節就不再說明囉!

ESP32-S3 + 2.8 吋 TFT LCD 顯示繁體中文(LovyanGFX)
使用 LovyanGFX 函式庫,在 ESP32-S3 開發板上驅動 2.8 吋 TFT LCD,內建 efontTW 字型直接顯示繁體中文,從接線、設定到顯示圖形與中文字,一步步帶你完成。

至於材料也和前面兩篇一樣,主要是一塊ESP32-S3開發板,還有本篇的主角2.8吋ST7789 LCD。

ESP32-S3 N16R8 開發板 (16MB Flash / 8MB PSRAM)
傑森創工,專注於Arduino、ESP32、樹莓派(Raspberry Pi)、物聯網、創客(Maker)相關商品的研究,專業銷售Arduino材料、Arduino教材、各種電子材料、開發板、Arduino套件、感測器模組,以及各類工具。更提供許多獨家的專題套件,供大學或高中職學生製作Arduino、ESP32專題。最專業的Arduino、ESP32供應商。
2.8吋SPI LCD 240*320 TFT模組 ST7789
2.8吋彩屏,支援16BIT RGB 65K色顯示,顯示色彩豐富 320X240解析度,可選觸摸功能 採用SPI序列匯流排,只需幾個IO即可點亮顯示 帶SD卡槽方便擴展實驗 提供豐富的示例程序 軍工級工藝標準,長期穩定工作 提供底層驅動技術支援

觸控原理簡介

這塊模組使用的是電阻式觸控(Resistive Touch),跟手機的電容式觸控不同。電阻式需要用指尖或觸控筆稍微施力按壓才能感應,用指甲或筆尖效果最好。

觸控晶片 XPT2046 透過 SPI 跟 ESP32-S3 溝通,跟螢幕共用同一組 SPI 匯流排(SCK、MOSI、MISO),只是用不同的 CS 腳位來切換。這也是為什麼 LovyanGFX 設定裡要把 bus_shared 設為 true

額外接線

在上一篇的基礎上,多接幾條線給觸控功能。

上一篇已經接好的(不動)

LCD 模組排針 ESP32-S3 GPIO
VCC 3.3V
GND GND
CS GPIO 15
RESET GPIO 16
DC GPIO 17
SDI (MOSI) GPIO 18
SCK GPIO 8
LED 3.3V

新增觸控接線

LCD 模組排針 ESP32-S3 GPIO 說明
SDO (MISO) GPIO 13 SPI 讀取(上一篇沒接,現在要接了)
T_CLK GPIO 8 共用 SCK(已經接好)
T_DIN GPIO 18 共用 MOSI(已經接好)
T_DO GPIO 13 共用 MISO(同上)
T_CS GPIO 21 觸控片選(獨立腳位)
T_IRQ 不接 可不接,用輪詢方式讀取
重點: 觸控的 T_CLK、T_DIN、T_DO 跟螢幕的 SCK、MOSI、MISO 是共用的。實際上你需要多拉兩條線:SDO (MISO) → GPIO 13T_CS → GPIO 21。但因為共用腳位的關係,SCK、MOSI、MISO 這三組訊號需要從一個 GPIO 同時接到螢幕端和觸控端,所以需要透過麵包板做分接。

共用 SPI 腳位的麵包板接法

螢幕和觸控共用 SCK、MOSI、MISO 三組 SPI 訊號。一個 GPIO 要同時接到兩個地方,利用麵包板同一行互通的特性來分接。

分接原理

麵包板同一直線的 5 個孔是互通的。把 ESP32 的 GPIO 接到同一行的上方孔,再從下方孔分別拉線到螢幕端和觸控端。

不經過麵包板的線(直接接)

連接 說明
3.3V → VCC 供電
3.3V → LED 背光
GND → GND 接地
GPIO 15 → CS 螢幕片選(獨立)
GPIO 16 → RESET 重置
GPIO 17 → DC 資料/命令
GPIO 21 → T_CS 觸控片選(獨立)

總線數

類型 數量
上一篇已有的線 8 條
SCK 分接(多拉到 T_CLK) 1 條
MISO 新接(GPIO 13 → 麵包板 → SDO + T_DO) 3 條
MOSI 分接(多拉到 T_DIN) 1 條
T_CS → GPIO 21 1 條
新增合計 6 條

跟上一篇的程式碼差異

設定項目 上一篇 這一篇
cfg.pin_miso -1 13
cfg.readable false true
cfg.bus_shared false true
觸控設定 新增 Touch_XPT2046

觸控校正(Calibration)

電阻式觸控的原始讀值跟螢幕像素不一定精確對齊,校正可以提高準確度。強烈建議在正式使用觸控之前先跑一次校正,記下校正數據。

校正程式


#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {
  lgfx::Bus_SPI _bus_instance;
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Touch_XPT2046 _touch_instance;

public:
  LGFX(void) {
    {
      auto cfg = _bus_instance.config();
      cfg.spi_host   = SPI2_HOST;
      cfg.spi_mode   = 0;
      cfg.freq_write  = 40000000;
      cfg.freq_read   = 16000000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_dc     = 17;
      _bus_instance.config(cfg);
      _panel_instance.setBus(&_bus_instance);
    }
    {
      auto cfg = _panel_instance.config();
      cfg.pin_cs     = 15;
      cfg.pin_rst    = 16;
      cfg.pin_busy   = -1;
      cfg.memory_width  = 240;
      cfg.memory_height = 320;
      cfg.panel_width   = 240;
      cfg.panel_height  = 320;
      cfg.readable   = true;
      cfg.invert     = false;
      cfg.rgb_order  = false;
      cfg.dlen_16bit = false;
      cfg.bus_shared = true;
      _panel_instance.config(cfg);
    }
    {
      auto cfg = _touch_instance.config();
      cfg.x_min      = 0;
      cfg.x_max      = 239;
      cfg.y_min      = 0;
      cfg.y_max      = 319;
      cfg.pin_int    = -1;
      cfg.bus_shared = true;
      cfg.offset_rotation = 0;
      cfg.spi_host   = SPI2_HOST;
      cfg.freq       = 2500000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_cs     = 21;
      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);
    }
    setPanel(&_panel_instance);
  }
};

LGFX display;

void setup() {
  Serial.begin(115200);
  display.init();
  display.setRotation(1);
  display.fillScreen(TFT_BLACK);

  display.setFont(&fonts::efontTW_16);
  display.setTextColor(TFT_WHITE);
  display.setCursor(10, 10);
  display.println("觸控校正程式");
  display.setCursor(10, 40);
  display.println("請依序點擊螢幕上的十字");
  delay(2000);

  // 執行觸控校正(螢幕會出現十字讓你點)
  uint16_t calData[8];
  display.calibrateTouch(calData, TFT_WHITE, TFT_BLACK, 20);

  // 把校正數據印出來,記下這組數字
  Serial.print("校正數據:{");
  for (int i = 0; i < 8; i++) {
    Serial.print(calData[i]);
    if (i < 7) Serial.print(", ");
  }
  Serial.println("};");

  display.fillScreen(TFT_BLACK);
  display.setFont(&fonts::efontTW_16);
  display.setTextColor(TFT_GREEN);
  display.setCursor(10, 10);
  display.println("校正完成!");
  display.setCursor(10, 40);
  display.println("請打開 Serial Monitor");
  display.setCursor(10, 70);
  display.println("記下校正數據");
  display.setCursor(10, 110);
  display.setTextColor(TFT_YELLOW);
  display.println("現在可以試著觸碰螢幕");
  display.println("看看座標是否正確");
}

void loop() {
  lgfx::touch_point_t tp;
  if (display.getTouch(&tp)) {
    display.fillCircle(tp.x, tp.y, 3, TFT_CYAN);
    Serial.printf("X:%d Y:%d\n", tp.x, tp.y);
  }
}

程式執行後,會依序在螢幕的4個角出現箭號,請用觸控筆依指示小心點4個角落。

完成後就可以在畫面中隨意塗鴨了!如果發現定位不準,那就重跑一次程式,校正到準確為止。

使用校正數據

校正後 Serial Monitor 會印出一組數字,例如:

校正數據:{319, 3, 3800, 252, 3, 3780, 250, 3800};

把它記下來。之後的程式在 setup() 裡直接載入,就不用每次都重新校正:

uint16_t calData[8] = {319, 3, 3800, 252, 3, 3780, 250, 3800};
display.setTouchCalibrate(calData);

範例一:觸控畫板

最簡單的觸控測試,把剛才校正程式中的塗鴨程式獨立出來,做個陽春版的小畫家, 手指(請用指甲)碰哪裡就畫一個點,所以連續畫就是一條線囉!

這支程式的重點,在於你必須把校正程式中得到的校正數據先填入程式中,這樣這支新的程式就可以不經過校正,而得到精準的點位囉!

#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {
  lgfx::Bus_SPI _bus_instance;
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Touch_XPT2046 _touch_instance;

public:
  LGFX(void) {
    {
      auto cfg = _bus_instance.config();
      cfg.spi_host   = SPI2_HOST;
      cfg.spi_mode   = 0;
      cfg.freq_write  = 40000000;
      cfg.freq_read   = 16000000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_dc     = 17;
      _bus_instance.config(cfg);
      _panel_instance.setBus(&_bus_instance);
    }
    {
      auto cfg = _panel_instance.config();
      cfg.pin_cs     = 15;
      cfg.pin_rst    = 16;
      cfg.pin_busy   = -1;
      cfg.memory_width  = 240;
      cfg.memory_height = 320;
      cfg.panel_width   = 240;
      cfg.panel_height  = 320;
      cfg.readable   = true;
      cfg.invert     = false;
      cfg.rgb_order  = false;
      cfg.dlen_16bit = false;
      cfg.bus_shared = true;
      _panel_instance.config(cfg);
    }
    {
      auto cfg = _touch_instance.config();
      cfg.x_min      = 0;
      cfg.x_max      = 239;
      cfg.y_min      = 0;
      cfg.y_max      = 319;
      cfg.pin_int    = -1;
      cfg.bus_shared = true;
      cfg.offset_rotation = 0;
      cfg.spi_host   = SPI2_HOST;
      cfg.freq       = 2500000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_cs     = 21;
      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);
    }
    setPanel(&_panel_instance);
  }
};

LGFX display;

void setup() {
  Serial.begin(115200);
  display.init();
  display.setRotation(1);
  display.fillScreen(TFT_BLACK);

  // 把校正數據貼在這裡  重要!
  uint16_t calData[8] = {你的校正數據};
  display.setTouchCalibrate(calData);

  display.setFont(&fonts::efontTW_16);
  display.setTextColor(TFT_WHITE);
  display.setCursor(10, 10);
  display.println("觸控畫板 - 請用手指畫畫看");

  display.drawRect(0, 0, 320, 240, TFT_DARKGREY);
}

void loop() {
  lgfx::touch_point_t tp;

  if (display.getTouch(&tp)) {
    display.fillCircle(tp.x, tp.y, 3, TFT_YELLOW);
    Serial.printf("X:%d Y:%d\n", tp.x, tp.y);
  }
}

上傳後用手指或觸控筆在螢幕上滑動,應該會看到黃色的筆跡。


範例二:觸控按鈕

畫面上有三個按鈕,按下去會變色並顯示對應文字:

#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {
  lgfx::Bus_SPI _bus_instance;
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Touch_XPT2046 _touch_instance;

public:
  LGFX(void) {
    {
      auto cfg = _bus_instance.config();
      cfg.spi_host   = SPI2_HOST;
      cfg.spi_mode   = 0;
      cfg.freq_write  = 40000000;
      cfg.freq_read   = 16000000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_dc     = 17;
      _bus_instance.config(cfg);
      _panel_instance.setBus(&_bus_instance);
    }
    {
      auto cfg = _panel_instance.config();
      cfg.pin_cs     = 15;
      cfg.pin_rst    = 16;
      cfg.pin_busy   = -1;
      cfg.memory_width  = 240;
      cfg.memory_height = 320;
      cfg.panel_width   = 240;
      cfg.panel_height  = 320;
      cfg.readable   = true;
      cfg.invert     = false;
      cfg.rgb_order  = false;
      cfg.dlen_16bit = false;
      cfg.bus_shared = true;
      _panel_instance.config(cfg);
    }
    {
      auto cfg = _touch_instance.config();
      cfg.x_min      = 0;
      cfg.x_max      = 239;
      cfg.y_min      = 0;
      cfg.y_max      = 319;
      cfg.pin_int    = -1;
      cfg.bus_shared = true;
      cfg.offset_rotation = 0;
      cfg.spi_host   = SPI2_HOST;
      cfg.freq       = 2500000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_cs     = 21;
      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);
    }
    setPanel(&_panel_instance);
  }
};

LGFX display;

// 按鈕結構
struct Button {
  int x, y, w, h;
  const char* label;
  uint16_t color;
  bool pressed;
};

Button buttons[3] = {
  {10,  80, 90, 50, "紅燈",  TFT_RED,    false},
  {115, 80, 90, 50, "綠燈",  TFT_GREEN,  false},
  {220, 80, 90, 50, "藍燈",  TFT_BLUE,   false},
};

void drawButton(Button &btn) {
  uint16_t color = btn.pressed ? TFT_WHITE : btn.color;
  display.fillRoundRect(btn.x, btn.y, btn.w, btn.h, 6, color);
  display.drawRoundRect(btn.x, btn.y, btn.w, btn.h, 6, TFT_WHITE);
  display.setFont(&fonts::efontTW_16);
  display.setTextColor(btn.pressed ? TFT_BLACK : TFT_WHITE);
  display.setCursor(btn.x + 18, btn.y + 16);
  display.println(btn.label);
}

void drawStatusBar(const char* msg, uint16_t color) {
  display.fillRect(0, 160, 320, 40, TFT_BLACK);
  display.setFont(&fonts::efontTW_24);
  display.setTextColor(color);
  display.setCursor(10, 166);
  display.println(msg);
}

void setup() {
  Serial.begin(115200);
  display.init();
  display.setRotation(1);
  display.fillScreen(TFT_BLACK);

  // 把校正數據貼在這裡
  uint16_t calData[8] = {你的校正數據};
  display.setTouchCalibrate(calData);

  // 標題
  display.fillRect(0, 0, 320, 40, display.color565(0, 80, 160));
  display.setFont(&fonts::efontTW_24);
  display.setTextColor(TFT_WHITE);
  display.setCursor(10, 8);
  display.println("觸控按鈕測試");

  // 說明文字
  display.setFont(&fonts::efontTW_14);
  display.setTextColor(TFT_DARKGREY);
  display.setCursor(10, 52);
  display.println("請按下方按鈕:");

  // 畫按鈕
  for (int i = 0; i < 3; i++) {
    drawButton(buttons[i]);
  }

  drawStatusBar("等待觸碰...", TFT_DARKGREY);

  // 底部提示
  display.setFont(&fonts::efontTW_12);
  display.setTextColor(TFT_DARKGREY);
  display.setCursor(10, 220);
  display.println("傑森創工 ESP32-S3 觸控教學");
}

void loop() {
  lgfx::touch_point_t tp;

  if (display.getTouch(&tp)) {
    // 檢查每個按鈕
    for (int i = 0; i < 3; i++) {
      Button &btn = buttons[i];
      if (tp.x >= btn.x && tp.x <= btn.x + btn.w &&
          tp.y >= btn.y && tp.y <= btn.y + btn.h) {
        if (!btn.pressed) {
          btn.pressed = true;
          drawButton(btn);
          drawStatusBar(btn.label, btn.color);
          Serial.printf("按下:%s\n", btn.label);
        }
      }
    }
    delay(50);  // 簡易防抖
  } else {
    // 放開手指,重置按鈕狀態
    for (int i = 0; i < 3; i++) {
      if (buttons[i].pressed) {
        buttons[i].pressed = false;
        drawButton(buttons[i]);
      }
    }
  }
}

範例三:觸控調色盤

用觸控滑動來調整 RGB 顏色,即時預覽混色結果:

#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {
  lgfx::Bus_SPI _bus_instance;
  lgfx::Panel_ST7789 _panel_instance;
  lgfx::Touch_XPT2046 _touch_instance;

public:
  LGFX(void) {
    {
      auto cfg = _bus_instance.config();
      cfg.spi_host   = SPI2_HOST;
      cfg.spi_mode   = 0;
      cfg.freq_write  = 40000000;
      cfg.freq_read   = 16000000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_dc     = 17;
      _bus_instance.config(cfg);
      _panel_instance.setBus(&_bus_instance);
    }
    {
      auto cfg = _panel_instance.config();
      cfg.pin_cs     = 15;
      cfg.pin_rst    = 16;
      cfg.pin_busy   = -1;
      cfg.memory_width  = 240;
      cfg.memory_height = 320;
      cfg.panel_width   = 240;
      cfg.panel_height  = 320;
      cfg.readable   = true;
      cfg.invert     = false;
      cfg.rgb_order  = false;
      cfg.dlen_16bit = false;
      cfg.bus_shared = true;
      _panel_instance.config(cfg);
    }
    {
      auto cfg = _touch_instance.config();
      cfg.x_min      = 0;
      cfg.x_max      = 239;
      cfg.y_min      = 0;
      cfg.y_max      = 319;
      cfg.pin_int    = -1;
      cfg.bus_shared = true;
      cfg.offset_rotation = 0;
      cfg.spi_host   = SPI2_HOST;
      cfg.freq       = 2500000;
      cfg.pin_sclk   =  8;
      cfg.pin_mosi   = 18;
      cfg.pin_miso   = 13;
      cfg.pin_cs     = 21;
      _touch_instance.config(cfg);
      _panel_instance.setTouch(&_touch_instance);
    }
    setPanel(&_panel_instance);
  }
};

LGFX display;

int r_val = 128, g_val = 128, b_val = 128;

void drawSlider(int y, const char* label, int value, uint16_t color) {
  // 標籤
  display.setFont(&fonts::efontTW_14);
  display.setTextColor(TFT_WHITE, TFT_BLACK);
  display.setCursor(10, y + 2);
  display.printf("%s: %3d", label, value);

  // 滑桿背景
  display.fillRect(100, y, 200, 20, TFT_DARKGREY);
  // 滑桿填充
  int barWidth = map(value, 0, 255, 0, 200);
  display.fillRect(100, y, barWidth, 20, color);
  // 滑桿邊框
  display.drawRect(100, y, 200, 20, TFT_WHITE);
}

void drawColorPreview() {
  uint16_t color = display.color565(r_val, g_val, b_val);
  display.fillRoundRect(10, 150, 300, 60, 8, color);
  display.drawRoundRect(10, 150, 300, 60, 8, TFT_WHITE);

  // 顯示 hex 色碼
  display.setFont(&fonts::efontTW_14);
  // 自動選擇文字顏色(深色背景用白字,淺色用黑字)
  int brightness = (r_val * 299 + g_val * 587 + b_val * 114) / 1000;
  display.setTextColor(brightness > 128 ? TFT_BLACK : TFT_WHITE);
  display.setCursor(110, 170);
  display.printf("#%02X%02X%02X", r_val, g_val, b_val);
}

void setup() {
  Serial.begin(115200);
  display.init();
  display.setRotation(1);
  display.fillScreen(TFT_BLACK);

  // 把校正數據貼在這裡
  uint16_t calData[8] = {你的校正數據};
  display.setTouchCalibrate(calData);

  // 標題
  display.fillRect(0, 0, 320, 36, display.color565(0, 80, 160));
  display.setFont(&fonts::efontTW_24);
  display.setTextColor(TFT_WHITE);
  display.setCursor(10, 6);
  display.println("觸控調色盤");

  // 說明
  display.setFont(&fonts::efontTW_12);
  display.setTextColor(TFT_DARKGREY);
  display.setCursor(10, 44);
  display.println("滑動色條調整 RGB 值:");

  drawSlider(65,  "紅", r_val, TFT_RED);
  drawSlider(95,  "綠", g_val, TFT_GREEN);
  drawSlider(125, "藍", b_val, TFT_BLUE);
  drawColorPreview();

  // 底部
  display.setFont(&fonts::efontTW_12);
  display.setTextColor(TFT_DARKGREY);
  display.setCursor(10, 220);
  display.println("傑森創工 ESP32-S3 觸控教學");
}

void loop() {
  lgfx::touch_point_t tp;

  if (display.getTouch(&tp)) {
    // 判斷觸碰在哪個滑桿上
    bool changed = false;

    if (tp.x >= 100 && tp.x <= 300) {
      int value = map(constrain(tp.x, 100, 300), 100, 300, 0, 255);

      if (tp.y >= 55 && tp.y <= 90) {
        r_val = value;
        drawSlider(65, "紅", r_val, TFT_RED);
        changed = true;
      }
      else if (tp.y >= 85 && tp.y <= 120) {
        g_val = value;
        drawSlider(95, "綠", g_val, TFT_GREEN);
        changed = true;
      }
      else if (tp.y >= 115 && tp.y <= 150) {
        b_val = value;
        drawSlider(125, "藍", b_val, TFT_BLUE);
        changed = true;
      }
    }

    if (changed) {
      drawColorPreview();
      Serial.printf("R:%d G:%d B:%d\n", r_val, g_val, b_val);
    }

    delay(30);
  }
}

常見問題排查

觸控完全沒反應

  1. 確認 MISO 有接 — 上一篇不需要 MISO,但觸控一定要接,因為 XPT2046 會回傳座標資料
  2. 確認 T_CS 有接 — T_CS 是觸控的片選,必須接到獨立的 GPIO
  3. 確認共用腳位有分接 — SCK、MOSI、MISO 要透過麵包板同時接到螢幕端和觸控端,缺一不可
  4. 確認 bus_shared = true — 螢幕和觸控共用 SPI,這個設定一定要開

觸控座標不準(碰左邊出現在右邊)

調整 cfg.offset_rotation,值 0~7 逐個試,找到跟你的 setRotation() 搭配正確的那一個。

觸控靈敏度太低

電阻式觸控需要一定的壓力才能感應。用指甲或觸控筆效果比指腹好。如果完全壓不出反應,可能是觸控面板本身有問題。

螢幕顯示會閃爍

開啟 bus_shared = true 後,每次讀取觸控都會短暫中斷螢幕的 SPI 通訊。如果你的 loop() 裡頻繁重繪整個畫面,就會看到閃爍。解法是只更新有變化的區域,不要整個 fillScreen 重畫。


下一步

到這裡,你已經能在 ESP32-S3 上實現螢幕顯示圖片 + 繁體中文 + 觸控互動了。後續可以進一步嘗試:

  • SD 卡讀取自訂字型 — 用 OpenFontRender 載入思源黑體 TTF
  • LVGL GUI 框架 — 專業級的嵌入式圖形介面
  • 實用專案 — WiFi 氣象站、智慧家居控制面板、IoT 儀表板