《DNESP32P4开发指南_V1.0》第四十一章 图片显示实验(下)

第四十一章 图片显示实验

41.3 程序设计
41.3.1 程序流程图

image088

图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)在该文件夹内),如下图所示:

image089

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

posted @ 2026-08-10 09:33  正点原子  阅读(4)  评论(0)    收藏  举报