第四十章 ESP32S3 图片显示实验
在开发产品的时候,很多时候,都会用到图片解码,本章将学习何通过 ESP32-S3 来解码 BMP/JPG/JPEG/PNG/GIF 等图片,并在 SPILCD 上显示出来。
本章分为如下几个小节:
40.1 图片格式简介
40.2 硬件设计
40.3 程序设计
40.4 下载验证
40.1 图片格式介绍
常用的图片格式有很多,一般最常用的有四种: JPEG(或JPG)、BMP、PNG和GIF。其中 JPEG(或 JPG)、 PNG 和 BMP 是静态图片,而 GIF 则是可以实现动态图片。下面对比这四种图片格式。
格式 | 全称 | 压缩类型 | 色彩支持 | 核心特点 | 主要用途 |
---|---|---|---|---|---|
JPEG/JPG | Joint Photographic Experts Group | 有损压缩 | 真彩色(24位) | 文件小,压缩率高,有损 | 照片、复杂图像 |
PNG | Portable Network Graphics | 无损压缩 | 调色板/真彩色+Alpha通道 | 支持透明背景,无损 | 网页图形、图标、UI元素 |
GIF | Graphics Interchange Format | 无损压缩(索引色) | 索引色(最多256色) | 支持简单动画,文件小 | 简单动画、表情包 |
BMP | Bitmap | 通常无压缩 | 调色板/真彩色 | 无压缩,质量最高,文件大 | 临时存储、简单图标 |
表40.1.1 JPEG(或JPG)、BMP、PNG和GIF对比
下面介绍四种图片:
(1)JPEG / JPG (联合图像专家小组格式)
核心原理:有损压缩。其编码设计基于人眼视觉特性(对亮度敏感,对颜色细节不敏感)。色彩空间转换:将图像从RGB颜色空间转换为YCbCr颜色空间(Y代表亮度,Cb和Cr代表色度)。
- 下采样(Chroma Subsampling):减少色度通道(Cb, Cr)的分辨率(例如,从4:4:4变为4:2:0),这是主要压缩来源之一,人眼几乎察觉不到;
- 分块与DCT:将图像分成8x8的小块,对每个块进行离散余弦变换(DCT),将像素信息转换为频率信息;
- 量化:用一个“量化表”去除高频分量(人眼不敏感的细节),这是有损压缩的关键步骤,质量等级就是控制这个表的强度;
- 编码:对量化后的数据进行Zig-Zag扫描,然后使用霍夫曼编码进行熵编码,进一步压缩数据。
特点:
优点:极高的压缩率,文件体积小,非常适合存储照片和色彩丰富的图片。
缺点:不适合存储线条、文字或图标(会有明显的伪影),不支持透明背景。
适用场景:数码照片、网络图片、需要小体积文件的复杂图像。
(2)PNG (便携式网络图形)
核心原理:无损压缩。旨在替代GIF和专利的TIFF格式。
- 过滤:对每一行像素数据进行预测(Filter),找出像素间的最佳差分关系,便于后续压缩;
- 压缩:使用DEFLATE算法(与ZIP文件使用的算法相同)进行压缩,结合LZ77和霍夫曼编码。这个过程是无损的。
特点:
优点:
- 无损压缩:图像质量没有任何损失。
- 支持Alpha透明通道:可以实现边缘平滑的半透明效果,这是相比GIF的巨大优势。
- 支持真彩色(24位)和调色板颜色。
缺点:文件体积比JPEG大,尤其是对于照片类图像。
适用场景:网页Logo、图标、UI元素、需要透明背景或高质量线条的图形。
(3)GIF (图形交换格式)
核心原理:基于调色板的无损压缩(LZW算法)。
- 索引颜色:将图像颜色限制在最多256色。首先为图像创建一个全局或局部调色板。
- LZW压缩:对索引后的颜色值进行LZW编码,寻找重复模式并进行压缩。
特点:
优点:
- 支持动画。
- 文件体积小(对于颜色简单的图像)。
- 支持简单的布尔透明(一个像素要么完全透明,要么完全不透明)。
缺点:
- 仅支持256色,不适合存储照片或色彩丰富的图片,会出现颜色 banding。
- 透明效果粗糙,没有半透明过渡。
- 适用场景:简单动画、表情包、颜色单一的简单图形。
(4)BMP (位图)
核心原理:几乎无压缩(也可以选择简单的RLE压缩,但不常用)。它是最简单的图像格式之一,直接存储每个像素的颜色值。
- 文件头:包含图像的基本信息(如大小、宽度、高度、色深)。
- 调色板(对于256色及以下的图像):定义使用的颜色。
- 像素数据:按照从下到上、从左到右的顺序,直接记录每个像素的RGB值。
特点:
优点:格式简单,易于程序读写,无损存储,显示速度快。
缺点:文件体积非常大,因为几乎没有压缩。
适用场景:临时存储、操作系统壁纸、屏幕截图、对体积无要求的简单应用。
40.2 硬件设计
40.2.1 例程功能
开机的时候先检测字库,然后检测 SD 卡是否存在,如果 SD 卡存在,则开始查找 SD 卡根目录下的 PICTURE 文件夹,如果找到则显示该文件夹下面的图片文件(支持 bmp、 jpg、 jpeg、png 和 gif 格式),循环显示,通过按 KEY0和 KEY2可以快速浏览下一张和上一张, KEY_UP 按键用于暂停/继续播放, DS1 用于指示当前是否处于暂停状态。如果未找到 PICTURE 文件夹/任何图片文件,则提示错误。同样我们也是用 DS0 来指示程序正在运行。
40.2.2 硬件资源
1.LED
LED-IO1
2.XL9555
IIC_SDA-IO41
IIC_SCL-IO42
3.SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
4.SD
CS-IO2
SCK-IO12
MOSI-IO11
MISO-IO13
40.3 程序设计
40.3.1 程序流程图
本实验的程序流程图:
图 40.3.1.1 图片显示实验程序流程图
40.3.2 图片显示函数解析
原子提供的 PICTURE 驱动源码包括以下文件,并且已经针对正点原子 ESP32-S3 软硬件进行了移植适配,在使用时,仅需将这以下文件添加到自己的工程中即可,如下图所示:
图 40.3.2.1 正点原子 PICTURE 驱动源码文件
其中:
bmp.c 和 bmp.h 用于实现对 bmp 文件的解码;
tjpgd.c 和 tjpgd.h 用于实现对 jpeg/jpg 文件的解码;
gif.c 和 gif.h 用于实现对 gif 文件的解码;
代码太长,而且也有规定的标准, 需要结合各个图片编码的格式来编写,具体查看源码的实现过程即可。
40.3.3 图片显示函数驱动解析
在 IDF 版StandardExampleIDF(v5.3.x)\29_pitures例程中,具体实现在该目录的PICTURE目录下,StandardExampleIDF(v5.3.x)\29_pitures\components\Middlewares。
(1)解码库的控制句柄_pic_phy 和_pic_info
使用这个接口,把解码后的图形数据与 LCD 的实际操作对应起来。为了方便去显示图片,需要将图片的信息与LCD 联系上。这里我们定义了_pic_phy 和_pic_info 分别用于定义图片解码库的 LCD 操作和存放解码后的图片尺寸颜色信息。它们的定义如下:
#define rgb565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))/* 图片显示物理层接口 */
/* 在移植的时候,必须由用户自己实现这几个函数 */
typedef struct
{/* void draw_point(uint16_t x,uint16_t y,uint32_t color) 画点函数 */void(*draw_point)(uint16_t, uint16_t, uint16_t);/* void fill(uint16_t sx,uint16_t sy,uint16_t ex,uint16_t ey,uint32_t color) 单色填充函数 */void(*fill)(uint16_t, uint16_t, uint16_t, uint16_t, uint16_t);/* void draw_hline(uint16_t x0,uint16_t y0,uint16_t len,uint16_t color) 画水平线函数 */void(*draw_hline)(uint16_t, uint16_t, uint16_t, uint16_t);/* void piclib_multi_color(uint16_t x, uint16_t y, uint16_t width, uint16_t *color) 多点填充 */void(*multicolor)(uint16_t, uint16_t, uint16_t, uint16_t *);
} _pic_phy;extern _pic_phy pic_phy;/* 图像信息 */
typedef struct
{uint16_t lcdwidth; /* LCD的宽度 */uint16_t lcdheight; /* LCD的高度 */
} _pic_info;extern _pic_info picinfo; /* 图像信息 */
在 piclib.c 文件中,我们用上述类型定义了两个结构体,声明如下:
_pic_info picinfo; /* 图片信息 */
_pic_phy pic_phy; /* 图片显示物理接口 */
(2)piclib_init 函数
piclib_init 函数,该函数用于初始化图片解码的相关信息,用于定义解码后的 LCD 操作。具体定义如下:
/*** @brief 画图初始化* @note 在画图之前,必须先调用此函数, 指定相关函数* @param 无* @retval 无*/
void piclib_init(void)
{pic_phy.draw_point = spilcd_draw_point; /* 画点函数实现,仅GIF需要 */pic_phy.fill = spilcd_fill; /* 填充函数实现,仅GIF需要 */pic_phy.draw_hline = spilcd_draw_hline; /* 画线函数实现,仅GIF需要 */pic_phy.multicolor = piclib_multi_color; /* 颜色填充函数实现,JPEG、BMP、PNG需要 */picinfo.lcdwidth = spilcddev.width; /* 得到LCD的宽度像素 */picinfo.lcdheight = spilcddev.height; /* 得到LCD的高度像素 */
}
初始化图片解码的相关信息,这些函数必须由用户在外部实现。使用之前 LCD 的操作函数对这个结构体中的绘制操作:画点、画线、画圆等定义与我们的 LCD 操作对应起来。具体这些操作可以查看 SPILCD 一节的描述。
(3)piclib_ai_load_picfile 函数
piclib_ai_load_picfile 得到需要显示的图片信息,并助于下一步的绘制。本函数需要结合文件系统来操作,图片根据后缀区分。
/*** @brief 智能画图* @note 图片仅在x,y和width, height限定的区域内显示.** @param filename : 包含路径的文件名(.bmp/.jpg/.jpeg/.gif等)* @param x, y : 起始坐标* @param width, height : 显示区域* @param fast : 使能快速解码* @arg 0, 不使能* @arg 1, 使能* @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 解码),执行解码,完成图片显示。
这里用到的 exfuns_file_type()函数是用来判断文件类型。由于图片显示需要用到大内存,使用动态内存分配来实现,仍使用自定的内存管理函数来管理程序内存。申请内存函数piclib_mem_malloc()和内存释放函数 piclib_mem_free()的实现就比较简单。
40.3.4 CMakeLists.txt 文件
打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:
set(src_dirsLEDKEYMYIICXL9555MYSPISPILCDSPI_SD)set(include_dirsLEDKEYMYIICXL9555MYSPISPILCDSPI_SD)set(requiresdriveresp_lcdfatfs)idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
打开本实验 Middlewares 文件下的 CMakeLists.txt 文件,其内容如下所示:
list(APPEND srcs fonts.ctext.c)idf_component_register(SRCS "${srcs}"INCLUDE_DIRS "."REQUIRES esp_partitionspi_flashfatfsBSP)
40.3.5 实验应用代码
打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。
/*** @brief 程序入口* @param 无* @retval 无*/
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;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());ESP_ERROR_CHECK(nvs_flash_init());}led_init(); my_spi_init();key_init();myiic_init();xl9555_init();spilcd_init();while (sd_spi_init()){spilcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);vTaskDelay(pdMS_TO_TICKS(500));spilcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);vTaskDelay(pdMS_TO_TICKS(500));}ret = exfuns_init();while (fonts_init()){spilcd_clear(WHITE);spilcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);while (key){spilcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);vTaskDelay(pdMS_TO_TICKS(200));spilcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);vTaskDelay(pdMS_TO_TICKS(200));}spilcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);vTaskDelay(pdMS_TO_TICKS(1000));spilcd_clear(WHITE); }text_show_string(30, 50, 200, 16, "正点原子ESP32-S3开发板",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);text_show_string(30, 130, 200, 16, "KEY_UP:PAUSE:", 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));spilcd_fill(30, 150, 240, 186, WHITE);vTaskDelay(pdMS_TO_TICKS(200));}totpicnum = pic_get_tnum("0:/PICTURE");while (totpicnum == 0) {text_show_string(30, 150, 240, 16, "没有图片文件", 16, 0, RED);vTaskDelay(pdMS_TO_TICKS(200));spilcd_fill(30, 150, 240, 186, WHITE);vTaskDelay(pdMS_TO_TICKS(200));}picfileinfo = (FILINFO *)malloc(sizeof(FILINFO));pname = malloc(255 * 2 + 1);picoffsettbl = malloc(4 * totpicnum);while (!picfileinfo || !pname || !picoffsettbl){text_show_string(30, 150, 240, 16, "没有图片文件!", 16, 0, RED);vTaskDelay(pdMS_TO_TICKS(200));spilcd_fill(30, 150, 240, 186, WHITE);vTaskDelay(pdMS_TO_TICKS(200));}ret = f_opendir(&picdir, "0:/PICTURE");if (ret == FR_OK){curindex = 0;while (1){temp = picdir.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; 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); spilcd_clear(BLACK);piclib_ai_load_picfile(pname, 0, 0, spilcddev.width, spilcddev.height); text_show_string(2, 2, spilcddev.width, 16, (char *)pname, 16, 0, RED); t = 0;while (1){t ++;key = xl9555_key_scan(0);if (t > 250) key = KEY0_PRES;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);
}
整个设计思路是跟据图片解码库来设计的, piclib_ai_load_picfile()是这套代码的核心,其它的交互是围绕它和图片解码后的图片信息作的显示。具体实现可以自行分析。另外,程序中只分配了 4 个文件索引,大于4个文件需要单独适配。
40.4 下载验证
代码编译成功,下载代码到开发板上,可以看到 LCD 开始显示图片(假设 SD卡及文件都准备好了,即:在 SD 卡根目录新建: PICTURE 文件夹,并存放一些图片文件(.bmp/.jpg/.gif/.png)在该文件夹内),如图 40.4.1 所示:
图 40.4.1 图片显示实验显示效果
按 KEY0 和 KEY2 可以快速切换到下一张或上一张, KEY_UP 按键可以暂停自动播放,同时 DS1亮,指示处于暂停状态,再按一次 KEY_UP则继续播放。