2. 开发环境搭建:硬件平台选择、获取LVGL源码、移植到裸机工程、配置lv_conf.h

说实话,很多初学者一上来就纠结「用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 智能家居、物联网终端、小屏交互
我的建议:如果你是纯学习,手头有STM32F429或F407的板子,直接用。如果是为了做联网产品,ESP32更省事。别在选平台上花超过半天时间。

2.2 获取LVGL源码

LVGL的源码托管在GitHub上,项目名叫 lvgl。我一般直接去Release页面下载稳定版,而不是拉master分支——你想想看,master上可能今天还能编译,明天就给你来个API变更,那多闹心。

获取方式有两种:

  1. 直接下载ZIP包:去 lvgl/releases 找最新稳定版,比如 v8.3.x 或 v9.x。解压后你会看到 src/、examples/、lv_conf_template.h 等核心目录。
  2. Git Submodule:如果你用Git管理项目,我推荐这种方式。在项目根目录执行:
git submodule add https://github.com/lvgl/lvgl.git
git submodule update --init --recursive

这样做的好处是,以后LVGL更新了,你只需要 git submodule update 就能同步,版本管理特别干净。

注意:LVGL v9 相比 v8 有较大的架构变化,比如移除了旧版的 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 整体移植流程一览

说了这么多,我画了一张流程图,帮你把整个移植过程串起来。你照着这个走,基本不会漏步骤。

LVGL裸机移植流程 ① 选择硬件平台 ② 获取LVGL源码 ③ 添加源码到工程 ④ 实现显示驱动 ⑤ 实现输入驱动 ⑥ 配置 lv_conf.h ⑦ 编译 & 运行

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!" 显示出来,后面的事情就好办了。