图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版的29_pitures例程中,作者在29_pitures \components\BSP路径下并未添加新的内容,而是在29_pitures \main\APP路径下面,新增了一个APP文件,我们将详细解析这四个文件的实现内容。
1,解码库的控制句柄_pic_phy和_pic_info
我们使用这个接口,把解码后的图形数据与LCD的实际操作对应起来。为了方便去显示图片,我们需要将图片的信息与我们的LCD联系上。这里我们定义了_pic_phy和_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 *);
} _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操作。 具体定义如下:
/**
* @NOTE 在画图之前,必须先调用此函数, 指定相关函数 * @retval 无
*/
void piclib_init(void)
{
pic_phy.draw_point = lcd_draw_pixel; /* 画点函数实现,仅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 = lcd_self.width; /* 得到LCD的宽度像素 */
picinfo.lcdheight = lcd_self.height; /* 得到LCD的高度像素 */
}
初始化图片解码的相关信息,这些函数必须由用户在外部实现。我们使用之前LCD的操作函数对这个结构体中的绘制操作:画点、画线、画圆等定义与我们的LCD操作对应起来。具体这些操作可以查看SPILCD一节的描述。
该函数的形参描述,如下表所示:
表40.3.1.1 函数piclib_init ()形参描述
该函数的返回值描述,如下表所示:
表40.3.1.2 函数piclib_init ()返回值描述
3,piclib_ai_load_picfile函数
piclib_ai_load_picfile帮助我们得到需要显示的图片信息并助于下一步的绘制。本函数需要结合文件系统来操作,图片根据后缀区分并且在文件夹在保存是我们在PC下的习分类,也是我们处理和分类图片的最方便的方式。
/**
* @brief 智能画图
* @note 图片仅在x,y和width, height限定的区域内显示.
*
* @param filename : 包含路径的文件名(.bmp/.jpg/.jpeg/.png/.gif等)
* @param x, y : 起始坐标
* @param width, height : 显示区域
* @param fast : 使能快速解码
* @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); /* 得到文件的类型 */
switch (temp)
{
case T_BMP:
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;
}
该函数的形参描述,如下表所示:
| |
| filename是文件的路径名,具体可以参考FATFS一节的描述,为字符口,我们的例程采用的是SD卡存图片,故一般为”0:/PICTURE/*.GIF”等类似格式。 |
| |
| |
| 形成了以x、y为起点的(x,y)~(x+width,y+height)的矩形显示区域,对屏幕坐标不理解的可能参考我们的SPILCD一节的描述。 |
| 形成了以x、y为起点的(x,y)~(x+width,y+height)的矩形显示区域,对屏幕坐标不理解的可能参考我们的SPILCD一节的描述。 |
表40.3.1.3 函数piclib_ai_load_picfile ()形参描述
该函数的返回值描述,如下表所示:
表40.3.1.4 函数piclib_ai_load_picfile ()返回值描述
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_dirs
IIC
LCD
LED
SPI
XL9555)
set(include_dirs
IIC
LCD
LED
SPI
XL9555)
set(requires
driver
fatfs)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的红色fatfs依赖库需要由开发者自行添加,以确保图片显示驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了图片显示驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
打开本实验main文件下的CMakeLists.txt文件,其内容如下所示:
idf_component_register(
SRC_DIRS
"."
"APP"
INCLUDE_DIRS
"."
"APP")
上述的红色APP驱动需要由开发者自行添加,在此便不做赘述了。
40.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
esp_err_t ret = 0;
uint8_t res = 0;
FF_DIR picdir; /* 图片目录 */
FILINFO *picfileinfo; /* 文件信息 */
char *pname; /* 带路径的文件名 */
uint16_t totpicnum; /* 图片文件总数 */
uint16_t curindex = 0; /* 图片当前索引 */
uint8_t key = 0; /* 键值 */
uint8_t pause = 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());
ret = nvs_flash_init();
}
led_init(); /* 初始化LED */
i2c0_master = iic_init(I2C_NUM_0); /* 初始化IIC0 */
spi2_init(); /* 初始化SPI */
xl9555_init(i2c0_master); /* 初始化IO扩展芯片 */
lcd_init(); /* 初始化LCD */
while (sd_spi_init()) /* 检测不到SD卡 */
{
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
vTaskDelay(500);
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
vTaskDelay(500);
}
res = exfuns_init(); /* 为fatfs相关变量申请内存 */
while (fonts_init()) /* 检查字库 */
{
lcd_clear(WHITE); /* 清屏 */
lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", 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(200);
lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
vTaskDelay(200);
}
lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
vTaskDelay(1500);
lcd_clear(WHITE); /* 清屏 */
}
text_show_string(30, 50, 200, 16, "正点原子ESP32开发板",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(200);
lcd_fill(30, 150, 240, 186, WHITE); /* 清除显示 */
vTaskDelay(200);
}
totpicnum = pic_get_tnum("0:/PICTURE"); /* 得到总有效文件数 */
while (totpicnum == NULL) /* 图片文件为0 */
{
text_show_string(30, 150, 240, 16, "没有图片文件!", 16, 0, RED);
vTaskDelay(200);
lcd_fill(30, 150, 240, 186, WHITE); /* 清除显示 */
vTaskDelay(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(200);
lcd_fill(30, 150, 240, 186, WHITE); /* 清除显示 */
vTaskDelay(200);
}
/* 记录索引 */
res = f_opendir(&picdir, "0:/PICTURE"); /* 打开目录 */
if (res == FR_OK)
{
curindex = 0; /* 当前索引为0 */
while (1) /* 全部查询一遍 */
{
temp = picdir.dptr; /* 记录当前dptr偏移 */
res = f_readdir(&picdir, picfileinfo); /* 读取目录下的一个文件 */
/* 错误了/到末尾了,退出 */
if (res != FR_OK || picfileinfo->fname[0] == 0)break;
res = exfuns_file_type(picfileinfo->fname);
if ((res & 0X0F) != 0X00) /* 取高四位,看看是不是图片文件 */
{
picoffsettbl[curindex] = temp; /* 记录索引 */
curindex++;
}
}
}
text_show_string(30, 150, 240, 16, "开始显示...", 16, 0, RED);
vTaskDelay(1500);
/* 初始化画图 */
piclib_init();
/* 从0开始显示 */
curindex = 0;
/* 打开目录 */
res = f_opendir(&picdir, (const TCHAR *)"0:/PICTURE");
/* 打开成功 */
while (res == FR_OK)
{
/* 改变当前目录索引 */
dir_sdi(&picdir, picoffsettbl[curindex]);
/* 读取目录下的一个文件 */
res = f_readdir(&picdir, picfileinfo);
/* 错误了/到末尾了,退出 */
if (res != FR_OK || picfileinfo->fname[0] == 0)break;
/* 复制路径(目录) */
strcpy((char *)pname, "0:/PICTURE/");
/* 将文件名接在后面 */
strcat((char *)pname, (const char *)picfileinfo->fname);
lcd_clear(BLACK);
/* 显示图片 */
piclib_ai_load_picfile(pname, 0, 0, lcd_self.width, lcd_self.height);
/* 显示图片名字 */
text_show_string(2, 2, lcd_self.width, 16, (char *)pname, 16, 0, RED);
t = 0;
while (1)
{
key = xl9555_key_scan(0); /* 扫描按键 */
if (t > 250)key = 1; /* 模拟一次按下KEY0 */
if ((t % 20) == 0)
{
LED_TOGGLE(); /* LED闪烁,提示程序正在运行. */
}
if (key == KEY1_PRES) /* 上一张 */
{
if (curindex)
{
curindex--;
}
else
{
curindex = totpicnum - 1;
}
break;
}
else if (key == KEY0_PRES) /* 下一张 */
{
curindex++;
if (curindex >= totpicnum)
{
curindex = 0; /* 到末尾的时候,自动从头开始 */
}
break;
}
else if (key == KEY3_PRES)
{
pause = !pause;
LED(pause); /* 暂停的时候LED1亮. */
}
if (pause == 0)t++;
vTaskDelay(10);
}
res = 0;
}
free(picfileinfo); /* 释放内存 */
free(pname); /* 释放内存 */
free(picoffsettbl); /* 释放内存 */
}
可以看到整个设计思路是跟据图片解码库来设计的,piclib_ai_load_picfile()是这套代码的核心,其它的交互是围绕它和图片解码后的图片信息作的显示。大家再仔细对照光盘中的源码进一步了解整个设置思路。另外,我们的程序中只分配了4个文件索引,故更多数量的图片无法直接在本程序下演示,大家根据自己的需要再进行修改即可。
40.4 下载验证
在代码编译成功之后,我们下载代码到开发板上,可以看到LCD开始显示图片(假设SD卡及文件都准备好了,即:在SD卡根目录新建:PICTURE文件夹,并存放一些图片文件(.bmp/.jpg/.gif/.png)在该文件夹内),如图40.4.1所示:
图40.4.1图片显示实验显示效果
按KEY0和KEY2可以快速切换到下一张或上一张,KEY_UP按键可以暂停自动播放,同时DS1亮,指示处于暂停状态,再按一次KEY_UP则继续播放。同时,由于我们的代码支持gif格式的图片显示(注意尺寸不能超过LCD屏幕尺寸),所以可以放一些gif图片到PICTURE文件夹,来看动画了。