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移植全流程框架图 LVGL移植全流程框架图 1. 下载LVGL源码 2. 复制lv_conf_template.h 3. 重命名为lv_conf.h 4. 配置lv_conf.h:颜色深度、内存大小、控件开关 5. 实现显示驱动(flush_cb) 6. 实现输入驱动(可选) 7. 配置心跳:定时器每1ms调用lv_tick_inc(1) 8. 主循环调用lv_timer_handler() 提示:移植完成后,先跑官方Demo验证,再开始画自己的界面

这张图把整个流程串起来了。你照着这个顺序,从源码准备到主循环,一步步来,移植就不会乱。

核心要点回顾:

  • LVGL源码的核心在src目录,别被其他文件夹干扰。
  • lv_conf.h是你的控制台,颜色深度、内存大小、控件开关都在这里配。
  • 显示驱动和心跳是移植的两大支柱,缺一不可。
  • 心跳函数lv_tick_inc()必须每1ms调用一次,最好放在定时器中断里。
  • 主循环里只调lv_timer_handler(),别放中断里。