|
本帖最后由 正点原子 于 2024-8-1 16:41 编辑
1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729
第四十三章 视频播放器实验
DNESP32S3的处理能力,不仅可以软解码音频,还可以用来播放视频!本章,我们将使用DNESP32S3开发板来播放AVI视频,本章我们将实现一个简单的视频播放器。
本章分为如下几个小节:
43.1 ES8388录音简介
43.2 硬件设计
43.3 程序设计
43.4 下载验证
43.1 AVI&libjpeg简介
本章,我们将使用libjepg(由IJG提供),来实现MJPEG编码的AVI格式视频播放,我们先来简单介绍一下AVI和libjpeg。
43.1.1 AVI简介
AVI是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合RIFF文件规范的数字音频与视频文件格式,原先用于Microsoft Video for Windows (简称VFW)环境,现在已被多数操作系统直接支持。
AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPEG编码方式。
在介绍AVI文件前,我们要先来看看RIFF文件结构。AVI文件采用的是RIFF文件结构方式,RIFF(Resource Interchange File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVE,MIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分:
4字节的数据块标记(或者叫做数据块的ID)
数据块的大小
数据
整个RIFF文件可以看成一个数据块,其数据块ID为RIFF,称为RIFF块。一个RIFF文件中只允许存在一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID为"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有的子块都不能再包含子块。
RIFF和LIST块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List Type)的数据域,其组成如下:
4字节的数据块标记(Chunk ID)
数据块的大小
4字节的形式类型或者列表类型(ID)
数据
下面我们看看AVI文件的结构。AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVI的RIFF块的形式类型(Form Type)是AVI,它一般包含3个子块,如下所述:
信息块,一个ID为"hdrl"的LIST块,定义AVI文件的数据格式。
数据块,一个ID为 "movi"的LIST块,包含AVI的音视频序列数据。
索引块,ID为"idxl"的子块,定义"movi"LIST块的索引数据,是可选块(不一定有)。
接下来,我们详细介绍下AVI文件的各子块构造,AVI文件的结构如图43.1.1所示:
图43.1.1 AVI文件结构图
从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。
1,信息块(HeaderList)
信息块,即ID为“hdrl”的LIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的子块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockID和BlockSize包含进来,下同)的定义如下:
- /* avih 子块信息 */
- typedef struct
- {
- uint32_t BlockID; /* 块标志:avih==0X61766968 */
- uint32_t BlockSize; /* 块大小(不包含最初8字节,也就是BlockID和BlockSize不在内*/
- uint32_t SecPerFrame; /* 视频帧间隔时间(单位为us) */
- uint32_t MaxByteSec; /* 最大数据传输率,字节/秒 */
- uint32_t PaddingGranularity; /* 数据填充的粒度 */
- uint32_t Flags; /* AVI文件的全局标记,比如是否含有索引块等 */
- uint32_t TotalFrame; /* 文件总帧数 */
- uint32_t InitFrames; /* 为交互格式指定初始帧数(非交互格式应该指定为0)*/
- uint32_t Streams; /* 包含的数据流种类个数,通常为2 */
- uint32_t RefBufSize;/* 建议读取本文件的缓存大小(应能容纳最大的块)默认是1M字节*/
- uint32_t Width; /* 图像宽 */
- uint32_t Height; /* 图像高 */
- uint32_t Reserved[4]; /* 保留 */
- } AVIH_HEADER;
复制代码 这里有很多我们要用到的信息,比如SecPerFrame,通过该参数,我们可以知道每秒钟的帧率,也就知道了每秒钟需要解码多少帧图片,才能正常播放。TotalFrame告诉我们整个视频有多少帧,结合SecPerFrame参数,就可以很方便计算整个视频的时间了。Streams告诉我们数据流的种类数,一般是2,即包含视频数据流和音频数据流。
在avih块之后,是一个或者多个strl子列表,文件中有多少种数据流(即前面的Streams),就有多少个strl子列表。每个strl子列表,至少包括一个strh(Stream Header)块和一个strf(Stream Format)块,还有一个可选的strn(Stream Name)块(未列出)。注意:strl子列表出现的顺序与媒体流的编号(比如:00dc,前面的00,即媒体流编号00)是对应的,比如第一个strl子列表说明的是第一个流(Stream 0),假设是视频流,则表征视频数据块的四字符码为“00dc”,第二个strl子列表说明的是第二个流(Stream 1),假设是音频流,则表征音频数据块的四字符码为“01dw”,以此类推。
先看strh子块,该块用于说明这个流的头信息,定义如下:
- /* strh 流头子块信息(strh∈strl) */
- typedef struct
- {
- uint32_t BlockID; /* 块标志:strh==0X73747268 */
- /* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
- uint32_t BlockSize;
- uint32_t StreamType;
- /* 数据流种类,vids(0X73646976):视频;auds(0X73647561):音频 */
- uint32_t Handler;
- /* 指定流的处理者,对于音视频来说就是解码器,比如MJPEG/H264之类的 */
- uint32_t Flags; /* 标记:是否允许这个流输出?调色板是否变化? */
- uint16_t Priority; /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流)*/
- uint16_t Language; /* 音频的语言代号 */
- uint32_t InitFrames; /* 为交互格式指定初始帧数 */
- uint32_t Scale; /* 数据量, 视频每帧的大小或者音频的采样大小 */
- uint32_t Rate; /* Scale/Rate=每秒采样数 */
- uint32_t Start; /* 数据流开始播放的位置,单位为Scale */
- uint32_t Length; /* 数据流的数据量,单位为Scale */
- uint32_t RefBufSize; /* 建议使用的缓冲区大小 */
- uint32_t Quality; /* 解压缩质量参数,值越大,质量越好 */
- uint32_t SampleSize; /* 音频的样本大小 */
- struct /* 视频帧所占的矩形 */
- {
- short Left;
- short Top;
- short Right;
- short Bottom;
- } Frame;
- } STRH_HEADER;
复制代码 这里面,对我们最有用的即StreamType 和Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPEG/H264等(实际以strf块为准)。
然后是strf子块,不过strf子块,需要根据strh子块的类型而定。
如果strh子块是视频数据流(StreamType=“vids”),则strf子块的内容定义如下:
- /* BMP结构体 */
- typedef struct
- {
- uint32_t BmpSize; /* bmp结构体大小,包含(BmpSize在内) */
- long Width; /* 图像宽 */
- long Height; /* 图像高 */
- uint16_t Planes; /* 平面数,必须为1 */
- uint16_t BitCount; /* 像素位数,0X0018表示24位 */
- uint32_t Compression; /* 压缩类型,比如:MJPEG/H264等 */
- uint32_t SizeImage; /* 图像大小 */
- long XpixPerMeter; /* 水平分辨率 */
- long YpixPerMeter; /* 垂直分辨率 */
- uint32_t ClrUsed; /* 实际使用了调色板中的颜色数,压缩格式中不使用 */
- uint32_t ClrImportant; /* 重要的颜色 */
- } BMP_HEADER;
- /* 颜色表 */
- typedef struct
- {
- uint8_t rgbBlue; /* 蓝色的亮度(值范围为0-255) */
- uint8_t rgbGreen; /* 绿色的亮度(值范围为0-255) */
- uint8_t rgbRed; /* 红色的亮度(值范围为0-255) */
- uint8_t rgbReserved; /* 保留,必须为0 */
- } AVIRGBQUAD;
- /* 对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER块 */
- typedef struct
- {
- uint32_t BlockID; /* 块标志,strf==0X73747266 */
- uint32_t BlockSize; /* 块大小(不包含最初的8字节,也就是BlockID
- 和本BlockSize不计算在内) */
- BMP_HEADER bmiHeader; /* 位图信息头 */
- AVIRGBQUAD bmColors[1]; /* 颜色表 */
- } STRF_BMPHEADER;
复制代码 这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(Width和Height),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPEG的视频格式。
如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下:
- /* 对于strh,如果是音频流,strf(流格式)使STRH_WAVHEADER块 */
- typedef struct
- {
- uint32_t BlockID; /* 块标志,strf==0X73747266 */
- uint32_t BlockSize; /* 块大小(不包含最初的8字节,也就是BlockID
- 和本BlockSize不计算在内) */
- uint16_t FormatTag; /* 格式标志:0X0001=PCM,0X0055=MP3 */
- uint16_t Channels; /* 声道数,一般为2,表示立体声 */
- uint32_t SampleRate; /* 音频采样率 */
- uint32_t BaudRate; /* 波特率 */
- uint16_t BlockAlign; /* 数据块对齐标志 */
- uint16_t Size; /* 该结构大小 */
- } STRF_WAVHEADER;
复制代码 本结构体对音频数据解码起决定性的作用,他告诉我们音频信号的编码方式(FormatTag)、声道数(Channels)和采样率(SampleRate)等重要信息。本章例程仅支持PCM格式(FormatTag=0X0001)的音频数据解码。
2、数据块(MovieList)
信息块,即ID为“movi”的LIST块,它包含AVI的音视频序列数据,是这个AVI文件的主体部分。音视频数据块交错的嵌入在“movi”LIST块里面,通过标准类型码进行区分,标准类型码有如下4种:
1)“##db”(非压缩视频帧)
2)“##dc”(压缩视频帧)
3)“##pc”(改用新的调色板)
4)“##wb”(音频帧)
其中##是编号,得根据我们的数据流顺序来确定,也就是前面的strl块。比如,如果第一个strl块是视频数据,那么对于压缩的视频帧,标准类型码就是:00dc。第二个strl块是音频数据,那么对于音频帧,标准类型码就是:01wb。
紧跟着标准类型码的是4个字节的数据长度(不包含类型码和长度参数本身,也就是总长度必须要加8才对),该长度必须是偶数,如果读到为奇数,则加1即可。我们读数据的时候,一般一次性要读完一个标准类型码所表征的数据,方便解码。
3、索引块(Index Chunk)
最后,紧跟在‘hdrl’列表和‘movi’列表之后的,就是AVI文件可选的索引块。这个索引块为AVI文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于‘movi’列表,也可能相对于AVI文件开头)。本章我们用不到索引块,这里就不详细介绍了。
关于AVI文件,我们就介绍到这,有兴趣的朋友,可以再看看光盘:6,软件资料3,AVI学习资料.zip里面的相关文档。
43.1.2 libjpeg简介
libjpeg是一个完全用C语言编写的库,包含了广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个IJG库由组织(Independent JPEG Group(独立JPEG小组))提供并维护。libjepg,目前最新版本为v9f,可以在https://www.ijg.org/files/这个网站下载。libjpeg具有稳定、兼容性强和解码速度较快等优点。
本章,我们使用libjpeg来实现MJPEG数据流的解码,MJPEG数据流,其实就是一张张的JPEG图片拼起来的图片视频流,只要能快速解码JPEG图片,就可以实现视频播放。
前面的图片显示实验我们使用了TJPGD实现JPEG解码,大家可能会问,为什么不直接用TJPGD来解码呢?因为TJPG的特点是:占用资源少,但是解码速度慢。在DNESP32S3上,同样一张320*240的JPG图片,用TJPGD来解码,需要120多毫秒,而用libjpeg,则只需要50ms左右即可完成解码,libjpeg的解码速度明显比TJPGD快了不少,使得解码视频成为可能。实际上,经过优化后的libjpeg,使用DNESP32S3不超频的情况下,可以流畅播放480*272@ 10帧的MJPEG视频(带音频)。
关于libjpeg的移植和使用,在下载的libjpeg源码里面还有很多介绍,可以重点看:readme.txt、filelist.txt、install.txt和libjpeg.txt等,也可以参考光盘源码进行移植与使用。
我们主要讲解一下如何使用libjpeg来实现一个jpeg图片的解码,这个在libjpeg源码里面:example.c,这个文件里面有简单的示例,在libjpeg.txt里面也有相关内容的介绍。我们主要简要介绍一下example.c里面标准解码流程(示例代码):
- struct my_error_mgr
- {
- struct jpeg_error_mgr pub; /* 公共”字段 */
- jmp_buf setjmp_buffer; /* 用于返回 */
- };
- typedef struct my_error_mgr * my_error_ptr;
- /**
- * @brief JPEG解码错误处理函数
- * @param 无
- * @retval 无
- */
- METHODDEF(void) my_error_exit (j_common_ptr cinfo)
- {
- my_error_ptr myerr = (my_error_ptr) cinfo->err; /* 指向cinfo->err */
- (*cinfo->err->output_message) (cinfo); /* 显示错误信息 */
- longjmp(myerr->setjmp_buffer, 1); /* 跳转到setjmp处 */
- }
- /**
- * @brief JPEG解码函数
- * @param filename : 解码文件
- * @retval 无
- */
- GLOBAL(int) read_JPEG_file(char* filename)
- {
- struct jpeg_decompress_struct cinfo;
- struct my_error_mgr jerr; /* 错误处理结构体 */
- FILE * infile; /* 输入源文件 */
- JSAMPARRAY buffer; /* 输出缓存 */
- int row_stride; /* physical row width in output buffer */
- if ((infile = fopen(filename, "rb")) == NULL) /* 尝试打开文件 */
- {
- fprintf(stderr, "can't open %s\n", filename);
- return 0;
- }
- /* 第一步,设置错误管理,初始化JPEG解码对象 */
- cinfo.err = jpeg_std_error(&jerr.pub); /* 建立JPEG错误处理流程 */
- jerr.pub.error_exit = my_error_exit; /* 处理函数指向 my_error_exit */
- /*
- * 建立my_error_exit函数使用的返回上下文,当其他地方
- * 调用longjmp函数时,可以返回到这里进行错误处理
- */
- if (setjmp(jerr.setjmp_buffer))
- {
- jpeg_destroy_decompress(&cinfo); /* 释放解码对象资源 */
- fclose(infile); /* 关闭文件 */
- return 0;
- }
- jpeg_create_decompress(&cinfo); /* 初始化解码对象cinfo */
-
- /* 第二步,指定数据源(比如一个文件) */
- jpeg_stdio_src(&cinfo, infile);
-
- /* 第三步,读取文件参数(通过jpeg_read_header函数) */
- (void)jpeg_read_header(&cinfo, TRUE);
-
- /* 第四步,设置解码参数(这里使用jpeg_read_header确认的默认参数),故无处理 */
-
- /* 第五步,开始解码 */
- (void)jpeg_start_decompress(&cinfo);
-
- /* 在读取数据之前,可以做一些处理,比如设定LCD窗口,设定LCD起始坐标等 */
-
- /* 确定一样有多少样本 */
- row_stride = cinfo.output_width * cinfo.output_components;
- /* 确保buffer至少可以保存一行的数据样本,并为其申请内存 */
- buffer = (*cinfo.mem->alloc_sarray)
- ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
- /* 第六步,循环读取数据 */
-
- /* 每次读取一样,直到读完整个文件 */
- while (cinfo.output_scanline < cinfo.output_height)
- {
- (void) jpeg_read_scanlines(&cinfo, buffer, 1);/* 解码一行数据 */
- put_scanline_someplace(buffer[0],row_stride);/* 将解码后的数据输出到某处 */
- }
- /* 第七步,结束解码 */
- (void) jpeg_finish_decompress(&cinfo);
- /* 第八步,释放解码对象资源 */
- jpeg_destroy_decompress(&cinfo); /* 释放解码申请的资源(SRAM内存) */
- fclose(infile); /* 关闭文件 */
- return 1;
- }
复制代码 以上代码,将一个jpeg解码分成了8个步骤,我们结合本例程代码简单讲解下这几个步骤。我们先来看一下一个很重要的结构体数据类型:struct jpeg_decompress_struct,定义成cinfo结构体变量,该变量保存着jpeg数据的详细信息,也保存着解码之后输出数据的详细信息。一般情况下,每次调用libjpeg库API的时候都需要把这个变量作为第一个参数传入。另外,用户也可以通过修改这个变量来修改libjpeg行为,比如输出数据格式,libjpeg库可用的最大内存等。
因为DNESP32S3堆栈有限,不能按照示例来定义cinfo和jerr结构体,cinfo和jerr都比较大(均超过400字节),很容易出现堆栈溢出的情况。在开发板源码中,我们使用的是全局指针变量,通过内存管理分配内存。
接下来看解码步骤,第一步是分配,并初始化解码对象结构体。做了两件事:1,错误管理;2,初始化解码对象。首先,错误管理使用setjmp和longjmp机制(不懂请百度)来实现类似C++的异常处理功能,外部代码可以调用longjmp来跳转到setjmp位置,执行错误管理(释放内存,关闭文件等)。这里注册了一个my_error_exit函数,来执行错误退出处理,在例程源码中,还实现了输出警告信息函数:my_emit_message,方便调试代码。然后,初始化解码对象cinfo,就是通过jpeg_create_decompress函数实现。
第二步,解压缩操作。示例代码使用的是jpeg_stdio_src函数。这个函数的作用是指定数据源,我们用另外一个函数实现:
- /**
- * @brief 设置JPEG解压缩的源数据
- * @param cinfo : 结构体指针
- Inbuffer :内存位置和大小所储存在结构体中的指针
- Insize : 数据大小
- * @retval 无
- */
- GLOBAL(void)
- jpeg_mem_src (j_decompress_ptr cinfo,
- const unsigned char * inbuffer,
- size_t insize)
- {
- struct jpeg_source_mgr * src;
- if (inbuffer == NULL || insize == 0)/* 将空输入视为致命错误 */
- ERREXIT(cinfo, JERR_INPUT_EMPTY);
- /* 源对象是永久性的,
- 因此可以通过只在第一个之前调用jpeg_mem_src从同一个缓冲区读取一系列JPEG图像 */
- if (cinfo->src == NULL) { /* first time for this JPEG object? */
- cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small)
- ((j_common_ptr) cinfo, JPOOL_PERMANENT, SIZEOF(struct jpeg_source_mgr));
- }
- src = cinfo->src;
- src->init_source = init_mem_source;
- src->fill_input_buffer = fill_mem_input_buffer;
- src->skip_input_data = skip_input_data;
- src->resync_to_restart = jpeg_resync_to_restart; /* 使用默认方法 */
- src->term_source = term_source;
- src->bytes_in_buffer = insize;
- src->next_input_byte = (const JOCTET *) inbuffer;
- }
复制代码 这里面重点是两个函数:fill_mem_input_buffer和skip_input_data,前者用于从内存填充数据给libjpeg,后者用于跳过一定字节的数据。这两个函数请看本例程源码(在mjpeg.c里面)。
第三步,读取文件参数。通过jpeg_read_header函数实现,该函数将读取JPEG的各个参数,必须在解码之前调用。
第四步,开始解码。示例代码首先调用jpeg_start_decompress函数,然后计算样本输出buffer大小,并为其申请内存,为后续读取解码后的数据做准备。
第五步,循环读取数据。通过jpeg_read_scanlines函数,循环解码并读取jpeg图片数据,实现jpeg解码。
第六步,解码结束。解码完成后,通过jepg_finish_decompress函数,结束jpeg解码。
第七步,释放解码对象资源。在所有操作完成后,通过jpeg_destroy_decompress,释放解码过程中用到的资源(比如释放内存)。
这样,我们就完成了一张jpeg图片的解码。详细的代码,请大家参考光盘本例程源码mjpeg.c。
最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下:
初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、SAI、DMA、ES8388、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
读取AVI文件,并解析
要解码,得先读取avi文件,按43.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。
根据解析结果,设置相关参数
根据第2步解析的结果,设置I²S的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。
读取数据流,开始解码
前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据。对于视频数据(00dc/01dc),本例程只支持MJPEG,通过libjpeg解码,所以将视频数据按前面所说的几个步骤解码即可。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。
解码完成,释放资源
最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止SAI播放音乐和关闭文件等一系列操作,等待下一次解码。
43.2 硬件设计
43.2.1 例程功能
1、本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放SD卡VIDEO文件夹里面的视频(.avi格式)。
注意:自备SD卡一张,并在SD卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPEG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤见<<DNESP32S3开发指南>>)。
视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY1用于选择上一个视频,KEY_UP可以快进,KEY1可以快退。
2、LED闪烁,提示程序运行。
43.2.2 硬件资源
1. LED灯
LED -IO0
2.独立按键
KEY0(XL9555) - IO1_7
KEY1(XL9555) - IO1_6
KEY2(XL9555) - IO1_5
KEY3(XL9555) - IO1_4
3. XL9555
IIC_SDA-IO41
IIC_SCL-IO42
4. SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
5. SD
CS-IO2
SCK-IO12
MOSI-IO11
MISO-IO13
6. ES8388音频CODEC芯片(IIC端口0)
IIC_SDA-IO41
IIC_SCL-IO42
I2S_BCK_IO-IO46
I2S_WS_IO-IO9
I2S_DO_IO-IO10
I2S_DI_IO-IO14
IS2_MCLK_IO-IO3
7. 开发板板载的咪头或自备麦克风输入
8. 喇叭或耳机
9. MJPEG解码库
10. 高分辨率定时器(ESP定时器)
43.2.3 原理图
本实验相关的原理图同上一章节。
43.3 程序设计
|
|