LVGL 文件系统接口:lv_fs_drv_t 结构体深度解析
好,咱们接着聊。上一章我们说了 LVGL 为什么要搞一套自己的文件系统抽象层。这一章,我们就直接扎进最核心的数据结构——lv_fs_drv_t。说白了,它就是 LVGL 文件系统驱动的“身份证”加“操作手册”。
我记得刚接触 LVGL 那会儿,看到这个结构体里一堆函数指针,心里有点发怵。后来用多了才发现,搞懂它,文件系统这块你就通了八成。
lv_fs_drv_t 结构体全景
这个结构体定义在 lv_fs.h 里。我直接把它拆开给你看:
typedef struct _lv_fs_drv_t {
char letter; // 驱动器盘符,比如 'S' 代表 SD 卡
uint16_t cache_size; // 缓存大小(字节),0 表示不缓存
bool ready_cb; // 驱动是否就绪的回调
lv_fs_res_t (*open_cb)(lv_fs_file_t *file, const char *path, lv_fs_mode_t mode);
lv_fs_res_t (*close_cb)(lv_fs_file_t *file);
lv_fs_res_t (*read_cb)(lv_fs_file_t *file, void *buf, uint32_t btr, uint32_t *br);
lv_fs_res_t (*write_cb)(lv_fs_file_t *file, const void *buf, uint32_t btw, uint32_t *bw);
lv_fs_res_t (*seek_cb)(lv_fs_file_t *file, uint32_t pos, lv_fs_whence_t whence);
lv_fs_res_t (*tell_cb)(lv_fs_file_t *file, uint32_t *pos_p);
lv_fs_res_t (*trunc_cb)(lv_fs_file_t *file);
lv_fs_res_t (*size_cb)(lv_fs_file_t *file, uint32_t *size_p);
lv_fs_res_t (*rename_cb)(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_dir_t *dir, const char *path);
lv_fs_res_t (*dir_read_cb)(lv_fs_dir_t *dir, char *fn);
lv_fs_res_t (*dir_close_cb)(lv_fs_dir_t *dir);
void *user_data; // 用户自定义数据,比如文件系统句柄
} lv_fs_drv_t;
嗯,看着挺长,但别慌。我习惯把它分成三组来理解:
| 分组 | 成员 | 作用 |
|---|---|---|
| 身份标识 | letter, cache_size, user_data |
告诉 LVGL 你是谁,用什么盘符,要不要缓存 |
| 文件操作 | open_cb ~ free_space_cb |
打开、读写、定位、改名…… 文件的基本操作 |
| 目录操作 | dir_open_cb, dir_read_cb, dir_close_cb |
遍历文件夹用的 |
核心要点:你不需要实现所有回调。LVGL 只要求你实现 open_cb、close_cb、read_cb 这三个,其他的可以设为 NULL。但如果你要做图片加载,seek_cb 和 size_cb 最好也实现,否则解码器会很难受。
盘符 letter:那个不起眼的字符
这个 letter 字段,我见过很多人栽跟头。它就是一个 ASCII 字符,比如 'S'、'F'、'U'。你在 LVGL 里访问文件时,路径要写成 "S:/images/logo.png" 这种格式。LVGL 就是靠这个字母找到对应的驱动。
我曾经遇到一个坑:注册了两个驱动,一个用 'S',一个用 's'。LVGL 内部比较盘符时是大小写敏感的,结果第二个驱动死活注册不上去。嗯,后来查源码才发现,注册函数里有个重复盘符检查,大小写算两个不同的盘符,但用户使用时容易混淆。我建议你统一用大写字母,省心。
注册与注销:lv_fs_drv_register 和 lv_fs_drv_unregister
驱动结构体填好了,怎么告诉 LVGL?两个函数搞定:
// 注册驱动
void lv_fs_drv_register(lv_fs_drv_t *drv_p);
// 注销驱动
void lv_fs_drv_unregister(char letter);
注册的逻辑很简单:把 drv_p 指针存到 LVGL 内部的一个全局数组里。但有几个细节你得注意:
- 注册前必须清零结构体:我习惯用
lv_fs_drv_init()或者直接memset,否则未初始化的字段可能带垃圾值。 - 注册后不要释放 drv_p 指向的内存:LVGL 只存了指针,不拷贝结构体。如果你用局部变量注册,函数退出后指针就悬空了。我一般用静态变量或者动态分配。
- 注销只是移除引用:LVGL 不会帮你释放
user_data里的资源,你得自己清理。
注意:LVGL 内部最多支持 LV_FS_MAX_DRV 个驱动,默认是 4 个。如果你要挂多个存储设备(比如 SD 卡 + U 盘 + Flash),记得改这个宏。我遇到过客户同时挂 5 个驱动,结果第 5 个注册失败,排查了半天才发现是数组满了。
驱动注册的完整流程
我画了一张图,把整个流程串起来:
实际注册代码示例
光说不练假把式。我给你看一段我项目里用过的代码,基于 FatFs 文件系统:
static lv_fs_drv_t fs_drv; // 静态分配,保证生命周期
void my_fs_init(void) {
// 1. 清零结构体
lv_fs_drv_init(&fs_drv);
// 2. 设置盘符和缓存
fs_drv.letter = 'S';
fs_drv.cache_size = 4096; // 4KB 缓存,适合图片读取
// 3. 绑定回调函数
fs_drv.open_cb = my_fs_open;
fs_drv.close_cb = my_fs_close;
fs_drv.read_cb = my_fs_read;
fs_drv.seek_cb = my_fs_seek;
fs_drv.tell_cb = my_fs_tell;
fs_drv.size_cb = my_fs_size;
// 4. 注册驱动
lv_fs_drv_register(&fs_drv);
LV_LOG_INFO("SD card driver registered as 'S:'");
}
我的习惯:注册成功后,我会立刻用 lv_fs_open 尝试打开一个测试文件,比如 "S:/test.txt"。如果失败,直接打印错误并进入断言。这样可以在开发阶段尽早暴露问题,而不是等到图片加载不出来才去排查。
注销驱动的正确姿势
注销驱动的情况比较少,一般是系统休眠或者热插拔时需要。调用很简单:
lv_fs_drv_unregister('S');
但这里有个坑:注销前要确保没有文件处于打开状态。LVGL 不会帮你关闭文件,如果你在文件打开时注销驱动,后续对文件的操作会崩溃。我建议你在注销前遍历所有打开的文件句柄,逐个关闭。
另外,unregister 只是把驱动从 LVGL 的内部数组里移除,不会调用你的 close_cb。如果你在 user_data 里挂了动态分配的资源,记得自己释放。
常见注册失败原因排查
注册失败时,LV_LOG 会打印错误。我整理了一份排查清单:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
注册返回 LV_FS_RES_NOT_IMP |
驱动数组已满 | 增大 LV_FS_MAX_DRV 宏 |
注册返回 LV_FS_RES_DENIED |
盘符重复 | 检查是否已注册相同 letter 的驱动 |
打开文件时返回 LV_FS_RES_NOT_EX |
驱动未注册或路径格式错误 | 确认盘符字母正确,路径以 "X:/" 开头 |
| 读写文件时崩溃 | 驱动结构体已被释放 | 确保 drv_p 指向静态或全局变量 |