LittleVGL (LVGL)干货入门教程三之LVGL的文件系统(fs)API对接。
前言:
阅读前,请确保你拥有以下条件:
- 你的项目已经完成“FatFS”的移植(例如你可以用FatFS进行SD卡的文件读写等)。
- 你已经完成“显示API”的移植。
LVGL有三大种需要对接的API
- 显示API(教程一已实现, 链接:LittleVGL (LVGL)入门教程一之移植到stm32芯片)
- 输入设备API(教程二已实现,链接:LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。)
- 文件系统API(如FatFS等,这篇文章实现)
这篇文章讲“文件系统API”的移植,默认你已经移植好了“显示API”。
(重要) 编译LVGL至少需要c99标准
目录:
一、为什么要为LVGL移植FatFS?
在笔者看来,LVGL中文件系统API是非常重要的东西,因为往往我们需要进行复杂的UI设计,你可以选择直接用LVGL内置的基本元素来进行设计,然而更明智的做法是从外部存储设备读取自绘的UI(比如不同的icons有不同的设计,例如“计算器UI”的图标明显和“设置UI”的图标不一样)。从外部存储设备读取自绘的UI的方法,可以设计出更美观优秀的交互界面。而你在LVGL中不可能直接使用FatFS的函数(会造成API的混乱,你的代码会非常难看,用一种说法就是:代码被污染了)。
二、什么地方用得上LVGL的文件系统API?
- 你可以像使用FatFS一样使用LVGL的文件系统API
- 在LVGL使用外部存储设备的资源时会被自动调用(此文第四章讲)。
三、lv_port_fs更改(重要)
(一)使能fs的port文件
- 在lv_port_fs文件中把“#if 0”改为“#if 1”,c文件和h文件都要改。
- 在lv_port_fs.h中添加声明void lv_port_fs_init(void);
(二)移植fs的port文件(重要)
在这个部分,我们需要干的事情会比较多。lvgl在port文件中给我们提供了很多文件系统的API,但其实重要的只有以下几个:
- fs_open(打开文件)
- fs_close(关闭文件)
- fs_read(读取文件)
- fs_write(写入文件)
- fs_seek(定位)
但是要注意的一点是,我们在使用时并不会直接调用这些函数,而是调用更高层的以“lv_fs_”为前缀的一系列函数。
以下的代码会比较长,我建议大家根据目录进行阅读。
(1)type的对接
我们打开lv_port_fs.c文件,根据注释找到“TYPEDEFS”。
// 这是原本的typedef,我们可以发现这个typedef几乎没有任何意义,只是个模板而已
// 你不改的话,也能编译,但程序跑起来,十有八九会奔溃
typedef struct {
/*Add the data you need to store about a file*/
uint32_t dummy1;
uint32_t dummy2;
}file_t;
/*Similarly to `file_t` create a type for directory reading too */
typedef struct {
/*Add the data you need to store about directory reading*/
uint32_t dummy1;
uint32_t dummy2;
}dir_t;
// 我们根据FatFS的变量类型对上面的代码进行更改
// 这里的更改比较灵活,你可以像上面一样定义一个你自己的结构体类型,里面放你的数据
// 当然FIL类型和DIR类型必须有
/*
* 第一种
*/
typedef FIL file_t; // 把FIL类型定义成file_t
typedef DIR dir_t; // 把DIR类型定义成dir_t
/*
* 第二种,比较灵活,但效率明显会低,初始化会比较麻烦
*/
typedef struct {
FIL file;
uint32_t user_data;
} file_t;
typedef struct {
DIR dir;
uint32_t user_data;
}dir_t;
以上的修改必须完成,不然无法使用。
(2)初始化函数
我们打开lv_port_fs.c文件,根据注释找到“GLOBAL FUNCTIONS”。里面开头有两个函数:
- lv_port_fs_init (初始化你的设备,并将对应的接口注册到LVGL里)
- fs_init (初始化你的设备)
参照下面改法或自行修改:
void lv_port_fs_init(void)
{
/*----------------------------------------------------
* Initialize your storage device and File System
* -------------------------------------------------*/
/* 这个函数实现在下面,里头是空的,由你自己实现 */
fs_init();
/*---------------------------------------------------
* Register the file system interface in LVGL
*--------------------------------------------------*/
/* Add a simple drive to open images */
lv_fs_drv_t fs_drv;
/* 上面这里默认只有一个fs_drv,如果我有多个存储设备的话,我们可以这样 */
// lv_fs_drv_t fs_drv[FF_VOLUMES]; // FF_VOLUMES 定义在fatfs的ffconf.h文件中
lv_fs_drv_init(&fs_drv);
/*Set up fields...*/
// ↓ 这里的file_t的size会被LVGL使用文件系统时调用进行内存分配,所以你上面一定要改好
fs_drv.file_size = sizeof(file_t);
// ↓ 这里letter是比较重要的一个东西,比如调用SD卡时,我们直接“S:/xxx/xxx”即可
// 如果我还用SPI Flash的话,这里的letter也可以写成F,避免和S冲突
fs_drv.letter = 'S';
/* 下面就是各个接口的注册 */
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.free_space_cb = fs_free; // 获取设备剩余空间和总空间
fs_drv.size_cb = fs_size; // 获取文件大小
fs_drv.remove_cb = fs_remove; // 删除文件
fs_drv.rename_cb = fs_rename; // 重命名
fs_drv.trunc_cb = fs_trunc; // 截取文件
/*这个预处理是我写的,你可以使用LV_USE_USER_DATA宏打开这个使用(在lv_conf.h中)*/
/* 后面我会讲这个东西怎么用,这里了解一下这个东西即可 */
#if LV_USE_USER_DATA == 1
fs_drv.user_data = xxx;
#endif
/* 这里和上面差不多,这个是文件夹的操作句柄,不过LVGL貌似没什么地方会用到这个 */
fs_drv.rddir_size = sizeof(dir_t);
fs_drv.dir_close_cb = fs_dir_close;
fs_drv.dir_open_cb = fs_dir_open;
fs_drv.dir_read_cb = fs_dir_read;
/* 注册设备 */
lv_fs_drv_register(&fs_drv);
}
/* Initialize your Storage device and File system. */
static void fs_init(void)
{
/*E.g. for FatFS initalize the SD card and FatFS itself*/
/*You code here*/
/* 这里是文件系统和你存储设备初始化的地方,你所有的设备最好别在别的地方初始化了 */
/* 到这里都会进行初始化和挂载 */
FATFS * fs = (FATFS*)lv_mem_alloc(sizeof(FATFS));
FRESULT fres = FR_NOT_READY;
/* 你设备的初始化 */
fatfs_dev_init();
/* 挂载设备 */
if (fres != FR_OK) {
/* 这里的驱动号我直接使用字符串“SD:”,在ffconf.h文件中的FF_VOLUME_STRS定义 */
fres = f_mount(fs, "SD:", 1);
if (fres != FR_OK) {
DEBUG_PRINT("SD Card mounted error. (%d)\n", fres );
} else
DEBUG_PRINT("SD Card mounted successfully.\n\n");
}
lv_mem_free(fs);
}
(3)fs_open函数
参照下面改法或自行修改:
/**
* Open a file
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable
* @param path path to the file beginning with the driver letter (e.g. S:/folder/file.txt)
* @param mode read: FS_MODE_RD, write: FS_MODE_WR, both: FS_MODE_RD | FS_MODE_WR
* @return LV_FS_RES_OK or any error from lv_fs_res_t enum
*/
static lv_fs_res_t fs_open (
lv_fs_drv_t * drv,
void * file_p,
const char * path,
lv_fs_mode_t mode
)
{
lv_fs_res_t res = LV_FS_RES_NOT_IMP;
/************************************************
* 在这里,我们需要先进行设备的判断,因为我们实际上
* 可能不只有一个设备,而且LVGL里使用letter(即单个字符)
* 作为驱动号
* 而在FatFS里使用VOLUME_STR(字符串)或者一个字节的pdrv
* 驱动号,使用前要进行转换。
*
* 有一种方便的方法,就是上面提到的user_data,我们可以把
* VOLUME_STR提前装入user_data,那我们的程序进行判断时
* 耦合度会更低,在更多设备时,这种方法会更方便
*************************************************/
char *path_buf = NULL;
uint8_t opt_mode = 0;
uint16_t path_len = strlen(path);
// 根据传入的letter判断是什么存储设备
switch (drv->letter) {
case 'S': // SD card
path_buf = (char *)lv_mem_alloc(sizeof(char) * (path_len + 4));
sprintf(path_buf, "SD:/%s", path);
break;
case 'F': // SPI FALSH
path_buf = (char *)lv_mem_alloc(sizeof(char) * (path_len + 6));
sprintf(path_buf, "SPIF:/%s", path);
break;
default:
printf("No drive %c\n", drv->letter);
return LV_FS_RES_NOT_EX;
}
/* 文件操作方法,将FatFS的转换成LVGL的操作方法 */
if(mode == LV_FS_MODE_WR) {
opt_mode = FA_OPEN_ALWAYS | FA_WRITE;
} else if(mode == LV_FS_MODE_RD) {
opt_mode = FA_OPEN_EXISTING | FA_READ;
} else if(mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) {
opt_mode = FA_WRITE | FA_READ;
}
/* 调用FatFs的函数 */
FRESULT fres = f_open((FIL*)file_p, path_buf, opt_mode);
if (fres != FR_OK) {
printf("f_open error (%d)\n", fres);
res = LV_FS_RES_NOT_IMP;
} else
res = LV_FS_RES_OK;
lv_mem_free(path_buf);
return res;
}
(4)fs_close函数
参照下面改法或自行修改:
/**
* Close an opened file
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable. (opened with lv_ufs_open)
* @return LV_FS_RES_OK: no error, the file is read
* any error from lv_fs_res_t enum
*/
static lv_fs_res_t fs_close (lv_fs_drv_t * drv, void * file_p)
{
if (f_close((FIL*)file_p) != FR_OK)
return LV_FS_RES_NOT_IMP;
else
return LV_FS_RES_OK;
}
(5)fs_read函数
参照下面改法或自行修改:
/**
* Read data from an opened file
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable.
* @param buf pointer to a memory block where to store the read data
* @param btr number of Bytes To Read
* @param br the real number of read bytes (Byte Read)
* @return LV_FS_RES_OK: no error, the file is read
* any error from lv_fs_res_t enum
*/
static lv_fs_res_t fs_read(
lv_fs_drv_t* drv,
void* file_p,
void* buf,
uint32_t btr,
uint32_t* br
)
{
FRESULT fres = f_read((FIL*)file_p, buf, btr, br);
if (fres != FR_OK) {
printf("f_read error (%d)\n", fres);
return LV_FS_RES_NOT_IMP;
} else
return LV_FS_RES_OK;
}
(6)fs_write函数
参照下面改法或自行修改:
/**
* Write into a file
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable
* @param buf pointer to a buffer with the bytes to write
* @param btr Bytes To Write
* @param br the number of real written bytes (Bytes Written). NULL if unused.
* @return LV_FS_RES_OK or any error from lv_fs_res_t enum
*/
static lv_fs_res_t fs_write(
lv_fs_drv_t* drv,
void* file_p,
const void* buf,
uint32_t btw,
uint32_t* bw
)
{
/* Add your code here*/
FRESULT fres = f_write((FIL*)file_p, buf, btw, bw);
if (fres != FR_OK) {
printf("f_read error (%d)\n", fres);
return LV_FS_RES_NOT_IMP;
} else
return LV_FS_RES_OK;
}
(7)fs_seek函数
参照下面改法或自行修改:
/**
* Set the read write pointer. Also expand the file size if necessary.
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable. (opened with lv_ufs_open )
* @param pos the new position of read write pointer
* @return LV_FS_RES_OK: no error, the file is read
* any error from lv_fs_res_t enum
*/
static lv_fs_res_t fs_seek(lv_fs_drv_t* drv, void* file_p, uint32_t pos)
{
/* Add your code here*/
FRESULT fres = f_lseek((FIL*)file_p, pos);
if (fres != FR_OK) {
printf("f_lseek error (%d)\n", fres);
return LV_FS_RES_NOT_IMP;
} else
return LV_FS_RES_OK;
}
(三)lv_port_fs修改结束
那么到这里,你完成了最基本也是必须的API对接,剩下的API你可以不管,除非你用的上。
四、LVGL如何使用外部资源?
在LVGL中,我们会调用外部存储设备的图像资源,我们使用一个小例程来展示如何使用外部资源。
LVGL的启动方法参考我的文章:LittleVGL (LVGL)入门教程一之移植到stm32芯片
这里涉及一个问题,即如何将图片格式(如JPG、PNG、GIF等)转换成LVGL可以识别的格式(内部数组、外部bin文件)的问题。有以下几种方法:
- 使用解码器(自己写或者使用LVGL官方GitHub提供的解码器)
- 使用官网的在线图片转换(常用)
- 使用第三方大牛的离线转换工具(常用)
工具的使用等不在此文讲,后面出文章讲。
#include <lvgl.h>
#define LVGL_TICK 5
/**
* 在LVGL启动运行后,会自动读取图片文件,你只要设置图片路径即可
*/
static void my_lvgl_test(void)
{
/* 创建img btn */
lv_obj_t * imgbtn = lv_imgbtn_create( lv_scr_act(), NULL );
/* 设置imgbtn在被按下时显示所调用的图片 */
lv_imgbtn_set_src( imgbtn, LV_BTN_STATE_PRESSED, "S:/icon_press.bin" );
/* 设置imgbtn在被释放时显示所调用的图片 */
lv_imgbtn_set_src( imgbtn, LV_BTN_STATE_RELEASED, "S:/icon_release.bin" );
/* 居中对齐 */
lv_obj_align( imgbtn, NULL, LV_ALIGN_CENTER, 0, 0 );
}
static void my_lvgl_fs_test(void)
{
lv_fs_file_t lv_file;
lv_fs_res_t lv_res;
lv_res = lv_fs_open( &lv_file, "S:/xxx/xxx", LV_FS_MODE_RD );
if ( lv_res != LV_FS_RES_OK ) {
printf( "LVGL FS open error. (%d)\n", lv_res );
} else
printf( "LVGL FS open Ok\n" );
lv_fs_close(&lv_file);
}
static void lvgl_init( void )
{
lv_init();
lv_port_disp_init(); // 显示器初始化
lv_port_indev_init(); // 输入设备初始化
lv_port_fs_init(); // 文件系统设备初始化
}
int main(void)
{
lvgl_init();
my_lvgl_fs_test();
my_lvgl_test();
while(1) {
// 先调用 lv_tick_inc 再调用 lv_task_handler
lv_tick_inc(LVGL_TICK);
lv_task_handler();
delay_ms(LVGL_TICK);
}
}
五、启动LVGL
参考我的第一篇文章即可:LittleVGL (LVGL)干货入门教程一之移植到stm32芯片
本篇完
其他:
LittleVGL (LVGL)干货入门教程一之移植到stm32芯片
LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。
LittleVGL (LVGL)干货入门教程三之LVGL的文件系统(fs)API对接。
LittleVGL (LVGL)干货入门教程四之制作和使用中文汉字字库
个人Github页:https://github.com/Trisuborn