以往我们说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_video
会ioctl VIDIOC_REQBUFS
去命令驱动申请缓存空间。接着从完成队列ok_queue
中取出节点,并将节点成员node->mem
赋值到v4l2_buf
由ioctrl 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
。