3、FatFS文件系统集成:FatFS简介、LVGL与FatFS的对接、读写操作实现
好,咱们今天聊聊FatFS。说实话,在嵌入式UI里做文件系统集成,FatFS几乎是绕不开的选择。我最早接触它是在一个带SD卡的音乐播放器项目上,那时候LVGL还没这么火,但FatFS已经相当成熟了。
3.1 FatFS简介
FatFS是一个专门为小型嵌入式系统设计的FAT文件系统模块。它用C语言写的,完全独立于底层存储介质。什么意思呢?就是说你不管用SD卡、SPI Flash还是NAND Flash,只要把底层的读写接口对接好,FatFS就能在上面跑起来。
我个人习惯把它看作一个「中间层」——上层应用只管调用f_open、f_read、f_write这些标准API,底层怎么跟硬件打交道,FatFS不关心。这种分层设计,说白了就是解耦,方便移植。
FatFS有几个关键特点:
- 轻量级:ROM占用小,RAM需求也低,适合资源受限的MCU
- 支持多种配置:FAT12、FAT16、FAT32,甚至exFAT(需要配置)
- 长文件名支持:可以配置是否启用LFN
- 多卷管理:同时管理多个存储设备
你想想看,如果你的UI需要从SD卡加载字体、图片或者配置文件,没有文件系统,那就只能把数据硬编码进固件里。每次改个图片都要重新烧录,多麻烦。
核心文件结构:FatFS主要由以下几个文件组成:
- ff.h / ff.c — 核心文件系统实现
- diskio.h / diskio.c — 底层磁盘I/O接口
- ffconf.h — 配置文件,决定功能开关
3.2 LVGL与FatFS的对接
LVGL本身不直接操作文件系统。它通过一个叫「文件系统驱动」的抽象层来访问文件。这个抽象层定义了一组回调函数,比如open、close、read、write、seek、tell等。你只要把这些回调函数实现出来,注册给LVGL,它就能用你的文件系统了。
我在项目中遇到过一个问题:LVGL默认的文件系统驱动是POSIX风格的,但FatFS的API命名和参数跟POSIX不太一样。所以需要写一个适配层。
具体怎么做呢?LVGL提供了一个结构体 lv_fs_drv_t,你需要填充它:
// 定义文件系统驱动
static lv_fs_drv_t fs_drv;
// 初始化并注册
void lv_fs_fatfs_init(void) {
lv_fs_drv_init(&fs_drv);
fs_drv.letter = 'S'; // 驱动器字母,比如'S'代表SD卡
fs_drv.open_cb = fs_open;
fs_drv.close_cb = fs_close;
fs_drv.read_cb = fs_read;
fs_drv.write_cb = fs_write;
fs_drv.seek_cb = fs_seek;
fs_drv.tell_cb = fs_tell;
fs_drv.dir_open_cb = fs_dir_open;
fs_drv.dir_read_cb = fs_dir_read;
fs_drv.free_space_cb = fs_free_space;
lv_fs_drv_register(&fs_drv);
}
嗯,这里要注意:letter字段决定了你在LVGL中访问文件的路径前缀。比如你设成'S',那访问文件时就要写成 S:/images/logo.bin。
每个回调函数内部,其实就是调用FatFS对应的API。举个例子:
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) {
FIL file;
BYTE flags = 0;
if (mode == LV_FS_MODE_RD) flags = FA_READ;
else if (mode == LV_FS_MODE_WR) flags = FA_WRITE | FA_CREATE_ALWAYS;
else if (mode == (LV_FS_MODE_RD | LV_FS_MODE_WR)) flags = FA_READ | FA_WRITE;
if (f_open(&file, path, flags) == FR_OK) {
FIL * p_file = malloc(sizeof(FIL));
memcpy(p_file, &file, sizeof(FIL));
return p_file;
}
return NULL;
}
我习惯把FatFS的FIL对象用malloc分配出来,因为LVGL要求返回一个指针作为文件句柄。记得在close回调里free掉,不然内存泄漏。
小技巧:如果你的MCU内存紧张,可以用静态数组池来管理文件句柄,避免动态内存分配带来的碎片问题。我曾在STM32F103上这么干过,效果不错。
3.3 读写操作实现
对接好之后,读写操作就变得很简单了。LVGL提供了统一的文件操作接口:
// 打开文件
lv_fs_file_t file;
lv_fs_res_t res = lv_fs_open(&file, "S:/config/settings.txt", LV_FS_MODE_RD);
if (res != LV_FS_RES_OK) {
// 处理错误
return;
}
// 读取数据
char buf[256];
uint32_t bytes_read;
lv_fs_read(&file, buf, sizeof(buf), &bytes_read);
// 关闭文件
lv_fs_close(&file);
写操作类似:
lv_fs_file_t file;
lv_fs_open(&file, "S:/data/log.txt", LV_FS_MODE_WR);
const char * text = "Hello LVGL!\n";
uint32_t bytes_written;
lv_fs_write(&file, text, strlen(text), &bytes_written);
lv_fs_close(&file);
我曾经踩过一个坑:LVGL的文件操作默认是阻塞的。如果你在UI主循环里读写大文件,界面会卡住。解决办法有两个:
- 用任务/线程:把文件读写放到后台任务里,通过消息队列通知UI更新
- 分块读写:每次只读一小块,配合LVGL的
lv_timer分多次完成
我个人更推荐第二种方式,因为它不需要引入RTOS,代码也更简单。
注意:FatFS的f_open和f_close是有开销的。频繁打开关闭小文件会降低性能。我建议在初始化时预打开常用文件,或者用缓存机制减少文件操作次数。
另外,LVGL还支持直接从文件系统加载图像和字体。比如:
// 从SD卡加载图片
lv_img_dsc_t img;
lv_fs_file_t file;
lv_fs_open(&file, "S:/images/icon.bin", LV_FS_MODE_RD);
// 读取文件头和数据...
lv_fs_close(&file);
// 或者直接用LVGL的图片解码器
lv_img_set_src(img_obj, "S:/images/photo.jpg");
这里有个细节:LVGL的图片解码器默认只支持内置格式(如BIN)。如果你想直接加载JPG或PNG,需要启用相应的解码器(如LV_USE_PNG、LV_USE_JPEG)。
最后,说一个调试技巧。当文件系统出问题时,我通常先确认FatFS本身能不能正常工作——写一个简单的测试函数,直接调用f_open、f_read,看看返回值。如果FatFS没问题,那问题大概率出在LVGL的适配层上。
好了,FatFS集成这部分就聊到这儿。下一节咱们看看如何用文件系统来管理UI资源,比如字体和图片的动态加载。