浅析 Hi MPP 中的 uvc_app

以往我们说UVC一般搜索到的内容是板端作为主机,外接USB视频设备并使用UVC去控制,那么板端也就是从机中的UVC是如何实现的。下面就记录一个海思SDK中的例子,源码路径HISDK/mpp/sample/uvc_app

文件描述

文件 说明
application.c 主函数起始
hiuac.c 提供hiuac对象,负责音频控制
hiuvc.c 提供hiuvc对象,负责视频控制
camera.c 提供hicamera摄像头对象,负责hiuvc,hiuac对象控制
uvc_gadget.c 实现uvc设备操作功能
frame_cache.c 实现uvc缓存操作功能
histream.c 实现视频流操作功能
sample*.c 实现对接mpp媒体开发框架操作功能

对象操作

以文件划分功能,文件的操作函数都为静态文件作用域(对外部不可见),这些函数最终被赋值到变量中,而这个变量也是静态的,只能被唯一一个的全局作用域函数get_xx()获取。

static int __init(void){};
static int __run(void){};

static hicamera __hi_camera =
{
    .init = __init,
    .run = __run,
};

hicamera *get_hicamera(void)
{
    return &__hi_camera;
}

对象分析

直接进入正题以hiuvc对象为切入点,它由初始化、打开、关闭和运行四个部分组成。对象主要负责流程控制,不包含具体实现,其中__init并没有做任何事,__open__close为直接调用,__run创建了线程uvc_send_data_thread去循环run_uvc_data,然后主线程就进入循环run_uvc_device状态。通过查找可以发现这些对象支持函数都指向文件uvc_gadget.c

__open

深入open_uvc_device函数,最后可以看出它的最终执行的是v4l2常规流程,首先open设备视频设备节点获得fd,其次ioctl VIDIOC_UERYCAP去查询v4l2能力,最后再ioctl VIDIOC_SUBSCRBE_EVENT去设定订阅事件,如:VC处理(UVC_EVENT_SETUP),VS处理(UVC_EVENT_DATA),开启流(UVC_EVENT_STREAMON), 停止流(UVC_EVENT_STREAMOFF)。

__close

关闭是打开的相反操作,主要是去close掉打开的描述符,在这之前需要关闭视频能力。

__run

这个函数会创建线程循环run_uvc_data,自身进入run_uvc_device循环。

run_uvc_data

这个功能块就负责一件事,在流启动后去监听视频设备描述符,就绪时就通过uvc_video_process_userptr把一帧数据推入UVC视频缓冲区,具体功能实现先跳过。

run_uvc_device

这个功能块负责执行UVC事件处理,函数通过select监听描述符,当描述符就绪时就从视频设备的事件队列中出队一个事件并做处理。当初始化事件完成后会触发UVC_EVENT_STREAMON事件,对应的执行enable_uvc_video()去启动流。当不需要据流时触发UVC_EVENT_STREAMOFF事件去执行disable_uvc_video()停止流。

至此整个框架基本完成,首先open_uvc_device打开视频设备驱动,其次run_uvc_device去控制应用层的视频流开启关闭,最后通过run_uvc_data推入流到驱动向外输出。接下来就看看数据是如何被启动关闭的又是如何流转的,这里就需要提到uvc_cache视频设备缓存管理。

数据缓存

uvc_cache它由有6个帧节点和2个帧队列组成。其中帧队列free_queue表示空闲节点队列,初始化时得到了所有节点的。帧队列ok_queue表示完成节点队列,当节点填充完数据后才会被put到这个队列当中。

create_uvc_cache
    create_cache_node_list
        node = malloc //创建6块
        put_node_to_queue(uvc_cache->free_queue, node)

数据制造

前面提到,当触发UVC_EVENT_STREAMON事件是会执行enable_uvc_video去启动流,可用看到启动通常是先经过清理关机再开机的方式。直接进入histream_startup()这个函数动作,以看到最终创建了一条线程不断去监听Venc描述符,当图像就绪时去获取保存。首先会从空闲队列free_queue中取出节点,然后填充帧数据,最后put到完成队列ok_queue中,这是节点在队列中的第一次位置交换。函数最后可用看到dev-streaming被置1这就标志着流被开启,上面说到的run_uvc_data根据这个状态就可以开始推数据了。

数据首次消费

到这里数据流是开启了,但是初始化并没有完成,对于视频设备/dev/video目前也就仅经历了open和订阅UVC_EVENT_操作。接下来enable_uvc_videoioctl VIDIOC_REQBUFS去命令驱动申请缓存空间。接着从完成队列ok_queue中取出节点,并将节点成员node->mem赋值到v4l2_bufioctrl VIDIOC_QBUF入队到内核缓存空间中,这个node也还被记录在等待队列__waited_node[]上表示这个节点真正被处理。到这里就完成了视频帧的第一次消费。

数据后续消费

初始化后,数据制造者_SAMPLE_COMM_VENC_SaveData()会不断的从free_queue取出节点填充并挂到ok_queue上。而后续的消费工作也交回到线程的run_uvc_data去处理。可以看到首先会ioctl VIDIOC_DQBUF从内核出队一个帧,并从完成队列取出一个节点,被出队的帧号也对应等待队列的标号,帧可以出队就表示它被处理完了,这时对应等待队列中的节点__waited_node[buf->index] 就可以把他放回空闲队列中去,并记录下本次取出节点。同样节点成员node->mem也将通过v4l2_buf被ioctrl到内核中。这各过程被重复执行就实现了内核与用户数据源源不断的轮换。

结束工作

UVC_EVENT_STREAMOFF事件到来时,表示结束当前工作,首先ioctl VIDIOC_STREAMOFF停止内核流传输,接着关闭应用成流生产, 最后清空完成队列ok_queue

posted @ 2021-03-12 00:29  派大海星  阅读(1341)  评论(2编辑  收藏  举报