2. 开发环境搭建:基于模拟器(SDL/PC模拟器)的LVGL环境搭建、工程结构解析
说实话,很多初学者一上来就买开发板、接屏幕、焊排针,折腾半天连个像素点都没点亮。我个人习惯是——先用模拟器把逻辑跑通,再往硬件上移植。这样调试快,心态也不容易崩。
这一章,我们就来搭一套基于SDL的PC模拟器环境。你不需要任何硬件,一台电脑就够了。
2.1 为什么选SDL模拟器?
LVGL官方支持多种模拟器后端:SDL、X11、FrameBuffer、Windows GDI等。我推荐SDL,原因有三:
- 跨平台:Windows、macOS、Linux都能跑,一套代码到处编译
- 生态成熟:SDL2是游戏开发常用的库,文档多、坑少
- 调试方便:可以在PC上打断点、看变量、测性能,比在嵌入式板子上舒服太多
2.2 环境准备:你需要什么?
嗯,这里列个清单,你照着准备就行:
| 工具/库 | 版本要求 | 说明 |
|---|---|---|
| CMake | ≥ 3.10 | 构建系统,跨平台编译必备 |
| SDL2 | ≥ 2.0.10 | 模拟器后端,负责窗口和输入 |
| GCC/Clang/MSVC | 支持C99+ | 编译器,看你系统选 |
| Git | 任意 | 拉取LVGL源码 |
2.3 搭建步骤:从零到跑起来
我们一步步来。别急,每一步我都会说明为什么这么做。
2.3.1 拉取LVGL源码
打开终端,执行:
git clone https://github.com/lvgl/lvgl.git
cd lvgl
git checkout v8.3.0 # 建议用稳定版
为什么用v8.3.0?因为v9.x改动较大,很多第三方库还没跟上。我们做课程,稳定第一。
2.3.2 拉取SDL模拟器工程
LVGL官方提供了一个SDL模拟器示例,在 lvgl/examples/porting 目录下。但更方便的是直接用 lv_port_pc_eclipse 这个仓库:
git clone https://github.com/lvgl/lv_port_pc_eclipse.git
cd lv_port_pc_eclipse
这个工程已经配好了CMakeLists.txt,你只需要把lvgl源码链接进来。
2.3.3 配置CMake
打开 CMakeLists.txt,你会看到类似这样的结构:
cmake_minimum_required(VERSION 3.10)
project(lvgl_simulator)
set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lvgl)
add_subdirectory(${LVGL_DIR} lvgl)
add_executable(main main.c)
target_link_libraries(main lvgl SDL2)
这里要注意:LVGL_DIR 必须指向你刚才克隆的lvgl目录。我习惯用相对路径,这样换电脑也能编译。
2.3.4 编译并运行
在工程根目录下:
mkdir build && cd build
cmake ..
make -j4
./main
如果一切顺利,你会看到一个灰色窗口,中间有个LVGL的logo。恭喜,环境搭好了!
sudo apt install libsdl2-dev,macOS用 brew install sdl2,Windows去官网下载开发包。
2.4 工程结构解析:每个文件是干什么的?
你想想看,一个LVGL模拟器工程,说白了就三层:应用层、LVGL核心层、平台适配层。我们来看具体文件:
我们重点看几个关键文件:
2.4.1 main.c —— 程序的入口
这个文件里做了三件事:
- 初始化LVGL库:
lv_init() - 初始化显示驱动和输入驱动:调用
lv_port_disp_init()和lv_port_indev_init() - 进入主循环:不断调用
lv_timer_handler()和SDL_Delay()
核心代码大概长这样:
int main(void) {
lv_init();
lv_port_disp_init(); // 初始化显示
lv_port_indev_init(); // 初始化输入(鼠标/键盘)
// 创建你的第一个控件
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 50);
lv_obj_center(btn);
while(1) {
lv_timer_handler();
SDL_Delay(5); // 控制帧率
}
return 0;
}
ui.c 和 ui.h 中,main.c只负责初始化和主循环。这样后期移植到硬件时,main.c几乎不用改。
2.4.2 lv_port_disp.c —— 显示适配
这个文件负责把LVGL的帧缓冲(framebuffer)显示到SDL窗口上。关键函数是 disp_flush():
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
// 把color_p指向的像素数据复制到SDL纹理
SDL_UpdateTexture(sdl_texture, NULL, color_p, screen_width * sizeof(lv_color_t));
SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, NULL);
SDL_RenderPresent(sdl_renderer);
lv_disp_flush_ready(disp_drv); // 通知LVGL刷新完成
}
这里有个坑:lv_color_t 的大小取决于你在 lv_conf.h 中配置的颜色深度。默认是32位(ARGB8888),如果你改成16位,SDL纹理格式也要对应调整。
2.4.3 lv_port_indev.c —— 输入适配
这个文件把SDL的鼠标/键盘事件转换成LVGL能识别的输入事件。核心逻辑:
static void mouse_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
// 从SDL事件队列中读取鼠标位置和按键状态
data->point.x = mouse_x;
data->point.y = mouse_y;
data->state = mouse_pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
}
嗯,这里要注意:SDL的事件处理是在主循环中通过 SDL_PollEvent() 完成的,而 mouse_read() 是被LVGL定时调用的。所以你需要一个全局变量来保存鼠标状态。
2.5 lv_conf.h 配置:别用默认值
LVGL的配置都在 lv_conf.h 里。你从 lvgl/lv_conf_template.h 复制一份到工程根目录,改名为 lv_conf.h。我建议你至少改这几个:
| 宏定义 | 推荐值 | 说明 |
|---|---|---|
LV_COLOR_DEPTH |
32 | 颜色深度,模拟器用32位,色彩更丰富 |
LV_MEM_CUSTOM |
0 | 使用LVGL内置内存分配器,省心 |
LV_USE_DEMO_WIDGETS |
1 | 打开官方demo,方便测试环境是否正常 |
LV_HOR_RES_MAX |
800 | 模拟器窗口宽度 |
LV_VER_RES_MAX |
480 | 模拟器窗口高度 |
LV_COLOR_DEPTH,一定要同步修改 lv_port_disp.c 中的SDL纹理格式。否则显示出来全是花屏。我曾经在16位色深下忘了改格式,调试了一下午才发现是纹理格式不匹配。
2.6 验证环境:跑一个官方demo
环境搭好了,我们来验证一下。在 main.c 中加上:
#include "lvgl/demos/lv_demos.h"
int main(void) {
lv_init();
lv_port_disp_init();
lv_port_indev_init();
lv_demo_widgets(); // 运行官方控件demo
while(1) {
lv_timer_handler();
SDL_Delay(5);
}
}
重新编译运行,你会看到一个漂亮的仪表盘界面,各种控件都有。这说明你的环境完全OK了。
说实话,环境搭建这一步虽然枯燥,但基础打好了,后面会非常顺畅。我见过太多人因为环境没配好,浪费了大量时间在“跑不起来”上。按照上面的步骤来,应该不会出问题。