2、LVGL文件系统接口:lv_fs驱动注册、文件打开/读取/关闭回调函数实现
好,咱们进入第二章。上一章我们把外部存储的硬件接口打通了,但LVGL怎么知道你的SD卡、Flash或者U盘里存了什么东西?它需要一个桥梁——文件系统接口。
说白了,LVGL本身不直接操作硬件扇区,它只认lv_fs这套抽象层。你给它注册几个回调函数,它就能像操作本地文件一样,去外部存储里读图片、读字体。我最早做这个的时候,以为只要把文件系统挂载上就完事了,结果LVGL死活打不开图片,后来才发现——回调函数没注册全。
2.1 lv_fs驱动注册:让LVGL认识你的存储
先看注册入口。LVGL里每个文件系统驱动都是一个lv_fs_drv_t结构体。你需要填充它,然后调用lv_fs_drv_register。
lv_fs_drv_t fs_drv;
lv_fs_drv_init(&fs_drv); // 初始化驱动结构体
fs_drv.letter = 'S'; // 盘符,比如 "S:/image.bin"
fs_drv.ready_cb = fs_ready_cb; // 检测存储是否就绪
fs_drv.open_cb = fs_open_cb; // 打开文件
fs_drv.read_cb = fs_read_cb; // 读取数据
fs_drv.close_cb = fs_close_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); // 注册到LVGL内核
这里有个关键点——盘符。你注册时指定的letter,就是LVGL访问这个存储的路径前缀。比如你设成'S',那LVGL里所有以S:开头的路径,都会走你这套回调。
经验之谈:我习惯把SD卡映射成'S',Flash映射成'F',U盘映射成'U'。这样代码里一看路径就知道数据存在哪,调试时特别方便。
2.2 回调函数实现:打开、读取、关闭
注册只是第一步。真正干活的是那三个核心回调:open_cb、read_cb、close_cb。LVGL每次加载图片,都会依次调用它们。
2.2.1 文件打开回调:open_cb
这个回调负责把LVGL传过来的路径,转换成底层文件系统的文件句柄。注意,LVGL传的是带盘符的完整路径,比如S:/images/logo.bin。你需要把盘符去掉,再传给底层API。
static void * fs_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
// 去掉盘符 "S:",得到 "/images/logo.bin"
const char * real_path = path + 2;
// 根据mode决定打开方式
uint8_t flags;
if (mode == LV_FS_MODE_RD)
flags = FA_READ;
else if (mode == LV_FS_MODE_WR)
flags = FA_WRITE;
else if (mode == (LV_FS_MODE_RD | LV_FS_MODE_WR))
flags = FA_READ | FA_WRITE;
else
return NULL;
FIL * file = lv_mem_alloc(sizeof(FIL));
if (file == NULL) return NULL;
FRESULT res = f_open(file, real_path, flags);
if (res != FR_OK) {
lv_mem_free(file);
return NULL;
}
return (void *)file; // 返回文件句柄
}
这里有个坑——内存分配。文件句柄必须用lv_mem_alloc分配,不能用malloc。因为LVGL有自己的内存管理,混用会导致内存碎片甚至崩溃。我曾经在这个问题上折腾了一下午,最后发现是malloc和lv_mem_alloc混用导致的。
2.2.2 文件读取回调:read_cb
LVGL读取图片数据时,会反复调用这个回调。它每次要读多少字节,你就得给它多少。注意,一定要检查实际读取的字节数,不能假设底层一定读满了。
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);
if (res != FR_OK) {
*br = 0;
return LV_FS_RES_UNKNOWN;
}
*br = bytes_read; // 实际读取的字节数
return LV_FS_RES_OK;
}
注意:btr是请求读取的字节数,br是实际读取的字节数。如果文件读到末尾,br可能小于btr。LVGL会根据br判断是否读完,所以这个值必须正确返回。
2.2.3 文件关闭回调:close_cb
这个回调相对简单,但容易忘——释放内存。文件句柄是你在open_cb里分配的,必须在close_cb里释放,否则内存泄漏。
static lv_fs_res_t fs_close_cb(lv_fs_drv_t * drv, void * file_p)
{
FIL * file = (FIL *)file_p;
f_close(file); // 关闭底层文件
lv_mem_free(file); // 释放内存
return LV_FS_RES_OK;
}
嗯,这里要特别提醒:先关闭底层文件,再释放内存。顺序反了的话,你释放了内存再去关文件,底层API可能操作野指针,轻则报错,重则死机。
2.3 完整回调函数表
我把常用的回调函数整理成了一张表,方便你对照检查。实际项目中,至少要实现加粗的那几个。
| 回调函数 | 必须实现? | 作用 | 常见坑点 |
|---|---|---|---|
| ready_cb | 是 | 检测存储设备是否就绪 | SD卡未插入时返回0,否则LVGL会卡死 |
| open_cb | 是 | 打开文件,返回句柄 | 路径要去掉盘符;句柄用lv_mem_alloc分配 |
| read_cb | 是 | 读取文件数据 | 必须返回实际读取字节数 |
| close_cb | 是 | 关闭文件,释放资源 | 先关文件,再释放内存 |
| seek_cb | 推荐 | 定位文件读写指针 | LVGL解码图片时需要随机访问 |
| tell_cb | 推荐 | 获取当前指针位置 | 配合seek_cb使用 |
| dir_open_cb | 按需 | 打开目录 | 文件浏览器功能需要 |
| dir_read_cb | 按需 | 读取目录项 | 同上 |
| dir_close_cb | 按需 | 关闭目录 | 同上 |
2.4 知识结构图:lv_fs驱动注册与回调流程
下面这张图展示了从注册到文件操作的整体流程。你可以看到,LVGL应用层通过盘符找到驱动,然后驱动回调与底层文件系统交互。
调试小技巧:如果你注册完驱动后,LVGL还是报LV_FS_RES_NOT_IMP,说明某个回调函数没实现或者返回了错误码。我一般会在每个回调入口加一个printf打印路径和参数,这样一眼就能看出是哪个环节出了问题。
2.5 避坑指南:我踩过的三个坑
最后分享几个实际项目中容易踩的坑,都是我亲身经历过的。
- 路径格式不一致:LVGL传过来的路径是
S:/dir/file.bin,但FatFS期望的是/dir/file.bin。别忘了去掉盘符和冒号。 - 文件句柄生命周期:LVGL可能在多个地方同时打开同一个文件(比如图片解码和字体加载)。确保你的
open_cb每次返回独立的句柄,不要复用。 - 缓冲区对齐:有些底层存储驱动(比如SDIO)要求读取缓冲区4字节对齐。如果LVGL传下来的buf不对齐,读出来的数据可能是错的。我遇到过图片花屏,查了两天才发现是缓冲区对齐问题。