赞
踩
转载自 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摄像头驱动,同时也验证了摄像头的可用。
最后为了学习,逐句写一个摄像头驱动,再总结。
UVC是USB video class的简写,也就是USB接口的视频设备。
UVC其实很好理解,就是V4L2+USB。
前面的虚拟摄像头驱动,数据的来源是自己构造的虚拟数据,现在V4L2的数据来源则是通过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;
首先从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
。
前面提到摄像头要把自己的特性(比如支持哪几种分辨率)告诉驱动,这个特性就是被放在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
等信息。
因此,从上面的一系列描述符,就可完全得知摄像头的特征,后面驱动用用到具体的特性再说明。
对UVC进行学习,步骤大致如下:
首先分析内核自带的UVC是如何实现的;
然后让手里的摄像头工作起来,可能内核自带的驱动可以直接用,也可能需要移植;
最后再尝试写一个精简版的UVC驱动,深入理解。
在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来获得;我手里使用的是百问网提供的二合一摄像头,它既有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
效果图:
UVC的驱动有点长,我尽量根据功能将其分解若干部分,逐一编写。
当USB插上主机,就会产生两个接口(VC和VS),然后获取到USB描述符并解析,从而设置摄像头(比如分辨率、格式);然后分配缓冲区,启动摄像头,便从USB得到摄像头采集数据,保存到缓冲区供应用程序使用。
整个流程就大致这样,因此将其分为了6个部分进行编写。
在入口函数先“套”一个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设备绑定了操作函数,后续的工作就是完善操作函数。
前面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
的设置。
应用层就可以对摄像头数据格式进行操作,比如选择何种数据格式、何种分辨率等,当然,这里的驱动没有提供选择,全都直接赋值了。
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]将被再次放入队列,此时在队列尾部。
周而复始完成放入、取出队列。
接下来是操作摄像头属性,以亮度控制为例,查询、获取、设置摄像头的亮度属性。
从前面的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;
- }
至此,对属性进行操作,比如对摄像头亮度控制就完成了,其它的属性控制类似。
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);
- }
- }
应用层调用ioctl()
传入的参数为VIDIOC_STREAMON
时,就会调用vidioc_streamon()
启动摄像头采集数据。
在该驱动函数里面,主要做三件事:
my_uvc_alloc_init_urbs()
函数)一般的摄像头,会支持多种格式,比如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_buffer
和URB;- 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;
- }
现在还遗留两个操作函数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;
- }
如前面测试内核自带驱动一样,先编译自己的驱动,然后加载内核自带的uvcvideo
及依赖,然后移除内核自带的驱动,安装自己写的新驱动,运行xawtv
应用程序:
- make
- sudo modprobe uvcvideo
- sudo rmmod uvcvideo
- sudo insmod my_uvc.ko
- xawtv -noalsa
完整代码见GitHub。
整体框图如下:
几个基本概念:
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,返回给应用层,完成了摄像头数据到应用层的传输。
参考文章:
韦东山第三期项目视频_摄像头
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。