6.7.5. 设计说明¶
6.7.5.1. 源码说明¶
源文件目录:
aic-mpp$ tree
.
├── base             // 公共模块:包括内存分配和链表等基础功能
│   ├── memory
├── ge              // 2D 图形加速模块
├── ve              // 编解码器模块
|   ├── include     // ve 模块头文件
│   ├── common      // 编解码器公共组件
|   ├── decoder
│        ├── h264   // h.264 解码模块
│        ├── jpeg   // jpg 解码模块
│        └── png    // png 解码模块
├── vin             // video input模块
├── include         // mpp 对外头文件
├── mpp_test        // mpp 测试用例
6.7.5.2. 软件架构¶
mpp 软件框图如下所示:
 
图 6.72 MPP 软件的系统框图¶
总体上,分为三层:
应用层:Luban-Lite提供了MPP player(播放器)、MPP测试、LVGL Demo,也支持增加其他App
MPP中间件:从功能上可以划分为4大块
MPP Decoder,实现h264、jpeg、png等解码功能
MPP Encoder,实现jpeg编码。
MPP GE,实现2D图形加速功能
MPP VIN,实现视频输入采集的功能
Driver层:MPP需要用到的驱动有VE、GE、DVP、Camera驱动
6.7.5.3. MPP Decoder 设计及接口说明¶
MPP Decoder 由三个主要模块组成:
- 解码模块(H264、JPEG、PNG等):负责将码流数据解码成视频图像 
- 输入码流数据管理模块(Packet manager):负责视频、图片码流数据和 buffer 的管理 
- 显示帧管理模块(Frame manager):负责解码图像 buffer 的管理 
6.7.5.3.1. packet 管理机制¶
Packet manager 负责管理码流数据和 buffer。初始化时,该模块申请一块物理连续的内存(buffer大小可由外部配置),用于存放视频/图片码流数据。
Packet manager 管理的数据单元为 packet,packet 表示一笔码流数据,它可以是完整的一帧数据,也支持不是完整的一帧数据。 每个 packet 与物理内存中的码流数据一一对应,它记录了每一笔码流的物理内存基地址、物理内存结束地址、物理内存偏移、虚拟内存地址、码流数据长度等信息。
 
图 6.73 packet管理¶
packet 通过 empty list 和 ready list 两个链表进行管理。 其中,empty list 用于存放空闲的 packet,ready list 用于存放待解码的 packet。
送码流数据时,从 empty list 获取一个空闲 packet,填充数据后,再把 packet 放入 ready list;
解码前,解码器从 ready list 获取一个填充数据的 packet,使用完后再把该 packet 放入 empty list。
 
图 6.74 packet manager 调用流程¶
6.7.5.3.2. frame 管理机制¶
Frame manager 负责管理图像 buffer。Frame manager 内部通过两个链表来管理图像 buffer:empty list 和 render list。 其中,empty list 存放可以给解码输出使用的图像 buffer,render list 存放解码完成但还未显示的图像 buffer。 在运行过程中,正在显示的图像 buffer 和用于参考的图像 buffer 可能不在这两个 list 中。
- frame 状态迁移 
初始化时,该模块申请指定个数的图像 buffer(个数可由外部配置),每个图像 buffer 的信息存放在内部数组中。 每个图像 buffer 有4种状态:
- Decoding: 该帧正在被解码器使用(用于解码输出或作为参考帧) 
- wait_render: 该帧在 render list 中,等待显示 
- Rendering: 该帧正在被显示占用 
- IDLE: 该帧处于空闲状态(既没有被显示占用,也没有被解码器用作参考帧) 
其状态转移如下图所示:
- 初始化时,所有图像 buffer 都在 empty list 中,此时处于 IDLE 状态; 
- 解码模块从 empty list 链表头部获取一个空图像 buffer,此时 buffer 被解码模块占用,从 IDLE 状态变为 Decoding 状态; 
- 解码完成后,解码模块还图像数据。此时分两种情况: - 1)如果当前帧还未被显示,该帧加入 render list 链表尾部,从 Decoding 状态变为 wait render 状态; 
- 2)该帧不再用做参考帧且已显示完成,此时该帧加入 empty list 链表尾部,由 Decoding 状态进入 IDLE 状态; 
 
- 显示模块从 render list 链表头部取一帧图像,此时当前帧由 wait render 状态进入 Rendering 状态; 
- 显示模块还图像 buffer,分两种情况: - 1)如果当前帧不用于参考,此时由 Rendering 状态回到IDLE状态,该帧加入 empty list 链表尾部; 
- 2)如果当前帧用于参考,此时由 Rendering 状态进入Decoding状态,该图像 buffer 不进入任何队列,等待解码器还参考帧; 
 
 
图 6.75 frame状态迁移¶
- frame manager 调用流程 
对于 JPEG、PNG 这类没有参考帧概念的编码格式,每一帧的状态是唯一的,解码后的数据帧可直接送 render list
 
图 6.76 frame manager 调用流程(JPEG/PNG)¶
但对于 H264 这类有参考帧的编码格式,解码后的视频帧可能既被显示占用也会被解码器用作参考帧,并且由于双向参考帧的存在, 视频帧需要重排序后才能送显示。 不同于JPG,H264 解码库内部存在一个 delay list 用于为显示帧重排序。
 
图 6.77 frame manager 调用流程(H264)¶
6.7.5.3.3. 物理连续内存使用情况¶
H264 解码所需的物理连续内存如下所示:
| 内存占用模块 | 计算方式 | 说明 | 
|---|---|---|
| 输入码流 | 大小由应用层配置 | |
| 输出帧 | width*height*3/2*frame_num | frame_num至少需要(参考帧个数+1)个 显示占用个数可由应用层通过struct decode_config 结构体中的extra_frame_num 配置 | 
| 帧内预测(需要上一行数据) | 帧格式:width*2 MBAFF:width*4 | |
| 宏块信息 | 固定12K | |
| dblk模块(上一个宏块行最后4行数据) | 帧格式:width*8 MBAFF:width*16 | |
| co-located信息 | 固定68K | |
| 每一帧co-located数据缓存 | (width/16)*(height/16)*32*frame_num | 
注解
co-located 两个buffer,I、P帧解码时会往buffer里写数据,B 帧解码时从buffer读数据。 如果当前码流中没有 B 帧,这两块内存也需要申请。
6.7.5.3.4. MPP Decoder 调用流程¶
在调用 MPP Decoder 的解码函数时,解码模块从 Packet manager 取一笔码流,同时从 Frame maneger 取一个空闲图像 buffer,对码流进行解码 并输出图像到图像 buffer。
解码后,解码模块将码流 buffer 归还 Packet manager,将解码图像 buffer 归还 Frame maneger。
为保证解码效率,建议调用者创建3个线程实现解码功能:
- send data thread
- 通过 mpp_decoder_get_packet 和 mpp_decoder_put_packet 这两个接口把码流数据送到 packet 管理模块 
 
- decode thread
- 通过调用 mpp_decoder_decode 控制解码,解码库从 packet 管理模块取一笔码流数据,解码完成后,将视频帧送入 frame 管理模块 
 
- render thread
- 通过 mpp_decoder_get_frame 和 mpp_decoder_put_frame 两个接口从 frame 管理模块获取视频帧,并控制该帧显示时机 
 
 
图 6.78 MPP Decoder 调用流程¶
6.7.5.3.5. MPP Decoder 数据结构¶
6.7.5.3.5.1. struct decode_config¶
struct decode_config {
    enum mpp_pixel_format pix_fmt;  // output pixel format
    int bitstream_buffer_size;      // bitstream buffer size in pm
    int packet_count;               // packet number in pm
    int extra_frame_num;            // extra frame number in fm
};
decode_config 结构体用于配置解码器初始化使用的参数。
- pix_fmt 表示解码输出的颜色格式 
- bitstream_buffer_size 表示存放输入码流缓存的总长度 
- packet_count 表示 packet manager 中 packet 的最大个数 
- extra_frame_num 表示解码器额外分配的帧个数,主要用于缓存显示帧以保证显示平滑。 
6.7.5.3.5.2. struct mpp_packet¶
struct mpp_packet {
    void *data;
    int size;
    long long pts;
    unsigned int flag;
};
mpp_packet 结构体用于表示输入码流信息。
- data 表示码流数据存放的起始地址 
- size 表示该笔码流数据长度 
- pts 表示该笔码流的时间戳 
- flag 表示该笔码流的标记位,目前仅用于确定该码流是否为最后一笔码流(PACKET_FLAG_EOS) 
6.7.5.3.5.3. struct mpp_frame¶
struct mpp_size {
    int width;
    int height;
};
struct mpp_rect {
    int x;
    int y;
    int width;
    int height;
};
enum mpp_buf_type {
    MPP_DMA_BUF_FD,
    MPP_PHY_ADDR,
};
struct mpp_buf {
    enum mpp_buf_type       buf_type;
    union {
            int             fd[3];
            unsigned int    phy_addr[3];
    };
    unsigned int            stride[3];
    struct mpp_size         size;
    unsigned int            crop_en;
    struct mpp_rect         crop;
    enum mpp_pixel_format   format;
    unsigned int            flags;
};
- buf_type:表示 mpp_buf 类型,以 fd 方式 MPP_DMA_BUF_FD 或 以物理地址方式 MPP_PHY_ADDR; 
- fd[3]:表示 buffer 三个分量的 fd 
- phy_addr[3]:表示 buffer 三个分量的物理地址 
- stride[3]:表示 buffer 三个分量的 stride 
- size:表示 buffer 的宽、高 
- crop_en: 表示该 buffer 是否需要 crop 
- crop:表示该 buffer 的 crop 信息 
- format: 表示该 buffer 的颜色格式类型 
struct mpp_frame {
    struct mpp_buf          buf;
    long long               pts;
    unsigned int            id;
    unsigned int            flags;
};
- buf:表示 mpp_frame 的 buffer 信息 
- pts:表示 mpp_frame 的时间戳 
- id:表示 mpp_frame 的唯一标识 
- flags:表示 mpp_frame 的标志位 
6.7.5.3.5.4. enum mpp_dec_errno¶
enum mpp_dec_errno {
    DEC_ERR_NOT_SUPPORT         = 0x90000001,
    DEC_ERR_NO_EMPTY_PACKET     = 0x90000002, // no packet in empty list
    DEC_ERR_NO_READY_PACKET     = 0x90000003, //
    DEC_ERR_NO_EMPTY_FRAME      = 0x90000004, //
    DEC_ERR_NO_RENDER_FRAME     = 0x90000005, //
    DEC_ERR_NULL_PTR            = 0x90000006,
    DEC_ERR_FM_NOT_CREATE       = 0x90000006,
};
- DEC_ERR_NOT_SUPPORT:该码流不支持 
- DEC_ERR_NO_EMPTY_PACKET:packet manager 中缺少空闲的 packet,可能是解码速度小于送 packet 速度,此时需要等待一段时间; 
- DEC_ERR_NO_READY_PACKET:packet manager 中缺少填好码流数据的 packet,可能是送 packet 速度小于解码速度,此时需要等待一段时间; 
- DEC_ERR_NO_EMPTY_FRAME:frame manager 中缺少空闲的 frame,表示所有帧都处于使用状态,通常是解码速度大于显示速度导致,此时需要等待一段时间; 
- DEC_ERR_NO_RENDER_FRAME:frame manager 中缺少待显示的 frame,表示所有帧都处于空闲状态,通常是解码速度小于显示速度导致,此时需要等待一段时间; 
- DEC_ERR_NULL_PTR:表示接口函数输入参数存在空指针 
- DEC_ERR_FM_NOT_CREATE:表示在获取待显示 frame 时 frame manager 还未创建 
6.7.5.3.5.5. enum mpp_codec_type¶
enum mpp_codec_type {
    MPP_CODEC_VIDEO_DECODER_H264 = 0x1000,         // decoder
    MPP_CODEC_VIDEO_DECODER_MJPEG,
    MPP_CODEC_VIDEO_DECODER_PNG,
    MPP_CODEC_VIDEO_ENCODER_H264 = 0x2000,         // encoder
};
mpp_codec_type 枚举类型表示支持的编解码格式。
6.7.5.3.5.6. enum mpp_dec_cmd¶
enum mpp_dec_cmd {
    MPP_DEC_INIT_CMD_SET_EXT_FRAME_ALLOCATOR,            // frame buffer allocator
    MPP_DEC_INIT_CMD_SET_ROT_FLIP_FLAG,
    MPP_DEC_INIT_CMD_SET_SCALE,
    MPP_DEC_INIT_CMD_SET_CROP_INFO,
    MPP_DEC_INIT_CMD_SET_OUTPUT_POS,
};
- MPP_DEC_INIT_CMD_SET_EXT_FRAME_ALLOCATOR:表示由外部设置帧 buffer 分配器 
- MPP_DEC_INIT_CMD_SET_ROT_FLIP_FLAG: 表示设置旋转、镜像后处理,只用于JPEG 
- MPP_DEC_INIT_CMD_SET_SCALE: 表示设置缩放系数,只用于JPEG 
- MPP_DEC_INIT_CMD_SET_CROP_INFO:表示设置输出 crop 信息 
- MPP_DEC_INIT_CMD_SET_OUTPUT_POS:表示设置解码图像在输出缓存的位置 
6.7.5.3.6. MPP Decoder 接口设计¶
接口如下 :
struct decode_config {
    enum mpp_pixel_format pix_fmt;  // output pixel format
    int bitstream_buffer_size;      // bitstream buffer size in pm
    int packet_count;               // packet number in pm
    int extra_frame_num;            // extra frame number in fm
};
struct mpp_decoder* create_mpp_decoder(enum mpp_codec_type type);
void destory_mpp_decoder(struct mpp_decoder* decoder);
int mpp_decoder_init(struct mpp_decoder *decoder, struct decode_config *config);
int mpp_decoder_decode(struct mpp_decoder* decoder);
int mpp_decoder_control(struct mpp_decoder* decoder, int cmd, void *param);
int mpp_decoder_reset(struct mpp_decoder* decoder);
int mpp_decoder_get_packet(struct mpp_decoder* decoder, struct mpp_packet* packet, int size);
int mpp_decoder_put_packet(struct mpp_decoder* decoder, struct mpp_packet* packet);
int mpp_decoder_get_frame(struct mpp_decoder* decoder, struct mpp_frame* frame);
int mpp_decoder_put_frame(struct mpp_decoder* decoder, struct mpp_frame* frame);
6.7.5.3.6.1. mpp_decoder_create¶
| 函数原型 | struct mpp_decoder* mpp_decoder_create(enum mpp_codec_type type) | 
|---|---|
| 功能说明 | 创建mpp_decoder对象 | 
| 参数定义 | type: 解码器类型 | 
| 返回值 | mpp_decoder对象 | 
| 注意事项 | 
6.7.5.3.6.2. mpp_decoder_destory¶
| 函数原型 | void mpp_decoder_destory(struct mpp_decoder* decoder) | 
|---|---|
| 功能说明 | 销毁mpp_decoder对象 | 
| 参数定义 | decoder: mpp_decoder对象 | 
| 返回值 | 无 | 
| 注意事项 | 
6.7.5.3.6.3. mpp_decoder_init¶
| 函数原型 | int mpp_decoder_init(struct mpp_decoder *decoder, struct decode_config *config) | 
|---|---|
| 功能说明 | 初始化解码器 | 
| 参数定义 | decoder: mpp_decoder对象 config:解码器的配置参数 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.4. mpp_decoder_decode¶
| 函数原型 | int mpp_decoder_decode(struct mpp_decoder* decoder) | 
|---|---|
| 功能说明 | 解码一笔数据 | 
| 参数定义 | decoder: mpp_decoder对象 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.5. mpp_decoder_control¶
| 函数原型 | int mpp_decoder_control(struct mpp_decoder* decoder, int cmd, void* param) | 
|---|---|
| 功能说明 | 向mpp_decoder对象发送控制命令 | 
| 参数定义 | decoder: mpp_decoder对象 cmd: 控制命令类型 param: 控制参数 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.6. mpp_decoder_reset¶
| 函数原型 | int mpp_decoder_reset(struct mpp_decoder* decoder) | 
|---|---|
| 功能说明 | 重置mpp_decoder对象 | 
| 参数定义 | decoder: mpp_decoder对象 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.7. mpp_decoder_get_packet¶
| 函数原型 | int mpp_decoder_get_packet(struct mpp_decoder* decoder, struct mpp_packet* packet, int size) | 
|---|---|
| 功能说明 | 获取一个写码流数据的packet | 
| 参数定义 | decoder: mpp_decoder对象 packet:码流数据结构指针 size:上层应用申请packet的buffer大小 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.8. mpp_decoder_put_packet¶
| 函数原型 | int mpp_decoder_put_packet(struct mpp_decoder* decoder, struct mpp_packet* packet) | 
|---|---|
| 功能说明 | 归还码流数据的packet对象 | 
| 参数定义 | decoder: mpp_decoder对象 packet:码流数据结构指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.9. mpp_decoder_get_frame¶
| 函数原型 | int mpp_decoder_get_frame(struct mpp_decoder* decoder, struct mpp_frame* frame) | 
|---|---|
| 功能说明 | 获取一个视频帧对象 | 
| 参数定义 | decoder: mpp_decoder对象 frame:视频帧数据结构指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.6.10. mpp_decoder_put_frame¶
| 函数原型 | int mpp_decoder_put_frame(struct mpp_decoder* decoder, struct mpp_frame* frame) | 
|---|---|
| 功能说明 | 归还视频帧对象 | 
| 参数定义 | decoder: mpp_decoder对象 frame:视频帧数据结构指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | 
6.7.5.3.7. MPP Decoder 参考Demo¶
以下 Demo 为基本流程调用,具体实现可以参考代码 mpp/mpp_test/picture_decoder_test.c
//* 1.创建 mpp_decoder 对象
struct mpp_decoder* dec = mpp_decoder_create(type);
struct decode_config config;
config.bitstream_buffer_size = (file_len + 1023) & (~1023);
config.extra_frame_num = 0;
config.packet_count = 1;
config.pix_fmt = MPP_FMT_ARGB_8888;
//* 2. 初始化 mpp_decoder
mpp_decoder_init(dec, &config);
//* 3. 获取一个空的packet
struct mpp_packet packet;
memset(&packet, 0, sizeof(struct mpp_packet));
mpp_decoder_get_packet(dec, &packet, file_len);
//* 4. 把视频码流数据拷贝到 packet
fread(packet.data, 1, file_len, fp);
packet.size = file_len;
packet.flag = PACKET_FLAG_EOS;
//* 5. 归还 packet
mpp_decoder_put_packet(dec, &packet);
//* 6. 解码该笔码流数据
mpp_decoder_decode(dec);
//* 7. 获取解码后视频帧数据
struct mpp_frame frame;
memset(&frame, 0, sizeof(struct mpp_frame));
mpp_decoder_get_frame(dec, &frame);
//* 8. 显示该视频帧
// render_frame...
//* 9. 归还该视频帧
mpp_decoder_put_frame(dec, &frame);
//* 10. 销毁 mpp_decoder
mpp_decoder_destory(dec);
6.7.5.4. MPP Encoder 设计及接口说明¶
MPP Encoder 目前只支持 JPEG 图片编码。
6.7.5.4.1. 接口设计¶
6.7.5.4.1.1. mpp_encode_jpeg¶
| 函数原型 | int mpp_encode_jpeg(struct mpp_frame* frame, int quality, int dma_buf_fd, int buf_len, int* len) | 
|---|---|
| 功能说明 | 编码一帧 JPEG 图片 | 
| 参数定义 | frame: 待编码的原始 YUV 数据 quality: 编码质量,取值范围1~100,1表示编码图片质量最差,100表示最好 dma_buf_fd:输出 JPEG 图片存放的 dma-buf fd buf_len:输出 JPEG 图片 dma-buf 的长度 len: 输出 JPEG 图片的真实大小 | 
| 返回值 | 0: 成功 <0:失败 | 
| 注意事项 | 
小技巧
输出 JPEG 图片的缓存 buffer 由调用者申请,但调用者并不知道编码后图片的实际大小, 为避免 VE 写输出数据时越界,该 buffer 需要预先申请较大的内存。
6.7.5.4.2. MPP Encoder 参考Demo¶
以下 Demo 为基本流程调用,具体实现可以参考代码 mpp/mpp_test/jpeg_encoder_test.c
//* 1. 获取 dma-buf device 句柄
int dma_fd = dmabuf_device_open();
//* 2. 设置输入 YUV 数据结构体
struct mpp_frame frame;
// ....
//* 3. 申请编码输出 buffer
int len = 0;
int buf_len = width * height * 4/5 * quality / 100;
int jpeg_data_fd = dmabuf_alloc(dma_fd, buf_len);
//* 4. 编码 JPEG 图片
mpp_encode_jpeg(&frame, quality, jpeg_data_fd, buf_len, &len);
//* 5. 保存编码后 JPEG 图片
unsigned char* jpeg_vir_addr = dmabuf_mmap(jpeg_data_fd, buf_len);
FILE* fp_save = fopen("/save.jpg", "wb");
fwrite(jpeg_vir_addr, 1, len, fp_save);
fclose(fp_save);
//* 6. 释放资源
dmabuf_munmap(jpeg_vir_addr, buf_len);
dmabuf_free(jpeg_data_fd);
dmabuf_device_close(dma_fd);
6.7.5.5. MPP zlib 设计及接口说明¶
MPP zlib 的功能:利用硬件加速解压 zlib 文件。
6.7.5.5.1. 接口设计¶
6.7.5.5.1.1. mpp_zlib_uncompressed¶
| 函数原型 | int mpp_zlib_uncompressed(unsigned char *compressed_data,unsigned int compressed_len, unsigned char *uncompressed_data,unsigned int uncompressed_len); | 
|---|---|
| 功能说明 | 解压zlib文件 | 
| 参数定义 | compressed_data - 压缩数据起始地址 compressed_len - 压缩数据长度 uncompressed_data - 存放解压数据 buffer 的起始地址 uncompressed_len - 存放解压数据 buffer 的长度 | 
| 返回值 | >0,解压数据实际长度; <0,失败 | 
| 注意事项 | compressed_data - 16 byte 对齐,填充数据后,需要刷 cache compressed_len - 实际有效数据长度 uncompressed_data - 8 byte 对齐,获取数据前,先清掉 cache uncompressed_len - 8 byte 对齐,必须大于等于解压数据实际长度 | 
6.7.5.5.2. 参考Demo¶
#include "dfs.h"
#include "unistd.h"
#include "mpp_zlib.h"
#include "mpp_mem.h"
#include "mpp_log.h"
#include <console.h>
#include "aic_core.h"
static void print_help(char *program)
{
    printf("Compile time: %s\n", __TIME__);
    printf("usage:%s input_file out_file out_put_buffer_len \n", program);
    printf("note:out_put_buffer_len >= out_file_size \n");
    printf("exsample:%s readme.zlib  readme.txt 204800\n",program);
}
int zlib_test(int argc,char **argv)
{
    int ret = 0;
    int fd_in = 0;
    int fd_out = 0;
    int file_len;
    int in_len_align;
    int out_len;
    int out_len_align;
    int r_len=0,w_len=0;
    int uncompress_len;
    unsigned long in_buff = 0;;
    unsigned long in_buff_align;
    unsigned long out_buff = 0;
    unsigned long out_buff_align;
    int align;
    unsigned int before;
    unsigned int after;
    if (argc != 4) {
        print_help(argv[0]);
    return -1;
    }
    fd_in = open(argv[1], O_RDONLY);
    if (fd_in < 0) {
        loge("open %s fail\n",argv[1]);
        return -1;
    }
    file_len = lseek(fd_in, 0, SEEK_END);
    lseek(fd_in, 0, SEEK_SET);
    #define INPUT_BUFFER_ALIGN 16
    if (CACHE_LINE_SIZE > INPUT_BUFFER_ALIGN) {
        align = CACHE_LINE_SIZE;
    } else {
        align = INPUT_BUFFER_ALIGN;
    }
    //input buffer len align max of {CACHE_LINE_SIZE,INPUT_BUFFER_ALIGN}
    in_len_align = (file_len+align-1)/align*align;
    in_buff = (unsigned long)aicos_malloc(MEM_CMA, in_len_align+align-1);
    if (in_buff == 0) {
        loge("mpp_alloc fail\n");
        ret = -1;
        goto _exit;
    }
    //input buffer addr align max of {CACHE_LINE_SIZE,INPUT_BUFFER_ALIGN}
    in_buff_align = ((in_buff+align-1)&(~(align-1)));
    r_len = read(fd_in,(void *)in_buff_align, file_len);
    logd("r_len:%d,in_len:%d\n",r_len,file_len);
    //flush cache*
    aicos_dcache_clean_range((unsigned long *)in_buff_align, (int64_t)in_len_align);
    out_len = atoi(argv[3]);
    if (out_len < file_len) {
        loge("param error :%d\n",out_len);
        ret = -1;
        goto _exit;
    }
    //  out buffer len align  CACHE_LINE_SIZE
    out_len_align = (out_len + CACHE_LINE_SIZE -1)/CACHE_LINE_SIZE*CACHE_LINE_SIZE;
    out_buff = (unsigned long)aicos_malloc(MEM_CMA, out_len_align+(CACHE_LINE_SIZE -1));
    if (out_buff == 0) {
        loge("mpp_alloc fail\n");
        ret = -1;
        goto _exit;
    }
    //out buffer addr align CACHE_LINE_SIZE
    out_buff_align = ((out_buff+CACHE_LINE_SIZE -1)&(~(CACHE_LINE_SIZE-1)));
    // uncompressed
    before = aic_get_time_us();
    uncompress_len =  mpp_zlib_uncompressed((unsigned char*)in_buff_align, file_len, (unsigned char*)out_buff_align, out_len_align);
    after =  aic_get_time_us();
    logd("diff:%u\n",after-before);
    if (uncompress_len < 0) {
        loge("mpp_zlib_uncompressed fail\n");
        ret = -1;
        goto _exit;
    }
    //save uncompressed data
    fd_out = open(argv[2], O_RDWR|O_CREAT);
    if (fd_out < 0) {
        loge("open %s fail\n",argv[2]);
        ret = -1;
        goto _exit;
    }
    // invalid cache
    aicos_dcache_invalid_range((unsigned long *)out_buff_align, (int64_t)out_len_align);
    w_len = write(fd_out,(void *)out_buff_align, uncompress_len);
    logd("w_len:%d,uncompress_len:%d\n", w_len,uncompress_len);
    close(fd_out);
_exit:
    if(out_buff)
        aicos_free(MEM_CMA, (void *)out_buff);
    if(in_buff)
        aicos_free(MEM_CMA, (void *)in_buff);
    if(fd_in)
        close(fd_in);
    return ret;
}
6.7.5.6. MPP GE 设计及接口说明¶
由于驱动支持非命令队列和命令队列两种模式,在提供的用户MPP接口中,对调用驱动的接口进行了封装,保持了统一的调用API, 建议用户统一使用MPP中间层API。在命令队列模式下,task会先缓存在用户的cmd buffer中,当调用mpp_ge_emit后, 会通过write接口把命令写入内核的ring buffer。
 
图 6.79 应用调用MPP框架¶
6.7.5.6.1. mpp_ge_open¶
struct mpp_ge *mpp_ge_open();
| 功能说明 | 打开ge设备 | 
|---|---|
| 参数定义 | 无 | 
| 返回值 | struct mpp_ge 结构体指针 NULL:失败 | 
| 注意事项 | 无 | 
6.7.5.6.2. mpp_ge_close¶
void mpp_ge_close(struct mpp_ge *ge);
| 功能说明 | 关闭ge设备 | 
|---|---|
| 参数定义 | ge: struct mpp_ge 结构体指针 | 
| 返回值 | 无 | 
| 注意事项 | 无 | 
6.7.5.6.3. mpp_ge_get_mode¶
enum ge_mode mpp_ge_get_mode(struct mpp_ge *ge);
| 功能说明 | 获取GE模式 | 
|---|---|
| 参数定义 | ge: struct mpp_ge 结构体指针 | 
| 返回值 | enum ge_mode枚举类型 通过返回值可以获取GE是否工作在命令队列 模式 | 
| 注意事项 | 无 | 
6.7.5.6.4. mpp_ge_fillrect¶
int mpp_ge_fillrect(struct mpp_ge *ge, struct ge_fillrect *fillrect);
| 功能说明 | 矩形填充 | 
|---|---|
| 参数定义 | ge: struct mpp_ge结构体指针 fillrect:struct ge_fillrect结构体指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | normal(非命令队列)模式此接口是同步 的。 命令队列模式此接口是异步的: (1)当用户的缓存buffer足够时候仅把 命令缓存在用户 (2)当用户的缓存空间不够的时候,先 通过write接口,把缓存的命令全部 写入驱动,然后再把当前命令缓存到 用户buffer | 
矩形填充在目标图像中指定一块矩形区域,填充颜色格式只能为ARGB8888格式, 在进行固定颜色填充的时候,不支持scaler,不支持90/180/270度旋转,不支持镜像, 填充的颜色可以和目标层进行alpha blending和color key。
 
图 6.80 矩形填充¶
6.7.5.6.5. mpp_ge_bitblt¶
int mpp_ge_bitblt(struct mpp_ge *ge, struct ge_bitblt *blt);
| 功能说明 | 位块搬移 | 
|---|---|
| 参数定义 | ge: struct mpp_ge结构体指针 blt:struct ge_bitblt结构体指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | normal(非命令队列)模式此接口是同步 的。 命令队列模式此接口是异步的: (1)当用户的缓存buffer足够时候仅把 命令缓存在用户 (2)当用户的缓存空间不够的时候,先 通过write接口,把缓存的命令全部 写入驱动,然后再把当前命令缓存到 用户buffer | 
位块搬移可以分两种情况:
- 原图的矩形区域搬移到目标图的矩形区域中不进行缩放 
 
图 6.81 不进行缩放¶
- 原图的矩形区域搬移到目标图的矩形区域中同时进行放大或者缩小 
 
图 6.82 进行缩放¶
在进行位块搬移的同时可以进行alpha blending和color key,同时也支持90/180/270度旋转和镜像。
6.7.5.6.6. mpp_ge_rotate¶
int mpp_ge_rotate(struct mpp_ge *ge, struct ge_rotation *rot);
| 功能说明 | 任意角度旋转 | 
|---|---|
| 参数定义 | ge: struct mpp_ge结构体指针 rot:struct ge_rotation结构体指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | normal(非命令队列)模式此接口是同步 的。 命令队列模式此接口是异步的: (1)当用户的缓存buffer足够时候仅把 命令缓存在用户 (2)当用户的缓存空间不够的时候,先 通过write接口,把缓存的命令全部 写入驱动,然后再把当前命令缓存到 用户buffer | 
进行任意角度旋转的时候可以进行alpha blending,并且可以指定原图和目标图的旋转中心,任意角度旋转原图和目标图都只支持RGB格式。 其中旋转角度传给驱动的是旋转角度的sin和cos值,为2.12定点数,其中小数部分12bits ,则应用程序计算sin和cos值的方法如下:
 
#include <stdio.h>
#include <math.h>
#define PI 3.14159265
#define SIN(x) (sin(x* PI / 180.0))
#define COS(x) (cos(x* PI / 180.0))
double degree = 30.0  // (0 <= degree < 360)
angle_sin = (int)(SIN(x) * 4096);
angle_cos = (int)(COS(x) * 4096);
// 应用程序也可以预先生成需要的角度的sin和cos值列表,通过查表减小计算量
6.7.5.6.7. mpp_ge_emit¶
iint mpp_ge_emit(struct mpp_ge *ge);
| 功能说明 | 向驱动发送命令 | 
|---|---|
| 参数定义 | ge: struct mpp_ge结构体指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | normal(非命令队列)模式此接口为空, 不产生任何作用 命令队列模式此接口通过write接口, 把用户buffer中缓存的命令都写入驱动 | 
6.7.5.6.8. mpp_ge_sync¶
iint mpp_ge_sync(struct mpp_ge *ge);
| 功能说明 | 阻塞等待所有任务执行完成 | 
|---|---|
| 参数定义 | ge: struct mpp_ge结构体指针 | 
| 返回值 | 0:成功 <0:失败 | 
| 注意事项 | normal(非命令队列)模式此接口为空, 不产生任何作用 命令队列模式此接口通过调用IOC_GE_SYNC 接口,等待所有任务都完成 | 
6.7.5.7. MPP VIN 设计及接口说明¶
MPP VIN模块主要实现两个功能:
- 对上封装了DVP、Camera驱动的ioctl接口(尽量做到和Linux MPP VIN保持一致) 
- 对视频Buf队列的管理,实现了DVP应用、DVP驱动之间的Buf轮转管理 
 
图 6.83 MPP VIN模块的软件框架¶
6.7.5.7.1. Buf队列管理¶
VIN模块中的队列管理,参考了Linux的V4L2框架,通过 struct vb_queue 结构中的两个Buf队列来管理,DVP驱动中还需要维护一个buf_list来配合DVP控制器的地址更新。
整个Buf流转的过程如下图:
 
图 6.84 MPP VIN中的Buf队列管理¶
- queued_list:是一些空闲Buf,等待Sensor的数据到来后,DVP驱动会从这个队列中找可用Buf来保存下一帧数据。 
- done_list:是一些填了视频数据的Buf,等待用户来处理这些数据,一般用户处理完后需要将Buf还给驱动,也就是通过Q_BUF命令还给queued_list。 
- 从图中的流转过程看,运行期间,在某一时刻,DVP需要使用一个Buf,APP需要使用一个Buf,QBuf需要有一个Buf在等待(否则DVP的done中断来了后发现没有等待的Qbuf会发生丢帧),一共 至少要有3个Buf。 
- 以YUV422格式计算,有两个plane,在V4L2框架中这一组plane算一个Buf,3个Buf就需要申请6个plane, - 总大小 = 长 * 宽 * 2 * 3。
对于每一帧图像数据来说,DVP的输出有两个plane:Y和UV。针对DVP的两种输出格式:YUV422_COMBINED_NV16和YUV420_COMBINED_NV12,两个plane的空间大小如下表:
| YUV422_COMBINED_NV16 | YUV420_COMBINED_NV12 | |
|---|---|---|
| Plane Y | Width * height | Width * height | 
| Plane UV | Width * height | Width * height / 2 | 
根据前面对“Buf队列管理”的分析可知:我们要分配的内存空间 至少要有3个Buf,每个Buf包含两个Plane。
6.7.5.7.2. 接口设计¶
6.7.5.7.2.1. mpp_vin_init¶
| 函数原型 | int mpp_vin_init(char *camera) | 
|---|---|
| 功能说明 | 初始化VIN模块,包括打开Camera设备、DVP设备、为Video Buf分配内存池 | 
| 参数定义 | camera - 摄像头的设备名称 | 
| 返回值 | 0,成功;<0,失败 | 
| 注意事项 | 内存池默认配置8MB,需要根据实际的应用场景来修改 | 
6.7.5.7.2.2. mpp_vin_deinit¶
| 函数原型 | int mpp_vin_deinit(void) | 
|---|---|
| 功能说明 | 释放VIN模块的资源,包括关闭Camera设备和DVP设备,释放Video Buf内存池 | 
| 参数定义 | 无 | 
| 返回值 | 无 | 
| 注意事项 | 
6.7.5.7.2.3. mpp_dvp_ioctl¶
| 函数原型 | int mpp_dvp_ioctl(int cmd, void *arg) | 
|---|---|
| 功能说明 | DVP 设备的ioctl接口 | 
| 参数定义 | cmd - ioctl 命令码 arg - ioctl 命令相应的参数 | 
| 返回值 | 0,成功;<0,失败 | 
| 注意事项 | 
6.7.5.7.3. 参考Demo¶
命令行工具test_dvp实现了一个完整的 Sensor -> DVP -> DE 数据通路,从Camera采集数据、然后会显到DE 的Video layer。 整体的处理流程如下图(图中按照访问对象分为三列,实际上整体是串行执行):
 
图 6.85 MPP VIN Demo 中的处理流程¶
此Demo的代码实现可作为MPP VIN的APP设计参考:(详见test_dvp.c)
#define VID_BUF_NUM             3
#define VID_BUF_PLANE_NUM       2
#define VID_SCALE_OFFSET        20
static const char sopts[] = "f:c:h";
static const struct option lopts[] = {
    {"format",        required_argument, NULL, 'f'},
    {"capture",       required_argument, NULL, 'c'},
    {"usage",               no_argument, NULL, 'h'},
    {0, 0, 0, 0}
};
struct aic_dvp_data {
    int w;
    int h;
    int frame_size;
    int frame_cnt;
    int dst_fmt;  // output format
    struct mpp_video_fmt src_fmt;
    uint32_t num_buffers;
    struct vin_video_buf binfo;
};
static struct aic_dvp_data g_vdata = {0};
#ifdef AIC_DISPLAY_DRV
static struct mpp_fb *g_fb = NULL;
static struct aicfb_screeninfo g_fb_info = {0};
#endif
/* Functions */
static void usage(char *program)
{
    printf("Usage: %s [options]: \n", program);
    printf("\t -f, --format\t\tformat of input video, NV16/NV12 etc\n");
    printf("\t -c, --count\t\tthe number of capture frame \n");
    printf("\t -u, --usage \n");
    printf("\n");
    printf("Example: %s -f nv16 -c 1\n", program);
}
static long long int str2int(char *_str)
{
    if (_str == NULL) {
        pr_err("The string is empty!\n");
        return -1;
    }
    if (strncmp(_str, "0x", 2))
        return atoi(_str);
    else
        return strtoll(_str, NULL, 16);
}
int get_fb_info(void)
{
    int ret = 0;
#ifdef AIC_DISPLAY_DRV
    ret = mpp_fb_ioctl(g_fb, AICFB_GET_SCREENINFO, &g_fb_info);
    if (ret < 0)
        pr_err("ioctl() failed! errno: -%d\n", -ret);
#endif
    pr_info("Screen width: %d, height %d\n",
            g_fb_info.width, g_fb_info.height);
    return ret;
}
int set_ui_layer_alpha(int val)
{
    int ret = 0;
#ifdef AIC_DISPLAY_DRV
    struct aicfb_alpha_config alpha = {0};
    alpha.layer_id = AICFB_LAYER_TYPE_UI;
    alpha.enable = 1;
    alpha.mode = 1;
    alpha.value = val;
    ret = mpp_fb_ioctl(g_fb, AICFB_UPDATE_ALPHA_CONFIG, &alpha);
    if (ret < 0)
        pr_err("ioctl() failed! errno: -%d\n", -ret);
#endif
    return ret;
}
int sensor_get_fmt(void)
{
    int ret = 0;
    struct mpp_video_fmt f = {0};
    ret = mpp_dvp_ioctl(DVP_IN_G_FMT, &f);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        // return -1;
    }
    g_vdata.src_fmt = f;
    g_vdata.w = g_vdata.src_fmt.width;
    g_vdata.h = g_vdata.src_fmt.height;
    pr_info("Sensor format: w %d h %d, code 0x%x, bus 0x%x, colorspace 0x%x\n",
            f.width, f.height, f.code, f.bus_type, f.colorspace);
    return 0;
}
int dvp_subdev_set_fmt(void)
{
    int ret = 0;
    ret = mpp_dvp_ioctl(DVP_IN_S_FMT, &g_vdata.src_fmt);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        return -1;
    }
    return 0;
}
int dvp_cfg(int width, int height, int format)
{
    int ret = 0;
    struct dvp_out_fmt f = {0};
    f.width = g_vdata.src_fmt.width;
    f.height = g_vdata.src_fmt.height;
    f.pixelformat = format;
    f.num_planes = VID_BUF_PLANE_NUM;
    ret = mpp_dvp_ioctl(DVP_OUT_S_FMT, &f);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        return -1;
    }
    return 0;
}
int dvp_request_buf(struct vin_video_buf *vbuf)
{
    int i;
    if (mpp_dvp_ioctl(DVP_REQ_BUF, (void *)vbuf) < 0) {
        pr_err("ioctl() failed!\n");
        return -1;
    }
    pr_info("Buf   Plane[0]     size   Plane[1]     size\n");
    for (i = 0; i < vbuf->num_buffers; i++) {
        pr_info("%3d 0x%x %8d 0x%x %8d\n", i,
            vbuf->planes[i * vbuf->num_planes].buf,
            vbuf->planes[i * vbuf->num_planes].len,
            vbuf->planes[i * vbuf->num_planes + 1].buf,
            vbuf->planes[i * vbuf->num_planes + 1].len);
    }
    return 0;
}
void dvp_release_buf(int num)
{
#if 0
    int i;
    struct video_buf_info *binfo = NULL;
    for (i = 0; i < num; i++) {
        binfo = &g_vdata.binfo[i];
        if (binfo->vaddr) {
            munmap(binfo->vaddr, binfo->len);
            binfo->vaddr = NULL;
        }
    }
#endif
}
int dvp_queue_buf(int index)
{
    if (mpp_dvp_ioctl(DVP_Q_BUF, (void *)(ptr_t)index) < 0) {
        pr_err("ioctl() failed!\n");
        return -1;
    }
    return 0;
}
int dvp_dequeue_buf(int *index)
{
    int ret = 0;
    ret = mpp_dvp_ioctl(DVP_DQ_BUF, (void *)index);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        return -1;
    }
    return 0;
}
int dvp_start(void)
{
    int ret = 0;
    ret = mpp_dvp_ioctl(DVP_STREAM_ON, NULL);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        return -1;
    }
    return 0;
}
int dvp_stop(void)
{
    int ret = 0;
    ret = mpp_dvp_ioctl(DVP_STREAM_OFF, NULL);
    if (ret < 0) {
        pr_err("ioctl() failed! err -%d\n", -ret);
        return -1;
    }
    return 0;
}
#define DVP_SCALE       0
int video_layer_set(struct aic_dvp_data *vdata, int index)
{
#ifdef AIC_DISPLAY_DRV
    int i;
    struct aicfb_layer_data layer = {0};
    struct vin_video_buf *binfo = &vdata->binfo;
    layer.layer_id = AICFB_LAYER_TYPE_VIDEO;
    layer.enable = 1;
#if DVP_SCALE
#if 0
    layer.scale_size.width = g_fb_info.width - VID_SCALE_OFFSET * 2;
    layer.scale_size.height = g_fb_info.height - VID_SCALE_OFFSET * 2;
    layer.pos.x = VID_SCALE_OFFSET;
    layer.pos.y = VID_SCALE_OFFSET;
#else
    /* Reduce the size to fb0*1/2 */
    layer.scale_size.width = g_fb_info.width / 2;
    layer.scale_size.height = g_fb_info.height / 2;
    layer.pos.x = g_fb_info.width / 2 - VID_SCALE_OFFSET;
    layer.pos.y = g_fb_info.height / 2 - VID_SCALE_OFFSET;
#endif
#else
    layer.scale_size.width = vdata->w;
    layer.scale_size.height = vdata->h;
    layer.pos.x = g_fb_info.width - vdata->w;
    layer.pos.y = 0;
#endif
    layer.buf.size.width = vdata->w;
    layer.buf.size.height = vdata->h;
    if (vdata->dst_fmt == MPP_FMT_NV16)
        layer.buf.format = MPP_FMT_NV16;
    else
        layer.buf.format = MPP_FMT_NV12;
    layer.buf.buf_type = MPP_PHY_ADDR;
    for (i = 0; i < VID_BUF_PLANE_NUM; i++) {
        layer.buf.stride[i] = vdata->w;
        layer.buf.phy_addr[i] = binfo->planes[index * VID_BUF_PLANE_NUM + i].buf;
    }
    if (mpp_fb_ioctl(g_fb, AICFB_UPDATE_LAYER_CONFIG, &layer) < 0) {
        pr_err("ioctl() failed!\n");
        return -1;
    }
#endif
    return 0;
}
#define NS_PER_SEC      1000000000
static void show_fps(struct timespec *start, struct timespec *end, int cnt)
{
     double diff;
    if (end->tv_nsec < start->tv_nsec) {
        diff = (double)(NS_PER_SEC + end->tv_nsec - start->tv_nsec)/NS_PER_SEC;
        diff += end->tv_sec - 1 - start->tv_sec;
    } else {
        diff = (double)(end->tv_nsec - start->tv_nsec)/NS_PER_SEC;
        diff += end->tv_sec - start->tv_sec;
    }
    printf("\nDVP frame rate: %d.%d, frame %d / %d.%d seconds\n",
           (u32)(cnt / diff), (u32)(cnt * 10 / diff) % 10, cnt,
           (u32)diff, (u32)(diff * 10) % 10);
}
static void test_dvp_thread(void *arg)
{
    int i, index = 0;
    struct timespec begin, now;
    if (dvp_request_buf(&g_vdata.binfo) < 0)
        return;
    for (i = 0; i < g_vdata.binfo.num_buffers; i++) {
        if (dvp_queue_buf(i) < 0)
            return;
    }
    if (dvp_start() < 0)
        return;
#if DVP_SCALE
    pr_info("DVP scale is enable\n");
#else
    pr_info("DVP scale is disable\n");
#endif
    clock_gettime(CLOCK_REALTIME, &begin);
    for (i = 0; i < g_vdata.frame_cnt; i++ ) {
        if (dvp_dequeue_buf(&index) < 0)
            break;
        // pr_debug("Set the buf %d to video layer\n", index);
        if (video_layer_set(&g_vdata, index) < 0)
            break;
        dvp_queue_buf(index);
        if (i && (i % 1000 == 0)) {
            clock_gettime(CLOCK_REALTIME, &now);
            show_fps(&begin, &now, i);
        }
    }
    if ((i - 1) % 1000 != 0) {
        clock_gettime(CLOCK_REALTIME, &now);
        show_fps(&begin, &now, i);
    }
    dvp_stop();
    dvp_release_buf(g_vdata.binfo.num_buffers);
    mpp_vin_deinit();
    if (g_fb)
        mpp_fb_close(g_fb);
}
static void cmd_test_dvp(int argc, char **argv)
{
    int c;
    aicos_thread_t thid = NULL;
    memset(&g_vdata, 0, sizeof(struct aic_dvp_data));
    g_vdata.dst_fmt = MPP_FMT_NV16;
    g_vdata.frame_cnt = 1;
    optind = 0;
    while ((c = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) {
        switch (c) {
        case 'f':
            if (strncasecmp("nv12", optarg, strlen(optarg)) == 0)
                g_vdata.dst_fmt = DVP_OUT_FMT_NV12;
            continue;
        case 'c':
            g_vdata.frame_cnt = str2int(optarg);
            continue;
        case 'h':
            usage(argv[0]);
            return;
        default:
            break;
        }
    }
    pr_info("Capture %d frames from camera\n", g_vdata.frame_cnt);
    pr_info("DVP out format: %s\n",
            g_vdata.dst_fmt == MPP_FMT_NV16 ? "NV16" : "NV12");
    if (mpp_vin_init(CAMERA_NAME_OV))
        return;
    if (sensor_get_fmt() < 0)
        goto error_out;
    if (dvp_subdev_set_fmt() < 0)
        goto error_out;
    if (g_vdata.dst_fmt == MPP_FMT_NV16)
        g_vdata.frame_size = g_vdata.w * g_vdata.h * 2;
    else if (g_vdata.dst_fmt == DVP_OUT_FMT_NV12)
        g_vdata.frame_size = (g_vdata.w * g_vdata.h * 3) >> 1;
    g_fb = mpp_fb_open();
    if (!g_fb) {
        pr_err("Failed to open FB\n");
        goto error_out;
    }
    if (get_fb_info() < 0)
        goto error_out;
    if (set_ui_layer_alpha(15) < 0)
        goto error_out;
    if (dvp_cfg(g_vdata.w, g_vdata.h, g_vdata.dst_fmt) < 0)
        goto error_out;
    g_vdata.num_buffers = VID_BUF_NUM;
    thid = aicos_thread_create("test_dvp", 4096, 0, test_dvp_thread, NULL);
    if (thid == NULL) {
        pr_err("Failed to create DVP thread\n");
        return;
    }
    return;
error_out:
    mpp_vin_deinit();
    if (g_fb)
        mpp_fb_close(g_fb);
}
MSH_CMD_EXPORT_ALIAS(cmd_test_dvp, test_dvp,
                     Test DVP and camera);