Linux摄像头驱动2——UVC

版权声明:本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,欢迎转载,但转载请注明来自hceng blog(www.hceng.cn),并保持转载后文章内容的完整。本人保留所有版权相关权利。 https://blog.csdn.net/hceng_linux/article/details/89874064

CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2018/04/22/Linux摄像头驱动2——UVC/#more

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。

  • 标准的设备描述符:
    {% codeblock lang:c %}
    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;
{% endcodeblock %}

  • 配置描述符:
    {% codeblock lang:c %}
    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;
{% endcodeblock %}

  • 接口描述符:
    {% codeblock lang:c %}
    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;
    {% endcodeblock %}

  • 端点描述符:
    {% codeblock lang:c %}
    typedef struct EndPoint_Descriptor
    {
    uchar bLength; //端点描述符字节数
    uchar bDescriptorType; //端点描述符类型编号
    uchar bEndpointAddress; //端点地址及输入输出类型
    uchar bmAtrributes; //端点的传输类型
    uint wMaxPacketSize; //端点收发的最大包大小
    uchar bInterval; //主机查询端点的时间间隔

}EndPoint_Descriptor,*pEndPoint_Descriptor;
{% endcodeblock %}

1.2 UVC硬件模型

首先从USB官网下载标准协议相关资料:Video Class -> Video Class 1.5 document set (.zip format, size 6.58MB)
USB_Video_Example 1.5.pdf里,可以得知硬件模型分为两部分:VC interfaceVS interface

VC interface用于控制,内部又分为多个unitterminalunit用于内部处理,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
{% codeblock lang:c %}
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,
},
};
{% endcodeblock %}
其中.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的“壳”。

驱动的核心还是fopsioctl_ops。下面对这两个操作函数的实现进行分析。
首先是v4l2_file_operations:
{% codeblock lang:c %}
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
};
{% endcodeblock %}
里面有open()release()ioctl2readmmappoll,这点和前面的虚拟驱动一样。

这其中最重要的就是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
{% codeblock lang:c %}
static struct usb_driver my_uvc_driver = {
.name = “my_uvc”,
.probe = my_uvc_probe,
.disconnect = my_uvc_disconnect,
.id_table = my_uvc_ids,
};
{% endcodeblock %}
其中的id_table只包含我们所需的VC和VS,这样摄像头的Audio接口,就不会被识别:
{% codeblock lang:c %}
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 */
{}
};
{% endcodeblock %}
这里USB_INTERFACE_INFO宏参数分别是前面接口描述符里的bInterfaceClass(接口类),bInterfaceSubClass(接口子类),bInterfaceProtocol(接口类协议)。
{% codeblock lang:c %}
#define USB_INTERFACE_INFO(cl, sc, pr)
.match_flags = USB_DEVICE_ID_MATCH_INT_INFO,
.bInterfaceClass = (cl),
.bInterfaceSubClass = (sc),
.bInterfaceProtocol = (pr)
{% endcodeblock %}
这里传入的第一个参数都是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
{% codeblock lang:c %}
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;

}
{% endcodeblock %}

对应的disconnect也会被调用两次,但只做一次释放操作:
{% codeblock lang:c %}
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);
}

}
{% endcodeblock %}

现在,就完成了USB设备Video设备的注册。
且为Video设备绑定了操作函数,后续的工作就是完善操作函数。

3.2 数据格式设置相关

前面Video设备绑定了fops,这里主要有五个操作函数:
{% codeblock lang:c %}
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,
};
{% endcodeblock %}

open()close()没什么好说的,常规操作:
{% codeblock lang:c %}
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;

}
{% endcodeblock %}
关闭的时候,顺手调用vidioc_streamoff关闭数据采集。

mmap()poll()涉及buf的操作,后面再讲。
先讲ioctl里面几个稍微简单点的操作函数。

首先是vidioc_querycap(),用于表明本设备是一个摄像头设备。
需要对v4l2_capability结构体的driver命名,card命名,version指定版本号,capabilities指定支持的功能,device_caps通过节点访问的功能。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


然后是vidioc_enum_fmt_vid_cap(),用于列举摄像头支持的格式。
从USB摄像头的设备描述符可知,本摄像头只支持一种MJPEG格式,通过index来限定只接受一种格式。
需要设置v4l2_fmtdesc结构体的description(格式名字)、pixelformat(格式对应的像素格式)和type(v4l2_buf_type)。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


之后是获取摄像头数据格式vidioc_g_fmt_vid_cap()操作函数。
这个比较简单,直接返回my_uvc_format即可。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


再是vidioc_try_fmt_vid_cap(),用于尝试设置摄像头数据的格式。
先判断传入的v4l2_format结构体里的typepixelformat是不是正确的格式。
再设置v4l2_pix_format结构体的width(宽)、height(高)和filed(数据扫描方式:不交错)。
以及sizeimage(每帧图像大小),这里的值大小的确定是通过probe()里打印的dwMaxVideoFrameSize值,这里每帧的理论大小是width*height=320*240=76800小于dwMaxVideoFrameSize=77312,估计最大每帧图像还会包含其它数据。
大多数网络摄像头的colorspace(颜色空间)都是V4L2_COLORSPACE_SRGB
priv(私有数据)由pixelformat决定。
这里的所有设置的值,理论上都来自对USB设备描述符的解析,这里简化了代码解析的过程,直接赋值,实际开发中为了适配多个摄像头,应该读取后解析。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


最后是设置摄像头的数据的格式vidioc_s_fmt_vid_cap()
先参数设置传入的v4l2_format,如果不支持返回错误。支持的话,直接赋值给my_uvc_format
{% codeblock lang:c %}
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;

}
{% endcodeblock %}
至此,就完成了对摄像头数据格式my_uvc_format的设置。
应用层就可以对摄像头数据格式进行操作,比如选择何种数据格式、何种分辨率等,当然,这里的驱动没有提供选择,全都直接赋值了。

3.3 缓冲区操作相关

buf操作是一个难点,容易出问题的地方。
首先是申请缓冲区vidioc_reqbufs(),应用层ioctl调用此函数,让其分配若干个buf,应用层后面将从这些buf读取视频数据。
驱动先从传入的v4l2_requestbuffers结构体获得count(buf数量),每个buf的大小是前面my_uvc_formatsizeimage(每帧图像大小),且长度页对齐。

  • 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_bufferstate(状态)和初始化等待队列wait
最后再设置my_uvc_q,记录buf首地址、数量和大小。
{% codeblock lang:c %}
/* 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;

}
{% endcodeblock %}
这样,我们就得到一个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,其实它们的值都是一样的,
{% codeblock lang:c %}
/* 查询缓存状态, 比如地址信息(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;
}
{% endcodeblock %}
这样,就将对应的v4l2_buffer相关信息传给了应用层,应用层就通过此函数查询各个buf信息。


vidioc_qbuf()是将前面的buf放入到队列中。
首先是判断传入的v4l2_buffer的类型、内存种类、节点是否超过最大数量和my_uvc_qmy_uvc_buffer状态是否处于空闲。
然后修改my_uvc_qmy_uvc_buffer状态为处于队列中VIDEOBUF_QUEUED,初始化v4l2_buffer中的bytesused(缓冲区中数据的大小)为0。
然后把对应buf的streamirq分别加到队列mainqueue和队列irqqueue尾部。

  • 队列mainqueue:供应用层使用,当队列中缓冲区有数据时, 应用层从mainqueue队列中取出数据;

  • 队列irqqueue:供产生数据的函数使用,当采集到数据时,从irqqueue队列中取出首个缓冲区,存入数据;
    {% codeblock lang:c %}
    /* 把传入的缓冲区放入队列, 底层的硬件操作函数将会把数据放入这个队列的缓存 */
    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;
    }
    {% endcodeblock %}
    通过此函数,就将传入的v4l2_buffer放在了两个队列中。


最后是将数据从队列取出vidioc_dqbuf()
这里是应用层想得到数据,因此是从mainqueue队列获取。
首先判断mainqueue是否是空队列,然后以my_uvc_q.mainqueue作为头节点,搜索my_uvc_buffer结构体中的stream,得到队列中第一个my_uvc_buffer的地址。
再把my_uvc_bufferstate(状态)改为VIDEOBUF_IDLE(空闲)。、
再将该节点从队列删除,最后返回v4l2_buf
{% codeblock lang:c %}
/* 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;

}
{% endcodeblock %}

至此,对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 DescriptorPROCESSING_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数组里。
{% codeblock lang:c %}
{
.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,
},
{% endcodeblock %}
现在,文档、硬件、代码三者都找到了对应。

此外,uvc_control_mapping结构体类型的uvc_ctrl_mappings数组更加细致地描述属性。
{% codeblock lang:c %}
{
.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,
},
{% endcodeblock %}

因此,属性控制的准备工作有:

1.获取摄像头的设备描述符,根据PU的描述符的bmControls,得知它支持哪些属性;
2.从uvc_ctrls数组中根据entityindex找到对应属性,得知其支持的操作(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

{% codeblock lang:c %}
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;

}
{% endcodeblock %}


之后是vidioc_g_ctrl()(获得属性)和vidioc_s_ctrl()(设置属性),操作和前面差不多,都是通过usb_control_msg()函数建立控制消息,从而发送/接收亮度数据。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}
至此,对属性进行操作,比如对摄像头亮度控制就完成了,其它的属性控制类似。

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来记录一次完整传输的信息,包括每次传多少,传几次,传的目标位置等。

{% codeblock lang:c %}
psize = my_uvc_wMaxPacketSize; //实时传输端点一次能传输的最大字节数;lsusb: wMaxPacketSize 0x0320 1x800 bytes;
size = my_uvc_params.dwMaxVideoFrameSize; //一帧数据的最大长度
npackets = DIV_ROUND_UP(size, psize); //传多少次(向上取整)
{% endcodeblock %}

  • psize就是每次传输的数据大小,通过USB摄像头的设备描述符wMaxPacketSize(最大每包大小)可以得知。
  • size就是每帧图像的大小,前面在my_uvc_params已经设置过了,是通过在probe()的打印dwMaxPayloadTransferSize得知的;
  • npackets就是size/psize再向上取整,得到需要传多少次。
    最后还要size = psize * npackets更新一下向上取整后的新大小。

这个分配MY_UVC_URBS_NUM个(一个就行)urb_bufferurb
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数据结构的参考博客。

{% codeblock lang:c %}
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;

}
{% endcodeblock %}

现在我们就设置好了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传输的结果:
{% codeblock lang:c %}
switch (urb->status) {
case 0: //Success
break;
case -ETIMEDOUT: //Nak
case -ECONNRESET: //Kill
case -ENOENT:
case -ESHUTDOWN:
default: //Error
return;
}
{% endcodeblock %}
只有urb->status = 0才表示传输成功,否则都直接返回。

然后在判断my_uvc_q.irqqueue队列不为空的情况下,取出首个buf,后面将从URB得到的数据放在这个buf里:
{% codeblock lang:c %}
if (!list_empty(&my_uvc_q.irqqueue)) //判断是不是空队列
buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);//取出首buf用于后续存放数据
else
buf = NULL;
{% endcodeblock %}

之后便是对每个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,这样才能再次进入中断,拷贝数据,如此反复。

{% codeblock lang:c %}
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);
}

}
{% endcodeblock %}

3.6 启动/停止

应用层调用ioctl()传入的参数为VIDIOC_STREAMON时,就会调用vidioc_streamon()启动摄像头采集数据。
在该驱动函数里面,主要做三件事:

  1. 设置USB摄像头参数;(比如使用何种视频数据格式、何种分辨率)
  2. 分配设置URB;(调用前面的my_uvc_alloc_init_urbs()函数)
  3. 提交URB,等待中断;

一般的摄像头,会支持多种格式,比如MJPEG、H264等,也会支持多种分辨率。
因此需要在开始传输前,通过USB设置摄像头,让其后面返回正确的数据。

假如我们直接设置,可能摄像头不支持我们设置的格式,后面对应的解析数据可能会出现错误。因此我们先尝试传入设置参数,摄像头接收后会保存起来,并根据自身情况做一些修正,再将该设置读取出来,再进行真正的设置。
这里我们定义一个my_uvc_streaming_control结构体,用于保存这个设置过程中的参数。

首先是尝试设置参数,根据摄像头版本,对应的分配一个data空间,用于等会保存参数进行USB传输。
{% codeblock lang:c %}
//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;
{% endcodeblock %}

再清空传入的my_uvc_streaming_control结构体,设置相应参数,再参考内核UVC驱动使用cpu_to_le16()my_uvc_streaming_control赋值给data
{% codeblock lang:c %}
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);

{% endcodeblock %}

最后调用usb_control_msg()data传给摄像头,这里的usb_control_msg()在前面的亮度控制详细介绍了每个参数的含义,当时使用的是VC接口,这里使用VS接口。
这里不是真正的设置,所以传入的参数是VS_PROBE_CONTROL
{% codeblock lang:c %}
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);
{% endcodeblock %}


尝试设置了USB后,再把摄像头修正的参数读取出来保存到my_uvc_streaming_control结构体中。
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


最后再将新的参数设置给摄像头,这样就能保证现在设置的参数对摄像头是有效的。
这里是真正的设置,所以传入的参数是VS_COMMIT_CONTROL
{% codeblock lang:c %}
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;

}
{% endcodeblock %}


然后还要指定bAlternateSettingbAlternateSetting用于在同一个接口中的多个描述符中进行切换。
也就是说,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

{% codeblock lang:c %}
/* 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); 

{% endcodeblock %}

设置好了摄像头的format(格式)、frame(分辨率)等,就可以分配设置URB,准备和USB摄像头传输数据了。
{% codeblock lang:c %}
/* 2.分配设置URB */
ret = my_uvc_alloc_init_urbs();
if (0 != ret)
{
printk(“my_uvc_alloc_init_urbs err : ret = %d\n”, ret);
return ret;
}
{% endcodeblock %}

分配完成后,就提交给USB核心,等待中断来临,读取摄像头发来的数据。
{% codeblock lang:c %}
/* 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;
    }
}

{% endcodeblock %}


停止采集数据vidioc_streamoff()也需要做三件事:

  1. 取消URB传输;
  2. 释放urb_buffer和URB;
  3. 设置接口为0,让其处于休眠状态;

{% codeblock lang:c %}
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;

}
{% endcodeblock %}

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()查询缓存状态时,用于更新标志。
{% codeblock lang:c %}
//把缓存映射到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;

}
{% endcodeblock %}


最后是poll()函数,用来确定buf是否准备就绪,即含有数据。
应用层调用poll()时,会尝试从my_uvc_q.mainqueue队列取出首个缓冲区,得到其buf->wait,然后调用poll_wait()wait为标志,进入休眠。等待中断里的wake_up(),再唤醒。根据buf->state返回对应的mask,对应的应用程序就读取数据。
{% codeblock lang:c %}
//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;
}
{% endcodeblock %}

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又同时在两个队列上:mianququeirqquque
  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_buffersurb,设置urb
3.上报urb,USB核心解析urb,向指定接口(摄像头VS接口)接收数据(放在usb_buffers);
4.urb传输完成后产生中断,中断里取出irqquque队列首buf,将usb_buffers数据放入,并唤醒休眠的poll
5.poll唤醒,vidioc_dqbuf()mianquque队列取出首buf,返回给应用层,完成了摄像头数据到应用层的传输。

参考文章:
韦东山第三期项目视频_摄像头

猜你喜欢

转载自blog.csdn.net/hceng_linux/article/details/89874064