第2章 LVGL基础框架移植:从源码到跑起来
好,咱们正式开始动手。上一章聊了LVGL和TouchGFX的整体差异,这一章我带你一步步把LVGL的底层框架搭起来。说白了,就是让LVGL能在你的硬件上“喘口气”——能显示、能触摸、能感知时间流逝。
我记得第一次移植LVGL时,对着那一堆文件真有点懵。后来摸清了门道,发现核心就三块:配置头文件、显示驱动、输入驱动。再加一个心跳时钟,齐活。
本章核心目标:让LVGL在裸机或RTOS环境下完成最基本的初始化,能在屏幕上画出一个像素点,并能响应触摸或按键输入。
2.1 源码获取与版本选择
LVGL的源码托管在GitHub上,搜索lvgl就能找到。我个人习惯直接从官方仓库的Release页面下载稳定版,而不是拉master分支——你想想看,master上可能正在开发新功能,说不定哪天就编译不过了。
目前主流版本是v8.x和v9.x。v9.x改动较大,API有调整。如果你刚入门,我建议先用v8.3.x,资料多、社区活跃、坑也少。我在项目里踩过v9早期版本的雷,有些驱动接口变了,文档还没跟上,折腾了好几天。
| 版本 | 特点 | 推荐场景 |
|---|---|---|
| v8.3.x | 稳定,资料丰富,API成熟 | 新手入门、产品量产 |
| v9.x | 新架构,性能优化,部分API不兼容 | 新项目评估、愿意尝鲜 |
下载后解压,你会看到这样的目录结构:
lvgl/
├── docs/ # 文档(英文为主)
├── examples/ # 示例代码
├── scripts/ # 构建脚本
├── src/ # 核心源码
│ ├── core/ # 核心对象、事件、动画
│ ├── draw/ # 绘图引擎
│ ├── extra/ # 额外组件(图表、表格等)
│ ├── fonts/ # 字体引擎
│ ├── hal/ # 硬件抽象层(显示、输入、时钟)
│ ├── misc/ # 杂项(内存、日志)
│ ├── layouts/ # 布局引擎
│ └── widgets/ # 基础控件(按钮、标签、滑块等)
├── lv_conf.h # 配置文件(模板)
└── lvgl.h # 主头文件
嗯,这里要注意:lv_conf.h 默认是模板,你需要复制一份到项目目录并重命名。别直接在源码目录里改,否则下次更新源码就覆盖了。
2.2 文件结构解析:哪些是必须的?
整个lvgl文件夹有几百个文件,但移植时你不需要全部编译。我一般只挑出核心部分:
- src/core/ — 必须。对象系统、事件循环、动画框架都在这里。
- src/hal/ — 必须。硬件抽象层,你要实现的显示和输入接口就在这里声明。
- src/misc/ — 必须。内存管理、日志、定时器。
- src/draw/ — 必须。绘图引擎,负责把像素刷到显存。
- src/widgets/ — 可选但推荐。基础控件,没有它们你只能画原始图形。
- src/extra/ — 按需添加。图表、表格、文件选择器等高级控件。
- src/fonts/ — 按需添加。如果你用内置字体就编译,否则可以跳过。
我曾经犯过一个错误:为了省空间,把 src/draw 整个跳过了,结果LVGL初始化后屏幕一片黑。后来才发现绘图引擎是显示的基础,没有它,你连一个像素都画不出来。
小技巧:第一次移植时,先把所有源文件加进工程,编译通过后再逐步裁剪。这样能快速定位问题,避免一开始就陷入“这个文件要不要”的纠结。
2.3 移植核心文件之一:lv_conf.h
这是LVGL的“总开关”。几乎所有功能都可以在这里打开或关闭。我把常用的配置项列出来:
/* lv_conf.h 关键配置 */
#define LV_COLOR_DEPTH 16 /* 颜色深度:16位RGB565最常见 */
#define LV_HOR_RES_MAX 240 /* 屏幕宽度 */
#define LV_VER_RES_MAX 320 /* 屏幕高度 */
#define LV_DPI_DEF 100 /* 每英寸点数,影响控件大小 */
#define LV_MEM_SIZE (32U * 1024U) /* LVGL专用堆大小 */
#define LV_TICK_CUSTOM 1 /* 使用自定义心跳时钟 */
#define LV_USE_DEMO_WIDGETS 1 /* 打开示例控件,测试用 */
这里有个坑:LV_MEM_SIZE 设太小,控件一多就分配失败;设太大,MCU内存不够。我一般先设32KB,跑起来后观察内存使用,再调整。
另外,LV_TICK_CUSTOM 必须设为1。这样LVGL就不会自己产生心跳,而是等你从外部喂给它。后面会细说。
注意:lv_conf.h 必须被你的编译器找到。要么放在工程根目录,要么在包含路径里加上它的目录。否则编译时会报“lv_conf.h not found”。
2.4 移植核心文件之二:lv_port_disp.c/h
显示驱动是移植的重头戏。LVGL不知道你的屏幕怎么刷,你得告诉它。接口很简单,就一个函数:
/* lv_port_disp.c 核心实现 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 将color_p指向的像素数据刷到屏幕的area区域 */
/* 以ILI9341为例:设置窗口,然后批量写像素 */
ili9341_set_window(area->x1, area->y1, area->x2, area->y2);
ili9341_write_pixels(color_p, lv_area_get_width(area) * lv_area_get_height(area));
/* 通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
然后初始化时注册这个函数:
void lv_port_disp_init(void)
{
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = disp_flush;
disp_drv.draw_buf = &draw_buf;
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
lv_disp_drv_register(&disp_drv);
}
这里要注意 draw_buf。LVGL需要一个缓冲区来绘制内容。缓冲区大小至少是一行像素。如果内存够,可以用全屏双缓冲,效果最流畅。我一般先用一行缓冲,跑起来再优化。
| 缓冲模式 | 内存需求 | 刷新效果 | 推荐场景 |
|---|---|---|---|
| 单行缓冲 | 低(一行像素) | 可能有撕裂 | 内存紧张 |
| 部分缓冲 | 中(1/10屏幕) | 较好 | 大多数MCU |
| 全屏双缓冲 | 高(两屏像素) | 无撕裂,最流畅 | 内存充裕 |
2.5 移植核心文件之三:lv_port_indev.c/h
输入设备驱动,比如触摸屏、按键、编码器。接口同样简单:
/* lv_port_indev.c 核心实现 */
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
/* 读取触摸坐标 */
if (touchpad_is_pressed()) {
data->state = LV_INDEV_STATE_PR;
touchpad_get_xy(&data->point.x, &data->point.y);
} else {
data->state = LV_INDEV_STATE_REL;
}
}
初始化注册:
void lv_port_indev_init(void)
{
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER; /* 触摸屏类型 */
indev_drv.read_cb = touchpad_read;
lv_indev_drv_register(&indev_drv);
}
如果你用的是按键,类型设为 LV_INDEV_TYPE_KEYPAD。编码器则用 LV_INDEV_TYPE_ENCODER。我做过一个项目,触摸屏坏了,临时用按键模拟触摸坐标,虽然体验差,但至少能调试界面布局。
调试技巧:在 touchpad_read 里加个串口打印,看看坐标值是否合理。我曾经遇到触摸屏X/Y反了,就是靠打印发现的。
2.6 心跳与时钟配置
LVGL需要知道时间流逝,才能做动画、长按检测、闪烁光标等。它不自己产生时钟,而是等你调用 lv_tick_inc() 来喂它。
通常的做法是用一个硬件定时器,每1ms中断一次,在中断里调用:
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
lv_tick_inc(1); /* 告诉LVGL过去了1ms */
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
然后在主循环里调用 lv_timer_handler() 处理定时任务:
while(1) {
lv_timer_handler(); /* 处理LVGL内部定时任务 */
delay_ms(5); /* 适当延时,避免CPU跑满 */
}
这里有个细节:lv_timer_handler() 不能放在中断里,它可能耗时较长。我见过有人把心跳和定时器处理都放中断,结果系统卡死。记住:心跳在中断喂,定时器在主循环处理。
常见错误:忘记调用 lv_tick_inc(),或者调用频率不对。如果动画不动、长按不触发,先检查心跳。我曾经排查了一整天,最后发现是定时器分频算错了,实际是2ms喂一次,LVGL以为时间过得很慢。
2.7 整体移植流程SVG图
下面这张图把整个移植流程串起来了。你可以对照着一步步走:
2.8 验证:让LVGL跑起来
移植完成后,写个简单的测试代码:
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
int main(void)
{
/* 硬件初始化:时钟、GPIO、SPI、触摸等 */
hardware_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();
delay_ms(5);
}
}
如果屏幕上出现了“Hello LVGL!”,恭喜你,移植成功了。如果没显示,别急。先检查硬件连接,再确认 disp_flush 有没有被调用。我习惯在 disp_flush 里加个GPIO翻转,用示波器看波形,就知道LVGL有没有在刷屏。
移植成功标志:
- 屏幕显示内容(哪怕只是一个像素点)
- 触摸或按键能触发事件(比如点击按钮有反馈)
- 动画能正常播放(比如进度条旋转)