4、文件系统驱动注册:lv_fs_drv_t结构体详解与注册流程

好,咱们进入第四章。这一章要聊的是 LVGL 文件系统驱动的注册机制。说白了,就是告诉 LVGL 内核:「嘿,我这儿有个文件系统,你要读图片、读字体,都通过它来走。」

很多新手朋友在移植 LVGL 时,卡就卡在这一步。明明 SPI Flash 初始化成功了,FATFS 也挂载上了,但 lv_img 就是显示不出来。为什么?因为 LVGL 根本不知道你的文件系统存在。你得给它注册一个「接口人」——也就是 lv_fs_drv_t。

4.1 lv_fs_drv_t 结构体:接口人的「身份证」

先来看看这个结构体长什么样。我直接贴出 LVGL 源码中的定义,咱们一行一行拆解。

typedef struct _lv_fs_drv_t {
    char letter;                /* 驱动器盘符,例如 'S'、'F'、'M' */
    uint16_t cache_size;        /* 缓存大小(字节),0 表示不缓存 */
    bool (*ready_cb)(lv_fs_drv_t *drv);           /* 就绪检查 */
    lv_fs_res_t (*open_cb)(lv_fs_drv_t *drv, void *file_p, const char *path, lv_fs_mode_t mode);
    lv_fs_res_t (*close_cb)(lv_fs_drv_t *drv, void *file_p);
    lv_fs_res_t (*read_cb)(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br);
    lv_fs_res_t (*write_cb)(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw);
    lv_fs_res_t (*seek_cb)(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence);
    lv_fs_res_t (*tell_cb)(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p);
    lv_fs_res_t (*trunc_cb)(lv_fs_drv_t *drv, void *file_p);
    lv_fs_res_t (*size_cb)(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p);
    lv_fs_res_t (*rename_cb)(lv_fs_drv_t *drv, const char *old_name, const char *new_name);
    lv_fs_res_t (*free_space_cb)(lv_fs_drv_t *drv, uint64_t *total_p, uint64_t *free_p);
    lv_fs_res_t (*dir_open_cb)(lv_fs_drv_t *drv, void *dir_p, const char *path);
    lv_fs_res_t (*dir_read_cb)(lv_fs_drv_t *drv, void *dir_p, char *fn, uint32_t fn_len);
    lv_fs_res_t (*dir_close_cb)(lv_fs_drv_t *drv, void *dir_p);
} lv_fs_drv_t;

嗯,看着挺长,但别慌。我把它分成三组,你就清楚了。

分组 成员 说明
身份标识 letter、cache_size 盘符和缓存配置,每个驱动一个字母
核心文件操作 ready_cb、open_cb、close_cb、read_cb、write_cb、seek_cb、tell_cb、trunc_cb、size_cb、rename_cb、free_space_cb 读写文件必备,类似 POSIX 的 open/read/write
目录操作 dir_open_cb、dir_read_cb、dir_close_cb 遍历目录用,如果只读图片可以只实现 dir_open/dir_close
核心要点: 你不需要实现所有回调。LVGL 只会在用到时调用对应的函数指针。如果某个回调设为 NULL,LVGL 会返回「不支持」的错误码。但 open_cb、close_cb、read_cb 这三个是底线,缺了就没法读文件。

4.2 盘符 letter:一个字母的「门牌号」

letter 字段是一个 ASCII 字符,比如 'S' 代表 SPI Flash,'F' 代表 FATFS,'M' 代表内存卡。LVGL 内部通过这个字母来路由文件路径。比如你写 "S:/images/bg.png",LVGL 就知道去找 letter 为 'S' 的驱动。

我个人习惯用 'S' 表示 SPI Flash 文件系统,'F' 表示 SD 卡 FATFS。这样路径一看就明白。注意,字母不能重复,否则 LVGL 只会保留最后一个注册的驱动。

4.3 回调函数:你的底层操作「翻译器」

每个回调函数,本质上就是把 LVGL 的请求「翻译」成你底层文件系统的操作。我拿最常见的 read_cb 举个例子。

static lv_fs_res_t my_read_cb(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br) {
    /* file_p 是 open_cb 时你传进去的文件对象指针 */
    FIL *fp = (FIL *)file_p;
    UINT bytes_read;
    FRESULT res = f_read(fp, buf, btr, &bytes_read);
    if (res == FR_OK) {
        *br = bytes_read;
        return LV_FS_RES_OK;
    } else {
        *br = 0;
        return LV_FS_RES_UNKNOWN;
    }
}

你看,逻辑很简单:LVGL 说「我要读 btr 个字节」,你就调用 f_read,然后把实际读到的字节数填到 *br 里。返回值必须是 lv_fs_res_t 枚举,比如 LV_FS_RES_OK、LV_FS_RES_NOT_EX 等。

小技巧: 我在项目中遇到过一个问题——file_p 到底该存什么?答案是:存你底层文件系统返回的文件句柄或文件对象指针。FATFS 里是 FIL 结构体指针,POSIX 里是 FILE *,LittleFS 里是 lfs_file_t *。你在 open_cb 里分配并初始化它,然后在其他回调里直接使用。

4.4 注册流程:三步走

结构体填好了,怎么告诉 LVGL?就三步。

  1. 定义并清零驱动结构体:用 lv_fs_drv_init() 或手动 memset 清零。
  2. 填充字段:设置 letter、cache_size,以及你实现了的回调函数指针。
  3. 调用注册函数:lv_fs_drv_register(&my_drv)。

看个完整示例:

static lv_fs_drv_t spi_flash_drv;

void my_fs_init(void) {
    lv_fs_drv_init(&spi_flash_drv);          /* 第一步:初始化 */
    
    spi_flash_drv.letter = 'S';              /* 第二步:配置 */
    spi_flash_drv.cache_size = 0;            /* 不缓存,直接读写 */
    spi_flash_drv.ready_cb = my_ready_cb;
    spi_flash_drv.open_cb = my_open_cb;
    spi_flash_drv.close_cb = my_close_cb;
    spi_flash_drv.read_cb = my_read_cb;
    spi_flash_drv.write_cb = my_write_cb;
    spi_flash_drv.seek_cb = my_seek_cb;
    spi_flash_drv.tell_cb = my_tell_cb;
    spi_flash_drv.size_cb = my_size_cb;
    
    lv_fs_drv_register(&spi_flash_drv);      /* 第三步:注册 */
}
注意: 注册操作必须在 lv_init() 之后、任何文件读写操作之前完成。我曾经在 lv_init() 之前就注册了驱动,结果 LVGL 内部链表还没初始化,直接写崩了内存。嗯,调试了一下午才找到原因。

4.5 缓存机制:cache_size 怎么设?

cache_size 这个字段,很多人直接填 0 不管了。但如果你读的是大图片,适当开缓存能显著提升性能。

LVGL 内部会为每个打开的文件分配一个缓存区,大小就是 cache_size。读文件时,LVGL 会先读一块到缓存,后续的读取直接从缓存拿,减少底层驱动调用次数。

场景 建议 cache_size 理由
SPI Flash,小图片(< 32KB) 0 或 256 图片小,缓存收益不大,省内存
SD 卡,大图片(> 100KB) 1024 ~ 4096 减少 FATFS 扇区读取次数
内存紧张 0 不缓存,直接透传

我个人习惯:如果内存够用,给 1024 字节。如果跑在 STM32F103 这种资源受限的芯片上,就设 0,省下那点 RAM 给 UI 控件用。

4.6 知识结构图:驱动注册全流程

下面这张图,把整个注册流程和结构体关系画清楚了。你可以对照着看。

lv_fs_drv_t 注册流程与结构关系 LVGL 应用层(lv_img、lv_font) lv_fs 文件系统抽象层 lv_fs_drv_t 驱动结构体 letter: 'S' | cache_size: 1024 open_cb | read_cb | write_cb | seek_cb | ... 身份标识 letter + cache_size 核心文件操作 open/close/read/write/seek 目录操作 dir_open/dir_read/dir_close 注册:lv_fs_drv_register(&drv) → 加入 LVGL 内部链表 路径格式:"S:/images/bg.png" → 根据 letter='S' 找到驱动

4.7 避坑指南:我踩过的三个坑

最后,分享几个实战中容易翻车的地方。

  • 坑一:回调函数返回值类型搞错。 所有回调必须返回 lv_fs_res_t,不是 FRESULT 也不是 int。我见过有人直接 return FR_OK,LVGL 收到后当成 LV_FS_RES_UNKNOWN 处理,文件死活打不开。
  • 坑二:open_cb 里没保存文件对象。 file_p 是 LVGL 传给你的一个 void * 指针,你需要把底层文件句柄的地址写进去。比如 *(FIL **)file_p = my_fp;。如果忘了这一步,后面的 read_cb 拿到的 file_p 是空的。
  • 坑三:多任务环境下不加锁。 如果你的 LVGL 跑在 RTOS 上,多个任务同时读写同一个文件,驱动回调里必须加互斥锁。否则两个任务同时调用 f_read,FATFS 内部状态就乱套了。
调试小技巧: 注册完成后,调用 lv_fs_get_letter() 检查你的盘符是否在已注册列表中。如果返回 0,说明注册没成功。另外,lv_fs_open() 的返回值一定要检查,不要假设文件一定存在。