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只管调用。
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通过这个字母找到对应的驱动表。
3.3 FatFS在STM32上的移植
STM32上最常用的文件系统就是FatFS。它轻量、稳定,支持FAT12/16/32。
移植步骤其实不复杂,我拆成几步说:
- 下载FatFS源码 —— 从ELM官网拿,或者用STM32CubeMX生成的版本
- 实现底层接口 —— 主要是
disk_read()、disk_write()、disk_ioctl()、get_fattime() - 配置 ffconf.h —— 根据你的需求打开/关闭功能
- 注册到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接口打开文件 */
...
}
open_cb 里调用 open() 或 fopen(),走的是ESP32的VFS。
3.5 注册自定义文件系统驱动
如果你用的存储介质比较特殊,比如外挂SPI Flash、U盘、甚至网络文件系统,那就得自己写驱动了。
流程很固定:
- 实现
lv_fs_drv_t里的所有回调函数 - 调用
lv_fs_drv_init()初始化驱动结构体 - 调用
lv_fs_drv_register()注册到LVGL
注册之后,LVGL就能通过盘符找到你的驱动。上层代码完全无感。
举个例子,我做过一个项目,用W25Q64 SPI Flash存图片。底层用的是一个极简的FAT-like文件系统。注册的时候盘符用的 'Q'。应用层加载图片时写 "Q:/pic/logo.bin",LVGL自动就路由到我的驱动里了。
这张图把整个流程串起来了。你从顶层应用发一个文件操作请求,LVGL根据盘符找到对应的驱动表,驱动表再调用底层的真实文件系统。每一层各司其职,互不干扰。