欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)

程序员文章站 2022-07-14 18:41:28
...

〇、写在最前

本文档帮助理解以hi3518ev200为例的H.264视频流采集流程,以及MPP业务在应用层的基本使用方式。
MPP(Media Process Platform)是海思提供的媒体处理软件平台,该应用屏蔽了芯片底层,对应用直接提供MPI(MPP Programe Interface)接口完成相应功能。所以对应用而言只需要关心MPP的业务流程就可以了。SAMPLE库中提供的程序中,为了规避错误以及提供演示,添加了很多的错误判断和状态输出,所以在下述叙述过程中将忽略例程中的debug信息。所有页码无特殊说明均指向《HiMPP IPC V2.0 媒体处理软件开发参考》,下文称“手册”。
我在项目中使用hi3518ev200,实现一路视频的采集编码,使用Socket通信发出,在这个过程中首次接触到了IPC SoC,这也是我步入嵌入式学习的第一步。我相信我现在的目光还是很短钱,描述也很粗浅,但是为了能够记录下我的学习和成长,备忘的同时帮助后来者,我愿意花时间将自己对程序的理解记录下来,并借此对自己的程序做精简和优化。
本记录试图不依赖官方SAMPLE库做解释,官方例程仅做参考。侧重在MPP业务应用流程上。
本文涉及到的程序参考源码和手册在文末第七章提供网盘下载链接。

一、MPP初始化

按照开发板提供的官方文档,海思的这款芯片属于经济型产品,上面运行SDK中提供的操作系统,官方提供的Linux源码里已经包含了芯片中相关的所有驱动,所以直接使用就可以了。在片上linux环境下编程,并挂载MPP的环境,此时程序运行才能找到所需要的资源,媒体处理平台处于操作系统和应用程序之间(26页),它支撑起视频编码的所有任务。
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)

1.建立视频缓冲池

视频缓冲池主要向媒体业务提供大块物理内存管理功能,负责内存的分配和回收(31页)。在视频编码过程中,这个缓冲池提供给MPP编码过程中视频数据的存放空间。视频帧内存大小计算方法和传入的像素格式有关(99页),缓存池参数结构体为VB_CONF_S类型(P98)。
该结构体中需要计算的参数如下:

VB_CONF_S stVbConf;       //缓存池参数结构体
.u32MaxPoolCnt=128        //hi3518ev200例程中默认就是128
.astCommPool[0].u32BlkSize//根据说明计算出来的缓冲区块大小(例程中在SAMPLE_SYS_CalcPicVbBlkSize函数完成),具体计算过程上述手册参考页中也有。
.astCommPool[0].u32BlkCnt //缓冲块数量,hi3518ev200例程中默认是4。

在缓冲区块大小的计算中有一个宏CEILING_2_POWER(宽或高[1280/1080/720…],对齐宽度[64/32/16…]):

#define CEILING_2_POWER(x,a)    ( ((x) + ((a) - 1) ) & ( ~((a) - 1) ) )

按照网上给出的解释,该宏返回参数a的倍数,该倍数是大于参数x的最小值,例如当x为1280,a为64时,由于1280÷64=20,所以返回64*20≥1280;当x为720,a为64时,由于720÷64=11.25,所以返回64 * 12=768≥720。
例程中计算的方式以1280 * 720非压缩YUV_420为例结果是:1280 * 768 * 1.5+VB_PIC_HEADER_SIZE(压缩头)。

2.退出MPP系统

为了防止一些错误操作,运行MPP之前应确保其处在停止状态。使用以下函数确保运行环境:

HI_MPI_SYS_Exit();
HI_MPI_VB_Exit();

3.配置并启动系统

MPP_SYS_CONF_S stSysConf={0};           //系统控制参数结构体
stSysConf.u32AlignWidth=SYS_ALIGN_WIDTH;//设置的对齐宽度(例程中是64)
s32Ret=HI_MPI_VB_SetConf(&stVbConf);    //设置MPP视频缓存池属性
s32Ret=HI_MPI_VB_Init();                //初始化MPP视频缓冲池
s32Ret=HI_MPI_SYS_SetConf(&stSysConf);  //配置系统控制参数
s32Ret=HI_MPI_SYS_Init();               //初始化MPP系统

其中,stVbConf即为缓存池参数结构体,stSysConf.u32AlignWidth提供第一步中的对齐宽度(例程中为64),s32Ret为函数返回值。
经过以上步骤,MPP业务已经为本应用程序启动并准备好了视频缓冲池。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出),否则MPP业务无法保证后续应用是否可以正常执行。

二、VI设备配置和初始化

该部分内容在手册118页-121页。VI(Video In)模块是将采集来的原始视频数据做基本处理(也可以不处理)后,输出给后续模块。实际上就是整个MPP业务对底层的对接。所以涉及到一些非常重要的配置例如掩码。
由于我的项目是基于hi3518ev200做的,所以我只关注手册上关于该款芯片的部分,为了尽可能简化记录内容,从此处的记录仅包括为了输出码流进行的操作,不包括图像校正和调优。
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)
手册上包括了Hi3516A/Hi3518EV200/Hi3519V100三款芯片VI模块的通道功能框图,仅从通道功能上看,Hi3518EV200比Hi3516A增加了MirrorFlip(镜像和翻转);Hi3519V100比Hi3518EV200增加了DIS(防抖动)功能。以Hi3518EV200为重点,其仅包含一个VI设备,即Dev0。这个Dev0可以接收镜头传感器或者采集AD的信息,从Dev0出来的数据传递给通道Chn0,Hi3518EV200仅包含一个VI物理通道,即Chn0,不存在次通道,但支持扩展通道。扩展通道最多支持到16和,数据来源于物理通道,主要实现缩放功能,此处我用不到。如果我们正确配置了sensor-Dev0-Chn0,使得原始视频数据从Chn0传输给后级模块,那么VI模块的配置就算完成了。

1.MIPI配置

按照上述所说,在完成了MPP初始化之后,我们的配置途径应该是sensor-Dev0-Chn0,本步骤配置的是sensor寄存器,需要直接和sensor进行通信。

        combo_dev_attr_t MIPI_CMOS3V3_ATTR=
        {
            /* input mode */
            .input_mode=INPUT_MODE_CMOS_33V,
            {
                
            }
        };
        
        combo_dev_attr_t *pstcomboDevAttr=&MIPI_CMOS3V3_ATTR;
        HI_S32 fd;
        fd=open("/dev/hi_mipi",O_RDWR);//打开MIPI设备,读写打开
        if(fd<0)//设备存在性检测
        {
            printf("Warning:Open HI-MIPI device failed!\n");
            //错误处理(如对已进行的操作回退并退出程序)
        }
        if(ioctl(fd, HI_MIPI_SET_DEV_ATTR, pstcomboDevAttr))//获取设备调用请求码
        {
            printf("Set MIPI Attributes failed!\n");
            close(fd);
            //错误处理(如对已进行的操作回退并退出程序)
        }
        close(fd);//关闭设备

在这个过程中由于设置的传感器输入模式是空结构体,所以我不是很能理解操作的目的,只能当做对传感器存在性的一种检测。

2.ISP(Image Signal Processing,图像信号处理)配置及初始化

ISP是运行在sensor和VI模块之间的控制结构(参见《HiISP 开发参考》概述部分),sensor经过光电转换,将Bayer格式的原始图像送给ISP,ISP经过算法处理,输出RGB空间域的图像给后端的视频采集单元。
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)
所以在MIPI配置完成后进行的,是ISP的配置。

        s32Ret=sensor_register_callback();          //1.传感器注册回调函数
        ISP_DEV IspDev=0;
        ALG_LIB_S stLib;
        stLib.s32Id=0;
        strcpy(stLib.acLibName,HI_AE_LIB_NAME);
        s32Ret=HI_MPI_AE_Register(IspDev, &stLib);  //2.注册海思AE库
        strcpy(stLib.acLibName, HI_AWB_LIB_NAME);
        s32Ret=HI_MPI_AWB_Register(IspDev, &stLib); //3.注册海思AWB库
        strcpy(stLib.acLibName, HI_AF_LIB_NAME);
        s32Ret=HI_MPI_AF_Register(IspDev, &stLib);  //4.注册海思AF库
        s32Ret=HI_MPI_ISP_MemInit(IspDev);          //5.ISP存储初始化
        ISP_WDR_MODE_S stWdrMode;
        stWdrMode.enWDRMode=WDR_MODE_NONE;          //不设置宽动态
        s32Ret=HI_MPI_ISP_SetWDRMode(0, &stWdrMode);//6.ISP设置宽动态模式
        //下方的ISP_PUB_ATTR_S需要适配传感器,原代码在sample_comm_isp.c中可以参考
        ISP_PUB_ATTR_S stPubAttr;
        stPubAttr.enBayer              =BAYER_BGGR;
        stPubAttr.f32FrameRate         =30;
        stPubAttr.stWndRect.s32X       =0;
        stPubAttr.stWndRect.s32Y       =0;
        stPubAttr.stWndRect.u32Width   =1280;
        stPubAttr.stWndRect.u32Height  =720;
        s32Ret=HI_MPI_ISP_SetPubAttr(IspDev, &stPubAttr);//7.ISP设置Pub属性
        s32Ret=HI_MPI_ISP_Init(IspDev);                  //8.ISP初始化
        gbIspInited=HI_TRUE;//置ISP成功初始化标志位

原例程中的gbIspInited是标志位,‘gb’表示是全局变量。该标志位设置的意义是在出现问题或者程序终止时能够按条件对ISP进行去初始化。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。

3.启动ISP线程

该步骤在SAMPLE例程中由单函数SAMPLE_ISP_Run()完成,从此处可以分析得知,ISP不是属性设置,也不是静态的函数库,而是配合MPP业务独立运行的线程。既然是线程就需要考虑资源回收等问题,也就是必须有相应的结束线程的操作,故此可以理解例程中对去初始化顺序如此设置的目的了。
该函数位于sample_comm_isp.c文件中,去除掉例程中关于宏定义的部分,实际参与编译的为:

        pthread_t gs_IspPid=0;//ISP线程描述符
        
        HI_VOID* Test_ISP_Run(HI_VOID *param)
        {
            ISP_DEV IspDev = 0;
            HI_MPI_ISP_Run(IspDev);

            return HI_NULL;
        }

        HI_S32 SAMPLE_ISP_Run()
        {
            pthread_attr_t attr;
            pthread_attr_init(&attr);
            pthread_attr_setstacksize(&attr,4096 * 1024);
            if(0!=pthread_create(&gs_IspPid,&attr,(void*(*)(void*))Test_ISP_Run,NULL))
            {
                printf("%s: create isp running thread failed!\n",__FUNCTION__);
                pthread_attr_destroy(&attr);
                return HI_FAILURE;
            }
            usleep(1000);
            pthread_attr_destroy(&attr);
            return HI_SUCCESS;
        }

函数参见《HiISP 开发参考》23页:该接口是阻塞接口,建议用户采用实时线程处理。虽然不明白为什么针对hi3518ev200需要设置给ISP Firmware运行的栈空间大小。但是由于例程如此操作,便保留了该步。

4.配置&启动视频输入捕获设备

        VI_DEV_ATTR_S DEV_ATTR_OV9712_DC_720P=
        {
            /*连接模式*/
            VI_MODE_DIGITAL_CAMERA,
            /*多重模式*/
            VI_WORK_MODE_1Multiplex,
            /* r_mask    g_mask    b_mask*/
            {0x3FF00000,    0x0},
            /*渐进式或交错式*/
            VI_SCAN_PROGRESSIVE,//渐进式
            /*AdChnId*/
            {-1, -1, -1, -1},
            /*支持的数据序列,仅支持YUV*/
            VI_INPUT_DATA_YUYV,
            /*同步信息*/
            {
                /*port_vsync   port_vsync_neg     port_hsync        port_hsync_neg        */
                VI_VSYNC_FIELD, VI_VSYNC_NEG_HIGH, VI_HSYNC_VALID_SINGNAL,VI_HSYNC_NEG_HIGH,VI_VSYNC_VALID_SINGAL,VI_VSYNC_VALID_NEG_HIGH,
                /*hsync_hfb    hsync_act    hsync_hhb*/
                {370,            1280,        0,
                /*vsync0_vhb vsync0_act vsync0_hhb*/
                 6,            720,        6,
                /*vsync1_vhb vsync1_act vsync1_hhb*/
                 0,            0,            0}
            },
            /*使用内置ISP*/
            VI_PATH_ISP,
            /*输入数据类型*/
            VI_DATA_TYPE_RGB,
            /*数据反转*/
            HI_FALSE,
            {0, 0, 1280, 720}
        };
        
        VI_DEV_ATTR_S stViDevAttr; //设备属性参数结构体
        memset(&stViDevAttr,0,sizeof(stViDevAttr));
        memcpy(&stViDevAttr,&DEV_ATTR_OV9712_DC_720P,sizeof(stViDevAttr));
        //下方代码需要适配传感器,原代码在sample_comm_isp.c中可以参考
        //此处适配仅针对分辨率的不同做调整即可
        stViDevAttr.stDevRect.s32X=0;
        stViDevAttr.stDevRect.s32Y=0;
        stViDevAttr.stDevRect.u32Width=1280;
        stViDevAttr.stDevRect.u32Height=720;
        HI_S32 s32IspDev=0;                                 //ISP设备序号——序号0
        s32Ret=HI_MPI_VI_SetDevAttr(ViDev, &stViDevAttr);   //设置设备属性
        
        //ISP_WDR_MODE_S stWdrMode;                         //WDR模式设置参数结构体,前面定义过的话就不用再定义
        s32Ret=HI_MPI_ISP_GetWDRMode(s32IspDev, &stWdrMode);//获取WDR模式信息
        
        VI_WDR_ATTR_S stWdrAttr;                            //WDR属性设置参数结构体
        stWdrAttr.enWDRMode=stWdrMode.enWDRMode;            //WDR属性设置——WDR模式:从MPI获取
        stWdrAttr.bCompress=HI_FALSE;                       //WDR属性设置——不压缩
        s32Ret=HI_MPI_VI_SetWDRAttr(ViDev, &stWdrAttr);     //设置WDR属性
        
        s32Ret=HI_MPI_VI_EnableDev(ViDev);                  //使能VI设备

这部分需要做几点说明,首先,VI配置结构体DEV_ATTR_OV9712_DC_720P的掩码部分是需要适配传感器硬件连接进行设置的,掩码的配置可以参见手册第120页,如果掩码配置不正确,轻则造成色偏,重则无法显示。
想要理解该部分的作用,需要整体观看ISP和视频捕获设备之间的关系,这里的视频捕获设备就是和ISP传出的数据做对接的那部分模块。所以无论在分辨率设置还是在WDR(宽动态,不用管是什么)的设置上都应该保持一致。所以stViDevAttr参数结构体的作用是使对接通道的窗口和ISP传来的图像保持一致。stWdrMode宽动态参数设置使用HI_MPI_ISP_GetWDRMode函数从ISP中获得并设置给ViDev。ViDev是在配置MIPI时就设置好的仅有一个的视频输入设备。最后将VI设备使能。
HI_MPI_VI_SetDevAttr()函数可以设置的参数和相关信息在手册124页,非常详细,足够参考。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。

5.配置&启动视频输入捕获设备的传输通道

还记的前面说的配置途径吧:sensor-Dev0-Chn0。截至上述步骤,便完成了sensor-Dev0的配置和使能,为了将VI模块产生的图像数据传给后级模块,需要使能唯一的物理通道Chn0。

        RECT_S stCapRect;                           //捕获尺寸
        SIZE_S stTargetSize;                        //目标尺寸
        stCapRect.s32X=0;                           //捕获水平偏移
        stCapRect.s32Y=0;                           //捕获垂直偏移
        stCapRect.u32Width=1280;                    //捕获图片宽度
        stCapRect.u32Height=720;                    //捕获图片高度
        stTargetSize.u32Width=stCapRect.u32Width;   //目标宽度
        stTargetSize.u32Height=stCapRect.u32Height; //目标高度
        
        VI_CHN_ATTR_S stChnAttr;                    //VI通道属性设置参数结构体
        memcpy(&stChnAttr.stCapRect, &stCapRect, sizeof(RECT_S));
        stChnAttr.enCapSel=VI_CAPSEL_BOTH;          //通道属性设置——抽场选择——两场
        stChnAttr.stDestSize.u32Width=stTargetSize.u32Width;  //通道属性设置——目的宽度
        stChnAttr.stDestSize.u32Height=stTargetSize.u32Height;//通道属性设置——目的高度
        stChnAttr.enPixFormat=PIXEL_FORMAT_YUV_SEMIPLANAR_420;//通道属性设置——像素格式——YUC420或YUV422
        stChnAttr.s32SrcFrameRate=-1;               //通道属性设置——源帧率——不进行帧率控制
        stChnAttr.s32DstFrameRate=-1;               //通道属性设置——目的帧率——不进行帧率控制
        stChnAttr.enCompressMode=COMPRESS_MODE_NONE;//通道属性设置——压缩模式——不压缩
        stChnAttr.bMirror=HI_FALSE;                 //通道属性设置——图像镜像——不镜像
        stChnAttr.bFlip=HI_FALSE;                   //通道属性设置——图像翻转——不翻转
        s32Ret=HI_MPI_VI_SetChnAttr(ViChn, &stChnAttr);  //设置VI通道属性
        //图像旋转不需要则不用设置,默认是不旋转
        //s32Ret=HI_MPI_VI_SetRotate(ViChn, ROTATE_NONE);//设置VI图像旋转
        s32Ret=HI_MPI_VI_EnableChn(ViChn);               //使能VI通道

物理通道同样需要和前面传来的数据格式做对接,表现在分辨率和偏移上,除此之外,还可以对图像做一些处理。不过大多数保持默认值就可以。
HI_MPI_VI_SetChnAttr()函数可以设置的参数和相关信息在手册132页,非常详细,足够参考。
另外手册上有一句话:必须先设置设备属性才能设置通道属性,否则会返回失败。从这里我们知道MPP的配置流程必须从源到尾。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。
至此,VI(sensor-Dev0-Chn0)部分已经配置完成,可以连接后级模块了。

三、配置VPSS以及VI-VPSS绑定

VPSS(Video Process Sub-System,视频处理子系统)支持对一幅图像进行统一预处理(手册376页)。
在手册27页已经完整给出了MPP下业务流程的对接关系。另外参照手册520页的VENC输入源可知,VI输出的数据可以经过VPSS发给VENC进行编码,也可以直接发送。由于VPSS支持多种图像处理功能,所以没有理由不加它。
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)

1.创建VPSS组

关于VPSS组的概念在手册376页:
…各芯片(指Hi3516A/Hi3518EV200/Hi3519V100)的最大组数目有所不同,各GROUP分时复用VPSS硬件。每个VPSS组包含多个通道…。

        SIZE_S stSize;                         //图片尺寸设置
        stSize.u32Width=1280;                  //根据设置的模式确定图片大小
        stSize.u32Height=720;                  //即720P-1280*720 1080P-1920*1080
        
        VPSS_GRP_ATTR_S stVpssGrpAttr;         //VPSS组属性参数结构体
        stVpssGrpAttr.u32MaxW=stSize.u32Width; //VPSS组属性——最大图像宽度
        stVpssGrpAttr.u32MaxH=stSize.u32Height;//VPSS组属性——最大图像高度
        stVpssGrpAttr.bIeEn=HI_FALSE;
        stVpssGrpAttr.bNrEn=HI_TRUE;
        stVpssGrpAttr.bHistEn=HI_FALSE;
        stVpssGrpAttr.bDciEn=HI_FALSE;         //VPSS组属性——DCI使能
        stVpssGrpAttr.enDieMode=VPSS_DIE_MODE_NODIE;           //VPSS组属性——DIE模式
        stVpssGrpAttr.enPixFmt=PIXEL_FORMAT_YUV_SEMIPLANAR_420;//VPSS组属性——像素格式——YUV420
        //——检查并创建VPSS组
        VPSS_GRP VpssGrp=0;                    //当前设置的VPSS组号——0
        s32Ret=HI_MPI_VPSS_CreateGrp(VpssGrp,&stVpssGrpAttr);  //创建VPSS组
        
        VPSS_NR_PARAM_U unNrParam={{0}};       //VPSS3DNR参数存放变量
        s32Ret=HI_MPI_VPSS_GetNRParam(VpssGrp, &unNrParam);    //获取VPSS-3DNR参数
        s32Ret=HI_MPI_VPSS_SetNRParam(VpssGrp, &unNrParam);    //设置VPSS-3DNR参数
        
        s32Ret=HI_MPI_VPSS_StartGrp(VpssGrp);  //启动VPSS组

HI_MPI_VPSS_CreateGrp()函数在手册386页,关于这个函数,在手册中有严格的要求:
离线模式时,可创建多个GROUP,最大GROUP数为VPSS_MAX_GRP_NUM;在线模式时,仅支持创建1个GROUP,且GROUP号仅能为0。不支持重复创建。在线模式时,由于VI和VPSS的逻辑处理需要时序严格同步,所以GROUP创建中的group的图像属性必须和VI的图像设置属性一致;否则会出现VPSS的中断错误。
这个时候,就必须要考虑一个问题:什么是离线模式和在线模式。这个问题的答案在手册32页:
…模式切换由load脚本参数控制…VI/VPSS在线模式是指VI进行时序解析后直接在芯片内部将数据传递到VPSS,中间无DDR写出的过程。…
对比项目所需以及我们的配置流程和硬件选型,推断出我使用的是在线模式,所以设且只能设置一组VPSS组,且VPSS组属性要和前级完全一致。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。

2.进行VI-VPSS绑定

想要理解VPSS的绑定,首先需要了解VPSS功能,VPSS功能描述在手册378页:
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)
通过调用SYS模块的绑定接口,可与VI和VO/VENC/IVE 等模块进行绑定,其中前者为VPSS的输入源,后者为VPSS的接收者。用户可通过提供的MPI接口对GROUP进行管理。每个GROUP仅可与一个输入源绑定。GROUP的物理通道有两种工作模式:AUTO和USER,两种模式间可动态切换。默认的工作模式为AUTO,此模式下各通道仅可与一个接收者绑定。若想使用USER模式,则需调用MPI接口进行设置,同时指定所需图像的大小和格式,此模式下各通道可与多个接收者绑定。

        MPP_CHN_S stSrcChn;          //源通道设备结构体
        MPP_CHN_S stDestChn;         //目的通道设备结构体
        stSrcChn.enModId=HI_ID_VIU;  //指定模块ID为VI
        stSrcChn.s32DevId=0;         //指定VI设备号
        stSrcChn.s32ChnId=0;         //指定VI通道号
        stDestChn.enModId=HI_ID_VPSS;//指定模块ID为VPSS
        stDestChn.s32DevId=VpssGrp;  //指定VPSS组号
        stDestChn.s32ChnId=0;        //指定VPSS通道号
        s32Ret=HI_MPI_SYS_Bind(&stSrcChn, &stDestChn);//执行绑定
        
        VPSS_CHN_MODE_S stVpssChnMode;//VPSS通道模式参数结构体
        stVpssChnMode.enChnMode        =VPSS_CHN_MODE_USER;//VPSS通道模式——通道模式
        stVpssChnMode.bDouble        =HI_FALSE;
        stVpssChnMode.enPixelFormat=PIXEL_FORMAT_YUV_SEMIPLANAR_420;//VPSS通道模式——像素格式——YUV420
        stVpssChnMode.u32Width        =stSize.u32Width;   //VPSS通道模式——最大图像宽度
        stVpssChnMode.u32Height     =stSize.u32Height;  //VPSS通道模式——最大图像高度
        stVpssChnMode.enCompressMode=COMPRESS_MODE_NONE;//VPSS通道模式——缩放模式——无缩放
        
        VPSS_CHN_ATTR_S stVpssChnAttr;//VPSS通道属性参数结构体
        memset(&stVpssChnAttr, 0, sizeof(stVpssChnAttr));
        stVpssChnAttr.s32SrcFrameRate=-1;//VPSS通道属性——源帧率——不进行帧率控制
        stVpssChnAttr.s32DstFrameRate=-1;//VPSS通道属性——目的帧率——不进行帧率控制
        
        VPSS_CHN VpssChn=0;           //当前设置的VPSS通道号——0
        s32Ret=HI_MPI_VPSS_SetChnAttr(VpssGrp, VpssChn, &stVpssChnAttr);//设置VPSS通道属性,如果需要扩展通道也在此处设置
        s32Ret=HI_MPI_VPSS_SetChnMode(VpssGrp, VpssChn,&stVpssChnMode);//设置VPSS通道模式
        s32Ret=HI_MPI_VPSS_EnableChn(VpssGrp, VpssChn);//使能指定的VPSS组和VPSS通道

和前文一样,这些通道的设置不是只有这些项,具体的在手册上都有描述,若只是传图像进行编码,上述的设置完全足够了,这些设置项均是例程中体现出来的,没有增删。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。
可以发现,VI-VPSS绑定的步骤是很简单的,因为没有底层的加入,所以只需设置对接双方的参数一致即可绑定。步骤就是:参数设置-写入参数-使能相关组或通道-绑定。
至此完成了VI(sensor-Dev0-Chn0)–VPSS(Group0,Chn0)的绑定。

四、启动VENC以及VPSS-VENC绑定

VENC(Video Encoder)即视频编码模块。
Hi3518EV200实现H264视频采集的源码及流程详解(不依赖SAMPLE库)
由于项目需要的是H264编码方式,所以下文记录以H264为主。由于我不需要对图像进行遮挡和覆盖,所以按照手册上所给的流程,只需要对VENC通道进行配置和对接、对编码方式作出配置,而后使能和绑定即可。

1.创建VENC通道&使能图像接收

        PAYLOAD_TYPE_E enPayLoad=PT_H264;    //选择编码方式——H264,更换编码要同时更换后面的设置参数
        SIZE_S stPicSize;                    //图像尺寸信息变量
        stPicSize.u32Width=1280;            //设置图像宽度
        stPicSize.u32Height=720;            //设置图像高度
        HI_U32 u32Profile=0;                //0: baseline; 1:MP; 2:HP;3:Svc_t
        
        VENC_CHN_ATTR_S stVencChnAttr;                   //VENC通道属性参数结构体
        stVencChnAttr.stVeAttr.enType=PT_H264;         //设置VENC通道属性有效荷载为H264编码
        if(enPayLoad==PT_H264)
        {
            VENC_ATTR_H264_S stH264Attr;                   //H264属性参数结构体
            stH264Attr.u32MaxPicWidth=stPicSize.u32Width;  //设置最大图像宽度
            stH264Attr.u32MaxPicHeight=stPicSize.u32Height;//设置最大图像高度
            stH264Attr.u32PicWidth=stPicSize.u32Width;     //设置图像宽度
            stH264Attr.u32PicHeight=stPicSize.u32Height;   //设置图像高度
            stH264Attr.u32BufSize=stPicSize.u32Width * stPicSize.u32Height; //流缓冲器大小
            stH264Attr.u32Profile=u32Profile;              //0: baseline; 1:MP; 2:HP;  3:svc_t
            stH264Attr.bByFrame=HI_TRUE;                   //获取流模式是切片模式还是帧模式?
            stH264Attr.u32BFrameNum=0;                     //0: 不支持B帧; >=1:B帧的数量
            stH264Attr.u32RefNum=1;                        //0: 默认; >=0:参考帧数量
            
            memcpy(&stVencChnAttr.stVeAttr.stAttrH264e, &stH264Attr, sizeof(VENC_ATTR_H264_S));
            
            stVencChnAttr.stRcAttr.enRcMode=VENC_RC_MODE_H264CBR;//选择编码码率控制方式——CBR
            VENC_ATTR_H264_CBR_S stH264Cbr;                   //H264-CBR码率控制参数结构体
            stH264Cbr.u32Gop         =(VIDEO_ENCODING_MODE_PAL==gs_enNorm)?25:30;
            stH264Cbr.u32StatTime    =1;                   //码率统计时间
            stH264Cbr.u32SrcFrmRate     =(VIDEO_ENCODING_MODE_PAL==gs_enNorm)?25:30;//输入帧率
            stH264Cbr.fr32DstFrmRate=(VIDEO_ENCODING_MODE_PAL==gs_enNorm)?25:30; //目标帧率
            //下一条配置需要根据实际分辨率进行更改
            stH264Cbr.u32BitRate=1024*2;//1280*720分辨率
            stH264Cbr.u32FluctuateLevel=0;//平均比特率
            memcpy(&stVencChnAttr.stRcAttr.stAttrH264Cbr, &stH264Cbr, sizeof(VENC_ATTR_H264_CBR_S));
        }
        
        VENC_CHN VencChn=0;//选择VENC通道号——0
        s32Ret=HI_MPI_VENC_CreateChn(VencChn, &stVencChnAttr);//创建VENC通道
        s32Ret=HI_MPI_VENC_StartRecvPic(VencChn);

为了以后修改编码方式方便,将VENC通道的有效荷载设置enPayLoad提取出来,这样有助于对参数配置进行整体替换。在这部分程序中,我们主要设置了两个参数结构体,一是H264属性参数结构体stH264Attr,二是H264-CBR码率控制参数结构体stH264Cbr。由于结构体及成员命名中有‘h264’字样,我们可以推测这部分的设置是针对编码通道进行的。
在这部分中有一些参数的设置需要开发者根据实际需求确定:
(1)图像宽和高设置
在手册521页有这样的描述:
通道接收到图像之后,比较图像尺寸和编码通道尺寸:
如果输入图像比编码通道尺寸大,VENC将按照编码通道尺寸大小,调用VGS对源图像进行缩小,然后对缩小之后的图像进行编码。
如果输入图像比编码通道尺寸小,VENC丢弃源图像。VENC不支持放大输入图像编码。
如果输入图像与编码通道尺寸相当,VENC直接接受源图像,进行编码。
也就是说VENC通道对图像有一定的缩小能力,所以如果摄像头时1080P而需要720P的图像时,可以从此处修改缩小比例得到。
(2)编码配置之stH264Attr.u32Profile
该配置取值0~3,对应baseline/MP/HP/svc_t四种编码方式,具体区别网上可以找到,在VENC设置时没有需要特别注意的地方,但是在码流获取的时候,svc_t的编码方式下可以由用户选择保存什么样质量的视频流。这部分区别可以在sample_comm_venc.c文件中对比SAMPLE_COMM_VENC_GetVencStreamProc()函数和SAMPLE_COMM_VENC_GetVencStreamProc_Svc_t()函数得到。
(3)VIDEO_ENCODING_MODE_PAL==gs_enNorm
gs_enNorm是例程中设置的一个全局静态变量,其值为VIDEO_ENCODING_MODE_NTSC。PAL和NTSC是不兼容的两种视频制式。
(4)VBR码率控制之stH264Cbr.u32BitRate
该值在sample_comm_venc.c文件中的SAMPLE_COMM_VENC_Start()函数里可以参考。但在手册538页,该值被设置为了一个比例程中任何一个都大的值10*1024。则可以知道,只需要设置项目所需最大编码分辨率对应的取值即可。
还有一些需要注意的地方:
(5)编码器的动态属性和静态属性
编码器属性中除通道宽高(u32PicWidth和u32PicHeight)外都是静态属性,一旦创建编码通道成功,静态属性不支持被修改,除非该通道被销毁,重新创建(手册536页)。使用函数HI_MPI_VENC_SetChnAttr()可以且只能设置编码通道属性中的动态属性,如果设置静态属性,则返回失败(手册549页)。
这似乎说明可以在视频编码时无需重新初始化地更改视频流分辨率大小。
(6)VENC通道号
关于VENC支持的通道数量我没有找到。但是我只需要一路的视频编码,所以通道0一定是可用的。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。

2.进行VPSS-VENC绑定

该步绑定操作和VI-VPSS绑定方法一致,就不多做记录了。

        stSrcChn.enModId=HI_ID_VPSS;
        stSrcChn.s32DevId=0;          //VPSS组号
        stSrcChn.s32ChnId=0;          //VPSS通道号
        stDestChn.enModId=HI_ID_VENC;
        stDestChn.s32DevId=0;         //VENC设备ID
        stDestChn.s32ChnId=0;         //VENC通道号
        s32Ret=HI_MPI_SYS_Bind(&stSrcChn, &stDestChn);//绑定源通道和目的通道

*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。
至此完成了VI(sensor-Dev0-Chn0)–VPSS(Group0,Chn0)–VENC(H264,CBR,Chn0)的绑定。

五、VENC流处理

执行完上述操作后,视频流就开始产生了,下面以多包模式下(手册527页),stH264Attr.u32Profile=0/1/2时,对H264编码码流进行一次查询和采集的过程做记录,操作过程可以参考手册790页的描述。

    VENC_CHN_STAT_S stStat;  //通道状态变量
    VENC_STREAM_S stStream;  //VENC流
    //--1--查询编码通道状态
    s32Ret=HI_MPI_VENC_Query(0, &stStat);
    if(0==stStat.u32CurPacks)//如果当前帧的码流包个数为0
    {
        continue;            //进入下一轮检测
    }
    //--2--开辟对应包节点数的空间
    stStream.pstPack=(VENC_PACK_S*)malloc(sizeof(VENC_PACK_S) * stStat.u32CurPacks);
    if(NULL==stStream.pstPack)
    {
        printf("Malloc stream pack failed!\n");
        break;
    }
    //--3--调用MPI获取一帧的流
    stStream.u32PackCount=stStat.u32CurPacks;
    s32Ret=HI_MPI_VENC_GetStream(0, &stStream, HI_TRUE);
    if(HI_SUCCESS!=s32Ret)
    {
        free(stStream.pstPack);
        stStream.pstPack=NULL;
        printf("Get VENC Stream failed with %#x!\n",s32Ret);
        break;
    }
    //--4--处理流
    if(stStream.u32PackCount==4)
    {
        //说明当前处理的是多包模式下每秒的第一帧
    }
    else
    {
        //说明当前处理的是多包模式下每秒的剩余帧
    }
    //--5--释放空间
    HI_MPI_VENC_ReleaseStream(0,&stStream);
    free(stStream.pstPack);
    stStream.pstPack=NULL;

这个过程应该是放在while里一直轮询完成的,所以其中存在continue和break操作。如果任何情况下任一函数返回值不为HI_SUCCESS,则应当给予处理(例如去初始化并退出)。
当视频编码为Svc_t时,大体流程一致,但需要在帧处理处对stStream.stH264Info.enRefType进行判断以确定要如何处理,该操作可以参考sample_comm_venc.c文件中SAMPLE_COMM_VENC_GetVencStreamProc_Svc_t()函数。
对于首帧,数据包帧头地址为stStream.pstPack[3].pu8Addr,对于其它帧,只有一个包,则帧头地址在stStream.pstPack[0].pu8Addr.如需访问其中每个字节,可以使用以下方式:

    HI_S32 i;
    HI_U8 *p;
    for(i=0,i<stStream.u32PackCount,i++)
    {
        p=stStream.pstPack[0].pu8Addr+i*sizeof(HI_U8);
        //*p即为当前偏移为i处的字节
    }

六、系统去初始化和退出处理

各部分退出时的操作和上述步骤顺序相反。需要理解的一点是,在停止某个模块的采集时,应该先将该模块和前级模块断开,否则传来的数据得不到处理可能会有错误。

1.VENC模块去初始化和退出

照此顺序首先处理VENC模块,先解绑通道,再销毁通道:

        stSrcChn.enModId=HI_ID_VPSS;
        stSrcChn.s32DevId=0;     //VPSS组号
        stSrcChn.s32ChnId=0;     //VPSS通道号
        stDestChn.enModId=HI_ID_VENC;
        stDestChn.s32DevId=0;    //VENC设备ID
        stDestChn.s32ChnId=0;    //VENC通道号
        /--1--解绑VPSS-VENC通道
        s32Ret=HI_MPI_SYS_UnBind(&stSrcChn, &stDestChn);
        //--2--停止接收图像
        s32Ret=HI_MPI_VENC_StopRecvPic(VencChn);
        //--3--销毁VENC通道
        s32Ret=HI_MPI_VENC_DestroyChn(VencChn);

过程中涉及到的变量在初始化和配置部分都已经设置,所以不再重复设置。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如直接关闭MPP并退出)。

2.VPSS模块去初始化和退出

此处需要保持各种标号和之前设置的一致:

        VpssGrp=0;
        stSrcChn.enModId=HI_ID_VIU;
        stSrcChn.s32DevId=0;       //VI设备号
        stSrcChn.s32ChnId=0;       //VI通道号
        stDestChn.enModId=HI_ID_VPSS;
        stDestChn.s32DevId=VpssGrp;//VPSS组号
        stDestChn.s32ChnId=0;      //VPSS通道号
        //--1--解绑VI-VPSS通道
        s32Ret=HI_MPI_SYS_UnBind(&stSrcChn, &stDestChn);
        //--2--失能VPSS通道
        s32Ret=HI_MPI_VPSS_DisableChn(VpssGrp, VpssChn);
        //--3--停止VPSS组
        s32Ret=HI_MPI_VPSS_StopGrp(VpssGrp);
        //--4--销毁VPSS组
        s32Ret=HI_MPI_VPSS_DestroyGrp(VpssGrp);

过程中涉及到的变量在初始化和配置部分都已经设置,所以不再重复设置。
*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如直接关闭MPP并退出)。

3.VI模块去初始化和退出

        ViDev=0;                           //指定VI设备号
        ViChn=0;                           //指定VI通道号
        //--1--失能VI通道
        s32Ret=HI_MPI_VI_DisableChn(ViChn);
        //--2--失能VI设备
        s32Ret=HI_MPI_VI_DisableDev(ViDev);
        //--3--停止ISP
        if(gbIspInited)                    //如果ISP确实初始化了,执行反注册
        {
            HI_MPI_ISP_Exit(IspDev);       //退出MPI-ISP系统
            if(gs_IspPid)
            {
                pthread_join(gs_IspPid, 0);//等待ISP线程结束
                gs_IspPid=0;
            }
            gbIspInited=HI_FALSE;          //置标志位为ISP停止
            
            stLib.s32Id=0;
            strcpy(stLib.acLibName, HI_AF_LIB_NAME);
            s32Ret=HI_MPI_AF_UnRegister(IspDev, &stLib); //AF库反注册
            
            stLib.s32Id=0;
            strcpy(stLib.acLibName, HI_AWB_LIB_NAME);
            s32Ret=HI_MPI_AWB_UnRegister(IspDev, &stLib);//AWB库反注册
            
            stLib.s32Id=0;
            strcpy(stLib.acLibName, HI_AE_LIB_NAME);
            s32Ret=HI_MPI_AE_UnRegister(IspDev, &stLib); //AE库反注册
            
            s32Ret=sensor_unregister_callback();         //传感器反注册回调函数
        }

*如果上述任何函数返回值不为HI_SUCCESS,则应当给予处理(例如直接关闭MPP并退出)。

4.MPP系统退出

        HI_MPI_SYS_Exit();
        HI_MPI_VB_Exit();

至此,整套MPP业务便结束运行。从例程对于程序Ctrl+C/kill退出方式的处理来看,只要保证ISP和MPP去初始化完成,前面对VI/VPSS/VENC的去初始化似乎可以不执行。实际使用中暂时没发现这样做造成什么问题。

七、参考文件下载

百度网盘链接:https://pan.baidu.com/s/1MduVXp6L0P4_cPJ7dGwp7g
提取码: 1afl

八、阅读注意

1.由于我只需要一路的视频采集和编码,所以我VI/VPSS/VENC设备号、组号通道号等都是使用最开始设置的0,如果需要添加处理通道,则在绑定、解绑、读取等操作时要确保操作了正确的号码。
2.在本记录中为了简便起见,删除了所有对函数返回值s32Ret的判断,在实际程序中应该对相应执行失败的函数做适当处理。
3.本文仅为个人学习记录,如造成经济损失等后果作者不承担责任,如需转载,请注明原作者及出处。
————2020-1-1 @燕卫博————