1、LVGL概述与环境搭建:LVGL是什么、为什么选择LVGL、LVGL在MCU上的优势、开发环境(Keil/IAR/STM32CubeIDE)搭建与验证

1.1 LVGL到底是什么?

LVGL,全称是Light and Versatile Graphics Library。说白了,它就是一个专门为嵌入式系统打造的图形库。

我刚开始接触嵌入式GUI时,用过uCGUI,也折腾过emWin。后来遇到LVGL,第一感觉就是——这玩意儿真轻。它不像Linux下的Qt那么重,也不像某些商业库那样藏着掖着。LVGL完全开源,用C语言写的,移植起来特别顺手。

你想想看,一个只有几百KB Flash的单片机,居然能跑出带动画、带触摸、带多语言的界面。这就是LVGL的厉害之处。

我个人习惯把LVGL比作「嵌入式界的Qt」。它提供了丰富的控件:按钮、标签、滑块、图表、键盘……基本上你能想到的UI元素,它都有。

1.2 为什么选择LVGL?

市面上嵌入式GUI方案不少,我列几个常见的:

方案 特点 适合场景
LVGL 开源、轻量、控件丰富、社区活跃 资源受限的MCU,如STM32、ESP32
emWin 商业库、稳定、但收费 有预算的商业产品
TouchGFX 界面华丽、但资源消耗大 高性能MCU,如STM32F7/H7
uGUI 极简、但功能有限 超低内存场景

我在项目中遇到过几次选型纠结。有一次客户要求做一个小家电,MCU只有64KB RAM,还要跑动画。emWin试了,内存不够。TouchGFX更别提了。最后用LVGL,配合帧缓冲优化,稳稳跑起来了。

为什么会这样?因为LVGL在设计之初就考虑到了低内存场景。它支持多种显示缓冲模式:单缓冲、双缓冲、甚至局部刷新。你内存小,就用单缓冲;内存够,就用双缓冲防撕裂。

核心优势总结:

  • 内存占用极低:最低只需几KB RAM就能跑
  • 控件丰富:30+内置控件,省去自己造轮子
  • 开源免费:MIT协议,商用无压力
  • 跨平台:支持裸机、FreeRTOS、RT-Thread等
  • 社区活跃:遇到问题,GitHub上基本能找到答案

1.3 LVGL在MCU上的优势

嗯,这里要重点说说。很多初学者会问:「MCU那么弱,跑GUI靠谱吗?」

我的回答是:靠谱,但要有技巧。

LVGL在MCU上的优势,主要体现在三个方面:

第一,资源占用可控。 LVGL允许你裁剪功能。不需要动画?关掉。不需要抗锯齿?关掉。不需要某些控件?也可以关掉。我见过一个项目,把LVGL裁剪到只占30KB Flash,照样跑出了漂亮的界面。

第二,刷新机制高效。 LVGL默认只刷新变化区域,而不是全屏刷新。这一点在低帧率的MCU上特别重要。你想想看,如果每次按键都全屏刷新,CPU基本就废了。LVGL的局部刷新机制,让CPU有更多时间处理业务逻辑。

第三,硬件加速可选。 如果你的MCU带硬件加速(比如STM32的LTDC、DMA2D),LVGL可以配合使用。我在一个项目里用DMA2D做颜色格式转换,帧率直接从15fps提到了30fps。

小提示: 如果你的MCU没有硬件加速,也别慌。LVGL的软件渲染效率也很高。我实测过,在72MHz的STM32F103上,跑一个带按钮和滑块的界面,帧率能稳定在20fps以上。

1.4 开发环境搭建与验证

好了,理论说完了,咱们动手。这里我以Keil为例,带你走一遍LVGL的移植流程。IAR和STM32CubeIDE的步骤类似,我就不重复了。

1.4.1 下载LVGL源码

去GitHub搜「lvgl」,下载最新稳定版。我个人习惯用v8.x系列,因为文档最全,社区支持最好。v9.x虽然新,但有些API变了,新手容易踩坑。

下载后,你会看到这样的目录结构:

lvgl/
├── src/          # 核心源码
├── examples/     # 示例代码
├── docs/         # 文档
└── lv_conf_template.h  # 配置文件模板

1.4.2 添加源码到工程

src/ 目录下的所有 .c 文件添加到你的Keil工程里。注意,不要漏掉 lvgl/src/ 下的子目录,每个子目录都有用。

然后,把 lv_conf_template.h 复制到工程根目录,重命名为 lv_conf.h。这个文件是LVGL的配置中心,所有功能开关都在这里。

1.4.3 配置 lv_conf.h

打开 lv_conf.h,找到下面几个关键配置:

#define LV_COLOR_DEPTH     16    // 颜色深度,16位色最常见
#define LV_HOR_RES_MAX     240   // 屏幕宽度
#define LV_VER_RES_MAX     320   // 屏幕高度
#define LV_USE_DEMO_WIDGETS 1    // 打开示例,方便验证

我曾经犯过一个低级错误:颜色深度设成了32位,结果屏幕显示全是花的。后来才发现,我的屏幕只支持16位色。嗯,这里要注意,颜色深度一定要和屏幕匹配。

1.4.4 实现显示驱动

LVGL需要知道怎么往屏幕上画点。你需要实现三个函数:

// 1. 初始化屏幕
void my_disp_init(void) {
    // 你的屏幕初始化代码
}

// 2. 画一个像素点
void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
    // 把 color_p 中的数据刷到 area 指定的区域
    // 比如用SPI发送数据
    lv_disp_flush_ready(disp_drv);  // 通知LVGL刷新完成
}

// 3. 注册到LVGL
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.hor_res = 240;
disp_drv.ver_res = 320;
lv_disp_drv_register(&disp_drv);

注意: 一定要在刷新完成后调用 lv_disp_flush_ready()。如果不调用,LVGL会一直等待,界面就卡死了。我刚开始移植时忘了这步,调试了一整天。

1.4.5 实现触摸驱动(可选)

如果你的屏幕支持触摸,还需要实现触摸读取函数:

void my_touch_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
    // 读取触摸坐标
    data->point.x = touch_x;
    data->point.y = touch_y;
    data->state = touch_pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
}

1.4.6 验证移植是否成功

main() 函数中,加入以下代码:

int main(void) {
    my_disp_init();          // 初始化屏幕
    lv_init();               // 初始化LVGL
    my_disp_flush_init();    // 注册显示驱动
    my_touch_init();         // 注册触摸驱动

    // 创建一个示例界面
    lv_demo_widgets();       // LVGL自带的控件示例

    while(1) {
        lv_timer_handler();  // LVGL的心跳函数,必须周期性调用
        my_delay_ms(5);      // 延时5ms
    }
}

如果一切顺利,屏幕上应该会出现一个带按钮、滑块、进度条的示例界面。你可以点一点、滑一滑,看看响应是否流畅。

验证小技巧: 如果屏幕没显示,先检查硬件连接。用逻辑分析仪抓一下SPI信号,看看数据有没有发出去。如果数据发了但屏幕没反应,大概率是初始化序列不对。我曾经被一个屏幕的初始化序列坑过,折腾了两天才发现是复位时序的问题。

1.5 常见问题与避坑指南

最后,分享几个我踩过的坑:

  • 编译报错: 如果提示 lv_conf.h 找不到,检查一下头文件路径有没有添加。Keil里要在「C/C++」选项卡的「Include Paths」里加上 lvgl/ 目录。
  • 屏幕闪烁: 大概率是刷新频率太低。把 lv_timer_handler() 的调用间隔缩短到5ms以内,或者改用双缓冲。
  • 触摸不灵: 检查触摸坐标是否需要校准。有些屏幕的触摸坐标和显示坐标是反的,需要做映射。
  • 内存不足: 如果编译提示RAM不够,可以减小 LV_MEM_SIZE 的值。默认是32KB,对于小内存MCU,可以降到8KB甚至4KB。

好了,第一章的内容就到这里。环境搭好了,下一章我们开始讲LVGL的核心机制——显示缓冲与刷新策略。到时候我会详细说说怎么在低内存下把性能榨到极致。