OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 34|回复: 0

《STM32H7R7开发指南 V1.1 》第六十二章 视频播放器实验

[复制链接]

1351

主题

1367

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5768
金钱
5768
注册时间
2019-5-8
在线时间
1573 小时
发表于 2 小时前 | 显示全部楼层 |阅读模式
第六十二章 视频播放器实验

1)实验平台:正点原子STM32H7R7开发板

2)章节摘自【正点原子】STM32H7R7开发指南 V1.1

3)购买链接: https://detail.tmall.com/item.htm?id=820823382459

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h7rx.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32开发板技术交流群:756580169


2.jpg

3.png

STM32H7R7自带了硬件JPEG解码器,完全可以用来播放视频!本章,我们将使用STM32H7R7的硬件JPEG解码器来实现播放AVI视频(MJPEG编码),本章我们将实现一个简单的视频播放器,实现AVI视频播放。
62.1 AVI简介
62.2 硬件设计
62.3 软件设计
62.4 下载验证


62.1 AVI简介
本章,我们使用STM32H7R7的硬件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文件的结构如图62.1.1所示:


第六十二章 视频播放器实验1352.png
图62.1.1 AVI文件结构图

从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。

1、信息块(HeaderList)
信息块,即ID为“hdrl”的LIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的字块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockID和BlockSize包含进来,下同)的定义如下:

  1. /* avih 子块信息 */
  2. typedef struct
  3. {
  4.     uint32_t BlockID;              /* 块标志:avih==0X61766968 */
  5.     uint32_t BlockSize;  /*块大小(不包含最初8字节,也就是BlockID和BlockSize不在内*/
  6.     uint32_t SecPerFrame;                 /* 视频帧间隔时间(单位为us) */
  7.     uint32_t MaxByteSec;                  /* 最大数据传输率,字节/秒 */
  8.     uint32_t PaddingGranularity;        /* 数据填充的粒度 */
  9.     uint32_t Flags;                /* AVI文件的全局标记,比如是否含有索引块等 */
  10.     uint32_t TotalFrame;                  /* 文件总帧数 */
  11.     uint32_t InitFrames;                  /* 为交互格式指定初始帧数(非交互格式应该指定为0)*/
  12.     uint32_t Streams;              /* 包含的数据流种类个数,通常为2 */
  13.     uint32_t RefBufSize;  /* 建议读取本文件的缓存大小(应能容纳最大的块)默认是1M字节*/
  14.     uint32_t Width;                /* 图像宽 */
  15.     uint32_t Height;               /* 图像高 */
  16.     uint32_t Reserved[4];                 /* 保留 */
  17. } 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子块,该块用于说明这个流的头信息,定义如下:

  1. /* strh 流头子块信息(strh∈strl) */
  2. typedef struct
  3. {
  4. uint32_t BlockID;       /* 块标志:strh==0X73747268 */
  5. /* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
  6. uint32_t BlockSize;
  7.     uint32_t StreamType;/*数据流种类,vids(0X73646976):视频;auds(0X73647561):音频*/
  8.     uint32_t Handler;  /*指定流的处理者,对于音视频来说就是解码器,比如MJPG/H264之类的*/
  9.     uint32_t Flags;        /* 标记:是否允许这个流输出?调色板是否变化? */
  10.     uint16_t Priority;     /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流) */
  11.     uint16_t Language;             /* 音频的语言代号 */
  12.     uint32_t InitFrames;           /* 为交互格式指定初始帧数 */
  13.     uint32_t Scale;        /* 数据量, 视频每帧的大小或者音频的采样大小 */
  14.     uint32_t Rate;         /* Scale/Rate=每秒采样数 */
  15.     uint32_t Start;        /* 数据流开始播放的位置,单位为Scale */
  16.     uint32_t Length;       /* 数据流的数据量,单位为Scale */
  17.     uint32_t RefBufSize;           /* 建议使用的缓冲区大小 */
  18.     uint32_t Quality;              /* 解压缩质量参数,值越大,质量越好 */
  19.     uint32_t SampleSize;          /* 音频的样本大小 */
  20.     struct                  /* 视频帧所占的矩形 */
  21.     {
  22.         short Left;
  23.         short Top;
  24.         short Right;
  25.         short Bottom;
  26.     } Frame;
  27. } STRH_HEADER;
复制代码
这里面,对我们最有用的即StreamType 和Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPG/H264等(实际以strf块为准)。
然后是strf子块,不过strf字块,需要根据strh字块的类型而定。
如果strh子块是视频数据流(StreamType=“vids”),则strf子块的内容定义如下:

  1. /* BMP结构体 */
  2. typedef struct
  3. {
  4.     uint32_t BmpSize;                /* bmp结构体大小,包含(BmpSize在内) */
  5.     long Width;                       /* 图像宽 */
  6.     long Height;                      /* 图像高 */
  7.     uint16_t  Planes;                /* 平面数,必须为1 */
  8.     uint16_t  BitCount;              /* 像素位数,0X0018表示24位 */
  9.     uint32_t  Compression;          /* 压缩类型,比如:MJPG/H264等 */
  10.     uint32_t  SizeImage;            /* 图像大小 */
  11.     long XpixPerMeter;              /* 水平分辨率 */
  12.     long YpixPerMeter;              /* 垂直分辨率 */
  13.     uint32_t  ClrUsed;              /* 实际使用了调色板中的颜色数,压缩格式中不使用 */
  14.     uint32_t  ClrImportant;            /* 重要的颜色 */
  15. } BMP_HEADER;

  16. /* 颜色表 */
  17. typedef struct
  18. {
  19.     uint8_t  rgbBlue;               /* 蓝色的亮度(值范围为0-255) */
  20.     uint8_t  rgbGreen;              /* 绿色的亮度(值范围为0-255) */
  21.     uint8_t  rgbRed;                /* 红色的亮度(值范围为0-255) */
  22.     uint8_t  rgbReserved;              /* 保留,必须为0 */
  23. } AVIRGBQUAD;

  24. /* 对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER块 */
  25. typedef struct
  26. {
  27.     uint32_t BlockID;                /* 块标志,strf==0X73747266 */
  28. uint32_t BlockSize;             /* 块大小(不包含最初的8字节,也就是BlockID
  29. 和本BlockSize不计算在内) */
  30.     BMP_HEADER bmiHeader;           /* 位图信息头 */
  31.     AVIRGBQUAD bmColors[1];          /* 颜色表 */
  32. } STRF_BMPHEADER;
复制代码
这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(Width和Height),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。
如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下:

  1. /* 对于strh,如果是音频流,strf(流格式)使STRH_WAVHEADER块 */
  2. typedef struct
  3. {
  4.     uint32_t BlockID;               /* 块标志,strf==0X73747266 */
  5.         uint32_t BlockSize;             /* 块大小(不包含最初的8字节,也就是BlockID
  6.     和本BlockSize不计算在内) */
  7.     uint16_t FormatTag;             /* 格式标志:0X0001=PCM,0X0055=MP3 */
  8.     uint16_t Channels;              /* 声道数,一般为2,表示立体声 */
  9.     uint32_t SampleRate;            /* 音频采样率 */
  10.     uint32_t BaudRate;              /* 波特率 */
  11.     uint16_t BlockAlign;               /* 数据块对齐标志 */
  12.     uint16_t Size;                  /* 该结构大小 */
  13. } 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卡用)、SAI、DMA、ES8388、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
2)读取AVI文件,并解析
要解码,得先读取avi文件,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。
3)根据解析结果,设置相关参数
根据第2步解析的结果,设置SAI的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。
4)读取数据流,开始解码
前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据,所以,直接填充到DMA缓冲区即可,由DMA循环发送给ES8388,播放音频。对于视频数据(00dc/01dc),本例程只支持MJPG,通过硬件JPEG解码,硬件JPEG解码流程详见第五十七章。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。
5)解码完成,释放资源
最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止SAI播放音乐和关闭文件等一系列操作,等待下一次解码。


62.2 硬件设计

1. 例程功能
1、本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放SD卡VIDEO文件夹里面的视频(.avi格式)。
注意:自备SD卡一张,并在SD卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤见后面62.4节下载验证部分。
视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY2用于选择上一个视频,KEY_UP可以快进,KEY1可以快退。
2、LED0闪烁,提示程序运行。


2. 硬件资源
1)LED灯
       LED0 – PD14
       LED1 – PC0
2)独立按键
       KEY0 – PE9
       KEY1 – PE8
       KEY2 – PE7
       KEY_UP – PC13 (程序中的宏名:WK_UP)
3)串口1 (PB14/PB15连接在板载USB转串口芯片CH340上面)
4)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)
5)SD卡:通过SDMMC1(SDMMC_D0~D4(PC8~PC11),
       SDMMC_SCK(PC12),SDMMC_CMD(PD2))连接
6)NOR FLASH
7)SAI,驱动ES8388芯片
8)喇叭或耳机
9)硬件JPEG解码内核(STM32H7R7自带)
10)定时器TIM6和TIM7


62.3 程序设计

62.3.1 程序解析

1. MJPEG驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。
avi.h头文件在62.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:

  1. AVI_INFO g_avix; /* avi文件相关信息 */
  2. /* 视频编码标志字符串,00dc/01dc */
  3. char *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
  4. /* 音频编码标志字符串,00wb/01wb */
  5. char *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};

  6. /**
  7. * @brief       avi解码初始化
  8. * [url=home.php?mod=space&uid=271674]@param[/url]       buf  : 输入缓冲区
  9. * @param       size : 缓冲区大小
  10. * @retval      执行结果
  11. *    @arg      OK,avi文件解析成功
  12. *    @arg      其他,错误代码
  13. */
  14. AVISTATUS avi_init(uint8_t *buf, uint32_t size)
  15. {
  16.     uint16_t offset;
  17.     uint8_t *tbuf;
  18.    
  19.     AVISTATUS res = AVI_OK;
  20.    
  21.     AVI_HEADER *aviheader;
  22.     LIST_HEADER *listheader;
  23.     AVIH_HEADER *avihheader;
  24.     STRH_HEADER *strhheader;

  25.     STRF_BMPHEADER *bmpheader;
  26.     STRF_WAVHEADER *wavheader;

  27.     tbuf = buf;
  28.     aviheader = (AVI_HEADER *)buf;

  29.     if (aviheader->RiffID != AVI_RIFF_ID)
  30.     {
  31.         return AVI_RIFF_ERR;        /* RIFF ID错误 */
  32.     }

  33.     if (aviheader->AviID != AVI_AVI_ID)
  34.     {
  35.         return AVI_AVI_ERR;         /* AVI ID错误 */
  36.     }

  37.     buf += sizeof(AVI_HEADER);      /* 偏移 */
  38.     listheader = (LIST_HEADER *)(buf);

  39.     if (listheader->ListID != AVI_LIST_ID)
  40.     {
  41.         return AVI_LIST_ERR;        /* LIST ID错误 */
  42.     }

  43.     if (listheader->ListType != AVI_HDRL_ID)
  44.     {
  45.         return AVI_HDRL_ERR;        /* HDRL ID错误 */
  46.     }

  47.     buf += sizeof(LIST_HEADER);     /* 偏移 */
  48.     avihheader = (AVIH_HEADER *)(buf);

  49.     if (avihheader->BlockID != AVI_AVIH_ID)
  50.     {
  51.         return AVI_AVIH_ERR;        /* AVIH ID错误 */
  52.     }

  53.     g_avix.SecPerFrame = avihheader->SecPerFrame;   /* 得到帧间隔时间 */
  54.     g_avix.TotalFrame = avihheader->TotalFrame;     /* 得到总帧数 */
  55.     buf += avihheader->BlockSize + 8;               /* 偏移 */
  56.     listheader = (LIST_HEADER *)(buf);

  57.     if (listheader->ListID != AVI_LIST_ID)
  58.     {
  59.         return AVI_LIST_ERR;        /* LIST ID错误 */
  60.     }

  61.     if (listheader->ListType != AVI_STRL_ID)
  62.     {
  63.         return AVI_STRL_ERR;        /* STRL ID错误 */
  64.     }

  65.     strhheader = (STRH_HEADER *)(buf + 12);

  66.     if (strhheader->BlockID != AVI_STRH_ID)
  67.     {
  68.         return AVI_STRH_ERR;        /* STRH ID错误 */
  69.     }

  70.     if (strhheader->StreamType == AVI_VIDS_STREAM)  /* 视频帧在前 */
  71.     {
  72.         if (strhheader->Handler != AVI_FORMAT_MJPG)
  73.         {
  74.             return AVI_FORMAT_ERR;  /* 非MJPG视频流,不支持 */
  75.         }

  76.         g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[0];    /* 视频流标记  "00dc" */
  77.         g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[1];    /* 音频流标记  "01wb" */
  78.         bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);

  79.         if (bmpheader->BlockID != AVI_STRF_ID)
  80.         {
  81.             return AVI_STRF_ERR;    /* STRF ID错误 */
  82.         }

  83.         g_avix.Width = bmpheader->bmiHeader.Width;
  84.         g_avix.Height = bmpheader->bmiHeader.Height;
  85.         buf += listheader->BlockSize + 8;       /* 偏移 */
  86.         listheader = (LIST_HEADER *)(buf);

  87.         if (listheader->ListID != AVI_LIST_ID)  /* 是不含有音频帧的视频文件 */
  88.         {
  89.             g_avix.SampleRate = 0;              /* 音频采样率 */
  90.             g_avix.Channels = 0;                /* 音频通道数 */
  91.             g_avix.AudioType = 0;               /* 音频格式 */

  92.         }
  93.         else
  94.         {
  95.             if (listheader->ListType != AVI_STRL_ID)
  96.             {
  97.                 return AVI_STRL_ERR;    /* STRL ID错误 */
  98.             }

  99.             strhheader = (STRH_HEADER *)(buf + 12);

  100.             if (strhheader->BlockID != AVI_STRH_ID)
  101.             {
  102.                 return AVI_STRH_ERR;    /* STRH ID错误 */
  103.             }

  104.             if (strhheader->StreamType != AVI_AUDS_STREAM)
  105.             {
  106.                 return AVI_FORMAT_ERR;  /* 格式错误 */
  107.             }

  108.             wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize+8);

  109.             if (wavheader->BlockID != AVI_STRF_ID)
  110.             {
  111.                 return AVI_STRF_ERR;    /* STRF ID错误 */
  112.             }

  113.             g_avix.SampleRate = wavheader->SampleRate;      /* 音频采样率 */
  114.             g_avix.Channels = wavheader->Channels;          /* 音频通道数 */
  115.             g_avix.AudioType = wavheader->FormatTag;        /* 音频格式 */
  116.         }
  117.     }
  118.     else if (strhheader->StreamType == AVI_AUDS_STREAM)     /* 音频帧在前 */
  119.     {
  120.         g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[1];         /* 视频流标记  "01dc" */
  121.         g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[0];         /* 音频流标记  "00wb" */
  122.         wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);   

  123.         if (wavheader->BlockID != AVI_STRF_ID)
  124.         {
  125.             return AVI_STRF_ERR;                            /* STRF ID错误 */
  126.         }

  127.         g_avix.SampleRate = wavheader->SampleRate;          /* 音频采样率 */
  128.         g_avix.Channels = wavheader->Channels;              /* 音频通道数 */
  129.         g_avix.AudioType = wavheader->FormatTag;            /* 音频格式 */
  130.         buf += listheader->BlockSize + 8;                   /* 偏移 */
  131.         listheader = (LIST_HEADER *)(buf);

  132.         if (listheader->ListID != AVI_LIST_ID)
  133.         {
  134.             return AVI_LIST_ERR;    /* LIST ID错误 */
  135.         }

  136.         if (listheader->ListType != AVI_STRL_ID)
  137.         {
  138.             return AVI_STRL_ERR;    /* STRL ID错误 */
  139.         }

  140.         strhheader = (STRH_HEADER *)(buf + 12);

  141.         if (strhheader->BlockID != AVI_STRH_ID)
  142.         {
  143.             return AVI_STRH_ERR;    /* STRH ID错误 */
  144.         }

  145.         if (strhheader->StreamType != AVI_VIDS_STREAM)
  146.         {
  147.             return AVI_FORMAT_ERR;  /* 格式错误 */
  148.         }

  149.         bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);

  150.         if (bmpheader->BlockID != AVI_STRF_ID)
  151.         {
  152.             return AVI_STRF_ERR;    /* STRF ID错误 */
  153.         }

  154.         if (bmpheader->bmiHeader.Compression != AVI_FORMAT_MJPG)
  155.         {
  156.             return AVI_FORMAT_ERR;  /* 格式错误 */
  157.         }

  158.         g_avix.Width = bmpheader->bmiHeader.Width;
  159.         g_avix.Height = bmpheader->bmiHeader.Height;
  160.     }

  161.     offset = avi_srarch_id(tbuf, size, "movi");     /* 查找movi ID */

  162.     if (offset == 0)
  163.     {
  164.         return AVI_MOVI_ERR;        /* MOVI ID错误 */
  165.     }

  166.     if (g_avix.SampleRate)          /* 有音频流,才查找 */
  167.     {
  168.         tbuf += offset;
  169.         offset = avi_srarch_id(tbuf, size, g_avix.AudioFLAG); /* 查找音频流标记 */

  170.         if (offset == 0)
  171.         {
  172.             return AVI_STREAM_ERR;  /* 流错误 */
  173.         }

  174.         tbuf += offset + 4;
  175.         g_avix.AudioBufSize = *((uint16_t *)tbuf);    /* 得到音频流buf大小. */
  176.     }

  177.     printf("avi init ok\r\n");
  178.     printf("g_avix.SecPerFrame:%d\r\n", g_avix.SecPerFrame);
  179.     printf("g_avix.TotalFrame:%d\r\n", g_avix.TotalFrame);
  180.     printf("g_avix.Width:%d\r\n", g_avix.Width);
  181.     printf("g_avix.Height:%d\r\n", g_avix.Height);
  182.     printf("g_avix.AudioType:%d\r\n", g_avix.AudioType);
  183.     printf("g_avix.SampleRate:%d\r\n", g_avix.SampleRate);
  184.     printf("g_avix.Channels:%d\r\n", g_avix.Channels);
  185.     printf("g_avix.AudioBufSize:%d\r\n", g_avix.AudioBufSize);
  186.     printf("g_avix.VideoFLAG:%s\r\n", g_avix.VideoFLAG);
  187.     printf("g_avix.AudioFLAG:%s\r\n", g_avix.AudioFLAG);

  188.     return res;
  189. }
复制代码
该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。
接下来介绍的是查找 ID函数,其定义如下:

  1. /**
  2. * @brief       查找 ID
  3. * @param       buf  : 输入缓冲区
  4. * @param       size : 缓冲区大小
  5. * @param       id   : 要查找的id,必须是4字节长度
  6. * @retval      执行结果
  7. *   @arg       0     , 没找到
  8. *   @arg       其他  , movi ID偏移量
  9. */
  10. uint32_t avi_srarch_id(uint8_t *buf, uint32_t size, char *id)
  11. {
  12.     uint32_t i;
  13.     uint32_t idsize = 0;
  14.     size -= 4;
  15.     for (i = 0; i < size; i++)
  16.     {
  17.         if ((buf[i] == id[0]) &&
  18.             (buf[i + 1] == id[1]) &&
  19.             (buf[i + 2] == id[2]) &&
  20.             (buf[i + 3] == id[3]))
  21.         {
  22.             idsize = MAKEDWORD(buf + i + 4);   
  23.             /* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */

  24.             if (idsize > 0X10)return i;         /* 找到"id"所在的位置 */
  25.         }
  26.     }

  27.     return 0;
  28. }
复制代码
该函数用于查找某个ID,可以是4个字节长度的ID,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。
接下来介绍的是得到stream流信息函数,其定义如下:

  1. /**
  2. * @brief       得到stream流信息
  3. * @param       buf:流开始地址(必须是01wb/00wb/01dc/00dc开头)
  4. * @retval      执行结果
  5. *   @arg       AVI_OK, AVI文件解析成功
  6. *   @arg       其他  , 错误代码
  7. */
  8. AVISTATUS avi_get_streaminfo(uint8_t *buf)
  9. {
  10.     g_avix.StreamID = MAKEWORD(buf + 2);          /* 得到流类型 */
  11.     g_avix.StreamSize = MAKEDWORD(buf + 4);       /* 得到流大小 */

  12.     if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE)   /* 帧大小太大了,直接返回错误 */
  13.     {
  14.         printf("FRAME SIZE OVER:%d\r\n", g_avix.StreamSize);
  15.         g_avix.StreamSize = 0;
  16.         return AVI_STREAM_ERR;
  17.     }

  18.     if (g_avix.StreamSize % 2)
  19.     {
  20.         g_avix.StreamSize++;    /* 奇数加1(g_avix.StreamSize,必须是偶数) */
  21.     }

  22.     if (g_avix.StreamID == AVI_VIDS_FLAG || g_avix.StreamID == AVI_AUDS_FLAG)
  23.     {
  24.         return AVI_OK;
  25.     }

  26.     return AVI_STREAM_ERR;
  27. }
复制代码
该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG解码函数,其定义如下:

  1. /**
  2. * @brief       初始化MJPEG
  3. * @param       offx    : 显示图像在LCD上x方向的偏移量
  4. * @param       offy    : 显示图像在LCD上y方向的偏移量
  5. * @param       width   : 显示图像的宽度
  6. * @param       height  : 显示图像的高度
  7. * @retval      执行结果
  8. *   @arg       0     , 成功
  9. *   @arg       其他  , 失败
  10. */
  11. uint8_t mjpeg_init(uint16_t offx, uint16_t offy, uint32_t width, uint32_t height)
  12. {
  13.     uint8_t i;
  14.     uint8_t res;
  15.     res = mjpeg_jpeg_core_init(&mjpeg);     /* 初始化JPEG内核,不申请IN BUF */

  16.     if (res)
  17.     {
  18.         return 1;
  19.     }

  20.     for (i = 0; i < JPEG_DMA_OUTBUF_NB; i++)
  21.     {
  22.         mjpeg.outbuf[i].buf = mymalloc(SRAMIN, width * 24 + 32);   
  23. /* 最大是图片宽度的24倍,另外还可能会多需要32字节内存 */

  24.         if (mjpeg.outbuf[i].buf == NULL)
  25.         {
  26.             return 2;
  27.         }
  28.     }

  29.     p_rgb565buf = 0;                                /* RGB565 BUF指针清零 */

  30. if (lcdltdc.pwidth == 0 || lcddev.dir == 0)     
  31. /* 如果不是RGB横屏,则需要p_rgb565buf数组 */
  32.     {
  33.         p_rgb565buf = mymalloc(SRAMEX, width * height * 2); /* 申请RGB缓存 */

  34.         if (p_rgb565buf == NULL)
  35.         {
  36.             return  3;
  37.         }
  38.     }

  39.     g_img_offx = offx;
  40.     g_img_offy = offy;
  41. p_mjpeg_rgb_framebuf = (uint16_t *)g_ltdc_framebuf[lcdltdc.activelayer];
  42. /* 指向RGBLCD当前显存 */

  43.     return 0;
  44. }
复制代码
该函数用于初始化jpeg解码,主要调用mjpeg_jpeg_core_init函数,对硬件JPEG解码内核进行初始化,然后申请内存,确定视频在液晶上面的偏移(让视频显示在LCD中央)。
注意:如果是MCU屏,我们需要申请内存。如果是RGB屏,则可以直接使用RGB屏的显存。
下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:

  1. /**
  2. * @brief       MJPEG释放所有申请的内存
  3. * @param       无
  4. * @retval      无
  5. */
  6. void mjpeg_free(void)
  7. {
  8.     mjpeg_jpeg_core_destroy(&mjpeg);
  9. }
复制代码
该函数用于释放内存,解码结束后调用。
下面介绍的是解码一副JPEG图片函数,其定义如下:

  1. /**
  2. * @brief       解码一副JPEG图片
  3. * [url=home.php?mod=space&uid=60778]@note[/url]        注意事项:
  4. *              1, 待解码图片的分辨率,必须小于等于屏幕的分辨率!
  5. *              2, 请保证图片的宽度是16的倍数,以免左侧出现花纹.
  6. * @param       buf     : jpeg数据流数组
  7. * @param       bsize   : 数组大小
  8. * @retval      执行结果
  9. *   @arg       0     , 成功
  10. *   @arg       其他  , 失败
  11. */
  12. uint8_t mjpeg_decode(uint8_t *buf, uint32_t bsize)
  13. {
  14.     volatile uint32_t timecnt = 0;

  15.     if (bsize == 0)
  16.     {
  17.         return 0;
  18.     }

  19.     jpeg_decode_init(&mjpeg);                    /* 初始化硬件JPEG解码器 */
  20.     g_mjpeg_remain_size = bsize;                 /* 记录当前图片的大小(字节数) */
  21.     mjpeg.inbuf[0].buf = buf;                    /* 指向jpeg数据流的首地址 */
  22.     g_mjpeg_fileover = 0;                        /* 标记未读完 */

  23.     if (g_mjpeg_remain_size < JPEG_DMA_INBUF_LEN)/* 图片比较小,一次就可以传输完成 */
  24.     {
  25.         mjpeg.inbuf[0].size = g_mjpeg_remain_size;/* 传输大小等于总大小 */
  26.         g_mjpeg_remain_size = 0;                  /* 一次传输就可以搞完 */
  27.         g_mjpeg_fileover = 1;
  28.     }
  29.     else                                          /* 图片比较大,需要分多次传输 */
  30.     {
  31.         mjpeg.inbuf[0].size = JPEG_DMA_INBUF_LEN; /* 按最大传输长度,分批次传输 */
  32.         g_mjpeg_remain_size -= JPEG_DMA_INBUF_LEN;/* 剩余长度 */
  33.     }

  34. jpeg_in_dma_init((uint32_t)mjpeg.inbuf[0].buf, mjpeg.inbuf[0].size);   
  35. /* 配置输入DMA */
  36.     jpeg_in_callback = mjpeg_dma_in_callback;  /* JPEG DMA读取数据回调函数 */
  37.     jpeg_out_callback = mjpeg_dma_out_callback;/* JPEG DMA输出数据回调函数 */
  38.     jpeg_eoc_callback = mjpeg_endofcovert_callback; /* JPEG 解码结束回调函数 */
  39.     jpeg_hdp_callback = mjpeg_hdrover_callback;/* JPEG Header解码完成回调函数 */
  40.     jpeg_in_dma_start();                 /* 启动DMA IN传输,开始解码JPEG图片 */

  41.     while (1)
  42.     {
  43.         if (mjpeg.outbuf[mjpeg.outbuf_read_ptr].sta==1)/* buf里面有数据要处理 */
  44.         {
  45.             //SCB_CleanInvalidateDCache();                  /* 清空D catch */
  46.             if (lcdltdc.pwidth == 0 || lcddev.dir == 0)     
  47. /* 非RGB横屏,需要先将YUV数据解码到p_rgb565buf,然后再从p_rgb565buf
  48. 拷贝到LCD的GRAM */
  49.             {
  50.                 jpeg_dma2d_yuv2rgb_conversion(&mjpeg, (uint32_t *)p_rgb565buf);
  51. /* 利用DMA2D,将YUV图像转成RGB565图像 */
  52.             }
  53.             else
  54. /* RGB横屏,直接将YUV数据解码到LCD的GRAM,省去了拷贝操作,可以达到最佳性能 */
  55.             {
  56.                 mjpeg_ltdc_dma2d_yuv2rgb_fill(g_img_offx, g_img_offy +
  57. mjpeg.yuvblk_curheight, &mjpeg);
  58. /* DMA2D将YUV解码到LCD GRAM,速度最快 */
  59.             }

  60.             //SCB_CleanInvalidateDCache();                  /* 清空D catch */
  61.             mjpeg.outbuf[mjpeg.outbuf_read_ptr].sta = 0;    /* 标记buf为空 */
  62.             mjpeg.outbuf[mjpeg.outbuf_read_ptr].size = 0;   /* 数据量清空 */
  63.             mjpeg.outbuf_read_ptr++;

  64.             if (mjpeg.outbuf_read_ptr >= JPEG_DMA_OUTBUF_NB)
  65. mjpeg.outbuf_read_ptr = 0;  /* 限制范围 */

  66.             if (mjpeg.yuvblk_curheight >= mjpeg.Conf.ImageHeight)break;     
  67. /* 当前高度等于或者超过图片分辨率的高度,则说明解码完成了,直接退出. */
  68.         }
  69.         else if (mjpeg.outdma_pause == 1 && mjpeg.outbuf
  70. [mjpeg.outbuf_write_ptr].sta == 0)
  71. /* out暂停,且当前writebuf已经为空了,则恢复out输出 */
  72.         {
  73.             jpeg_out_dma_resume((uint32_t)mjpeg.outbuf[mjpeg.outbuf_write_ptr]
  74. .buf, mjpeg.yuvblk_size); /* 继续下一次DMA传输 */
  75.             mjpeg.outdma_pause = 0;
  76.         }

  77.         if (mjpeg.state == JPEG_STATE_ERROR)        /* 解码出错,直接退出 */
  78.         {
  79.             break;
  80.         }

  81.         if (mjpeg.state == JPEG_STATE_FINISHED)/* 解码结束了,检查是否异常结束 */
  82.         {
  83.             if (mjpeg.yuvblk_curheight < mjpeg.Conf.ImageHeight)
  84.             {
  85.                 if (mjpeg.Conf.ImageHeight > (mjpeg.yuvblk_curheight + 16))
  86. /* 数据异常,直接退出 */
  87.                 {
  88.                     mjpeg.state = JPEG_STATE_ERROR;     /* 标记错误 */
  89.                     printf("early finished!\r\n");
  90.                     break;
  91.                 }
  92.             }
  93.         }

  94.         if (g_mjpeg_fileover)   /* 文件读完了,及时退出,防止死循环 */
  95.         {
  96.             timecnt++;

  97.             if (mjpeg.state == JPEG_STATE_NOHEADER)break;/* 解码JPEG头失败了 */

  98.             if (timecnt > 0X3FFFF)break;                 /* 超时退出 */
  99.         }
  100.     }

  101.     if (mjpeg.state == JPEG_STATE_FINISHED)              /* 解码完成了 */
  102.     {
  103.         mjpeg_fill_color(g_img_offx, g_img_offy, mjpeg.Conf.ImageWidth,
  104. mjpeg.Conf.ImageHeight, p_rgb565buf);
  105.     }

  106.     return 0;
  107. }
复制代码
该函数是解码jpeg的主要函数,通过前面62.1.2节介绍过的步骤进行解码,该函数解码后将YUV转换成RGB565数据,存放在p_rgb565buf里面,然后通过mjpeg_fill_color函数,将RGB565数据显示到LCD上。

2. APP驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。APP驱动源码包括两个文件:videoplayer.c和videoplayer.h。
videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:

  1. uint8_t video_play_mjpeg(uint8_t *pname)
  2. {
  3.     uint8_t *framebuf;      /* 视频解码buf */
  4.     uint8_t *pbuf;          /* buf指针 */
  5.     FIL *favi;
  6.     uint8_t  res = 0;
  7.     uint8_t i = 0;
  8.     uint16_t offset = 0;
  9.     uint32_t nr;
  10.     uint8_t key;
  11.     uint8_t saisavebuf;

  12.     for (i = 0; i < AVI_AUDIO_BUF_NUM; i++)
  13.     {
  14.         p_avi_sai_buf[i] = mymalloc(SRAMIN, AVI_AUDIO_BUF_SIZE);   
  15. /* 申请音频内存 */
  16.         
  17.         if (p_avi_sai_buf[i] == NULL)     /* 申请失败, 直接退出 */
  18.         {
  19.             break;
  20.         }
  21.         
  22.         memset(p_avi_sai_buf[i], 0, AVI_AUDIO_BUF_SIZE);   /* 数据清零 */
  23.     }

  24.     favi = (FIL *)mymalloc(SRAMIN, sizeof(FIL));           /* 申请favi内存 */
  25.     framebuf = mymalloc(SRAMIN, AVI_VIDEO_BUF_SIZE) ;      /* 申请视频buf */

  26. if (!framebuf)  
  27. /* 只要最后这个视频buf申请失败, 前面的申请失不失败都不重要, 总之就是失败了 */
  28.     {
  29.         printf("memory error!\r\n");
  30.         res = 0XFF;
  31.     }

  32.     while (res == 0)
  33.     {
  34.         res = f_open(favi, (char *)pname, FA_READ);

  35.         if (res == 0)
  36.         {
  37.             pbuf = framebuf;
  38.             res = f_read(favi, pbuf, AVI_VIDEO_BUF_SIZE, &nr);  /* 开始读取 */

  39.             if (res)
  40.             {
  41.                 printf("fread error:%d\r\n", res);
  42.                 break;
  43.             }

  44.             /* 开始avi解析 */
  45.             res = avi_init(pbuf, AVI_VIDEO_BUF_SIZE);           /* avi解析 */


  46.             if (res || g_avix.Width > lcddev.width)
  47.             {
  48.                 printf("avi err:%d,width:%d\r\n", res, g_avix.Width);
  49.                 res = KEY0_PRES;
  50.                 break;
  51.             }

  52.             video_info_show(&g_avix);
  53.             btim_tim7_int_init(g_avix.SecPerFrame / 100 - 1, 30000 - 1);   
  54. /* 10Khz计数频率,加1是100us */
  55.             offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");      
  56. /* 寻找movi ID */
  57.             avi_get_streaminfo(pbuf + offset + 4);  /* 获取流信息 */
  58.             f_lseek(favi, offset + 12);  /* 跳过标志ID,读地址偏移到流数据开始处 */

  59.             if (lcddev.height <= g_avix.Height)
  60.             {
  61.                 res = mjpeg_init((lcddev.width - g_avix.Width) / 2,
  62. (lcddev.height - g_avix.Height) / 2, g_avix.Width,
  63. g_avix.Height);              /* JPG解码初始化 */
  64.             }
  65.             else
  66.             {
  67.                 res = mjpeg_init((lcddev.width - g_avix.Width) / 2, 110 +
  68. (lcddev.height - 110 - g_avix.Height) / 2, g_avix.Width,
  69. g_avix.Height);   /* JPG解码初始化 */
  70.             }

  71.             if (g_avix.SampleRate) /* 有音频信息,才初始化 */
  72.             {
  73.                 es8388_sai_cfg(0, 3);    /* 飞利浦标准,16位数据长度 */
  74.                 sai1_saia_init(SAI_MODEMASTER_TX, SAI_CLOCKSTROBING_RISINGEDGE,
  75. SAI_DATASIZE_16);   /* 设置SAI,主发送,16位数据 */
  76.                 sai1_samplerate_set(g_avix.SampleRate);         /* 设置采样率 */
  77.                 sai1_tx_dma_init(p_avi_sai_buf[0], p_avi_sai_buf[1],
  78. g_avix.AudioBufSize / 2, 1);   /* TX DMA 16位宽 */
  79.                 sai1_tx_callback = audio_sai_dma_callback;      
  80. /* 回调函数指向 audio_sai_dma_callback */

  81.                 g_avi_sai_playbuf = 0;
  82.                 saisavebuf = 0;
  83.                 sai1_play_start();  /* 开启SAI播放 */
  84.             }

  85.             while (1)               /* 播放循环 */
  86.             {
  87.                 if (g_avix.StreamID == AVI_VIDS_FLAG)           /* 视频流 */
  88.                 {
  89.                     pbuf = framebuf;
  90.                     f_read(favi, pbuf, g_avix.StreamSize + 8, &nr);   
  91. /* 读入整帧+下一数据流ID信息 */
  92.                     res = mjpeg_decode(pbuf, g_avix.StreamSize);

  93.                     if (res)
  94.                     {
  95.                         printf("decode error!\r\n");
  96.                     }

  97.                     while (g_avi_frameup == 0);                 
  98. /* 待时间到达(在TIM的中断里面设置为1) */

  99.                     g_avi_frameup = 0;                          /* 标志清零 */
  100.                     g_avi_frame++;
  101.                 }
  102.                 else                /* 音频流 */
  103.                 {
  104.                     video_time_show(favi, &g_avix);   /* 显示当前播放时间 */
  105.                     if (++saisavebuf == AVI_AUDIO_BUF_NUM)
  106.                     {
  107.                         saisavebuf = 0;
  108.                     }

  109.                     while (saisavebuf == g_avi_sai_playbuf);

  110.                     f_read(favi, p_avi_sai_buf[saisavebuf], g_avix.StreamSize
  111. + 8, &nr); /* 填充p_avi_sai_buf */
  112.                     pbuf = p_avi_sai_buf[saisavebuf];
  113.                 }

  114.                 key = key_scan(0);

  115.                 if (key == KEY0_PRES || key == KEY2_PRES)      
  116. /* KEY0/KEY2按下,播放下一个/上一个视频 */
  117.                 {
  118.                     res = key;
  119.                     break;
  120.                 }
  121.                 else if (key == KEY1_PRES || key == WKUP_PRES)
  122.                 {
  123.                     sai1_play_stop();                           /* 关闭音频 */
  124.                     video_seek(favi, &g_avix, framebuf);
  125.                     pbuf = framebuf;
  126.                     sai1_play_start();        /* 开启DMA播放 */
  127.                 }

  128.                 if (avi_get_streaminfo(pbuf + g_avix.StreamSize))     
  129. /* 读取下一帧 流标志 */
  130.                 {
  131.                     printf("frame error \r\n");
  132.                     res = KEY0_PRES;
  133.                     break;
  134.                 }
  135.             }

  136.             sai1_play_stop();                                   /* 关闭音频 */
  137.             TIM7->CR1 &= ~(1 << 0);                             /* 关闭定时器7 */
  138.             lcd_set_window(0, 0, lcddev.width, lcddev.height);  /* 恢复窗口 */
  139.             mjpeg_free();                                       /* 释放内存 */
  140.             f_close(favi);                                      /* 关闭文件 */
  141.         }
  142.     }

  143.     /* 释放内存 */
  144.     for (i = 0; i < AVI_AUDIO_BUF_NUM; i++)
  145.     {
  146.         myfree(SRAMIN, p_avi_sai_buf[i]);
  147.     }
  148.    
  149.     myfree(SRAMIN, framebuf);
  150.     myfree(SRAMIN, favi);

  151.     return res;
  152. }
复制代码
该函数用来播放一个avi视频文件(mjpg编码),解码过程就是根据前面我们在62.1节最后所介绍的步骤进行。其他代码,我们就不介绍了,请大家参考本例程源码。

3. main.c代码
下面是main函数,其定义如下:

  1. int main(void)
  2. {
  3.     sys_mpu_config();                           /* 配置MPU */
  4.     sys_cache_enable();                         /* 使能Cache */
  5.     HAL_Init();                                 /* 初始化HAL库 */
  6.     sys_stm32_clock_init(300, 6, 2);            /* 配置时钟,600MHz */
  7.     delay_init(600);                            /* 初始化延时 */
  8.     usart_init(115200);                         /* 初始化串口 */
  9.     led_init();                                 /* 初始化LED */
  10.     key_init();                                 /* 初始化按键 */
  11.     btim_tim6_int_init(10000 - 1, 30000 - 1);   /* 初始化基本定时器6中断 */
  12.     hyperram_init();                            /* 初始化HyperRAM */
  13.     lcd_init();                                 /* 初始化LCD */
  14.     es8388_init();                              /* 初始化ES8388 */
  15.     my_mem_init(SRAMIN);                        /* 初始化AXI-SRAM1~4内存池 */
  16.     my_mem_init(SRAMEX);                        /* 初始化XSPI2 HyperRAM内存池 */
  17.     my_mem_init(SRAM12);                        /* 初始化AHB-SRAM1~2内存池 */
  18.     my_mem_init(SRAMDTCM);                      /* 初始化DTCM内存池 */
  19.     my_mem_init(SRAMITCM);                      /* 初始化ITCM内存池 */
  20.     exfuns_init();                              /* 为exfuns申请内存 */
  21.     f_mount(fs[0], "0:", 1);                    /* 挂载SD卡 */
  22.     f_mount(fs[1], "1:", 1);                    /* 挂载NOR Flash */
  23.     f_mount(fs[2], "2:", 1);                    /* 挂载NAND Flash */
  24.    
  25.     /* 初始化SD卡 */
  26.     while (sd_init() != 0)
  27.     {
  28.         lcd_show_string(30, 30, 200, 16, 16, "SD Card Error!", RED);
  29.         delay_ms(500);
  30.         lcd_show_string(30, 30, 200, 16, 16, "Please Check! ", RED);
  31.         delay_ms(500);
  32.     }
  33.     lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
  34.    
  35.     /* 检查字库 */
  36.     while (fonts_init() != 0)
  37.     {
  38.         lcd_show_string(30, 30, 200, 16, 16, "Font Error!  ", RED);
  39.         delay_ms(500);
  40.         lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
  41.         delay_ms(500);
  42.     }
  43.     lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
  44.    
  45.     text_show_string(30, 50, 200, 16, "正点原子STM32开发板",16,0, RED);
  46.     text_show_string(30, 70, 200, 16, "视频播放器实验", 16, 0, RED);
  47.     text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  48.     text_show_string(30, 110, 200, 16, "KEY0: Next", 16, 0, RED);
  49.     text_show_string(30, 130, 200, 16, "KEY2: Prev", 16, 0, RED);
  50.     text_show_string(30, 150, 200, 16, "KEY_UP: FF", 16, 0, RED);
  51.     text_show_string(30, 170, 200, 16, "KEY1: REW", 16, 0, RED);
  52.     delay_ms(1500);
  53.    
  54.     /* 配置ES8388 */
  55.     es8388_adda_cfg(1, 0);
  56.     es8388_input_cfg(0);
  57.     es8388_output_cfg(1, 1);
  58.     es8388_hpvol_set(25);
  59.     es8388_spkvol_set(25);
  60.    
  61.     while (1)
  62.     {
  63.         video_play();
  64.     }
  65. }
复制代码
该函数代码比较简单,我们就不多说了。

62.4 下载验证
本章,我们例程仅支持MJPG编码的avi格式视频,H7R7开发板板载了的ES8388音频解码芯片和扬声器等,所以支持视频和音频播放。注意:视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料→软件→视频转换软件→狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图62.4.1和62.4.2所示:

第六十二章 视频播放器实验31574.png
图62.4.1 软件启动界面和设置

第六十二章 视频播放器实验31594.png
图62.4.2 高级设置

首先,如图62.4.1所示,点击1处,添加视频,找到你要转换的视频,添加进来。有的视频可能有独立字幕,比如我们打开的这个视频就有,所以在2处选择下字幕(如果没有的,可以忽略此步)。然后在3处,点击▼图标,选择预制方案:AVI-Audio-Video Interleaved(*.avi),即生成.avi文件,然后点击4处的高级设置按钮,进入62.4.2所示的界面,设置详细参数如下:
视频编码器:选择MJPEG。本例程仅支持MJPG视频解码,所以选择这个编码器。
视频尺寸:480x272。这里得根据所用LCD分辨率来选择,假设我们用800*480的4.3寸电容屏模块,则这里最大可以设置:800x480。PS:如果是2.8屏,最大宽度只能是240)。
比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。
帧率:10。即每秒钟10帧。对于480*272的视频,本例程最高能播放30帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。
音频编码器:PCMS16LE。本例程不支持音频。
采样率:这里设置为110250,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。
其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。
点击图62.4.1的5处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中6处的按钮,即可开始转换了,如图62.4.3所示:


第六十二章 视频播放器实验32306.png
图62.4.3 正在转换

等转换完成后,将转换后的.avi文件,拷贝到SD卡→VIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。
将程序下载到开发板后,程序先检测字库,只有字库已经更新才可以继续执行后面的程序。字库已更新,就可以看到LCD首先显示一些实验相关的信息,如图62.4.4所示:


第六十二章 视频播放器实验32465.png
图62.4.4显示实验相关信息

显示了上图的信息后,检测SD卡的VIDEO文件夹,并查找avi视频文件,在找到有效视频文件后,便开始播放视频,如图62.4.5所示:

第六十二章 视频播放器实验32551.png
图62.4.5 视频播放中

可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY1,可以切换到下一个/上一个视频,按KEY_UP,可以快进。
至此,本例程介绍就结束了。本实验,我们在开发板上实现了视频播放,体现了STM32H7R7强大的处理能力。
附本实验测试结果(视频比特率: 1000,音频均为: 110250,立体声)
对 240*160/240*180 分辨率,可达 30 帧
对 320*240 分辨率,可达 20 帧
对 480*272 分辨率,可达 10 帧
最后提醒大家,转换的视频分辨率,一定要根据自己的LCD设置,不能超过LCD的尺寸!!否则无法播放。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-6-15 11:59

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表