MQ. Logo
enes
cover22

How to configure LVGL with ESP-IDF

In this second part we are going to configure the touch screen and make a demo with LVGL

Photo of MQuero.

Manuel Quero

·
11/14/2024

In this second part of the tutorial we are going to configure the i2c driver, the touch screen and we are going to start LVGL.

When you finish the article, you will be able to run one of the default examples that LVGL comes with:

LVGL-music-demo

Let's continue where we left off in the first part of the tutorial. If you have not read the first part of the tutorial I leave it in the following link:

Part 1: How to configure a display to use LVGL with ESP-IDF

My Setup

I am using the following configuration of components and versions. Even if you are not using the same components or the same version of ESP-IDF, don't worry because it will probably work for you too.

  • Microcontroller: ESP32-S3-WROOM-2
  • 2-inch 320x240 Touch Screen
    • Driver: ST7789
    • Driver del táctil: cst816s
  • ESP-IDF 5.3.1
  • LVGL 9.2.2

En el siguiente repositorio de GitHub, os podeis descargar y copiar el codigo completo del artículo, por si lo quereis tener entero ya a mano.

GitHub Repository

We are going to write the code step by step and explain it.

I2C Configuration

First of all, let's configure the i2c to communicate with the touch screen driver and to be able to use it.

We are going to create two new files in the same folder as the main.c folder.

  • i2c_driver.h
/**
* @file i2c_driver.h
* @brief I2C driver interface configuration header file
* @author MQuero
* @see mquero.com
*/

#ifndef I2C_DRIVER_H
#define I2C_DRIVER_H

/**********************
*      INCLUDES
*********************/
#include "driver/i2c_master.h"

/**********************
*      VARIABLES
**********************/
extern i2c_master_bus_handle_t i2c_bus_handle;

/**********************
* GLOBAL PROTOTYPES
**********************/
void i2c_init(void);
void i2c_deinit(void);

#endif /*I2C_DRIVER_H*/
  • i2c_driver.c
/**
* @file i2c_driver.c
* @brief I2C driver implementation file
* @author MQuero
* @see mquero.com
*/

/**********************
*      INCLUDES
*********************/
#include "i2c_driver.h"

/**********************
*      DEFINES
*********************/
#define BOARD_I2C_PORT I2C_NUM_0
#define BOARD_I2C_SDA 47
#define BOARD_I2C_SCL 21

/**********************
*      VARIABLES
**********************/
i2c_master_bus_handle_t i2c_bus_handle;

/**********************
* GLOBAL FUNCTIONS
**********************/
void i2c_init(void)
{
     i2c_master_bus_config_t i2c_bus_config = {
          .i2c_port = BOARD_I2C_PORT,
          .sda_io_num = BOARD_I2C_SDA,
          .scl_io_num = BOARD_I2C_SCL,
          .clk_source = I2C_CLK_SRC_DEFAULT,
          .glitch_ignore_cnt = 7,
          .intr_priority = 0,
          .flags.enable_internal_pullup = 0};
     ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle));    
}

void i2c_deinit(void)
{
     ESP_ERROR_CHECK(i2c_del_master_bus(i2c_bus_handle));
}

These two files will allow us to initialize the i2c bus through the i2c_init(void) function, and we will be able to control it through its i2c_bus_handle, which we will have available because in the header file we have declared it as extern.

It is important that you modify the DEFINES in the source file to configure the pins to your board or configuration.

In another tutorial I will tell you about the i2c driver and all its configuration. For now, with these two files everything will work.

To use the touch screen and to make it work with LVGL, we are going to configure it based on the templates that the LVGL documentation has.

LVGL Templates

As we did with the screen in the first part of the tutorial, we are going to create two new files, lv_port_indev.h and lv_port_indev.c

These two files are going to take care of initializing the screen touch and configuring LVGL to use it.

Header File lv_port_indev.h

Let's write this code:

/**
 * @file lv_port_indev.h
 * @brief Input device interface configuration header file
 * @author MQuero
 * @see mquero.com
*/

#ifndef LV_PORT_INDEV_H
#define LV_PORT_INDEV_H

/**********************
 *      INCLUDES
 *********************/
#include "esp_lcd_touch.h"

/**********************
 *      VARIABLES
**********************/
extern esp_lcd_touch_handle_t tp_handle;

/**********************
 * GLOBAL PROTOTYPES
 **********************/ 
void lv_port_indev_init(void);

#endif /*LV_PORT_INDEV_H*/

First, we add the file information and include guards to avoid multiple inclusion of the header file.

In the INCLUDES section, we add the header file esp_lcd_touch.h, which will allow us in the VARIABLES section to declare the variable tp_handle of type esp_lcd_touch_handle_t.

This variable, is going to be the handler of the touch. If you don't know what is a handler, in the first part of the tutorial I explain it (and if not ask ChatGPT that explains better than me🤪).

The word extern is used to be able to access the handler from other files we have in the project.

In the GLOBAL PROTOTYPES section we are going to declare the only global function that we are going to need lv_port_indev_init. This function is going to initialize the touch and register it in LVGL.

Source File lv_port_indev.c

As this file is much longer, let's go in parts.

/**
 * @file lv_port_indev.h
 * @brief Input device interface configuration header file
 * @author MQuero
 * @see mquero.com
 */

/**********************
 *      INCLUDES
 *********************/
#include "lv_port_indev.h"
#include "i2c_driver.h"
#include "esp_lcd_touch_cst816s.h"
#include "lvgl.h"

/**********************
 *      DEFINES
 *********************/
#define CONFIG_LCD_H_RES 240
#define CONFIG_LCD_V_RES 320

#define BOARD_TOUCH_IRQ (-1)
#define BOARD_TOUCH_RST (-1)

/**********************
 *      VARIABLES
 **********************/
esp_lcd_touch_handle_t tp_handle = NULL;

/**********************
 *  STATIC VARIABLES
 **********************/
lv_indev_t *indev_touchpad;

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void touchpad_init(void);
static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data);

We are going to start by writing the basic file information and then, in the INCLUDES section, we are going to add the following header files:

  • lv_port_indev.h to include the previous header.
  • i2c_driver.h to include the file where we have configured the i2c.
  • esp_lcd_touch_cst816s.h to configure our touch.
  • lvgl.h to use the LVGL library.

Then in the DEFINES section, we are going to define the horizontal and vertical resolution of the screen and the interrupt and reset pins. In my case, my screen is 240x320 and I have no interrupt or reset pins.

In the VARIABLES section, we are going to define the screen handler and initialize it with NULL.

For the STATIC VARIABLES section, we are going to create the variable indev_touchpad, which is a pointer of type lv_indev_t. This variable is going to be the handler used by LVGL.

We are going to declare two function prototypes in the STATIC PROTOTYPES section:

  • touchpad_init(void)
  • touchpad_read(lv_indev_t *indev, lv_indev_data_t *data);

These two functions will be used to start and read the touchpad. We will define them later.

Let's continue writing the following code:

/**********************
 *   GLOBAL FUNCTIONS
 **********************/
void lv_port_indev_init(void)
{
     /*Initialize touchpad*/
     touchpad_init();

     /*Register a touchpad input device*/
     indev_touchpad = lv_indev_create();
     lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
     lv_indev_set_read_cb(indev_touchpad, touchpad_read);
}

Here we declare the only global function we have.

  1. We initialize the touchpad with touchpad_init();
  2. With lv_indev_create() we create a new input device for LVGL.
  3. With lv_indev_set_type we tell LVGL that it is of pointer type via LV_INDEV_TYPE_POINTER.
  4. We configure with lv_indev_set_read_cb our callback, which is our function that reads the touchpad.

Let's now define our two static functions:

/**********************
 *   STATIC FUNCTIONS
 **********************/
/*Initialize your touchpad*/
static void touchpad_init(void)
{
    esp_lcd_panel_io_handle_t tp_io_handle = NULL;
    esp_lcd_panel_io_i2c_config_t tp_io_config =
        ESP_LCD_TOUCH_IO_I2C_CST816S_CONFIG();
    tp_io_config.scl_speed_hz = (400 * 1000);
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &tp_io_config,
                                             &tp_io_handle));

    esp_lcd_touch_config_t tp_cfg = {
        .x_max = CONFIG_LCD_H_RES,
        .y_max = CONFIG_LCD_V_RES,
        .rst_gpio_num = BOARD_TOUCH_RST,
        .int_gpio_num = BOARD_TOUCH_IRQ,
        .levels =
            {
                .reset = 0,
                .interrupt = 0,
            },
        .flags =
            {
                .swap_xy = 0,
                .mirror_x = 0,
                .mirror_y = 0,
            },
        .interrupt_callback = NULL,
        .process_coordinates = NULL,
    };
    esp_lcd_touch_new_i2c_cst816s(tp_io_handle, &tp_cfg, &tp_handle);
}

First, let's define touchpad_init to initialize the touchpad. Let's go step by step.

We create a variable called tp_io_handle of type esp_lcd_panel_io_handle_t and initialize it with NULL.

We create another one called tp_io_config of type esp_lcd_panel_io_i2c_config_t and initialize it with the configuration that gives us the library of our touch. In addition, as it is a structure, we configure the speed of the i2c by accessing the variable scl_speed_hz.

Using esp_lcd_new_panel_io_i2c, we configure everything, passing the i2c bus driver, the configuration and the driver.

Now we are going to configure the touch, creating a new variable called tp_cfg of type esp_lcd_touch_config_t. The same, as it is a structure we are going to configure the internal variables one by one.

To finish this function, using esp_lcd_touch_new_i2c_cst816s, we configure our tp_handle touch handler.

Let's go now with the function that reads where we have touched the screen:

/*Will be called by the library to read the touchpad*/
static void touchpad_read(lv_indev_t *indev_drv, lv_indev_data_t *data)
{
    uint16_t touch_x[1];
    uint16_t touch_y[1];
    uint8_t touch_cnt = 0;

    esp_lcd_touch_read_data(tp_handle);
    bool touchpad_is_pressed = esp_lcd_touch_get_coordinates(
        tp_handle, touch_x, touch_y, NULL, &touch_cnt, 1);

    /*Save the pressed coordinates and the state*/
    if (touchpad_is_pressed)
    {
        data->state = LV_INDEV_STATE_PRESSED;
        data->point.x = touch_x[0];
        data->point.y = touch_y[0];
    }
    else
    {
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

First we create three variables:

  • touch_x[1], which will store the x coordinate.
  • touch_y[1], which will store the y-coordinate y.
  • touch_cnt, which will store how many fingers have touched the screen.

Since we are only going to enable the screen to be touched with one finger, the length of the vectors is 1 and we initialize touch_cnt to 0.

Then using the function esp_lcd_touch_read_data we read the touchpad. This function is provided by the esp_lcd_touch.h library.

Luego mediante esp_lcd_touch_get_coordinates leemos las coordenadas de donde se ha tocado la pantalla. Si se ha tocado, devolverá las coordenadas y sino, devolverá 0.

Then using esp_lcd_touch_get_coordinates we read the coordinates of where the screen has been touched. If it has been touched, it will return the coordinates and if not, it will return 0.

Knowing this condition, the if/else can be done. If it has been touched, the state is modified and updated to LV_INDEV_STATE_PRESSED and by accessing point.x and point.y the x and y coordinates are passed respectively.

If not pressed, the status is updated to LV_INDEV_STATE_RELEASED.

With all this, the lv_port_indev.c file should look like this:

/**
 * @file lv_port_indev.h
 * @brief Input device interface configuration header file
 * @author MQuero
 * @see mquero.com
 */

/**********************
 *      INCLUDES
 *********************/
#include "lv_port_indev.h"
#include "i2c_driver.h"
#include "esp_lcd_touch_cst816s.h"
#include "lvgl.h"

/**********************
 *      DEFINES
 *********************/
#define CONFIG_LCD_H_RES 240
#define CONFIG_LCD_V_RES 320

#define BOARD_TOUCH_IRQ (-1)
#define BOARD_TOUCH_RST (-1)

/**********************
 *      VARIABLES
 **********************/
esp_lcd_touch_handle_t tp_handle = NULL;

/**********************
 *  STATIC VARIABLES
 **********************/
lv_indev_t *indev_touchpad;

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void touchpad_init(void);
static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data);

/**********************
 *   GLOBAL FUNCTIONS
 **********************/
void lv_port_indev_init(void)
{
     /*Initialize touchpad*/
     touchpad_init();

     /*Register a touchpad input device*/
     indev_touchpad = lv_indev_create();
     lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
     lv_indev_set_read_cb(indev_touchpad, touchpad_read);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/
/*Initialize your touchpad*/
static void touchpad_init(void)
{
    esp_lcd_panel_io_handle_t tp_io_handle = NULL;
    esp_lcd_panel_io_i2c_config_t tp_io_config =
        ESP_LCD_TOUCH_IO_I2C_CST816S_CONFIG();
    tp_io_config.scl_speed_hz = (400 * 1000);
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &tp_io_config,
                                             &tp_io_handle));

    esp_lcd_touch_config_t tp_cfg = {
        .x_max = CONFIG_LCD_H_RES,
        .y_max = CONFIG_LCD_V_RES,
        .rst_gpio_num = BOARD_TOUCH_RST,
        .int_gpio_num = BOARD_TOUCH_IRQ,
        .levels =
            {
                .reset = 0,
                .interrupt = 0,
            },
        .flags =
            {
                .swap_xy = 0,
                .mirror_x = 0,
                .mirror_y = 0,
            },
        .interrupt_callback = NULL,
        .process_coordinates = NULL,
    };
    esp_lcd_touch_new_i2c_cst816s(tp_io_handle, &tp_cfg, &tp_handle);
}

/*Will be called by the library to read the touchpad*/
static void touchpad_read(lv_indev_t *indev_drv, lv_indev_data_t *data)
{
    uint16_t touch_x[1];
    uint16_t touch_y[1];
    uint8_t touch_cnt = 0;

    esp_lcd_touch_read_data(tp_handle);
    bool touchpad_is_pressed = esp_lcd_touch_get_coordinates(
        tp_handle, touch_x, touch_y, NULL, &touch_cnt, 1);

    /*Save the pressed coordinates and the state*/
    if (touchpad_is_pressed)
    {
        data->state = LV_INDEV_STATE_PRESSED;
        data->point.x = touch_x[0];
        data->point.y = touch_y[0];
    }
    else
    {
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

Congratulations we're almost there! Let's configure the main.c to show a demo of LVGL on the screen🚀

main.c Configuration

First of all, let's open the SDK Configuration Editor and select the “Music player demo” checkbox.

music-demo-check

Let's start by writing the main.c with the following:

/**
 * @file main.c
 * @brief Main file for the project
 * @author MQuero
 * @see mquero.com
 */

/**********************
 *      INCLUDES
 *********************/
#include "i2c_driver.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
#include "lvgl.h"
#include "esp_lcd_panel_ops.h"
#include "esp_timer.h"
#include "lv_demos.h"

/**********************
 *      VARIABLES
 **********************/
SemaphoreHandle_t xGuiSemaphore;
TaskHandle_t gui_task_handle;

/*********************
 * STATIC PROTOTYPES
 * *******************/
static uint32_t lv_tick_task(void);
static void task_gui(void *arg);

Como hemos hecho antes, primero escribimos la información básica del archivo y luego en INCLUDES añadimos los archivos de cabecera:

As we have done before, we first write the basic file information and then in INCLUDES we add the header files:

  • i2c_driver.h to be able to start the i2c bus.
  • lv_port_disp.h, to start the display.
  • lv_port_indev.h, to start the touch.
  • lvgl.h, to use the LVGL library.
  • esp_lcd_panel_ops.h, to be able to turn on the display.
  • esp_timer.h to control the LVGL timing.
  • lv_demos.h to use a demo.

Then we are going to write two variables:

  1. xGuiSemaphore of type SemaphoreHandle_t. We are going to use it to control the access to the resources of the task that is going to control the graphical interface.
  2. gui_task_handle of type TaskHandle_t. It is going to be the task where we are going to start the graphical interface and LVGL.

In the STATIC PROTOTYPES section we are going to declare two prototypes, lv_tick_task y task_gui. When we define them later I will explain you what they are for.

/*********************
 * MAIN FUNCTION
 * *******************/
void app_main(void)
{
    xGuiSemaphore = xSemaphoreCreateMutex();

    i2c_init();

    xTaskCreatePinnedToCore(task_gui, "gui", (1024 * 18), NULL,
                            5, &gui_task_handle, 1);

    while (1)
    {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

In the app_main we are going to do three quick things:

  1. We create a Mutex type semaphore to protect access to LVGL functions (when we use it later you are going to see what it is for).
  2. We start the i2c bus with i2c_init.
  3. We create the task “gui” and link it to the task_gui function we declared earlier. Also, we give it a data stack of 18 KB, a priority of 5 and tell it to run on ESP32-S3 core 1. Also, we store the task identifier in gui_task_handle.
  4. We create an infinite loop with a timeout of 1000 milliseconds. We do this so that the application never terminates.

Let's go to the last thing 🚀

/**********************
 *   STATIC FUNCTIONS
 **********************/
static uint32_t lv_tick_task(void)
{
    return (((uint32_t)esp_timer_get_time()) / 1000);
}

static void task_gui(void *arg)
{
    lv_init();
    lv_tick_set_cb((lv_tick_get_cb_t)lv_tick_task);
    lv_port_disp_init();
    lv_port_indev_init();

    lv_demo_music();

    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));

    uint32_t time_till_next = 0;
    while (1)
    {
        if (pdTRUE == xSemaphoreTake(xGuiSemaphore, 50 / portTICK_PERIOD_MS))
        { /*Take semaphore to update slider and arc values*/
            time_till_next = lv_timer_handler();
            xSemaphoreGive(xGuiSemaphore);
        }
        vTaskDelay(time_till_next / portTICK_PERIOD_MS);
    }
}

The lv_tick_task(void) function will be used to return the current time in microseconds since the system was started. We need it because LVGL needs to keep track of time.

Since LVGL needs the time to be expressed in milliseconds, we divide the value returned by esp_timer_get_time() by 1000.

The function task_gui(void *arg) is going to take care of continuously managing the GUI.

First, with lv_init() we initialize the LVGL library. It has to be called before using any other LVGL function.

Then we set the lv_tick_task function, which we created earlier, to be used by LVGL to measure time.

With lv_port_disp_init() and lv_port_indev_init() we initialize both the screen and its touch.

We start the demo with lv_demo_music() and turn on the screen with esp_lcd_panel_disp_disp_on_off.

Now it only remains to make the loop that is going to be in charge of continuously updating the graphical interface. For that, inside the while loop, we create a conditional.

If the semaphore is free and we manage to catch it, we enter the conditional. Inside this we call lv_timer_handler(), which is going to handle the LVGL periodic tasks. Also, this function returns the time in milliseconds until the next time it should be executed.

Finally, we return the semaphore with xSemaphoreGive and wait for the time returned by lv_timer_handler.

Test Final

We have already finished writing all the code!

If all goes well, when you compile and upload the code to ESP32-S3, you should start seeing the default LVGL music demo.

music-demo

It has been a long tutorial, but from now on you will have everything ready to implement LVGL in your projects🙌.

Recuerda que puedes seguirme en mis redes sociales buscando @mquerostudio y en mi canal de YouTube @MQuero.

Remember that you can follow me on my social networks by searching @mquerostudio and on my YouTube channel @MQuero.

If you have found any typo in the article or you think you can make some improvement send me a message through any of my networks and we'll discuss it 🤝

:)

location

based in Spain

MQ. Logo