3、LVGL文件系统接口:lv_fs接口注册与FATFS文件系统移植
好,咱们进入第三章。这一章讲的是LVGL怎么跟外部存储“对话”。说白了,就是让LVGL能认识你的SD卡、U盘或者Flash上的文件。
你想想看,LVGL本身只是一个图形库,它没有文件系统的能力。你要从SD卡里读一张BMP图片,LVGL得先知道怎么打开那个文件、怎么读取数据。这个桥梁,就是lv_fs接口。
核心概念:lv_fs是LVGL定义的一套抽象文件系统接口。它不关心底层是FATFS、LittleFS还是什么别的,只要你的驱动实现了这些接口,LVGL就能用。
3.1 lv_fs接口注册流程
我个人习惯把lv_fs的注册分成三步走。每一步都不复杂,但顺序错了就容易出问题。
- 定义驱动结构体:填充一个
lv_fs_drv_t结构体,告诉LVGL你的文件系统叫什么名字、支持哪些操作。 - 实现回调函数:把open、close、read、write、seek、tell这些函数指针,指向你真正干活的函数。
- 注册驱动:调用
lv_fs_drv_register(),把上面准备好的结构体注册到LVGL内部。
来看一个典型的注册代码。我一般把它放在 lv_port_fs.c 这个文件里,方便管理。
static lv_fs_drv_t fs_drv; /* 驱动结构体 */
void lv_port_fs_init(void)
{
lv_fs_drv_init(&fs_drv); /* 初始化结构体 */
fs_drv.letter = 'S'; /* 盘符:S: */
fs_drv.cache_size = 256; /* 缓存大小,我习惯设256字节 */
/* 挂载回调函数 */
fs_drv.ready_cb = fs_ready_cb;
fs_drv.open_cb = fs_open_cb;
fs_drv.close_cb = fs_close_cb;
fs_drv.read_cb = fs_read_cb;
fs_drv.write_cb = fs_write_cb;
fs_drv.seek_cb = fs_seek_cb;
fs_drv.tell_cb = fs_tell_cb;
fs_drv.dir_open_cb = fs_dir_open_cb;
fs_drv.dir_read_cb = fs_dir_read_cb;
fs_drv.dir_close_cb = fs_dir_close_cb;
lv_fs_drv_register(&fs_drv); /* 注册驱动 */
}
小提示:盘符字母可以随便选,但建议用大写字母。我见过有人用小写 's',结果LVGL内部大小写敏感,死活打不开文件。嗯,踩过这个坑。
3.2 FATFS文件系统移植要点
FATFS是嵌入式领域最常用的文件系统之一。它轻量、稳定,而且跟LVGL配合得很好。移植FATFS本身不是本章重点,但有几个关键点我必须提一下。
| FATFS配置项 | 推荐值 | 说明 |
|---|---|---|
FF_USE_STRFUNC |
2 | 支持长文件名,LVGL路径可能包含中文 |
FF_USE_LFN |
3 | 启用长文件名,堆上分配缓冲区 |
FF_VOLUMES |
2 | 至少2个卷:SD卡和Flash |
FF_FS_RPATH |
1 | 启用相对路径,方便切换目录 |
我曾经在一个项目里忘了开长文件名支持,结果LVGL加载图片时路径被截断,图片显示不出来。查了两天才找到原因。所以,长文件名一定要开。
3.3 回调函数实现:把FATFS挂到LVGL上
这是最核心的部分。每个回调函数都要把LVGL的参数转换成FATFS能理解的参数。我挑几个重点讲。
3.3.1 open_cb:打开文件
static void * fs_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
FIL * file = lv_mem_alloc(sizeof(FIL));
if(file == NULL) return NULL;
BYTE fatfs_mode;
if(mode == LV_FS_MODE_WR) fatfs_mode = FA_WRITE | FA_CREATE_ALWAYS;
else if(mode == LV_FS_MODE_RD) fatfs_mode = FA_READ;
else if(mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) fatfs_mode = FA_READ | FA_WRITE;
FRESULT res = f_open(file, path, fatfs_mode);
if(res != FR_OK) {
lv_mem_free(file);
return NULL;
}
return file;
}
注意这里:路径要去掉盘符。LVGL传进来的路径是 S:/images/bg.bmp,但FATFS的 f_open 需要的是 /images/bg.bmp。所以你在回调里要跳过第一个字符(盘符)和冒号。
警告:千万不要在回调里直接使用 path + 2 来跳过盘符。如果路径格式变了(比如没有冒号),你的程序会直接崩溃。建议写一个安全解析函数。
3.3.2 read_cb:读取数据
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 * file = (FIL *)file_p;
UINT bytes_read;
FRESULT res = f_read(file, buf, btr, &bytes_read);
*br = bytes_read;
return (res == FR_OK) ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN;
}
这个函数很简单,就是把FATFS的 f_read 包装一下。但有一个细节:btr和br的类型。LVGL用的是 uint32_t,FATFS用的是 UINT。在32位平台上没问题,但在某些16位平台上,UINT 可能是16位的,会有截断风险。我建议在移植时统一用 uint32_t 做强制转换。
3.4 知识结构图:lv_fs与FATFS的关系
下面这张图展示了LVGL、lv_fs接口、FATFS和底层硬件之间的关系。你看一眼就能明白整个数据流向。
3.5 路径处理:一个容易忽略的细节
LVGL的文件路径格式是 盘符:路径,比如 S:/images/bg.bmp。但FATFS不认识这个格式。所以你在回调函数里必须做路径转换。
我一般这样处理:
static const char * skip_drive_letter(const char * path)
{
/* 跳过盘符和冒号,例如 "S:/abc" -> "/abc" */
if(path[0] != '\0' && path[1] == ':') {
return path + 2;
}
return path; /* 没有盘符,直接返回 */
}
然后在每个回调里调用它:
static void * fs_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
const char * real_path = skip_drive_letter(path);
/* 然后用 real_path 调用 f_open */
}
经验之谈:如果你同时挂载了多个存储设备(比如SD卡和Flash),每个设备用不同的盘符。这样LVGL可以通过路径的第一个字符就知道去哪个设备找文件。我习惯用 S: 表示SD卡,F: 表示Flash。
3.6 测试你的文件系统移植
移植完成后,别急着跑LVGL的图片加载。先写一个简单的测试函数,验证文件系统能不能正常工作。
void test_fs_port(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) {
printf("文件打开失败: %d\n", res);
return;
}
/* 读取内容 */
char buf[128];
uint32_t bytes_read;
res = lv_fs_read(&file, buf, sizeof(buf), &bytes_read);
if(res == LV_FS_RES_OK) {
printf("读取 %d 字节: %s\n", bytes_read, buf);
}
/* 关闭文件 */
lv_fs_close(&file);
}
这个测试通过了,再去做图片加载。否则出了问题,你很难分清是文件系统的问题还是图片解码的问题。
注意:测试文件 test.txt 要提前放到SD卡根目录。我第一次测试时忘了放文件,结果一直报错,还以为是移植代码写错了。嗯,这种低级错误谁都会犯。
3.7 常见问题与排查思路
移植过程中最容易遇到这几个问题:
- 文件打不开:检查盘符是否正确,路径格式是否对,FATFS有没有初始化。
- 读取数据为空:检查文件指针是否正确,缓存区有没有分配,FATFS的
f_read返回值。 - LVGL报错
LV_FS_RES_FS_ERR:通常是回调函数返回了错误码,检查每个回调的返回值。 - 中文路径乱码:确认
FF_USE_LFN已经开启,并且编码格式一致。
我曾经在一个项目里遇到诡异的问题:文件能打开,但读取的数据全是0。查了两天,最后发现是 f_read 的第三个参数 btr 传成了指针地址,而不是值。这种低级错误,写代码时真的要多看一眼。
好了,这一章的内容就到这里。lv_fs接口注册和FATFS移植是LVGL外部存储的基础,基础打牢了,后面加载图片、字体、动画文件都会很顺畅。