第四十一章 图片显示实验
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.cgif.cjpeg.cpiclib.cpng.cpngle.c)idf_component_register(SRCS "jpeg.c" "bmp.c" "${srcs}"INCLUDE_DIRS "."REQUIRES BSPMYFATFSesp_driver_jpegfatfs)
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 图片显示实验显示效果
