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,你会看到三个需要你填的函数:

  1. disp_init():初始化屏幕。如果你的屏幕已经在 main 函数里初始化过了,这里可以留空。
  2. disp_flush():核心函数。LVGL 把一帧画面的一部分数据传给你,你要把它刷到屏幕上。
  3. 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 本章知识体系总览

下面这张图,把三个文件的关系和流程画清楚了。你可以对照着看,心里更有底。

LVGL 移植核心文件关系图 LVGL 核心库 lv_conf.h 总配置开关 lv_port_disp.c 显示驱动 lv_port_indev.c 输入设备驱动 颜色深度 分辨率 内存大小 disp_flush 缓冲区配置 坐标对齐 indev_read 坐标映射 触摸校准 💡 移植顺序建议 先配 lv_conf.h → 再写显示驱动 → 最后写输入驱动 每完成一步,都可以编译验证,不要一口气全写完再调试

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 正确识别。你想想看,如果点了没反应,是驱动没通还是坐标映射错了?有个计数器,一眼就能看出来。