在嵌入式GUI开发中,每次修改界面都要烧录到硬件测试,是无数开发者挥之不去的噩梦。调整一个按钮的位置,需要花5分钟编译、烧录、重启设备——这种低效的开发方式严重拖慢了项目进度。虽然市面上有GUI Guider、SquareLine Studio等现成工具,但它们存在版本兼容性、屏幕尺寸限制、代码与实际硬件驱动差异等致命痛点。
LVGL(Light and Versatile Graphics Library) 作为一款免费开源的轻量级嵌入式图形库,提供了完美的解决方案:开发者可以在PC上通过模拟器进行UI开发与调试,待界面效果确认后,再将完全相同的代码移植到硬件平台。本文将深入讲解LVGL模拟开发环境的搭建原理、实战步骤,以及向嵌入式硬件移植的完整流程。
LVGL之所以能实现“一次编写,到处运行”,关键在于其精妙的分层架构。在嵌入式环境中,LVGL通过显示驱动回调函数将像素数据写入LCD显存,这个函数通常对接硬件厂商提供的LCD填充接口。而在PC环境下,我们只需要用SDL2等图形库创建一个虚拟的“显存”区域,让LVGL以为在操作真实硬件即可。
// 嵌入式环境典型实现
void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) {
LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, (uint16_t*)color_p);
lv_disp_flush_ready(drv);
}LVGL能够运行在FreeRTOS、RT-Thread、Zephyr、Linux等多种操作系统之上,也可以直接运行在裸机环境中。开发者只需要为LVGL提供底层的显示驱动和输入驱动接口,便可以快速完成移植。
LVGL官方提供了多种PC模拟器方案:
本文采用 VSCode + CMake + SDL2 方案,因其跨平台性好、工程结构清晰、紧跟LVGL主版本。
需要安装以下工具:
安装完成后,将MinGW和CMake的bin目录添加到系统PATH环境变量中,通过 cmake --version 和 gcc -v 验证安装。
LVGL官方提供了VSCode模拟器模板,需注意选择与LVGL版本匹配的分支:
git clone -b release/v9.3 https://github.com/lvgl/lv_port_pc_vscode.git关键点:不要直接下载默认的main分支,要选择
release/v9.3等稳定版本分支。
在VSCode中打开工程文件夹,安装推荐插件后,通过CMake选择正确的工具链进行编译:
# 首次编译后生成bin和build目录
# 将SDL2.dll(Windows)拷贝到bin目录
# 点击运行即可看到LVGL模拟器窗口SDL2模拟器的本质是创建一个虚拟显示窗口,通过 disp_flush 回调将LVGL渲染的像素数据绘制到SDL窗口上。
#include "lvgl.h"
void lv_example_hello_world(void) {
/* 创建标签控件 */
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
int main(void) {
lv_init(); /* 初始化LVGL */
lv_display_t *disp = lv_display_create(800, 480); /* 创建显示设备 */
lv_example_hello_world(); /* 创建UI */
while(1) {
lv_timer_handler(); /* 处理LVGL任务 */
delay_ms(5);
}
}将 lvgl/lv_conf_template.h 复制到 lvgl 同级目录并重命名为 lv_conf.h,将开头的 #if 0 改为 #if 1 以启用配置。
颜色深度(必须与硬件匹配):
#define LV_COLOR_DEPTH 16 /* 可选: 1, 8, 16, 24, 32 */内存配置:
#define LV_MEM_CUSTOM 1 /* 1:使用自定义malloc/free, 0:使用内置内存管理 */
#if LV_MEM_CUSTOM == 0
#define LV_MEM_SIZE (48U * 1024U) /* 内置内存池大小,至少48KB */
#endif运行demo时需确保
LV_MEM_SIZE至少为38KB,推荐48KB。如果LVGL无法启动或随机崩溃,首先尝试增大LV_MEM_SIZE。
显示缓冲区配置:
#define LV_USE_DRAW_SW 1 /* 启用软件渲染 */
#define LV_DRAW_SW_SHADOW 1 /* 启用阴影效果 */字体配置:
#define LV_TXT_ENC LV_TXT_ENC_UTF8 /* UTF-8编码 */
/* 内置字体: lv_font_montserrat_16, lv_font_montserrat_24等 */LVGL通过事件回调机制处理用户交互:
static void btn_event_cb(lv_event_t *e) {
lv_obj_t *label = (lv_obj_t*)lv_event_get_user_data(e);
static int count = 0;
count++;
lv_label_set_text_fmt(label, "Clicked: %d times", count);
}
void create_button_demo(void) {
lv_obj_t *btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Click Me");
lv_obj_center(label);
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, label);
}static void slider_event_cb(lv_event_t *e) {
lv_obj_t *slider = lv_event_get_target(e);
lv_obj_t *label = (lv_obj_t*)lv_event_get_user_data(e);
int32_t val = lv_slider_get_value(slider);
lv_label_set_text_fmt(label, "%d%%", val);
}
void create_slider_demo(void) {
lv_obj_t *slider = lv_slider_create(lv_screen_active());
lv_obj_set_size(slider, 200, 20);
lv_obj_align(slider, LV_ALIGN_CENTER, 0, -30);
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "50%");
lv_obj_align_to(label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, label);
}LVGL移植的核心是提供三个接口:
步骤一:添加LVGL源码到工程
将整个LVGL文件夹复制到STM32CubeIDE工程的 Drivers/lvgl 目录,在工程设置中添加头文件包含路径。
步骤二:创建帧缓冲区
/* 定义显示尺寸和颜色格式 */
#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320
#define BYTES_PER_PIXEL (LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565))
/* 双缓冲配置 */
#define BUFF_SIZE (DISPLAY_WIDTH * 10 * BYTES_PER_PIXEL)
static uint8_t buf_1[BUFF_SIZE];
static uint8_t buf_2[BUFF_SIZE];步骤三:实现显示刷新回调
static void disp_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
int32_t w = lv_area_get_width(area);
int32_t h = lv_area_get_height(area);
/* 调用LCD驱动,将px_map中的数据写入指定区域 */
LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, (uint16_t*)px_map);
/* 通知LVGL刷新完成 */
lv_display_flush_ready(disp);
}步骤四:初始化LVGL
int main(void) {
HAL_Init();
/* 初始化时钟、LCD、触摸等外设 */
LCD_Init();
Touch_Init();
lv_init(); /* 初始化LVGL核心 */
/* 创建显示设备并注册缓冲区 */
lv_display_t *disp = lv_display_create(DISPLAY_WIDTH, DISPLAY_HEIGHT);
lv_display_set_buffers(disp, buf_1, buf_2, BUFF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(disp, disp_flush_cb);
/* 创建UI */
create_ui();
while(1) {
lv_timer_handler(); /* 处理LVGL任务 */
delay_ms(5);
}
}static void touchpad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
uint16_t x, y;
uint8_t pressed = Touch_GetPosition(&x, &y);
if(pressed) {
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
/* 在初始化中注册触摸设备 */
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touchpad_read_cb);LVGL需要1ms精度的时钟滴答,可以使用硬件定时器或RTOS的时钟节拍:
/* 在定时器中断中调用 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM1) {
lv_tick_inc(1); /* 每1ms增加一次 */
}
}git submodule update --init --recursive 执行失败LV_COLOR_DEPTH 是否与LCD硬件颜色格式匹配,检查颜色字节序(RGB565 vs BGR565)LV_MEM_SIZELV_USE_LOG 1CONFIG_LV_COLOR_16_SWAP 宏disp_flush_cb、touchpad_read_cb 和时钟滴答LV_MEM_SIZE 等配置LVGL模拟开发与硬件移植的精髓在于 "一次编写,到处运行" 。通过在PC上搭建VSCode + CMake + SDL2模拟环境,开发者可以像开发桌面应用一样高效地调试UI,将开发-调试循环从"分钟级"缩短到"秒级"。完成UI设计后,只需实现显示驱动、输入驱动和时钟滴答三个接口,即可将完全相同的代码部署到STM32、ESP32、Linux等各类嵌入式平台上。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。