3、LVGL存储系统与文件系统接口

这一章,我们聊聊LVGL怎么跟外部存储打交道。

说白了,LVGL本身不关心你的图片存在SD卡还是Flash里。它只认一个东西——文件系统接口。你给它注册好驱动,它就能读文件。这个设计很巧妙,把上层应用和底层存储彻底解耦了。

3.1 LVGL存储抽象层

LVGL内部维护了一套存储抽象层。它的核心思路是:所有文件操作都通过函数指针调用。

你看LVGL的源码,会发现它定义了一组标准接口:

typedef struct _lv_fs_drv_t {
    char letter;            /* 驱动器盘符,比如 'S' 代表 SD 卡 */
    uint16_t cache_size;    /* 缓存大小 */
    bool (*ready_cb)(lv_fs_drv_t * drv);
    lv_fs_res_t (*open_cb)(lv_fs_drv_t * drv, void * file_p, const char * path, lv_fs_mode_t mode);
    lv_fs_res_t (*close_cb)(lv_fs_drv_t * drv, void * file_p);
    lv_fs_res_t (*read_cb)(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br);
    lv_fs_res_t (*seek_cb)(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence);
    lv_fs_res_t (*tell_cb)(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p);
    lv_fs_res_t (*size_cb)(lv_fs_drv_t * drv, void * file_p, uint32_t * size_p);
    ...
} lv_fs_drv_t;

每个回调函数,你都得自己实现。LVGL只管调用。

核心思想: LVGL不直接操作硬件。它通过驱动表间接访问文件。你换存储介质,只需要换驱动表,上层代码一行都不用改。

3.2 文件系统接口 lv_fs

lv_fs 是LVGL对外暴露的文件操作API。你写应用代码时,用的就是它。

常用的几个函数:

  • lv_fs_open() —— 打开文件
  • lv_fs_read() —— 读取数据
  • lv_fs_close() —— 关闭文件
  • lv_fs_seek() —— 定位文件指针
  • lv_fs_tell() —— 获取当前位置
  • lv_fs_dir_open() / lv_fs_dir_read() —— 目录操作

举个例子,加载一张图片:

lv_fs_file_t file;
lv_fs_res_t res;

res = lv_fs_open(&file, "S:/images/bg.bin", LV_FS_MODE_RD);
if (res != LV_FS_RES_OK) {
    LV_LOG_ERROR("打开文件失败");
    return;
}

/* 读取文件内容到缓冲区 */
uint8_t buf[1024];
uint32_t br;
lv_fs_read(&file, buf, sizeof(buf), &br);

lv_fs_close(&file);

注意那个 "S:/images/bg.bin"。开头的 S 就是盘符,对应你注册驱动时指定的 letter。LVGL通过这个字母找到对应的驱动表。

我的习惯: 盘符用大写字母,后面跟冒号和路径。这样路径格式统一,不容易出错。我在STM32上习惯用 'S' 表示SD卡,'F' 表示Flash。到了ESP32上,'S' 还是SD卡,'L' 留给LittleFS。

3.3 FatFS在STM32上的移植

STM32上最常用的文件系统就是FatFS。它轻量、稳定,支持FAT12/16/32。

移植步骤其实不复杂,我拆成几步说:

  1. 下载FatFS源码 —— 从ELM官网拿,或者用STM32CubeMX生成的版本
  2. 实现底层接口 —— 主要是 disk_read()、disk_write()、disk_ioctl()、get_fattime()
  3. 配置 ffconf.h —— 根据你的需求打开/关闭功能
  4. 注册到LVGL —— 写一个适配层,把FatFS的函数包装成LVGL的回调

适配层代码大概长这样:

static lv_fs_res_t fs_open_cb(lv_fs_drv_t * drv, void * file_p, const char * path, lv_fs_mode_t mode)
{
    FIL * fp = (FIL *)file_p;
    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(fp, path, fat_mode);
    return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}

static lv_fs_res_t fs_read_cb(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br)
{
    FIL * fp = (FIL *)file_p;
    UINT read_bytes;
    FRESULT res = f_read(fp, buf, btr, &read_bytes);
    *br = read_bytes;
    return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}

void lv_fs_fatfs_init(void)
{
    lv_fs_drv_t drv;
    lv_fs_drv_init(&drv);

    drv.letter = 'S';
    drv.cache_size = 256;
    drv.open_cb = fs_open_cb;
    drv.close_cb = fs_close_cb;
    drv.read_cb = fs_read_cb;
    drv.write_cb = fs_write_cb;
    drv.seek_cb = fs_seek_cb;
    drv.tell_cb = fs_tell_cb;
    drv.size_cb = fs_size_cb;

    lv_fs_drv_register(&drv);
}
我曾经踩过的坑: 注册驱动时,cache_size 不要设太大。LVGL内部会用这个值做内存分配。设成256字节就够了,设成4096反而可能因为内存碎片导致分配失败。尤其是STM32这种RAM不太富裕的芯片。

3.4 SPIFFS / LittleFS在ESP32上的使用

ESP32的情况不太一样。它自带Flash,更适合用SPIFFS或LittleFS这类日志型文件系统。

我个人更推荐LittleFS。为什么?

特性 SPIFFS LittleFS
磨损均衡 基本支持 更好
掉电恢复 一般 强
目录支持 不支持 支持
内存占用 较低 稍高
文件数量限制 有 几乎无

ESP-IDF里已经集成了LittleFS的组件。你只需要在 menuconfig 里配好分区表,然后调用 esp_vfs_littlefs_register() 挂载就行。

挂载之后,ESP32的VFS层会把LittleFS映射成一个路径。比如 /littlefs。然后你再写一个LVGL驱动,把 open_cb 里的路径拼上这个前缀。

void lv_fs_littlefs_init(void)
{
    lv_fs_drv_t drv;
    lv_fs_drv_init(&drv);

    drv.letter = 'L';
    drv.cache_size = 256;
    drv.open_cb = lfs_open_cb;
    drv.close_cb = lfs_close_cb;
    drv.read_cb = lfs_read_cb;
    drv.write_cb = lfs_write_cb;
    drv.seek_cb = lfs_seek_cb;
    drv.tell_cb = lfs_tell_cb;
    drv.size_cb = lfs_size_cb;

    lv_fs_drv_register(&drv);
}

static lv_fs_res_t lfs_open_cb(lv_fs_drv_t * drv, void * file_p, const char * path, lv_fs_mode_t mode)
{
    char full_path[128];
    snprintf(full_path, sizeof(full_path), "/littlefs%s", path);
    /* 调用ESP32的VFS接口打开文件 */
    ...
}
注意: ESP32的VFS和LVGL的FS是两层东西。VFS是操作系统层的,LVGL的FS是应用层的。你得在中间搭一座桥。说白了,LVGL的 open_cb 里调用 open() 或 fopen(),走的是ESP32的VFS。

3.5 注册自定义文件系统驱动

如果你用的存储介质比较特殊,比如外挂SPI Flash、U盘、甚至网络文件系统,那就得自己写驱动了。

流程很固定:

  1. 实现 lv_fs_drv_t 里的所有回调函数
  2. 调用 lv_fs_drv_init() 初始化驱动结构体
  3. 调用 lv_fs_drv_register() 注册到LVGL

注册之后,LVGL就能通过盘符找到你的驱动。上层代码完全无感。

举个例子,我做过一个项目,用W25Q64 SPI Flash存图片。底层用的是一个极简的FAT-like文件系统。注册的时候盘符用的 'Q'。应用层加载图片时写 "Q:/pic/logo.bin",LVGL自动就路由到我的驱动里了。

总结一下: LVGL的文件系统接口设计得足够灵活。你只要把回调函数填好,剩下的交给LVGL。不管是SD卡、Flash、还是网络,对LVGL来说都一样——就是一堆文件操作函数。
LVGL文件系统架构图 LVGL 应用层(lv_img / lv_fs) lv_fs 文件系统接口 驱动表 'S' (FatFS) 驱动表 'L' (LittleFS) 驱动表 'Q' (自定义) FatFS + SD卡驱动 LittleFS + SPI Flash驱动 自定义FS + W25Q64驱动 LVGL通过盘符字母路由到对应的驱动表 应用层写 "S:/pic.bin" 自动走FatFS,"L:/pic.bin" 走LittleFS

这张图把整个流程串起来了。你从顶层应用发一个文件操作请求,LVGL根据盘符找到对应的驱动表,驱动表再调用底层的真实文件系统。每一层各司其职,互不干扰。