Thư viện LVGL: UI đẹp như smartphone trên TFT ESP32
LVGL (Light and Versatile Graphics Library) là engine UI hàng đầu cho embedded — render giao diện đẹp như mobile app trên TFT ESP32. Có button, slider, chart, list scroll mượt, theme đen/sáng, anti-aliasing. Bài này hướng dẫn cài, setup, và tạo dashboard đầu tiên.
1. LVGL là gì?
LVGL là graphics framework open-source (MIT) viết bằng C — chạy trên bất kỳ MCU nào có 64KB+ RAM. ESP32 (320KB RAM) dư sức. Tính năng:
- 30+ widget có sẵn: button, slider, switch, chart, gauge, calendar, keyboard.
- Anti-aliased rendering — chữ và border mượt.
- Animation engine — fade, slide, zoom.
- Theme đen/sáng, customize màu.
- Touch input handling.
- Layout flex/grid như CSS.
2. So với Adafruit_GFX và TFT_eSPI
| Tiêu chí | GFX/eSPI | LVGL |
|---|---|---|
| Mức độ | Vẽ pixel/shape | Widget UI cấp cao |
| Setup | 5 phút | 1-2 giờ |
| RAM | ~5KB | ~40KB+ |
| Đẹp | Basic | Smartphone-level |
| Touch handling | Tự code | Built-in |
| Animation | Tự code | Built-in |
Quy tắc: project hiển thị đơn giản (3 chỉ số sensor) → eSPI. Project UI phức tạp (menu, settings, dashboard nhiều page) → LVGL.
3. Cài đặt
Library Manager → "lvgl" by kisvegabor. Cài cùng TFT_eSPI (driver TFT cho LVGL).
Setup phức tạp hơn so với thư viện khác — phải sửa nhiều file. Khuyến nghị: dùng PlatformIO để dễ quản lý dependency và config.
4. Cấu hình TFT_eSPI
Vào thư viện TFT_eSPI/User_Setup.h, sửa:
#define ILI9341_DRIVER
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4
#define LOAD_GLCD
#define SMOOTH_FONT
#define SPI_FREQUENCY 40000000
5. Cấu hình LVGL — lv_conf.h
Copy lv_conf_template.h trong thư mục library → lv_conf.h đặt cạnh thư mục lvgl. Enable:
#define LV_CONF_INCLUDE_SIMPLE
#define LV_COLOR_DEPTH 16
#define LV_TICK_CUSTOM 1
#define LV_TICK_CUSTOM_INCLUDE "Arduino.h"
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis())
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_24 1
#define LV_USE_PERF_MONITOR 1
6. Setup cơ bản — hello world
#include
#include
TFT_eSPI tft;
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[320 * 10]; // buffer 10 dòng
void my_disp_flush(lv_disp_drv_t* disp, 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);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors((uint16_t*)&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(1);
lv_init();
lv_disp_draw_buf_init(&draw_buf, buf, NULL, 320 * 10);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
// Tao label
lv_obj_t* label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello LVGL");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
void loop() {
lv_timer_handler();
delay(5);
}
7. Widget — button có callback
static void btn_event_cb(lv_event_t* e) {
Serial.println("Clicked!");
}
lv_obj_t* btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t* btn_label = lv_label_create(btn);
lv_label_set_text(btn_label, "Bam vao");
lv_obj_center(btn_label);
8. Slider + chart real-time
// Slider
lv_obj_t* slider = lv_slider_create(lv_scr_act());
lv_obj_set_width(slider, 200);
lv_obj_align(slider, LV_ALIGN_TOP_MID, 0, 30);
lv_slider_set_range(slider, 0, 100);
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
// Chart
lv_obj_t* chart = lv_chart_create(lv_scr_act());
lv_obj_set_size(chart, 300, 150);
lv_obj_align(chart, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_chart_set_type(chart, LV_CHART_TYPE_LINE);
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
lv_chart_series_t* ser = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_BLUE), LV_CHART_AXIS_PRIMARY_Y);
// Update mỗi giây
for (uint16_t i = 0; i < 10; i++) {
lv_chart_set_next_value(chart, ser, random(0, 100));
delay(500);
lv_timer_handler();
}
9. Theme và styling
// Theme tối
lv_theme_t* theme = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_BLUE),
lv_palette_main(LV_PALETTE_PINK), true /*dark*/, LV_FONT_DEFAULT);
lv_disp_set_theme(NULL, theme);
// Style custom
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0xff5722));
lv_style_set_radius(&style_btn, 20);
lv_obj_add_style(btn, &style_btn, 0);
10. Layout flex/grid
// Flex container
lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont, 300, 200);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Thêm 6 button - tự xếp ngang theo wrap
for (int i = 0; i < 6; i++) {
lv_obj_t* b = lv_btn_create(cont);
lv_obj_set_size(b, 60, 60);
}
Cú pháp giống CSS flex — không phải tính toạ độ thủ công.
11. Hiệu năng
- ESP32 + ILI9341 SPI 40MHz: ~30 FPS với LVGL.
- ESP32-S3 + ILI9488 RGB parallel: 60 FPS.
- Buffer lớn (160 dòng) → ít flush, smooth hơn nhưng tốn RAM.
- Enable PSRAM (ESP32-WROVER) cho UI phức tạp.
12. Ứng dụng
- Smart home control panel.
- Đồng hồ digital UI đẹp.
- Weather station với chart history.
- Settings UI cho thiết bị IoT.
- POS terminal mini.
- Game đơn giản (Tetris, Snake).
Liên quan
Đọc TFT_eSPI: thư viện TFT tốc độ cao để hiểu driver bên dưới. Áp dụng vào project Smart mirror Raspberry Pi hoặc smart home controller riêng.