目录
系列文章目录
文章目录
前言
一、准备工作
二、移植文件系统
1.创建文件系统组件
2.底层代码的实现
1.修改lv_port_fs.h
2. 修改lv_port_fs.c
3.CMakeLists.txt 的实现
三、演示
1.图片准备
2.工程修改
2.1 使用自定义分区表
2.2 将图片文件加入工程
2.3 修改CMakeLists.txt
2.4 修改 mian.c
2.5 实际效果
四、其它
总结
前言
由于lvgl的交互特性,从外部存储器读写数据是非常常见的操作,比如从sd卡中读取图片数据给ui做显示,又或者是将一些图像数据保存到sd卡等。本文是基于lvgl v8.3版本将文件系统移植到ESP32S3的一些踩坑记录。
另外,由于手上的开发板没有外部sd卡硬件,本文便先使用ESP32S3内部flash的文件系统进行操作,后续有机会的话也会补上sd卡相关部分。
,
一、准备工作
在移植之前,笔者也在网上找过一些经验帖,很多都说需要引入lv_fs_if库,但是后来发现8.3版本的lvgl已经合并了这个库,并不需要再单独引入,只需要按照之前外部按键输入的移植方式操作即可。
芯片平台:ESP32S3
LVGL版本:V8.3
ESP-IDF版本:v4.4
1.已完成屏幕显示功能
2.已完成lvgl库移植到esp32s3
二、移植文件系统
1.创建文件系统组件
在工程的components/lvgl/examples/porting/文件夹下找到 lv_port_fs_template.c 和lv_port_fs_template.h 两个文件,将其复制拷贝到新的 lv_port_fs 组件文件夹下并改名为 lv_port_fs.c 和 lv_port_fs.h,并创建CMakeLists.txt 文件。结构如下:
2.底层代码的实现
1.修改lv_port_fs.h
lv_port_fs.h 需要修改的不多,只需要把#if 0 改为 #if 1,同时自定义一个文件路径 LV_FS_PATH 即可。
/**
* @file lv_port_fs_templ.h
*
*/
/*Copy this file as "lv_port_fs.h" and set this value to "1" to enable content*/
#if 1
#ifndef LV_PORT_FS_H
#define LV_PORT_FS_H
#ifdef __cplusplus
extern "C" {
#endif
/*********************
* INCLUDES
*********************/
#include "lvgl/lvgl.h"
/*********************
* DEFINES
*********************/
#define LV_FS_PATH "/storage" /*Projet root*/
/**********************
* TYPEDEFS
**********************/
/**********************
* GLOBAL PROTOTYPES
**********************/
void lv_port_fs_init(void);
/**********************
* MACROS
**********************/
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /*LV_PORT_FS_TEMPL_H*/
#endif /*Disable/Enable content*/
2. 修改lv_port_fs.c
lv_port_fs.c 文件中最主要的是实现fs_init()函数,这是底层硬件驱动的接口,这里参考了ESP-IDF中example/storage下的示例。
其次则是读写接口函数,这里实现了fs_open、fs_close、fs_read、fs_write、fs_seek、fs_tell等几个函数。这些函数的实现可以直接参考 lvgl/src/extra/libs/fsdrv/lv_fs_stdio.c 的实现。
驱动器接口关联字符这里我们使用的是内部flash,则将其与字符‘F’相关联,如果使用的是SD卡则与字符‘S’相关联。
/**
* @file lv_port_fs_templ.c
*
*/
/*Copy this file as "lv_port_fs.c" and set this value to "1" to enable content*/
#if 1
/*********************
* INCLUDES
*********************/
#include "lv_port_fs.h"
#include "lvgl.h"
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include "esp_system.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "esp_err.h"
/*********************
* DEFINES
*********************/
#define TAG "lv_fs" // log输出的标签
/**********************
* TYPEDEFS
**********************/
typedef FILE *file_t;
typedef DIR *dir_t;
/**********************
* STATIC PROTOTYPES
**********************/
static void fs_init(void);
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close(lv_fs_drv_t * drv, void * file_p);
static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br);
static lv_fs_res_t fs_write(lv_fs_drv_t * drv, void * file_p, const void * buf, uint32_t btw, uint32_t * bw);
static lv_fs_res_t fs_seek(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_size(lv_fs_drv_t * drv, void * file_p, uint32_t * size_p);
static lv_fs_res_t fs_tell(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p);
static void * fs_dir_open(lv_fs_drv_t * drv, const char * path);
static lv_fs_res_t fs_dir_read(lv_fs_drv_t * drv, void * rddir_p, char * fn);
static lv_fs_res_t fs_dir_close(lv_fs_drv_t * drv, void * rddir_p);
/**********************
* STATIC VARIABLES
**********************/
/**********************
* GLOBAL PROTOTYPES
**********************/
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
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*/
static lv_fs_drv_t fs_drv;
lv_fs_drv_init(&fs_drv);
/*Set up fields...*/
fs_drv.letter = 'F';
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.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);
}
/**********************
* STATIC FUNCTIONS
**********************/
/*Initialize your Storage device and File system.*/
static void fs_init(void)
{
esp_vfs_spiffs_conf_t conf = {
.base_path = LV_FS_PATH,
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = true};
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
ESP_LOGE(TAG, "Failed to mount or format filesystem");
}
else if (ret == ESP_ERR_NOT_FOUND)
{
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
}
else
{
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
}
size_t total = 0, used = 0;
ret = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
}
else
{
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
}
/**
* Open a file
* @param drv pointer to a driver where this function belongs
* @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 a file descriptor or NULL on error
*/
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
void * f = NULL;
if(mode == LV_FS_MODE_WR)
{
f ="wb";
}
else if(mode == LV_FS_MODE_RD)
{
f ="rb";
}
else if(mode == (LV_FS_MODE_WR | LV_FS_MODE_RD))
{
f ="rb+";
}
char filepath[256]={0};
sprintf(filepath, LV_FS_PATH "/%s",path);
return fopen(filepath, f);
}
/**
* 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 fs_open)
* @return LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
*/
static lv_fs_res_t fs_close(lv_fs_drv_t * drv, void * file_p)
{
LV_UNUSED(drv);
fclose(file_p);
return LV_FS_RES_OK;
}
/**
* 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 or 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)
{
LV_UNUSED(drv);
*br = fread(buf, 1, btr, file_p);
return (int32_t)(*br) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}
/**
* 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 btw Bytes To Write
* @param bw the number of real written bytes (Bytes Written). NULL if unused.
* @return LV_FS_RES_OK: no error 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)
{
lv_fs_res_t res = LV_FS_RES_OK;
/*Add your code here*/
LV_UNUSED(drv);
*bw = fwrite(buf, 1, btw, file_p);
return (int32_t)(*bw) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}
/**
* 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 fs_open )
* @param pos the new position of read write pointer
* @param whence tells from where to interpret the `pos`. See @lv_fs_whence_t
* @return LV_FS_RES_OK: no error or 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, lv_fs_whence_t whence)
{
lv_fs_res_t res = LV_FS_RES_OK;
/*Add your code here*/
switch (whence)
{
case LV_FS_SEEK_SET:
{
LV_UNUSED(drv);
fseek(file_p, pos, SEEK_SET);
}
default:
break;
}
return res;
}
/**
* Give the position of the read write pointer
* @param drv pointer to a driver where this function belongs
* @param file_p pointer to a file_t variable.
* @param pos_p pointer to to store the result
* @return LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
*/
static lv_fs_res_t fs_tell(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p)
{
LV_UNUSED(drv);
*pos_p = ftell(file_p);
return LV_FS_RES_OK;
}
/**
* Initialize a 'lv_fs_dir_t' variable for directory reading
* @param drv pointer to a driver where this function belongs
* @param path path to a directory
* @return pointer to the directory read descriptor or NULL on error
*/
static void * fs_dir_open(lv_fs_drv_t * drv, const char * path)
{
lv_fs_res_t res = LV_FS_RES_NOT_IMP;
/*Add your code here*/
return res;
}
/**
* Read the next filename form a directory.
* The name of the directories will begin with '/'
* @param drv pointer to a driver where this function belongs
* @param rddir_p pointer to an initialized 'lv_fs_dir_t' variable
* @param fn pointer to a buffer to store the filename
* @return LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
*/
static lv_fs_res_t fs_dir_read(lv_fs_drv_t * drv, void * rddir_p, char * fn)
{
lv_fs_res_t res = LV_FS_RES_NOT_IMP;
/*Add your code here*/
return res;
}
/**
* Close the directory reading
* @param drv pointer to a driver where this function belongs
* @param rddir_p pointer to an initialized 'lv_fs_dir_t' variable
* @return LV_FS_RES_OK: no error or any error from @lv_fs_res_t enum
*/
static lv_fs_res_t fs_dir_close(lv_fs_drv_t * drv, void * rddir_p)
{
lv_fs_res_t res = LV_FS_RES_NOT_IMP;
/*Add your code here*/
return res;
}
#else /*Enable this file at the top*/
/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif
3.CMakeLists.txt 的实现
CMakeLists.txt 这里引用lvgl 以及spiffs组件。
file(GLOB_RECURSE srcs *.c)
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS .
REQUIRES lvgl spiffs
)
到了这一步我们的lvgl 文件系统差不多就可以在esp32s3上跑起来了,下面我们试下效果。
三、演示
这里我们通过文件系统从esp32s3内部flash读取一张图片并通过img控件显示到屏幕上。
1.图片准备
准备一张jpg或png图片并将其调整到合适大小(不超过屏幕),使用lvgl官网提供的图片取模工具取模为二进制文件。
这里我们颜色格式选择CF_TRUE_COLOR,输出格式选择Binary RGB565,如果后面发现图片输出的颜色不对,也可以尝试使用Binary RGB565 Swap 。
2.工程修改
因为这里我们使用的是esp32s3内部的flash来挂载文件系统,所以需要对我们的工程做一定的修改。
2.1 使用自定义分区表
从其它工程中拷贝一份partitions.csv 文件到我们工程的main 文件夹下,并进行如下修改。
增加storage分区,分区大小依据esp32s3的flash大小进行修改;同时在menuconfig里使用自定义分区表。
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
factory, app, factory, 0x10000, 2M,
storage, data, spiffs, , 2M,
2.2 将图片文件加入工程
在工程的main文件夹下新建文件夹storage,并将图片取模后的bin文件放入该文件夹。
2.3 修改CMakeLists.txt
修改main文件夹下的CMakeLists.txt文件以创建相应的系统分区。
spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT)
2.4 修改 mian.c
在 lv_init()之后初始化文件系统;设置img控件时根据图片实际大小来设置控件大小,图片源设置路径为:F:lv.bin。
#include <stdio.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "lvgl.h"
#include "lv_port_fs.h"
#include "lvgl_helpers.h"
#include "demos/lv_demos.h"
#define LVGL_TICK_MS 1
#define TAG "main"
void lv_tick_task(void *arg)
{
lv_tick_inc(LVGL_TICK_MS);
}
static void lv_example_canvas_1(void)
{
lv_obj_t * img_play = lv_img_create(lv_scr_act());
lv_obj_set_pos(img_play, 0, 0);
//根据图片实际大小设置
lv_obj_set_size(img_play, 163, 220);
lv_obj_set_style_img_opa(img_play, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
// lv_img_set_src(img_play,&esp_logo);
//设置图片源
lv_img_set_src(img_play, "F:lv_1.bin");
}
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
/* Initialize SPI or I2C bus used by the drivers */
lvgl_driver_init();
lv_init();
lv_port_fs_init(); //初始化文件系统
lv_color_t *buf1 = heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(buf1 != NULL);
static lv_color_t *buf2 = NULL;
static lv_disp_draw_buf_t disp_buf;
uint32_t size_in_px = DISP_BUF_SIZE;
lv_disp_draw_buf_init(&disp_buf, buf1, buf2, size_in_px);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
disp_drv.flush_cb = disp_driver_flush;
disp_drv.draw_buf = &disp_buf;
lv_disp_drv_register(&disp_drv);
const esp_timer_create_args_t periodic_timer_args = {
.callback = &lv_tick_task,
.name = "periodic_gui"};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1 * 1000));
// lvgl demo演示
// lv_demo_music();
// lv_demo_stress();
//ui 显示
lv_example_canvas_1();
while (1)
{
/* Delay 1 tick (assumes FreeRTOS tick is 10ms */
vTaskDelay(pdMS_TO_TICKS(10));
lv_task_handler();
}
}
2.5 实际效果
四、其它
这里吐槽几个点吧,这也是篇文章的主要(不是)顺带的目的。
第一个就是lvgl更新的速度确实是有点快,这导致笔者在网上找到的很多关于lvgl的文章都还是v7版本,有时候需要从这里那里引入各种库,而新版本可能已经把库合并了,之前的经验也可能也行不通了,遇到无法解决的问题的时候需要去验证这样那样的方法确实是非常的浪费时间,等这篇文章发出来后说不定也已经过时了。
第二个就是拿来主义也不是那么好拿的,之前在一个前辈的工程里找到了一个移植好了的组件库,使用的时候才发现好像是v7版本的lvgl,本来想着应该差不多改改就行,结果后来在fs_open()这个函数的实现上栽了跟头。
v7与v8版本的fs_open()实现函数在传入参数上有了区别,v7的fs_open函数不直接返回文件描述符的指针,而v8 要直接返回一个void* 的指针,一开始由于没搞清楚这个void * 到底需要我们返回什么,导致在使用文件系统读取图片时报各种错,搞了几天才想到可能是返回值出错了,最后才在lvgl找到了官方示例。
v7:
static lv_fs_res_t fs_open(lv_fs_drv_t *drv, void *file_p, const char *path, lv_fs_mode_t mode)
v8:
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
第三个就是在找错误的过程中觉得指针不愧是c语言的扛把子,直接被绕晕了。
typedef FILE *file_t;
static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br)
{
file_t *fp = file_p;
//*br = fread(buf, 1, btr, *fp); //(v7抄的出错了)
*br = fread(buf, 1, btr, fp);
return (int32_t)(*br) < 0 ? LV_FS_RES_UNKNOWN : LV_FS_RES_OK;
}
总结
本文主要是一些移植lvgl文件系统到esp32s3的踩坑记录,后续如果用到sd卡的话会做一些补充。