转载自 hceng blog https://hceng.cn/2018/04/22/Linux%E6%91%84%E5%83%8F%E5%A4%B4%E9%A9%B1%E5%8A%A82%E2%80%94%E2%80%94UVC/
Linux摄像头驱动学习第二篇,对USB摄像头驱动USB video class(UVC)进行详细分析、编写。
这次要写一个真正的摄像头驱动,内容有点多。
先简单的介绍了USB接口,了解Linux中USB设备描述符的意义。
然后再移植内核自带的USB摄像头驱动,同时也验证了摄像头的可用。
最后为了学习,逐句写一个摄像头驱动,再总结。
1.UVC基础
UVC是USB video class的简写,也就是USB接口的视频设备。
UVC其实很好理解,就是V4L2+USB。
前面的虚拟摄像头驱动,数据的来源是自己构造的虚拟数据,现在V4L2的数据来源则是通过USB传进来的真实摄像头视频数据。
除了视频数据,摄像头还把自己的特性(比如支持哪几种分辨率)告诉驱动,驱动则要配置摄像头(指定何种分辨率)。
1.1 USB基础知识
USB分主从系统,一般而言,PC中的USB系统就是作主系统,而一般的USB鼠标、U盘则是典型的USB从系统。
为了方便开发,USB定义了一套标准,只要是支持USB的主机,就可以支持任何一个厂商的USB鼠标、U盘,只要是被USB系统包含的设备,只要这些设备支持相应的标准,就无需重新设计驱动而直接使用。
下面简单的列出了USB设备类型,理想情况的USB系统要对这些设备作完整的支持,设备也必须符合USB规范中的要求。
Base Class | Descriptor Usage | Description |
---|---|---|
00h | Device | Use class information in the Interface Descriptors |
01h | Interface | Audio |
02h | Both | Communications and CDC Control(通讯设备) |
03h | Interface | HID (Human Interface Device) |
05h | Interface | Physical |
06h | Interface | Image |
07h | Interface | Printer |
08h | Interface | Mass Storage(存储) |
09h | Device | Hub |
0Ah | Interface | CDC-Data |
0Bh | Interface | Smart Card |
0Dh | Interface | Content Security |
0Eh | Interface | Video |
0Fh | Interface | Personal Healthcare |
10h | Interface | Audio/Video Devices |
11h | Device | Billboard Device Class |
12h | Interface | USB Type-C Bridge Class |
DCh | Both | Diagnostic Device |
E0h | Interface | Wireless Controller |
EFh | Both | Miscellaneous |
FEh | Interface | Application Specific |
FFh | Both | Vendor Specific |
其中UVC就是Video类。
为了更好地描述USB设备的特征,USB提出了设备架构的概念。
从这个角度来看,可以认为USB设备是由一些配置,接口和端点;
即一个USB设备可以含有一个或多个配置,在每个配置中可含有一个或多个接口,在每个接口中可含有若干个端点。
此外,驱动是绑定到USB接口上,而不是整个设备。
体现到驱动上,就是一个一个的结构体,对应设备、配置、接口、端点。
其中USB video class它是在在标准的USB协议上进行了扩展,扩展的部分称为Class Specific。
- 标准的设备描述符:
typedef struct Device_Descriptor
{
uchar bLength; //设备描述符的字节数
uchar bDescriptorType; //设备描述符类型编号
uint bcdUSB; //USB版本号
uchar bDeviceClass; //USB分配的设备类
uchar bDeviceSubClass; //USB分配的设备子类
uchar bDeviceProtocol; //USB分配的设备协议代码
uchar bMaxPacketSize0; //端点0的最大包大小
uint idVendor; //厂商编号
uint idProduct; //产品编号
uint bcdDevice; //设备出厂编号
uchar iManufacturer; //设备厂商字符串索引
uchar iProduct; //产品字符串索引
uchar iSerialNumber; //设备序列号索引
uchar bNumConfigurations; //可能的配置数
}Device_Descriptor,*pDevice_Descriptor;
- 配置描述符:
typedef struct Configuration_Descriptor
{
uchar bLength; //配置描述符 的字节数
uchar bDescriptorType; //配置描述符类型编号
uint wTotalLength; //此配置返回的所有数据大小
uchar bNumInterfaces; //此配置支持的接口数量
uchar bConfigurationValue;//Set_Configuration命令所需要的参数
uchar iConfiguration; //描述该配置的字符串索引
uchar bmAttributes; //供电模式的选择
uchar bMaxPower; //设备从总线获取的最大电流
}Configuration_Descriptor,*pConfiguration_Descriptor;
- 接口描述符:
typedef struct Interface_Descriptor
{
uchar bLength; //接口描述符的字节数
uchar bDescriptorType; //接口描述符的类型编号
uchar bInterfaceNumber; //该接口的编号
uchar bAlternateSetting; //备用的接口描述符的编号
uchar bNumEndPoints; //该接口使用 的端点数,不包括端点0
uchar bInterfaceClass; //接口类
uchar bInterfaceSubClass; //接口子类
uchar bInterfaceProtocol; //接口类协议
uchar iInterface; //描述该接口的字符串索引值
}Interface_Descriptor,*pInterface_Descriptor;
- 端点描述符:
typedef struct EndPoint_Descriptor
{
uchar bLength; //端点描述符字节数
uchar bDescriptorType; //端点描述符类型编号
uchar bEndpointAddress; //端点地址及输入输出类型
uchar bmAtrributes; //端点的传输类型
uint wMaxPacketSize; //端点收发的最大包大小
uchar bInterval; //主机查询端点的时间间隔
}EndPoint_Descriptor,*pEndPoint_Descriptor;
1.2 UVC硬件模型
首先从USB官网下载标准协议相关资料:Video Class -> Video Class 1.5 document set (.zip format, size 6.58MB)
。
在USB_Video_Example 1.5.pdf
里,可以得知硬件模型分为两部分:VC interface
和VS interface
。VC interface
用于控制,内部又分为多个unit
和terminal
,unit
用于内部处理,terminal
用于内外链接;VS interface
用于传输,内部包括视频数据传输的端点以及摄像头支持的视频格式等信息;
每个视频有且仅有一个Vieo Control
接口和可有多个Video Streaming
接口;
一个接口,就相当于一个逻辑上的USB设备。
现在,想象一下当USB摄像头插上主机,就相当于同时插上了两个设备,可通过函数去选中其中一个设备,从而去操作它。
一个设备用于控制,比如设置亮度等;
一个设备用于获取数据,选择所支持的某个格式等;
这样就基本把控制和数据分开,要控制则操作控制接口,要数据则通过数据接口。
VideoControl Interface
用于控制,比如设置亮度。
它内部有多个Unit/Terminal(在程序里Unit/Terminal都称为entity)
可以通过uvc_query_ctrl
类似的函数来访问:
ret = uvc_query_ctrl(dev /*哪一个USB设备*/, SET_CUR, ctrl->entity->id /*哪一个unit/terminal*/, dev->intfnum /*哪一个接口:VC interface*/, ctrl->info->selector, uvc_ctrl_data(ctrl, UVC_CTRL_DATA_CURRENT), ctrl->info->size);
VideoStreaming Interface
用于获得视频数据,也可以用来选择fromat/frame(VS可能有多种format,一个format支持多种frame,frame用来表示分辨率等信息)
可以通过__uvc_query_ctrl
类似的函数来访问:
ret = __uvc_query_ctrl(video->dev /*哪一个USB设备*/, SET_CUR, 0, video->streaming->intfnum /*哪一个接口: VS*/, probe ? VS_PROBE_CONTROL : VS_COMMIT_CONTROL, data, size, uvc_timeout_param);
这里的参数VS_PROBE_CONTROL
只是枚举尝试,并不是设置,真正要设置需要使用参数VS_COMMIT_CONTROL
。
1.3 USB描述符
前面提到摄像头要把自己的特性(比如支持哪几种分辨率)告诉驱动,这个特性就是被放在USB描述符里面。
在前面下载的USB_Video_Example 1.5.pdf
文档里,有个UVC描述符层次结构例子:
将USB插在Ubuntu主机上,执行lsusb
可以看到当前的USB设备:
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 012: ID 1b3b:2977 iPassion Technology Inc.
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
可根据厂家名字iPassion Technology Inc
知道ID为1b3b:2977
的USB设备就是摄像头。
再使用-v
(显示USB设备的详细信息)和-d
(仅显示指定厂商和产品编号的设备)获取指定设备的详细信息:
lsusb -v -d 1b3b:2977
此时会打印出许多信息,精简去掉详细的数据,只留下大致框架如下:
Device Descriptor:
Configuration Descriptor:
Interface Association:
Interface Descriptor:
VideoControl Interface Descriptor:
VideoControl Interface Descriptor:
Endpoint Descriptor:
Interface Descriptor:
VideoStreaming Interface Descriptor:
VideoStreaming Interface Descriptor:
Interface Descriptor:
Endpoint Descriptor:
Interface Descriptor:
Endpoint Descriptor:
Interface Association:
Interface Descriptor:
AudioControl Interface Descriptor:
AudioControl Interface Descriptor:
Interface Descriptor:
AudioStreaming Interface Descriptor:
AudioStreaming Interface Descriptor:
Endpoint Descriptor:
AudioControl Endpoint Descriptor:
Interface Descriptor:
AudioStreaming Interface Descriptor:
AudioStreaming Interface Descriptor:
Endpoint Descriptor:
AudioControl Endpoint Descriptor:
可以看到设备描述符下有一个配置描述符,配置描述符下有两个联合接口(IAD),一个是视频的,一个是音频的。
同级的还有若干接口描述符,接口描述符下有若干VC、VS和端点,与前面的框架是完全对应的。
任取其中一个描述符:
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 1
bmCapabilities 0x01
Still image supported
wWidth 640
wHeight 480
dwMinBitRate 2304000
dwMaxBitRate 2304000
dwMaxVideoFrameBufferSize 76800
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
就可以得知该摄像头支持一种叫FRAME_MJPEG的格式,分辨率为640*480
等信息。
因此,从上面的一系列描述符,就可完全得知摄像头的特征,后面驱动用用到具体的特性再说明。
2.内核摄像头驱动
对UVC进行学习,步骤大致如下:
首先分析内核自带的UVC是如何实现的;
然后让手里的摄像头工作起来,可能内核自带的驱动可以直接用,也可能需要移植;
最后再尝试写一个精简版的UVC驱动,深入理解。
2.1分析内核摄像头驱动
在4.13.9内核中,UVC驱动在drivers/media/usb/uvc/
文件夹里,下面对uvc_driver.c
进行分析。
a.构造usb_driver
struct uvc_driver {
struct usb_driver driver;
};
struct uvc_driver uvc_driver = {
.driver = {
.name = "uvcvideo",
.probe = uvc_probe,
.disconnect = uvc_disconnect,
.suspend = uvc_suspend,
.resume = uvc_resume,
.reset_resume = uvc_reset_resume,
.id_table = uvc_ids,
.supports_autosuspend = 1,
},
};
其中.id_table
里列举了驱动支持哪些USB设备。
b.设置usb_driver
uvc_probe
kzalloc //分配video_device
uvc_register_chains
uvc_register_terms
uvc_register_video
vdev->v4l2_dev = &dev->vdev; //设置video_device
vdev->fops = &uvc_fops;
vdev->ioctl_ops = &uvc_ioctl_ops;
vdev->release = uvc_release;
video_register_device //注册video_device
c.注册usb_driver
uvc_init
usb_register
可以看到,probe()
函数里面的操作就是前面vivid
驱动里一样的操作方式。
然后在外面加了一个usb的“壳”。
驱动的核心还是fops
和ioctl_ops
。下面对这两个操作函数的实现进行分析。
首先是v4l2_file_operations
:
const struct v4l2_file_operations uvc_fops = {
.owner = THIS_MODULE,
.open = uvc_v4l2_open,
.release = uvc_v4l2_release,
.unlocked_ioctl = video_ioctl2,
#ifdef CONFIG_COMPAT
.compat_ioctl32 = uvc_v4l2_compat_ioctl32,
#endif
.read = uvc_v4l2_read,
.mmap = uvc_v4l2_mmap,
.poll = uvc_v4l2_poll,
#ifndef CONFIG_MMU
.get_unmapped_area = uvc_v4l2_get_unmapped_area,
#endif
};
里面有open()
、release()
、ioctl2
、read
、mmap
、poll
,这点和前面的虚拟驱动一样。
这其中最重要的就是ioctl2
,它使用video_usercopy()
获得用户空间传进来的参数,调用__video_do_ioctl()
在v4l2_ioctls[]
数组里找到对应的uvc_ioctl_ops
。
uvc_ioctl_ops
每个函数的实现放在后面写代码里,逐个讲解。
UVC驱动的重点在于:
- 对描述符的解析;
- 属性的控制: 通过
VideoControl Interface
来设置; - 格式的选择:通过
VideoStreaming Interface
来设置; - 数据的获得:通过
VideoStreaming Interface
的URB来获得;
2.2移植内核摄像头驱动
我手里使用的是百问网提供的二合一摄像头,它既有CMOS接口,也有USB接口。
使用USB接口时,上面有一个DSP芯片,可以将原始的YUV数据转换成MJPEG的压缩数据。
它基本是符合UVC规范的,但有些小差别,厂家提供的文档里面有说明,按着说明修改即可。
主要添加了usb_device_id
和修改了数据的处理。详细参考补丁,修改后的代码在Github。
编译完成后,先加载内核自带的uvcvideo
及依赖,然后移除内核自带的驱动,安装修改后的驱动,运行xawtv
应用程序:
sudo modprobe uvcvideo
sudo rmmod uvcvideo
sudo insmod uvcvideo.ko
xawtv -noalsa
效果图:
3.编写UVC驱动
UVC的驱动有点长,我尽量根据功能将其分解若干部分,逐一编写。
当USB插上主机,就会产生两个接口(VC和VS),然后获取到USB描述符并解析,从而设置摄像头(比如分辨率、格式);然后分配缓冲区,启动摄像头,便从USB得到摄像头采集数据,保存到缓冲区供应用程序使用。
整个流程就大致这样,因此将其分为了6个部分进行编写。
- 1.注册(USB和Video)
- 2.数据格式设置相关
- 3.缓冲区操作相关
- 4.属性相关(以亮度控制为例)
- 5.URB
- 6.启动/停止
- 7.其它操作函数(mmap和poll)
- 8.测试/效果
3.1 注册(USB和Video)
在入口函数先“套”一个USB驱动的框架,首先分配一个usb_driver
:
static struct usb_driver my_uvc_driver = {
.name = "my_uvc",
.probe = my_uvc_probe,
.disconnect = my_uvc_disconnect,
.id_table = my_uvc_ids,
};
其中的id_table
只包含我们所需的VC和VS,这样摄像头的Audio接口,就不会被识别:
static struct usb_device_id my_uvc_ids[] =
{
/* Generic USB Video Class */
{ USB_INTERFACE_INFO(USB_CLASS_VIDEO, 1, 0) }, /* VideoControl Interface */
{ USB_INTERFACE_INFO(USB_CLASS_VIDEO, 2, 0) }, /* VideoStreaming Interface */
{}
};
这里USB_INTERFACE_INFO
宏参数分别是前面接口描述符里的bInterfaceClass
(接口类),bInterfaceSubClass
(接口子类),bInterfaceProtocol
(接口类协议)。
#define USB_INTERFACE_INFO(cl, sc, pr) \
.match_flags = USB_DEVICE_ID_MATCH_INT_INFO, \
.bInterfaceClass = (cl), \
.bInterfaceSubClass = (sc), \
.bInterfaceProtocol = (pr)
这里传入的第一个参数都是video类,第二个分别是VC和VS,第三个参数都是无协议。这些设置的依据来自于摄像头的USB描述符:
Interface Descriptor:
……
bInterfaceClass 14 Video
bInterfaceSubClass 1 Video Control
bInterfaceProtocol 0
……
Interface Descriptor:
……
bInterfaceClass 14 Video
bInterfaceSubClass 2 Video Streaming
bInterfaceProtocol 0
……
这里驱动的usb_device_id
和摄像头提供的一旦匹配后,就会调用probe()
函数,这里两个接口,就会调用两次。在probe()
函数里,需要先得到usb_device
,用于对usb设备的操作,以及分别得到两个接口的编号,用于后面分别调用每个接口。再在probe()
函数里做常规的分配、设置、注册video_device
。
static int my_uvc_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
static int cnt = 0;
int ret;
printk("enter %s\n", __func__);
//usb_device_id会使probe()调用两次,然而创建video_device只需要一次
cnt++;
my_uvc_udev = interface_to_usbdev(intf); //获取usb设备
if (cnt == 1) //获取编号
my_uvc_control_intf = intf->cur_altsetting->desc.bInterfaceNumber;
else if (cnt == 2)
my_uvc_streaming_intf = intf->cur_altsetting->desc.bInterfaceNumber;
if (cnt == 2)
{
/* 1.分配一个video_device结构体 */
my_uvc_vdev = video_device_alloc();
if (NULL == my_uvc_vdev)
{
printk("Faile to alloc video device (%d)\n", ret);
return -ENOMEM;
}
/* 2.设置 */
my_uvc_vdev->release = my_uvc_release;
my_uvc_vdev->fops = &my_uvc_fops;
my_uvc_vdev->ioctl_ops = &my_uvc_ioctl_ops;
my_uvc_vdev->v4l2_dev = &v4l2_dev;
/* 3. 注册 */
ret = video_register_device(my_uvc_vdev, VFL_TYPE_GRABBER, -1);
if (ret < 0)
{
printk("Faile to video_register_device.\n");
return ret;
}
else
printk("video_register_device ok.\n");
/* 为了确定带宽,使用哪一个setting */
my_uvc_try_streaming_params(&my_uvc_params); //测试参数
my_uvc_get_streaming_params(&my_uvc_params); //取出参数
my_uvc_set_streaming_params(&my_uvc_params); //设置参数
}
return 0;
}
对应的disconnect
也会被调用两次,但只做一次释放操作:
static void my_uvc_disconnect(struct usb_interface *intf)
{
static int cnt = 0;
printk("enter %s\n", __func__);
cnt++;
if (cnt == 2)
{
video_unregister_device(my_uvc_vdev);
video_device_release(my_uvc_vdev);
}
}
现在,就完成了USB设备和Video设备的注册。
且为Video设备绑定了操作函数,后续的工作就是完善操作函数。
3.2 数据格式设置相关
前面Video设备绑定了fops
,这里主要有五个操作函数:
static const struct v4l2_file_operations my_uvc_fops =
{
.owner = THIS_MODULE,
.open = my_uvc_open,
.release = my_uvc_close,
.mmap = my_uvc_mmap,
.unlocked_ioctl = video_ioctl2, /* V4L2 ioctl handler */
.poll = my_uvc_poll,
};
open()
和close()
没什么好说的,常规操作:
static int my_uvc_open(struct file *file)
{
printk("enter %s\n", __func__);
return 0;
}
static int my_uvc_close(struct file *file)
{
printk("enter %s\n", __func__);
my_uvc_vidioc_streamoff(NULL, NULL, 0);
return 0;
}
关闭的时候,顺手调用vidioc_streamoff
关闭数据采集。
mmap()
和poll()
涉及buf的操作,后面再讲。先讲ioctl
里面几个稍微简单点的操作函数。
首先是vidioc_querycap()
,用于表明本设备是一个摄像头设备。需要对v4l2_capability
结构体的driver
命名,card
命名,version
指定版本号,capabilities
指定支持的功能,device_caps
通过节点访问的功能。
static int my_uvc_vidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap)
{
struct video_device *vdev = video_devdata(file);
printk("enter %s\n", __func__);
strlcpy(cap->driver, "my_uvc_video", sizeof(cap->driver));
strlcpy(cap->card, vdev->name, sizeof(cap->card));
cap->version = 4;
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;
return 0;
}
然后是vidioc_enum_fmt_vid_cap()
,用于列举摄像头支持的格式。
从USB摄像头的设备描述符可知,本摄像头只支持一种MJPEG
格式,通过index
来限定只接受一种格式。
需要设置v4l2_fmtdesc
结构体的description
(格式名字)、pixelformat
(格式对应的像素格式)和type
(v4l2_buf_type)。
static int my_uvc_vidioc_enum_fmt_vid_cap(struct file *file, void *priv, struct v4l2_fmtdesc *f)
{
printk("enter %s\n", __func__);
/* 根据摄像头的设备描述符可知,只支持一种格式:VS_FORMAT_MJPEG */
if(f->index >= 1)
return -EINVAL;
strcpy(f->description, MY_UVC_FMT); //支持格式
f->pixelformat = V4L2_PIX_FMT_MJPEG;
f->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
return 0;
}
之后是获取摄像头数据格式vidioc_g_fmt_vid_cap()
操作函数。这个比较简单,直接返回my_uvc_format
即可。
static int my_uvc_vidioc_g_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
{
printk("enter %s\n", __func__);
memcpy(f, &my_uvc_format, sizeof(my_uvc_format));
return 0;
}
再是vidioc_try_fmt_vid_cap()
,用于尝试设置摄像头数据的格式。
先判断传入的v4l2_format
结构体里的type
和pixelformat
是不是正确的格式。
再设置v4l2_pix_format
结构体的width
(宽)、height
(高)和filed
(数据扫描方式:不交错)。
以及sizeimage
(每帧图像大小),这里的值大小的确定是通过probe()
里打印的dwMaxVideoFrameSize
值,这里每帧的理论大小是width*height=320*240=76800
小于dwMaxVideoFrameSize=77312
,估计最大每帧图像还会包含其它数据。
大多数网络摄像头的colorspace
(颜色空间)都是V4L2_COLORSPACE_SRGB
。priv
(私有数据)由pixelformat
决定。
这里的所有设置的值,理论上都来自对USB设备描述符的解析,这里简化了代码解析的过程,直接赋值,实际开发中为了适配多个摄像头,应该读取后解析。
static int my_uvc_vidioc_try_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
{
printk("enter %s\n", __func__);
if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE || f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG)
return -EINVAL;
/* 调整format的width, height */
f->fmt.pix.width = my_uvc_wWidth; //设备描述符里支持的分辨率:640x480,320x240,160x120
f->fmt.pix.height = my_uvc_wHeight;
f->fmt.pix.field = V4L2_FIELD_NONE;
/* 计算bytesperline, sizeimage */
//bBitsPerPixel = my_uvc_bBitsPerPixel; //lsusb:bBitsPerPixel
//f->fmt.pix.bytesperline = (f->fmt.pix.width * bBitsPerPixel) >> 3;
f->fmt.pix.sizeimage = dwMaxVideoFrameSize; //f->fmt.pix.height * f->fmt.pix.bytesperline;
f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
f->fmt.pix.priv = 0; /* private data, depends on pixelformat */
return 0;
}
最后是设置摄像头的数据的格式vidioc_s_fmt_vid_cap()
。
先参数设置传入的v4l2_format
,如果不支持返回错误。支持的话,直接赋值给my_uvc_format
。
static int my_uvc_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
{
int ret;
printk("enter %s\n", __func__);
ret = my_uvc_vidioc_try_fmt_vid_cap(file, NULL, f);
if (ret < 0)
return ret;
memcpy(&my_uvc_format, f, sizeof(my_uvc_format));
return 0;
}
至此,就完成了对摄像头数据格式my_uvc_format
的设置。
应用层就可以对摄像头数据格式进行操作,比如选择何种数据格式、何种分辨率等,当然,这里的驱动没有提供选择,全都直接赋值了。
3.3 缓冲区操作相关
buf操作是一个难点,容易出问题的地方。
首先是申请缓冲区vidioc_reqbufs()
,应用层ioctl
调用此函数,让其分配若干个buf,应用层后面将从这些buf读取视频数据。
驱动先从传入的v4l2_requestbuffers
结构体获得count
(buf数量),每个buf的大小是前面my_uvc_format
的sizeimage
(每帧图像大小),且长度页对齐。
- PAGE_ALIGN
PAGE_ALIGN在内核里作用是将数据以4K页大小上界对齐。
举个例子:
假如传入的数据大小是4000字节,那么结果得到4096字节;
假如传入的数据大小是4096字节,那么结果得到4096字节;
假如传入的数据大小是5000字节,那么结果得到8192字节;源码:
#define PAGE_SIZE 4096
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_ALIGN(x) ((x + PAGE_SIZE - 1) & PAGE_MASK)
实质:
PAGE_ALIGN(x) = ((x + 4095) & (~4095))
然后再判断my_uvc_queue
结构体里的mem
(内存地址)是否为空,非空的话说明原来已经分配了buf,需要先释放内存、清空my_uvc_queue
。
如果传入需要的buf数量为0,则表明不需要分配,直接退出。
然后就分配buf,将所有buf作为一个整体一次性分配,大小也就是nbuffers * bufsize
,如果分配失败,减小buf数量,再尝试。
现在就有了一整块buf,对应的起始地址是mem
,再清空my_uvc_queue
进行初始化。
再初始化两个队列(双向链表),mainqueue
用于供应用层读取数据用,irqqueue
用于供驱动产生数据用。
再依次设置每个buf的v4l2_buffer
结构体的index
(索引)、m.offset
(偏移)、length
(大小)、type
(类型)、sequence
(序列计数)、field
(扫描方式)、memory
(内存类型)、flags
(标志),再设置my_uvc_buffer
的state
(状态)和初始化等待队列wait
。
最后再设置my_uvc_q
,记录buf首地址、数量和大小。
/* APP调用该ioctl让驱动程序分配若干个buf, APP将从这些buf中读到视频数据 */
static int my_uvc_vidioc_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p)
{
unsigned int i;
void *mem = NULL;
int nbuffers = p->count; //buf数量
int bufsize = PAGE_ALIGN(my_uvc_format.fmt.pix.sizeimage); //buf大小,且长度页对齐
printk("enter %s\n", __func__);
if (my_uvc_q.mem) //如果原来分配了buf,先释放原来的buf
{
vfree(my_uvc_q.mem);
memset(&my_uvc_q, 0, sizeof(my_uvc_q));
my_uvc_q.mem = NULL;
}
if (nbuffers == 0) //没有需要分配的,直接退出
return 0;
for (; nbuffers > 0; --nbuffers) //依次减少buf数量,直到分配成功
{
mem = vmalloc_32(nbuffers * bufsize); //这些buf是一次性作为一个整体来分配的
if (mem != NULL)
break;
}
if (mem == NULL)
return -ENOMEM;
memset(&my_uvc_q, 0, sizeof(my_uvc_q)); //清空my_uvc_q,初始化
INIT_LIST_HEAD(&my_uvc_q.mainqueue); //初始化两个队列,my_uvc_vidioc_qbuf
INIT_LIST_HEAD(&my_uvc_q.irqqueue);
for (i = 0; i < nbuffers; ++i)
{
my_uvc_q.buffer[i].buf.index = i; //索引
my_uvc_q.buffer[i].buf.m.offset = i * bufsize; //偏移
my_uvc_q.buffer[i].buf.length = my_uvc_format.fmt.pix.sizeimage; //原始大小;实测PAGE_ALIGN对齐,也没问题
my_uvc_q.buffer[i].buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //视频捕获设备
my_uvc_q.buffer[i].buf.sequence = 0;
my_uvc_q.buffer[i].buf.field = V4L2_FIELD_NONE;
my_uvc_q.buffer[i].buf.memory = V4L2_MEMORY_MMAP;
my_uvc_q.buffer[i].buf.flags = 0;
my_uvc_q.buffer[i].state = VIDEOBUF_IDLE; //分配完更新状态为空闲
init_waitqueue_head(&my_uvc_q.buffer[i].wait); //初始化一个等待队列
}
my_uvc_q.mem = mem;
my_uvc_q.count = nbuffers;
my_uvc_q.buf_size = bufsize;
return nbuffers;
}
这样,我们就得到一个my_uvc_queue
结构体,这个结构体里面的my_uvc_buffer
结构体数组,存放了每个buf的信息。示意如下:
接下来是vidioc_querybuf()
,用于查询buf,获得buf的地址信息等。
先判断传入的v4l2_buffer
结构体中的index
是否超出了buf数量范围。
然后将my_uvc_q
中的对应的v4l2_buffer
传给传入的v4l2_buf
。
再判断my_uvc_buffer
中的vma_use_count
是否表示被mmap()
,对应修改标准位。
最后再将uvc的state flags转换成V4L2的state flags,其实它们的值都是一样的,
/* 查询缓存状态, 比如地址信息(APP可以用mmap进行映射) */
static int my_uvc_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
int ret = 0;
printk("enter %s\n", __func__);
if (v4l2_buf->index >= my_uvc_q.count)
{
ret = -EINVAL;
goto done;
}
memcpy(v4l2_buf, &my_uvc_q.buffer[v4l2_buf->index].buf, sizeof(*v4l2_buf));
if (my_uvc_q.buffer[v4l2_buf->index].vma_use_count) //更新flags
v4l2_buf->flags |= V4L2_BUF_FLAG_MAPPED;
#if 0
switch (my_uvc_q.buffer[v4l2_buf->index].state) //将uvc flags转换成V4L2 flags
{
case VIDEOBUF_ERROR:
case VIDEOBUF_DONE:
v4l2_buf->flags |= V4L2_BUF_FLAG_DONE;
break;
case VIDEOBUF_QUEUED:
case VIDEOBUF_ACTIVE:
v4l2_buf->flags |= V4L2_BUF_FLAG_QUEUED;
break;
case VIDEOBUF_IDLE:
default:
break;
}
#endif
done:
return ret;
}
这样,就将对应的v4l2_buffer
相关信息传给了应用层,应用层就通过此函数查询各个buf信息。
vidioc_qbuf()
是将前面的buf放入到队列中。
首先是判断传入的v4l2_buffer
的类型、内存种类、节点是否超过最大数量和my_uvc_q
的my_uvc_buffer
状态是否处于空闲。
然后修改my_uvc_q
的my_uvc_buffer
状态为处于队列中VIDEOBUF_QUEUED
,初始化v4l2_buffer
中的bytesused
(缓冲区中数据的大小)为0。
然后把对应buf的stream
和irq
分别加到队列mainqueue
和队列irqqueue
尾部。
- 队列
mainqueue
:供应用层使用,当队列中缓冲区有数据时, 应用层从mainqueue
队列中取出数据; - 队列
irqqueue
:供产生数据的函数使用,当采集到数据时,从irqqueue
队列中取出首个缓冲区,存入数据;
/* 把传入的缓冲区放入队列, 底层的硬件操作函数将会把数据放入这个队列的缓存 */
static int my_uvc_vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
printk("enter %s\n", __func__);
/* 0. APP传入的v4l2_buf可能有问题, 要做判断 */
if (v4l2_buf->type != V4L2_BUF_TYPE_VIDEO_CAPTURE || v4l2_buf->memory != V4L2_MEMORY_MMAP)
return -EINVAL;
if (v4l2_buf->index >= my_uvc_q.count)
return -EINVAL;
if (my_uvc_q.buffer[v4l2_buf->index].state != VIDEOBUF_IDLE)
return -EINVAL;
/* 1. 修改状态 */
my_uvc_q.buffer[v4l2_buf->index].state = VIDEOBUF_QUEUED;
my_uvc_q.buffer[v4l2_buf->index].buf.bytesused = 0;
/* 2. 放入2个队列 */
//队列1: 供应用层使用
//当队列中缓冲区有数据时, 应用层从mainqueue队列中取出数据
list_add_tail(&my_uvc_q.buffer[v4l2_buf->index].stream, &my_uvc_q.mainqueue);
//队列2: 供产生数据的函数使用
//当采集到数据时,从irqqueue队列中取出首个缓冲区,存入数据
list_add_tail(&my_uvc_q.buffer[v4l2_buf->index].irq, &my_uvc_q.irqqueue);
return 0;
}
- 通过此函数,就将传入的
v4l2_buffer
放在了两个队列中。
最后是将数据从队列取出vidioc_dqbuf()
。
这里是应用层想得到数据,因此是从mainqueue
队列获取。
首先判断mainqueue
是否是空队列,然后以my_uvc_q.mainqueue
作为头节点,搜索my_uvc_buffer
结构体中的stream
,得到队列中第一个my_uvc_buffer
的地址。
再把my_uvc_buffer
的state
(状态)改为VIDEOBUF_IDLE
(空闲)。、
再将该节点从队列删除,最后返回v4l2_buf
。
/* APP通过poll/select确定有数据后,把buf从mainqueue队列中取出来 */
static int my_uvc_vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
struct my_uvc_buffer *get_buf;
printk("enter %s\n", __func__);
if (list_empty(&my_uvc_q.mainqueue))
return -EINVAL;
get_buf = list_first_entry(&my_uvc_q.mainqueue, struct my_uvc_buffer, stream); //取出buf
switch (get_buf->state) //修改状态
{
case VIDEOBUF_ERROR:
return -EIO;
case VIDEOBUF_DONE:
get_buf->state = VIDEOBUF_IDLE;
break;
case VIDEOBUF_IDLE:
case VIDEOBUF_QUEUED:
case VIDEOBUF_ACTIVE:
default:
return -EINVAL;
}
list_del(&get_buf->stream); //从队列删除
memcpy(v4l2_buf, &get_buf->buf, sizeof *v4l2_buf); //复制返回数据
return 0;
}
至此,对buf的基本操作就完成了,包括buf的申请、查询、放入/取出到队列。
其中,队列的变化如下:
初始状态,队列mainqueue
和队列irqqueue
串连起了传进来的buf。
产生数据的时候,buf[0]装入数据,且断开与队列irqqueue
的连接,此时buf[1]是队列irqqueue
的第一个节点。
取出数据的时候,buf[0]取出数据,且断开与队列mainqueue
的连接,此时buf[1]是队列mainqueue
的第一个节点。
待数据处理完成,buf[0]将被再次放入队列,此时在队列尾部。
周而复始完成放入、取出队列。
3.4 属性相关(以亮度控制为例)
接下来是操作摄像头属性,以亮度控制为例,查询、获取、设置摄像头的亮度属性。
从前面的UVC硬件模型中可以得知,VC interface
是用于控制摄像头的,其中PU
单元用于属性的控制。
在UVC 1.5 Class specification.pdf
文档里,找到Processing Unit Descriptor
,其中的bmControls
表示摄像头支持属性的含义:
A bit set to 1 indicates that the mentioned Control is supported for the video stream.
D0: Brightness
D1: Contrast
D2: Hue
D3: Saturation
D4: Sharpness
D5: Gamma
D6: White Balance Temperature
D7: White Balance Component
D8: Backlight Compensation
D9: Gain
……
再找到本摄像头USB描述符中VC interface Descriptor
的PROCESSING_UNIT
中的bmControls
,其值是0x0000053f
,对应支持的属性也就是其下面的几个属性,Brightness
(亮度)控制是支持的。
VideoControl Interface Descriptor:
bLength 11
bDescriptorType 36
bDescriptorSubtype 5 (PROCESSING_UNIT)
Warning: Descriptor too short
bUnitID 3
bSourceID 1
wMaxMultiplier 0
bControlSize 2
bmControls 0x0000053f
Brightness
Contrast
Hue
Saturation
Sharpness
Gamma
Backlight Compensation
Power Line Frequency
在代码中,UVC规范定义的属性在uvc_ctrl.c
里的一个uvc_control_info
结构体类型的vc_ctrls
数组里。
{
.entity = UVC_GUID_UVC_PROCESSING, //属于哪个entity(比如PU)
.selector = UVC_PU_BRIGHTNESS_CONTROL, //用于亮度
.index = 0, //对应Processing Unit Descriptor的bmControls[0]
.size = 2, //数据长度为2字节
.flags = UVC_CTRL_FLAG_SET_CUR //支持SET_CUR、GET_RANGE(GET_CUR、GET_MIN、GET_MAX)等
| UVC_CTRL_FLAG_GET_RANGE
| UVC_CTRL_FLAG_RESTORE,
},
{
.entity = UVC_GUID_UVC_PROCESSING,
.selector = UVC_PU_CONTRAST_CONTROL,
.index = 1,
.size = 2,
.flags = UVC_CTRL_FLAG_SET_CUR
| UVC_CTRL_FLAG_GET_RANGE
| UVC_CTRL_FLAG_RESTORE,
},
现在,文档、硬件、代码三者都找到了对应。
此外,uvc_control_mapping
结构体类型的uvc_ctrl_mappings
数组更加细致地描述属性。
{
.id = V4L2_CID_BRIGHTNESS, //应用层根据ID来找到对应属性
.name = "Brightness", //名字
.entity = UVC_GUID_UVC_PROCESSING, //属于哪了个entity(比如PU)
.selector = UVC_PU_BRIGHTNESS_CONTROL, //用于亮度控制
.size = 16, //数据占多少位
.offset = 0, //从哪位开始
.v4l2_type = V4L2_CTRL_TYPE_INTEGER, //属性类别(整数)
.data_type = UVC_CTRL_DATA_TYPE_SIGNED, //数据类型(有符号整数)
},
{
.id = V4L2_CID_CONTRAST,
.name = "Contrast",
.entity = UVC_GUID_UVC_PROCESSING,
.selector = UVC_PU_CONTRAST_CONTROL,
.size = 16,
.offset = 0,
.v4l2_type = V4L2_CTRL_TYPE_INTEGER,
.data_type = UVC_CTRL_DATA_TYPE_UNSIGNED,
},
因此,属性控制的准备工作有:
1.获取摄像头的设备描述符,根据PU的描述符的
bmControls
,得知它支持哪些属性;
2.从uvc_ctrls
数组中根据entity
和index
找到对应属性,得知其支持的操作(SET_CUR、GET_CUR等);
3.从uvc_ctrl_mappings
数组中根据ID
找到对应属性,得知其更加详细信息(整数等);
首先是查询属性vidioc_queryctrl()
,应用层传入一个v4l2_queryctrl
结构体,驱动设置其参数返回。
需要设置的参数有id
(ID)、type
(类型)、name
(名字)、flags
(标志)、minimum
(最小值)、maximum
(最大值)、step
(步长)、default_value
(典型值),其中前面几个是根据前面的准备工作得知的值,直接赋值,后面的几个需要使用usb_control_msg()
函数向摄像头发起USB传输,获取对应值。
- usb_control_msg()
功能:发送一个简单的控制消息到指定的端点,并等待消息完成或超时;
参数:
dev:指向控制消息所发送的目标USB设备(usb_device
)的指针; <这里是在probe()
里获取的my_uvc_udev
>
pipe:控制消息所发送的目标USB设备的特定端点,调用usb_sndctrlpipe
(把指定USB设备的指定端点设置为一个控制OUT端点)或usb_rcvctrlpipe
(把指定USB设备的指定端点设置为一个控制IN端点)来创建的; <这里把my_uvc_udev
设置为接收端点>
request:控制消息的USB请求值; <这里分别是需要的GET_MIN、GET_MAX、GET_RES、GET_DEF>
requesttype:控制消息的USB请求类型值; <这里为USB_TYPE_CLASS(1<<5)、usb_recip_interface(1<<0)、usb_dir_in(1<<7)>
D7:数据的传输方向:0表示从主机到设备;1表示从设备到主机;
D6~5:命令的类型:0表示标准命令;1表示类命令;2表示厂商提供的命令;3保留;
D4~0:接收对象:0表示设备; 1表示接口;2表示端点;3表示其他;
value:控制消息的USB消息值; <这里是PU亮度控制>
index:控制消息的USB消息索引值;<这里是PU对应的ID和控制接口>
data:指向要发送/接收的数据的指针; <这里是接收数据>
size:data参数所指缓冲区的大小; <这里是两字节,bControlSize=2>
timeout:以msecs为单位,期望等待的超时时间,如果为0,该函数将一直等待消息结束以的时间;<这里是5s>
返回值:
成功返回接收/发送的字节数,否则返回负的错误值;
通过usb_control_msg()
获得的data
还需要调用my_uvc_get_le_value()
进行转换成value
。
int my_uvc_vidioc_queryctrl (struct file *file, void *fh, struct v4l2_queryctrl *ctrl)
{
unsigned char data[2];
if (ctrl->id != V4L2_CID_BRIGHTNESS) //这里只操作控制亮度的v4l2_queryctrl
return -EINVAL;
memset(ctrl, 0, sizeof * ctrl); //初始化,清空
ctrl->id = V4L2_CID_BRIGHTNESS; //设置ID
ctrl->type = V4L2_CTRL_TYPE_INTEGER; //设置属性类别(整数)
strcpy(ctrl->name, "MY_UVC_BRIGHTNESS"); //设置名字
ctrl->flags = 0; //默认支持设置等
/* 发起USB传输,从摄像头获取这些值 */
//设置最小值
if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_MIN, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
ctrl->minimum = my_uvc_get_le_value(data);
//设置最大值
if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_MAX, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
ctrl->maximum = my_uvc_get_le_value(data);
//设置步长
if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_RES, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
ctrl->step = my_uvc_get_le_value(data);
//设置典型值
if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_DEF, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
ctrl->default_value = my_uvc_get_le_value(data);
printk("Brightness: min =%d, max = %d, step = %d, default = %d\n", ctrl->minimum, ctrl->maximum, ctrl->step, ctrl->default_value);
return 0;
}
之后是vidioc_g_ctrl()
(获得属性)和vidioc_s_ctrl()
(设置属性),操作和前面差不多,都是通过usb_control_msg()
函数建立控制消息,从而发送/接收亮度数据。
int my_uvc_vidioc_g_ctrl (struct file *file, void *fh, struct v4l2_control *ctrl)
{
unsigned char data[2];
if (ctrl->id != V4L2_CID_BRIGHTNESS)
return -EINVAL;
if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
ctrl->value = my_uvc_get_le_value(data);
return 0;
}
int my_uvc_vidioc_s_ctrl (struct file *file, void *fh, struct v4l2_control *ctrl)
{
unsigned char data[2];
if (ctrl->id != V4L2_CID_BRIGHTNESS)
return -EINVAL;
my_uvc_set_le_value(ctrl->value, data);
if(2 != usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),
SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))
return -EIO;
return 0;
}
至此,对属性进行操作,比如对摄像头亮度控制就完成了,其它的属性控制类似。
3.5 URB
USB Request Block
(URB)是Linux内核中,USB驱动实现的一个数据结构,用于组织每一次的USB设备驱动的数据传输请求。
也就是说,将USB传输相关信息放到URB这个结构体中,发送给USB核心,USB核心解析该结构体,从而进行所需数据/控制相关操作。
所需的操作大致有三步:
1.分配usb_buffers,作为数据的缓冲区;
2.分配URB;
3.设置URB;
- 为什么要
usb_buffer
?
从这个角度想:前面的my_uvc_buffer
作为内核与用于空间的buf进行交互,这里的urb_buffer
作为内核与USB设备的buf进行交互,最后类似urb_buffer = my_uvc_buffer
,就实现了USB设备的数据传到用户层了。
首先,USB每次传输的数据大小,是可变的,根据外部设备的能力决定,比如外部设备支持一次传输100、200或800字节数据,每次传输称为Packet
(包);
其次,USB每次需要传输的数据,很可能大于前面的最大包(800字节),因此每次传输的数据,将会被分割成N个包来传输。
因此,用URB来记录一次完整传输的信息,包括每次传多少,传几次,传的目标位置等。
psize = my_uvc_wMaxPacketSize; //实时传输端点一次能传输的最大字节数;lsusb: wMaxPacketSize 0x0320 1x800 bytes;
size = my_uvc_params.dwMaxVideoFrameSize; //一帧数据的最大长度
npackets = DIV_ROUND_UP(size, psize); //传多少次(向上取整)
psize
就是每次传输的数据大小,通过USB摄像头的设备描述符wMaxPacketSize
(最大每包大小)可以得知。size
就是每帧图像的大小,前面在my_uvc_params
已经设置过了,是通过在probe()
的打印dwMaxPayloadTransferSize
得知的;npackets
就是size/psize
再向上取整,得到需要传多少次。
最后还要size = psize * npackets
更新一下向上取整后的新大小。
这个分配MY_UVC_URBS_NUM
个(一个就行)urb_buffer
和urb
。urb_buffer
通过usb_alloc_coherent()
函数分配,大小为前面的调整后的size
,得到指向buf的指针和DMA地址。urb
通过my_uvc_uninit_urbs
函数分配,数量为npackets
,得到指向该urb
的指针。
对应的,如果分配失败,相应的调用usb_free_coherent()
和usb_free_urb()
释放空间,并相应的清空指针和重置my_uvc_q.urb_size
。
然后就是设置URB:
urb->dev
:指向目标设备的指针;<这里是USB摄像头my_uvc_udev
>urb->pipe
:与目标设置的管道;<这里使用usb_rcvisocpipe()
创建等时(ISO:Isochronous)管道,参数是对应VS的端点地址>urb->transfer_flags
:传输标志;<URB_ISO_ASAP
(开始调度)和URB_NO_TRANSFER_DMA_MAP
(使用DMA对应的buf)>urb->interval
:传输间隔;<来自USB描述符的bInterval=1
>urb->transfer_buffer
:要传输的buf;<前面得到的my_uvc_q.urb_buffer[i]
指针>urb->transfer_dma
:buf对应的dma物理地址;<前面得到的my_uvc_q.urb_dma[i]
地址>urb->complete
:收完数据后的中断处理函数;<后面再编写>urb->number_of_packets
:该URB要传输多少个包;<前面计算的npackets
>urb->transfer_buffer_length
:总共的数据长度;<前面计算的size
>urb->iso_frame_desc[j].offset
:每个包的偏移位置;<j * psize
就对应每个包的偏移>urb->iso_frame_desc[j].length
:每个包的大小;<前面得到的psize
>
关于URB数据结构的参考博客。
static void my_uvc_uninit_urbs(void)
{
unsigned int i;
for (i = 0; i < MY_UVC_URBS_NUM; ++i)
{
//释放usb_buffers
//同时判断urb大小,如果非0才执行,因为本函数最后会将其置0,streamoff调用时,就不应该再释放一次
if (my_uvc_q.urb_buffer[i] && my_uvc_q.urb_size)
{
usb_free_coherent(my_uvc_udev, my_uvc_q.urb_size, my_uvc_q.urb_buffer[i], my_uvc_q.urb_dma[i]);
my_uvc_q.urb_buffer[i] = NULL;
}
//释放urb
if (my_uvc_q.urb[i])
{
usb_free_urb(my_uvc_q.urb[i]);
my_uvc_q.urb[i] = NULL;
}
}
my_uvc_q.urb_size = 0;
}
static int my_uvc_alloc_init_urbs(void)
{
int i, j;
int npackets;
unsigned int size;
unsigned short psize;
struct urb *urb;
psize = my_uvc_wMaxPacketSize; //实时传输端点一次能传输的最大字节数;lsusb: wMaxPacketSize
size = my_uvc_params.dwMaxVideoFrameSize; //一帧数据的最大大小
npackets = DIV_ROUND_UP(size, psize); //传多少次(向上取整)
if (npackets == 0)
return -ENOMEM;
size = my_uvc_q.urb_size = psize * npackets; //取整后新大小
for (i = 0; i < MY_UVC_URBS_NUM; ++i)
{
/* 1.分配usb_buffers */
my_uvc_q.urb_buffer[i] = usb_alloc_coherent(my_uvc_udev, size,
GFP_KERNEL | __GFP_NOWARN, &my_uvc_q.urb_dma[i]);
/* 2.分配urb */
my_uvc_q.urb[i] = usb_alloc_urb(npackets, GFP_KERNEL);
if (!my_uvc_q.urb_buffer[i] || !my_uvc_q.urb[i]) //如果分配失败
{
my_uvc_uninit_urbs();
return -ENOMEM;
}
}
/* 3. 设置urb */
for (i = 0; i < MY_UVC_URBS_NUM; ++i)
{
urb = my_uvc_q.urb[i];
urb->dev = my_uvc_udev;
urb->pipe = usb_rcvisocpipe(my_uvc_udev, my_uvc_bEndpointAddress); //lsusb: bEndpointAddress 0x82
urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;
urb->interval = 1; //lsusb: bInterval 1
urb->transfer_buffer = my_uvc_q.urb_buffer[i];
urb->transfer_dma = my_uvc_q.urb_dma[i];
urb->complete = my_uvc_video_complete; //中断处理函数
urb->number_of_packets = npackets;
urb->transfer_buffer_length = size;
for (j = 0; j < npackets; ++j)
{
urb->iso_frame_desc[j].offset = j * psize;
urb->iso_frame_desc[j].length = psize;
}
}
return 0;
}
现在我们就设置好了URB,包含了目标设备USB摄像头和urb_buffer
等信息,只要把这个URB传给USB核心,USB核心就会解析URB,与指定的USB设备传输数据,数据将被放在urb_buffer
里,接收到USB设备传来的数据包时,将产生一个中断,执行中断处理函数my_uvc_video_complete
。
中断函数里会依次处理每个包,将包的数据放到my_uvc_q.irqqueue
队列首个节点所指的buf,当多个包的数据量足够一帧时,就唤醒休眠的应用层,应用层就会得到数据,最后中断程序再发送URB,再次进入中断,依次循环。
下面就是实现my_uvc_video_complete
,在里面首先判断之前URB传输的结果:
switch (urb->status) {
case 0: //Success
break;
case -ETIMEDOUT: //Nak
case -ECONNRESET: //Kill
case -ENOENT:
case -ESHUTDOWN:
default: //Error
return;
}
只有urb->status = 0
才表示传输成功,否则都直接返回。
然后在判断my_uvc_q.irqqueue
队列不为空的情况下,取出首个buf,后面将从URB得到的数据放在这个buf里:
if (!list_empty(&my_uvc_q.irqqueue)) //判断是不是空队列
buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);//取出首buf用于后续存放数据
else
buf = NULL;
之后便是对每个URB的子包进行处理:
1.判断状态
urb->iso_frame_desc[i].status
小于0,跳过处理该子包;
2.计算数据源(来自URB)、长度、目的地址(放到队列提取的buf);
3.判断该包数据是否有效,其中data[0]
包含头部长度,data[1]
包含错误状态;
4.使用摄像头厂家提供的特殊处理,完成对fid
的操作;<fid
介绍见下面>
5.如果buf=NULL
,表示之前irqqueue
队列没有空间了,没必要后续操作了;
6.判断buf->state
是不是VIDEOBUF_ACTIVE
(正在接收数据)状态,即是不是第一次开始接收数据,是的话改为VIDEOBUF_ACTIVE
;
7.让last_fid = fid
,表示要开始接收本帧数据;
8.传输的数据长度为:子包去除头部信息后的数据长度与buf剩余空间的 最小值;
9.将URB子包复制到buf中;
10.引用厂家代码,对buf数据进行某些处理;
11.当子包数据长度大于该buf剩下空间、得到标志UVC_STREAM_EOF
且收到数据不为空时,表明一帧数据传完,修改buf状态VIDEOBUF_DONE
;
12.从irqqueue
队列删除该节点;唤醒应用层读取mainqueue
队列的数据,即本帧数据;修改mem
偏移和date_len
,取出下一个buf;
以上就是对每个子包的操作,主要包含了子包状态的判断、对是否完成一帧传输的判断、复制子包数据到buf、厂家特殊处理、再次从队列获取buf。
讲一下
fid
(frame id)。
我们看到的连续视频,可以分成若干个1s的视频,再把每个1s的视频分成30份,每一份就是一张图片,称之为帧(frame)。
这个帧的数据,是由URB传输中的若干个pack组成的,在URB传输中,产生一连续的pack,我们如何知道其中的某几个pack属于某一帧的呢?
摄像头厂家的解决方案是,为每个pack也编号,属于同一帧的几个连续pack编号相同,这就实现了在pack上出现0、1交替时,就表示该帧传输完了,开始传输下一帧。
在中断函数的最后,还要再次提交URB,这样才能再次进入中断,拷贝数据,如此反复。
static void my_uvc_video_complete(struct urb *urb)
{
int ret, i;
unsigned char *mem;
unsigned char *src, *dest;
struct my_uvc_buffer *buf;
int len, maxlen, nbytes, data_len;
static int fid, last_fid = -1;
//要修改影像資料,必須先宣告一個特別型態的指標變數,才能正確存取記憶體中的資料
unsigned char *point_mem;
static unsigned char *mem_temp = NULL;
//初始化暫存用的記憶體位置
static unsigned int nArrayTemp_Size = 1000;
printk("enter %s\n", __func__);
printk("=======urb->status: %d ======\n", urb->status);
switch (urb->status) {
case 0: //Success
break;
case -ETIMEDOUT: //Nak
case -ECONNRESET: //Kill
case -ENOENT:
case -ESHUTDOWN:
default: //Error
return;
}
/* 从irqqueue队列中取出首个缓冲区 */
if (!list_empty(&my_uvc_q.irqqueue)) //判断是不是空队列
buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);//取出首buf用于后续存放数据
else
buf = NULL;
for (i = 0; i < urb->number_of_packets; ++i) //一次urb传输包含number_of_packets个子包
{
if (urb->iso_frame_desc[i].status < 0)
continue;
src = urb->transfer_buffer + urb->iso_frame_desc[i].offset; //数据源
len = urb->iso_frame_desc[i].actual_length; //数据长度
if(buf)
dest = my_uvc_q.mem + buf->buf.m.offset + buf->buf.bytesused; //目的地址
//判断数据是否有效;URB数据含义: data[0]->头部长度;data[1]->错误状态
if ((len < 2) || (src[0] < 2) || (src[0] > len) || (src[1] & UVC_STREAM_ERR))
continue;
if (my_uvc_udev->descriptor.idVendor == 0x1B3B) /* ip2970/ip2977 */
{
if ( len >= 16 ) // have data in buffer
{
// 資料必須從data[12]開始判斷,是因為前面的資料是封包專用
if ( (src[12] == 0xFF && src[13] == 0xD8 && src[14] == 0xFF) ||
(src[12] == 0xD8 && src[13] == 0xFF && src[14] == 0xC4))
{
if(last_fid) //效果:取反
fid &= ~UVC_STREAM_FID;
else
fid |= UVC_STREAM_FID;
}
}
}
else
{
fid = src[1] & UVC_STREAM_FID;
}
/* Store the payload FID bit and return immediately when the buffer is NULL.*/
if (buf == NULL)
{
last_fid = fid;//?必要性?
continue;
}
if (buf->state != VIDEOBUF_ACTIVE) //!= VIDEOBUF_ACTIVE, 表示"之前还未接收数据"
{
if (fid == last_fid)
continue; //因为是第一次接收数据,前面的fid已经被取反,不该等于上一次的last_fid
buf->state = VIDEOBUF_ACTIVE; //表示开始接收第1个数据
}
last_fid = fid; //开始传本帧数据
len -= src[0]; //除去头部后的数据长度
maxlen = buf->buf.length - buf->buf.bytesused; //缓冲区最多还能存多少数据
nbytes = min(len, maxlen);
//dest = my_uvc_q.mem + buf->buf.m.offset + buf->buf.bytesused; //目的地址
memcpy(dest, src + src[0], nbytes); //复制数据
buf->buf.bytesused += nbytes; //更新buf已使用空间
/* ip2970/ip2977 */
if (my_uvc_udev->descriptor.idVendor == 0x1B3B)
{
if(mem_temp == NULL)
{
mem_temp = kmalloc(nArrayTemp_Size, GFP_KERNEL);
}
else if(nArrayTemp_Size <= nbytes) //當收到的資料長度大於上一次的資料長度,則重新分配所需的空間+
{
kfree(mem_temp);
nArrayTemp_Size += 500;
mem_temp = kmalloc(nArrayTemp_Size, GFP_KERNEL);
}
memset(mem_temp, 0x00, nArrayTemp_Size);
// 指向資料儲存的記憶體位置
point_mem = (unsigned char *)dest;
if( *(point_mem) == 0xD8 && *(point_mem + 1) == 0xFF && *(point_mem + 2) == 0xC4)
{
memcpy( mem_temp + 1, point_mem, nbytes);
mem_temp[0] = 0xFF;
memcpy(point_mem, mem_temp, nbytes + 1);
}
}
/* 判断一帧数据是否已经全部接收到 */
if (len > maxlen)
buf->state = VIDEOBUF_DONE;
/* Mark the buffer as done if the EOF marker is set. */
if ((src[1] & UVC_STREAM_EOF) && (buf->buf.bytesused != 0))
buf->state = VIDEOBUF_DONE;
/* 当接收完一帧数据,从irqqueue中删除这个缓冲区,唤醒等待数据的进程 */
if ((buf->state == VIDEOBUF_DONE) || (buf->state == VIDEOBUF_ERROR))
{
list_del(&buf->irq);
wake_up(&buf->wait);
mem = my_uvc_q.mem + buf->buf.m.offset;
data_len = buf->buf.bytesused;
/* 取出下一个buf */
if (!list_empty(&my_uvc_q.irqqueue))
buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);
else
buf = NULL;
}
}
/* 再次提交URB */
if ((ret = usb_submit_urb(urb, GFP_ATOMIC)) < 0)
{
printk("Failed to resubmit video URB (%d).\n", ret);
}
}
3.6 启动/停止
应用层调用ioctl()
传入的参数为VIDIOC_STREAMON
时,就会调用vidioc_streamon()
启动摄像头采集数据。
在该驱动函数里面,主要做三件事:
- 设置USB摄像头参数;(比如使用何种视频数据格式、何种分辨率)
- 分配设置URB;(调用前面的
my_uvc_alloc_init_urbs()
函数) - 提交URB,等待中断;
一般的摄像头,会支持多种格式,比如MJPEG、H264等,也会支持多种分辨率。
因此需要在开始传输前,通过USB设置摄像头,让其后面返回正确的数据。
假如我们直接设置,可能摄像头不支持我们设置的格式,后面对应的解析数据可能会出现错误。因此我们先尝试传入设置参数,摄像头接收后会保存起来,并根据自身情况做一些修正,再将该设置读取出来,再进行真正的设置。
这里我们定义一个my_uvc_streaming_control
结构体,用于保存这个设置过程中的参数。
首先是尝试设置参数,根据摄像头版本,对应的分配一个data
空间,用于等会保存参数进行USB传输。
//lsusb得到:bcdUVC = 1.00;再BCD转换,eg:2.10 -> 210H, 1.50 -> 150H
size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根据版本分配buf大小
data = kmalloc(size, GFP_KERNEL);
if (data == NULL)
return -ENOMEM;
再清空传入的my_uvc_streaming_control
结构体,设置相应参数,再参考内核UVC驱动使用cpu_to_le16()
将my_uvc_streaming_control
赋值给data
。
memset(ctrl, 0, sizeof * ctrl);
ctrl->bmHint = 1; //保持dwFrameInterval不变
ctrl->bFormatIndex = 1; //支持格式数量
ctrl->bFrameIndex = bFrameIndex; //使用第二种分辨率:640x480(1),320x240(2),160x120(3)
ctrl->dwFrameInterval = 333333; //lsusb: dwFrameInterval(0) 333333 每秒30帧
ctrl_to_data(ctrl, data, size);
最后调用usb_control_msg()
将data
传给摄像头,这里的usb_control_msg()
在前面的亮度控制详细介绍了每个参数的含义,当时使用的是VC接口,这里使用VS接口。
这里不是真正的设置,所以传入的参数是VS_PROBE_CONTROL
。
ret = usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),
SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
VS_PROBE_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);
kfree(data);
尝试设置了USB后,再把摄像头修正的参数读取出来保存到my_uvc_streaming_control
结构体中。
static int my_uvc_get_streaming_params(struct my_uvc_streaming_control *ctrl)
{
int ret = 0;
unsigned char *data;
unsigned short size;
//lsusb得到:bcdUVC=1.00;再BCD转换,eg:2.10 -> 210H, 1.50 -> 150H
size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根据版本分配buf大小
data = kmalloc(size, GFP_KERNEL);
if (data == NULL)
return -ENOMEM;
//通过usb获取摄像头参数
ret = usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),
GET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
VS_PROBE_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);
if (ret < 0)
goto done;
//返回摄像头参数
data_to_ctrl(data, ctrl, size);
done:
kfree(data);
return ret;
}
最后再将新的参数设置给摄像头,这样就能保证现在设置的参数对摄像头是有效的。
这里是真正的设置,所以传入的参数是VS_COMMIT_CONTROL
。
static int my_uvc_set_streaming_params(struct my_uvc_streaming_control *ctrl)
{
int ret = 0;
unsigned char *data;
unsigned short size;
//lsusb得到:bcdUVC=1.00;再BCD转换,eg:2.10 -> 210H, 1.50 -> 150H
size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根据版本分配buf大小
data = kmalloc(size, GFP_KERNEL);
if (data == NULL)
return -ENOMEM;
ctrl_to_data(ctrl, data, size);
//通过usb尝试设置摄像头参数
ret = usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),
SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
VS_COMMIT_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);
kfree(data);
return ret;
}
然后还要指定bAlternateSetting
,bAlternateSetting
用于在同一个接口中的多个描述符中进行切换。
也就是说,USB摄像头提供多种Interface Descriptor
(接口),每个接口的支持一种wMaxPacketSize
(带宽,一次传输提供的数据量)、dwMaxPayloadTransferSize
(每帧最大数据,实测等于分辨率加512)。
当摄像头分辨率变化时,相应所需的接口也会变化,比如分辨率变大,要选择带宽更大的接口。bAlternateSetting
就相当于是接口的索引,因此不同分辨率,应该选择对应的接口。比如本次使用的分辨率为640x480
,从my_uvc_params
获取的推荐接口就是bAlternateSetting=6
:
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 6
bNumEndpoints 1
bInterfaceClass 14 Video
bInterfaceSubClass 2 Video Streaming
bInterfaceProtocol 0
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 5
Transfer Type Isochronous
Synch Type Asynchronous
Usage Type Data
wMaxPacketSize 0x03bc 1x 956 bytes
bInterval 1
/* 1. 向USB摄像头设置参数:比如使用哪个format, 使用这个format下的哪个frame(分辨率等) */
// 根据结构体my_uvc_streaming_control设置数据包;再调用usb_control_msg发出数据包;
//a.测试参数
my_uvc_try_streaming_params(&my_uvc_params);
//b.取出参数
my_uvc_get_streaming_params(&my_uvc_params);
//c.设置参数
my_uvc_set_streaming_params(&my_uvc_params);
//d.设置VideoStreaming Interface所使用的setting
//从my_uvc_params.dwMaxPayloadTransferSize得知所需带宽;实测分辨率不一样,所需的带宽也不一样;
//根据wMaxPacketSize得到对应的bAlternateSetting;
usb_set_interface(my_uvc_udev, my_uvc_streaming_intf, my_uvc_bAlternateSetting);
设置好了摄像头的format
(格式)、frame
(分辨率)等,就可以分配设置URB,准备和USB摄像头传输数据了。
/* 2.分配设置URB */
ret = my_uvc_alloc_init_urbs();
if (0 != ret)
{
printk("my_uvc_alloc_init_urbs err : ret = %d\n", ret);
return ret;
}
分配完成后,就提交给USB核心,等待中断来临,读取摄像头发来的数据。
/* 3.提交URB以接收数据 */
for (i = 0; i < MY_UVC_URBS_NUM; ++i)
{
if ((ret = usb_submit_urb(my_uvc_q.urb[i], GFP_KERNEL)) < 0)
{
printk("Failed to submit URB %u (%d).\n", i, ret);
my_uvc_uninit_urbs();
return ret;
}
}
停止采集数据vidioc_streamoff()
也需要做三件事:
- 取消URB传输;
- 释放
urb_buffer
和URB; - 设置接口为0,让其处于休眠状态;
static int my_uvc_vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type t)
{
struct urb *urb;
unsigned int i;
printk("enter %s\n", __func__);
/* 1. kill all URB */
for (i = 0; i < MY_UVC_URBS_NUM; ++i)
{
if ((urb = my_uvc_q.urb[i]) == NULL)
continue;
usb_kill_urb(urb);
}
/* 2. free all URB */
my_uvc_uninit_urbs();
/* 3. 设置VideoStreaming Interface为setting 0 */
usb_set_interface(my_uvc_udev, my_uvc_streaming_intf, 0);
return 0;
}
3.7 其它操作函数(mmap和poll)
现在还遗留两个操作函数mmap()
和poll()
,因为涉及buf和队列,前面无法理解,现在应该好理解了。
首先是mmap()
,前面提到应用层调用vidioc_queryctrl()
时,会让驱动程序分配若干个buf,也就是my_uvc_q.buf[N]
;
现在我们需要做的就是把buf映射到用户空间,以后用户空间操作映射后的空间,就间接的操作了内核的my_uvc_q.buf[N]
。
根据传入的vma->vm_pgoff
偏移,对应找到my_uvc_q.buf
,如果没找到或者大小不对,就退出。
如果找到了对应偏移的my_uvc_q.buf
,就可以根据该buf的起始地址和偏移得到物理地址addr
;
再将物理地址传入vmalloc_to_page()
函数得到page
结构体,再使用vm_insert_page()
函数将page
结构体和传入的vma
虚拟地址绑定,以PAGE_SIZE
大小分割总的size
。
最后在使用计数加1,后面vidioc_querybuf()
查询缓存状态时,用于更新标志。
//把缓存映射到APP的空间,以后APP就可以直接操作这块缓存
static int my_uvc_mmap(struct file *file, struct vm_area_struct *vma)
{
int i, ret = 0;
struct page *page;
struct my_uvc_buffer *buffer;
unsigned long addr, start, size;
printk("enter %s\n", __func__);
start = vma->vm_start;
size = vma->vm_end - vma->vm_start;
//应用程序调用mmap函数时,会传入offset参数,再根据offset找出指定的缓冲区
for (i = 0; i < my_uvc_q.count; ++i)
{
buffer = &my_uvc_q.buffer[i];
if ((buffer->buf.m.offset >> PAGE_SHIFT) == vma->vm_pgoff)
break;
}
//没找到对应的my_uvc_q.buffer或大小不对
if ((i == my_uvc_q.count) || (size != my_uvc_q.buf_size))
return -EINVAL;
/* VM_IO marks the area as being an mmaped region for I/O to a
* device. It also prevents the region from being core dumped. */
vma->vm_flags |= VM_IO;
//根据虚拟地址得到缓冲区对应的page结构体
addr = (unsigned long)my_uvc_q.mem + buffer->buf.m.offset;
while (size > 0) //循环把size大小的空间变为page
{
page = vmalloc_to_page((void *)addr);
//把page和APP传入的虚拟地址挂构
if ((ret = vm_insert_page(vma, start, page)) < 0)
return ret ;
start += PAGE_SIZE;
addr += PAGE_SIZE;
size -= PAGE_SIZE;
}
buffer->vma_use_count++; //引用计数+1
return ret;
}
最后是poll()
函数,用来确定buf是否准备就绪,即含有数据。
应用层调用poll()
时,会尝试从my_uvc_q.mainqueue
队列取出首个缓冲区,得到其buf->wait
,然后调用poll_wait()
以wait
为标志,进入休眠。等待中断里的wake_up()
,再唤醒。根据buf->state
返回对应的mask
,对应的应用程序就读取数据。
//APP调用POLL/select来确定缓存是否就绪(有数据)
static unsigned int my_uvc_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_uvc_buffer *buf;
unsigned int mask = 0;
printk("enter %s\n", __func__);
//从mainqueuq中取出第1个缓冲区,判断它的状态, 如果未就绪,休眠
if (list_empty(&my_uvc_q.mainqueue))
{
mask |= POLLERR;
goto done;
}
buf = list_first_entry(&my_uvc_q.mainqueue, struct my_uvc_buffer, stream);
poll_wait(file, &buf->wait, wait);
if (buf->state == VIDEOBUF_DONE || buf->state == VIDEOBUF_ERROR)
mask |= POLLIN | POLLRDNORM; //普通或优先级带数据可读 | 普通数据可读
done:
return mask;
}
3.8 测试/效果
如前面测试内核自带驱动一样,先编译自己的驱动,然后加载内核自带的uvcvideo
及依赖,然后移除内核自带的驱动,安装自己写的新驱动,运行xawtv
应用程序:
make
sudo modprobe uvcvideo
sudo rmmod uvcvideo
sudo insmod my_uvc.ko
xawtv -noalsa
- 效果:
完整代码见GitHub。
4.总体分析
整体框图如下:
几个基本概念:
1.应用层有五个操作函数,其中ioctl
下至少有11个基本的操作函数;
2.USB摄像头有且只有一个VC
接口用于控制,可有多个VS
接口用于数据传输;
3.11个操作函数可以分为四类:数据buf的操作、摄像头格式的操作、摄像头属性的操作、摄像头的启动与停止;
4.数据buf的操作:
a.根据应用层参数生成指定个数的v4l2_buffer
,这些buf又同时在两个队列上:mianquque
和irqquque
;
b.摄像头产生的数据通过VS
接口和USB核心的URB,放入irqquque
队列的首buf,并将该buf从该队列删除;
c.应用层取出mianquque
队列的首buf,得到数据,并将该buf从该队列删除,此时该buf同时不在两个队列上,将被重新放在尾部;
5.摄像头格式的操作:使用interface_to_usbdev()
得到对应接口的USB设备描述符,描述符包含摄像头的各种特性信息,保存在v4l2_format
结构体中;
6.摄像头属性的操作:使用·usb_control_msg()
通过VC
接口设置相关属性;
有了上面的基本概念,现在开始调用vidioc_streamon()
启动传输:
1.设置USB摄像头对应带宽接口等;
2.分配usb_buffers
和urb
,设置urb
;
3.上报urb
,USB核心解析urb
,向指定接口(摄像头VS接口)接收数据(放在usb_buffers
);
4.urb
传输完成后产生中断,中断里取出irqquque
队列首buf,将usb_buffers
数据放入,并唤醒休眠的poll
;
5.poll
唤醒,vidioc_dqbuf()
从mianquque
队列取出首buf,返回给应用层,完成了摄像头数据到应用层的传输。
参考文章:
韦东山第三期项目视频_摄像头