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调用到硬件读写,每一步都清晰可见。

LVGL文件系统对接FATFS流程图 LVGL应用层 lv_img_set_src(img, "S:/pic.png") LVGL文件系统接口 lv_fs_drv_t 回调函数 FATFS文件系统 f_open / f_read / f_close diskio底层接口 disk_read / disk_write SD卡硬件 SPI / SDIO 驱动 关键点 • 盘符统一用 'S' • 路径用 '/' 分隔 • 缓冲区4字节对齐 • 内存用lv_mem_alloc 常见问题 • 文件打开返回NULL • 读取数据全是0xFF • 长文件名乱码 • 写入后数据丢失

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加载图片时,如果开启了图片缓存,第二次加载会快很多。这个后面章节会详细讲。