LVGL (Light and Versatile Graphics Library) is the most widely used open-source embedded UI framework, and the ESP32-S3-Touch-LCD-4.3’s 800x480 display makes it a natural fit. The 8 MB PSRAM provides sufficient memory for double-buffered rendering at full resolution, and the GT911 touch controller integrates cleanly with LVGL’s input device abstraction. Whether you are building a simple status dashboard or a full industrial HMI panel, LVGL handles widget layout, event routing, animations, and theme management so that application code can focus on domain logic rather than pixel manipulation.
Benchmark results on this board establish a solid baseline for what the hardware can deliver under real rendering workloads.
Metric
Result
LVGL benchmark demo
~26 FPS average (ESP-IDF v5.3, single-core)
Interface frame rate
41 FPS at PCLK 21 MHz
Anti-tearing (direct mode, double buffer)
Smooth rendering, no visible artifacts
These numbers reflect a board running at 240 MHz with PSRAM at 80 MHz. Pushing PSRAM to the experimental 120 MHz clock yields noticeable improvements, particularly for full-screen transitions and animations that touch large framebuffer regions.
Several lv_conf.h settings have outsized impact on rendering throughput. The defaults assume a microcontroller with limited RAM and no standard library, neither of which applies here.
// Use stdlib malloc instead of LVGL's internal allocator
// PSRAM-backed heap is larger and fragmentation-resistant
#defineLV_MEM_CUSTOM1
// Use standard memcpy/memset -- the ESP32-S3 toolchain
// provides optimized implementations
#defineLV_MEMCPY_MEMSET_STD1
// Place critical rendering functions in IRAM for
// instruction cache bypass
#defineLV_ATTRIBUTE_FAST_MEM IRAM_ATTR
CONFIG_LV_MEM_CUSTOM=y
CONFIG_LV_MEMCPY_MEMSET_STD=y
CONFIG_LV_ATTRIBUTE_FAST_MEM=y
The IRAM_ATTR placement is particularly important for the blend and draw functions that LVGL calls thousands of times per frame. Moving these out of the flash-backed instruction cache eliminates stalls when cache lines are evicted during large renders.
The Arduino approach uses the ESP32_Display_Panel library, which handles all the low-level LCD initialization, touch controller registration, and IO expander setup. Application code only needs to create LVGL widgets.
#include<Arduino.h>
#include<esp_display_panel.hpp>
#include<lvgl.h>
#include"lvgl_v8_port.h"
#include<demos/lv_demos.h>
usingnamespace esp_panel::drivers;
usingnamespace esp_panel::board;
voidsetup() {
Serial.begin(115200);
// Initialize the board (LCD + touch + IO expander)
Board *board =newBoard();
board->init();
assert(board->begin());
// Initialize LVGL port -- binds display driver and touch input
In ESP-IDF, initialization involves more explicit setup through the Waveshare RGB LCD port library. This library creates the RGB panel driver, configures the GT911 touch controller, initializes the CH422G IO expander, and registers the LVGL display and input device drivers.
#include"waveshare_rgb_lcd_port.h"
voidapp_main(void) {
// Initialize display, touch, and LVGL drivers
waveshare_esp32_s3_rgb_lcd_init();
// Lock LVGL before any widget operations
if (lvgl_port_lock(-1)) {
lv_demo_widgets();
lvgl_port_unlock();
}
}
The waveshare_rgb_lcd_port abstraction mirrors what ESP32_Display_Panel does on the Arduino side, but exposes the underlying ESP-IDF component APIs for projects that need finer control over DMA channels, interrupt priorities, or task pinning.
The display supports several rendering strategies, each trading memory consumption for visual quality. The choice depends on your application’s animation complexity and available PSRAM budget.
Normal Mode
Single buffer in PSRAM with partial updates. LVGL only redraws dirty regions and flushes them to the LCD driver. This is the simplest configuration and uses the least memory, but fast-moving animations may show tearing where the LCD scanline overtakes the buffer write position.
Full Refresh with Double Buffer
Two complete frame buffers in PSRAM. LVGL renders to the back buffer while the LCD reads from the front buffer, then the buffers swap on VSYNC. Eliminates tearing entirely but consumes approximately 1.5 MB of PSRAM (800 x 480 x 2 bytes x 2 buffers).
Direct Mode
LVGL writes directly into the frame buffer, updating only dirty regions. A VSYNC callback swaps the active buffer pointer so the LCD always reads a complete frame. This provides the best balance of performance and memory usage, and is the recommended mode for most applications on this board.
The demo code uses the LVGL_PORT_AVOID_TEARING_MODE define to select between these modes. Set it to 0 for normal mode, 1 for full refresh, or 2 for direct mode.
The LVGL port supports 0, 90, 180, and 270 degree rotation through the LVGL_PORT_ROTATION_DEGREE define. Because the RGB LCD peripheral always outputs in landscape orientation (800 wide by 480 tall), rotation is handled in software rather than through a hardware register. The port allocates a rotation buffer and copies dirty areas from LVGL’s internal buffer to the frame buffer with coordinate transformation applied.
Touch coordinates are automatically remapped to match the display rotation. The GT911 touch driver reports in the native landscape coordinate space, and the LVGL input device read callback applies the same rotation transform so that touch input aligns with the visual layout regardless of orientation.
The 800x480 resolution at 4.3 inches yields a pixel density of approximately 217 PPI, which is high enough that anti-aliased fonts look crisp but low enough that single-pixel lines remain visible. A few practical considerations help get the most from the display.
LVGL’s SquareLine Studio provides visual UI design with drag-and-drop widget placement, and it exports C code compatible with this board. For teams that separate UI design from firmware development, this workflow allows designers to iterate on layouts without touching the build system.
For custom fonts, use LVGL’s online font converter to generate C arrays. The Montserrat font family ships with LVGL in several sizes, but application-specific typefaces or icon fonts need to be converted. Include only the Unicode ranges your application actually uses to keep the font data compact.
Keep widget hierarchies flat to reduce draw overhead. Each level of nesting adds clipping and coordinate transformation calculations during rendering. A screen with dozens of nested containers will render measurably slower than one with the same visual result achieved through fewer, larger objects with explicit positioning.
Use lv_obj_invalidate() sparingly. Invalidating the entire screen forces LVGL to redraw every visible widget, which defeats the dirty-region optimization that makes partial updates efficient. Prefer invalidating specific objects that have actually changed, and let LVGL’s internal change tracking handle the rest.