4、移植核心文件:lv_conf.h、lv_port_disp.c、lv_port_indev.c
好,到了真正动手的阶段了。前面我们把源码和工程都搭好了,现在要动的是 LVGL 的三个核心配置文件。说白了,这三个文件就是 LVGL 跟你硬件之间的「翻译官」。
我记得第一次移植的时候,光看 lv_conf.h 里那几百行宏定义就头大。后来摸到规律了——其实你只需要关注其中 10% 的配置项,剩下的保持默认就行。今天我就带你把这 10% 的核心配置捋清楚。
核心思路:lv_conf.h 决定 LVGL 的「能力范围」,lv_port_disp.c 决定「怎么画」,lv_port_indev.c 决定「怎么点」。三个文件各司其职,缺一不可。
4.1 lv_conf.h:LVGL 的「总开关」
这个文件是所有配置的起点。你把它放在工程里,LVGL 才会知道你要用哪些功能、分配多少内存、开多大的缓冲区。
4.1.1 文件位置与启用
从 LVGL 源码的 lvgl/ 目录下找到 lv_conf_template.h,复制一份到你的工程根目录,重命名为 lv_conf.h。然后打开它,找到第一行:
#if 0 /*Set this to 1 to enable content*/
改成:
#if 1 /*Set this to 1 to enable content*/
嗯,这一步很简单,但很多人会忘。我有个同事曾经在这卡了半小时,一直以为代码没编译进去……
4.1.2 必须修改的核心配置项
下面这几个宏,是你必须根据自己硬件改的。我列了个表,你对着改就行。
| 宏定义 | 说明 | 典型值(以 320x240 屏幕为例) |
|---|---|---|
LV_COLOR_DEPTH |
颜色深度,决定每个像素占多少位 | 16(RGB565,最常见) |
LV_HOR_RES_MAX |
屏幕水平分辨率 | 320 |
LV_VER_RES_MAX |
屏幕垂直分辨率 | 240 |
LV_DPI_DEF |
屏幕 DPI,影响 UI 元素物理尺寸 | 130(3.5寸屏常用) |
LV_MEM_SIZE |
LVGL 动态内存池大小 | (32U * 1024U)(32KB,够用) |
LV_DISP_DEF_REFR_PERIOD |
屏幕刷新周期(毫秒) | 30(约 33fps) |
我的经验:LV_MEM_SIZE 别设太小。我曾经在 16KB 上跑一个带图标的界面,结果动不动就卡死。后来换成 32KB,世界清净了。如果你的界面复杂,建议直接上 48KB 或 64KB。
4.1.3 功能模块的开关
往下翻,你会看到一大片 #define LV_USE_XXX 的宏。这些是 LVGL 各种控件的开关。我的建议是:
- 刚开始只开你需要的:比如
LV_USE_BTN、LV_USE_LABEL、LV_USE_CONT。这样编译快,内存占用也小。 - 等界面做起来了,再回头把需要的控件打开。
- 字体模块:
LV_FONT_MONTSERRAT_14和LV_FONT_MONTSERRAT_16基本够用。别一股脑全开,不然固件体积会暴涨。
注意:如果你用到了外部字体或图片,记得把 LV_USE_FS_IF 打开,并配置好文件系统接口。不然 LVGL 读不到你的资源文件。
4.2 lv_port_disp.c:告诉 LVGL「怎么画」
这个文件负责把 LVGL 的绘图命令,转换成你屏幕能理解的像素数据。说白了,就是「刷屏」。
4.2.1 三个关键函数
打开 lv_port_disp.c,你会看到三个需要你填的函数:
disp_init():初始化屏幕。如果你的屏幕已经在 main 函数里初始化过了,这里可以留空。disp_flush():核心函数。LVGL 把一帧画面的一部分数据传给你,你要把它刷到屏幕上。rounder()(可选):坐标对齐函数。如果你的屏幕支持区域刷新,可以用它优化性能。
4.2.2 disp_flush 的实现模板
这是最关键的函数。我直接给你一个标准模板:
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 设置屏幕的写入区域 */
LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
/* 发送像素数据(假设你的屏幕驱动有写数据函数) */
LCD_WriteDataMultiple((uint16_t *)color_p,
(area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1));
/* 通知 LVGL 刷新完成 */
lv_disp_flush_ready(disp_drv);
}
避坑指南:我曾经忘了调用 lv_disp_flush_ready(),结果屏幕刷到一半就停了,画面像鬼影一样。记住,这个函数必须放在 flush 的最后,否则 LVGL 会认为屏幕还在忙,不会继续发数据。
4.2.3 缓冲区配置
在 lv_port_disp.c 的初始化部分,你会看到缓冲区配置代码。LVGL 支持三种模式:
| 模式 | 缓冲区大小 | 性能 | 适用场景 |
|---|---|---|---|
| 单缓冲区 | 1/10 屏幕大小 | 低 | 资源极受限的 MCU |
| 双缓冲区 | 1/6 屏幕大小 | 中 | 大多数嵌入式设备 |
| 全屏双缓冲 | 2 个全屏大小 | 高 | 带显存的设备或高性能 MCU |
我个人习惯用双缓冲区,大小设为屏幕像素数的 1/10 左右。比如 320x240 的屏幕,缓冲区大小就是 320 * 240 / 10 = 7680 个像素,约 15KB(RGB565 下)。
4.3 lv_port_indev.c:告诉 LVGL「怎么点」
这个文件处理输入设备——触摸屏、按键、编码器等等。我们重点讲触摸屏,因为这是最常用的。
4.3.1 输入设备的初始化
跟显示驱动类似,你需要实现两个函数:
indev_init():初始化触摸芯片(比如 FT6236、GT911)。如果已经在别处初始化了,这里可以空着。indev_read():读取触摸数据,告诉 LVGL「手指按在哪」。
4.3.2 indev_read 的实现模板
static void indev_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
/* 读取触摸坐标 */
uint16_t x, y;
uint8_t touched = Touch_GetPoint(&x, &y);
if(touched)
{
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
}
else
{
data->state = LV_INDEV_STATE_REL;
}
}
注意:触摸坐标可能需要做「坐标映射」。比如你的触摸芯片原生坐标是 0-4095,而屏幕是 320x240,你就得做线性映射。我见过有人忘了这步,结果点左上角,响应在右下角……
4.3.3 坐标映射公式
如果你的触摸芯片和屏幕分辨率不一致,用这个公式:
data->point.x = (uint16_t)((uint32_t)raw_x * LV_HOR_RES_MAX / touch_max_x);
data->point.y = (uint16_t)((uint32_t)raw_y * LV_VER_RES_MAX / touch_max_y);
其中 touch_max_x 和 touch_max_y 是你的触摸芯片最大坐标值。
4.4 本章知识体系总览
下面这张图,把三个文件的关系和流程画清楚了。你可以对照着看,心里更有底。
4.5 移植后的验证方法
三个文件都改完后,怎么知道对不对?我的做法是写一个最简单的测试:
void lvgl_port_test(void)
{
/* 创建一个标签,显示 "Hello LVGL" */
lv_obj_t * label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(label, "Hello LVGL");
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);
}
把这个函数放在你的主循环里调用一次。如果屏幕上出现了文字,说明显示驱动通了。然后你用手指点一下文字位置,如果触摸有反应,说明输入驱动也通了。
我的习惯:我会在测试代码里加一个计数器,每次触摸就更新数字。这样能直观地看到触摸是否被 LVGL 正确识别。你想想看,如果点了没反应,是驱动没通还是坐标映射错了?有个计数器,一眼就能看出来。