2. 开发环境搭建
说实话,很多初学者一上来就纠结「用STM32还是ESP32」,结果纠结了两周还没动手。我的建议是——先别想太多,选一个你手头有的板子就行。如果两个都没有,那就看你的目标场景。
这一章,我会带你走一遍完整的搭建流程。从选硬件到点亮屏幕,每一步我都会把当年踩过的坑指出来。嗯,咱们开始吧。
2.1 硬件平台怎么选?STM32 vs ESP32
先说说我的个人习惯。我最早接触LVGL是在STM32F429上,那时候用的是429的LTDC硬件控制器,直接驱动RGB接口的屏幕。后来做物联网项目,换成了ESP32,因为Wi-Fi和蓝牙集成实在太方便了。
这两个平台,说白了各有各的脾气。我整理了一张对比表,你看完心里就有数了:
| 对比项 | STM32(以F4/H7为例) | ESP32 |
|---|---|---|
| 主频 | 168~480 MHz | 240 MHz |
| RAM | 192 KB ~ 1 MB(片内) | 520 KB(片内) |
| Flash | 1~2 MB(片内) | 4~16 MB(片外SPI Flash) |
| 硬件图形加速 | LTDC、DMA2D、Chrom-ART | 无专用图形硬件 |
| 无线连接 | 需外挂Wi-Fi/BT模块 | Wi-Fi + BLE 原生集成 |
| LVGL流畅度 | 高(尤其H7+DMA2D) | 中等(靠CPU刷屏) |
| 适合场景 | 工业仪表、医疗设备、高端HMI | 智能家居、物联网终端、小屏交互 |
2.2 获取LVGL源码
LVGL的源码托管在GitHub上,项目名叫 lvgl。我一般直接去Release页面下载稳定版,而不是拉master分支——你想想看,master上可能今天还能编译,明天就给你来个API变更,那多闹心。
获取方式有两种:
- 直接下载ZIP包:去
lvgl/releases找最新稳定版,比如 v8.3.x 或 v9.x。解压后你会看到src/、examples/、lv_conf_template.h等核心目录。 - Git Submodule:如果你用Git管理项目,我推荐这种方式。在项目根目录执行:
git submodule add https://github.com/lvgl/lvgl.git
git submodule update --init --recursive
这样做的好处是,以后LVGL更新了,你只需要 git submodule update 就能同步,版本管理特别干净。
lv_disp_drv_t 初始化方式。如果你跟着网上的教程走,一定要确认版本一致。我曾经见过有人拿v8的教程去配v9的库,折腾了两天没跑起来。
2.3 移植LVGL到裸机工程
移植LVGL到裸机,说白了就是三步:把源码加进工程 → 实现显示驱动 → 实现输入驱动。我以STM32裸机工程为例,给你拆开讲。
2.3.1 添加源码到工程
把下载好的 lvgl 文件夹整个复制到你的项目目录下。然后在IDE里(Keil、IAR或STM32CubeIDE都行)把以下路径加进去:
lvgl/src/下的所有.c文件lvgl/examples/porting/下的lv_port_disp_template.c和lv_port_indev_template.c
头文件路径别忘了加:
lvgl/
lvgl/src/
lvgl/examples/porting/
2.3.2 实现显示驱动
打开 lv_port_disp_template.c,找到 disp_init() 函数。这里要初始化你的LCD屏——比如用FSMC接口驱动ILI9341,或者用SPI驱动ST7789。
核心是下面这个回调函数:
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
// 把 color_p 里的像素数据刷到屏幕的 (area->x1, area->y1) 到 (area->x2, area->y2) 区域
LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
LCD_WriteDataMultiple((uint16_t *)color_p, lv_area_get_width(area) * lv_area_get_height(area));
lv_disp_flush_ready(disp_drv); // 通知LVGL刷完了
}
lv_disp_flush_ready() 一定要在刷屏完成后调用,否则LVGL会一直等待,界面卡死。我刚开始移植时就忘了这行,屏幕死活不亮,查了半天才发现。
2.3.3 实现输入驱动
如果你用触摸屏,打开 lv_port_indev_template.c,实现触摸读取函数:
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
uint16_t x, y;
uint8_t pressed;
TP_Read(&pressed, &x, &y); // 你的触摸芯片读取函数
if(pressed) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
} else {
data->state = LV_INDEV_STATE_REL;
}
}
如果是按键,那就实现 keypad_read,把按键编码映射成LVGL的 LV_KEY_* 枚举值。
2.3.4 主循环调用
在 main() 里,初始化完硬件后,调用 lv_init(),然后注册显示和输入设备。最后在 while(1) 里放:
while(1) {
lv_timer_handler(); // 处理LVGL任务
delay_ms(5); // 适当延时,让出CPU
}
你想想看,lv_timer_handler() 就是LVGL的心脏,它负责处理动画、刷新、输入响应。不调它,界面就是死的。
2.4 配置lv_conf.h
这是移植过程中最容易出错的一步。LVGL的配置文件叫 lv_conf.h,你需要从 lv_conf_template.h 复制一份,改名为 lv_conf.h,放到项目根目录或 lvgl/ 同级目录下。
打开文件,第一件事就是:
#if 0 /* 把这个改成 #if 1,启用配置 */
#define LV_CONF_H
#endif
然后,我建议你重点关注这几个配置项:
| 宏定义 | 说明 | 我的推荐值 |
|---|---|---|
LV_COLOR_DEPTH |
颜色深度,16位或32位 | 16(如果屏幕是RGB565) |
LV_HOR_RES_MAX |
屏幕水平分辨率 | 240 / 320 / 480(按实际) |
LV_VER_RES_MAX |
屏幕垂直分辨率 | 240 / 320 / 480(按实际) |
LV_MEM_SIZE |
LVGL动态内存池大小 | 32 * 1024(32KB起步) |
LV_USE_DEMO_WIDGETS |
是否启用内置demo | 1(调试阶段打开) |
LV_FONT_MONTSERRAT_* |
启用哪些字号 | 12、14、16(够用) |
LV_MEM_SIZE 设太小会导致控件创建失败,界面一片空白。我遇到过最离谱的一次,设了8KB,结果创建个按钮就崩了。裸机工程建议至少32KB,如果RAM够,64KB更稳。
2.5 整体移植流程一览
说了这么多,我画了一张流程图,帮你把整个移植过程串起来。你照着这个走,基本不会漏步骤。
2.6 验证移植是否成功
移植完成后,怎么知道它跑起来了?我一般会在 main() 里加一段测试代码:
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);
如果屏幕上出现了 "Hello LVGL!",恭喜你,移植成功了。如果没显示,别慌,按这个顺序排查:
- 屏幕亮了但没字 → 检查
disp_flush回调是否被调用,用断点或串口打印确认。 - 屏幕不亮 → 先测裸机驱动,单独写个刷全屏红色的函数,排除LVGL的问题。
- 编译报错 → 检查
lv_conf.h是否被正确包含,宏定义是否冲突。 - 卡死或重启 → 大概率是
LV_MEM_SIZE太小,或者栈溢出。
LV_USE_LOG 打开,LVGL会通过串口打印详细的运行日志。看到日志输出,你就知道它跑到哪一步了。
好了,环境搭建这部分就到这。你跟着走一遍,把屏幕点亮、把 "Hello LVGL!" 显示出来,后面的事情就好办了。