4、LVGL移植第一步:裸机移植——创建工程模板、添加LVGL源码、配置lv_conf.h
好,咱们正式开始动手了。
前面三章我们把LVGL的架构、显示原理和输入设备机制都捋了一遍。说实话,光看理论容易犯困。我当年学的时候也是这样,看了三天文档,脑子里全是概念,手上一行代码没写。后来我换了个思路——先搭个能跑起来的工程,哪怕屏幕上只亮一个像素,也比看一百页文档强。
这一章,我们就来干这件事。目标很明确:在裸机环境下,把LVGL跑起来。不涉及RTOS,不涉及复杂的外设驱动,就是最纯粹的“裸奔”移植。
4.1 裸机移植到底在做什么?
说白了,LVGL的裸机移植就三件事:
- 把LVGL的源码放进你的工程里
- 告诉LVGL你的屏幕有多大、用什么颜色格式
- 给LVGL提供两个函数:一个画点,一个读时间
没了。真的就这么多。
你可能会问:“那触摸屏呢?按键呢?”别急,输入设备我们下一章单独讲。这一章先把显示搞定。屏幕能亮起来,你就成功了一半。
核心原则:裸机移植的本质是让LVGL的绘图引擎能往你的显存里写数据。至于这个显存是SRAM、SDRAM还是LCD控制器自带的,LVGL不关心。它只关心你给它的那个“画点”函数能不能把颜色填对位置。
4.2 创建工程模板
我个人习惯,每个项目都从零搭一个干净的模板。不依赖厂商的例程包,因为那些例程里经常塞了一堆用不上的东西。
4.2.1 工程目录结构
先建个文件夹,我一般这么组织:
lvgl_port_project/
├── lvgl/ # LVGL源码
│ ├── src/ # 核心源码
│ ├── examples/ # 官方示例(可选)
│ └── lv_conf.h # 配置文件
├── port/ # 移植文件
│ ├── lv_port_disp.c # 显示接口
│ └── lv_port_disp.h
├── driver/ # 硬件驱动
│ ├── lcd_driver.c
│ └── lcd_driver.h
├── main.c # 主函数
├── main.h
└── Makefile / .uvprojx # 构建文件
这个结构很清晰:lvgl/ 放官方源码,不动它;port/ 放我们自己写的移植层;driver/ 放LCD、触摸等硬件驱动。各司其职,后期维护起来不头疼。
小技巧:我建议把 lv_conf.h 放在 lvgl/ 目录下,因为LVGL源码里默认用相对路径 #include "lv_conf.h" 来找它。如果你放别处,就得改源码或者加额外的包含路径,容易出问题。
4.2.2 主函数框架
主函数怎么写?先看个最简单的骨架:
#include "lvgl.h"
#include "lcd_driver.h"
int main(void)
{
/* 1. 硬件初始化 */
lcd_init(); // 初始化LCD
lcd_set_backlight(100); // 开背光
/* 2. LVGL初始化 */
lv_init();
/* 3. 显示驱动注册 */
lv_port_disp_init();
/* 4. 主循环 */
while(1)
{
lv_timer_handler(); // LVGL心跳处理
lv_tick_inc(5); // 喂时间(5ms一次)
delay_ms(5); // 简单延时
}
}
嗯,这里有个关键点:lv_tick_inc() 是给LVGL提供系统心跳的。LVGL内部所有动画、定时器都依赖这个函数。你喂的间隔越均匀,动画就越流畅。我在项目里一般用硬件定时器中断来调用它,精度更高。
4.3 添加LVGL源码
从GitHub把LVGL v8.3.x(我推荐这个版本,稳定)下载下来,解压。然后把 src/ 目录整个复制到你的工程里。
这里有个坑——文件太多了。LVGL的源码有几百个.c文件,如果你用Keil或者IAR,一个个加进去能加到手抽筋。我教你个办法:
- Keil:在工程里建一个Group,然后把
lvgl/src/下的所有.c文件拖进去。Keil支持多选,按住Shift一次选完。 - Makefile:用通配符
$(wildcard lvgl/src/**/*.c)自动扫描。 - CMake:用
file(GLOB_RECURSE)命令。
注意:不要复制 lvgl/examples/ 目录下的文件,除非你确定要用。那些示例代码会额外占用大量Flash,而且很多初学者搞不清哪些是示例、哪些是核心,编译报错了都不知道去哪找。
4.4 配置lv_conf.h——这是最关键的一步
LVGL的灵活性,很大程度上体现在 lv_conf.h 这个配置文件上。它就像一个总开关面板,你想开什么功能、用什么颜色深度、分配多少内存,都在这里设。
4.4.1 找到并启用配置文件
在 lvgl/ 目录下,有一个 lv_conf_template.h。把它复制一份,重命名为 lv_conf.h。然后打开文件,找到第一行:
#if 0 /* 把这个改成 1 来启用配置 */
改成:
#if 1 /* 已启用 */
这一步不做,LVGL会全部用默认值,而默认值通常不适合你的硬件。
4.4.2 核心配置项
下面这几个配置,是裸机移植必须调的。我列个表,你对着改就行:
| 配置宏 | 推荐值 | 说明 |
|---|---|---|
LV_COLOR_DEPTH |
16 |
颜色深度。大部分RGB565屏幕用16位,RGB888用32位 |
LV_COLOR_16_SWAP |
1 |
如果你的MCU是大端模式,或者LCD控制器要求字节交换,就开这个 |
LV_MEM_SIZE |
(32U * 1024U) |
LVGL动态内存池大小。裸机至少32KB,复杂UI建议64KB以上 |
LV_HOR_RES_MAX |
320 |
屏幕水平分辨率,改成你实际的值 |
LV_VER_RES_MAX |
240 |
屏幕垂直分辨率 |
LV_DPI_DEF |
130 |
每英寸像素数,影响触摸校准和控件大小。拿不准就设130 |
LV_TICK_CUSTOM |
0 |
裸机下关掉,我们自己手动调用 lv_tick_inc() |
关于LV_MEM_SIZE:这个值不是越大越好。LVGL的内存池是在编译时静态分配的,你设64KB,它真的就吃掉64KB RAM。如果你的MCU只有128KB RAM,设个32KB比较稳妥。我在一个STM32F103项目里,32KB跑一个带仪表盘和列表的UI,完全够用。
4.4.3 关闭不需要的功能
LVGL默认开了很多功能,比如动画、抗锯齿、主题、字体等等。裸机移植阶段,我建议全部关掉,只保留最核心的绘图引擎。等跑起来了,再一个一个打开。
把下面这些宏设为 0:
LV_USE_ANIMATION—— 动画LV_USE_SHADOW—— 阴影LV_USE_ANTIALIAS—— 抗锯齿LV_USE_THEME_DEFAULT—— 默认主题LV_FONT_MONTSERRAT_*—— 所有内置字体,只留一个LV_FONT_MONTSERRAT_12用于调试
这样做的好处是:编译速度快、占用Flash小、内存开销低。等你把“Hello World”显示出来之后,再慢慢加功能,心里有底。
4.5 显示接口实现——lv_port_disp.c
这是移植的核心文件。LVGL通过这个文件里的三个回调函数,把绘图数据送到你的屏幕上。
4.5.1 注册显示驱动
在 lv_port_disp_init() 函数里,我们要做这几件事:
void lv_port_disp_init(void)
{
/* 1. 创建一个显示缓冲区 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_1[LV_HOR_RES_MAX * 40]; // 一行40像素高
lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, LV_HOR_RES_MAX * 40);
/* 2. 注册显示驱动 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = my_disp_flush; // 刷新回调
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
/* 3. 注册到LVGL */
lv_disp_drv_register(&disp_drv);
}
这里有个细节:缓冲区大小。我用了 LV_HOR_RES_MAX * 40,也就是一行40像素高。这个值可以根据你的RAM调整。缓冲区越大,刷新效率越高,但占RAM也多。10行到40行是比较常见的范围。
4.5.2 实现刷新回调
my_disp_flush 函数是真正往LCD写像素的地方:
static void my_disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
lcd_draw_pixel(x, y, color_p->full);
color_p++;
}
}
lv_disp_flush_ready(disp_drv); // 通知LVGL刷新完成
}
注意最后一行 lv_disp_flush_ready(),这个必须调用。否则LVGL会认为刷新还没完成,一直等着,整个UI就卡死了。我曾经在这个坑里爬了整整一个下午,最后发现是忘了加这一句。
4.6 整体流程梳理
说了这么多,咱们用一张图把整个流程串起来:
4.7 验证与调试
代码写完了,编译通过,下载到板子上。然后呢?
屏幕上应该还是黑的。别慌,LVGL默认不画任何东西。你需要在主循环里加一行测试代码:
/* 创建一个标签,显示 "Hello LVGL!" */
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello LVGL!");
lv_obj_center(label);
把这行代码放在 lv_port_disp_init() 之后、主循环之前。重新编译下载,如果屏幕上出现了文字,恭喜你——移植成功了。
常见问题:
- 屏幕白屏或花屏:检查
LV_COLOR_DEPTH和LV_COLOR_16_SWAP是否匹配你的LCD。我遇到过一块屏幕,明明是RGB565,但必须开字节交换才能正常显示。 - 编译报错,提示 undefined reference:多半是
lv_port_disp.c没加到编译列表里,或者lv_conf.h没被找到。 - 屏幕有显示但卡死:检查
lv_disp_flush_ready()是否在flush回调末尾调用了。没调的话,LVGL会一直等待。
4.8 小结
这一章我们走完了裸机移植的完整流程。从创建工程目录,到添加源码,再到配置 lv_conf.h 和实现显示接口,每一步我都把当年踩过的坑指给你看了。
说实话,第一次移植成功时,屏幕上那个小小的“Hello LVGL!”让我兴奋了好一阵。虽然现在看来那只是个开始,但那种“我能控制每一个像素”的感觉,真的很棒。