4、文件系统移植与配置:在 STM32/ESP32 上移植 FATFS,对接 LVGL 文件系统接口
好,咱们进入第四章。上一章我们把存储介质搞定了,SD卡能读能写了。但LVGL不认识SD卡,它只认文件系统接口。说白了,我们需要在中间搭一座桥——把FATFS挂上去,再让LVGL通过这个桥去读图片文件。
我个人习惯把这一步叫做“打通任督二脉”。桥搭好了,LVGL就能直接从外部存储加载图片,不用再费劲把图片编译进固件里。嗯,这才是做产品该有的样子。
4.1 为什么需要文件系统层?
先想一个问题:LVGL怎么知道SD卡里有个叫“logo.png”的文件?它又不懂SD卡的扇区、簇、FAT表这些东西。所以我们需要一个中间层——文件系统。
FATFS就是干这个的。它把底层的扇区读写封装成“打开文件”、“读取文件”、“关闭文件”这些操作。LVGL再通过自己的文件系统接口调用FATFS,就能像操作本地文件一样操作SD卡了。
核心链路:
LVGL → LVGL文件系统接口 → FATFS → SD卡驱动 → 硬件
每一层各司其职,缺一不可。
4.2 FATFS移植:三步走
移植FATFS其实不复杂,核心就三步。我在STM32F4和ESP32上都做过,流程大同小异。
4.2.1 第一步:下载并添加源码
去FATFS官网下载源码包,把 ff.c、ff.h、diskio.c、diskio.h 和 ffconf.h 这几个文件加到你的工程里。ESP32用户注意,ESP-IDF自带FATFS组件,直接用 idf.py menuconfig 配置就行,不用手动移植。
4.2.2 第二步:实现底层接口
这是移植的关键。FATFS通过 diskio.c 里的几个函数跟硬件打交道。你需要实现以下函数:
| 函数名 | 功能 | 注意事项 |
|---|---|---|
disk_initialize() |
初始化磁盘 | 返回0表示成功 |
disk_status() |
获取磁盘状态 | 通常返回0或STA_NOINIT |
disk_read() |
读取扇区 | 注意缓冲区对齐 |
disk_write() |
写入扇区 | 只读卡可返回RES_WRPRT |
disk_ioctl() |
控制命令 | 必须实现GET_SECTOR_COUNT等 |
get_fattime() |
获取当前时间 | 返回FAT时间戳格式 |
这里有个坑,我踩过。 disk_read() 和 disk_write() 的缓冲区必须4字节对齐。有些SD卡驱动用DMA传输,如果缓冲区没对齐,DMA会直接报错。我曾经因为这个查了两天,最后发现是malloc返回的地址没对齐。
小技巧: 用 malloc() 分配缓冲区时,可以手动对齐到4字节:
uint8_t *buf = (uint8_t *)malloc(size + 3);
uint8_t *aligned_buf = (uint8_t *)(((uint32_t)buf + 3) & ~3);
4.2.3 第三步:配置ffconf.h
ffconf.h 是FATFS的配置文件。有几个宏必须改:
#define FF_USE_STRFUNC 1 // 允许字符串操作
#define FF_USE_MKFS 1 // 允许格式化
#define FF_USE_FASTSEEK 1 // 启用快速定位
#define FF_CODE_PAGE 936 // 简体中文,支持长文件名
#define FF_USE_LFN 2 // 启用长文件名(堆栈动态分配)
#define FF_VOLUMES 1 // 卷数量,一般1个就够了
我个人建议把 FF_USE_LFN 设为2。设为1的话,长文件名缓冲区在全局区,多任务环境下会有问题。设为2用堆栈分配,更安全。
4.3 LVGL文件系统接口对接
FATFS跑起来了,接下来要让LVGL认识它。LVGL从v7开始就内置了文件系统抽象层,我们只需要注册一个驱动。
4.3.1 注册文件系统驱动
LVGL用 lv_fs_drv_t 结构体描述一个文件系统驱动。我们需要填充这个结构体,然后调用 lv_fs_drv_register() 注册。
static lv_fs_drv_t fs_drv;
void lv_fs_fatfs_init(void) {
lv_fs_drv_init(&fs_drv);
fs_drv.letter = 'S'; // 盘符,LVGL用'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.dir_close_cb = fs_dir_close;
fs_drv.ready_cb = fs_ready;
lv_fs_drv_register(&fs_drv);
}
每个回调函数都要对接FATFS的对应API。比如 fs_open 的实现:
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
FIL *file = (FIL *)lv_mem_alloc(sizeof(FIL));
if (!file) return NULL;
BYTE fat_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 fat_mode = FA_READ | FA_WRITE;
FRESULT res = f_open(file, path, fat_mode);
if (res != FR_OK) {
lv_mem_free(file);
return NULL;
}
return file;
}
注意这里用 lv_mem_alloc 而不是 malloc。LVGL有自己的内存管理,保持一致能避免碎片问题。
4.3.2 路径格式
注册时盘符设为 'S',那么LVGL中访问SD卡文件的路径就是 S:/images/logo.png。这个盘符可以自己定,但别跟其他文件系统冲突。
注意: FATFS的路径分隔符是 /,不是 \。有些同学从Windows带过来的习惯,写成 S:\images\logo.png,结果文件死活打不开。嗯,我刚开始也犯过这个错。
4.4 完整对接流程图
下面这张图展示了整个对接流程,从LVGL调用到硬件读写,每一步都清晰可见。
4.5 测试验证
移植完成后,写个简单的测试代码验证一下:
void fs_test(void) {
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_ERROR("文件打开失败: %d", res);
return;
}
// 读取内容
char buf[128];
uint32_t br;
res = lv_fs_read(&file, buf, sizeof(buf), &br);
if (res == LV_FS_RES_OK) {
buf[br] = '\0';
LV_LOG_USER("读取到 %d 字节: %s", br, buf);
}
lv_fs_close(&file);
}
如果日志能正常打印出文件内容,恭喜你,桥搭好了。接下来LVGL就能通过 "S:/images/logo.png" 这样的路径直接加载图片了。
调试小技巧: 如果文件打不开,先检查FATFS本身能不能正常工作。单独写个测试,用 f_open() 直接操作,排除LVGL接口的问题。我一般会在 disk_read() 里加个打印,看看读到的第一个扇区是不是MBR或DBR,这样能快速定位是驱动问题还是文件系统问题。
4.6 性能考量
文件系统对接好了,但性能怎么样?这里有几个关键点会影响图片加载速度:
- 缓冲区大小: FATFS默认的缓冲区是512字节(一个扇区)。如果图片文件很大,频繁的扇区读写会拖慢速度。可以考虑把
FF_MAX_SS设大一些,或者用f_read()一次读多个扇区。 - 文件系统簇大小: 格式化SD卡时,簇大小选大一点(比如32KB),对大文件读取更友好。但簇太大会浪费空间,自己权衡。
- LVGL缓存: LVGL加载图片时,如果开启了图片缓存,第二次加载会快很多。这个后面章节会详细讲。