Building an ESP32-based Smart Switch with LVGL Display: Part 2 - Touch Integration

Project Intro

Welcome to our series on building a sophisticated ESP32-based smart switch! Our final product will feature a touch-enabled LVGL display, WiFi connectivity, MQTT integration for remote control, and audio feedback. This powerful device will allow you to control your home appliances with style and ease.

In this second post, we'll focus on integrating touch input. By the end of this tutorial, you'll create a simple "Hello Arduino!" label on the display and learn how to integrate touch to make the "Click Me!" button work.

Let's dive in!

Important Note on Libraries and Hardware

LVGL (Light and Versatile Graphics Library) is a powerful and efficient graphics library designed for embedded systems. It allows you to create beautiful and responsive user interfaces with minimal resource usage. This project combines LVGL with the Arduino_GFX library, a versatile graphics library that supports a wide range of displays, to provide a seamless development experience on the ESP32.

This project utilizes the following libraries and hardware:

ArduinoGFX Library: Version 1.4.7 (the latest as of this writing)
LVGL Library: Version 8.3.11
While newer versions (9.x) of LVGL exist, we're using 8.3.11 for two reasons:
* Compatibility with the Squareline software, which will be used in later parts of this series.
* Long-term support for the 8.x versions.
Hardware: WT32SC01 board (standard version, not the Plus variant)

While this guide is tailored for the WT32SC01 board, you can make minor adjustments to adapt it for other hardware. The principles and most of the code will remain applicable across various ESP32-based boards with suitable displays.

WT32-SC01 Board Config

TFT (ST7796)

  • TFT_RST=22
  • TFT_SCLK=14
  • TFT_DC=21
  • TFT_CS=15
  • TFT_MOSI=13
  • TFT_MISO=-1
  • TFT_BCKL=23

Touch (FT6336U)

  • TOUCH_SDA=18
  • TOUCH_SCL=19
  • I2C_TOUCH_ADDRESS=0x38

Step-by-step Instruction

  1. Install Libraries:

    • In the Arduino IDE, go to Sketch > Include Library > Manage Libraries...
    • Search for "Arduino_GFX" and install the latest version (1.4.7 or later).
    • Search for "LVGL" and install version 8.3.11.
  2. Prepare the Code:

    • Copy and paste the following code into your Arduino IDE:
// Core Arduino and ESP32 libraries
#include <Arduino.h>
#include <SPI.h>

// Graphics and UI libraries
#include <lvgl.h>
#include <Arduino_GFX_Library.h>

// Display configuration
#define GFX_BL 23 // Default backlight pin

// Screen configuration
static const uint32_t SCREEN_WIDTH = 480;
static const uint32_t SCREEN_HEIGHT = 320;

// LVGL draw buffer
static lv_disp_draw_buf_t draw_buf;
static lv_color_t *disp_draw_buf;
static lv_disp_drv_t disp_drv;

// Display setup
#if defined(DISPLAY_DEV_KIT)
Arduino_GFX *gfx = create_default_Arduino_GFX();
#else
Arduino_DataBus *bus = new Arduino_ESP32SPI(21, 15, 14, 13, GFX_NOT_DEFINED, HSPI);
Arduino_GFX *gfx = new Arduino_ST7796(bus, 22, 3, false);
#endif

// Include touch handling
#include "src/touch.h"

// Function declarations
void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
void my_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data);
void initSerial();
void initDisplay();
void initLVGL();
void setupLVGLBuffer();
void setupLVGLDriver();

void setup() {
    initSerial();
    initDisplay();
    initLVGL();
}

void loop() {
    lv_timer_handler(); // Handle LVGL tasks
    delay(5);
}

// Initialize serial communication
void initSerial() {
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    while(!Serial);
    Serial.println("Arduino_GFX LVGL_Arduino example");
}

// Initialize the display and touch screen
void initDisplay() {
    if (!gfx->begin()) {
        Serial.println("gfx->begin() failed!");
        return;
    }
    gfx->fillScreen(BLACK);
    
    #ifdef GFX_BL
        pinMode(GFX_BL, OUTPUT);
        digitalWrite(GFX_BL, HIGH);
    #endif
    
    touch_init(gfx->width(), gfx->height(), gfx->getRotation());
}

// Initialize LVGL library and set up UI
void initLVGL() {
    lv_init();
    
    setupLVGLBuffer();
    setupLVGLDriver();

    // Create a simple label
    lv_obj_t * label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Hello Arduino!");
    lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}

// Set up LVGL draw buffer
void setupLVGLBuffer() {
    uint32_t bufSize = SCREEN_WIDTH * 40;
    disp_draw_buf = (lv_color_t *)heap_caps_malloc(bufSize * sizeof(lv_color_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
    if (!disp_draw_buf) {
        Serial.println("LVGL disp_draw_buf allocate failed!");
        return;
    }
    lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, NULL, bufSize);
}

// Configure LVGL display driver
void setupLVGLDriver() {
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res = SCREEN_WIDTH;
    disp_drv.ver_res = SCREEN_HEIGHT;
    disp_drv.flush_cb = my_disp_flush;
    disp_drv.draw_buf = &draw_buf;
    lv_disp_drv_register(&disp_drv);

    static lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = my_touchpad_read;
    lv_indev_drv_register(&indev_drv);
}

// LVGL display flush callback
void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    #if (LV_COLOR_16_SWAP != 0)
    gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
    #else
    gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
    #endif

    lv_disp_flush_ready(disp_drv);
}

// LVGL touchpad read callback
void my_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
    if (touch_has_signal()) {
        if (touch_touched()) {
            data->state = LV_INDEV_STATE_PR;
            data->point.x = touch_last_x;
            data->point.y = touch_last_y;
        } else if (touch_released()) {
            data->state = LV_INDEV_STATE_REL;
        }
    } else {
        data->state = LV_INDEV_STATE_REL;
    }
}

  1. Create touch.h:
    • Create a new file named touch.h.
    • Copy and paste the following code into touch.h:
/*******************************************************************************
 * Touch libraries:
 * XPT2046: https://github.com/PaulStoffregen/XPT2046_Touchscreen.git
 *
 * Capacitive touchscreen libraries
 * TouchLib: https://github.com/mmMicky/TouchLib.git
 *
 * #define CTS328_SLAVE_ADDRESS  (0x1A)
 * #define L58_SLAVE_ADDRESS     (0X5A)
 * #define CTS826_SLAVE_ADDRESS  (0X15)
 * #define CTS820_SLAVE_ADDRESS  (0X15)
 * #define CTS816S_SLAVE_ADDRESS (0X15)
 * #define FT3267_SLAVE_ADDRESS  (0x38)
 * #define FT5x06_ADDR           (0x38)
 * #define GT911_SLAVE_ADDRESS1  (0X5D)
 * #define GT911_SLAVE_ADDRESS2  (0X14)
 * #define ZTW622_SLAVE1_ADDRESS (0x20)
 * #define ZTW622_SLAVE2_ADDRESS (0x46)
 *
 ******************************************************************************/
#define FT5x06_ADDR           (0x38)

/* uncomment for XPT2046 */
// #define TOUCH_XPT2046
// #define TOUCH_XPT2046_SCK 12
// #define TOUCH_XPT2046_MISO 13
// #define TOUCH_XPT2046_MOSI 11
// #define TOUCH_XPT2046_CS 10
// #define TOUCH_XPT2046_INT 18
// #define TOUCH_XPT2046_ROTATION 0
// #define TOUCH_XPT2046_SAMPLES 50

// uncomment for most capacitive touchscreen
 #define TOUCH_MODULES_FT5x06 // GT911 / CST_SELF / CST_MUTUAL / ZTW622 / L58 / FT3267 / FT5x06
 #define TOUCH_MODULE_ADDR FT5x06_ADDR // CTS328_SLAVE_ADDRESS / L58_SLAVE_ADDRESS / CTS826_SLAVE_ADDRESS / CTS820_SLAVE_ADDRESS / CTS816S_SLAVE_ADDRESS / FT3267_SLAVE_ADDRESS / FT5x06_ADDR / GT911_SLAVE_ADDRESS1 / GT911_SLAVE_ADDRESS2 / ZTW622_SLAVE1_ADDRESS / ZTW622_SLAVE2_ADDRESS
 #define TOUCH_SCL 19
 #define TOUCH_SDA 18
 #define TOUCH_RES -1
 #define TOUCH_INT -1

// Please fill below values from Arduino_GFX Example - TouchCalibration
bool touch_swap_xy = false;
int16_t touch_map_x1 = -1;
int16_t touch_map_x2 = -1;
int16_t touch_map_y1 = -1;
int16_t touch_map_y2 = -1;

int16_t touch_max_x = 0, touch_max_y = 0;
int16_t touch_raw_x = 0, touch_raw_y = 0;
int16_t touch_last_x = 0, touch_last_y = 0;

#if defined(TOUCH_XPT2046)
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
XPT2046_Touchscreen ts(TOUCH_XPT2046_CS, TOUCH_XPT2046_INT);

#elif defined(TOUCH_MODULE_ADDR) // TouchLib
#include <Wire.h>
#include <TouchLib.h>
TouchLib touch(Wire, TOUCH_SDA, TOUCH_SCL, TOUCH_MODULE_ADDR);

#endif // TouchLib

void touch_init(int16_t w, int16_t h, uint8_t r)
{
  touch_max_x = w - 1;
  touch_max_y = h - 1;
  if (touch_map_x1 == -1)
  {
    switch (r)
    {
    case 3:
      touch_swap_xy = true;
      touch_map_x1 = touch_max_x;
      touch_map_x2 = 0;
      touch_map_y1 = 0;
      touch_map_y2 = touch_max_y;
      break;
    case 2:
      touch_swap_xy = false;
      touch_map_x1 = touch_max_x;
      touch_map_x2 = 0;
      touch_map_y1 = touch_max_y;
      touch_map_y2 = 0;
      break;
    case 1:
      touch_swap_xy = true;
      touch_map_x1 = 0;
      touch_map_x2 = touch_max_x;
      touch_map_y1 = touch_max_y;
      touch_map_y2 = 0;
      break;
    default: // case 0:
      touch_swap_xy = false;
      touch_map_x1 = 0;
      touch_map_x2 = touch_max_x;
      touch_map_y1 = 0;
      touch_map_y2 = touch_max_y;
      break;
    }
  }

#if defined(TOUCH_XPT2046)
  SPI.begin(TOUCH_XPT2046_SCK, TOUCH_XPT2046_MISO, TOUCH_XPT2046_MOSI, TOUCH_XPT2046_CS);
  ts.begin();
  ts.setRotation(TOUCH_XPT2046_ROTATION);

#elif defined(TOUCH_MODULE_ADDR) // TouchLib
  // Reset touchscreen
#if (TOUCH_RES > 0)
  pinMode(TOUCH_RES, OUTPUT);
  digitalWrite(TOUCH_RES, 0);
  delay(200);
  digitalWrite(TOUCH_RES, 1);
  delay(200);
#endif
  Wire.begin(TOUCH_SDA, TOUCH_SCL);
  touch.init();

#endif // TouchLib
}

bool touch_has_signal()
{
#if defined(TOUCH_XPT2046)
  return ts.tirqTouched();

#elif defined(TOUCH_MODULE_ADDR) // TouchLib
  // TODO: implement TOUCH_INT
  return true;
#endif                           // TouchLib

  return false;
}

void translate_touch_raw()
{
  if (touch_swap_xy)
  {
    touch_last_x = map(touch_raw_y, touch_map_x1, touch_map_x2, 0, touch_max_x);
    touch_last_y = map(touch_raw_x, touch_map_y1, touch_map_y2, 0, touch_max_y);
  }
  else
  {
    touch_last_x = map(touch_raw_x, touch_map_x1, touch_map_x2, 0, touch_max_x);
    touch_last_y = map(touch_raw_y, touch_map_y1, touch_map_y2, 0, touch_max_y);
  }
  // Serial.printf("touch_raw_x: %d, touch_raw_y: %d, touch_last_x: %d, touch_last_y: %d\n", touch_raw_x, touch_raw_y, touch_last_x, touch_last_y);
}

bool touch_touched()
{
#if defined(TOUCH_XPT2046)
  if (ts.touched())
  {
    TS_Point p = ts.getPoint();
    touch_raw_x = p.x;
    touch_raw_y = p.y;
    int max_z = p.z;
    int count = 0;
    while ((ts.touched()) && (count < TOUCH_XPT2046_SAMPLES))
    {
      count++;

      TS_Point p = ts.getPoint();
      if (p.z > max_z)
      {
        touch_raw_x = p.x;
        touch_raw_y = p.y;
        max_z = p.z;
      }
      Serial.printf("touch_raw_x: %d, touch_raw_y: %d, p.z: %d\n", touch_raw_x, touch_raw_y, p.z);
    }
    translate_touch_raw();
    return true;
  }
#elif defined(TOUCH_MODULE_ADDR) // TouchLib
  if (touch.read())
  {
    TP_Point t = touch.getPoint(0);
    touch_raw_x = t.x;
    touch_raw_y = t.y;

    touch_last_x = touch_raw_x;
    touch_last_y = touch_raw_y;

    translate_touch_raw();
    return true;
  }

#endif // TouchLib

  return false;
}

bool touch_released()
{
#if defined(TOUCH_XPT2046)
  return true;

#elif defined(TOUCH_MODULE_ADDR) // TouchLib
  return false;
#endif                           // TouchLib

  return false;
}
  1. Install the FT6336U Library:
    • In the Arduino IDE, go to Sketch > Include Library > Manage Libraries...
    • Search for "FT6336U" and install the latest version.

Upload and Test

  1. Connect your WT32-SC01 board to your computer.
  2. Select the correct board and port in the Arduino IDE.
  3. Upload the code to your board.
  4. Once uploaded, you should see the "Hello Arduino!" label displayed on the screen.
  5. Try touching the screen; the touch input should be registered.

This project provides a basic foundation for using LVGL on the ESP32 with the Arduino_GFX library. You can expand upon this by adding more complex UI elements and interactions to create your own custom embedded applications. Remember to consult the LVGL documentation for more advanced features and customization options.

Code Explanation

Unlock the secrets of this project! Subscribe for a detailed code walkthrough.