Tổng Quan Về LVGL Trên Nền Tảng Vi Điều Khiển
Khi làm việc với các hệ thống nhúng có khả năng xử lý hạn chế, việc tích hợp giao diện người dùng (GUI) đòi hỏi sự cân bằng giữa tính thẩm mỹ và tài nguyên phần cứng. Thư viện LVGL (Light and Versatile Graphics Library) nổi bật nhờ kiến trúc nhẹ, tối ưu hóa bộ nhớ và hiệu suất, đặc biệt phù hợp cho Arduino hoặc các board STM32. Khác với các thư viện đồ họa nặng nề truyền thống, LVGL cung cấp các công cụ để quản lý bộ đệm hiển thị thông minh, hỗ trợ nhiều định dạng màn hình và cơ chế cập nhật hình ảnh không làm treo hệ thống.
Một trong những lợi thế lớn nhất của LVGL là khả năng tùy biến giao diện thông qua theme và style. Nhà phát triển có thể tạo ra các nút bấm, biểu đồ hay bảng dữ liệu phức tạp mà không cần vẽ trực tiếp lên từng pixel. Điều này cực kỳ quan trọng khi cần xây dựng các ứng dụng thực tế như thiết bị giám sát nhiệt độ, đồng hồ đếm ngược hoặc bảng điều khiển công nghiệp, nơi thông tin cần được cập nhật liên tục theo thời gian thực.
Cơ Chế Cập Nhật Nội Dung Qua Bộ Định Thời
Vấn đề phổ biến trong lập trình nhúng là làm sao để thay đổi nội dung hiển thị mà không gây gián đoạn luồng chạy chính (main loop). Giải pháp sử dụng kết hợp giữa nhãn văn bản (label) và hàm gọi lại của bộ định thời (callback) mang lại sự linh hoạt cao. Thay vì kiểm tra trạng thái liên tục trong vòng lặp `loop()`, chúng ta giao việc cập nhật thời gian cho trình duyệt tác vụ (task scheduler) của LVGL.
Kỹ thuật này cho phép giảm tải cho CPU, tránh hiện tượng mất mát khung hình khi xử lý các lệnh dài. Khi bộ định thời kích hoạt, nó sẽ truy xuất đối tượng nhãn hiện tại và gửi lệnh ghi nội dung mới xuống driver màn hình. Phương pháp này đặc biệt hữu ích cho việc hiển thị giá trị cảm biến, trạng thái hệ thống hoặc số lần chạy của một chu trình.
Mẫu Cơ Bản: Tăng Chỉ Số Đếm Theo Chu Kỳ
Đầu tiên, chúng ta xem xét ví dụ đơn giản nhất: hiển thị một con số tăng dần dựa trên khoảng thời gian cố định. Việc này giúp hiểu rõ cách thức khởi tạo bộ nhớ hiển thị và gắn kết bộ định thời vào đối tượng GUI.
#include <Arduino.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
// Biến toàn cục lưu trữ chỉ số
volatile uint32_t tick_index = 0;
// Khởi tạo màn hình và thư viện
TFT_eSPI tft = TFT_eSPI();
lv_obj_t *ui_label_counter;
// Hàm gọi lại cho bộ định thời - thực thi mỗi giây
void refresh_periodic_timer(lv_timer_t *tmr) {
// Gia tăng chỉ số
tick_index++;
// Cập nhật văn bản trên nhãn
char buf[64];
snprintf(buf, sizeof(buf), "Chỉ số: %lu", tick_index);
lv_label_set_text(ui_label_counter, buf);
}
void init_display_driver(void) {
static lv_disp_buf_t disp_buf;
static lv_color_t buffer[SCREEN_WIDTH * 10];
// Phân bổ bộ nhớ đệm
lv_disp_buf_init(&disp_buf, buffer, NULL, SCREEN_WIDTH * 10);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.buffer = &disp_buf;
disp_drv.flush_cb = display_flush_func; // Điểm nối với driver màn hình vật lý
lv_disp_drv_register(&disp_drv);
}
void setup() {
Serial.begin(115200);
tft.begin();
lv_init();
init_display_driver();
lv_obj_t *scr = lv_obj_create(NULL);
lv_scr_load(scr);
// Tạo nhãn ở trung tâm
ui_label_counter = lv_label_create(scr);
lv_label_set_text(ui_label_counter, "Chỉ số: 0");
lv_obj_align(ui_label_counter, LV_ALIGN_CENTER, 0, 0);
// Thiết lập bộ định thời 1000ms (1 giây)
lv_timer_create(refresh_periodic_timer, 1000, NULL);
}
void loop() {
// Xử lý các nhiệm vụ của LVGL bắt buộc phải gọi thường xuyên
lv_timer_handler();
delay(5);
}
void display_flush_func(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
tft.setAddrWindow(area->x1, area->y1, area->x2, area->y2);
size_t len = lv_area_get_size(area);
tft.pushColors((uint16_t *)color_p, len);
lv_disp_flush_ready(disp);
}
Xử Lý Dữ Liệu Cảm Biến Giả Lập
Trong thực tế, dữ liệu đến từ cảm biến thường là số thực (float/double). LVGL hỗ trợ định dạng chuỗi tiện lợi giúp hiển thị dấu thập phân chính xác. Ví dụ sau mô phỏng dữ liệu nhiệt độ dao động ngẫu nhiên.
// Thay đổi tên biến để tránh xung đột
float simulated_temp = 25.5f;
const char *ui_label_id = "temp_display";
lv_obj_t *temp_obj;
void process_sensor_data(lv_timer_t *ctx) {
// Mô phỏng nhiễu nhiệt độ
float noise = (float)(rand() % 200 - 100) / 100.0f;
simulated_temp += noise;
if (simulated_temp > 100.0f) simulated_temp = 25.0f; // Giới hạn trên giả lập
char text_buffer[32];
sprintf(text_buffer, "Nhiệt: %.1f °C", simulated_temp);
lv_label_set_text(temp_obj, text_buffer);
}
Kết Hợp Điều Khiển Phần Cứng (GPIO)
Giao diện không chỉ để xem mà còn để tương tác. Chúng ta có thể đồng bộ trạng thái vật lý của LED với nội dung nhãn. Ở đây, mục tiêu là bật/tắt đèn LED và thay đổi văn bản báo trạng thái tương ứng.
#define LED_PIN_DEFAULT 13
bool led_is_on = false;
lv_obj_t *status_txt;
void cycle_system_mode(lv_timer_t *timer) {
// Đảo trạng thái chân GPIO
led_is_on = !led_is_on;
digitalWrite(LED_PIN_DEFAULT, led_is_on ? HIGH : LOW);
// In trạng thái lên màn hình
const char *mode_text = led_is_on ? "Trạng thái: MỞ" : "Trạng thái: ĐÓNG";
lv_label_set_text(status_txt, mode_text);
}
Sử Dụng Tham Số Riêng Cho Bộ Định Thời
Tốt hơn là lưu trữ tham chiếu nhãn bên ngoài, chúng ta có thể chuyển địa chỉ đối tượng nhãn trực tiếp vào hàm tạo bộ định thời thông qua thuộc tính `user_data`. Cách này giúp hàm callback độc lập và dễ bảo trì.
// Hàm xử lý nhận đối tượng nhãn qua tham số context
void smart_update_callback(lv_timer_t *timer) {
// Lấy con trỏ nhãn đã lưu trước đó
lv_obj_t *target_label = (lv_obj_t *)timer->user_data;
// Tăng biến đếm
int current_val = 0; // Giả sử lấy từ context hoặc biến tĩnh
if(timer->user_data == target_label) {
// Logic cập nhật nếu cần
lv_label_set_text(target_label, "Hệ thống đang chạy");
}
}
Bổ Sung Tương Tác Từ Nút Ấn
Các tính năng tự động cần được kiểm soát. Bằng cách thêm sự kiện nhấn nút (event listener), chúng ta có thể tạm dừng hoặc bắt đầu việc cập nhật nhãn mà không cần tắt hoàn toàn bộ định thời. Điều này rất tốt cho trải nghiệm người dùng.
volatile bool system_running = true;
lv_obj_t *run_btn;
// Sự kiện xử lý khi chạm nút
void handle_button_click(lv_event_t *e) {
system_running = !system_running;
if(system_running) {
lv_label_set_text(run_btn, "Dừng");
} else {
lv_label_set_text(run_btn, "Tiếp tục");
}
}
void timer_logic(lv_timer_t *t) {
if(system_running) {
static uint32_t ticks = 0;
ticks++;
char msg[20];
sprintf(msg, "Lần: %d", ticks);
// Giả sử nhãn đã được định nghĩa sẵn ở biến toàn cục 'disp_label'
lv_label_set_text(disp_label, msg);
}
}
// Trong hàm setup():
run_btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(run_btn, handle_button_click, LV_EVENT_CLICKED, NULL);
lv_label_set_text(lv_label_create(run_btn), "Bắt đầu");
Quy Tắc Tối Ưu Hóa Tài Nguyên
Việc cập nhật nhãn quá nhanh (ví dụ dưới 10ms) sẽ chiếm dụng CPU không cần thiết. Nên đặt mức trễ phù hợp với tần suất thay đổi dữ liệu thực tế. Ngoài ra, hãy nhớ giải phóng bộ nhớ bất động (deallocate) các đối tượng không dùng nữa để tránh rò rỉ RAM. Đảm bảo rằng các biến toàn cục được khai báo đúng kiểu dữ liệu (static/global) để tránh mất giá trị giữa các lần gọi callback.