3. LVGL文件系统接口
好,咱们进入第三章。上一章我们把外部存储的硬件接口打通了,但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呢?很简单,三步走:
- 定义一个
lv_fs_drv_t变量,并清零。 - 填充各个字段,把回调函数地址赋进去。
- 调用
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存起来 → 当你用 "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这些格式。不过那是后面章节的事了。先把驱动跑通,这是地基。