嵌入式Linux驅動筆記(十七)------詳解V4L2框架(UVC驅動)

風箏丶發表於2017-11-18

你好!這裡是風箏的部落格,

歡迎和我一起交流。


Video for Linux 2,簡稱V4l2,是Linux核心中關於視訊裝置的核心驅動框架,為上層的訪問底層的視訊裝置提供了統一的介面。凡是核心中的子系統都有抽象底層硬體的差異,為上層提供統一的介面和提取出公共程式碼避免程式碼冗餘等好處。

首先來看看所有的v4l2驅動都必須要有的幾個組成部分:
– 用來描述每一個v4l2裝置例項狀態的結構(struct v4l2_device)。
– 用來初始化和控制子裝置的方法(struct v4l2_subdev)。
– 要能建立裝置節點(/dev/videoX、/dev/vbiX 和 /dev/radioX)並且能夠對該節點所持有的資料進行跟蹤(structvideo_device)。
– 為每一個被開啟的節點維護一個檔案控制程式碼(structv4l2_fh)。
– 視訊緩衝區的處理(videobuf或者videobuf2 framework)。

用一個比較粗糙的圖來表現他們之間的關係,大致為:
裝置例項(v4l2_device)
|__子裝置例項(v4l2_subdev)
|__視訊裝置節點(video_device)
|__檔案訪問控制(v4l2_fh)
|__視訊緩衝的處理(videobuf/videobuf2)

許多驅動需要與子裝置通訊。這些裝置可以完成各種任務,但通常他們負責 音視訊複用和編解碼。如網路攝像頭的子裝置通常是感測器和攝像頭控制器。
這些一般為 I2C 介面裝置,但並不一定都是。為了給驅動提供呼叫子裝置的 統一介面,v4l2_subdev 結構體(v4l2-subdev.h)產生了。
在別的文章看到的圖,覺得還不錯,貼一下:
風箏
可以看出:
每個子裝置驅動都必須有一個 v4l2_subdev 結構體(實際的硬體裝置都被抽象為v4l2_subdev),代表一個簡單的子裝置,也可以嵌入到一個更大的結構體中,與更多裝置狀態 資訊儲存在一起。
v4l2_device在v4l2框架中充當所有v4l2_subdev的父裝置,管理著註冊在其下的子裝置。
因為子裝置千差萬別,所以v4l2-device又向上層提供一個標準的介面。所以可以認為v4l2-device就是一箇中間層。

在說v4l2之前,先說下uvc吧:
USB video class(又稱為USB video device class or UVC)就是USB device class視訊產品在不需要安裝任何的驅動程式下即插即用,包括攝像頭、數字攝影機、模擬視訊轉換器、電視卡及靜態視訊相機。

V4L2就是用來管理UVC裝置的並且能夠提供視訊相關的一些API

我們以Linux kernel 4.8.17為例,分析下實現過程:
drivers\media\usb\uvc\uvc_driver.c檔案:

struct uvc_driver uvc_driver = {
    .driver = {
        .name       = "uvcvideo",
        .probe      = uvc_probe,//支援的video裝置插入就會進入
        .disconnect = uvc_disconnect,
        .suspend    = uvc_suspend,
        .resume     = uvc_resume,
        .reset_resume   = uvc_reset_resume,
        .id_table   = uvc_ids,
        .supports_autosuspend = 1,
    },
};

當特定的usb裝置被插入時,就會觸發probe函式:

static int uvc_probe(struct usb_interface *intf,
             const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);
    struct uvc_device *dev;
    int ret;

    /*省略部分內容*/
    if ((dev = kzalloc(sizeof *dev, GFP_KERNEL)) == NULL)//【1】
        return -ENOMEM;
    /*省略部分內容*/
    dev->udev = usb_get_dev(udev);//【2】
    dev->intf = usb_get_intf(intf);
    dev->intfnum = intf->cur_altsetting->desc.bInterfaceNumber;
    dev->quirks = (uvc_quirks_param == -1)
            ? id->driver_info : uvc_quirks_param;
    /*省略部分內容*/
    /* Parse the Video Class control descriptor. */
    if (uvc_parse_control(dev) < 0) {//【3】
        uvc_trace(UVC_TRACE_PROBE, "Unable to parse UVC "
            "descriptors.\n");
        goto error;
    }
    /*省略部分內容*/
    if (v4l2_device_register(&intf->dev, &dev->vdev) < 0)//【4】
        goto error;
    /* Initialize controls. */
    if (uvc_ctrl_init_device(dev) < 0)//【5】
        goto error;
    /* Scan the device for video chains. */
    if (uvc_scan_device(dev) < 0)
        goto error;
    /* Register video device nodes. */
    if (uvc_register_chains(dev) < 0)//【6】
        goto error;
    /*省略部分內容*/
    /* Initialize the interrupt URB. */
    if ((ret = uvc_status_init(dev)) < 0) {//【7】uvc狀態的處理由中斷端點來控制處理
    /*省略部分內容*/
    return 0;
error:
    uvc_unregister_video(dev);
    return -ENODEV;
}

函式太長了,省略了部分內容,但是可以看出,主要的就是做幾件事情:
【1】分配一個dev
【2】給dev設定各種引數,如dev->udevudev
【3】呼叫uvc_parse_control函式分析裝置的控制描述符
【4】呼叫v4l2_device_register函式初始化v4l2_dev
【5】呼叫uvc_ctrl_init_device函式初始化uvc控制裝置
【6】呼叫uvc_register_chains函式註冊所有通道
【7】呼叫uvc_status_init函式初始化uvc狀態

我們來一個個分析下:
【3】:呼叫uvc_parse_control函式
看下呼叫關係:

uvc_parse_control(dev)
    uvc_parse_standard_control(dev, buffer, buflen)
        uvc_parse_streaming(dev, intf)      

跟蹤下uvc_parse_streaming函式:

static int uvc_parse_streaming(struct uvc_device *dev,
    struct usb_interface *intf)
{
    /*以下大部分內容省略,只顯示重要的*/
    struct uvc_streaming *streaming = NULL;
    struct uvc_format *format;
    struct uvc_frame *frame;

    streaming = kzalloc(sizeof *streaming, GFP_KERNEL);
    size = nformats * sizeof *format + nframes * sizeof *frame
         + nintervals * sizeof *interval;

    format = kzalloc(size, GFP_KERNEL);//申請format陣列存放格式
    streaming->format = format;//設定格式
    streaming->nformats = nformats;//最多支援nformats種格式
    ret = uvc_parse_format(dev, streaming, format,
                &interval, buffer, buflen);//分析格式
    list_add_tail(&streaming->list, &dev->streams);
    return 0;
}

這裡面申請了streaming和format記憶體
streaming是uvc_streaming 結構體,視訊流,很重要,大部分引數都是存在裡面。這函式裡申請了之後進行了很多設定,不過現在我省略了寫。
format記憶體存放的是視訊的格式,frame存放的是如解析度
這裡面都把他設定到了streaming裡面(streaming->format = format;streaming->nformats = nformats;)
最後呼叫uvc_parse_format函式分析格式:

static int uvc_parse_format()
{
    fmtdesc = uvc_format_by_guid(&buffer[5]);//通過GUID找到格式format
    /*裡面還會對frame進行各種分析和設定,
     *如設定format->nframes得出最多有多少種解析度選擇
     *暫時忽略*/
}

裡面uvc_format_by_guid函式會從uvc_fmts陣列中通過匹配guid找到格式:

static struct uvc_format_desc uvc_fmts[] = {
    {
        .name       = "YUV 4:2:2 (YUYV)",
        .guid       = UVC_GUID_FORMAT_YUY2,
        .fcc        = V4L2_PIX_FMT_YUYV,
    },
    {
        .name       = "YUV 4:2:2 (YUYV)",
        .guid       = UVC_GUID_FORMAT_YUY2_ISIGHT,
        .fcc        = V4L2_PIX_FMT_YUYV,
    },
    {
        .name       = "YUV 4:2:0 (NV12)",
        .guid       = UVC_GUID_FORMAT_NV12,
        .fcc        = V4L2_PIX_FMT_NV12,
    },
    {
        .name       = "MJPEG",
        .guid       = UVC_GUID_FORMAT_MJPEG,
        .fcc        = V4L2_PIX_FMT_MJPEG,
    },
    /*後面省略......*/
}

.
這樣【3】的工作就完成了,我們來看下【4】的:

int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
{
    INIT_LIST_HEAD(&v4l2_dev->subdevs);//用來管理v4l2_device 下的subdevs例項
    spin_lock_init(&v4l2_dev->lock);
    v4l2_prio_init(&v4l2_dev->prio);
    kref_init(&v4l2_dev->ref);
    get_device(dev);
    v4l2_dev->dev = dev;
    if (!v4l2_dev->name[0])
        snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s %s",
            dev->driver->name, dev_name(dev));
    if (!dev_get_drvdata(dev))//dev->driver_data 域 為 NULL
        dev_set_drvdata(dev, v4l2_dev);//就將其指向 v4l2_dev
    return 0;
}

簡單,沒啥好講的,就是初始化v4l2_dev->subdevs子裝置例項的連結串列,然後設定名字和設定dev->driver_data

看下【5】呼叫uvc_ctrl_init_device

int uvc_ctrl_init_device(struct uvc_device *dev)
{
    /*省略了部分內容*/
    list_for_each_entry(entity, &dev->entities, list) {
        bmControls = entity->extension.bmControls;//控制點陣圖
        bControlSize = entity->extension.bControlSize;//控制位域大小
        entity->controls = kcalloc(ncontrols, sizeof(*ctrl),
                       GFP_KERNEL);//分配ncontrols個uvc控制記憶體
        if (entity->controls == NULL)
            return -ENOMEM;
        entity->ncontrols = ncontrols;//設定uvc控制個數

        /* Initialize all supported controls */
        ctrl = entity->controls;//指向uvc控制陣列
        for (i = 0; i < bControlSize * 8; ++i) {
            if (uvc_test_bit(bmControls, i) == 0)//跳過控制位域設定0的
                continue;
            ctrl->entity = entity;
            ctrl->index = i;//設定控制位域索引
            uvc_ctrl_init_ctrl(dev, ctrl);//初始化uvc控制元件
            ctrl++;//uvc控制 指向下一個uvc控制陣列項
        }
    }
}

uvc_ctrl_init_device主要就是初始化控制引數,裡面就會遍歷uvc裝置實體entities連結串列,然後設定點陣圖和位域大小
最後還會呼叫uvc_ctrl_init_ctrl函式設定背光,色溫等等

接下來繼續看【6】呼叫uvc_register_chains函式:
呼叫關係:

uvc_register_chains
    uvc_register_terms(dev, chain)
        uvc_stream_by_id
        uvc_register_video
    uvc_mc_register_entities(chain)

uvc_stream_by_id函式會通過函式傳入的id和dev->streams連結串列的header.bTerminalLink匹配,尋找到stream
這不是重點,我們的重點是uvc_register_video函式,找到stream會就要註冊:

static int uvc_register_video(struct uvc_device *dev,
        struct uvc_streaming *stream)
{
    /*部分內容省略......*/
    struct video_device *vdev = &stream->vdev;

    ret = uvc_queue_init(&stream->queue, stream->type, !uvc_no_drop_param);//初始化佇列
    ret = uvc_video_init(stream);//初始化

    uvc_debugfs_init_stream(stream);
    vdev->v4l2_dev = &dev->vdev;
    vdev->fops = &uvc_fops;//v4l2操作函式集
    vdev->ioctl_ops = &uvc_ioctl_ops;//設定真正的ioctl操作集
    vdev->release = uvc_release;//釋放方法
    vdev->prio = &stream->chain->prio;

    strlcpy(vdev->name, dev->name, sizeof vdev->name);
    video_set_drvdata(vdev, stream);//將uvc視訊流作為v4l2裝置的驅動資料

    ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);//註冊
    return 0;
}

這是非常重要的函式,我們來一點一點分析:
看下uvc_queue_init函式,佇列初始化,佇列這東西,我們視訊傳輸時會呼叫到,在ioctl裡操作:

static struct vb2_ops uvc_queue_qops = {
    .queue_setup = uvc_queue_setup,
    .buf_prepare = uvc_buffer_prepare,
    .buf_queue = uvc_buffer_queue,
    .buf_finish = uvc_buffer_finish,
    .wait_prepare = vb2_ops_wait_prepare,
    .wait_finish = vb2_ops_wait_finish,
    .start_streaming = uvc_start_streaming,
    .stop_streaming = uvc_stop_streaming,
};
int uvc_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,
            int drop_corrupted)
{
    queue->queue.type = type;
    queue->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
    queue->queue.drv_priv = queue;
    queue->queue.buf_struct_size = sizeof(struct uvc_buffer);
    queue->queue.ops = &uvc_queue_qops;//stream->queue->queue.ops
    queue->queue.mem_ops = &vb2_vmalloc_memops;
    queue->queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
        | V4L2_BUF_FLAG_TSTAMP_SRC_SOE;
    queue->queue.lock = &queue->mutex;
    ret = vb2_queue_init(&queue->queue);//初始化queue

    mutex_init(&queue->mutex);
    spin_lock_init(&queue->irqlock);
    INIT_LIST_HEAD(&queue->irqqueue);//初始化stream->queue->irqqueue
    queue->flags = drop_corrupted ? UVC_QUEUE_DROP_CORRUPTED : 0;

    return 0;
}

裡面先對佇列進行初始化設定,如設定type和ops。
這裡queue->queue.ops = &uvc_queue_qops非常重要,之後我們呼叫vidioc_streamon回撥函式時就是呼叫到這裡的uvc_queue_qops結構體裡的.start_streaming函式
這函式裡對各種佇列進行了初始化:

vb2_queue_init(&queue->queue)
    q->buf_ops = &v4l2_buf_ops;
    vb2_core_queue_init(struct vb2_queue *q)
        INIT_LIST_HEAD(&q->queued_list);//stream->queue->queue->queued_list
        INIT_LIST_HEAD(&q->done_list);//stream->queue->done_list
INIT_LIST_HEAD(&queue->irqqueue);//初始化stream->queue->irqqueue

我們繼續看回uvc_register_video函式,裡面接著呼叫了uvc_video_init函式初始化UVC視訊裝置:

int uvc_video_init(struct uvc_streaming *stream)
{
    /*省略部分內容*/
    struct uvc_streaming_control *probe = &stream->ctrl;//獲取uvc資料流的uvs資料流控制物件

    if (uvc_get_video_ctrl(stream, probe, 1, UVC_GET_DEF) == 0)//先得到定義的控制引數
        uvc_set_video_ctrl(stream, probe, 1);//再設定uvc視訊控制
    ret = uvc_get_video_ctrl(stream, probe, 1, UVC_GET_CUR);//最後在get一次
    for (i = stream->nformats; i > 0; --i) {
        format = &stream->format[i-1];//獲取對應的uvc格式
        if (format->index == probe->bFormatIndex)
            break;
    }
    probe->bFormatIndex = format->index;//設定uvc視訊流控制的格式索引為uvc格式的索引
    probe->bFrameIndex = frame->bFrameIndex;//設定uvc視訊流控制的解析度索引為uvc解析度的索引

    stream->def_format = format;
    stream->cur_format = format;//設定uvc格式為uvc資料流的cur_format成員
    stream->cur_frame = frame;//設定uvc幀為uvc資料流的cur_frame成員

    if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {//視訊採集
        if (stream->dev->quirks & UVC_QUIRK_BUILTIN_ISIGHT)
            stream->decode = uvc_video_decode_isight;
        else if (stream->intf->num_altsetting > 1)
            stream->decode = uvc_video_decode_isoc;//同步方式
        else
            stream->decode = uvc_video_decode_bulk;//bluk方式
    } 
    return 0;
}

這裡面內容就比較多了,先得到,然後設定uvc的控制引數,裡面會操作urb發出usb資料。
然後通過probe->bFormatIndex索引找到使用的format格式和通過probe->bFrameIndex找到對應的frame解析度,然後設定到stream裡。
最後選擇解碼方式,如同步方式或者bluk方式,解碼方式會在資料完成時被回撥函式complete裡呼叫。

再次回到uvc_register_video函式,沒辦法,這個函式太重要了:
裡面繼續:

    vdev->fops = &uvc_fops;//v4l2操作函式集
    vdev->ioctl_ops = &uvc_ioctl_ops;//設定真正的ioctl操作集
    vdev->release = uvc_release;//釋放方法
    ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);

裡面就是vdev->v4l2_dev = &dev->vdev;這樣v4l2_device就與video_device關聯起來,也就是我們文章一開始那個圖看到的。
然後設定fops操作函式vdev->fops = &uvc_fops,雖然這不是給使用者空間使用的open、read、write函式,但是最後vdev->cdev->ops還是最呼叫到這個uvc_fops的,所以使用者空間實際上的pen、read、write函式還是會在這呼叫。 然後ioctl操作函式最終是會呼叫到vdev->ioctl_ops = &uvc_ioctl_ops。可以說,V4L2最重要的就是各種形式的ioctl了,這裡先不講,下一節在分析看看。
然後最終就是我們的註冊函式了:video_register_device裡呼叫到__video_register_device函式:

int __video_register_device(struct video_device *vdev, int type, int nr,
        int warn_if_nr_in_use, struct module *owner)
{
    /*省略部分函式*/
    vdev->minor = -1;//-1表明這個video device從未被註冊過
    switch (type) {//根據type選擇裝置名稱
    case VFL_TYPE_GRABBER:
        name_base = "video";
        break;
    case VFL_TYPE_VBI:
        name_base = "vbi";
        break;
    case VFL_TYPE_RADIO:
        name_base = "radio";
        break;
    case VFL_TYPE_SUBDEV:
        name_base = "v4l-subdev";
        break;
    case VFL_TYPE_SDR:
        name_base = "swradio";
        break;
    default:
        printk(KERN_ERR "%s called with unknown type: %d\n", __func__, type);
        return -EINVAL;
    }
    switch (type) {//選擇得到次裝置號偏移值
    case VFL_TYPE_GRABBER://用於視訊輸入/輸出裝置的 videoX
        minor_offset = 0;
        minor_cnt = 64;
        break;
    case VFL_TYPE_RADIO://用於廣播調諧器的 radioX
        minor_offset = 64;
        minor_cnt = 64;
        break;
    case VFL_TYPE_VBI://用於垂直消隱資料的 vbiX (例如,隱藏式字幕,圖文電視)
        minor_offset = 224;
        minor_cnt = 32;
        break;
    default:
        minor_offset = 128;
        minor_cnt = 64;
        break;
    }
    nr = devnode_find(vdev, 0, minor_cnt);//獲取一個沒有被使用的裝置節點序號
    for (i = 0; i < VIDEO_NUM_DEVICES; i++)
        if (video_device[i] == NULL)//從video_device[]陣列中選擇一個空缺項,這個空缺項的索引值放到i中
            break;
    vdev->minor = i + minor_offset;//裝置的次裝置號
    video_device[vdev->minor] = vdev;//注意:將設定好的video_device放入到video_device[]
    vdev->cdev->ops = &v4l2_fops;//操作使用者空間操作函式集
    ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);//新增字元裝置到系統
    ret = device_register(&vdev->dev);//裝置註冊
    set_bit(V4L2_FL_REGISTERED, &vdev->flags);//將flags第0為設定為1,表示這個video_device是註冊過的了

    return 0;

}

我們梳理一下里面做的事情:
1.確定裝置名稱,也就是我們在/dev/下生成的video啊,radio之類的
2.得到次裝置的偏移值
3.找到一個空的video_device陣列,把vdev存進去
4.設定vdev->cdev,這裡就設定了vdev->cdev->ops = &v4l2_fops;裡面就是真正的使用者空間操作集合
5.註冊video_device裝置
6.就是標誌此video_device以註冊

最後【6】呼叫uvc_register_chains函式裡還會呼叫一個uvc_mc_register_entities函式,裡面繼續呼叫uvc_mc_init_entity函式,這就是v4l2_device_register_subdev函式,進行註冊v4l2_subdev,同時初始化然後連線到v4l2_dev->subdevs管理。

好了,【6】呼叫uvc_register_chains函式:就分析完了,我們最後剩一個了:

【7】呼叫uvc_status_init函式

int uvc_status_init(struct uvc_device *dev)
{
    /*省略部分函式*/
    struct usb_host_endpoint *ep = dev->int_ep;//獲取usb_host_endpoint

    uvc_input_init(dev);//初始化uvc輸入裝置,裡面註冊input裝置
    dev->status = kzalloc(UVC_MAX_STATUS_SIZE, GFP_KERNEL);//分配urb裝置狀態記憶體
    dev->int_urb = usb_alloc_urb(0, GFP_KERNEL);//分配urb
    pipe = usb_rcvintpipe(dev->udev, ep->desc.bEndpointAddress);//中斷輸入端點
    usb_fill_int_urb(dev->int_urb, dev->udev, pipe,
        dev->status, UVC_MAX_STATUS_SIZE, uvc_status_complete,
        dev, interval);//填充中斷urb

    return 0;
}

裡面就是關於urb的一些東西了,看看就好。

最後,我們使用者空間怎麼才操作的?
看看__video_register_device函式裡的:vdev->cdev->ops = &v4l2_fops;

static const struct file_operations v4l2_fops = {
    .owner = THIS_MODULE,
    .read = v4l2_read,
    .write = v4l2_write,
    .open = v4l2_open,
    .get_unmapped_area = v4l2_get_unmapped_area,
    .mmap = v4l2_mmap,
    .unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = v4l2_compat_ioctl32,
#endif
    .release = v4l2_release,
    .poll = v4l2_poll,
    .llseek = no_llseek,
};
static int v4l2_open(struct inode *inode, struct file *filp)
{
    /*省略部分函式*/
    struct video_device *vdev;
    vdev = video_devdata(filp);//根據次裝置號從video_devices[]陣列中得到video_device
    if (vdev->fops->open) {
        if (video_is_registered(vdev))
            ret = vdev->fops->open(filp);//實際就是vdev->fops
        else
            ret = -ENODEV;
    }
}

記得我們之前把video_device放入到video_device[]嗎?就是這裡取了出來
然後呼叫vdev->fops->open(filp)
vdev->fops就是我們在uvc_register_video函式裡設定的:
vdev->fops = &uvc_fops

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
};

至於這個uvc_fops 裡的回撥函式,特別是ioctl,這是V4L2的重頭,就在下一章試著分析吧,我對這個也是比較模糊……

下一章:嵌入式Linux驅動筆記(十八)——淺析V4L2框架之ioctl

相關文章