Welcome to the second part of the this project! In this segment, we’ll focus on the display setup. Here the Part 1 if you missed.
I’ve decided to use the LVGL graphics library combined with ESP-IDF. While I could have managed without it since it’s a relatively simple project, likely with a single screen UI, utilizing LVGL allows us to explore setting up and configuring this powerful library package. We’ll be using the latest version available (v9.1.0) in combination with an ESP32-S3 microcontroller, and the code will be based on ESP-IDF within the Eclipse Espressif-IDE development environment (Link here).
About LVGL
Just to give a little context to it, LVGL (Light and Versatile Graphics Library) is an open-source graphics library providing everything you need to create embedded GUI with easy-to-use graphical elements, animations, and transitions. Everything you need to know about it is available at this Link.
Pros:
- Rich Feature Set: Includes widgets, animations, styles, and themes.
- Lightweight: Optimized for low resource usage, suitable for embedded systems.
- Cross-Platform: Works with various microcontrollers and display controllers.
- Extensible: Allows custom object creation and customization.
- Community Support: Large and active community offering support and contributions.
Cons:
- Complexity: Can be overkill for very simple applications, adding unnecessary complexity.
- Learning Curve: Requires some time to learn and integrate effectively.
- Resource Usage: Although lightweight, it still requires more resources compared to simpler display solutions.
Using LVGL in this project not only enhances the functionality and aesthetics of the display but also provides a valuable learning experience in integrating advanced graphics libraries with embedded systems.
Let’s get hands-on!
Installing LVGL
As mentioned, LVGL can seem a bit complicated at first if you’ve never used it. However, once the initial setup is done, it’s quite manageable. Start by including the library package in your project. You can do this by using the “install new components” function in the Espressif IDE (right-click on the project -> ESP-IDF: Install new components), search for LVGL, and press Install. Alternatively, create a subfolder in the project called components and clone the official repository from GitHub. After this, compile the project and access the configurations directly via sdkconfig. The only change I made was selecting the color depth as RGB565 (16-bit); I left everything else as default.
Initializing the graphics librariy
There are several steps to correctly initialize the LVGL libraries:
- Call the
lv_init()
function. - Define the dimensions to create the structure of our display.
- Create a draw buffer that LVGL will use to store what will be projected on the display.
- Assign the flush function that LVGL will use to communicate with the display.
- LVGL can handle both displays and input devices like a touchscreen. Since my display doesn’t have touch capabilities, I’ll focus on the display setup. If you have a touchscreen, assign the necessary functions to handle touch commands during initialization.
- Create a timer that periodically calls the
lv_tick_inc()
function, which is essential for the libraries to function. This should be called every 1-10 ms. - Call the
lv_timer_handler()
function periodically to refresh the screen when needed. This function should be included in awhile(1)
loop. Using FreeRTOS, I will create a specific task to manage the graphics library, calling this function every 10 ms. - Create a semaphore to handle graphics operations. This is useful to prevent conflicts when operating on graphic structures that could be read or written by the graphics library, ensuring smooth operation.
Since I work with freeRTOs I created a task to manage lvgl. Let’s see some code now with all the steps above:
static void gui_task(void *arg){
lv_init();
lv_display_t *display = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
/*Declare a buffer for 1/10 screen size*/
static lv_color_t buf1[DISP_BUF_SIZE / 10];
/*Declare a buffer for 1/10 screen size*/
static lv_color_t buf2[DISP_BUF_SIZE / 10];
/*Initialize the display buffer.*/
lv_display_set_buffers(display , buf1, buf2, sizeof(buf1),LV_DISPLAY_RENDER_MODE_PARTIAL );
lv_display_set_flush_cb(display , st7789_flush);
/* Create and start a periodic timer interrupt to call lv_tick_inc */
const esp_timer_create_args_t periodic_timer_args = {
.callback = &lv_tick_task,
.name = "periodic_gui"
};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args , &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LV_TICK_PERIOD_MS * 1000));
while(1){
vTaskDelay(10 / portTICK_PERIOD_MS);
if (pdTRUE == xSemaphoreTake(xGuiSemaphore, 50 / portTICK_PERIOD_MS)) {
lv_task_handler();
xSemaphoreGive(xGuiSemaphore);
}
}
}
Initializing the display
LVGL doesn’t directly manage the display driver, so it’s crucial to initialize before the display driver. In my case, the driver is the ST7789_V2. I use slightly modified libraries from GitHub to suit my needs. You can find the references in the st7789.h
file in the project’s GitHub repository (see link at the end of the post).
First, assign the pins in the st7789.h
file and define parameters like resolution, color inversion, and display orientation (I use landscape). Then, call the spi_display_init()
function to initialize the SPI peripheral and st7789_init()
to configure the display.
A critical part is the flush function. The display driver expects color bytes in a different order than LVGL, so I added a procedure in the st7789_send_color()
function to fix it and invert the byte order (see st7789.c on GitHub). I also adjusted the flush function parameters to match LVGL’s requirements. Finally, call the lv_display_flush_ready()
function at the end of the flush function to notify LVGL that the display is updated and ready for further updates.
To summarize, using the libraries:
- Pin definition in st7789.h
- Customize the defines in st7789.h to match your display (resolution, orientation, colors)
- call spi_display_init() and st7789_init()
- set the flush function st7789_flush() in lvgl (done in gui_task code above)
That’s it, let’s see the code in app_main:
void app_main(void )
{
xGuiSemaphore = xSemaphoreCreateMutex();
/* Init spi and display driver */
spi_display_init();
st7789_init();
/* Create the task for UI */
xTaskCreatePinnedToCore(gui_task, "gui", 18*1024, NULL, 5, &gui_task_Handle,1 );
while(1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
I’ve also created a GitHub repository with sample firmware to correctly configure LVGL 9.1.0 with ESP-IDF. It includes sample graphics and librariy for the ST7789. The firmware example shown is the screenshot you see as the featured image of this article. Check it out here.
Creating Essential UI Elements for the Thermal Camera
Let’s try adding the graphic elements needed for the thermal camera. To make it very minimal I’ve identified the following:
- Battery indicator at the top right: I did it using a bar element (usign the function lv_bar_create()).
- Labels for displaying temperatures at the bottom: there are 3 labels, one for each temperature indication. Use lv_label_create() to create labels.
- Central window to display the thermal camera frame: this is an image onject creted with lv_img_create() and a png with the thermal frame.
But all of this is for the next part!
Part 3 preview
In the third part of the project, we’ll continue with firmware development for our thermal camera. Now we have lvgl running properly on our hardware, we need to integrate thermal sensor reading, color conversion, and thermal frame visualization.
Thank you for reading and supporting my blog! If you enjoyed this content and would like to help me continue creating more, please consider leaving a donation. Your generosity will help me cover the costs of materials and tools for future projects. Any amount is greatly appreciated! And remember to follow me on Instagram for more updates and behind-the-scenes content.