2. 外部存储驱动层设计
好,我们直接进入正题。上一章聊了为什么要把图片扔到外部存储,这一章咱们就动手解决「怎么让LVGL跟外部存储里的文件系统对上话」。
说白了,LVGL本身不认识FATFS,也不认识LittleFS。它只认一个东西——文件描述符和读写回调函数。所以我们要做的,就是在中间搭一座桥:文件系统抽象层。
2.1 为什么需要文件系统抽象层?
你想想看,如果你的项目今天用SD卡(FATFS),明天换成SPI Flash(LittleFS),难道要把LVGL的图片加载代码全部重写一遍?
我在项目中遇到过这种惨痛教训。早期一个HMI项目,直接让LVGL调用FATFS的f_read,后来换Flash存储,改代码改到怀疑人生。从那以后,我养成了一个习惯:不管底层用什么文件系统,上层永远只认一套抽象接口。
抽象层的好处很明显:
- 解耦:LVGL不依赖具体文件系统API
- 可替换:换文件系统只需改底层驱动,上层不动
- 可测试:可以Mock文件系统做单元测试
2.2 抽象层接口设计
我个人习惯把抽象层设计成一组函数指针结构体。这样既灵活,又方便切换。
/* 文件系统抽象层接口 */
typedef struct {
void (*init)(void); // 初始化
int (*open)(const char *path, int mode); // 打开文件
int (*close)(int fd); // 关闭文件
int (*read)(int fd, void *buf, uint32_t size); // 读取数据
int (*seek)(int fd, uint32_t offset, int whence); // 定位
uint32_t (*size)(int fd); // 获取文件大小
int (*eof)(int fd); // 判断是否到文件尾
} fs_driver_t;
嗯,这里要注意:open返回的fd不能跟标准库的混淆。我一般用int类型,从0开始递增,内部维护一个文件句柄表。
2.3 FATFS对接实现
FATFS是嵌入式领域最常用的文件系统,尤其跟SD卡搭配。对接其实不复杂,就是把抽象层的函数映射到FATFS的API。
/* FATFS驱动实例 */
static FATFS fatfs;
static FIL file_table[MAX_OPEN_FILES];
static int fatfs_open(const char *path, int mode) {
// 找一个空闲句柄
for (int i = 0; i < MAX_OPEN_FILES; i++) {
if (file_table[i].flag == 0) {
FRESULT res = f_open(&file_table[i], path, FA_READ);
if (res == FR_OK) {
file_table[i].flag = 1;
return i; // 返回句柄索引
}
}
}
return -1;
}
static int fatfs_read(int fd, void *buf, uint32_t size) {
UINT bytes_read;
FRESULT res = f_read(&file_table[fd], buf, size, &bytes_read);
return (res == FR_OK) ? bytes_read : -1;
}
/* 注册到抽象层 */
fs_driver_t fs_driver = {
.init = fatfs_init,
.open = fatfs_open,
.close = fatfs_close,
.read = fatfs_read,
.seek = fatfs_seek,
.size = fatfs_size,
.eof = fatfs_eof
};
f_open上踩过坑——忘记检查返回值。SD卡热插拔时,f_open可能返回FR_DISK_ERR,如果不处理,LVGL会直接卡死。所以,每个FATFS调用都要检查返回值。
2.4 LittleFS对接实现
LittleFS更适合Flash存储,掉电安全、磨损均衡。它的API跟FATFS不太一样,但抽象层可以统一。
/* LittleFS驱动实例 */
static lfs_t lfs;
static lfs_file_t file_table[MAX_OPEN_FILES];
static int lfs_open(const char *path, int mode) {
for (int i = 0; i < MAX_OPEN_FILES; i++) {
if (file_table[i].flag == 0) {
int res = lfs_file_open(&lfs, &file_table[i], path, LFS_O_RDONLY);
if (res == LFS_ERR_OK) {
file_table[i].flag = 1;
return i;
}
}
}
return -1;
}
static int lfs_read(int fd, void *buf, uint32_t size) {
int res = lfs_file_read(&lfs, &file_table[fd], buf, size);
return (res >= 0) ? res : -1;
}
/* 注册到抽象层 */
fs_driver_t fs_driver = {
.init = lfs_init,
.open = lfs_open,
.close = lfs_close,
.read = lfs_read,
.seek = lfs_seek,
.size = lfs_size,
.eof = lfs_eof
};
你看,上层代码完全不用改,只需要在初始化时选择用哪个驱动实例就行。
2.5 LVGL对接:文件系统回调
LVGL从v8开始,提供了lv_fs_drv_t结构体来注册自定义文件系统。我们只需要把抽象层的函数指针填进去。
/* LVGL文件系统驱动注册 */
static lv_fs_drv_t fs_drv;
void lv_fs_driver_init(void) {
lv_fs_drv_init(&fs_drv);
fs_drv.letter = 'S'; // 驱动器字母,例如 S:/
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;
fs_drv.tell_cb = fs_tell_cb;
fs_drv.size_cb = fs_size_cb;
lv_fs_drv_register(&fs_drv);
}
/* 回调函数实现 */
static void *fs_open_cb(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
int fd = fs_driver.open(path, mode);
if (fd < 0) return NULL;
/* LVGL要求返回void*,我们把fd包装一下 */
int *p_fd = lv_mem_alloc(sizeof(int));
*p_fd = fd;
return p_fd;
}
static lv_fs_res_t fs_read_cb(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br) {
int fd = *(int *)file_p;
int ret = fs_driver.read(fd, buf, btr);
if (ret >= 0) {
*br = ret;
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
open_cb返回的是void*,不是文件描述符本身。我习惯动态分配一个int来存fd,但一定要在close_cb里释放,否则会内存泄漏。我曾经因为这个漏了释放,跑几天就崩了。
2.6 整体架构图
下面这张图展示了从LVGL到物理存储的完整调用链路:
2.7 关键对比:FATFS vs LittleFS
选哪个文件系统,取决于你的硬件平台。我整理了一张对比表,方便你决策:
| 对比项 | FATFS | LittleFS |
|---|---|---|
| 适用介质 | SD卡、NAND Flash | SPI Flash、NOR Flash |
| 掉电安全 | 一般(需额外处理) | 优秀(内建CRC校验) |
| 磨损均衡 | 不支持 | 支持 |
| RAM占用 | 约2-4KB | 约1-2KB |
| 最大文件大小 | 4GB(FAT32) | 取决于配置 |
| PC兼容性 | 好(可直接读卡) | 差(需专用工具) |
| 典型场景 | 大容量图片存储 | 小文件、频繁写入 |
2.8 初始化流程
最后,把整个初始化流程串起来。我一般这样写:
void storage_init(void) {
/* 1. 初始化硬件(SPI/SDIO等) */
hal_storage_init();
/* 2. 初始化文件系统 */
#if defined(USE_FATFS)
fs_driver = fatfs_driver; // 选择FATFS
#elif defined(USE_LITTLEFS)
fs_driver = lfs_driver; // 选择LittleFS
#endif
fs_driver.init();
/* 3. 注册到LVGL */
lv_fs_driver_init();
/* 4. 验证:尝试打开一个测试文件 */
int fd = fs_driver.open("S:/test.bin", 0);
if (fd >= 0) {
// 文件系统工作正常
fs_driver.close(fd);
} else {
// 处理错误
}
}
嗯,这里有个小细节:hal_storage_init()负责初始化硬件接口,比如SPI时钟、SDIO模式等。这部分跟具体芯片强相关,我一般单独放在hal目录下。