3. 文件系统接口
好,咱们进入第三章。前两章我们把外部存储的硬件选型和底层驱动搞定了,现在要解决一个关键问题:LVGL怎么跟你的存储设备对话?
说白了,LVGL本身不认识什么SD卡、SPI Flash、U盘。它只认一个东西——文件系统接口。这个接口就是 lv_fs 模块。你只要给它注册一个驱动,它就能用统一的方式读写你的存储设备。
我刚开始用LVGL做产品时,就踩过这个坑。当时图省事,直接把图片数组编译进固件里。结果客户要换一套皮肤,我得重新编译整个固件……那叫一个痛苦。后来老老实实把文件系统接上,世界清净了。
3.1 lv_fs模块:LVGL的文件抽象层
lv_fs 是LVGL内置的一个轻量级文件系统抽象层。它不实现具体的文件操作,而是定义了一套标准接口。你想想看,就像USB协议一样,不管插的是U盘还是键盘,只要符合协议就能通信。lv_fs 也是这个道理。
它的核心结构体是 lv_fs_drv_t。这个结构体里定义了所有你需要实现的回调函数:
typedef struct _lv_fs_drv_t {
char letter; /* 驱动器盘符,比如 'S' 代表SD卡 */
uint16_t cache_size; /* 文件缓存大小,字节 */
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 * rddir_p, const char * path);
lv_fs_res_t (*dir_read_cb)(lv_fs_drv_t * drv, void * rddir_p, char * fn, uint32_t fn_len);
lv_fs_res_t (*dir_close_cb)(lv_fs_drv_t * drv, void * rddir_p);
} lv_fs_drv_t;
嗯,看着挺长,但别怕。实际项目中,你只需要实现几个核心回调就够了。我一般只实现 ready_cb、open_cb、close_cb、read_cb、seek_cb 这五个,剩下的用默认值就行。
"S:/images/bg.png"。这个盘符是硬编码的,选一个不冲突的字母就行。
3.2 注册自定义文件系统驱动
好,理论说完了,咱们直接上手。我以FatFS + SD卡为例,给你演示怎么注册一个完整的驱动。FatFS是嵌入式里最常用的文件系统库,LVGL官方也推荐用它。
3.2.1 第一步:实现回调函数
先写一个 fs_sd.c 文件,把回调函数填上:
#include "lvgl.h"
#include "ff.h" /* FatFS 头文件 */
#include "diskio.h" /* 底层磁盘I/O */
/* 检查SD卡是否就绪 */
static bool fs_ready_cb(lv_fs_drv_t * drv) {
(void)drv;
/* 调用FatFS的磁盘状态检查函数 */
if (disk_status(0) & STA_NOINIT) {
return false;
}
return true;
}
/* 打开文件 */
static lv_fs_res_t fs_open_cb(lv_fs_drv_t * drv, void * file_p,
const char * path, lv_fs_mode_t mode) {
(void)drv;
FIL * fp = (FIL *)file_p; /* FatFS的文件对象 */
BYTE fat_mode;
/* 转换LVGL的mode到FatFS的mode */
if (mode == LV_FS_MODE_WR) {
fat_mode = FA_WRITE | FA_CREATE_ALWAYS;
} else if (mode == LV_FS_MODE_RD) {
fat_mode = FA_READ;
} else if (mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) {
fat_mode = FA_READ | FA_WRITE | FA_CREATE_ALWAYS;
} else {
return LV_FS_RES_UNKNOWN;
}
FRESULT res = f_open(fp, path, fat_mode);
if (res == FR_OK) {
return LV_FS_RES_OK;
} else {
return LV_FS_RES_FS_ERR;
}
}
/* 读取文件 */
static lv_fs_res_t fs_read_cb(lv_fs_drv_t * drv, void * file_p,
void * buf, uint32_t btr, uint32_t * br) {
(void)drv;
FIL * fp = (FIL *)file_p;
UINT read_bytes;
FRESULT res = f_read(fp, buf, btr, &read_bytes);
*br = read_bytes;
if (res == FR_OK) {
return LV_FS_RES_OK;
} else {
return LV_FS_RES_FS_ERR;
}
}
/* 定位文件指针 */
static lv_fs_res_t fs_seek_cb(lv_fs_drv_t * drv, void * file_p,
uint32_t pos, lv_fs_whence_t whence) {
(void)drv;
FIL * fp = (FIL *)file_p;
FRESULT res;
switch (whence) {
case LV_FS_SEEK_SET:
res = f_lseek(fp, pos);
break;
case LV_FS_SEEK_CUR:
res = f_lseek(fp, f_tell(fp) + pos);
break;
case LV_FS_SEEK_END:
res = f_lseek(fp, f_size(fp) + pos);
break;
default:
return LV_FS_RES_UNKNOWN;
}
if (res == FR_OK) {
return LV_FS_RES_OK;
} else {
return LV_FS_RES_FS_ERR;
}
}
/* 关闭文件 */
static lv_fs_res_t fs_close_cb(lv_fs_drv_t * drv, void * file_p) {
(void)drv;
FIL * fp = (FIL *)file_p;
FRESULT res = f_close(fp);
if (res == FR_OK) {
return LV_FS_RES_OK;
} else {
return LV_FS_RES_FS_ERR;
}
}
(void)drv; 来消除未使用参数的警告。虽然是小细节,但代码审查时能少很多麻烦。
3.2.2 第二步:注册驱动
回调写好了,接下来在系统初始化时注册它:
void lv_fs_sd_init(void) {
lv_fs_drv_t fs_drv;
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.seek_cb = fs_seek_cb;
/* 注册到LVGL */
lv_fs_drv_register(&fs_drv);
LV_LOG_INFO("SD card file system driver registered.");
}
然后在你主程序的初始化里调用它:
int main(void) {
/* 硬件初始化 */
hal_init();
/* 初始化FatFS */
f_mount(&fatfs, "", 1);
/* 初始化LVGL */
lv_init();
/* 注册SD卡文件系统驱动 */
lv_fs_sd_init();
/* 其他初始化... */
while(1) {
lv_task_handler();
delay(5);
}
}
3.2.3 第三步:在LVGL中使用
注册完成后,你就可以用 "S:" 盘符访问SD卡里的文件了:
/* 加载SD卡里的图片 */
lv_img_dsc_t img_dsc;
lv_fs_file_t file;
lv_fs_res_t res;
/* 打开文件 */
res = lv_fs_open(&file, "S:/images/background.bin", LV_FS_MODE_RD);
if (res != LV_FS_RES_OK) {
LV_LOG_ERROR("Failed to open file!");
return;
}
/* 读取文件头,获取图片信息 */
uint8_t header[16];
uint32_t br;
lv_fs_read(&file, header, 16, &br);
/* 解析BIN文件头,填充 img_dsc... */
/* 关闭文件 */
lv_fs_close(&file);
/* 创建图片对象 */
lv_obj_t * img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &img_dsc);
不过说实话,实际项目中我很少手动这么操作。LVGL提供了更高级的API,比如 lv_img_set_src(img, "S:/images/bg.png"),它会自动调用文件系统接口去加载。但底层原理就是上面这套,你得心里有数。
3.3 文件系统驱动的性能考量
驱动写好了,能跑了,但性能怎么样?我在项目里遇到过一个问题:加载一张200KB的PNG图片,花了将近3秒。用户都以为死机了。
后来一查,问题出在 cache_size 上。这个参数决定了每次文件读取的缓存大小。默认值是0,意味着每次读一个字节都去访问磁盘。你想想看,SD卡的随机读取速度本来就慢,这么搞不卡才怪。
| cache_size 值 | 读取200KB文件耗时 | 内存占用 | 适用场景 |
|---|---|---|---|
| 0(无缓存) | ~2800ms | 0字节 | 几乎不用,除非内存极度紧张 |
| 128字节 | ~1200ms | 128字节 | 小文件、文本文件 |
| 256字节 | ~600ms | 256字节 | 一般图片加载,推荐 |
| 512字节 | ~350ms | 512字节 | 大图片、连续读取 |
| 1024字节 | ~200ms | 1024字节 | 性能优先,内存充裕 |
3.4 多文件系统共存
有些项目会同时用SD卡和SPI Flash。比如SD卡存用户数据,SPI Flash存固件和字体。这时候就需要注册多个驱动:
/* 注册SD卡驱动,盘符 'S' */
lv_fs_drv_t sd_drv;
lv_fs_drv_init(&sd_drv);
sd_drv.letter = 'S';
/* ... 配置SD卡回调 ... */
lv_fs_drv_register(&sd_drv);
/* 注册SPI Flash驱动,盘符 'F' */
lv_fs_drv_t flash_drv;
lv_fs_drv_init(&flash_drv);
flash_drv.letter = 'F';
/* ... 配置Flash回调 ... */
lv_fs_drv_register(&flash_drv);
使用时通过盘符区分:
"S:/userdata/config.ini"→ SD卡"F:/fonts/NotoSansSC.bin"→ SPI Flash
这个设计很巧妙,LVGL内部完全不用关心底层是什么存储介质,它只认盘符和路径。你想想看,如果以后要换存储方案,只需要改驱动注册那部分代码,上层应用完全不用动。
3.5 避坑指南:我踩过的三个坑
最后分享几个实战中容易翻车的地方:
- 路径分隔符:LVGL统一用
/,但FatFS在Windows上习惯用\。我刚开始写路径时写成"S:\images\bg.png",结果死活打不开文件。记住,在LVGL里永远用/。 - 文件指针生命周期:
lv_fs_open返回的文件指针,用完后必须lv_fs_close。我曾经在一个循环里反复打开文件但不关闭,最后内存泄漏导致系统崩溃。嗯,血的教训。 - 并发访问:LVGL的事件处理是单线程的,但如果你在多个任务里同时访问文件系统,一定要加互斥锁。FatFS本身支持重入,但需要你配置
FF_FS_REENTRANT宏。
lv_fs_get_letter 检查盘符是否被占用。如果返回非0值,说明这个盘符已经被注册了,换个字母就行。
好,文件系统接口这部分就讲到这里。记住一个核心思想:LVGL不关心你用什么存储,只关心你有没有注册驱动。把驱动写好了,后面加载图片、字体、动画文件就都水到渠成了。