ITPub博客

首页 > 嵌入式/内核开发 > 嵌入式/内核开发 > 《STM32H562开发指南 V1.0 》第五十章 视频播放器实验

《STM32H562开发指南 V1.0 》第五十章 视频播放器实验

原创 嵌入式/内核开发 作者:正点原子 时间:2025-12-29 10:03:08 0 删除 编辑

第五十章 视频播放器实验


       STM32H562不仅可以软解码音频,还可以软解码JPEG,完全可以用来播放视频!本章,我们将使用 STM32H562的软件JPEG解码器来实现播放AVI视频,实现一个简单的视频播放器。

       本章分为如下几个小节:

       50.1 AVI&libjpeg简介 

       50.2 硬件设计

       50.3 程序设计

       50.4 下载验证


        50.1 AVI&libjpeg简介

       本章,我们将使用libjepg来实现MJPG编码的AVI格式视频播放,我们先来简单介绍一下AVI和libjpeg。


       50.1.1 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文件的结构如图57.1.1所示:


图57.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;   /*指定流的处理者,对于音视频来说就是解码器,比如MJPG/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则告诉我们所使用的解码器,比如MJPG/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;   /* 压缩类型,比如:MJPG/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),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。

       如果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,软件资料àAVI学习资料 里面的相关文档。


       50.1.2 libjpeg简介

       libjpeg是一个完全用C语言编写的库,包含了广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个IJG库由组织(Independent JPEG Group(独立JPEG小组))提供并维护。libjepg,目前最新版本为v9d,可以再 http://www.ijg.com 这个网站下载。libjpeg具有稳定、兼容性强和解码速度较快等优点。

       本章,我们使用libjpeg来实现MJPG数据流的解码。MJPG数据流,其实就是一张张的JPEG图片拼起来的图片视频流,只要能快速解码JPEG图片,就可以实现视频播放。

       前面的图片显示实验我们使用了TJPGD实现JPEG解码,大家可能会问,为什么不直接用TJPGD来解码呢?因为TJPG的特点是:占用资源少,但是解码速度慢。比如:在STM32F407上,同样一张320*240的JPG图片,用TJPGD来解码,需要120多毫秒,而用libjpeg,则只需要50ms左右即可完成解码,libjpeg的解码速度明显比TJPGD快了不少,使得解码视频成为可能。实际上,经过优化后的libjpeg,使用STM32F407不超频的情况下,可以流畅播放480*272@ 10帧的MJPG视频(带音频)。

       关于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;  /* jpeg_error_mgr结构体,里面有很多错误处理函数 */
    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库可用的最大内存等。

       因为STM32H562堆栈有限,不能按照示例来定义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 : 结构体指针
 * @retval   无
 */
static void jpeg_filerw_src_init(j_decompress_ptr cinfo)
{
    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));
    }
 
    cinfo->src->init_source = init_source;
    cinfo->src->fill_input_buffer = fill_input_buffer;
    cinfo->src->skip_input_data = skip_input_data;
    /* use default method */
    cinfo->src->resync_to_restart = jpeg_resync_to_restart;
    cinfo->src->term_source = term_source;
    cinfo->src->bytes_in_buffer = 0;/* forces fill_input_buffer on first read */
    cinfo->src->next_input_byte = NULL; /* until buffer loaded */
}

       该函数设置了cinfo->src的各个函数指针,用于获取外部数据。这里面重点是两个函数:fill_input_buffer和skip_input_data,前者用于填充数据给libjpeg,后者用于跳过一定字节的数据。这两个函数请看本例程源码(在mjpeg.c里面)。

       第三步,读取文件参数。通过jpeg_read_header函数实现,该函数将读取JPEG的各个参数,必须在解码之前调用。

       第四步,设置解码参数,示例代码没有做任何设置(使用默认值)。本章源码做了如下设置:

   cinfo->dct_method = JDCT_IFAST;
   cinfo->do_fancy_upsampling = 0;

       这里,我们设置了使用快速整型DCT和do_fancy_upsampling的值为假(0),以提高解码速度。

       第五步,开始解码。示例代码首先调用jpeg_start_decompress函数,然后计算样本输出buffer大小,并为其申请内存,为后续读取解码后的数据做准备。不过本章例程,我们为了提高速度,没有做这些处理了,我们直接修改底层函数:h2v1_merged_upsample和h2v2_merged_upsample(这两个函数在jdmerge.c里面),将输出的RGB数据直接转换成RGB565,然后送给LCD。为了正确的输送到LCD,我们在jpeg_start_decompress函数之后,加入如下代码:

    lcd_set_window(imgoffx, imgoffy, cinfo->output_width, cinfo->output_height);
    lcd_write_ram_prepare();        /* 开始写入GRAM */

       这两个函数,先设置好开窗大小(jpeg图片尺寸),然后就发送准备写入GRAM指令。后续需要解码时,直接在h2v1_merged_upsample和h2v2_merged_upsample里面丢数据给LCD,实现jpeg解码输出到LCD。

       第六步,循环读取数据。通过jpeg_read_scanlines函数,循环解码并读取jpeg图片数据,实现jpeg解码。示例代码通过put_scanline_someplace函数,输出到某个地方(如LCD,文件等),本章例程在直接解码的时候就输出到LCD了,所以仅剩jpeg_read_scanlines函数,循环调用即可实现jpeg传到LCD的操作。

       第七步,解码结束。解码完成后,通过jepg_finish_decompress函数,结束jpeg解码。

       第八步,释放解码对象资源。在所有操作完成后,通过jpeg_destroy_decompress,释放解码过程中用到的资源(比如释放内存)。

       这样,我们就完成了一张jpeg图片的解码。详细的代码,请大家参考光盘本例程源码mjpeg.c。

       最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下:

       1)初始化各外设

       要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、SAI、DMA、ES8388、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。

       2)读取AVI文件,并解析

       要解码,得先读取avi文件,按50.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。

       3)根据解析结果,设置相关参数

       根据第2步解析的结果,设置SAI的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。

       4)读取数据流,开始解码

       前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据,所以,直接填充到DMA缓冲区即可,由DMA循环发送给ES8388,播放音频。对于视频数据(00dc/01dc),本例程只支持 MJPG,通过 libjpeg 解码,所以将视频数据按前面所说的几个步骤解码即可。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。

       5)解码完成,释放资源

       最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止SAI播放音乐和关闭文件等一系列操作,等待下一次解码。


        50.2 硬件设计


       1 . 例程功能

       1、本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放TF卡VIDEO文件夹里面的视频(.avi格式)。

       注意:自备TF卡一张,并在TF卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤见50.4章节内容。

       视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY1用于选择上一个视频,KEY_UP可以快进。

       2、LED0闪烁,提示程序运行。


       2. 硬件资源

       1)LED灯

               LED0 – PB1

               LED1 – PB5

       2)独立按键

              KEY0  - PA1

              KEY1  - PA15

              WK_UP - PA0

       3)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)

       4)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅支持MCU屏)

       5)SD卡:通过SDMMC连接

       6)NOR FLASH(QSPI FLASH芯片)

       7)SAI,驱动ES8388芯片

       8)开发板板载的咪头

       9)喇叭或耳机

       10)定时器6、7


        50.3 程序设计 


       50.3.1 程序流程图


图50.3.1.1 视频播放器实验程序流程图


       50.3.2 程序解析


       1. MJPEG驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。  

       avi.h头文件在50.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下: 

/* avi文件相关信息 */
AVI_INFO g_avix;
/* 视频编码标志字符串,00dc/01dc */                                      
char *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
/* 音频编码标志字符串,00wb/01wb */  
char *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};  
 
/**
 * @brief       AVI解码初始化
 * @param       buf  : 输入缓冲区
 * @param       size : 缓冲区大小
 * @retval      执行结果
 *   @arg       AVI_OK, AVI文件解析成功
 *   @arg       其他  , 错误代码
 */
AVISTATUS avi_init(uint8_t *buf, uint32_t size)
{
    uint16_t offset;
    uint8_t *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错误 */
    g_avix.SecPerFrame = avihheader->SecPerFrame; /* 得到帧间隔时间 */
    g_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)               /* 视频帧在前 */
{
/* 非MJPG视频流,不支持 */
        if (strhheader->Handler != AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;   
        g_avix.VideoFLAG=(uint8_t *)AVI_VIDS_FLAG_TBL[0];/* 视频流标记  "00dc" */
        g_avix.AudioFLAG=(uint8_t *)AVI_AUDS_FLAG_TBL[1];/* 音频流标记  "01wb" */
/* strf */
        bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
        if(bmpheader->BlockID!= AVI_STRF_ID)return AVI_STRF_ERR;/*STRF ID错误*/
        g_avix.Width = bmpheader->bmiHeader.Width;
        g_avix.Height = bmpheader->bmiHeader.Height;
        buf += listheader->BlockSize + 8;     /* 偏移 */
        listheader = (LIST_HEADER *)(buf);
        if (listheader->ListID != AVI_LIST_ID)   /* 是不含有音频帧的视频文件 */
        {
            g_avix.SampleRate = 0;     /* 音频采样率 */
            g_avix.Channels = 0;       /* 音频通道数 */
            g_avix.AudioType = 0;      /* 音频格式 */
        }
        else
        {
/* STRL ID错误 */
            if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
            strhheader = (STRH_HEADER *)(buf + 12);
/* STRH ID错误 */
            if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;     
/* 格式错误 */
            if (strhheader->StreamType != AVI_AUDS_STREAM)
return AVI_FORMAT_ERR;   
/* strf */ 
            wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize+8);   
/* STRF ID错误 */
            if (wavheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;  
            g_avix.SampleRate = wavheader->SampleRate;   /* 音频采样率 */
            g_avix.Channels = wavheader->Channels;        /* 音频通道数 */
            g_avix.AudioType = wavheader->FormatTag;      /* 音频格式 */
        }
    }
    else if (strhheader->StreamType == AVI_AUDS_STREAM) /* 音频帧在前 */
    {
       g_avix.VideoFLAG = (uint8_t *)AVI_VIDS_FLAG_TBL[1]; 
/* 视频流标记 "01dc" */
       g_avix.AudioFLAG = (uint8_t *)AVI_AUDS_FLAG_TBL[0]; 
/* 音频流标记 "00wb" */
/* strf */
       wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
       if (wavheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;
/* STRF ID错误 */
       g_avix.SampleRate = wavheader->SampleRate;        /* 音频采样率 */
       g_avix.Channels = wavheader->Channels;             /* 音频通道数 */
       g_avix.AudioType = wavheader->FormatTag;          /* 音频格式 */
       buf += listheader->BlockSize + 8;                /* 偏移 */
       listheader = (LIST_HEADER *)(buf);
       if (listheader->ListID != AVI_LIST_ID)return AVI_LIST_ERR;
/* LIST ID错误 */
        /* STRL ID错误 */
       if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
       strhheader = (STRH_HEADER *)(buf + 12);/* STRH ID错误 */
       if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;/* 格式错误 */
       if (strhheader->StreamType != AVI_VIDS_STREAM)return AVI_FORMAT_ERR;
       /* strf */    
       bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
       if (bmpheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;
/* STRF ID错误 */
       if (bmpheader->bmiHeader.Compression != AVI_FORMAT_MJPG
       return AVI_FORMAT_ERR;                             /* 格式错误 */
       g_avix.Width = bmpheader->bmiHeader.Width;
       g_avix.Height = bmpheader->bmiHeader.Height;
    }
    offset = avi_srarch_id(tbuf, size, (uint8_t*)"movi"); /* 查找movi ID */
    if (offset == 0)return AVI_MOVI_ERR;              /* MOVI ID错误 */
    if (g_avix.SampleRate)                              /* 有音频流,才查找 */
    {
        tbuf += offset;
        offset = avi_srarch_id(tbuf, size, g_avix.AudioFLAG); /* 查找音频流标记 */
        if (offset == 0)return AVI_STREAM_ERR;       /* 流错误 */
        tbuf += offset + 4;
        g_avix.AudioBufSize = *((uint16_t *)tbuf);  /* 得到音频流buf大小 */
    }
    printf("avi init ok\r\n");
    printf("g_avix.SecPerFrame:%d\r\n", g_avix.SecPerFrame);
    printf("g_avix.TotalFrame:%d\r\n", g_avix.TotalFrame);
    printf("g_avix.Width:%d\r\n", g_avix.Width);
    printf("g_avix.Height:%d\r\n", g_avix.Height);
    printf("g_avix.AudioType:%d\r\n", g_avix.AudioType);
    printf("g_avix.SampleRate:%d\r\n", g_avix.SampleRate);
    printf("g_avix.Channels:%d\r\n", g_avix.Channels);
    printf("g_avix.AudioBufSize:%d\r\n", g_avix.AudioBufSize);
    printf("g_avix.VideoFLAG:%s\r\n", g_avix.VideoFLAG);
    printf("g_avix.AudioFLAG:%s\r\n", g_avix.AudioFLAG);
    return res;
}

       该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。

       接下来介绍的是查找 ID函数,其定义如下:

/**
 * @brief       查找 ID
 * @param       buf  : 输入缓冲区
 * @param       size : 缓冲区大小
 * @param       id   : 要查找的id, 必须是4字节长度
 * @retval      执行结果
 *   @arg       0     , 没找到
 *   @arg       其他  , movi ID偏移量
 */
uint32_t avi_srarch_id(uint8_t *buf, uint32_t size, char *id)
{
    uint32_t i;
    uint32_t idsize = 0;
    size -= 4;
    for (i = 0; i < size; i++)
    {
        if ((buf[i] == id[0]) &&
            (buf[i + 1] == id[1]) &&
            (buf[i + 2] == id[2]) &&
            (buf[i + 3] == id[3]))
        {
/* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */
            idsize = MAKEDWORD(buf + i + 4);    
            if (idsize > 0X10)return i;         /* 找到"id"所在的位置 */
        }
    }
    return 0;
}

       该函数用于查找某个ID,可以是4个字节长度的ID,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。

       接下来介绍的是得到stream流信息函数,其定义如下:

/**
 * @brief       得到stream流信息
 * @param       buf  : 流开始地址(必须是01wb/00wb/01dc/00dc开头)
 * @retval      执行结果
 *   @arg       AVI_OK, AVI文件解析成功
 *   @arg       其他  , 错误代码
 */
AVISTATUS avi_get_streaminfo(uint8_t *buf)
{
    g_avix.StreamID = MAKEWORD(buf + 2);           /* 得到流类型 */
    g_avix.StreamSize = MAKEDWORD(buf + 4);       /* 得到流大小 */
    if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE)  /* 帧大小太大了,直接返回错误 */
    {
        g_avix.StreamSize = 0;
        return AVI_STREAM_ERR;
    }
/* 奇数加1(g_avix.StreamSize,必须是偶数) */
    if (g_avix.StreamSize % 2)g_avix.StreamSize++;  
if (g_avix.StreamID == AVI_VIDS_FLAG || g_avix.StreamID == AVI_AUDS_FLAG)
return AVI_OK;
    return AVI_STREAM_ERR;
}

       该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。

       mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG解码数据源的函数,其定义如下:

/**
 * @brief        初始化jpeg解码数据源
 * @param        offx   : x方向的偏移
 * @param        offy   : y方向的偏移
 * @retval       0,成功;
 *               1,失败
 */
uint8_t mjpegdec_init(uint16_t offx, uint16_t offy)
{
    cinfo = mymalloc(SRAMIN, sizeof(struct jpeg_decompress_struct));
    jerr = mymalloc(SRAMIN, sizeof(struct my_error_mgr));
    jmembuf = mymalloc(SRAMIN, MJPEG_MAX_MALLOC_SIZE);  /* MJPEG解码内存池申请 */
 
    if (cinfo == 0 || jerr == 0 || jmembuf == 0)
    {
        mjpegdec_free();
        return 1;
    }
 
    /* 保存图像在x,y方向的偏移量 */
    imgoffx = offx;
    imgoffy = offy;
    
    return 0;
}

       该函数用于初始化jpeg解码,主要是申请内存,然后确定视频在液晶上面的偏移(让视频显示在LCD中央)。

       注意:如果是MCU屏,我们需要申请内存。如果是RGB屏,则可以直接使用RGB屏的显存。但是我们这款开发板不支持RGB屏,所以这种情况就不分析了。

       下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:

/**
 * @brief        mjpeg结束,释放内存
 * @param        无
 * @retval       无
 */
void mjpegdec_free(void)
{
    myfree(SRAMIN, p_cinfo);
    myfree(SRAMIN, p_jerr);
    myfree(SRAMIN, p_jmembuf);
}

       该函数用于释放内存,解码结束后调用。

       下面介绍的是解码一副JPEG图片函数,其定义如下:

/**
 * @brief        解码一副JPEG图片
 * @param        buf     : jpeg数据流数组
 * @param        bsize   : 数组大小
 * @retval       0,成功;
 *                 其他,错误
 */
uint8_t mjpegdec_decode(uint8_t *buf, uint32_t bsize)
{
    JSAMPARRAY buffer;
 
    if (bsize == 0)return 1;
 
    p_jpegbuf = buf;
    g_jbufsize = bsize;
    g_jmempos = 0;    /* MJEPG解码, 重新从0开始分配内存 */
 
    p_cinfo->err = jpeg_std_error(&p_jerr->pub);
    p_jerr->pub.error_exit = my_error_exit;
    p_jerr->pub.emit_message = my_emit_message;
 
    //if(bsize>20*1024)printf("s:%d\r\n",bsize);
    if (setjmp(p_jerr->setjmp_buffer))    /* 错误处理 */
    {
        jpeg_abort_decompress(p_cinfo);
        jpeg_destroy_decompress(p_cinfo);
        return 2;
    }
 
    jpeg_create_decompress(p_cinfo);
    jpeg_filerw_src_init(p_cinfo);
    jpeg_read_header(p_cinfo, TRUE);
    p_cinfo->dct_method = JDCT_IFAST;
    p_cinfo->do_fancy_upsampling = 0;
    jpeg_start_decompress(p_cinfo);
lcd_set_window(g_imgoffx, g_imgoffy, p_cinfo->output_width, 
                  p_cinfo->output_height);
    lcd_write_ram_prepare();        /* 开始写入GRAM */
 
    while (p_cinfo->output_scanline < p_cinfo->output_height)
    {
        jpeg_read_scanlines(p_cinfo, buffer, 1);
    }
 
    lcd_set_window(0, 0, lcddev.width, lcddev.height);  /* 恢复窗口 */
    jpeg_finish_decompress(p_cinfo);
    jpeg_destroy_decompress(p_cinfo);
    
    return 0;
}

       该函数是解码jpeg的主要函数,通过前面50.1.2节介绍过的步骤进行解码,该函数的参数buf指向内存里面的一帧jpeg数据,bsize是数据大小。


       2. APP驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。APP驱动源码包括两个文件:videoplayer.c和videoplayer.h。  

       videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:  

/**
 * @brief       播放一个MJPEG文件
 * @param       pname   : 文件名
 * @retval      执行结果
 *   @arg       KEY0_PRES , 下一个视频
 *   @arg       KEY1_PRES , 上一个视频
 *   @arg       其他      , 错误
 */
uint8_t video_play_mjpeg(uint8_t *pname)
{
    uint8_t *framebuf;  /* 视频解码buf */
    uint8_t *pbuf;      /* buf指针 */
    FIL *favi;
    uint8_t  res = 0;
    uint8_t i = 0;
    uint16_t offset = 0;
    uint32_t nr;
    uint8_t key;
    uint8_t saisavebuf;
 
    for (i = 0; i < AVI_AUDIO_BUF_NUM; i++)
    {
        p_avi_sai_buf[i] = mymalloc(SRAMIN, AVI_AUDIO_BUF_SIZE);   
/* 申请音频内存 */
        
        if (p_avi_sai_buf[i] == NULL)          /* 申请失败, 直接退出 */
        {
            break;
        }
        
        memset(p_avi_sai_buf[i], 0, AVI_AUDIO_BUF_SIZE); /* 数据清零 */
    }
 
    favi = (FIL *)mymalloc(SRAMIN, sizeof(FIL));          /* 申请favi内存 */
    framebuf = mymalloc(SRAMIN, AVI_VIDEO_BUF_SIZE) ;    /* 申请视频buf */
 
if (!framebuf)  
/* 只要最后这个视频buf申请失败, 前面的申请失不失败都不重要, 总之就是失败了 */
    {
        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(&g_avix);
            /* 10Khz计数频率,加1是100us */
            btim_tim7_int_init(g_avix.SecPerFrame / 100 - 1, 25000 - 1);  
/* 寻找movi ID */
            offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");
            avi_get_streaminfo(pbuf + offset + 4);              /* 获取流信息 */
            f_lseek(favi, offset + 12);    /* 跳过标志ID,读地址偏移到流数据开始处 */
            res = mjpegdec_init((lcddev.width - g_avix.Width) / 2, 110 
+ (lcddev.height - 110 - g_avix.Height) / 2);    /* JPG解码初始化 */
 
            if (g_avix.SampleRate)       /* 有音频信息,才初始化 */
            {
                es8388_sai_cfg(0, 3);      /* 飞利浦标准,16位数据长度 */
                sai1_saia_init(SAI_MODEMASTER_TX, SAI_CLOCKSTROBING_RISINGEDGE, 
SAI_DATASIZE_16);
                sai1_samplerate_set(g_avix.SampleRate);      /* 设置采样率 */
                sai1_tx_dma_init(p_avi_sai_buf[0], p_avi_sai_buf[1], 
g_avix.AudioBufSize / 2, 1);  /* 配置DMA */
                /* 回调函数指向SAI_DMA_Callback */
sai1_tx_callback = audio_sai_dma_callback;
                g_avi_sai_playbuf = 0;
                saisavebuf = 0;
                sai1_play_start();   /* 开启SAI播放 */
            }
 
            while (1)            /* 播放循环 */
            {
                if (g_avix.StreamID == AVI_VIDS_FLAG)  /* 视频流 */
                {
                    pbuf = framebuf;
                    /* 读入整帧+下一数据流ID信息 */
                    f_read(favi, pbuf, g_avix.StreamSize + 8, &nr);   
                    res = mjpegdec_decode(pbuf, g_avix.StreamSize);
                    if (res)
                    {
                        printf("decode error!\r\n");
                    }
 
                    while (g_avi_frameup == 0);/*待时间到达(在TIM7的中断里面设置为1)*/
 
                    g_avi_frameup = 0;           /* 标志清零 */
                    g_avi_frame++;
                }
                else         /* 音频流 */
                {
                    video_time_show(favi, &g_avix);       /* 显示当前播放时间 */
                    if (++saisavebuf == AVI_AUDIO_BUF_NUM)
                    {
                        saisavebuf = 0;
                    }
 
                    while (saisavebuf == g_avi_sai_playbuf);
 
                    f_read(favi, p_avi_sai_buf[saisavebuf], g_avix.StreamSize + 8,
 &nr);    /* 填充p_avi_sai_buf */
                    pbuf = p_avi_sai_buf[saisavebuf];
                } 
 
                key = key_scan(0);
                /* KEY0/KEY1按下,播放下一个/上一个视频 */
                if (key == KEY0_PRES || key == KEY1_PRES)  
                {
                    res = key;
                    break;
                }
                else if (key == WKUP_PRES)
                {
                    sai1_play_stop();    /* 关闭音频 */
                    video_seek(favi, &g_avix, framebuf);
                    pbuf = framebuf;
                    sai1_play_start();   /* 开启DMA播放 */
                }
/* 读取下一帧 流标志 */
                if (avi_get_streaminfo(pbuf + g_avix.StreamSize))  
                {
                    printf("g_frame error \r\n");
                    res = KEY0_PRES;
                    break;
                }
            }
 
            sai1_play_stop();                                        /* 关闭音频 */
            TIM7->CR1 &= ~(1 << 0);                                  /* 关闭定时器7 */
            lcd_set_window(0, 0, lcddev.width, lcddev.height);  /* 恢复窗口 */
            mjpegdec_free();                                         /* 释放内存 */
            f_close(favi);                                           /* 关闭文件 */
        }
    }
 
    /* 释放内存 */
    for(i = 0; i < AVI_AUDIO_BUF_NUM; i++)
    {
        myfree(SRAMIN, p_avi_sai_buf[i]);
    }
    myfree(SRAMIN, framebuf);
    myfree(SRAMIN, favi);
 
    return res;
}

       该函数用来播放一个avi视频文件(mjpg编码),解码过程就是根据前面我们在50.1节最后所介绍的步骤进行。不过在这里,我们的音频播放用了2个buf,以提高解码的流畅度。其他代码,我们就不介绍了,请大家参考本例程源码。


       3. main.c代码

       下面是main函数,其定义如下:

int main(void)
{
    uint16_t id;
 
    HAL_Init();                                   /* HAL库初始化 */
    HAL_ICACHE_Enable();                         /* 使能指令缓存 */
    sys_stm32_clock_init(250, 4, 2, 2);          /* 配置时钟,250MHz */
    delay_init(250);                              /* 初始化延时 */
    usart_init(115200);                          /* 初始化串口 */
    led_init();                                   /* 初始化LED */
    key_init();                                   /* 初始化按键 */
    btim_tim6_int_init(10000 - 1, 25000 - 1);   /* 初始化基本定时器6 */
    lcd_init();                                   /* 初始化LCD */
    es8388_init();                                /* 初始化ES8388 */
    my_mem_init(SRAMIN);                         /* 初始化内部内存池 */
    exfuns_init();                                /* 为exfuns申请内存 */
    f_mount(fs[0], "0:", 1);                     /* 挂载SD卡 */
    f_mount(fs[1], "1:", 1);                     /* 挂载NOR Flash */
    
    /* 初始化SD卡 */
    while (sd_init() != 0)
    {
        lcd_show_string(30, 30, 200, 16, 16, "SD Card Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 30, 200, 16, 16, "Please Check! ", RED);
        delay_ms(500);
    }
    lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
    
    /* 初始化NOR Flash */
    norflash_init();
    id = norflash_read_id();
    while ((id == 0) || (id == 0xFFFF))
    {
        lcd_show_string(30, 30, 200, 16, 16, "NOR Flash Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 30, 200, 16, 16, "Please Check!    ", RED);
        delay_ms(500);
    }
    
    /* 检查字库 */
    while (fonts_init() != 0)
    {
        lcd_show_string(30, 30, 200, 16, 16, "Font Error!  ", RED);
        delay_ms(500);
        lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
        delay_ms(500);
    }
    lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
    
    text_show_string(30, 50, 200, 16, "正点原子STM32开发板",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", 16, 0, RED);
    text_show_string(30, 130, 200, 16, "KEY1: Prev", 16, 0, RED);
    text_show_string(30, 150, 200, 16, "KEY_UP: FF", 16, 0, RED);
    delay_ms(1500);
    
    /* 配置ES8388 */
    es8388_adda_cfg(1, 0);
    es8388_input_cfg(0);
    es8388_output_cfg(1, 1);
    es8388_hpvol_set(25);
    es8388_spkvol_set(25);
    
    while (1)
    {
        video_play();
    }
}

       该函数代码比较简单,我们就不多说了。最后,为了提高速度,我们可以对编译器进行设置,选择使用-O2优化,从而优化代码,提高速度(但调试效果不好,建议调试时设置为-O0),编译器设置如图50.3.2.1所示:


图50.3.2.1 编译器优化设置


       设置完后,重新编译即可。至此,本实验的软件设计部分结束。


        50.4 下载验证

       本章,我们例程仅支持MJPG编码的avi格式视频,且音频必须是PCM格式,另外视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料→软件 →视频转换软件 →狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图50.4.1和50.4.2所示:


图50.4.1 软件启动界面和设置


图50.4.2 高级设置


       首先,如图50.4.1所示,点击1处,添加视频,找到你要转换的视频,添加进来。有的视频可能有独立字幕,比如我们打开的这个视频就有,所以在2处选择下字幕(如果没有的,可以忽略此步)。然后在3处,点击▼图标,选择预制方案:AVI-Audio-Video Interleaved(*.avi),即生成.avi文件,然后点击4处的高级设置按钮,进入50.4.2所示的界面,设置详细参数如下:

       视频编码器:选择MJPEG。本例程仅支持MJPG视频解码,所以选择这个编码器。

       视频尺寸:480x272。这里得根据所用LCD分辨率来选择,假设我们用800*480的4.3寸电容屏模块,则这里最大可以设置:272x480。PS:如果是2.8屏,最大宽度只能是240)。

       比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。

       帧率:10。即每秒钟10帧。对于480*272的视频,本例程最高能播放30帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。

       音频编码器:PCMS16LE。本例程只支持PCM音频,所以选择音频编码器为这个。

       采样率:这里设置为11025,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。

       其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。

       点击图50.4.1的5处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中6处的按钮,即可开始转换了,如图50.4.3所示:


图50.4.3 正在转换


       等转换完成后,将转换后的.avi文件,拷贝到SD卡→VIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。

       将程序下载到开发板后,程序先检测字库,只有字库已经更新才可以继续执行后面的程序。字库已更新,就可以看到LCD首先显示一些实验相关的信息,如图50.4.4所示:


图50.4.4显示实验相关信息


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


图50.4.5 视频播放中


       可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY1,可以切换到下一个/上一个视频,按KEY_UP,可以快进。

       至此,本例程介绍就结束了。本实验,我们在开发板上实现了视频播放,体现了STM32H562强大的处理能力。

       本例程只支持竖屏宽度的分辨率解码(比如800*480的屏,最大只支持480宽度的视频解码),如果想要支持更大分辨率的视频解码,则必须使用横屏模式,需要在本例程源码的基础上稍作修改。

       附本实验测试结果(视频比特率:1000,音频均为:11025,立体声)

       对 240*160/240*180 分辨率,可达30帧

       对 320*240 分辨率,可达25帧

       对 480*272 分辨率,可达15帧

       最后提醒大家,转换的视频分辨率,一定要根据自己的LCD设置,不能超过LCD的尺寸!!否则无法播放(可能只听到声音,看不到图像)。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3106525/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2024-08-30

  • 博文量
    588
  • 访问量
    764013