LVGL(Light and Versatile Graphics Library)是一款开源的嵌入式图形库,广泛应用于微控制器和资源受限的嵌入式设备上。在嵌入式GUI开发中,一个常见的痛点是:每次修改UI都需要烧录固件到硬件上验证,调试效率极低。LVGL的PC模拟器完美解决了这个问题——你可以在PC上编写和调试UI代码,确认无误后再无缝移植到嵌入式硬件上。
本文将从PC模拟开发和嵌入式移植两个维度,系统性地讲解LVGL的开发流程,包含完整的代码示例和配置细节。
在PC上运行LVGL模拟器具有以下核心优势:
LVGL官方推荐在开始嵌入式移植之前,先在PC模拟器上完成UI开发和验证。
PC模拟器依赖SDL2(Simple DirectMedia Layer)作为图形后端,负责窗口创建、鼠标键盘事件处理和渲染输出。
各平台安装命令:
# Ubuntu/Debian
sudo apt-get update && sudo apt-get install -y build-essential libsdl2-dev cmake
# Arch Linux
sudo pacman -Syu && sudo pacman -S sdl2 base-devel cmake
# macOS (Homebrew)
brew install sdl2 cmake make
# Fedora
sudo dnf install @development-tools cmake SDL2-develLVGL官方为多种IDE提供了开箱即用的模拟器项目:
IDE | 推荐场景 | 仓库地址 |
|---|---|---|
VSCode | Linux/macOS(SDL) | lv_port_pc_vscode |
Visual Studio | Windows | lv_port_pc_visual_studio |
Eclipse | Linux/macOS(SDL) | lv_sim_eclipse_sdl |
Generic Linux | fbdev/DRM/SDL切换 | lv_port_linux |
以VSCode为例,克隆项目:
git clone --recursive https://github.com/lvgl/lv_port_pc_vscode.git--recursive参数会同时拉取LVGL核心源码作为子模块。
克隆完成后,项目结构如下:
lv_port_pc_vscode/
├── lvgl/ # LVGL核心源码(子模块)
├── lv_conf.h # LVGL配置文件
├── main.c # 主程序入口
├── simulator.code-workspace # VSCode工作区文件
└── CMakeLists.txt # CMake构建配置关键配置文件:lv_conf.h
将lvgl/lv_conf_template.h复制到项目根目录并重命名为lv_conf.h,将开头的#if 0改为#if 1以启用配置。核心配置项包括:
/* 颜色深度:根据目标显示屏设置 */
#define LV_COLOR_DEPTH 16 /* 可选: 1, 8, 16, 24, 32 */
/* 内存分配方式:0使用LVGL内置分配器,1使用标准C库 */
#define LV_MEM_CUSTOM 0
/* 动态内存大小(仅当LV_MEM_CUSTOM=0时生效) */
#define LV_MEM_SIZE (48U * 1024U) /* 至少2KB */
/* 启用SDL后端 */
#define LV_USE_SDL 1在VSCode中:
simulator.code-workspace打开工作区Debug LVGL demo with gdb模拟器窗口将显示LVGL的demo界面,你可以在main.c中编写自己的UI代码。
为了更真实地模拟嵌入式环境,该项目还集成了FreeRTOS。在lv_conf.h中配置:
#define LV_USE_OS LV_OS_FREERTOS注意:FreeRTOS需要足够的堆内存,建议配置为512MB:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 512 * 1024 * 1024 ) )LVGL支持多种显示后端,你可以在lv_conf.h中轻松切换:
/* 使用Linux Framebuffer替代SDL */
#define LV_USE_LINUX_FBDEV 1然后在代码中创建对应的display:
lv_display_t *display = lv_linux_fbdev_create();
lv_linux_fbdev_set_file(display, "/dev/fb0");将LVGL移植到嵌入式平台通常需要完成以下步骤:
lv_conf.h 适配目标硬件flush_cb回调)lv_tick_inc)指定版本克隆(避免使用最新版导致的不兼容):
git clone -b v8.3.10 --depth=1 https://github.com/lvgl/lvgl.git将lvgl文件夹复制到你的工程目录下,并将lv_conf_template.h复制为lv_conf.h,放置在与lvgl文件夹同级的目录中。
显示驱动是移植中最关键的部分。LVGL通过flush_cb回调函数将渲染好的像素数据发送到显示屏。
步骤一:创建Display对象
#include "lvgl.h"
/* 定义显示屏分辨率 */
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
/* 帧缓冲区(根据颜色深度计算大小) */
#define BYTES_PER_PIXEL (LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565))
#define BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT * BYTES_PER_PIXEL)
static uint8_t buf_1[BUFFER_SIZE];
static uint8_t buf_2[BUFFER_SIZE]; /* 双缓冲,可选 */
void lvgl_init(void)
{
/* 1. 初始化LVGL核心 */
lv_init();
/* 2. 创建Display */
lv_display_t *disp = lv_display_create(DISPLAY_WIDTH, DISPLAY_HEIGHT);
/* 3. 注册flush回调 */
lv_display_set_flush_cb(disp, my_flush_cb);
/* 4. 设置帧缓冲区(双缓冲) */
lv_display_set_buffers(disp, buf_1, buf_2, BUFFER_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
}步骤二:实现flush_cb回调
void my_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
/* area->x1, area->y1: 左上角坐标 */
/* area->x2, area->y2: 右下角坐标 */
/* px_map: 渲染好的像素数据指针 */
int32_t width = area->x2 - area->x1 + 1;
int32_t height = area->y2 - area->y1 + 1;
/* 将px_map中的数据通过LCD驱动发送到显示屏 */
lcd_draw_area(area->x1, area->y1, width, height, px_map);
/* 重要:通知LVGL刷新完成 */
lv_display_flush_ready(disp);
}步骤一:创建输入设备
void lvgl_touch_init(void)
{
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); /* 触摸屏类型 */
lv_indev_set_read_cb(indev, touch_read_cb);
}步骤二:实现read回调
void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
{
int16_t x, y;
uint8_t pressed;
/* 从触摸驱动读取坐标和按压状态 */
touch_driver_read(&x, &y, &pressed);
if (pressed) {
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}LVGL需要知道时间流逝才能处理动画和超时。你需要定期调用lv_tick_inc():
/* 在定时器中断或RTOS任务中调用 */
void timer_interrupt_handler(void)
{
lv_tick_inc(1); /* 假设定时器周期为1ms */
}在main()函数中完成初始化后,进入主循环:
int main(void)
{
/* 初始化硬件外设(LCD、触摸、时钟等) */
hardware_init();
/* 初始化LVGL */
lvgl_init();
/* 创建UI */
create_ui();
while (1) {
lv_timer_handler(); /* 处理LVGL任务 */
delay_ms(5); /* 建议5ms间隔 */
}
}STM32平台:将LVGL文件夹复制到[project_folder]/Drivers/lvgl,在STM32CubeIDE中通过Project → Properties → C/C++ Build → Settings → Include paths添加LVGL目录。建议使用DMA2D硬件加速提升性能。
ESP32平台:最简单的方式是通过esp_lvgl_port组件集成:
idf.py add-dependency "espressif/esp_lvgl_port^2.3.0"Linux Framebuffer平台:使用官方lv_port_linux项目,通过Kconfig菜单选择fbdev、DRM或SDL后端。
如果LVGL运行异常,首先检查lv_conf.h中的LV_MEM_SIZE是否足够。对于复杂UI,建议至少配置为64KB以上。
LV_COLOR_DEPTH是否与显示屏匹配(RGB565对应16位)flush_cb中发送数据的格式(颜色字节顺序)是否正确在启用FreeRTOS的模拟器项目中,如果SDL窗口不出现,通常是因为堆内存不足。将configTOTAL_HEAP_SIZE调整为512MB可解决。
lv_conf.h中禁用不需要的模块(如未使用的widget、字体、图片格式等)LV_DISPLAY_RENDER_MODE_PARTIAL渲染面积LVGL的模拟开发与嵌入式移植构成了完整的开发闭环:在PC模拟器上快速验证UI逻辑和交互效果,然后将经过验证的代码无缝迁移到嵌入式硬件上。这种工作流大幅提升了嵌入式GUI的开发效率。
本文涵盖了从环境搭建、模拟运行到实际移植的完整技术路径,包含lv_conf.h配置、flush_cb显示驱动实现、输入设备注册等核心代码。掌握这些基础,你就能够在任何嵌入式平台上高效地使用LVGL构建图形界面。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。