《DNESP32P4开发指南_V1.0》第四十一章 图片显示实验(下)
第四十一章 图片显示实验
41.3 程序设计
41.3.1 程序流程图

图41.3.1.1 图片显示实验程序流程图
41.3.2 程序解析
在32_picture_display例程中,作者在32_picture_display\components\Middlewares路径下新建PICTURE和MYFATFS文件夹,并在这两个文件夹下新建了CMakeLists.txt文件。其中,PICTURE和MYFATFS文件夹用于存放PICTURE和MYFATFS组件驱动,而CMakeLists.txt文件则用于将PICTURE和MYFATFS组件添加至构建系统,以便项目工程能够使用PICTURE和MYFATFS组件功能。
1,MYFATFS驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MYFATFS组件源码包括两个文件:exfuns.c和exfuns.h。
下面先解析exfuns.h的程序。定义了文件的类型识别宏定义。
/* exfuns_file_type返回的类型定义
* 根据表FILE_TYPE_TBL获得.在exfuns.c里面定义
*/
#define T_BIN 0x00 /* BIN文件 */
#define T_LRC 0x10 /* LRC文件 */
#define T_NES 0x20 /* NES文件 */
#define T_SMS 0x21 /* SMS文件 */
#define T_TEXT 0x30 /* TXT文件 */
#define T_C 0x31 /* C文件 */
#define T_H 0x32 /* H文件 */
#define T_WAV 0x40 /* WAV文件 */
#define T_MP3 0x41 /* MP3文件 */
#define T_APE 0x42 /* APE文件 */
#define T_FLAC 0x43 /* FLAC文件 */
#define T_BMP 0x51 /* BMP文件 */
#define T_JPG 0x52 /* JPG文件 */
#define T_JPEG 0x53 /* JPEG文件 */
#define T_GIF 0x54 /* GIF文件 */
#define T_PNG 0x55 /* GIF文件 */
#define T_AVI 0x60 /* AVI文件 */
下面我们再解析exfuns.c的文件,该文件包含了多个文件系统扩展功能接口,这些功能实现如下所示。
1,文件类型定义
FILE_TYPE_TBL是一个二维数组,用来定义不同类型的文件及其子类型。例如,音频文件(如WAV、MP3)、图片文件(如BMP、JPG)等。该表格便于后续代码识别文件类型,如下代码所示:
#define FILE_MAX_TYPE_NUM 7 /* 最多FILE_MAX_TYPE_NUM个大类 */
#define FILE_MAX_SUBT_NUM 7 /* 最多FILE_MAX_SUBT_NUM个小类 */
/* 文件类型定义 */
static const char *FILE_TYPE_TBL[FILE_MAX_TYPE_NUM][FILE_MAX_SUBT_NUM] = {
{"BIN"," "," "," "," "," "," "}, /* BIN文件 */
{"LRC"," "," "," "," "," "," "}, /* LRC文件 */
{"NES", "SMS"," "," "," "," "," "}, /* NES/SMS文件 */
{"TXT", "C", "H"," "," "," "," "}, /* 文本文件 */
{"WAV", "MP3", "OGG", "FLAC", "AAC", "WMA", "MID"}, /* 音乐文件 */
{"DB","BMP", "JPG", "JPEG", "GIF","PNG"," "}, /* 图片文件 */
{"AVI"," "," "," "," "," "," "}, /* 视频文件 */
};
2,字符转大写
exfuns_char_upper函数将传入的字符转换为大写字母。如果字符是数字或已经是大写字母,则保持不变,如下代码:
/**
* @brief 将小写字母转为大写字母,如果是数字,则保持不变.
* @param c : 要转换的字母
* @retval 转换后的字母,大写
*/
uint8_t exfuns_char_upper(uint8_t c)
{
if (c < 'A') return c; /* 数字,保持不变. */
if (c >= 'a')
{
return c - 0x20; /* 变为大写. */
}
else
{
return c; /* 大写,保持不变 */
}
}
3,文件类型判断
exfuns_file_type根据文件名后缀判断文件的类型,并返回文件所属的大类和子类。若无法识别,则返回0xFF。如下代码:
/**
* @brief 报告文件的类型
* @param fname : 文件名
* @retval 文件类型
* @arg 0XFF , 表示无法识别的文件类型编号.
* @arg 其他 , 高四位表示所属大类, 低四位表示所属小类.
*/
uint8_t exfuns_file_type(char *fname)
{
uint8_t tbuf[5];
char *attr = 0; /* 后缀名 */
uint8_t i = 0, j;
while (i < 250)
{
i++;
if (*fname == '\0')break; /* 偏移到了最后了. */
fname++;
}
if (i == 250)return 0XFF; /* 错误的字符串. */
for (i = 0; i < 5; i++) /* 得到后缀名 */
{
fname--;
if (*fname == '.')
{
fname++;
attr = fname;
break;
}
}
if (attr == 0) return 0XFF;
strcpy((char *)tbuf, (const char *)attr); /* copy */
for (i = 0; i < 4; i++) tbuf[i] = exfuns_char_upper(tbuf[i]);/* 变为大写 */
for (i = 0; i < FILE_MAX_TYPE_NUM; i++) /* 大类对比 */
{
for (j = 0; j < FILE_MAX_SUBT_NUM; j++) /* 子类对比 */
{
if (*FILE_TYPE_TBL[i][j] == 0) break; /* 此组已经没有可对比的成员了. */
if (strcmp(FILE_TYPE_TBL[i][j],(const char *)tbuf)== 0)
{
return (i << 4) | j;
}
}
}
return 0XFF; /* 没找到 */
}
4,文件复制功能
exfuns_file_copy实现文件的复制功能,支持文件的拷贝进度显示。复制时可以选择覆盖原文件或不覆盖原文件。通过传入的回调函数fcpymsg显示文件复制进度(包括文件名、百分比等信息),如下代码:
/**
* @brief 文件复制
* @note 将psrc文件,copy到pdst.
* 注意: 文件大小不要超过4GB.
*
* @param fcpymsg : 函数指针, 用于实现拷贝时的信息显示
* pname:文件/文件夹名
* pct:百分比
* mode:
* bit0 : 更新文件名
* bit1 : 更新百分比pct
* bit2 : 更新文件夹
* 其他 : 保留
* 返回值: 0, 正常; 1, 强制退出;
*
* @param psrc : 源文件
* @param pdst : 目标文件
* @param totsize : 总大小(当totsize为0的时候,表示仅仅为单个文件拷贝)
* @param cpdsize : 已复制了的大小.
* @param fwmode : 文件写入模式
* @arg 0: 不覆盖原有的文件
* @arg 1: 覆盖原有的文件
*
* @retval 执行结果
* @arg 0 , 正常
* @arg 0XFF, 强制退出
* @arg 其他, 错误代码
*/
uint8_t exfuns_file_copy(uint8_t(*fcpymsg)(uint8_t *pname, uint8_t pct,
uint8_t mode), uint8_t *psrc, uint8_t *pdst,
uint32_t totsize, uint32_t cpdsize, uint8_t fwmode)
{
uint8_t res;
uint16_t br = 0;
uint16_t bw = 0;
FIL *fsrc = 0;
FIL *fdst = 0;
uint8_t *fbuf = 0;
uint8_t curpct = 0;
unsigned long long lcpdsize = cpdsize;
fsrc = (FIL *)malloc(sizeof(FIL)); /* 申请内存 */
fdst = (FIL *)malloc(sizeof(FIL));
fbuf = (uint8_t *)malloc(8192);
if (fsrc == NULL || fdst == NULL || fbuf == NULL)
{
res = 100; /* 前面的值留给fatfs */
}
else
{
if (fwmode == 0)
{
fwmode = FA_CREATE_NEW; /* 不覆盖 */
}
else
{
fwmode = FA_CREATE_ALWAYS; /* 覆盖存在的文件 */
}
/* 打开只读文件 */
res = f_open(fsrc, (const TCHAR *)psrc, FA_READ | FA_OPEN_EXISTING);
/* 第一个打开成功,才开始打开第二个 */
if (res == 0)res = f_open(fdst, (const TCHAR *)pdst, FA_WRITE | fwmode);
if (res == 0) /* 两个都打开成功了 */
{
if (totsize == 0) /* 仅仅是单个文件复制 */
{
totsize = fsrc->obj.objsize;
lcpdsize = 0;
curpct = 0;
}
else
{
curpct = (lcpdsize * 100) / totsize; /* 得到新百分比 */
}
fcpymsg(psrc, curpct, 0X02); /* 更新百分比 */
while (res == 0) /* 开始复制 */
{
res = f_read(fsrc, fbuf, 8192, (UINT *)&br);/* 源头读出512字节 */
if (res || br == 0)break;
res = f_write(fdst, fbuf,(UINT)br, (UINT *)&bw);/* 写入目的文件 */
lcpdsize += bw;
if (curpct != (lcpdsize * 100) / totsize) /* 是否需要更新百分比 */
{
curpct = (lcpdsize * 100) / totsize;
if (fcpymsg(psrc, curpct, 0X02)) /* 更新百分比 */
{
res = 0XFF; /* 强制退出 */
break;
}
}
if (res || bw < br)break;
}
f_close(fsrc);
f_close(fdst);
}
}
free(fsrc); /* 释放内存 */
free(fdst);
free(fbuf);
return res;
}
5,获取文件夹名称
exfuns_get_src_dname从完整路径中提取文件夹的名称,如下代码:
/**
* @brief 得到路径下的文件夹
* @note 即把路径全部去掉, 只留下文件夹名字.
* @param pname : 详细路径
* @retval 0 , 路径就是个卷标号.
* 其他, 文件夹名字首地址
*/
uint8_t *exfuns_get_src_dname(uint8_t *pname)
{
uint16_t temp = 0;
while (*pname != 0)
{
pname++;
temp++;
}
if (temp < 4) return 0;
/* 追述到倒数第一个"\"或者"/"处 */
while ((*pname != 0x5c) && (*pname != 0x2f)) pname--;
return ++pname;
}
6,获取文件夹大小
exfuns_get_folder_size计算指定文件夹的总大小,递归计算文件夹内所有文件的大小,返回文件夹总大小,如下代码:
/**
* @brief 得到文件夹大小
* @note 注意: 文件夹大小不要超过4GB.
* @param pname : 详细路径
* @retval 0 , 文件夹大小为0, 或者读取过程中发生了错误.
* 其他, 文件夹大小
*/
uint32_t exfuns_get_folder_size(uint8_t *fdname)
{
#define MAX_PATHNAME_DEPTH 512 + 1 /* 最大目标文件路径+文件名深度 */
uint8_t res = 0;
FF_DIR *fddir = 0; /* 目录 */
FILINFO *finfo = 0; /* 文件信息 */
uint8_t *pathname = 0; /* 目标文件夹路径+文件名 */
uint16_t pathlen = 0; /* 目标路径长度 */
uint32_t fdsize = 0;
fddir = (FF_DIR *)malloc(sizeof(FF_DIR)); /* 申请内存 */
finfo = (FILINFO *)malloc(sizeof(FILINFO));
if (fddir == NULL || finfo == NULL)res = 100;
if (res == 0)
{
pathname = malloc(MAX_PATHNAME_DEPTH);
if (pathname == NULL)res = 101;
if (res == 0)
{
pathname[0] = 0;
strcat((char *)pathname, (const char *)fdname); /* 复制路径 */
res = f_opendir(fddir, (const TCHAR *)fdname); /* 打开源目录 */
if (res == 0) /* 打开目录成功 */
{
while (res == 0) /* 开始复制文件夹里面的东东 */
{
res = f_readdir(fddir, finfo); /* 读取目录下的一个文件 */
if (res != FR_OK || finfo->fname[0] == 0) break;
if (finfo->fname[0] == '.')continue;
if (finfo->fattrib & 0X10)
{
pathlen = strlen((const char *)pathname);
strcat((char *)pathname, (const char *)"/");
strcat((char *)pathname, (const char *)finfo->fname);
fdsize += exfuns_get_folder_size(pathname);
pathname[pathlen] = 0;
}
else
{
fdsize += finfo->fsize; /* 非目录,直接加上文件的大小 */
}
}
}
free(pathname);
}
}
free(fddir);
free(finfo);
if (res)
{
return 0;
}
else
{
return fdsize;
}
}
7,文件夹复制功能
exfuns_folder_copy类似于文件复制功能,但它支持文件夹的复制。该函数能够递归地复制文件夹中的所有文件和子文件夹。它同样支持进度显示,并允许选择覆盖文件或不覆盖文件,如下代码:
/**
* @brief 文件夹复制
* @note 将psrc文件夹, 拷贝到pdst文件夹.
* 注意: 文件大小不要超过4GB.
*
* @param fcpymsg : 函数指针, 用于实现拷贝时的信息显示
* pname:文件/文件夹名
* pct:百分比
* mode:
* bit0 : 更新文件名
* bit1 : 更新百分比pct
* bit2 : 更新文件夹
* 其他 : 保留
* 返回值: 0, 正常; 1, 强制退出;
*
* @param psrc : 源文件夹
* @param pdst : 目标文件夹
* @note 必须形如"X:"/"X:XX"/"X:XX/XX"之类的. 且要确认上一级文件夹存在
*
* @param totsize : 总大小(当totsize为0的时候,表示仅仅为单个文件拷贝)
* @param cpdsize : 已复制了的大小.
* @param fwmode : 文件写入模式
* @arg 0: 不覆盖原有的文件
* @arg 1: 覆盖原有的文件
*
* @retval 执行结果
* @arg 0 , 正常
* @arg 0XFF, 强制退出
* @arg 其他, 错误代码
*/
uint8_t exfuns_folder_copy(uint8_t(*fcpymsg)(uint8_t *pname, uint8_t pct,
uint8_t mode), uint8_t *psrc, uint8_t *pdst,
uint32_t *totsize, uint32_t *cpdsize, uint8_t fwmode)
{
#define MAX_PATHNAME_DEPTH 512 + 1 /* 最大目标文件路径+文件名深度 */
uint8_t res = 0;
FF_DIR *srcdir = 0; /* 源目录 */
FF_DIR *dstdir = 0; /* 源目录 */
FILINFO *finfo = 0; /* 文件信息 */
uint8_t *fn = 0; /* 长文件名 */
uint8_t *dstpathname = 0; /* 目标文件夹路径+文件名 */
uint8_t *srcpathname = 0; /* 源文件夹路径+文件名 */
uint16_t dstpathlen = 0; /* 目标路径长度 */
uint16_t srcpathlen = 0; /* 源路径长度 */
srcdir = (FF_DIR *)malloc(sizeof(FF_DIR)); /* 申请内存 */
dstdir = (FF_DIR *)malloc(sizeof(FF_DIR));
finfo = (FILINFO *)malloc(sizeof(FILINFO));
if (srcdir == NULL || dstdir == NULL || finfo == NULL)res = 100;
if (res == 0)
{
dstpathname = malloc(MAX_PATHNAME_DEPTH);
srcpathname = malloc(MAX_PATHNAME_DEPTH);
if (dstpathname == NULL || srcpathname == NULL)res = 101;
if (res == 0)
{
dstpathname[0] = 0;
srcpathname[0] = 0;
strcat((char *)srcpathname,(const char *)psrc);/* 复制原始源文件路径 */
strcat((char *)dstpathname,(const char *)pdst);
res = f_opendir(srcdir, (const TCHAR *)psrc); /* 打开源目录 */
if (res == 0) /* 打开目录成功 */
{
strcat((char *)dstpathname, (const char *)"/"); /* 加入斜杠 */
fn = exfuns_get_src_dname(psrc);
if (fn == 0) /* 卷标拷贝 */
{
dstpathlen = strlen((const char *)dstpathname);
dstpathname[dstpathlen] = psrc[0]; /* 记录卷标 */
dstpathname[dstpathlen + 1] = 0; /* 结束符 */
}
else strcat((char *)dstpathname, (const char *)fn); /* 加文件名 */
fcpymsg(fn, 0, 0X04); /* 更新文件夹名 */
res = f_mkdir((const TCHAR *)dstpathname);
if (res == FR_EXIST) res = 0;
while (res == 0) /* 开始复制文件夹里面的东东 */
{
res = f_readdir(srcdir, finfo); /* 读取目录下的一个文件 */
if (res != FR_OK || finfo->fname[0] == 0)break;
if (finfo->fname[0] == '.')continue;
fn = (uint8_t *)finfo->fname; /* 得到文件名 */
dstpathlen = strlen((const char *)dstpathname);
srcpathlen = strlen((const char *)srcpathname);
strcat((char *)srcpathname, (const char *)"/");
if (finfo->fattrib & 0X10)
{
strcat((char *)srcpathname, (const char *)fn);
res = exfuns_folder_copy(fcpymsg, srcpathname,
dstpathname, totsize, cpdsize, fwmode);
}
else /* 非目录 */
{
strcat((char *)dstpathname, (const char *)"/");
strcat((char *)dstpathname, (const char *)fn);
strcat((char *)srcpathname, (const char *)fn);
fcpymsg(fn, 0, 0X01); /* 更新文件名 */
res = exfuns_file_copy(fcpymsg, srcpathname,
dstpathname, *totsize, *cpdsize, fwmode);
*cpdsize += finfo->fsize; /* 增加一个文件大小 */
}
srcpathname[srcpathlen] = 0; /* 加入结束符 */
dstpathname[dstpathlen] = 0; /* 加入结束符 */
}
}
free(dstpathname);
free(srcpathname);
}
}
free(srcdir);
free(dstdir);
free(finfo);
return res;
}
MYFATFS组件实现了一些FATFS文件系统的扩展功能,包含了文件的类型识别、文件和文件夹的复制、磁盘容量查询等基本文件操作。这些功能对于嵌入式系统中管理文件和存储资源非常有用。
2,PICTURE驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括十二个文件:bmp.c、bmp.h、jpeg.c、jpeg.h、gif.c、gif.h、png.c、png.h、pngle.c、pngle.h、piclib.c和piclib.h。
其中:
bmp.c和bmp.h用于实现对bmp文件的解码;
jpeg.c和jpeg.h用于实现对jpeg/jpg文件的硬件解码;
gif.c和gif.h用于实现对gif文件的解码;
png.c和png.h用于实现对png文件的解码;
pngle.c/.h用于png解码及系统配置;
这些文件的代码太长了,而且也有规定的标准,需要结合各个图片编码的格式来编写,所以我们在这里不贴出来,大家查看光盘中的源码的实现过程即可。下面我们重点讲解这几个解码库对应到我们的LCD的显示部分。
1,解码库的控制句柄_pic_phy和_pic_info
我们通过定义_pic_phy和_pic_info两个结构体,实现了将解码后的图片数据与LCD显示操作的有效连接。_pic_phy用于存储和管理与LCD显示相关的操作接口,而_pic_info则包含了解码后的图片尺寸、颜色等基本信息,确保图片能够正确显示在屏幕上。这种设计不仅使图片解码和显示的流程更加简洁清晰,还能方便地在LCD上显示不同格式的图像数据。
/* 图片显示物理层接口 */
/* 在移植的时候,必须由用户自己实现这几个函数 */
typedef struct
{
/* 画点函数 */
void(*draw_point)(uint16_t, uint16_t, uint16_t);
/* 填充函数 */
void(*fill)(uint16_t, uint16_t, uint16_t, uint16_t, uint16_t);
/* 画水平线函数 */
void(*draw_hline)(uint16_t, uint16_t, uint16_t, uint16_t);
/*多点填充 */
void(*multicolor)(uint16_t, uint16_t, uint16_t, uint16_t,uint16_t *);
} _pic_phy;
/* 图像信息 */
typedef struct
{
uint16_t lcdwidth; /* LCD的宽度 */
uint16_t lcdheight; /* LCD的高度 */
} _pic_info;
在piclib.c文件中,我们用上述类型定义了两个结构体,具体如下:
_pic_info picinfo; /* 图片信息 */
_pic_phy pic_phy; /* 图片显示物理接口 */
2,piclib_init函数
piclib_init函数,该函数用于初始化图片解码的相关信息,用于定义解码后的LCD操作。具体定义如下:
/**
* @brief 多点填充
* @param x, y : 起始坐标
* @param width, height : 宽度和高度
* @param color : 颜色数组
* @retval 无
*/
static void piclib_multi_color(uint16_t x, uint16_t y, uint16_t ex,
uint16_t ey,uint16_t *color)
{
esp_lcd_panel_draw_bitmap(lcddev.lcd_panel_handle, x, y, ex, ey + 1, color);
}
/**
* @brief 画图初始化
* @note 在画图之前,必须先调用此函数, 指定相关函数
* @param 无
* @retval 无
*/
void piclib_init(void)
{
pic_phy.draw_point = lcd_draw_point; /* 画点函数实现,仅GIF需要 */
pic_phy.fill = lcd_fill; /* 填充函数实现,仅GIF需要 */
pic_phy.draw_hline = lcd_draw_hline; /* 画线函数实现,仅GIF需要 */
pic_phy.multicolor = piclib_multi_color;/* 填充函数实现,JPEG、BMP、PNG需要 */
picinfo.lcdwidth = lcddev.width; /* 得到LCD的宽度像素 */
picinfo.lcdheight = lcddev.height; /* 得到LCD的高度像素 */
}
初始化图片解码的相关信息,这些函数必须由用户在外部实现。我们使用之前LCD的操作函数对这个结构体中的绘制操作:画点、画线、画圆等定义与我们的LCD操作对应起来。具体这些操作可以查看LCD这一章节的描述。
3,piclib_ai_load_picfile函数
piclib_ai_load_picfile帮助我们得到需要显示的图片信息并助于下一步的绘制。本函数需要结合文件系统来操作,图片根据后缀区分并且在文件夹在保存是我们在PC下的细分类,也是我们处理和分类图片的最方便的方式:
/**
* @brief 智能画图
* @note 图片仅在x,y和width, height限定的区域内显示.
*
* @param filename : 包含路径的文件名(.bmp/.jpg/.jpeg/.gif等)
* @param x, y : 起始坐标
* @param width, height : 显示区域
* @note 图片尺寸小于等于液晶分辨率,才支持快速解码
* @retval 无
*/
uint8_t piclib_ai_load_picfile(char *filename, uint16_t x, uint16_t y,
uint16_t width, uint16_t height)
{
uint8_t res = 0;/* 返回值 */
uint8_t temp;
if ((x + width) > picinfo.lcdwidth) return PIC_WINDOW_ERR; /* x坐标超范围了 */
if ((y + height) > picinfo.lcdheight)return PIC_WINDOW_ERR;/* y坐标超范围了 */
/* 得到显示方框大小 */
if (width == 0 || height == 0)return PIC_WINDOW_ERR; /* 窗口设定错误 */
/* 文件名传递 */
temp = exfuns_file_type(filename); /* 得到文件的类型 */
ESP_LOGI("here","temp:%#x ", temp);
switch (temp)
{
case T_BMP:
ESP_LOGI("here","enter");
res = bmp_decode(filename, width, height); /* 解码BMP */
break;
case T_JPG:
case T_JPEG:
res = jpeg_decode(filename, width, height); /* 解码JPG/JPEG */
break;
case T_GIF:
res = gif_decode(filename, x, y, width, height); /* 解码gif */
break;
case T_PNG:
res = png_decode(filename, width, height); /* 解码PNG */
break;
default:
res = PIC_FORMAT_ERR; /* 非图片格式!!! */
break;
}
return res;
}
piclib_ai_load_picfile函数,整个图片显示的对外接口,外部程序,通过调用该函数,可以实现bmp、jpg/jpeg、png和gif的显示,该函数根据输入文件的后缀名,判断文件格式,然后交给相应的解码程序(bmp解码/jpeg解码/gif解码/png解码),执行解码,完成图片显示。
3,CMakeLists.txt文件
本例程的功能实现主要依靠MYFATFS和PICTURE组件。要在main函数中,成功调用MYFATFS和PICTURE文件中的内容,就得需要修改Middlewares/MYFATFS和Middlewares/PICTURE文件夹下的CMakeLists.txt文件,修改如下:
#Middlewares/MYFATFS文件夹下的CMakeLists.txt文件内容
list(APPEND srcs exfuns.c)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
REQUIRES fatfs
)
#Middlewares/PICTURE文件夹下的CMakeLists.txt文件内容
list(APPEND srcs bmp.c
gif.c
jpeg.c
piclib.c
png.c
pngle.c
)
idf_component_register(SRCS "jpeg.c" "bmp.c" "${srcs}"
INCLUDE_DIRS "."
REQUIRES BSP
MYFATFS
esp_driver_jpeg
fatfs
)
4,main.c驱动代码
在main.c里面编写如下代码。
/**
* @brief 得到path路径下,目标文件的总个数
* @param path : 路径
* @retval 总有效文件数
*/
uint16_t pic_get_tnum(char *path)
{
uint8_t res;
uint16_t rval = 0;
FF_DIR tdir; /* 临时目录 */
FILINFO *tfileinfo; /* 临时文件信息 */
tfileinfo = (FILINFO *)malloc(sizeof(FILINFO)); /* 申请内存 */
res = f_opendir(&tdir, (const TCHAR *)path); /* 打开目录 */
if (res == FR_OK && tfileinfo)
{
while (1) /* 查询总的有效文件数 */
{
res = f_readdir(&tdir, tfileinfo); /* 读取目录下的一个文件 */
if (res != FR_OK || tfileinfo->fname[0] == 0)break;
res = exfuns_file_type(tfileinfo->fname);
if ((res & 0X0F) != 0X00) /* 取低四位,看看是不是图片文件 */
{
rval++; /* 有效文件数增加1 */
}
}
}
free(tfileinfo); /* 释放内存 */
return rval;
}
/**
* @brief 转换
* @param fs:文件系统对象
* @param clst:转换
* @retval =0:扇区号,0:失败
*/
static LBA_t atk_clst2sect(FATFS *fs, DWORD clst)
{
clst -= 2; /* Cluster number is origin from 2 */
if (clst >= fs->n_fatent - 2)
{
return 0; /* Is it invalid cluster number? */
}
/* Start sector number of the cluster */
return fs->database + (LBA_t)fs->csize * clst;
}
/**
* @brief 偏移
* @param dp:指向目录对象
* @param Offset:目录表的偏移量
* @retval FR_OK(0):成功,!=0:错误
*/
FRESULT atk_dir_sdi(FF_DIR *dp, DWORD ofs)
{
DWORD clst;
FATFS *fs = dp->obj.fs;
if (ofs >= (DWORD)((FF_FS_EXFAT && fs->fs_type == FS_EXFAT)
? 0x10000000 : 0x200000) || ofs % 32)
{
/* Check range of offset and alignment */
return FR_INT_ERR;
}
dp->dptr = ofs; /* Set current offset */
clst = dp->obj.sclust; /* Table start cluster (0:root) */
if (clst == 0 && fs->fs_type >= FS_FAT32)
{ /* Replace cluster# 0 with root cluster# */
clst = (DWORD)fs->dirbase;
if (FF_FS_EXFAT)
{
dp->obj.stat = 0;
}
/* exFAT: Root dir has an FAT chain */
}
if (clst == 0)
{ /* Static table (root-directory on the FAT volume) */
if (ofs / 32 >= fs->n_rootdir)
{
return FR_INT_ERR; /* Is index out of range? */
}
dp->sect = fs->dirbase;
}
else
{
dp->sect = atk_clst2sect(fs, clst);
}
dp->clust = clst; /* Current cluster# */
if (dp->sect == 0)
{
return FR_INT_ERR;
}
dp->sect += ofs / fs->ssize; /* Sector# of the directory entry */
dp->dir = fs->win +(ofs % fs->ssize);/* Pointer to the entry in the win[] */
return FR_OK;
}
void app_main(void)
{
esp_err_t ret;
FF_DIR picdir; /* 图片目录 */
FILINFO *picfileinfo; /* 文件信息 */
char *pname; /* 带路径的文件名 */
uint16_t totpicnum; /* 图片文件总数 */
uint16_t curindex = 0; /* 图片当前索引 */
uint8_t key = 0; /* 键值 */
uint8_t t;
uint16_t temp;
uint32_t *picoffsettbl; /* 图片文件offset索引表 */
ret = nvs_flash_init(); /* 初始化NVS */
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
led_init(); /* LED初始化 */
key_init(); /* KEY初始化 */
lcd_init(); /* LCD屏初始化 */
myiic_init(); /* MYIIC初始化 */
xl9555_init(); /* XL9555初始化 */
while (sdmmc_init()) /* 检测不到SD卡 */
{
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 110, 239, 126, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
ret = exfuns_init(); /* 为fatfs相关变量申请内存 */
while (fonts_init()) /* 检查字库 */
{
lcd_clear(WHITE);
lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED); /* 更新字库 */
while (key) /* 更新失败 */
{
lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
vTaskDelay(pdMS_TO_TICKS(1000));
lcd_clear(WHITE);
}
text_show_string(30, 50, 200, 16, "正点原子ESP32-P4开发板",16,0, RED);
text_show_string(30, 70, 200, 16, "图片显示 实验", 16, 0, RED);
text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY0:NEXT KEY1:PREV", 16, 0, RED);
while (f_opendir(&picdir, "0:/PICTURE")) /* 打开图片文件夹 */
{
text_show_string(30, 150, 240, 16, "PICTURE文件夹错误!", 16, 0, RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 150, 240, 186, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
totpicnum = pic_get_tnum("0:/PICTURE"); /* 得到总有效文件数 */
while (totpicnum == 0) /* 图片文件为0 */
{
text_show_string(30, 150, 240, 16, "没有图片文件!", 16, 0, RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 150, 240, 186, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
picfileinfo = (FILINFO *)malloc(sizeof(FILINFO)); /* 申请内存 */
pname = malloc(255 * 2 + 1); /* 为带路径的文件名分配内存 */
/* 申请4*totpicnum个字节的内存,用于存放图片索引 */
picoffsettbl = malloc(4 * totpicnum);
while (!picfileinfo || !pname || !picoffsettbl) /* 内存分配出错 */
{
text_show_string(30, 150, 240, 16, "内存分配失败!", 16, 0, RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 150, 240, 186, WHITE); /* 清除显示 */
vTaskDelay(pdMS_TO_TICKS(200));
}
/* 记录索引 */
ret = f_opendir(&picdir, "0:/PICTURE"); /* 打开目录 */
if (ret == FR_OK)
{
curindex = 0; /* 当前索引为0 */
while (1) /* 全部查询一遍 */
{
temp = picdir.dptr; /* 记录当前dptr偏移 */
ret = f_readdir(&picdir, picfileinfo); /* 读取目录下的一个文件 */
if (ret != FR_OK || picfileinfo->fname[0] == 0) break;
ret = exfuns_file_type(picfileinfo->fname);
if ((ret & 0X0F) != 0X00) /* 取高四位,看看是不是图片文件 */
{
picoffsettbl[curindex] = temp; /* 记录索引 */
curindex++;
}
}
ESP_LOGI("main", "0:/PICTURE pic_num:%d", curindex);
}
text_show_string(30, 150, 240, 16, "开始显示...", 16, 0, RED);
vTaskDelay(pdMS_TO_TICKS(1000));
piclib_init(); /* 初始化画图 */
curindex = 0; /* 从0开始显示 */
ret = f_opendir(&picdir, (const TCHAR *)"0:/PICTURE"); /* 打开目录 */
while (ret == FR_OK) /* 打开成功 */
{
atk_dir_sdi(&picdir, picoffsettbl[curindex]);
ret = f_readdir(&picdir, picfileinfo); /* 读取目录下的一个文件 */
if (ret != FR_OK || picfileinfo->fname[0] == 0)
{
picdir.dptr = picoffsettbl[curindex];
ret = f_readdir(&picdir, picfileinfo); /* 读取目录下的一个文件 */
if (ret != FR_OK || picfileinfo->fname[0] == 0)
{
ESP_LOGE(__FUNCTION__, "Read Failed");
break; /* 错误了/到末尾了,退出 */
}
}
strcpy((char *)pname, "0:/PICTURE/"); /* 复制路径(目录) */
/* 将文件名接在后面 */
strcat((char *)pname, (const char *)picfileinfo->fname);
lcd_clear(BLACK);
/* 显示图片 */
piclib_ai_load_picfile(pname, 0, 0, lcddev.width, lcddev.height);
/* 显示图片名字 */
text_show_string(2, 2, lcddev.width, 16, (char *)pname, 16, 0, RED);
t = 0;
while (1)
{
t ++;
key = xl9555_key_scan(0); /* 扫描按键 */
if (t > 250) key = KEY0_PRES; /* 模拟一次按下KEY0 */
if ((t % 20) == 0)
{
LED0_TOGGLE();
}
if (key == KEY1_PRES) /* 上一张 */
{
if (curindex)
{
curindex--;
}
else
{
curindex = totpicnum - 1;
}
break;
}
else if (key == KEY0_PRES) /* 下一张 */
{
curindex++;
if (curindex >= totpicnum)
{
curindex = 0; /* 到末尾的时候,自动从头开始 */
}
break;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
ret = 0;
}
free(picfileinfo); /* 释放内存 */
free(pname); /* 释放内存 */
free(picoffsettbl); /* 释放内存 */
}
上述代码的实际功能是从SD卡中读取图片文件,并在LCD屏幕上显示这些图片。用户可以通过按键操作浏览图片,支持上一张和下一张的切换。具体流程如下:
1)初始化硬件:首先初始化了NVS(非易失性存储器)、LED、按键、LCD显示、I2C接口以及外部扩展模块(如XL9555)。之后,检查SD卡是否正常连接,如果未连接,则显示错误信息并继续检测。
2)加载字库:在SD卡成功连接后,初始化了SPIFFS文件系统,并准备好对SD卡中的字库文件进行读取。最后,初始化了显示所需的字体库。
3)读取图片文件:程序打开0:/PICTURE文件夹,统计文件夹中有效的图片文件数量。如果没有图片文件,程序会显示相应的提示。然后,程序会遍历目录,记录图片文件的索引,并加载图片文件的路径。
4)显示图片:读取图片文件后,程序会通过LCD显示屏显示图片内容,并在屏幕上显示图片的文件名。图片的显示是通过调用图像加载函数piclib_ai_load_picfile来实现的。
5)按键控制:用户可以通过按下KEY0或KEY1键来切换图片。KEY0切换到下一张图片,KEY1切换到上一张图片。程序支持在图片末尾后自动返回到开头。
6)内存管理和错误处理:代码在内存分配时考虑了内存不足的情况,并进行了有效的错误处理和提示。每次使用完毕后,会释放申请的内存,以避免内存泄漏。
通过这样的流程,代码不仅实现了图片浏览功能,还考虑了设备的稳定性和用户交互的便利性,使得用户能够方便地查看存储在SD卡中的图片。
41.4 下载验证
将程序下载到开发板后,可以看到LCD开始显示图片(假设SD卡及文件都准备好了,即:在SD卡根目录新建:PICTURE文件夹,并存放一些图片文件(.bmp/.jpg/.gif/png)在该文件夹内),如下图所示:

图41.4.1 图片显示实验显示效果

浙公网安备 33010602011771号