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资源,比如字体和图片的动态加载。