4、文件系统集成:FatFS 与 LVGL 的对接
好,到了这一章,我们终于要动真格的了。前面几章我们把 LVGL 的显示驱动和触摸输入都调通了,但说实话,那只是让屏幕亮起来、能点两下。真正的嵌入式 UI 产品,哪个不得从 SD 卡或者 Flash 里读图片?
LVGL 本身不带文件系统,它只认一个抽象层——lv_fs_drv_t。你得把底层的 FatFS 挂上去,告诉 LVGL:「喏,这就是你的文件系统」。说白了,就是写一个适配器。
4.1 为什么需要文件系统适配层
LVGL 的设计思路很清晰:它只管 UI 渲染,不管数据从哪来。图片、字体、配置文件,这些统统交给外部存储。你想想看,如果把图片数据全部编译进固件,那 Flash 得有多大?而且换一张图就得重新烧录,这谁受得了。
所以 LVGL 定义了一套文件系统接口。你只要实现这几个回调函数,LVGL 就能像操作本地数组一样读写外部文件。我最早接触这个设计时觉得有点绕,后来发现这其实是嵌入式软件里很经典的「面向接口编程」——上层不关心底层是 SD 卡还是 NAND Flash,底层也不关心上层是 LVGL 还是别的什么库。
4.2 FatFS 与 LVGL 的对接流程
整个对接过程,我习惯分成三步走:
- 初始化 FatFS —— 挂载 SD 卡或 Flash 分区
- 实现 LVGL 文件操作回调 —— open、close、read、seek、tell
- 注册驱动到 LVGL —— 调用
lv_fs_drv_register()
嗯,这里有个坑:很多人一上来就写回调,结果发现 LVGL 读文件时返回乱码。为什么?因为 FatFS 的文件指针和 LVGL 的文件对象没对应好。我待会儿会细说。
4.3 关键数据结构:lv_fs_drv_t
先看看 LVGL 2.x 里这个结构体长什么样(LVGL 8.x 也类似,只是成员名略有变化):
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 (*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_drv_t;
注意看 open_cb 的第一个参数 void *file——这个指针指向的是 LVGL 内部维护的一个文件对象。你的任务就是把 FatFS 的 FIL 结构体地址塞进去,或者反过来从里面取出来。
4.4 代码实现:完整的 FatFS 适配器
下面是我在实际项目中用过的代码片段,去掉了硬件相关的细节,保留了核心逻辑:
/* 盘符定义 */
#define FS_LETTER 'S'
/* FatFS 文件对象指针,存放在 LVGL 的文件对象中 */
typedef struct {
FIL fil;
} fatfs_file_t;
/* 检查 SD 卡是否插入 */
static bool fs_ready_cb(void)
{
return (SD_Detect() == 0) ? true : false;
}
/* 打开文件 */
static lv_fs_res_t fs_open_cb(void *file, const char *path, lv_fs_mode_t mode)
{
fatfs_file_t *fp = (fatfs_file_t *)file;
BYTE fatfs_mode;
/* 转换 LVGL 模式到 FatFS 模式 */
if (mode == LV_FS_MODE_RD) {
fatfs_mode = FA_READ;
} else if (mode == LV_FS_MODE_WR) {
fatfs_mode = FA_WRITE | FA_CREATE_ALWAYS;
} else if (mode == (LV_FS_MODE_RD | LV_FS_MODE_WR)) {
fatfs_mode = FA_READ | FA_WRITE | FA_OPEN_ALWAYS;
} else {
return LV_FS_RES_UNKNOWN;
}
/* 调用 FatFS 打开文件 */
if (f_open(&fp->fil, path, fatfs_mode) == FR_OK) {
return LV_FS_RES_OK;
}
return LV_FS_RES_NOT_EX;
}
/* 读取文件 */
static lv_fs_res_t fs_read_cb(void *file, void *buf, uint32_t btr, uint32_t *br)
{
fatfs_file_t *fp = (fatfs_file_t *)file;
UINT bytes_read;
if (f_read(&fp->fil, buf, btr, &bytes_read) == FR_OK) {
*br = bytes_read;
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
/* 关闭文件 */
static lv_fs_res_t fs_close_cb(void *file)
{
fatfs_file_t *fp = (fatfs_file_t *)file;
f_close(&fp->fil);
return LV_FS_RES_OK;
}
/* 定位文件指针 */
static lv_fs_res_t fs_seek_cb(void *file, uint32_t pos, lv_fs_whence_t whence)
{
fatfs_file_t *fp = (fatfs_file_t *)file;
FSIZE_t fatfs_offset;
switch (whence) {
case LV_FS_SEEK_SET: fatfs_offset = pos; break;
case LV_FS_SEEK_CUR: fatfs_offset = f_tell(&fp->fil) + pos; break;
case LV_FS_SEEK_END: fatfs_offset = f_size(&fp->fil) + pos; break;
default: return LV_FS_RES_UNKNOWN;
}
if (f_lseek(&fp->fil, fatfs_offset) == FR_OK) {
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
/* 获取当前文件位置 */
static lv_fs_res_t fs_tell_cb(void *file, uint32_t *pos_p)
{
fatfs_file_t *fp = (fatfs_file_t *)file;
*pos_p = f_tell(&fp->fil);
return LV_FS_RES_OK;
}
/* 注册驱动 */
void lv_fs_fatfs_init(void)
{
lv_fs_drv_t drv;
lv_fs_drv_init(&drv);
drv.letter = FS_LETTER;
drv.ready_cb = fs_ready_cb;
drv.open_cb = fs_open_cb;
drv.close_cb = fs_close_cb;
drv.read_cb = fs_read_cb;
drv.seek_cb = fs_seek_cb;
drv.tell_cb = fs_tell_cb;
lv_fs_drv_register(&drv);
}
4.5 路径格式与盘符约定
注册好驱动之后,LVGL 怎么引用文件呢?答案是:S:/images/bg.png。前面的 S: 就是你在 drv.letter 里设置的盘符。
这里有个容易混淆的地方:FatFS 的路径是 /images/bg.png(从根目录开始),但 LVGL 要求带上盘符前缀。实际上,LVGL 在调用你的 open_cb 之前,会把 S: 去掉,只把 /images/bg.png 传进去。所以你不用在回调里处理盘符。
| LVGL 路径 | 实际传给 open_cb 的路径 | 说明 |
|---|---|---|
S:/logo.bin |
/logo.bin |
根目录下的文件 |
S:/images/bg.png |
/images/bg.png |
子目录下的文件 |
S:fonts/16px.bin |
/fonts/16px.bin |
注意没有前导斜杠时,FatFS 默认从根目录开始 |
4.6 验证对接是否成功
写完代码别急着加载图片。先跑一个简单的文件存在性检查:
lv_fs_file_t file;
lv_fs_res_t res;
res = lv_fs_open(&file, "S:/test.txt", LV_FS_MODE_RD);
if (res == LV_FS_RES_OK) {
LV_LOG_USER("文件打开成功!");
lv_fs_close(&file);
} else {
LV_LOG_ERROR("文件打开失败,错误码:%d", res);
}
如果这一步能打印出「文件打开成功」,那恭喜你,对接基本没问题了。接下来就可以用 lv_img_set_src() 加载外部图片了。
4.7 本章知识结构图
下面这张图梳理了 FatFS 与 LVGL 对接的完整数据流:
从这张图可以看得很清楚:LVGL 不直接操作 FatFS,中间隔了一层适配器。这一层虽然代码量不大,但它是整个外部存储加载图片的「咽喉要道」。只要这里不出问题,后面加载图片、字体就顺风顺水。
4.8 常见问题与避坑
- 文件打开失败但 SD 卡正常 —— 检查路径格式,是不是忘了加盘符?或者路径里用了反斜杠
\?FatFS 只认正斜杠/。 - 读取数据全是 0xFF —— 大概率是文件指针没定位对。在 seek_cb 里加个日志,看看传进来的 pos 和 whence 对不对。
- LVGL 卡死或重启 —— 检查 cache_size 设置。如果 SD 卡速度慢,建议设成 256 或 512 字节,不要设 0。
- 多个文件同时打开时崩溃 —— 确认你的 open_cb 里为每个文件分配了独立的 FIL 结构体。共用同一个 FIL 会导致文件指针互相覆盖。
好了,这一章的内容就到这里。文件系统对接是 LVGL 外部存储加载图片的基石,基础打牢了,后面的事情就简单了。