3、LVGL文件系统接口:lv_fs_drv_t结构体详解,注册自定义文件系统驱动

好,咱们进入第三章。上一章我们把外部存储的硬件接口打通了,但LVGL怎么知道怎么读写你的SD卡或Flash呢?它又不认识硬件。这时候就需要一个中间人——文件系统驱动。

说白了,LVGL定义了一套标准接口,你只要按这个接口把函数填好,注册进去,LVGL就能像操作本地文件一样操作你的外部存储。这个接口的核心,就是 lv_fs_drv_t 结构体。

3.1 lv_fs_drv_t 结构体长什么样?

先看源码定义。在 lv_fs.h 里,这个结构体大概长这样(我简化了一下,去掉了一些不常用的字段):

typedef struct _lv_fs_drv_t {
    char letter;                // 驱动器盘符,比如 'S' 代表 S:/
    uint16_t cache_size;        // 缓存大小(字节),0表示不缓存
    bool (*ready_cb)(void);     // 检查设备是否就绪
    lv_fs_res_t (*open_cb)(void *file, const char *path, lv_fs_mode_t mode);
    lv_fs_res_t (*close_cb)(void *file);
    lv_fs_res_t (*read_cb)(void *file, void *buf, uint32_t btr, uint32_t *br);
    lv_fs_res_t (*write_cb)(void *file, const void *buf, uint32_t btw, uint32_t *bw);
    lv_fs_res_t (*seek_cb)(void *file, uint32_t pos, lv_fs_whence_t whence);
    lv_fs_res_t (*tell_cb)(void *file, uint32_t *pos_p);
    lv_fs_res_t (*trunc_cb)(void *file);
    lv_fs_res_t (*size_cb)(void *file, uint32_t *size_p);
    lv_fs_res_t (*rename_cb)(const char *old_name, const char *new_name);
    lv_fs_res_t (*remove_cb)(const char *fn);
    lv_fs_res_t (*mkdir_cb)(const char *fn);
    lv_fs_res_t (*rddir_cb)(const char *fn, lv_fs_dir_t *dir);
    lv_fs_res_t (*open_dir_cb)(void *dir, const char *path);
    lv_fs_res_t (*close_dir_cb)(void *dir);
    lv_fs_res_t (*read_dir_cb)(void *dir, char *fn, uint32_t fn_len);
} lv_fs_drv_t;

嗯,看着有点长,但别怕。实际项目中,你不需要全部实现。我个人的经验是:只实现你需要的。比如你只从SD卡读图片,那 read_cb、open_cb、close_cb、seek_cb、tell_cb 这五个就够用了。写操作、目录操作那些,可以设为 NULL。

小技巧: 我在项目里习惯把 ready_cb 也实现一下。它用来检测硬件是否在线。如果SD卡没插,这个回调返回 false,LVGL就不会去尝试读写,避免卡死。

3.2 核心字段逐个拆解

咱们挑几个最重要的字段聊一聊。

字段 类型 说明 我的建议
letter char 盘符,例如 'S'、'F'、'U' 用大写字母,别用数字或小写
cache_size uint16_t 读缓存大小,单位字节 一般设 256 或 512,和扇区对齐
ready_cb 函数指针 设备就绪检查 建议实现,避免空操作
open_cb 函数指针 打开文件 必须实现
read_cb 函数指针 读取文件内容 必须实现
close_cb 函数指针 关闭文件 必须实现
seek_cb 函数指针 定位文件指针 必须实现
tell_cb 函数指针 获取当前文件指针位置 建议实现

你可能会问:为什么 seek_cb 和 tell_cb 这么重要?因为LVGL加载图片时,不是一口气读完的。它会先 open,然后 seek 到文件头,再分段 read。如果 seek 没实现,图片解码到一半就乱了。

注意: 我曾经在一个项目里忘了实现 seek_cb,结果图片加载出来是花的,花了我半天才定位到问题。后来发现LVGL的PNG解码器需要频繁跳转文件指针。所以这几个回调,能实现就都实现了吧。

3.3 注册驱动的完整流程

结构体定义好了,回调函数写好了,怎么告诉LVGL呢?很简单,三步走:

  1. 定义一个 lv_fs_drv_t 变量,并清零。
  2. 填充各个字段,把回调函数地址赋进去。
  3. 调用 lv_fs_drv_register(&drv) 注册。

看个完整例子:

// 1. 定义驱动变量
static lv_fs_drv_t sdcard_drv;

// 2. 实现回调(这里只展示关键几个)
static bool sdcard_ready(void) {
    // 检查SD卡是否插入
    return sd_card_detect();
}

static lv_fs_res_t sdcard_open(void *file, const char *path, lv_fs_mode_t mode) {
    // 调用底层FatFs的f_open
    FIL *fp = (FIL *)file;
    BYTE flags = (mode == LV_FS_MODE_WR) ? FA_WRITE : FA_READ;
    FRESULT res = f_open(fp, path, flags);
    return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}

static lv_fs_res_t sdcard_read(void *file, void *buf, uint32_t btr, uint32_t *br) {
    FIL *fp = (FIL *)file;
    UINT read_bytes;
    FRESULT res = f_read(fp, buf, btr, &read_bytes);
    *br = read_bytes;
    return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}

// 3. 注册函数
void fs_driver_init(void) {
    lv_fs_drv_init(&sdcard_drv);          // 初始化清零
    sdcard_drv.letter = 'S';              // 盘符 S:/
    sdcard_drv.cache_size = 256;          // 256字节缓存
    sdcard_drv.ready_cb = sdcard_ready;
    sdcard_drv.open_cb = sdcard_open;
    sdcard_drv.read_cb = sdcard_read;
    sdcard_drv.close_cb = sdcard_close;
    sdcard_drv.seek_cb = sdcard_seek;
    sdcard_drv.tell_cb = sdcard_tell;
    // 其他不需要的设为 NULL(默认就是 NULL)
    lv_fs_drv_register(&sdcard_drv);      // 注册到LVGL
}

注册完之后,你在LVGL里就可以用 "S:/images/bg.png" 这样的路径来加载图片了。LVGL看到 S: 这个前缀,就会自动调用你注册的驱动。

核心逻辑: 盘符 letter 就是LVGL识别不同文件系统的钥匙。你可以注册多个驱动,比如 'S' 给SD卡,'F' 给Flash,'U' 给U盘。互不干扰。

3.4 驱动注册的完整流程图

下面这张图,是我画的一个驱动注册与调用流程。你看一眼就明白了。

LVGL文件系统驱动注册与调用流程 LVGL 文件系统驱动注册与调用流程 用户代码 1. 定义 lv_fs_drv_t 变量 2. 填充回调函数指针 3. lv_fs_drv_register() LVGL 核心 注册到内部驱动链表 根据盘符匹配驱动 实际文件操作 open_cb → f_open read_cb → f_read seek_cb → f_lseek close_cb → f_close 底层硬件(SD卡/Flash/磁盘)

流程很清晰:你注册驱动 → LVGL存起来 → 当你用 "S:/xxx.png" 时 → LVGL解析盘符 S → 找到对应的驱动 → 调用你的回调函数 → 最终操作硬件。

3.5 避坑指南与个人经验

最后,分享几个我踩过的坑:

  • 盘符冲突: 别用 '0'、'1' 这种数字,LVGL内部可能用它做别的事。我统一用大写字母。
  • 缓存大小: cache_size 设成0虽然可以,但性能很差。我一般设256或512,和底层存储的扇区大小对齐,效率最高。
  • 回调返回值: 所有回调必须返回 lv_fs_res_t 枚举。如果底层FatFs返回错误,记得转换成LVGL的错误码。比如 FR_DISK_ERR 转 LV_FS_RES_HW_ERR。
  • 多任务安全: 如果你的回调里用到了全局变量或静态缓冲区,记得加锁。LVGL本身不是线程安全的,但你的驱动可能会被多个任务同时调用。
曾经踩过的坑: 我早期一个项目,注册驱动时忘了调用 lv_fs_drv_init() 清零,结果结构体里有随机值,LVGL把垃圾数据当成了函数指针,一调用就硬错误。所以记住:先 lv_fs_drv_init(),再赋值,最后 lv_fs_drv_register()。顺序别搞反。

好了,这一章就到这里。驱动注册好之后,下一件大事就是配置LVGL的图片解码器,让它知道怎么处理PNG、JPG这些格式。不过那是后面章节的事了。先把驱动跑通,这是地基。