第五十五章 视频播放器实验
STM32F767 自带了硬件 JPEG 解码器,完全可以用来播放视频!
本章,我们将使用 STM32F7
的硬件 JPEG 解码器来实现播放 AVI 视频(MJPEG 编码),本章我们将实现一个简单的视频播

放器,实现 AVI 视频播放。本章分为如下几个部:
55.1 AVI 简介
55.2 硬件设计
55.3 软件设计
55.4 下载验证
555.1 AVI 简介
本章,我们使用 STM32F7 的硬件 JPEG 解码器,来实现 MJPG 编码的 AVI 格式视频播放,
硬件 JPEG 解码器我们在第五十章已经介绍过了。接下来给大家简单介绍一下 AVI 格式。
AVI 是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合 RIFF
文件规范的数字音频与视频文件格式,原先用于 Microsoft Video for Windows (简称 VFW)环境,
现在已被多数操作系统直接支持。
AVI 格式允许视频和音频交错在一起同步播放,支持 256 色和 RLE 压缩,但 AVI 文件并
未限定压缩标准,AVI 仅仅是一个容器,用不同压缩算法生成的 AVI 文件,必须使用相应的解
压缩算法才能播放出来。比如本章,我们使用的 AVI,其音频数据采用 16 位线性 PCM 格式(未
压缩),而视频数据,则采用 MJPG 编码方式。
在介绍 AVI 文件前,我们要先来看看 RIFF 文件结构。AVI 文件采用的是 RIFF 文件结构
方式,RIFF(Resource Interchange File Format,资源互换文件格式)是微软定义的一种用于管
理 WINDOWS 环境中多媒体数据的文件格式,波形音频 WAVE,MIDI 和数字视频 AVI 都采用
这种格式存储。构造 RIFF 文件的基本单元叫做数据块(Chunk),每个数据块包含 3 个部分,
1、4 字节的数据块标记(或者叫做数据块的 ID)
2、数据块的大小
3、数据
整个 RIFF 文件可以看成一个数据块,其数据块 ID 为 RIFF,称为 RIFF 块。一个 RIFF 文
件中只允许存在一个 RIFF 块。RIFF 块中包含一系列的子块,其中有一种子块的 ID 为"LIST",
称为 LIST 块,LIST 块中可以再包含一系列的子块,但除了 LIST 块外的其他所有的子块都不
能再包含子块。
RIFF 和 LIST 块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List
Type)的数据域,其组成如下:
1、4 字节的数据块标记(Chunk ID)
2、数据块的大小
3、4 字节的形式类型或者列表类型(ID)
4、数据
下面我们看看 AVI 文件的结构。AVI 文件是目前使用的最复杂的 RIFF 文件,它能同时存
储同步表现的音频视频数据。AVI 的 RIFF 块的形式类型(Form Type)是 AVI,它一般包含 3
个子块,如下所述:
1、信息块,一个 ID 为"hdrl"的 LIST 块,定义 AVI 文件的数据格式。
2、数据块,一个 ID 为 "movi"的 LIST 块,包含 AVI 的音视频序列数据。
3、索引块,ID 为"idxl"的子块,定义"movi"LIST 块的索引数据,是可选块(不一定有)。
接下来,我们详细介绍下 AVI 文件的各子块构造,AVI 文件的结构如图 55.1.1 所示:
图 55.1.1 AVI 文件结构图
从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI 文件,由:信息块(HeaderList)、
数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。
1,信息块(HeaderList)
信息块,即 ID 为“hdrl”的 LIST 块,它包含文件的通用信息,定义数据格式,所用的压
缩算法等参数等。hdrl 块还包括了一系列的字块,首先是:avih 块,用于记录 AVI 的全局信息,
比如数据流的数量,视频图像的宽度和高度等信息,avih 块(结构体都有把 BlockID 和 BlockSize
包含进来,下同)的定义如下:
//avih 子块信息
typedef struct
{
u32 BlockID;
//块标志:avih==0X61766968
u32 BlockSize;
//块大小(不包含最初的 8 字节,即 BlockID 和 BlockSize 不算)
u32 SecPerFrame;
//视频帧间隔时间(单位为 us)
u32 MaxByteSec;
//最大数据传输率,字节/秒
u32 PaddingGranularity; //数据填充的粒度
u32 Flags;
//AVI 文件的全局标记,比如是否含有索引块等
u32 TotalFrame;
//文件总帧数
u32 InitFrames;
//为交互格式指定初始帧数(非交互格式应该指定为 0)
u32 Streams;
//包含的数据流种类个数,通常为 2
u32 RefBufSize;
//建议读取本文件的缓存大小(应能容纳最大的块)
u32 Width;
//图像宽
u32 Height;
//图像高
u32 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
{
u32 BlockID;
//块标志:strh==0X73747268
u32 BlockSize;
//块大小(不包含最初的 8 字节,即 BlockID 和 BlockSize 不算)
u32 StreamType;
//数据流种类,vids(0X73646976):视频;auds(0X73647561):音频
u32 Handler;
//指定流的处理者,对于音视频来说即解码器,如 MJPG/H264 等.
u32 Flags;
//标记:是否允许这个流输出?调色板是否变化?
u16 Priority;
//流的优先级(当有多个同类型的流时优先级最高的为默认流)
u16 Language;
//音频的语言代号
u32 InitFrames;
//为交互格式指定初始帧数
u32 Scale;
//数据量, 视频每桢的大小或者音频的采样大小
u32 Rate;
//Scale/Rate=每秒采样数
u32 Start;
//数据流开始播放的位置,单位为 Scale
u32 Length;
//数据流的数据量,单位为 Scale
u32 RefBufSize;
//建议使用的缓冲区大小
u32 Quality;
//解压缩质量参数,值越大,质量越好
u32 SampleSize;
//音频的样本大小
struct
//视频帧所占的矩形
{
short Left;
short Top;
short Right;
short Bottom;
}Frame;
}STRH_HEADER;
这里面,对我们最有用的即 StreamType 和 Handler 这两个参数了,StreamType 用于告诉我
们此 strl 描述的是音频流(“auds”),还是视频流(“vids”)。而 Handler 则告诉我们所使用的解
码器,比如 MJPG/H264 等(实际以 strf 块为准)。
然后是 strf 子块,不过 strf 字块,需要根据 strh 字块的类型而定。
如果 strh 子块是视频数据流(StreamType=“vids”),则 strf 子块的内容定义如下:
//BMP 结构体
typedef struct
{
u32 BmpSize;
//bmp 结构体大小,包含(BmpSize 在内)
long Width;
//图像宽
long Height;
//图像高
u16 Planes;
//平面数,必须为 1
u16 BitCount;
//像素位数,0X0018 表示 24 位
u32 Compression;
//压缩类型,比如:MJPG/H264 等
u32 SizeImage;
//图像大小
long XpixPerMeter;
//水平分辨率
long YpixPerMeter;
//垂直分辨率
u32 ClrUsed;
//实际使用了调色板中的颜色数,压缩格式中不使用
u32 ClrImportant;
//重要的颜色
}BMP_HEADER;
//颜色表
typedef struct
{
u8 rgbBlue;
//蓝色的亮度(值范围为 0-255)
u8 rgbGreen;
//绿色的亮度(值范围为 0-255)
u8 rgbRed;
//红色的亮度(值范围为 0-255)
u8 rgbReserved;
//保留,必须为 0
}AVIRGBQUAD;
//对于 strh,如果是视频流,strf(流格式)使 STRF_BMPHEADER 块
typedef struct
{
u32 BlockID;
//块标志,strf==0X73747266
u32 BlockSize;
//块大小(不包含最初的 8 字节,即 BlockID 和 BlockSize 不算)
BMP_HEADER bmiHeader; //位图信息头
AVIRGBQUAD bmColors[1];
//颜色表
}STRF_BMPHEADER;
这里有 3 个结构体,strf 子块完整内容即:STRF_BMPHEADER 结构体,不过对我们有用
的信息,都存放在 BMP_HEADER 结构体里面,本结构体对视频数据的解码起决定性的作用,
它告诉我们视频的分辨率(Width 和 Height),以及视频所用的编码器(Compression),因此它
决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是 MJPG
的视频格式。
如果 strh 子块是音频数据流(StreamType=“auds”),则 strf 子块的内容定义如下:
//对于 strh,如果是音频流,strf(流格式)使 STRF_WAVHEADER 块
typedef struct
{
u32 BlockID;
//块标志,strf==0X73747266
u32 BlockSize;
//块大小(不包含最初的 8 字节,即 BlockID 和 BlockSize 不算)
u16 FormatTag;
//格式标志:0X0001=PCM,0X0055=MP3...
u16 Channels;
//声道数,一般为 2,表示立体声
u32 SampleRate;
//音频采样率
u32 BaudRate;
//波特率
u16 BlockAlign;
//数据块对齐标志
u16 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,软件资料AVI 学
习资料 里面的相关文档。
最后,我们看看要实现 avi 视频文件的播放,主要有哪些步骤,如下:
1)初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SDMMC(驱动 SD 卡用)、I2S、DMA、
WM8978、LCD 和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就
不再细说了。
2)读取 AVI 文件,并解析
要解码,得先读取 avi 文件,按 55.1.1 节的介绍,读取出音视频关键信息,音频参数:编
码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺
寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始
化音视频解码,为后续解码做好准备。
3)根据解析结果,设置相关参数
根据第 2 步解析的结果,设置 I2S 的音频采样率和位数,同时要让视频显示在 LCD 中间区
域,得根据图片尺寸,设置 LCD 开窗时 x,y 方向的偏移量。
4)读取数据流,开始解码
前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi 块),根据类型码,
执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的 PCM 数据,所以,直
接填充到DMA缓冲区即可,由DMA循环发送给WM8978,播放音频。对于视频数据(00dc/01dc),
本例程只支持 MJPG,通过硬件 JPEG 解码,硬件 JPEG 解码流程详见第五十章。然后,利用定
时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。
5)解码完成,释放资源
最后在文件读取完后(或者出错了),需要释放申请的内存、恢复 LCD 窗口、关闭定时器、
停止 I2S 播放音乐和关闭文件等一系列操作,等待下一次解码。
55.2 硬件设计
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,
则开始播放 SD 卡 VIDEO 文件夹里面的视频(.avi 格式)。注意:1,在 SD 卡根目录必须建
立一个 VIDEO 文件夹,并存放 AVI 视频(仅支持 MJPG 视频,音频必须是 PCM,且视频分辨
率必须小于等于屏幕分辨率)在里面。2,我们所需要的视频,可以通过:狸窝全能视频转换
器,转换后得到,具体步骤后续会讲到(55.4 节)。
视频播放时,LCD 上还会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、
帧率、播放时间和总时间等信息。KEY0 用于选择下一个视频,KEY2 用于选择上一个视频,
KEY_UP 可以快进,KEY1 可以快退。DS0 还是用于指示程序运行状态(仅字库错误时)。
本实验用到的资源如下:
1) 指示灯 DS0
2) 4 个按键(KEY_UP/KEY0/KEY1/KEY2)
3) 串口
4) LCD 模块
5) SD 卡
6) SPI FLASH
7) WM8978
8) SAI
9) 硬件 JPEG 解码器
这些前面都已介绍过。本实验,大家需要准备 1 个 SD 卡和一个耳机,分别插入 SD 卡接
口和耳机接口(PHONE),然后下载本实验就可以看视频了!
55.3 软件设计
本实验,我们音乐播放器实验(第五十二章)的基础上进行修改。本章要用到硬件 JPEG
解码和定时器,所以添加 jpegcodec.c、jpeg_utils.c 和 timer.c。
之后,在工程目录新建 MJPEG 文件夹,在该文件夹里面新建 JPEG 文件夹,新建 avi.c、
avi.h、mjpeg.c 和 mjpeg.h 四个文件。然后,工程里面,新建 MJPEG 分组,将 avi.c 和 mjpeg.c
添加到该分组下面,并将 MJPEG 文件夹加入头文件包含路径。
最后,在 APP 文件夹下面新建 videoplayer.c 和 videoplayer.h 两个文件,然后将 videoplayer.c
加入到工程的 APP 组下。本例程代码比较多,这里我们只介绍一些重要的函数。详细代码,请
大家参考本例程源码。
首先是 avi.c 里面的几个函数,代码如下:
AVI_INFO avix;//avi 文件相关信息u8const AVI_VIDS_FLAG_TBL[2]={"00dc","01dc"};//视频编码标志字符串,00dc/01dcu8const AVI_AUDS_FLAG_TBL[2]={"00wb","01wb"};//音频编码标志字符串,00wb/01wb//avi 解码初始化//buf:输入缓冲区//size:缓冲区大小//返回值:AVI_OK,avi 文件解析成功// 其他,错误代码AVISTATUS avi_init(u8 buf,u32 size){u16 offset;u8 tbuf;AVISTATUS res=AVI_OK;AVI_HEADER aviheader;LIST_HEADER listheader;AVIH_HEADER avihheader;STRH_HEADER strhheader;STRF_BMPHEADER bmpheader;STRF_WAVHEADER wavheader;tbuf=buf;aviheader=(AVI_HEADER)buf;if(aviheader->RiffID!=AVI_RIFF_ID)return AVI_RIFF_ERR;//RIFF ID 错误if(aviheader->AviID!=AVI_AVI_ID)return AVI_AVI_ERR;//AVI ID 错误buf+=sizeof(AVI_HEADER);//偏移listheader=(LIST_HEADER)(buf); if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR; //LIST ID 错误if(listheader->ListType!=AVI_HDRL_ID)return AVI_HDRL_ERR; //HDRL ID 错误buf+=sizeof(LIST_HEADER); //偏移avihheader=(AVIH_HEADER)(buf);if(avihheader->BlockID!=AVI_AVIH_ID)return AVI_AVIH_ERR; //AVIH ID 错误avix.SecPerFrame=avihheader->SecPerFrame; //得到帧间隔时间avix.TotalFrame=avihheader->TotalFrame; //得到总帧数 buf+=avihheader->BlockSize+8; //偏移listheader=(LIST_HEADER)(buf);if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR; //LIST ID 错误if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR; //STRL ID 错误 strhheader=(STRH_HEADER)(buf+12);if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR; //STRH ID 错误if(strhheader->StreamType==AVI_VIDS_STREAM) //视频帧在前{if(strhheader->Handler!=AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;//非 MJPG 视频流,不支持avix.VideoFLAG=(u8)AVI_VIDS_FLAG_TBL[0]; //视频流标记 "00dc"avix.AudioFLAG=(u8)AVI_AUDS_FLAG_TBL[1]; //音频流标记 "01wb"bmpheader=(STRF_BMPHEADER)(buf+12+strhheader->BlockSize+8);//strfif(bmpheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;//STRF ID 错误 avix.Width=bmpheader->bmiHeader.Width;avix.Height=bmpheader->bmiHeader.Height; buf+=listheader->BlockSize+8; //偏移listheader=(LIST_HEADER)(buf);if(listheader->ListID!=AVI_LIST_ID)//是不含有音频帧的视频文件{avix.SampleRate=0; //音频采样率avix.Channels=0; //音频通道数avix.AudioType=0; //音频格式}else{if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR;//STRL ID 错误 strhheader=(STRH_HEADER)(buf+12);if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR;//STRH ID 错误if(strhheader->StreamType!=AVI_AUDS_STREAM)return AVI_FORMAT_ERR;//格式错误wavheader=(STRF_WAVHEADER)(buf+12+strhheader->BlockSize+8);//strfif(wavheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;//STRF ID 错误 avix.SampleRate=wavheader->SampleRate; //音频采样率 avix.Channels=wavheader->Channels; //音频通道数avix.AudioType=wavheader->FormatTag; //音频格式}}else if(strhheader->StreamType==AVI_AUDS_STREAM)//音频帧在前{ avix.VideoFLAG=(u8)AVI_VIDS_FLAG_TBL[1]; //视频流标记 "01dc"avix.AudioFLAG=(u8)AVI_AUDS_FLAG_TBL[0]; //音频流标记 "00wb"wavheader=(STRF_WAVHEADER)(buf+12+strhheader->BlockSize+8);//strfif(wavheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR; //STRF ID 错误avix.SampleRate=wavheader->SampleRate; //音频采样率avix.Channels=wavheader->Channels; //音频通道数avix.AudioType=wavheader->FormatTag; //音频格式buf+=listheader->BlockSize+8; //偏移listheader=(LIST_HEADER)(buf);if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR; //LIST ID 错误if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR;//STRL ID 错误 strhheader=(STRH_HEADER)(buf+12);if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR;//STRH ID 错误if(strhheader->StreamType!=AVI_VIDS_STREAM)return AVI_FORMAT_ERR;//格式错误 bmpheader=(STRF_BMPHEADER)(buf+12+strhheader->BlockSize+8);//strfif(bmpheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR; //STRF ID 错误 if(bmpheader->bmiHeader.Compression!=AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;//格式错误 avix.Width=bmpheader->bmiHeader.Width;avix.Height=bmpheader->bmiHeader.Height; }offset=avi_srarch_id(tbuf,size,"movi"); //查找 movi IDif(offset==0)return AVI_MOVI_ERR; //MOVI ID 错误if(avix.SampleRate)//有音频流,才查找{tbuf+=offset;offset=avi_srarch_id(tbuf,size,avix.AudioFLAG); //查找音频流标记if(offset==0)return AVI_STREAM_ERR; //流错误tbuf+=offset+4;avix.AudioBufSize=((u16)tbuf); //得到音频流 buf 大小. }return res;}//查找 ID//buf:待查缓存区 //size:缓存大小//id:要查找的 id,必须是 4 字节长度//返回值:0,查找失败,其他:movi ID 偏移量u16 avi_srarch_id(u8 buf,u32 size,u8 id){u16 i;size-=4;for(i=0;i<size;i++){ if(buf[i]==id[0])if(buf[i+1]==id[1])if(buf[i+2]==id[2])if(buf[i+3]==id[3])return i;//找到"id"所在的位置}return 0;}//得到 stream 流信息//buf:流开始地址(必须是 01wb/00wb/01dc/00dc 开头)AVISTATUS avi_get_streaminfo(u8 buf){avix.StreamID=MAKEWORD(buf+2); //得到流类型avix.StreamSize=MAKEDWORD(buf+4); //得到流大小if(avix.StreamSize%2)avix.StreamSize++; //奇数加 1(avix.StreamSize,必须是偶数)if(avix.StreamID==AVI_VIDS_FLAG||avix.StreamID==AVI_AUDS_FLAG)return AVI_OK;return AVI_STREAM_ERR;}
这里三个函数,其中 avi_ini 用于解析 AVI 文件,获取音视频流数据的详细信息,为后续解
码做准备。而 avi_srarch_id 用于查找某个 ID,可以是 4 个字节长度的 ID,比如 00dc,01wb,
movi 之类的,在解析数据以及快进快退的时候,有用到。avi_get_streaminfo 函数,则是用来获
取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
接下来,我们看 mjpeg.c 里面的几个函数,代码如下:
//mjpeg 解码初始化//offx,offy:x,y 方向的偏移//返回值:0,成功;// 1,失败u8 mjpeg_init(u16 offx,u16 offy,u32 width,u32 height){u8 res;res=JPEG_Core_Init(&mjpeg); //初始化 JPEG 内核if(res)return 1;rgb565buf=mymalloc(SRAMEX,widthheight2); //申请 RGB 缓存if(rgb565buf==NULL)return 2; imgoffx=offx;imgoffy=offy;mjpeg_rgb_framebuf=(u16)ltdc_framebuf[lcdltdc.activelayer];//指向 RGBLCD 当前显存return 0; }//mjpeg 结束,释放内存void mjpegdec_free(void){ JPEG_Core_Destroy(&mjpeg); myfree(SRAMEX,rgb565buf);}//填充颜色//x,y:起始坐标//width,height:宽度和高度。//color:颜色数组void mjpeg_fill_color(u16 x,u16 y,u16 width,u16 height,u16 color){ u16 i,j;u32 param1;u32 param2;u32 param3;u16 pdata; if(lcdltdc.pwidth!=0&&lcddev.dir==0)//如果是 RGB 屏,且竖屏,则填充函数不可直接用{ param1=lcdltdc.pixsizelcdltdc.pwidth(lcdltdc.pheight-x-1)+lcdltdc.pixsizey;//将运算先做完,提高速度param2=lcdltdc.pixsizelcdltdc.pwidth;for(i=0;i<height;i++){param3=ilcdltdc.pixsize+param1;pdata=color+iwidth;for(j=0;j<width;j++){ (u16)((u32)mjpeg_rgb_framebuf+param3-param2j)=pdata[j]; } }}else LCD_Color_Fill(x,y,x+width-1,y+height-1,color);//其他情况,直接填充}//解码一副 JPEG 图片//buf:jpeg 数据流数组//bsize:数组大小//返回值:0,成功// 其他,错误u8 mjpegdec_decode(u8 buf,u32 bsize){vu32 timecnt=0;u8 fileover=0;u8 i=0; u32 mcublkindex=0; if(bsize==0)return 0;JPEG_Decode_Init(&mjpeg); //初始化硬件 JPEG 解码器for(i=0;i<JPEG_DMA_INBUF_NB;i++){if(bsize>JPEG_DMA_INBUF_LEN){mymemcpy(mjpeg.inbuf[i].buf,buf,JPEG_DMA_INBUF_LEN);mjpeg.inbuf[i].size=JPEG_DMA_INBUF_LEN; //读取了的数据长度mjpeg.inbuf[i].sta=1; //标记 buf 满buf+=JPEG_DMA_INBUF_LEN; //源数组往后偏移bsize-=JPEG_DMA_INBUF_LEN; //文件大小减少}else{mymemcpy(mjpeg.inbuf[i].buf,buf,bsize);mjpeg.inbuf[i].size=bsize; //读取了的数据长度mjpeg.inbuf[i].sta=1; //标记 buf 满buf+=bsize; //源数组往后偏移bsize=0; //文件大小为 0 了.break;} }JPEG_IN_OUT_DMA_Init((u32)mjpeg.inbuf[0].buf,(u32)mjpeg.outbuf[0].buf,mjpeg.inbuf[0].size,JPEG_DMA_OUTBUF_LEN);//配置 DMAjpeg_in_callback=mjpeg_dma_in_callback; //JPEG DMA 读取数据回调函数jpeg_out_callback=mjpeg_dma_out_callback; //JPEG DMA 输出数据回调函数jpeg_eoc_callback=mjpeg_endofcovert_callback;//JPEG 解码结束回调函数jpeg_hdp_callback=mjpeg_hdrover_callback; //JPEG Header 解码完成回调函数JPEG_DMA_Start(); //启动 DMA 传输while(1){ SCB_CleanInvalidateDCache(); //清空 D catchif(mjpeg.inbuf[mjpeg.inbuf_write_ptr].sta==0&&fileover==0) //有 buf 为空{if(bsize>JPEG_DMA_INBUF_LEN){mymemcpy(mjpeg.inbuf[mjpeg.inbuf_write_ptr].buf,buf,JPEG_DMA_INBUF_LEN); mjpeg.inbuf[mjpeg.inbuf_write_ptr].size=JPEG_DMA_INBUF_LEN;//读取了的数据长度mjpeg.inbuf[mjpeg.inbuf_write_ptr].sta=1; //标记 buf 满buf+=JPEG_DMA_INBUF_LEN; //源数组往后偏移bsize-=JPEG_DMA_INBUF_LEN; //文件大小减少}else{mymemcpy(mjpeg.inbuf[mjpeg.inbuf_write_ptr].buf,buf,bsize);mjpeg.inbuf[mjpeg.inbuf_write_ptr].size=bsize; //读取了的数据长度mjpeg.inbuf[mjpeg.inbuf_write_ptr].sta=1; //标记 buf 满buf+=bsize; //源数组往后偏移bsize=0; //文件大小为 0 了.timecnt=0; //清零计时器fileover=1; //文件结束了...} if(mjpeg.indma_pause==1&&mjpeg.inbuf[mjpeg.inbuf_read_ptr].sta==1)//之前是暂停的了,继续传输{JPEG_IN_DMA_Resume((u32)mjpeg.inbuf[mjpeg.inbuf_read_ptr].buf,mjpeg.inbuf[mjpeg.inbuf_read_ptr].size); //继续下一次 DMA 传输mjpeg.indma_pause=0;}mjpeg.inbuf_write_ptr++;if(mjpeg.inbuf_write_ptr>=JPEG_DMA_INBUF_NB)mjpeg.inbuf_write_ptr=0;}if(mjpeg.outbuf[mjpeg.outbuf_read_ptr].sta==1) //buf 里面有数据要处理{mcublkindex+=mjpeg.ycbcr2rgb(mjpeg.outbuf[mjpeg.outbuf_read_ptr].buf,(u8)rgb565buf,mcublkindex,mjpeg.outbuf[mjpeg.outbuf_read_ptr].size);//YUV --> RGB565mjpeg.outbuf[mjpeg.outbuf_read_ptr].sta=0;//标记 buf 为空mjpeg.outbuf[mjpeg.outbuf_read_ptr].size=0; //数据量清空mjpeg.outbuf_read_ptr++;if(mjpeg.outbuf_read_ptr>=JPEG_DMA_OUTBUF_NB)mjpeg.outbuf_read_ptr=0;//限制范围if(mcublkindex==mjpeg.total_blks){break;}}else if(mjpeg.outdma_pause==1&&mjpeg.outbuf[mjpeg.outbuf_write_ptr].sta==0)//out 暂停,且当前 writebuf 已经为空了,则恢复 out 输出{JPEG_OUT_DMA_Resume((u32)mjpeg.outbuf[mjpeg.outbuf_write_ptr].buf,JPEG_DMA_OUTBUF_LEN);//继续下一次 DMA 传输 mjpeg.outdma_pause=0;}timecnt++; if(fileover)//文件结束后,及时退出,防止死循环{if(mjpeg.state==JPEG_STATE_NOHEADER)break; //解码失败了if(timecnt>0X3FFF)break; //超时退出}} if(mjpeg.state==JPEG_STATE_FINISHED) //解码完成了{mjpeg_fill_color(imgoffx,imgoffy,mjpeg.Conf.ImageWidth,mjpeg.Conf.ImageHeight,rgb565buf); } return 0;}
其中,mjpeg_init 函数,用于初始化 jpeg 解码 ,调用 JPEG_Core_Init 函数,对硬件 JPEG
解码内核进行初始化,然后申请内存,确定视频在液晶上面的偏移(让视频显示在 LCD 中央)。
mjpeg_free 函数,用于释放内存,解码结束后调用。
mjpeg_fill_color 函数,用于解码完成后,将 RGB565 数据填充到液晶屏上,对于 RGB 屏
的竖屏模式,不能用 DMA2D 填充,只能打点的方式填充,通过计算参量,提高打点速度。对
于 MCU 屏和 RGB 横屏,则直接调用 LCD_Color_Fill 函数进行填充即可。
mjpeg_decode 函数,是解码 jpeg 的主要函数,解码步骤参见第五十章相关内容。解码后将
YUV 转换成 RGB565 数据,存放在 rgb565buf 里面,然后通过 mjpeg_fill_color 函数,将 RGB565
数据显示到 LCD 屏幕上 。
接下来,我们看 videoplayer.c 里面 video_play_mjpeg 函数,代码如下:
//播放一个 mjpeg 文件//pname:文件名//返回值://KEY0_PRES:下一曲//KEY1_PRES:上一曲//其他:错误u8 video_play_mjpeg(u8 pname){ u8 framebuf; //视频解码 bufu8 pbuf; //buf 指针 FIL favi;u8 res=0;u16 offset=0; u32 nr; u8 key; u8 saisavebuf; saibuf[0]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存 saibuf[1]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存saibuf[2]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存saibuf[3]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存framebuf=mymalloc(SRAMIN,AVI_VIDEO_BUF_SIZE); //申请视频 buffavi=(FIL)mymalloc(SRAMIN,sizeof(FIL)); //申请 favi 内存memset(saibuf[0],0,AVI_AUDIO_BUF_SIZE);memset(saibuf[1],0,AVI_AUDIO_BUF_SIZE); memset(saibuf[2],0,AVI_AUDIO_BUF_SIZE);memset(saibuf[3],0,AVI_AUDIO_BUF_SIZE); if(!saibuf[3]||!framebuf||!favi){printf("memory error!\r\n");res=0XFF;} while(res==0){ res=f_open(favi,(char )pname,FA_READ);if(res==0){pbuf=framebuf;res=f_read(favi,pbuf,AVI_VIDEO_BUF_SIZE,&nr);//开始读取if(res){printf("fread error:%d\r\n",res);break;} //开始 avi 解析res=avi_init(pbuf,AVI_VIDEO_BUF_SIZE); //avi 解析if(res){printf("avi err:%d\r\n",res);break;} video_info_show(&avix); TIM6_Init(avix.SecPerFrame/100-1,10800-1);//10Khz 计数频率,加 1 是 100us offset=avi_srarch_id(pbuf,AVI_VIDEO_BUF_SIZE,"movi");//寻找 movi IDavi_get_streaminfo(pbuf+offset+4); //获取流信息f_lseek(favi,offset+12); //跳过标志 ID,读地址偏移到流数据开始处res=mjpeg_init((lcddev.width-avix.Width)/2,110+(lcddev.height-110-avix.Height)/2,avix.Width,avix.Height);//JPG 解码初始化if(avix.SampleRate) //有音频信息,才初始化 { WM8978_I2S_Cfg(2,0);//飞利浦标准,16 位数据长度SAIA_Init(SAI_MODEMASTER_TX,SAI_CLOCKSTROBING_RISINGEDGE,SAI_DATASIZE_16);//设置 SAI,主发送,16 位数据SAIA_SampleRate_Set(avix.SampleRate); //设置采样率SAIA_TX_DMA_Init(saibuf[1],saibuf[2],avix.AudioBufSize/2,1);//配置 DMAsai_tx_callback=audio_sai_dma_callback;//回调函数指向 SAI_DMA_Callbacksaiplaybuf=0;saisavebuf=0; SAI_Play_Start(); //开启 sai 播放}while(1)//播放循环{if(avix.StreamID==AVI_VIDS_FLAG)//视频流{pbuf=framebuf;f_read(favi,pbuf,avix.StreamSize+8,&nr);//读入整帧+下一数据流 ID 信息 res=mjpeg_decode(pbuf,avix.StreamSize);if(res){printf("decode error!\r\n");} while(frameup==0);//等待时间到达(在 TIM6 的中断里面设置为 1)frameup=0; //标志清零frame++; }else //音频流{ video_time_show(favi,&avix); //显示当前播放时间saisavebuf++;if(saisavebuf>3)saisavebuf=0;do{nr=saiplaybuf;if(nr)nr--;else nr=3; }while(saisavebuf==nr);//碰撞等待.f_read(favi,saibuf[saisavebuf],avix.StreamSize+8,&nr);//填充 saibufpbuf=saibuf[saisavebuf]; } key=KEY_Scan(0); if(key==KEY0_PRES||key==KEY2_PRES)//KEY0/KEY2 按下,播放下一个/上一个视频{res=key;break; }else if(key==KEY1_PRES||key==WKUP_PRES){SAI_Play_Stop();//关闭音频video_seek(favi,&avix,framebuf);pbuf=framebuf;SAI_Play_Start();//开启 DMA 播放}if(avi_get_streaminfo(pbuf+avix.StreamSize))//读取下一帧 流标志{printf("frame error \r\n"); res=KEY0_PRES;break; } }SAI_Play_Stop(); //关闭音频TIM6->CR1&=~(1<<0); //关闭定时器 6LCD_Set_Window(0,0,lcddev.width,lcddev.height);//恢复窗口mjpeg_free(); //释放内存f_close(favi); } }myfree(SRAMIN,saibuf[0]);myfree(SRAMIN,saibuf[1]);myfree(SRAMIN,saibuf[2]);myfree(SRAMIN,saibuf[3]);myfree(SRAMIN,framebuf);myfree(SRAMIN,favi);return res;}
该函数用来播放一个 avi 视频文件(mjpg 编码),解码过程就是根据前面我们在 55.1.2 节
最后所介绍的步骤进行,不过在这里,我们的音频播放用了 4 个 buf,以提高解码的流畅度。
其他代码,我们就不再介绍了,请大家参考本例程源码。
最后,看看主函数:
int main(void){ Cache_Enable(); //打开 L1-Cache MPU_Memory_Protection(); //保护相关存储区域 HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhz delay_init(216); //延时初始化uart_init(115200); //串口初始化 TIM3_Init(10000-1,10800-1); //10Khz 计数,1 秒钟中断一次 LED_Init(); //初始化 LED KEY_Init(); //初始化按键 SDRAM_Init(); //初始化 SDRAM LCD_Init(); //初始化 LCDW25QXX_Init(); //初始化 W25Q256 WM8978_Init(); //初始化 WM8978WM8978_ADDA_Cfg(1,0); //开启 DACWM8978_Input_Cfg(0,0,0); //关闭输入通道WM8978_Output_Cfg(1,0); //开启 DAC 输出 WM8978_HPvol_Set(40,40); //耳机音量设置WM8978_SPKvol_Set(50); //喇叭音量设置 my_mem_init(SRAMIN); //初始化内部内存池 my_mem_init(SRAMEX); //初始化外部 SDRAM 内存池 my_mem_init(SRAMDTCM); //初始化内部 CCM 内存池 exfuns_init(); //为 fatfs 相关变量申请内存 f_mount(fs[0],"0:",1); //挂载 SD 卡f_mount(fs[1],"1:",1); //挂载 SPI FLASH. f_mount(fs[2],"2:",1); //挂载 NAND FLASH. POINT_COLOR=RED; while(font_init()) //检查字库{ LCD_ShowString(30,50,200,16,16,"Font Error!");delay_ms(200); LCD_Fill(30,50,240,66,WHITE);//清除显示 delay_ms(200); } POINT_COLOR=RED; Show_Str(60,50,200,16,"阿波罗 STM32F4/F7 开发板",16,0); Show_Str(60,70,200,16,"视频播放器实验",16,0); Show_Str(60,90,200,16,"正点原子@ALIENTEK",16,0); Show_Str(60,110,200,16,"2016 年 7 月 18 日",16,0);Show_Str(60,130,200,16,"KEY0:NEXT KEY2:PREV",16,0); Show_Str(60,150,200,16,"KEY_UP:FF KEY1:REW",16,0);delay_ms(1500);while(1){ video_play(); }}
该函数代码比较简单,我们就不多说了。最后,为了提高速度,我们对编译器进行设置,
选择使用-O2 优化,从而优化代码,提高速度(但调试效果不好,建议调试时设置为-O0),编
译器设置如图 55.3.1 所示:
图 52.3.2 编译器优化设置
设置完后,重新编译即可。至此,本实验的软件设计部分结束。
55.4 下载验证
本章,我们例程仅支持 MJPG 编码的 avi 格式视频,且音频必须是 PCM 格式,另外视频分
辨率不能大于 LCD 分辨率。要满足这些要求,现成的 avi 文件是很难找到的,所以我们需要用
软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频
转换器,这款软件来实现(路径:光盘:6,软件资料软件视频转换软件狸窝全能视频转
换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图 55.4.1 和 55.4.2 所示:
图 55.4.1 软件启动界面和设置
图 55.4.2 高级设置
首先,如图 55.4.1 所示,点击 1 处,添加视频,找到你要转换的视频,添加进来。有的视
频可能有独立字幕,比如我们打开的这个视频就有,所以在 2 处选择下字幕(如果没有的,可
以忽略此步)。然后在3处,点击图标,选择预制方案:AVI-Audio-Video Interleaved(.avi),
即生成.avi 文件,然后点击 4 处的高级设置按钮,进入 55.4.2 所示的界面,设置详细参数如
下:
视频编码器:选择 MJPEG。本例程仅支持 MJPG 视频解码,所以选择这个编码器。
视频尺寸:480x272。这里得根据所用 LCD 分辨率来选择,假设我们用 480800 的 4.3 寸电
容屏模块,则这里最大可以设置:480x272。PS:如果是 2.8 屏,最大宽度只能是 240)
比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为 1000,
可以得到比较好的视频质量,同时也不怎么会卡。
帧率:10。即每秒钟 10 帧。对于 480272 的视频,本例程最高能播放 30 帧左右的视频,
如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。
音频编码器:PCMS16LE。本例程只支持 PCM 音频,所以选择音频编码器为这个。
采样率:这里设置为 11025,即 11.025Khz 的采样率。这里越高,声音质量越好,不过,
转换后的文件就越大,而且视频可能会卡。
其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。
点击图55.4.1的5处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,
这样转换后的视频,会保存在桌面。最后,点击图中 6 处的按钮,即可开始转换了,如图 55.4.3
所示:
图 55.4.3 正在转换
等转换完成后,将转换后的.avi 文件,拷贝到 SD 卡VIDEO 文件夹下,然后插入开发板
的 SD 卡接口,就可以开始测试本章例程了。
在代码编译成功之后,我们下载代码到 ALIENTEK 阿波罗 STM32 开发板上,程序先检测
字库,然后检测 SD 卡的 VIDEO 文件夹,并查找 avi 视频文件,在找到有效视频文件后,便开
始播放视频,如图 55.4.4 所示:
图 55.4.4 视频播放中
可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,
我们按 KEY0/KEY2,可以切换到下一个/上一个视频,按 KEY_UP/KEY1,可以快进/快退。
至此,本例程介绍就结束了。本实验,我们在阿波罗 STM32 开发板上实现了视频播放,
体现了 STM32F767 强大的处理能力。
本例程只支持竖屏宽度的分辨率解码(比如 800480 的屏,最大只支持 480 宽度的视频解
码),如果想要支持更大分辨率的视频解码,则必须使用横屏模式,需要在本例程源码的基础上
稍作修改(参见综合实验的视频播放器功能)。
附 STM32F767 硬件 JPEG 视频解码性能:
对 480272 及以下分辨率,可达 30 帧
对 800480 分辨率,可达 20 帧
对 1024600 分辨率,可达 10 帧
最后提醒大家,转换的视频分辨率,一定要根据自己的 LCD 设置,不能超过 LCD 的尺寸!
!
否则无法播放(可能只听到声音,看不到图像)。