2. 嵌入式UI与LVGL概述
好,咱们正式开始动手了。这一章,我带你拆开LVGL的“黑盒子”,看看里面到底有什么,然后把它移植到你的STM32或ESP32上。说白了,就是让这块屏幕听你使唤。
很多新手一上来就急着画界面,结果卡在移植这一步,折腾好几天。我个人习惯是,先把框架摸清楚,再动手。你想想看,连代码怎么跑起来的都不知道,出了问题怎么查?
2.1 LVGL源码结构:别被文件夹吓到
下载下来的LVGL源码,乍一看文件夹挺多。别慌,核心就这几个:
- lvgl/src:核心库,所有控件、绘图、输入设备都在这里。
- lvgl/examples:官方示例,我刚开始学的时候,全靠抄这里的代码。
- lvgl/demos:完整Demo,比如那个经典的“手表”界面,跑起来很有成就感。
- lv_conf_template.h:配置模板,这是你的“控制台”。
我建议你先把src里面的子目录扫一眼:
| 目录 | 作用 | 我踩过的坑 |
|---|---|---|
core |
对象、显示、输入设备的核心管理 | 初始化顺序错了,屏幕死活不亮 |
draw |
底层绘图引擎,负责画点、画线 | 移植时没配好,显示全是花屏 |
widgets |
所有控件,按钮、标签、滑块都在这里 | —— |
layouts |
Flex和Grid布局,做复杂界面必备 | —— |
hal |
硬件抽象层,你的显示屏和触摸屏就接在这里 | 接口写错了,触摸没反应 |
misc |
杂项,定时器、内存管理、日志等 | —— |
我的习惯: 移植前,先把
lv_conf_template.h复制一份,改名为lv_conf.h。然后打开它,从头到尾扫一遍。别急着改,先知道有哪些开关。
2.2 核心文件介绍:你必须认识的几个“大佬”
在src里,有几个文件你绕不开。我挑最重要的说:
- lv_init.c:LVGL的“总开关”。调用
lv_init()后,所有内部模块才会就绪。 - lv_hal_disp.c:显示驱动接口。你要在这里告诉LVGL:“我的屏幕分辨率是320x240,刷新函数是这个。”
- lv_hal_indev.c:输入设备接口。触摸屏、按键、编码器,都通过它告诉LVGL。
- lv_tick.c:心跳计时器。LVGL的动画、延时、任务调度全靠它。
嗯,这里要注意:lv_tick.c里的lv_tick_inc()函数,你必须每隔1毫秒调用一次。我曾经忘了加这个,结果动画卡得像PPT,排查了半天才发现是心跳没给上。
2.3 移植到STM32/ESP32:分两步走
移植其实就两件事:告诉LVGL怎么画屏幕,告诉LVGL时间过了多久。我以STM32为例,ESP32同理。
第一步:配置lv_conf.h
打开lv_conf.h,找到这几个关键配置:
/* 颜色深度,根据你的屏幕来。16位色最常见 */
#define LV_COLOR_DEPTH 16
/* 内存大小,STM32一般给32KB就够用 */
#define LV_MEM_SIZE (32U * 1024U)
/* 开启你要用的控件,节省内存 */
#define LV_USE_BTN 1
#define LV_USE_LABEL 1
#define LV_USE_SLIDER 1
/* 其他控件按需开启,默认全关 */
注意:
LV_MEM_SIZE不是越大越好。我见过有人给STM32F103分配了128KB,结果内存溢出直接死机。根据你的芯片RAM来,一般32KB起步,不够再加。
第二步:实现显示驱动和心跳
你需要写两个函数:一个负责把LVGL的绘图缓冲区刷到屏幕上,另一个负责提供毫秒级时间戳。
显示驱动示例(STM32 + ILI9341):
/* 定义一个缓冲区,LVGL会先画到这里 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
/* 刷新函数:把缓冲区内容送到屏幕 */
static void my_disp_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p)
{
/* 你的屏幕驱动函数,比如ILI9341的写区域函数 */
ili9341_fill(area->x1, area->y1, area->x2, area->y2, color_p);
/* 告诉LVGL刷新完成 */
lv_disp_flush_ready(disp);
}
/* 初始化显示 */
void lv_port_disp_init(void)
{
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
lv_disp_drv_register(&disp_drv);
}
心跳函数:
/* 在定时器中断里,每1ms调用一次 */
void SysTick_Handler(void)
{
lv_tick_inc(1);
lv_timer_handler(); /* 处理LVGL的任务 */
}
避坑指南: 我曾经把
lv_tick_inc()和lv_timer_handler()都放在主循环里,结果因为主循环被其他任务阻塞,心跳不准,动画一卡一卡的。后来我改成在定时器中断里调用lv_tick_inc(),主循环里只调lv_timer_handler(),问题就解决了。
2.4 初始化与心跳函数:让LVGL“活”起来
移植完成后,你的main()函数大概长这样:
int main(void)
{
/* 硬件初始化:时钟、GPIO、SPI、屏幕、触摸 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
ili9341_init();
/* LVGL初始化 */
lv_init();
/* 显示驱动初始化 */
lv_port_disp_init();
/* 输入设备初始化(如果有触摸屏) */
lv_port_indev_init();
/* 创建你的第一个界面 */
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);
/* 主循环 */
while (1)
{
lv_timer_handler(); /* 处理LVGL的任务 */
HAL_Delay(5); /* 适当延时,别让CPU跑满 */
}
}
这里有个细节:lv_timer_handler()不能放在中断里,否则会嵌套调用,导致栈溢出。我刚开始没注意,结果程序跑着跑着就HardFault了。
2.5 知识体系总览:一张图看懂
下面这张图,是我自己总结的LVGL移植全流程。你照着这个顺序来,基本不会出错。
这张图把整个流程串起来了。你照着这个顺序,从源码准备到主循环,一步步来,移植就不会乱。
核心要点回顾:
- LVGL源码的核心在
src目录,别被其他文件夹干扰。 lv_conf.h是你的控制台,颜色深度、内存大小、控件开关都在这里配。- 显示驱动和心跳是移植的两大支柱,缺一不可。
- 心跳函数
lv_tick_inc()必须每1ms调用一次,最好放在定时器中断里。 - 主循环里只调
lv_timer_handler(),别放中断里。