3、LVGL文件系统接口:lv_fs接口注册与FATFS文件系统移植

好,咱们进入第三章。这一章讲的是LVGL怎么跟外部存储“对话”。说白了,就是让LVGL能认识你的SD卡、U盘或者Flash上的文件。

你想想看,LVGL本身只是一个图形库,它没有文件系统的能力。你要从SD卡里读一张BMP图片,LVGL得先知道怎么打开那个文件、怎么读取数据。这个桥梁,就是lv_fs接口。

核心概念:lv_fs是LVGL定义的一套抽象文件系统接口。它不关心底层是FATFS、LittleFS还是什么别的,只要你的驱动实现了这些接口,LVGL就能用。

3.1 lv_fs接口注册流程

我个人习惯把lv_fs的注册分成三步走。每一步都不复杂,但顺序错了就容易出问题。

  1. 定义驱动结构体:填充一个 lv_fs_drv_t 结构体,告诉LVGL你的文件系统叫什么名字、支持哪些操作。
  2. 实现回调函数:把open、close、read、write、seek、tell这些函数指针,指向你真正干活的函数。
  3. 注册驱动:调用 lv_fs_drv_register(),把上面准备好的结构体注册到LVGL内部。

来看一个典型的注册代码。我一般把它放在 lv_port_fs.c 这个文件里,方便管理。

static lv_fs_drv_t fs_drv;  /* 驱动结构体 */

void lv_port_fs_init(void)
{
    lv_fs_drv_init(&fs_drv);          /* 初始化结构体 */
    fs_drv.letter = 'S';              /* 盘符:S: */
    fs_drv.cache_size = 256;          /* 缓存大小,我习惯设256字节 */

    /* 挂载回调函数 */
    fs_drv.ready_cb = fs_ready_cb;
    fs_drv.open_cb = fs_open_cb;
    fs_drv.close_cb = fs_close_cb;
    fs_drv.read_cb = fs_read_cb;
    fs_drv.write_cb = fs_write_cb;
    fs_drv.seek_cb = fs_seek_cb;
    fs_drv.tell_cb = fs_tell_cb;
    fs_drv.dir_open_cb = fs_dir_open_cb;
    fs_drv.dir_read_cb = fs_dir_read_cb;
    fs_drv.dir_close_cb = fs_dir_close_cb;

    lv_fs_drv_register(&fs_drv);      /* 注册驱动 */
}

小提示:盘符字母可以随便选,但建议用大写字母。我见过有人用小写 's',结果LVGL内部大小写敏感,死活打不开文件。嗯,踩过这个坑。

3.2 FATFS文件系统移植要点

FATFS是嵌入式领域最常用的文件系统之一。它轻量、稳定,而且跟LVGL配合得很好。移植FATFS本身不是本章重点,但有几个关键点我必须提一下。

FATFS配置项 推荐值 说明
FF_USE_STRFUNC 2 支持长文件名,LVGL路径可能包含中文
FF_USE_LFN 3 启用长文件名,堆上分配缓冲区
FF_VOLUMES 2 至少2个卷:SD卡和Flash
FF_FS_RPATH 1 启用相对路径,方便切换目录

我曾经在一个项目里忘了开长文件名支持,结果LVGL加载图片时路径被截断,图片显示不出来。查了两天才找到原因。所以,长文件名一定要开。

3.3 回调函数实现:把FATFS挂到LVGL上

这是最核心的部分。每个回调函数都要把LVGL的参数转换成FATFS能理解的参数。我挑几个重点讲。

3.3.1 open_cb:打开文件

static void * fs_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
    FIL * file = lv_mem_alloc(sizeof(FIL));
    if(file == NULL) return NULL;

    BYTE fatfs_mode;
    if(mode == LV_FS_MODE_WR) fatfs_mode = FA_WRITE | FA_CREATE_ALWAYS;
    else if(mode == LV_FS_MODE_RD) fatfs_mode = FA_READ;
    else if(mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) fatfs_mode = FA_READ | FA_WRITE;

    FRESULT res = f_open(file, path, fatfs_mode);
    if(res != FR_OK) {
        lv_mem_free(file);
        return NULL;
    }
    return file;
}

注意这里:路径要去掉盘符。LVGL传进来的路径是 S:/images/bg.bmp,但FATFS的 f_open 需要的是 /images/bg.bmp。所以你在回调里要跳过第一个字符(盘符)和冒号。

警告:千万不要在回调里直接使用 path + 2 来跳过盘符。如果路径格式变了(比如没有冒号),你的程序会直接崩溃。建议写一个安全解析函数。

3.3.2 read_cb:读取数据

static lv_fs_res_t fs_read_cb(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br)
{
    FIL * file = (FIL *)file_p;
    UINT bytes_read;
    FRESULT res = f_read(file, buf, btr, &bytes_read);
    *br = bytes_read;
    return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}

这个函数很简单,就是把FATFS的 f_read 包装一下。但有一个细节:btr和br的类型。LVGL用的是 uint32_t,FATFS用的是 UINT。在32位平台上没问题,但在某些16位平台上,UINT 可能是16位的,会有截断风险。我建议在移植时统一用 uint32_t 做强制转换。

3.4 知识结构图:lv_fs与FATFS的关系

下面这张图展示了LVGL、lv_fs接口、FATFS和底层硬件之间的关系。你看一眼就能明白整个数据流向。

lv_fs接口与FATFS文件系统架构图 LVGL应用层(图片加载、字体读取) lv_fs接口层(抽象文件系统) FATFS文件系统(底层文件操作) 底层驱动(SDIO/SPI/Flash) 关键回调函数 open_cb read_cb seek_cb close_cb FATFS配置要点 FF_USE_LFN = 3 FF_VOLUMES = 2 FF_FS_RPATH = 1

3.5 路径处理:一个容易忽略的细节

LVGL的文件路径格式是 盘符:路径,比如 S:/images/bg.bmp。但FATFS不认识这个格式。所以你在回调函数里必须做路径转换。

我一般这样处理:

static const char * skip_drive_letter(const char * path)
{
    /* 跳过盘符和冒号,例如 "S:/abc" -> "/abc" */
    if(path[0] != '\0' && path[1] == ':') {
        return path + 2;
    }
    return path;  /* 没有盘符,直接返回 */
}

然后在每个回调里调用它:

static void * fs_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
    const char * real_path = skip_drive_letter(path);
    /* 然后用 real_path 调用 f_open */
}

经验之谈:如果你同时挂载了多个存储设备(比如SD卡和Flash),每个设备用不同的盘符。这样LVGL可以通过路径的第一个字符就知道去哪个设备找文件。我习惯用 S: 表示SD卡,F: 表示Flash。

3.6 测试你的文件系统移植

移植完成后,别急着跑LVGL的图片加载。先写一个简单的测试函数,验证文件系统能不能正常工作。

void test_fs_port(void)
{
    lv_fs_file_t file;
    lv_fs_res_t res;

    /* 打开文件 */
    res = lv_fs_open(&file, "S:/test.txt", LV_FS_MODE_RD);
    if(res != LV_FS_RES_OK) {
        printf("文件打开失败: %d\n", res);
        return;
    }

    /* 读取内容 */
    char buf[128];
    uint32_t bytes_read;
    res = lv_fs_read(&file, buf, sizeof(buf), &bytes_read);
    if(res == LV_FS_RES_OK) {
        printf("读取 %d 字节: %s\n", bytes_read, buf);
    }

    /* 关闭文件 */
    lv_fs_close(&file);
}

这个测试通过了,再去做图片加载。否则出了问题,你很难分清是文件系统的问题还是图片解码的问题。

注意:测试文件 test.txt 要提前放到SD卡根目录。我第一次测试时忘了放文件,结果一直报错,还以为是移植代码写错了。嗯,这种低级错误谁都会犯。

3.7 常见问题与排查思路

移植过程中最容易遇到这几个问题:

  • 文件打不开:检查盘符是否正确,路径格式是否对,FATFS有没有初始化。
  • 读取数据为空:检查文件指针是否正确,缓存区有没有分配,FATFS的 f_read 返回值。
  • LVGL报错 LV_FS_RES_FS_ERR:通常是回调函数返回了错误码,检查每个回调的返回值。
  • 中文路径乱码:确认 FF_USE_LFN 已经开启,并且编码格式一致。

我曾经在一个项目里遇到诡异的问题:文件能打开,但读取的数据全是0。查了两天,最后发现是 f_read 的第三个参数 btr 传成了指针地址,而不是值。这种低级错误,写代码时真的要多看一眼。

好了,这一章的内容就到这里。lv_fs接口注册和FATFS移植是LVGL外部存储的基础,基础打牢了,后面加载图片、字体、动画文件都会很顺畅。