实现功能
设备侧获取摄像头传感器的数据,通过UVC协议传给上位机。同时,上位机发送控制命令给设备侧。
参考源码:https://github.com/wlhe/uvc-gadget
1. 概念
UVC:是一种USB视频设备驱动。用来支持USB视频设备,凡是USB接口的摄像头都能够支持。
V4L2:是Linux下视频采集和输出框架。用来统一接口,向应用层提供API。
UVC和V4L2关系: V4L2就是用来管理UVC设备的并且能够提供视频相关的一些应用程序接口。在Linux系统上有很多的开源软件能够支持V4L2。常见的有FFmpeg(我用这个)、opencv、Skype、Mplayer等等。
2. 具体流程
2.1 打开video设备
Linux一切皆文件,首先打开视频数据要输出的设备文件,假如为"/dev/video18":
dev->fd = open("/dev/video18", O_RDWR | O_NONBLOCK);
以非阻塞的方式打开设备文件。启动时,驱动会先把缓存里初始化数据通过设备输出到上位机,然后等待视频数据填充缓存。
2.2 获取video设备的属性
struct v4l2_capability cap;
ret = ioctl(dev->fd, VIDIOC_QUERYCAP, &cap);
使用 VIDIOC_QUERYCAP 命令来获得设备的各个属性及对各项功能的支持程度,这里主要关注 V4L2_CAP_VIDEO_OUTPUT ,即 video output 的功能。
2.3 其他配置
2.3.1 查询设备输出格式fmtdes
struct v4l2_fmtdesc fmtdes;
fmtdes.index = 0; // 查询格式序号
fmtdes.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_ENUM_FMT, &fmtdes);
使用 VIDIOC_ENUM_FMT 命令来列举设备所支持的所有image格式。
2.3.2 获取修剪能力cropcap,设置输出景象crop
获取:
struct v4l2_cropcap cropcap;
memset(&cropcap, 0, sizeof(cropcap));
cropcap.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_CROPCAP, &cropcap); // 查询驱动的修剪能力
设置:
struct v4l2_crop crop;
crop.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
crop.c.top = g_display_top; // 0
crop.c.left = g_display_left; // 0
crop.c.width = g_display_width; // 显示宽度
crop.c.height = g_display_height; // 显示高度
ioctl(dev->fd, VIDIOC_S_CROP, &crop);
2.3.3 设置输出视频格式fmt
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
fmt.fmt.pix.width = g_in_width;
fmt.fmt.pix.height = g_in_height;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;
fmt.fmt.pix.bytesperline = g_in_width;
fmt.fmt.pix.priv = 0;
fmt.fmt.pix.sizeimage = 0;
ioctl(dev->fd, VIDIOC_S_FMT, &fmt);
ioctl(dev->fd, VIDIOC_G_FMT, &fmt);
设置视频设备的视频数据格式,例如视频图像数据的宽、高、图像格式(JPEG、YUYV等)。
如果该视频设备驱动不支持你所设定的图像格式,视频驱动会重新修改 struct v4l2_format 结构体的值为该视频设备所支持的图像格式。
所以在程序设计中,设置完所有的视频格式后,要获取实际的视频格式,即要重新读取 struct v4l2_format 结构体。
2.4 初始化并订阅UVC事件
2.4.1 初始化流控制 probe 和 commit 数据结构
uvc_streaming_control probe;
uvc_streaming_control commit;
uvc_streaming_control 的结构体成员:
2.4.2 订阅事件
struct v4l2_event_subscription sub;
memset(&sub, 0, sizeof sub);
sub.type = UVC_EVENT_CONNECT;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_DISCONNECT;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_SETUP;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_DATA;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_STREAMON;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_STREAMOFF;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
将UVC事件的connect、disconnect、setup、data、streamon、streamoff,通过 VIDIOC_SUBSCRIBE_EVENT 命令设置到驱动里(或者说注册/订阅),上位机通过UVC事件与V4L2进行交互。
2.5 while循环等待事件触发
fd_set efds;
FD_ZERO(&efds);
FD_SET(dev->fd, &efds);
ret = select(dev->fd + 1, NULL, NULL, &efds, &tv); // 阻塞,等待efds信号
struct v4l2_event v4l2_event;
struct uvc_event *uvc_event = (void *)&v4l2_event.u.data;
ret = ioctl(dev->fd, VIDIOC_DQEVENT, &v4l2_event);
当有订阅的事件到来时,触发efds信号,并获取事件信息。
2.6 主要事件处理
2.6.1 UVC_EVENT_SETUP
处理host请求的 UVC_EVENT_SETUP 事件分为: 标准USB命令 和 class类命令 。
标准USB命令 (USB_TYPE_STANDARD): 不处理。这部分驱动只需响应,不需要额外信息。
class类命令 (USB_TYPE_CLASS): 又分 control 和 streaming,就是UVC协议相关的两个接口 VC 和 VS 。
1)VS接口处理(参考函数:uvc_events_process_streaming)
流控制命令:(参考 UVC 1.5 Class specification 4.3 节)
参数说明:
bmRequestType: 请求类型。参考标准USB协议。
bRequest: 子类。定义在Table A-8。
CS(Control Selector): 是 probe 还是 commit。定义在 Table A-16。
wIndex: 高字节为0,低字节为接口号。
wLength 和 Data: 数据长度和数据。和标准USB协议一样。
参数设置的过程需要主机和USB设备进行协商。协商过程大致如下图所示:
流程说明:
-> Host 先将期望的设置发送给USB设备(PROBE);
-> 设备将Host期望设置在自身能力范围之内进行修改,返回给Host(PROBE);
-> Host 认为设置可行的话,Commit 提交(COMMIT);
-> 设置接口的当前设置为某一个设置。
2)VC接口处理(参考函数:uvc_events_process_control)
struct uvc_camera_terminal camera_terminal;
struct uvc_processing_unit processing_unit;
VC接口内部有许多 unit 和 terminal 用来控制摄像头。比如,我们可以通过 Process unit 设置白平衡、曝光等等。
2.6.2 UVC_EVENT_DATA
处理host请求的 UVC_EVENT_DATA 事件:传递参数信息。
VS命令:probe 和 commit 。
VC命令:曝光、白平衡,亮度,对比度等等。
根据请求,update当前的参数。
2.6.3 UVC_EVENT_STREAMON
处理host请求的 UVC_EVENT_STREAMON 事件:配置缓存空间,视频流传输启动。
1)配置缓存空间
操作系统一般把系统使用的内存划分成用户空间和内核空间,分别由应用程序管理和操作系统管理。应用程序可以直接访问内存的地址,而内核空间存放的是供内核访问的代码和数据,用户不能直接访问。
V4L2缓存的数据,存放在内核空间。这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。主要采用内存映射方式和用户指针模式。
内存映射方式 (mmap):把设备里的内存映射到应用程序中的内存空间,直接处理设备内存。
用户指针模式 (userptr):内存片段由应用程序自己分配。这点需要在 struct v4l2_requestbuffers 里将 memory 字段设置成 V4L2_MEMORY_USERPTR。
v4l2_requestbuffers 结构体成员:
struct v4l2_requestbuffers {
__u32 count;
__u32 type; // enum v4l2_buf_type
__u32 memory; // enum v4l2_memory
__u32 reserved[2];
};
count: 要申请的buffer数量。
memory: V4L2_MEMORY_MMAP 或 V4L2_MEMORY_USERPTR。
这里主要讲 V4L2_MEMORY_USERPTR 模式:
a. 向驱动申请视频流数据的帧缓冲区:
struct v4l2_requestbuffers reqbufs;
memset(&reqbufs, 0, sizeof reqbufs);
reqbufs.count = nbufs;
reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
reqbufs.memory = V4L2_MEMORY_USERPTR;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &reqbufs);
b. 用户申请内存片段,并加入到输出缓存队列中:
struct v4l2_buffer buf;
for (i = 0; i < dev->nbufs; ++i) {
memset(&buf, 0, sizeof buf);
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.memory = V4L2_MEMORY_USERPTR;
buf.length = MAX_BUFFER_SIZE;
buf.m.userptr = (unsigned long)dev->dummy_buf[i].start; //用户空间
ret = ioctl(dev->fd, VIDIOC_QBUF, &buf);
}
通过 VIDIOC_QBUF 命令逐一将 buf 推到输出缓存队列尾部。申请若干个帧缓冲区,通常 nbufs 不少于3个。
注意:测试验证用户申请的空间地址 dev->dummy_buf[i].start 要初始化为 0,否则无法正常传输视频数据!
v4l2_buffer 结构体成员:
struct v4l2_buffer {
__u32 index; /* 应用程序来设定,仅仅用来申明是哪个 buffer */
__u32 type;
__u32 bytesused; /* buffer 中已经使用的 byte 数,如果是 input stream 由 driver 来设 定,相反则由应用程序来设定 */
__u32 flags; /* 定义了 buffer 的一些标志位,来表明这个 buffer 处在哪个队列(输入队列或者输出队列)(V4L2_BUF_FLAG_QUEUED, V4L2_BUF_FLAG_DONE) ,是否 关键帧 等等 */
__u32 memory; /* V4L2_MEOMORY_MMAP / V4L2_MEMORY_USERPTR / V4L2_MEMORY_OVERLAY */
union m :
__u32 offset; /* 当 memory 类型是 V4L2_MEOMORY_MMAP 的时候,主要用来表明 buffer 在 device momory 中相对起始位置的偏移,主要用在 mmap() 参数 中,对应用程序没有左右 */
unsigned long userptr; /* 当 memory 类型是 V4L2_MEMORY_USERPTR 的时候,这是 一个指向虚拟内存中 buffer 的指针,由应用程序来设定 */
__u32 length; /* buffer 的 size */
}
在驱动内部管理着2个缓存队列,一个输入队列,一个输出队列。
对于 capture 类型的设备 来说,当 输入队列中的 buffer 被塞满数据后 会自动变为输出队列,等待调用 VIDIOC_DQBUF 命令将数据进行处理以后,重新调用 VIDIOC_QBUF 命令,将 buffer 重新放进输入队列。
对于 output 类型的设备 来说,当 buffer 中的数据被读走后 会自动变为输入队列,等待调用 VIDIOC_DQBUF 命令,buffer 被塞满数据后,调用 VIDIOC_QBUF 命令将 buffer 重新放进输入队列。
2)开始视频流数据的采集
int type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl (fd_v4l, VIDIOC_STREAMON, &type);
2.6.4 UVC_EVENT_STREAMOFF
处理host的 UVC_EVENT_STREAMOFF 请求。
a. 停止视频流输出:
int type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_STREAMOFF, &type);
b. 释放内核缓存:
nbufs = 0; // 设置为0
memset(&reqbufs, 0, sizeof(reqbufs));
reqbufs.count = nbufs;
reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
reqbufs.memory = V4L2_MEMORY_USERPTR;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &reqbufs);
2.7 视频数据输出
2.7.1 while循环等待写准备信号
a. 当有wfds信号到来时,表示可以对输出队列进行写操作:
ret = select(dev->fd + 1, NULL, &wfds, NULL, &tv); // &wfds
if (ret > 0) {
ret = uvc_video_process(dev);
}
b. 从视频缓冲区中的输出队列取得一个可写的缓冲区,并将视频数据copy到该缓冲区:
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.memory = V4L2_MEMORY_USERPTR;
buf.length = MAX_BUFFER_SIZE;
ret = ioctl(dev->fd, VIDIOC_DQBUF, &buf);
uvc_video_fill_buffer(dev, &buf);
c. 将该缓冲区重新投放到视频缓冲区的输入队列中,等待被上位机读取或显示:
ret = ioctl(dev->fd, VIDIOC_QBUF, &buf);
2.8 结束关闭视频设备
close(dev->fd);
free(dev->mem); // 释放用户申请的内存
————————————————
原文链接:https://blog.youkuaiyun.com/h1527820835/article/details/124366709